From 1c09f5fa8c8e0a8b332caddddf3b99d55d99fd5b Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Tue, 19 Aug 2025 11:32:03 -0300 Subject: [PATCH 001/193] make mapReactTree support promise function components --- .../tailwind/src/utils/react/map-react-tree.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/tailwind/src/utils/react/map-react-tree.ts b/packages/tailwind/src/utils/react/map-react-tree.ts index 492dcac986..48999cc468 100644 --- a/packages/tailwind/src/utils/react/map-react-tree.ts +++ b/packages/tailwind/src/utils/react/map-react-tree.ts @@ -16,16 +16,16 @@ import { isComponent } from './is-component'; export function mapReactTree( value: React.ReactNode, process: (node: React.ReactNode) => React.ReactNode, -): React.ReactNode { - const mapped = React.Children.map(value, (node) => { +): Promise { + const mapped = React.Children.map(value, async (node) => { if (React.isValidElement<{ children?: React.ReactNode }>(node)) { const newProps = { ...node.props }; if (node.props.children && !isComponent(node)) { - newProps.children = mapReactTree(node.props.children, process); + newProps.children = await mapReactTree(node.props.children, process); } - const processed = process( + const processed = await process( React.cloneElement(node, newProps, newProps.children), ); @@ -41,8 +41,8 @@ export function mapReactTree( (processed.type.render as React.FC) : (processed.type as React.FC); - const rendered = OriginalComponent(processed.props); - const mappedRenderedNode = mapReactTree(rendered, process); + const rendered = await OriginalComponent(processed.props); + const mappedRenderedNode = await mapReactTree(rendered, process); return mappedRenderedNode; } @@ -53,5 +53,9 @@ export function mapReactTree( return process(node); }); - return mapped && mapped.length === 1 ? mapped[0] : mapped; + if (mapped) { + return mapped.length === 1 ? mapped[0] : Promise.all(mapped); + } + + return Promise.resolve(mapped); } From f76feb30cd1e64e4f556aab88a1c60ef0e7dcf29 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Tue, 19 Aug 2025 11:33:00 -0300 Subject: [PATCH 002/193] new standalone generateRootForClasses with tailwindcss v4's compile function --- .../generate-root-for-classes.spec.ts.snap | 176 ++++ .../__snapshots__/setup-tailwind.spec.ts.snap | 16 - .../clone-element-with-inlined-styles.ts | 10 +- .../generate-root-for-classes.spec.ts | 10 + .../tailwindcss/generate-root-for-classes.ts | 72 ++ .../tailwindcss/setup-tailwind-context.ts | 15 - .../utils/tailwindcss/setup-tailwind.spec.ts | 15 - .../src/utils/tailwindcss/setup-tailwind.ts | 61 -- .../utils/tailwindcss/tailwind-internals.d.ts | 129 --- .../tailwindcss/tailwind-stylesheets/index.ts | 900 ++++++++++++++++++ .../tailwind-stylesheets/preflight.ts | 397 ++++++++ .../tailwindcss/tailwind-stylesheets/theme.ts | 466 +++++++++ .../tailwind-stylesheets/utilities.ts | 5 + 13 files changed, 2032 insertions(+), 240 deletions(-) create mode 100644 packages/tailwind/src/utils/tailwindcss/__snapshots__/generate-root-for-classes.spec.ts.snap delete mode 100644 packages/tailwind/src/utils/tailwindcss/__snapshots__/setup-tailwind.spec.ts.snap create mode 100644 packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.spec.ts create mode 100644 packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.ts delete mode 100644 packages/tailwind/src/utils/tailwindcss/setup-tailwind-context.ts delete mode 100644 packages/tailwind/src/utils/tailwindcss/setup-tailwind.spec.ts delete mode 100644 packages/tailwind/src/utils/tailwindcss/setup-tailwind.ts delete mode 100644 packages/tailwind/src/utils/tailwindcss/tailwind-internals.d.ts create mode 100644 packages/tailwind/src/utils/tailwindcss/tailwind-stylesheets/index.ts create mode 100644 packages/tailwind/src/utils/tailwindcss/tailwind-stylesheets/preflight.ts create mode 100644 packages/tailwind/src/utils/tailwindcss/tailwind-stylesheets/theme.ts create mode 100644 packages/tailwind/src/utils/tailwindcss/tailwind-stylesheets/utilities.ts diff --git a/packages/tailwind/src/utils/tailwindcss/__snapshots__/generate-root-for-classes.spec.ts.snap b/packages/tailwind/src/utils/tailwindcss/__snapshots__/generate-root-for-classes.spec.ts.snap new file mode 100644 index 0000000000..d265af9741 --- /dev/null +++ b/packages/tailwind/src/utils/tailwindcss/__snapshots__/generate-root-for-classes.spec.ts.snap @@ -0,0 +1,176 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`tailwind's generateRootForClasses() 1`] = ` +"/*! tailwindcss v4.1.12 | MIT License | https://tailwindcss.com */ +@layer theme, base, components, utilities; +@layer theme { + .bg-slate-900{ + background-color: oklch(20.8% 0.042 265.755); + } + .text-red-500{ + color: oklch(63.7% 0.237 25.331); + } +} +@layer base { + *, ::after, ::before, ::backdrop, ::file-selector-button { + box-sizing: border-box; + margin: 0; + padding: 0; + border: 0 solid; + } + html, :host { + line-height: 1.5; + -webkit-text-size-adjust: 100%; + tab-size: 4; + font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"); + font-feature-settings: var(--default-font-feature-settings, normal); + font-variation-settings: var(--default-font-variation-settings, normal); + -webkit-tap-highlight-color: transparent; + } + hr { + height: 0; + color: inherit; + border-top-width: 1px; + } + abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + } + h1, h2, h3, h4, h5, h6 { + font-size: inherit; + font-weight: inherit; + } + a { + color: inherit; + -webkit-text-decoration: inherit; + text-decoration: inherit; + } + b, strong { + font-weight: bolder; + } + code, kbd, samp, pre { + font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace); + font-feature-settings: var(--default-mono-font-feature-settings, normal); + font-variation-settings: var(--default-mono-font-variation-settings, normal); + font-size: 1em; + } + small { + font-size: 80%; + } + sub, sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + } + sub { + bottom: -0.25em; + } + sup { + top: -0.5em; + } + table { + text-indent: 0; + border-color: inherit; + border-collapse: collapse; + } + :-moz-focusring { + outline: auto; + } + progress { + vertical-align: baseline; + } + summary { + display: list-item; + } + ol, ul, menu { + list-style: none; + } + img, svg, video, canvas, audio, iframe, embed, object { + display: block; + vertical-align: middle; + } + img, video { + max-width: 100%; + height: auto; + } + button, input, select, optgroup, textarea, ::file-selector-button { + font: inherit; + font-feature-settings: inherit; + font-variation-settings: inherit; + letter-spacing: inherit; + color: inherit; + border-radius: 0; + background-color: transparent; + opacity: 1; + } + :where(select:is([multiple], [size])) optgroup { + font-weight: bolder; + } + :where(select:is([multiple], [size])) optgroup option { + padding-inline-start: 20px; + } + ::file-selector-button { + margin-inline-end: 4px; + } + ::placeholder { + opacity: 1; + } + @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) { + ::placeholder { + color: currentcolor; + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, currentcolor 50%, transparent); + } + } + } + textarea { + resize: vertical; + } + ::-webkit-search-decoration { + -webkit-appearance: none; + } + ::-webkit-date-and-time-value { + min-height: 1lh; + text-align: inherit; + } + ::-webkit-datetime-edit { + display: inline-flex; + } + ::-webkit-datetime-edit-fields-wrapper { + padding: 0; + } + ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field { + padding-block: 0; + } + ::-webkit-calendar-picker-indicator { + line-height: 1; + } + :-moz-ui-invalid { + box-shadow: none; + } + button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button { + appearance: button; + } + ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { + height: auto; + } + [hidden]:where(:not([hidden="until-found"])) { + display: none !important; + } +} +@layer utilities { + .bg-slate-900 { + background-color: var(--color-slate-900); + } + .text-red-500 { + color: var(--color-red-500); + } + .sm\\:bg-blue-300 { + @media (width >= 40rem) { + background-color: var(--color-blue-300); + } + } +} +" +`; diff --git a/packages/tailwind/src/utils/tailwindcss/__snapshots__/setup-tailwind.spec.ts.snap b/packages/tailwind/src/utils/tailwindcss/__snapshots__/setup-tailwind.spec.ts.snap deleted file mode 100644 index bab4404dbb..0000000000 --- a/packages/tailwind/src/utils/tailwindcss/__snapshots__/setup-tailwind.spec.ts.snap +++ /dev/null @@ -1,16 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`tailwind's generateRootForClasses() 1`] = ` -".text-red-500 { - color: rgb(239 68 68 / 1) -} - @media (min-width: 640px) { - .sm\\:bg-blue-300 { - background-color: rgb(147 197 253 / 1) - } -} - .bg-slate-900 { - background-color: rgb(15 23 42 / 1) -} -" -`; diff --git a/packages/tailwind/src/utils/tailwindcss/clone-element-with-inlined-styles.ts b/packages/tailwind/src/utils/tailwindcss/clone-element-with-inlined-styles.ts index e73390abb8..7dae868663 100644 --- a/packages/tailwind/src/utils/tailwindcss/clone-element-with-inlined-styles.ts +++ b/packages/tailwind/src/utils/tailwindcss/clone-element-with-inlined-styles.ts @@ -1,16 +1,17 @@ import type { Node } from 'postcss'; import React from 'react'; +import type { Config } from 'tailwindcss'; import type { EmailElementProps } from '../../tailwind'; import { sanitizeClassName } from '../compatibility/sanitize-class-name'; import { makeInlineStylesFor } from '../css/make-inline-styles-for'; import { sanitizeDeclarations } from '../css/sanitize-declarations'; import { sanitizeNonInlinableClasses } from '../css/sanitize-non-inlinable-classes'; import { isComponent } from '../react/is-component'; -import type { setupTailwind } from './setup-tailwind'; +import { generateRootForClasses } from './generate-root-for-classes'; -export const cloneElementWithInlinedStyles = ( +export const cloneElementWithInlinedStyles = async ( element: React.ReactElement, - tailwind: ReturnType, + config: Config, ) => { const propsToOverwrite: Partial = {}; @@ -18,8 +19,9 @@ export const cloneElementWithInlinedStyles = ( let nonInlineStyleNodes: Node[] = []; if (element.props.className) { - const rootForClasses = tailwind.generateRootForClasses( + const rootForClasses = await generateRootForClasses( element.props.className.split(' '), + config, ); sanitizeDeclarations(rootForClasses); diff --git a/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.spec.ts b/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.spec.ts new file mode 100644 index 0000000000..0884c78750 --- /dev/null +++ b/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.spec.ts @@ -0,0 +1,10 @@ +import { generateRootForClasses } from './generate-root-for-classes'; + +test("tailwind's generateRootForClasses()", async () => { + expect( + (await generateRootForClasses( + ['text-red-500', 'sm:bg-blue-300', 'bg-slate-900'], + {}, + )).toString(), + ).toMatchSnapshot(); +}); diff --git a/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.ts b/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.ts new file mode 100644 index 0000000000..8665ee27b0 --- /dev/null +++ b/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.ts @@ -0,0 +1,72 @@ +import { parse } from 'postcss'; +import { type Config, compile } from 'tailwindcss'; +import { resolveAllCSSVariables } from '../css/resolve-all-css-variables'; +import indexCss from './tailwind-stylesheets/index'; +import preflightCss from './tailwind-stylesheets/preflight'; +import themeCss from './tailwind-stylesheets/theme'; +import utilitiesCss from './tailwind-stylesheets/utilities'; + +const baseCss = `@import "tailwindcss";`; + +export async function generateRootForClasses( + classes: string[], + config: Config, +) { + const compiler = await compile(baseCss, { + async loadModule(id, base, resourceHint) { + if (resourceHint === 'config') { + return { + path: id, + base: base, + module: config, + }; + } + + throw new Error( + `NO-OP: should we implement support for ${resourceHint}?`, + ); + }, + async loadStylesheet(id, base) { + if (id === 'tailwindcss') { + return { + base, + path: 'tailwindcss/index.css', + content: indexCss, + }; + } + + if (id === 'tailwindcss/preflight') { + return { + base, + path: 'tailwindcss/preflight.css', + content: preflightCss, + }; + } + + if (id === 'tailwindcss/theme') { + return { + base, + path: 'tailwindcss/theme.css', + content: themeCss, + }; + } + + if (id === 'tailwindcss/utilities') { + return { + base, + path: 'tailwindcss/utilities.css', + content: utilitiesCss, + }; + } + + throw new Error( + 'stylesheet not supported, you can only import tailwindcss', + ); + }, + }); + const css = compiler.build(classes); + const root = parse(css); + resolveAllCSSVariables(root); + + return root; +} diff --git a/packages/tailwind/src/utils/tailwindcss/setup-tailwind-context.ts b/packages/tailwind/src/utils/tailwindcss/setup-tailwind-context.ts deleted file mode 100644 index fa8e672fda..0000000000 --- a/packages/tailwind/src/utils/tailwindcss/setup-tailwind-context.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { createContext } from 'tailwindcss/lib/lib/setupContextUtils'; -import resolveConfig from 'tailwindcss/resolveConfig'; -import type { TailwindConfig } from '../../tailwind'; - -export const setupTailwindContext = (config: TailwindConfig) => { - return createContext( - resolveConfig({ - ...config, - content: [], - corePlugins: { - preflight: false, - }, - }), - ); -}; diff --git a/packages/tailwind/src/utils/tailwindcss/setup-tailwind.spec.ts b/packages/tailwind/src/utils/tailwindcss/setup-tailwind.spec.ts deleted file mode 100644 index 2e08dceee3..0000000000 --- a/packages/tailwind/src/utils/tailwindcss/setup-tailwind.spec.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { setupTailwind } from './setup-tailwind'; - -test("tailwind's generateRootForClasses()", () => { - const tailwind = setupTailwind({}); - - expect( - tailwind - .generateRootForClasses([ - 'text-red-500', - 'sm:bg-blue-300', - 'bg-slate-900', - ]) - .toString(), - ).toMatchSnapshot(); -}); diff --git a/packages/tailwind/src/utils/tailwindcss/setup-tailwind.ts b/packages/tailwind/src/utils/tailwindcss/setup-tailwind.ts deleted file mode 100644 index 55ac18e9d0..0000000000 --- a/packages/tailwind/src/utils/tailwindcss/setup-tailwind.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { Rule } from 'postcss'; -import { parse } from 'postcss'; -import collapseAdjacentRules from 'tailwindcss/lib/lib/collapseAdjacentRules'; -import collapseDuplicateDeclarations from 'tailwindcss/lib/lib/collapseDuplicateDeclarations'; -import evaluateTailwindFunctions from 'tailwindcss/lib/lib/evaluateTailwindFunctions'; -import expandApplyAtRules from 'tailwindcss/lib/lib/expandApplyAtRules'; -import expandTailwindAtRules from 'tailwindcss/lib/lib/expandTailwindAtRules'; -import { generateRules as rawGenerateRules } from 'tailwindcss/lib/lib/generateRules'; -import partitionApplyAtRules from 'tailwindcss/lib/lib/partitionApplyAtRules'; -import resolveDefaultsAtRules from 'tailwindcss/lib/lib/resolveDefaultsAtRules'; -import substituteScreenAtRules from 'tailwindcss/lib/lib/substituteScreenAtRules'; -import type { TailwindConfig } from '../../tailwind'; -import { resolveAllCSSVariables } from '../css/resolve-all-css-variables'; -import { setupTailwindContext } from './setup-tailwind-context'; - -const tailwindAtRulesRoot = parse( - ` - @tailwind base; - @tailwind components; -`, -).root(); - -export function setupTailwind(config: TailwindConfig) { - // See https://github.com/resend/react-email/issues/1907#issuecomment-2668720428 - if ('safelist' in config) { - console.warn( - 'The `safelist` option is not supported in the `Tailwind` component, it will not change any behavior.', - ); - delete config.safelist; - } - const tailwindContext = setupTailwindContext(config); - return { - generateRootForClasses: (classes: string[]) => { - tailwindContext.candidateRuleCache = new Map(); - const bigIntRuleTuples: [bigint, Rule][] = rawGenerateRules( - new Set(classes), - tailwindContext, - ); - - const root = tailwindAtRulesRoot - .clone() - .append(...bigIntRuleTuples.map(([, rule]) => rule)); - partitionApplyAtRules()(root); - // This is fine because the internal await is never actually called out - // because of there not being any `changedContent` files on the context - // eslint-disable-next-line @typescript-eslint/no-floating-promises - expandTailwindAtRules(tailwindContext)(root); - partitionApplyAtRules()(root); - expandApplyAtRules(tailwindContext)(root); - evaluateTailwindFunctions(tailwindContext)(root); - substituteScreenAtRules(tailwindContext)(root); - resolveDefaultsAtRules(tailwindContext)(root); - collapseAdjacentRules()(root); - collapseDuplicateDeclarations()(root); - - resolveAllCSSVariables(root); - - return root; - }, - }; -} diff --git a/packages/tailwind/src/utils/tailwindcss/tailwind-internals.d.ts b/packages/tailwind/src/utils/tailwindcss/tailwind-internals.d.ts deleted file mode 100644 index 6a1f7382d5..0000000000 --- a/packages/tailwind/src/utils/tailwindcss/tailwind-internals.d.ts +++ /dev/null @@ -1,129 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ - -declare module "tailwindcss/lib/lib/evaluateTailwindFunctions" { - import type { JitContext } from "tailwindcss/lib/lib/setupContextUtils"; - import type { Root } from "postcss"; - - export default function evaluateTailwindFunctions( - context: JITContext, - ): (root: Root) => void; -} - -declare module "tailwindcss/lib/lib/resolveDefaultsAtRules" { - import type { JitContext } from "tailwindcss/lib/lib/setupContextUtils"; - import type { Root } from "postcss"; - - export default function expandApplyAtRules( - context: JITContext, - ): (root: Root) => void; -} - -declare module "tailwindcss/lib/lib/partitionApplyAtRules" { - import type { Root } from "postcss"; - - export default function partitionApplyAtRules(): (root: Root) => void; -} - -declare module "tailwindcss/lib/lib/substituteScreenAtRules" { - import type { Root } from "postcss"; - - export default function substituteScreenAtRules( - context: JITContext, - ): (root: Root) => void; -} - -declare module "tailwindcss/lib/lib/resolveDefaultsAtRules" { - import type { JitContext } from "tailwindcss/lib/lib/setupContextUtils"; - import type { Root } from "postcss"; - - export default function expandApplyAtRules( - context: JITContext, - ): (root: Root) => void; -} - -declare module "tailwindcss/lib/lib/expandApplyAtRules" { - import type { JitContext } from "tailwindcss/lib/lib/setupContextUtils"; - import type { Root } from "postcss"; - - export default function expandApplyAtRules( - context: JITContext, - ): (root: Root) => void; -} - -declare module "tailwindcss/lib/lib/expandTailwindAtRules" { - import type { JitContext } from "tailwindcss/lib/lib/setupContextUtils"; - import type { Root } from "postcss"; - - export default async function expandTailwindAtRules( - context: JITContext, - ): (root: Root) => Promise; -} - -declare module "tailwindcss/lib/lib/collapseAdjacentRules" { - import type { JitContext } from "tailwindcss/lib/lib/setupContextUtils"; - import type { Root } from "postcss"; - - export default function collapseAdjacentRules(): (root: Root) => void; -} - -declare module "tailwindcss/lib/lib/collapseDuplicateDeclarations" { - import type { JitContext } from "tailwindcss/lib/lib/setupContextUtils"; - import type { Root } from "postcss"; - - export default function collapseDuplicateDeclarations(): (root: Root) => void; -} - -declare module "tailwindcss/lib/lib/generateRules" { - import type { JitContext } from "tailwindcss/lib/lib/setupContextUtils"; - import type { Rule } from "postcss"; - - export function generateRules( - classNames: Set, - context: JITContext, - ): [bigint, Rule][]; -} - -// taken from https://github.com/vinicoder/tw-to-css/blob/main/types.d.ts -// thanks vinicoder! -declare module "tailwindcss/lib/lib/setupContextUtils" { - import type { Container, Node } from "postcss"; - import type { Config } from "tailwindcss"; - import type resolveConfig from "tailwindcss/resolveConfig"; - - interface ChangedContent { - content: string; - extension?: string; - } - - interface Api { - container: Container; - separator: string; - format: (def: string) => void; - wrap: (rule: Container) => void; - } - - type VariantPreview = string; - - type VariantFn = [number, (api: Api) => VariantPreview | undefined]; - - type VariantName = string; - - export interface JitContext { - changedContent: ChangedContent[]; - ruleCache: Set; - candidateRuleCache: Map; - classCache: Map; - applyClassCache: Map; - notClassCache: Set; - postCssNodeCache: Map; - - getClassList: () => string[]; - tailwindConfig: TailwindConfig; - variantMap: Map; - } - - export function createContext( - config: ReturnType, - changedContent?: ChangedContent[], - ): JitContext; -} diff --git a/packages/tailwind/src/utils/tailwindcss/tailwind-stylesheets/index.ts b/packages/tailwind/src/utils/tailwindcss/tailwind-stylesheets/index.ts new file mode 100644 index 0000000000..dd40cee3d8 --- /dev/null +++ b/packages/tailwind/src/utils/tailwindcss/tailwind-stylesheets/index.ts @@ -0,0 +1,900 @@ +const css = ` +@layer theme, base, components, utilities; + +@layer theme { + @theme default { + --font-sans: + ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", + "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; + --font-mono: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", + "Courier New", monospace; + + --color-red-50: oklch(97.1% 0.013 17.38); + --color-red-100: oklch(93.6% 0.032 17.717); + --color-red-200: oklch(88.5% 0.062 18.334); + --color-red-300: oklch(80.8% 0.114 19.571); + --color-red-400: oklch(70.4% 0.191 22.216); + --color-red-500: oklch(63.7% 0.237 25.331); + --color-red-600: oklch(57.7% 0.245 27.325); + --color-red-700: oklch(50.5% 0.213 27.518); + --color-red-800: oklch(44.4% 0.177 26.899); + --color-red-900: oklch(39.6% 0.141 25.723); + --color-red-950: oklch(25.8% 0.092 26.042); + + --color-orange-50: oklch(98% 0.016 73.684); + --color-orange-100: oklch(95.4% 0.038 75.164); + --color-orange-200: oklch(90.1% 0.076 70.697); + --color-orange-300: oklch(83.7% 0.128 66.29); + --color-orange-400: oklch(75% 0.183 55.934); + --color-orange-500: oklch(70.5% 0.213 47.604); + --color-orange-600: oklch(64.6% 0.222 41.116); + --color-orange-700: oklch(55.3% 0.195 38.402); + --color-orange-800: oklch(47% 0.157 37.304); + --color-orange-900: oklch(40.8% 0.123 38.172); + --color-orange-950: oklch(26.6% 0.079 36.259); + + --color-amber-50: oklch(98.7% 0.022 95.277); + --color-amber-100: oklch(96.2% 0.059 95.617); + --color-amber-200: oklch(92.4% 0.12 95.746); + --color-amber-300: oklch(87.9% 0.169 91.605); + --color-amber-400: oklch(82.8% 0.189 84.429); + --color-amber-500: oklch(76.9% 0.188 70.08); + --color-amber-600: oklch(66.6% 0.179 58.318); + --color-amber-700: oklch(55.5% 0.163 48.998); + --color-amber-800: oklch(47.3% 0.137 46.201); + --color-amber-900: oklch(41.4% 0.112 45.904); + --color-amber-950: oklch(27.9% 0.077 45.635); + + --color-yellow-50: oklch(98.7% 0.026 102.212); + --color-yellow-100: oklch(97.3% 0.071 103.193); + --color-yellow-200: oklch(94.5% 0.129 101.54); + --color-yellow-300: oklch(90.5% 0.182 98.111); + --color-yellow-400: oklch(85.2% 0.199 91.936); + --color-yellow-500: oklch(79.5% 0.184 86.047); + --color-yellow-600: oklch(68.1% 0.162 75.834); + --color-yellow-700: oklch(55.4% 0.135 66.442); + --color-yellow-800: oklch(47.6% 0.114 61.907); + --color-yellow-900: oklch(42.1% 0.095 57.708); + --color-yellow-950: oklch(28.6% 0.066 53.813); + + --color-lime-50: oklch(98.6% 0.031 120.757); + --color-lime-100: oklch(96.7% 0.067 122.328); + --color-lime-200: oklch(93.8% 0.127 124.321); + --color-lime-300: oklch(89.7% 0.196 126.665); + --color-lime-400: oklch(84.1% 0.238 128.85); + --color-lime-500: oklch(76.8% 0.233 130.85); + --color-lime-600: oklch(64.8% 0.2 131.684); + --color-lime-700: oklch(53.2% 0.157 131.589); + --color-lime-800: oklch(45.3% 0.124 130.933); + --color-lime-900: oklch(40.5% 0.101 131.063); + --color-lime-950: oklch(27.4% 0.072 132.109); + + --color-green-50: oklch(98.2% 0.018 155.826); + --color-green-100: oklch(96.2% 0.044 156.743); + --color-green-200: oklch(92.5% 0.084 155.995); + --color-green-300: oklch(87.1% 0.15 154.449); + --color-green-400: oklch(79.2% 0.209 151.711); + --color-green-500: oklch(72.3% 0.219 149.579); + --color-green-600: oklch(62.7% 0.194 149.214); + --color-green-700: oklch(52.7% 0.154 150.069); + --color-green-800: oklch(44.8% 0.119 151.328); + --color-green-900: oklch(39.3% 0.095 152.535); + --color-green-950: oklch(26.6% 0.065 152.934); + + --color-emerald-50: oklch(97.9% 0.021 166.113); + --color-emerald-100: oklch(95% 0.052 163.051); + --color-emerald-200: oklch(90.5% 0.093 164.15); + --color-emerald-300: oklch(84.5% 0.143 164.978); + --color-emerald-400: oklch(76.5% 0.177 163.223); + --color-emerald-500: oklch(69.6% 0.17 162.48); + --color-emerald-600: oklch(59.6% 0.145 163.225); + --color-emerald-700: oklch(50.8% 0.118 165.612); + --color-emerald-800: oklch(43.2% 0.095 166.913); + --color-emerald-900: oklch(37.8% 0.077 168.94); + --color-emerald-950: oklch(26.2% 0.051 172.552); + + --color-teal-50: oklch(98.4% 0.014 180.72); + --color-teal-100: oklch(95.3% 0.051 180.801); + --color-teal-200: oklch(91% 0.096 180.426); + --color-teal-300: oklch(85.5% 0.138 181.071); + --color-teal-400: oklch(77.7% 0.152 181.912); + --color-teal-500: oklch(70.4% 0.14 182.503); + --color-teal-600: oklch(60% 0.118 184.704); + --color-teal-700: oklch(51.1% 0.096 186.391); + --color-teal-800: oklch(43.7% 0.078 188.216); + --color-teal-900: oklch(38.6% 0.063 188.416); + --color-teal-950: oklch(27.7% 0.046 192.524); + + --color-cyan-50: oklch(98.4% 0.019 200.873); + --color-cyan-100: oklch(95.6% 0.045 203.388); + --color-cyan-200: oklch(91.7% 0.08 205.041); + --color-cyan-300: oklch(86.5% 0.127 207.078); + --color-cyan-400: oklch(78.9% 0.154 211.53); + --color-cyan-500: oklch(71.5% 0.143 215.221); + --color-cyan-600: oklch(60.9% 0.126 221.723); + --color-cyan-700: oklch(52% 0.105 223.128); + --color-cyan-800: oklch(45% 0.085 224.283); + --color-cyan-900: oklch(39.8% 0.07 227.392); + --color-cyan-950: oklch(30.2% 0.056 229.695); + + --color-sky-50: oklch(97.7% 0.013 236.62); + --color-sky-100: oklch(95.1% 0.026 236.824); + --color-sky-200: oklch(90.1% 0.058 230.902); + --color-sky-300: oklch(82.8% 0.111 230.318); + --color-sky-400: oklch(74.6% 0.16 232.661); + --color-sky-500: oklch(68.5% 0.169 237.323); + --color-sky-600: oklch(58.8% 0.158 241.966); + --color-sky-700: oklch(50% 0.134 242.749); + --color-sky-800: oklch(44.3% 0.11 240.79); + --color-sky-900: oklch(39.1% 0.09 240.876); + --color-sky-950: oklch(29.3% 0.066 243.157); + + --color-blue-50: oklch(97% 0.014 254.604); + --color-blue-100: oklch(93.2% 0.032 255.585); + --color-blue-200: oklch(88.2% 0.059 254.128); + --color-blue-300: oklch(80.9% 0.105 251.813); + --color-blue-400: oklch(70.7% 0.165 254.624); + --color-blue-500: oklch(62.3% 0.214 259.815); + --color-blue-600: oklch(54.6% 0.245 262.881); + --color-blue-700: oklch(48.8% 0.243 264.376); + --color-blue-800: oklch(42.4% 0.199 265.638); + --color-blue-900: oklch(37.9% 0.146 265.522); + --color-blue-950: oklch(28.2% 0.091 267.935); + + --color-indigo-50: oklch(96.2% 0.018 272.314); + --color-indigo-100: oklch(93% 0.034 272.788); + --color-indigo-200: oklch(87% 0.065 274.039); + --color-indigo-300: oklch(78.5% 0.115 274.713); + --color-indigo-400: oklch(67.3% 0.182 276.935); + --color-indigo-500: oklch(58.5% 0.233 277.117); + --color-indigo-600: oklch(51.1% 0.262 276.966); + --color-indigo-700: oklch(45.7% 0.24 277.023); + --color-indigo-800: oklch(39.8% 0.195 277.366); + --color-indigo-900: oklch(35.9% 0.144 278.697); + --color-indigo-950: oklch(25.7% 0.09 281.288); + + --color-violet-50: oklch(96.9% 0.016 293.756); + --color-violet-100: oklch(94.3% 0.029 294.588); + --color-violet-200: oklch(89.4% 0.057 293.283); + --color-violet-300: oklch(81.1% 0.111 293.571); + --color-violet-400: oklch(70.2% 0.183 293.541); + --color-violet-500: oklch(60.6% 0.25 292.717); + --color-violet-600: oklch(54.1% 0.281 293.009); + --color-violet-700: oklch(49.1% 0.27 292.581); + --color-violet-800: oklch(43.2% 0.232 292.759); + --color-violet-900: oklch(38% 0.189 293.745); + --color-violet-950: oklch(28.3% 0.141 291.089); + + --color-purple-50: oklch(97.7% 0.014 308.299); + --color-purple-100: oklch(94.6% 0.033 307.174); + --color-purple-200: oklch(90.2% 0.063 306.703); + --color-purple-300: oklch(82.7% 0.119 306.383); + --color-purple-400: oklch(71.4% 0.203 305.504); + --color-purple-500: oklch(62.7% 0.265 303.9); + --color-purple-600: oklch(55.8% 0.288 302.321); + --color-purple-700: oklch(49.6% 0.265 301.924); + --color-purple-800: oklch(43.8% 0.218 303.724); + --color-purple-900: oklch(38.1% 0.176 304.987); + --color-purple-950: oklch(29.1% 0.149 302.717); + + --color-fuchsia-50: oklch(97.7% 0.017 320.058); + --color-fuchsia-100: oklch(95.2% 0.037 318.852); + --color-fuchsia-200: oklch(90.3% 0.076 319.62); + --color-fuchsia-300: oklch(83.3% 0.145 321.434); + --color-fuchsia-400: oklch(74% 0.238 322.16); + --color-fuchsia-500: oklch(66.7% 0.295 322.15); + --color-fuchsia-600: oklch(59.1% 0.293 322.896); + --color-fuchsia-700: oklch(51.8% 0.253 323.949); + --color-fuchsia-800: oklch(45.2% 0.211 324.591); + --color-fuchsia-900: oklch(40.1% 0.17 325.612); + --color-fuchsia-950: oklch(29.3% 0.136 325.661); + + --color-pink-50: oklch(97.1% 0.014 343.198); + --color-pink-100: oklch(94.8% 0.028 342.258); + --color-pink-200: oklch(89.9% 0.061 343.231); + --color-pink-300: oklch(82.3% 0.12 346.018); + --color-pink-400: oklch(71.8% 0.202 349.761); + --color-pink-500: oklch(65.6% 0.241 354.308); + --color-pink-600: oklch(59.2% 0.249 0.584); + --color-pink-700: oklch(52.5% 0.223 3.958); + --color-pink-800: oklch(45.9% 0.187 3.815); + --color-pink-900: oklch(40.8% 0.153 2.432); + --color-pink-950: oklch(28.4% 0.109 3.907); + + --color-rose-50: oklch(96.9% 0.015 12.422); + --color-rose-100: oklch(94.1% 0.03 12.58); + --color-rose-200: oklch(89.2% 0.058 10.001); + --color-rose-300: oklch(81% 0.117 11.638); + --color-rose-400: oklch(71.2% 0.194 13.428); + --color-rose-500: oklch(64.5% 0.246 16.439); + --color-rose-600: oklch(58.6% 0.253 17.585); + --color-rose-700: oklch(51.4% 0.222 16.935); + --color-rose-800: oklch(45.5% 0.188 13.697); + --color-rose-900: oklch(41% 0.159 10.272); + --color-rose-950: oklch(27.1% 0.105 12.094); + + --color-slate-50: oklch(98.4% 0.003 247.858); + --color-slate-100: oklch(96.8% 0.007 247.896); + --color-slate-200: oklch(92.9% 0.013 255.508); + --color-slate-300: oklch(86.9% 0.022 252.894); + --color-slate-400: oklch(70.4% 0.04 256.788); + --color-slate-500: oklch(55.4% 0.046 257.417); + --color-slate-600: oklch(44.6% 0.043 257.281); + --color-slate-700: oklch(37.2% 0.044 257.287); + --color-slate-800: oklch(27.9% 0.041 260.031); + --color-slate-900: oklch(20.8% 0.042 265.755); + --color-slate-950: oklch(12.9% 0.042 264.695); + + --color-gray-50: oklch(98.5% 0.002 247.839); + --color-gray-100: oklch(96.7% 0.003 264.542); + --color-gray-200: oklch(92.8% 0.006 264.531); + --color-gray-300: oklch(87.2% 0.01 258.338); + --color-gray-400: oklch(70.7% 0.022 261.325); + --color-gray-500: oklch(55.1% 0.027 264.364); + --color-gray-600: oklch(44.6% 0.03 256.802); + --color-gray-700: oklch(37.3% 0.034 259.733); + --color-gray-800: oklch(27.8% 0.033 256.848); + --color-gray-900: oklch(21% 0.034 264.665); + --color-gray-950: oklch(13% 0.028 261.692); + + --color-zinc-50: oklch(98.5% 0 0); + --color-zinc-100: oklch(96.7% 0.001 286.375); + --color-zinc-200: oklch(92% 0.004 286.32); + --color-zinc-300: oklch(87.1% 0.006 286.286); + --color-zinc-400: oklch(70.5% 0.015 286.067); + --color-zinc-500: oklch(55.2% 0.016 285.938); + --color-zinc-600: oklch(44.2% 0.017 285.786); + --color-zinc-700: oklch(37% 0.013 285.805); + --color-zinc-800: oklch(27.4% 0.006 286.033); + --color-zinc-900: oklch(21% 0.006 285.885); + --color-zinc-950: oklch(14.1% 0.005 285.823); + + --color-neutral-50: oklch(98.5% 0 0); + --color-neutral-100: oklch(97% 0 0); + --color-neutral-200: oklch(92.2% 0 0); + --color-neutral-300: oklch(87% 0 0); + --color-neutral-400: oklch(70.8% 0 0); + --color-neutral-500: oklch(55.6% 0 0); + --color-neutral-600: oklch(43.9% 0 0); + --color-neutral-700: oklch(37.1% 0 0); + --color-neutral-800: oklch(26.9% 0 0); + --color-neutral-900: oklch(20.5% 0 0); + --color-neutral-950: oklch(14.5% 0 0); + + --color-stone-50: oklch(98.5% 0.001 106.423); + --color-stone-100: oklch(97% 0.001 106.424); + --color-stone-200: oklch(92.3% 0.003 48.717); + --color-stone-300: oklch(86.9% 0.005 56.366); + --color-stone-400: oklch(70.9% 0.01 56.259); + --color-stone-500: oklch(55.3% 0.013 58.071); + --color-stone-600: oklch(44.4% 0.011 73.639); + --color-stone-700: oklch(37.4% 0.01 67.558); + --color-stone-800: oklch(26.8% 0.007 34.298); + --color-stone-900: oklch(21.6% 0.006 56.043); + --color-stone-950: oklch(14.7% 0.004 49.25); + + --color-black: #000; + --color-white: #fff; + + --spacing: 0.25rem; + + --breakpoint-sm: 40rem; + --breakpoint-md: 48rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; + + --container-3xs: 16rem; + --container-2xs: 18rem; + --container-xs: 20rem; + --container-sm: 24rem; + --container-md: 28rem; + --container-lg: 32rem; + --container-xl: 36rem; + --container-2xl: 42rem; + --container-3xl: 48rem; + --container-4xl: 56rem; + --container-5xl: 64rem; + --container-6xl: 72rem; + --container-7xl: 80rem; + + --text-xs: 0.75rem; + --text-xs--line-height: calc(1 / 0.75); + --text-sm: 0.875rem; + --text-sm--line-height: calc(1.25 / 0.875); + --text-base: 1rem; + --text-base--line-height: calc(1.5 / 1); + --text-lg: 1.125rem; + --text-lg--line-height: calc(1.75 / 1.125); + --text-xl: 1.25rem; + --text-xl--line-height: calc(1.75 / 1.25); + --text-2xl: 1.5rem; + --text-2xl--line-height: calc(2 / 1.5); + --text-3xl: 1.875rem; + --text-3xl--line-height: calc(2.25 / 1.875); + --text-4xl: 2.25rem; + --text-4xl--line-height: calc(2.5 / 2.25); + --text-5xl: 3rem; + --text-5xl--line-height: 1; + --text-6xl: 3.75rem; + --text-6xl--line-height: 1; + --text-7xl: 4.5rem; + --text-7xl--line-height: 1; + --text-8xl: 6rem; + --text-8xl--line-height: 1; + --text-9xl: 8rem; + --text-9xl--line-height: 1; + + --font-weight-thin: 100; + --font-weight-extralight: 200; + --font-weight-light: 300; + --font-weight-normal: 400; + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; + --font-weight-extrabold: 800; + --font-weight-black: 900; + + --tracking-tighter: -0.05em; + --tracking-tight: -0.025em; + --tracking-normal: 0em; + --tracking-wide: 0.025em; + --tracking-wider: 0.05em; + --tracking-widest: 0.1em; + + --leading-tight: 1.25; + --leading-snug: 1.375; + --leading-normal: 1.5; + --leading-relaxed: 1.625; + --leading-loose: 2; + + --radius-xs: 0.125rem; + --radius-sm: 0.25rem; + --radius-md: 0.375rem; + --radius-lg: 0.5rem; + --radius-xl: 0.75rem; + --radius-2xl: 1rem; + --radius-3xl: 1.5rem; + --radius-4xl: 2rem; + + --shadow-2xs: 0 1px rgb(0 0 0 / 0.05); + --shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --shadow-md: + 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --shadow-lg: + 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --shadow-xl: + 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); + --shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25); + + --inset-shadow-2xs: inset 0 1px rgb(0 0 0 / 0.05); + --inset-shadow-xs: inset 0 1px 1px rgb(0 0 0 / 0.05); + --inset-shadow-sm: inset 0 2px 4px rgb(0 0 0 / 0.05); + + --drop-shadow-xs: 0 1px 1px rgb(0 0 0 / 0.05); + --drop-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.15); + --drop-shadow-md: 0 3px 3px rgb(0 0 0 / 0.12); + --drop-shadow-lg: 0 4px 4px rgb(0 0 0 / 0.15); + --drop-shadow-xl: 0 9px 7px rgb(0 0 0 / 0.1); + --drop-shadow-2xl: 0 25px 25px rgb(0 0 0 / 0.15); + + --text-shadow-2xs: 0px 1px 0px rgb(0 0 0 / 0.15); + --text-shadow-xs: 0px 1px 1px rgb(0 0 0 / 0.2); + --text-shadow-sm: + 0px 1px 0px rgb(0 0 0 / 0.075), 0px 1px 1px rgb(0 0 0 / 0.075), + 0px 2px 2px rgb(0 0 0 / 0.075); + --text-shadow-md: + 0px 1px 1px rgb(0 0 0 / 0.1), 0px 1px 2px rgb(0 0 0 / 0.1), + 0px 2px 4px rgb(0 0 0 / 0.1); + --text-shadow-lg: + 0px 1px 2px rgb(0 0 0 / 0.1), 0px 3px 2px rgb(0 0 0 / 0.1), + 0px 4px 8px rgb(0 0 0 / 0.1); + + --ease-in: cubic-bezier(0.4, 0, 1, 1); + --ease-out: cubic-bezier(0, 0, 0.2, 1); + --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); + + --animate-spin: spin 1s linear infinite; + --animate-ping: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite; + --animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; + --animate-bounce: bounce 1s infinite; + + @keyframes spin { + to { + transform: rotate(360deg); + } + } + + @keyframes ping { + 75%, + 100% { + transform: scale(2); + opacity: 0; + } + } + + @keyframes pulse { + 50% { + opacity: 0.5; + } + } + + @keyframes bounce { + 0%, + 100% { + transform: translateY(-25%); + animation-timing-function: cubic-bezier(0.8, 0, 1, 1); + } + + 50% { + transform: none; + animation-timing-function: cubic-bezier(0, 0, 0.2, 1); + } + } + + --blur-xs: 4px; + --blur-sm: 8px; + --blur-md: 12px; + --blur-lg: 16px; + --blur-xl: 24px; + --blur-2xl: 40px; + --blur-3xl: 64px; + + --perspective-dramatic: 100px; + --perspective-near: 300px; + --perspective-normal: 500px; + --perspective-midrange: 800px; + --perspective-distant: 1200px; + + --aspect-video: 16 / 9; + + --default-transition-duration: 150ms; + --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + --default-font-family: --theme(--font-sans, initial); + --default-font-feature-settings: --theme( + --font-sans--font-feature-settings, + initial + ); + --default-font-variation-settings: --theme( + --font-sans--font-variation-settings, + initial + ); + --default-mono-font-family: --theme(--font-mono, initial); + --default-mono-font-feature-settings: --theme( + --font-mono--font-feature-settings, + initial + ); + --default-mono-font-variation-settings: --theme( + --font-mono--font-variation-settings, + initial + ); + } + + /* Deprecated */ + @theme default inline reference { + --blur: 8px; + --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --shadow-inner: inset 0 2px 4px 0 rgb(0 0 0 / 0.05); + --drop-shadow: 0 1px 2px rgb(0 0 0 / 0.1), 0 1px 1px rgb(0 0 0 / 0.06); + --radius: 0.25rem; + --max-width-prose: 65ch; + } +} + +@layer base { + /* + 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) + 2. Remove default margins and padding + 3. Reset all borders. +*/ + + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + box-sizing: border-box; /* 1 */ + margin: 0; /* 2 */ + padding: 0; /* 2 */ + border: 0 solid; /* 3 */ + } + + /* + 1. Use a consistent sensible line-height in all browsers. + 2. Prevent adjustments of font size after orientation changes in iOS. + 3. Use a more readable tab size. + 4. Use the user's configured \`sans\` font-family by default. + 5. Use the user's configured \`sans\` font-feature-settings by default. + 6. Use the user's configured \`sans\` font-variation-settings by default. + 7. Disable tap highlights on iOS. +*/ + + html, + :host { + line-height: 1.5; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ + tab-size: 4; /* 3 */ + font-family: --theme( + --default-font-family, + ui-sans-serif, + system-ui, + sans-serif, + "Apple Color Emoji", + "Segoe UI Emoji", + "Segoe UI Symbol", + "Noto Color Emoji" + ); /* 4 */ + font-feature-settings: --theme( + --default-font-feature-settings, + normal + ); /* 5 */ + font-variation-settings: --theme( + --default-font-variation-settings, + normal + ); /* 6 */ + -webkit-tap-highlight-color: transparent; /* 7 */ + } + + /* + 1. Add the correct height in Firefox. + 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) + 3. Reset the default border style to a 1px solid border. +*/ + + hr { + height: 0; /* 1 */ + color: inherit; /* 2 */ + border-top-width: 1px; /* 3 */ + } + + /* + Add the correct text decoration in Chrome, Edge, and Safari. +*/ + + abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + } + + /* + Remove the default font size and weight for headings. +*/ + + h1, + h2, + h3, + h4, + h5, + h6 { + font-size: inherit; + font-weight: inherit; + } + + /* + Reset links to optimize for opt-in styling instead of opt-out. +*/ + + a { + color: inherit; + -webkit-text-decoration: inherit; + text-decoration: inherit; + } + + /* + Add the correct font weight in Edge and Safari. +*/ + + b, + strong { + font-weight: bolder; + } + + /* + 1. Use the user's configured \`mono\` font-family by default. + 2. Use the user's configured \`mono\` font-feature-settings by default. + 3. Use the user's configured \`mono\` font-variation-settings by default. + 4. Correct the odd \`em\` font sizing in all browsers. +*/ + + code, + kbd, + samp, + pre { + font-family: --theme( + --default-mono-font-family, + ui-monospace, + SFMono-Regular, + Menlo, + Monaco, + Consolas, + "Liberation Mono", + "Courier New", + monospace + ); /* 1 */ + font-feature-settings: --theme( + --default-mono-font-feature-settings, + normal + ); /* 2 */ + font-variation-settings: --theme( + --default-mono-font-variation-settings, + normal + ); /* 3 */ + font-size: 1em; /* 4 */ + } + + /* + Add the correct font size in all browsers. +*/ + + small { + font-size: 80%; + } + + /* + Prevent \`sub\` and \`sup\` elements from affecting the line height in all browsers. +*/ + + sub, + sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + } + + sub { + bottom: -0.25em; + } + + sup { + top: -0.5em; + } + + /* + 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) + 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) + 3. Remove gaps between table borders by default. +*/ + + table { + text-indent: 0; /* 1 */ + border-color: inherit; /* 2 */ + border-collapse: collapse; /* 3 */ + } + + /* + Use the modern Firefox focus style for all focusable elements. +*/ + + :-moz-focusring { + outline: auto; + } + + /* + Add the correct vertical alignment in Chrome and Firefox. +*/ + + progress { + vertical-align: baseline; + } + + /* + Add the correct display in Chrome and Safari. +*/ + + summary { + display: list-item; + } + + /* + Make lists unstyled by default. +*/ + + ol, + ul, + menu { + list-style: none; + } + + /* + 1. Make replaced elements \`display: block\` by default. (https://github.com/mozdevs/cssremedy/issues/14) + 2. Add \`vertical - align: middle\` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + + img, + svg, + video, + canvas, + audio, + iframe, + embed, + object { + display: block; /* 1 */ + vertical-align: middle; /* 2 */ + } + + /* + Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + + img, + video { + max-width: 100%; + height: auto; + } + + /* + 1. Inherit font styles in all browsers. + 2. Remove border radius in all browsers. + 3. Remove background color in all browsers. + 4. Ensure consistent opacity for disabled states in all browsers. +*/ + + button, + input, + select, + optgroup, + textarea, + ::file-selector-button { + font: inherit; /* 1 */ + font-feature-settings: inherit; /* 1 */ + font-variation-settings: inherit; /* 1 */ + letter-spacing: inherit; /* 1 */ + color: inherit; /* 1 */ + border-radius: 0; /* 2 */ + background-color: transparent; /* 3 */ + opacity: 1; /* 4 */ + } + + /* + Restore default font weight. +*/ + + :where(select:is([multiple], [size])) optgroup { + font-weight: bolder; + } + + /* + Restore indentation. +*/ + + :where(select:is([multiple], [size])) optgroup option { + padding-inline-start: 20px; + } + + /* + Restore space after button. +*/ + + ::file-selector-button { + margin-inline-end: 4px; + } + + /* + Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +*/ + + ::placeholder { + opacity: 1; + } + + /* + Set the default placeholder color to a semi-transparent version of the current text color in browsers that do not + crash when using \`color - mix(…)\` with \`currentcolor\`. (https://github.com/tailwindlabs/tailwindcss/issues/17194) +*/ + + @supports (not (-webkit-appearance: -apple-pay-button)) /* Not Safari */ or + (contain-intrinsic-size: 1px) /* Safari 17+ */ { + ::placeholder { + color: color-mix(in oklab, currentcolor 50%, transparent); + } + } + + /* + Prevent resizing textareas horizontally by default. +*/ + + textarea { + resize: vertical; + } + + /* + Remove the inner padding in Chrome and Safari on macOS. +*/ + + ::-webkit-search-decoration { + -webkit-appearance: none; + } + + /* + 1. Ensure date/time inputs have the same height when empty in iOS Safari. + 2. Ensure text alignment can be changed on date/time inputs in iOS Safari. +*/ + + ::-webkit-date-and-time-value { + min-height: 1lh; /* 1 */ + text-align: inherit; /* 2 */ + } + + /* + Prevent height from changing on date/time inputs in macOS Safari when the input is set to \`display: block\`. +*/ + + ::-webkit-datetime-edit { + display: inline-flex; + } + + /* + Remove excess padding from pseudo-elements in date/time inputs to ensure consistent height across browsers. +*/ + + ::-webkit-datetime-edit-fields-wrapper { + padding: 0; + } + + ::-webkit-datetime-edit, + ::-webkit-datetime-edit-year-field, + ::-webkit-datetime-edit-month-field, + ::-webkit-datetime-edit-day-field, + ::-webkit-datetime-edit-hour-field, + ::-webkit-datetime-edit-minute-field, + ::-webkit-datetime-edit-second-field, + ::-webkit-datetime-edit-millisecond-field, + ::-webkit-datetime-edit-meridiem-field { + padding-block: 0; + } + + /* + Center dropdown marker shown on inputs with paired \`\`s in Chrome. (https://github.com/tailwindlabs/tailwindcss/issues/18499) +*/ + + ::-webkit-calendar-picker-indicator { + line-height: 1; + } + + /* + Remove the additional \`: invalid\` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + + :-moz-ui-invalid { + box-shadow: none; + } + + /* + Correct the inability to style the border radius in iOS Safari. +*/ + + button, + input:where([type="button"], [type="reset"], [type="submit"]), + ::file-selector-button { + appearance: button; + } + + /* + Correct the cursor style of increment and decrement buttons in Safari. +*/ + + ::-webkit-inner-spin-button, + ::-webkit-outer-spin-button { + height: auto; + } + + /* + Make elements with the HTML hidden attribute stay hidden by default. +*/ + + [hidden]:where(:not([hidden="until-found"])) { + display: none !important; + } +} + +@layer utilities { + @tailwind utilities; +} +`; + +export default css; diff --git a/packages/tailwind/src/utils/tailwindcss/tailwind-stylesheets/preflight.ts b/packages/tailwind/src/utils/tailwindcss/tailwind-stylesheets/preflight.ts new file mode 100644 index 0000000000..4f8bf18507 --- /dev/null +++ b/packages/tailwind/src/utils/tailwindcss/tailwind-stylesheets/preflight.ts @@ -0,0 +1,397 @@ +const css = ` +/* + 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) + 2. Remove default margins and padding + 3. Reset all borders. +*/ + +*, +::after, +::before, +::backdrop, +::file-selector-button { + box-sizing: border-box; /* 1 */ + margin: 0; /* 2 */ + padding: 0; /* 2 */ + border: 0 solid; /* 3 */ +} + +/* + 1. Use a consistent sensible line-height in all browsers. + 2. Prevent adjustments of font size after orientation changes in iOS. + 3. Use a more readable tab size. + 4. Use the user's configured \`sans\` font-family by default. + 5. Use the user's configured \`sans\` font-feature-settings by default. + 6. Use the user's configured \`sans\` font-variation-settings by default. + 7. Disable tap highlights on iOS. +*/ + +html, +:host { + line-height: 1.5; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ + tab-size: 4; /* 3 */ + font-family: --theme( + --default-font-family, + ui-sans-serif, + system-ui, + sans-serif, + 'Apple Color Emoji', + 'Segoe UI Emoji', + 'Segoe UI Symbol', + 'Noto Color Emoji' + ); /* 4 */ + font-feature-settings: --theme(--default-font-feature-settings, normal); /* 5 */ + font-variation-settings: --theme(--default-font-variation-settings, normal); /* 6 */ + -webkit-tap-highlight-color: transparent; /* 7 */ +} + +/* + 1. Add the correct height in Firefox. + 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) + 3. Reset the default border style to a 1px solid border. +*/ + +hr { + height: 0; /* 1 */ + color: inherit; /* 2 */ + border-top-width: 1px; /* 3 */ +} + +/* + Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* + Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* + Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + -webkit-text-decoration: inherit; + text-decoration: inherit; +} + +/* + Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* + 1. Use the user's configured \`mono\` font-family by default. + 2. Use the user's configured \`mono\` font-feature-settings by default. + 3. Use the user's configured \`mono\` font-variation-settings by default. + 4. Correct the odd \`em\` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: --theme( + --default-mono-font-family, + ui-monospace, + SFMono-Regular, + Menlo, + Monaco, + Consolas, + 'Liberation Mono', + 'Courier New', + monospace + ); /* 1 */ + font-feature-settings: --theme(--default-mono-font-feature-settings, normal); /* 2 */ + font-variation-settings: --theme(--default-mono-font-variation-settings, normal); /* 3 */ + font-size: 1em; /* 4 */ +} + +/* + Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* + Prevent \`sub\` and \`sup\` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* + 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) + 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) + 3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; /* 1 */ + border-color: inherit; /* 2 */ + border-collapse: collapse; /* 3 */ +} + +/* + Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* + Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* + Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* + Make lists unstyled by default. +*/ + +ol, +ul, +menu { + list-style: none; +} + +/* + 1. Make replaced elements \`display: block\` by default. (https://github.com/mozdevs/cssremedy/issues/14) + 2. Add \`vertical-align: middle\` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; /* 1 */ + vertical-align: middle; /* 2 */ +} + +/* + Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* + 1. Inherit font styles in all browsers. + 2. Remove border radius in all browsers. + 3. Remove background color in all browsers. + 4. Ensure consistent opacity for disabled states in all browsers. +*/ + +button, +input, +select, +optgroup, +textarea, +::file-selector-button { + font: inherit; /* 1 */ + font-feature-settings: inherit; /* 1 */ + font-variation-settings: inherit; /* 1 */ + letter-spacing: inherit; /* 1 */ + color: inherit; /* 1 */ + border-radius: 0; /* 2 */ + background-color: transparent; /* 3 */ + opacity: 1; /* 4 */ +} + +/* + Restore default font weight. +*/ + +:where(select:is([multiple], [size])) optgroup { + font-weight: bolder; +} + +/* + Restore indentation. +*/ + +:where(select:is([multiple], [size])) optgroup option { + padding-inline-start: 20px; +} + +/* + Restore space after button. +*/ + +::file-selector-button { + margin-inline-end: 4px; +} + +/* + Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +*/ + +::placeholder { + opacity: 1; +} + +/* + Set the default placeholder color to a semi-transparent version of the current text color in browsers that do not + crash when using \`color-mix(…)\` with \`currentcolor\`. (https://github.com/tailwindlabs/tailwindcss/issues/17194) +*/ + +@supports (not (-webkit-appearance: -apple-pay-button)) /* Not Safari */ or + (contain-intrinsic-size: 1px) /* Safari 17+ */ { + ::placeholder { + color: color-mix(in oklab, currentcolor 50%, transparent); + } +} + +/* + Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* + Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* + 1. Ensure date/time inputs have the same height when empty in iOS Safari. + 2. Ensure text alignment can be changed on date/time inputs in iOS Safari. +*/ + +::-webkit-date-and-time-value { + min-height: 1lh; /* 1 */ + text-align: inherit; /* 2 */ +} + +/* + Prevent height from changing on date/time inputs in macOS Safari when the input is set to \`display: block\`. +*/ + +::-webkit-datetime-edit { + display: inline-flex; +} + +/* + Remove excess padding from pseudo-elements in date/time inputs to ensure consistent height across browsers. +*/ + +::-webkit-datetime-edit-fields-wrapper { + padding: 0; +} + +::-webkit-datetime-edit, +::-webkit-datetime-edit-year-field, +::-webkit-datetime-edit-month-field, +::-webkit-datetime-edit-day-field, +::-webkit-datetime-edit-hour-field, +::-webkit-datetime-edit-minute-field, +::-webkit-datetime-edit-second-field, +::-webkit-datetime-edit-millisecond-field, +::-webkit-datetime-edit-meridiem-field { + padding-block: 0; +} + +/* + Center dropdown marker shown on inputs with paired \`\`s in Chrome. (https://github.com/tailwindlabs/tailwindcss/issues/18499) +*/ + +::-webkit-calendar-picker-indicator { + line-height: 1; +} + +/* + Remove the additional \`:invalid\` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* + Correct the inability to style the border radius in iOS Safari. +*/ + +button, +input:where([type='button'], [type='reset'], [type='submit']), +::file-selector-button { + appearance: button; +} + +/* + Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* + Make elements with the HTML hidden attribute stay hidden by default. +*/ + +[hidden]:where(:not([hidden='until-found'])) { + display: none !important; +} +`; + +export default css; diff --git a/packages/tailwind/src/utils/tailwindcss/tailwind-stylesheets/theme.ts b/packages/tailwind/src/utils/tailwindcss/tailwind-stylesheets/theme.ts new file mode 100644 index 0000000000..f67d580c5f --- /dev/null +++ b/packages/tailwind/src/utils/tailwindcss/tailwind-stylesheets/theme.ts @@ -0,0 +1,466 @@ +const css = ` +@theme default { + --font-sans: + ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', + 'Noto Color Emoji'; + --font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif; + --font-mono: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', + monospace; + + --color-red-50: oklch(97.1% 0.013 17.38); + --color-red-100: oklch(93.6% 0.032 17.717); + --color-red-200: oklch(88.5% 0.062 18.334); + --color-red-300: oklch(80.8% 0.114 19.571); + --color-red-400: oklch(70.4% 0.191 22.216); + --color-red-500: oklch(63.7% 0.237 25.331); + --color-red-600: oklch(57.7% 0.245 27.325); + --color-red-700: oklch(50.5% 0.213 27.518); + --color-red-800: oklch(44.4% 0.177 26.899); + --color-red-900: oklch(39.6% 0.141 25.723); + --color-red-950: oklch(25.8% 0.092 26.042); + + --color-orange-50: oklch(98% 0.016 73.684); + --color-orange-100: oklch(95.4% 0.038 75.164); + --color-orange-200: oklch(90.1% 0.076 70.697); + --color-orange-300: oklch(83.7% 0.128 66.29); + --color-orange-400: oklch(75% 0.183 55.934); + --color-orange-500: oklch(70.5% 0.213 47.604); + --color-orange-600: oklch(64.6% 0.222 41.116); + --color-orange-700: oklch(55.3% 0.195 38.402); + --color-orange-800: oklch(47% 0.157 37.304); + --color-orange-900: oklch(40.8% 0.123 38.172); + --color-orange-950: oklch(26.6% 0.079 36.259); + + --color-amber-50: oklch(98.7% 0.022 95.277); + --color-amber-100: oklch(96.2% 0.059 95.617); + --color-amber-200: oklch(92.4% 0.12 95.746); + --color-amber-300: oklch(87.9% 0.169 91.605); + --color-amber-400: oklch(82.8% 0.189 84.429); + --color-amber-500: oklch(76.9% 0.188 70.08); + --color-amber-600: oklch(66.6% 0.179 58.318); + --color-amber-700: oklch(55.5% 0.163 48.998); + --color-amber-800: oklch(47.3% 0.137 46.201); + --color-amber-900: oklch(41.4% 0.112 45.904); + --color-amber-950: oklch(27.9% 0.077 45.635); + + --color-yellow-50: oklch(98.7% 0.026 102.212); + --color-yellow-100: oklch(97.3% 0.071 103.193); + --color-yellow-200: oklch(94.5% 0.129 101.54); + --color-yellow-300: oklch(90.5% 0.182 98.111); + --color-yellow-400: oklch(85.2% 0.199 91.936); + --color-yellow-500: oklch(79.5% 0.184 86.047); + --color-yellow-600: oklch(68.1% 0.162 75.834); + --color-yellow-700: oklch(55.4% 0.135 66.442); + --color-yellow-800: oklch(47.6% 0.114 61.907); + --color-yellow-900: oklch(42.1% 0.095 57.708); + --color-yellow-950: oklch(28.6% 0.066 53.813); + + --color-lime-50: oklch(98.6% 0.031 120.757); + --color-lime-100: oklch(96.7% 0.067 122.328); + --color-lime-200: oklch(93.8% 0.127 124.321); + --color-lime-300: oklch(89.7% 0.196 126.665); + --color-lime-400: oklch(84.1% 0.238 128.85); + --color-lime-500: oklch(76.8% 0.233 130.85); + --color-lime-600: oklch(64.8% 0.2 131.684); + --color-lime-700: oklch(53.2% 0.157 131.589); + --color-lime-800: oklch(45.3% 0.124 130.933); + --color-lime-900: oklch(40.5% 0.101 131.063); + --color-lime-950: oklch(27.4% 0.072 132.109); + + --color-green-50: oklch(98.2% 0.018 155.826); + --color-green-100: oklch(96.2% 0.044 156.743); + --color-green-200: oklch(92.5% 0.084 155.995); + --color-green-300: oklch(87.1% 0.15 154.449); + --color-green-400: oklch(79.2% 0.209 151.711); + --color-green-500: oklch(72.3% 0.219 149.579); + --color-green-600: oklch(62.7% 0.194 149.214); + --color-green-700: oklch(52.7% 0.154 150.069); + --color-green-800: oklch(44.8% 0.119 151.328); + --color-green-900: oklch(39.3% 0.095 152.535); + --color-green-950: oklch(26.6% 0.065 152.934); + + --color-emerald-50: oklch(97.9% 0.021 166.113); + --color-emerald-100: oklch(95% 0.052 163.051); + --color-emerald-200: oklch(90.5% 0.093 164.15); + --color-emerald-300: oklch(84.5% 0.143 164.978); + --color-emerald-400: oklch(76.5% 0.177 163.223); + --color-emerald-500: oklch(69.6% 0.17 162.48); + --color-emerald-600: oklch(59.6% 0.145 163.225); + --color-emerald-700: oklch(50.8% 0.118 165.612); + --color-emerald-800: oklch(43.2% 0.095 166.913); + --color-emerald-900: oklch(37.8% 0.077 168.94); + --color-emerald-950: oklch(26.2% 0.051 172.552); + + --color-teal-50: oklch(98.4% 0.014 180.72); + --color-teal-100: oklch(95.3% 0.051 180.801); + --color-teal-200: oklch(91% 0.096 180.426); + --color-teal-300: oklch(85.5% 0.138 181.071); + --color-teal-400: oklch(77.7% 0.152 181.912); + --color-teal-500: oklch(70.4% 0.14 182.503); + --color-teal-600: oklch(60% 0.118 184.704); + --color-teal-700: oklch(51.1% 0.096 186.391); + --color-teal-800: oklch(43.7% 0.078 188.216); + --color-teal-900: oklch(38.6% 0.063 188.416); + --color-teal-950: oklch(27.7% 0.046 192.524); + + --color-cyan-50: oklch(98.4% 0.019 200.873); + --color-cyan-100: oklch(95.6% 0.045 203.388); + --color-cyan-200: oklch(91.7% 0.08 205.041); + --color-cyan-300: oklch(86.5% 0.127 207.078); + --color-cyan-400: oklch(78.9% 0.154 211.53); + --color-cyan-500: oklch(71.5% 0.143 215.221); + --color-cyan-600: oklch(60.9% 0.126 221.723); + --color-cyan-700: oklch(52% 0.105 223.128); + --color-cyan-800: oklch(45% 0.085 224.283); + --color-cyan-900: oklch(39.8% 0.07 227.392); + --color-cyan-950: oklch(30.2% 0.056 229.695); + + --color-sky-50: oklch(97.7% 0.013 236.62); + --color-sky-100: oklch(95.1% 0.026 236.824); + --color-sky-200: oklch(90.1% 0.058 230.902); + --color-sky-300: oklch(82.8% 0.111 230.318); + --color-sky-400: oklch(74.6% 0.16 232.661); + --color-sky-500: oklch(68.5% 0.169 237.323); + --color-sky-600: oklch(58.8% 0.158 241.966); + --color-sky-700: oklch(50% 0.134 242.749); + --color-sky-800: oklch(44.3% 0.11 240.79); + --color-sky-900: oklch(39.1% 0.09 240.876); + --color-sky-950: oklch(29.3% 0.066 243.157); + + --color-blue-50: oklch(97% 0.014 254.604); + --color-blue-100: oklch(93.2% 0.032 255.585); + --color-blue-200: oklch(88.2% 0.059 254.128); + --color-blue-300: oklch(80.9% 0.105 251.813); + --color-blue-400: oklch(70.7% 0.165 254.624); + --color-blue-500: oklch(62.3% 0.214 259.815); + --color-blue-600: oklch(54.6% 0.245 262.881); + --color-blue-700: oklch(48.8% 0.243 264.376); + --color-blue-800: oklch(42.4% 0.199 265.638); + --color-blue-900: oklch(37.9% 0.146 265.522); + --color-blue-950: oklch(28.2% 0.091 267.935); + + --color-indigo-50: oklch(96.2% 0.018 272.314); + --color-indigo-100: oklch(93% 0.034 272.788); + --color-indigo-200: oklch(87% 0.065 274.039); + --color-indigo-300: oklch(78.5% 0.115 274.713); + --color-indigo-400: oklch(67.3% 0.182 276.935); + --color-indigo-500: oklch(58.5% 0.233 277.117); + --color-indigo-600: oklch(51.1% 0.262 276.966); + --color-indigo-700: oklch(45.7% 0.24 277.023); + --color-indigo-800: oklch(39.8% 0.195 277.366); + --color-indigo-900: oklch(35.9% 0.144 278.697); + --color-indigo-950: oklch(25.7% 0.09 281.288); + + --color-violet-50: oklch(96.9% 0.016 293.756); + --color-violet-100: oklch(94.3% 0.029 294.588); + --color-violet-200: oklch(89.4% 0.057 293.283); + --color-violet-300: oklch(81.1% 0.111 293.571); + --color-violet-400: oklch(70.2% 0.183 293.541); + --color-violet-500: oklch(60.6% 0.25 292.717); + --color-violet-600: oklch(54.1% 0.281 293.009); + --color-violet-700: oklch(49.1% 0.27 292.581); + --color-violet-800: oklch(43.2% 0.232 292.759); + --color-violet-900: oklch(38% 0.189 293.745); + --color-violet-950: oklch(28.3% 0.141 291.089); + + --color-purple-50: oklch(97.7% 0.014 308.299); + --color-purple-100: oklch(94.6% 0.033 307.174); + --color-purple-200: oklch(90.2% 0.063 306.703); + --color-purple-300: oklch(82.7% 0.119 306.383); + --color-purple-400: oklch(71.4% 0.203 305.504); + --color-purple-500: oklch(62.7% 0.265 303.9); + --color-purple-600: oklch(55.8% 0.288 302.321); + --color-purple-700: oklch(49.6% 0.265 301.924); + --color-purple-800: oklch(43.8% 0.218 303.724); + --color-purple-900: oklch(38.1% 0.176 304.987); + --color-purple-950: oklch(29.1% 0.149 302.717); + + --color-fuchsia-50: oklch(97.7% 0.017 320.058); + --color-fuchsia-100: oklch(95.2% 0.037 318.852); + --color-fuchsia-200: oklch(90.3% 0.076 319.62); + --color-fuchsia-300: oklch(83.3% 0.145 321.434); + --color-fuchsia-400: oklch(74% 0.238 322.16); + --color-fuchsia-500: oklch(66.7% 0.295 322.15); + --color-fuchsia-600: oklch(59.1% 0.293 322.896); + --color-fuchsia-700: oklch(51.8% 0.253 323.949); + --color-fuchsia-800: oklch(45.2% 0.211 324.591); + --color-fuchsia-900: oklch(40.1% 0.17 325.612); + --color-fuchsia-950: oklch(29.3% 0.136 325.661); + + --color-pink-50: oklch(97.1% 0.014 343.198); + --color-pink-100: oklch(94.8% 0.028 342.258); + --color-pink-200: oklch(89.9% 0.061 343.231); + --color-pink-300: oklch(82.3% 0.12 346.018); + --color-pink-400: oklch(71.8% 0.202 349.761); + --color-pink-500: oklch(65.6% 0.241 354.308); + --color-pink-600: oklch(59.2% 0.249 0.584); + --color-pink-700: oklch(52.5% 0.223 3.958); + --color-pink-800: oklch(45.9% 0.187 3.815); + --color-pink-900: oklch(40.8% 0.153 2.432); + --color-pink-950: oklch(28.4% 0.109 3.907); + + --color-rose-50: oklch(96.9% 0.015 12.422); + --color-rose-100: oklch(94.1% 0.03 12.58); + --color-rose-200: oklch(89.2% 0.058 10.001); + --color-rose-300: oklch(81% 0.117 11.638); + --color-rose-400: oklch(71.2% 0.194 13.428); + --color-rose-500: oklch(64.5% 0.246 16.439); + --color-rose-600: oklch(58.6% 0.253 17.585); + --color-rose-700: oklch(51.4% 0.222 16.935); + --color-rose-800: oklch(45.5% 0.188 13.697); + --color-rose-900: oklch(41% 0.159 10.272); + --color-rose-950: oklch(27.1% 0.105 12.094); + + --color-slate-50: oklch(98.4% 0.003 247.858); + --color-slate-100: oklch(96.8% 0.007 247.896); + --color-slate-200: oklch(92.9% 0.013 255.508); + --color-slate-300: oklch(86.9% 0.022 252.894); + --color-slate-400: oklch(70.4% 0.04 256.788); + --color-slate-500: oklch(55.4% 0.046 257.417); + --color-slate-600: oklch(44.6% 0.043 257.281); + --color-slate-700: oklch(37.2% 0.044 257.287); + --color-slate-800: oklch(27.9% 0.041 260.031); + --color-slate-900: oklch(20.8% 0.042 265.755); + --color-slate-950: oklch(12.9% 0.042 264.695); + + --color-gray-50: oklch(98.5% 0.002 247.839); + --color-gray-100: oklch(96.7% 0.003 264.542); + --color-gray-200: oklch(92.8% 0.006 264.531); + --color-gray-300: oklch(87.2% 0.01 258.338); + --color-gray-400: oklch(70.7% 0.022 261.325); + --color-gray-500: oklch(55.1% 0.027 264.364); + --color-gray-600: oklch(44.6% 0.03 256.802); + --color-gray-700: oklch(37.3% 0.034 259.733); + --color-gray-800: oklch(27.8% 0.033 256.848); + --color-gray-900: oklch(21% 0.034 264.665); + --color-gray-950: oklch(13% 0.028 261.692); + + --color-zinc-50: oklch(98.5% 0 0); + --color-zinc-100: oklch(96.7% 0.001 286.375); + --color-zinc-200: oklch(92% 0.004 286.32); + --color-zinc-300: oklch(87.1% 0.006 286.286); + --color-zinc-400: oklch(70.5% 0.015 286.067); + --color-zinc-500: oklch(55.2% 0.016 285.938); + --color-zinc-600: oklch(44.2% 0.017 285.786); + --color-zinc-700: oklch(37% 0.013 285.805); + --color-zinc-800: oklch(27.4% 0.006 286.033); + --color-zinc-900: oklch(21% 0.006 285.885); + --color-zinc-950: oklch(14.1% 0.005 285.823); + + --color-neutral-50: oklch(98.5% 0 0); + --color-neutral-100: oklch(97% 0 0); + --color-neutral-200: oklch(92.2% 0 0); + --color-neutral-300: oklch(87% 0 0); + --color-neutral-400: oklch(70.8% 0 0); + --color-neutral-500: oklch(55.6% 0 0); + --color-neutral-600: oklch(43.9% 0 0); + --color-neutral-700: oklch(37.1% 0 0); + --color-neutral-800: oklch(26.9% 0 0); + --color-neutral-900: oklch(20.5% 0 0); + --color-neutral-950: oklch(14.5% 0 0); + + --color-stone-50: oklch(98.5% 0.001 106.423); + --color-stone-100: oklch(97% 0.001 106.424); + --color-stone-200: oklch(92.3% 0.003 48.717); + --color-stone-300: oklch(86.9% 0.005 56.366); + --color-stone-400: oklch(70.9% 0.01 56.259); + --color-stone-500: oklch(55.3% 0.013 58.071); + --color-stone-600: oklch(44.4% 0.011 73.639); + --color-stone-700: oklch(37.4% 0.01 67.558); + --color-stone-800: oklch(26.8% 0.007 34.298); + --color-stone-900: oklch(21.6% 0.006 56.043); + --color-stone-950: oklch(14.7% 0.004 49.25); + + --color-black: #000; + --color-white: #fff; + + --spacing: 0.25rem; + + --breakpoint-sm: 40rem; + --breakpoint-md: 48rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; + + --container-3xs: 16rem; + --container-2xs: 18rem; + --container-xs: 20rem; + --container-sm: 24rem; + --container-md: 28rem; + --container-lg: 32rem; + --container-xl: 36rem; + --container-2xl: 42rem; + --container-3xl: 48rem; + --container-4xl: 56rem; + --container-5xl: 64rem; + --container-6xl: 72rem; + --container-7xl: 80rem; + + --text-xs: 0.75rem; + --text-xs--line-height: calc(1 / 0.75); + --text-sm: 0.875rem; + --text-sm--line-height: calc(1.25 / 0.875); + --text-base: 1rem; + --text-base--line-height: calc(1.5 / 1); + --text-lg: 1.125rem; + --text-lg--line-height: calc(1.75 / 1.125); + --text-xl: 1.25rem; + --text-xl--line-height: calc(1.75 / 1.25); + --text-2xl: 1.5rem; + --text-2xl--line-height: calc(2 / 1.5); + --text-3xl: 1.875rem; + --text-3xl--line-height: calc(2.25 / 1.875); + --text-4xl: 2.25rem; + --text-4xl--line-height: calc(2.5 / 2.25); + --text-5xl: 3rem; + --text-5xl--line-height: 1; + --text-6xl: 3.75rem; + --text-6xl--line-height: 1; + --text-7xl: 4.5rem; + --text-7xl--line-height: 1; + --text-8xl: 6rem; + --text-8xl--line-height: 1; + --text-9xl: 8rem; + --text-9xl--line-height: 1; + + --font-weight-thin: 100; + --font-weight-extralight: 200; + --font-weight-light: 300; + --font-weight-normal: 400; + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; + --font-weight-extrabold: 800; + --font-weight-black: 900; + + --tracking-tighter: -0.05em; + --tracking-tight: -0.025em; + --tracking-normal: 0em; + --tracking-wide: 0.025em; + --tracking-wider: 0.05em; + --tracking-widest: 0.1em; + + --leading-tight: 1.25; + --leading-snug: 1.375; + --leading-normal: 1.5; + --leading-relaxed: 1.625; + --leading-loose: 2; + + --radius-xs: 0.125rem; + --radius-sm: 0.25rem; + --radius-md: 0.375rem; + --radius-lg: 0.5rem; + --radius-xl: 0.75rem; + --radius-2xl: 1rem; + --radius-3xl: 1.5rem; + --radius-4xl: 2rem; + + --shadow-2xs: 0 1px rgb(0 0 0 / 0.05); + --shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); + --shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25); + + --inset-shadow-2xs: inset 0 1px rgb(0 0 0 / 0.05); + --inset-shadow-xs: inset 0 1px 1px rgb(0 0 0 / 0.05); + --inset-shadow-sm: inset 0 2px 4px rgb(0 0 0 / 0.05); + + --drop-shadow-xs: 0 1px 1px rgb(0 0 0 / 0.05); + --drop-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.15); + --drop-shadow-md: 0 3px 3px rgb(0 0 0 / 0.12); + --drop-shadow-lg: 0 4px 4px rgb(0 0 0 / 0.15); + --drop-shadow-xl: 0 9px 7px rgb(0 0 0 / 0.1); + --drop-shadow-2xl: 0 25px 25px rgb(0 0 0 / 0.15); + + --text-shadow-2xs: 0px 1px 0px rgb(0 0 0 / 0.15); + --text-shadow-xs: 0px 1px 1px rgb(0 0 0 / 0.2); + --text-shadow-sm: + 0px 1px 0px rgb(0 0 0 / 0.075), 0px 1px 1px rgb(0 0 0 / 0.075), 0px 2px 2px rgb(0 0 0 / 0.075); + --text-shadow-md: + 0px 1px 1px rgb(0 0 0 / 0.1), 0px 1px 2px rgb(0 0 0 / 0.1), 0px 2px 4px rgb(0 0 0 / 0.1); + --text-shadow-lg: + 0px 1px 2px rgb(0 0 0 / 0.1), 0px 3px 2px rgb(0 0 0 / 0.1), 0px 4px 8px rgb(0 0 0 / 0.1); + + --ease-in: cubic-bezier(0.4, 0, 1, 1); + --ease-out: cubic-bezier(0, 0, 0.2, 1); + --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); + + --animate-spin: spin 1s linear infinite; + --animate-ping: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite; + --animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; + --animate-bounce: bounce 1s infinite; + + @keyframes spin { + to { + transform: rotate(360deg); + } + } + + @keyframes ping { + 75%, + 100% { + transform: scale(2); + opacity: 0; + } + } + + @keyframes pulse { + 50% { + opacity: 0.5; + } + } + + @keyframes bounce { + 0%, + 100% { + transform: translateY(-25%); + animation-timing-function: cubic-bezier(0.8, 0, 1, 1); + } + + 50% { + transform: none; + animation-timing-function: cubic-bezier(0, 0, 0.2, 1); + } + } + + --blur-xs: 4px; + --blur-sm: 8px; + --blur-md: 12px; + --blur-lg: 16px; + --blur-xl: 24px; + --blur-2xl: 40px; + --blur-3xl: 64px; + + --perspective-dramatic: 100px; + --perspective-near: 300px; + --perspective-normal: 500px; + --perspective-midrange: 800px; + --perspective-distant: 1200px; + + --aspect-video: 16 / 9; + + --default-transition-duration: 150ms; + --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + --default-font-family: --theme(--font-sans, initial); + --default-font-feature-settings: --theme(--font-sans--font-feature-settings, initial); + --default-font-variation-settings: --theme(--font-sans--font-variation-settings, initial); + --default-mono-font-family: --theme(--font-mono, initial); + --default-mono-font-feature-settings: --theme(--font-mono--font-feature-settings, initial); + --default-mono-font-variation-settings: --theme(--font-mono--font-variation-settings, initial); +} + +/* Deprecated */ +@theme default inline reference { + --blur: 8px; + --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --shadow-inner: inset 0 2px 4px 0 rgb(0 0 0 / 0.05); + --drop-shadow: 0 1px 2px rgb(0 0 0 / 0.1), 0 1px 1px rgb(0 0 0 / 0.06); + --radius: 0.25rem; + --max-width-prose: 65ch; +} +` + +export default css; diff --git a/packages/tailwind/src/utils/tailwindcss/tailwind-stylesheets/utilities.ts b/packages/tailwind/src/utils/tailwindcss/tailwind-stylesheets/utilities.ts new file mode 100644 index 0000000000..413a00673b --- /dev/null +++ b/packages/tailwind/src/utils/tailwindcss/tailwind-stylesheets/utilities.ts @@ -0,0 +1,5 @@ +const css = ` +@tailwind utilities; +`; + +export default css; From c1910c3855ef1750841aa64e25627467b308cf75 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Tue, 19 Aug 2025 11:33:06 -0300 Subject: [PATCH 003/193] update types --- packages/tailwind/src/tailwind.tsx | 31 +++++++----------------------- 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/packages/tailwind/src/tailwind.tsx b/packages/tailwind/src/tailwind.tsx index f8b100bfb2..97fd7a8dda 100644 --- a/packages/tailwind/src/tailwind.tsx +++ b/packages/tailwind/src/tailwind.tsx @@ -1,27 +1,12 @@ import { Root } from 'postcss'; import * as React from 'react'; -import type { Config as TailwindOriginalConfig } from 'tailwindcss'; +import type { Config } from 'tailwindcss'; import { minifyCss } from './utils/css/minify-css'; import { removeRuleDuplicatesFromRoot } from './utils/css/remove-rule-duplicates-from-root'; import { mapReactTree } from './utils/react/map-react-tree'; import { cloneElementWithInlinedStyles } from './utils/tailwindcss/clone-element-with-inlined-styles'; -import { setupTailwind } from './utils/tailwindcss/setup-tailwind'; - -export type TailwindConfig = Pick< - TailwindOriginalConfig, - | 'important' - | 'prefix' - | 'separator' - | 'safelist' - | 'blocklist' - | 'presets' - | 'future' - | 'experimental' - | 'darkMode' - | 'theme' - | 'corePlugins' - | 'plugins' ->; + +export type TailwindConfig = Config; export interface TailwindProps { children: React.ReactNode; @@ -91,21 +76,19 @@ export const pixelBasedPreset: TailwindConfig = { }, }; -export const Tailwind: React.FC = ({ children, config }) => { - const tailwind = setupTailwind(config ?? {}); - +export const Tailwind: React.FC = async ({ children, config }) => { const nonInlineStylesRootToApply = new Root(); let mediaQueryClassesForAllElement: string[] = []; let hasNonInlineStylesToApply = false as boolean; - let mappedChildren: React.ReactNode = mapReactTree(children, (node) => { + let mappedChildren: React.ReactNode = await mapReactTree(children, async (node) => { if (React.isValidElement(node)) { const { elementWithInlinedStyles, nonInlinableClasses, nonInlineStyleNodes, - } = cloneElementWithInlinedStyles(node, tailwind); + } = await cloneElementWithInlinedStyles(node, config ?? {}); mediaQueryClassesForAllElement = mediaQueryClassesForAllElement.concat(nonInlinableClasses); nonInlineStylesRootToApply.append(nonInlineStyleNodes); @@ -125,7 +108,7 @@ export const Tailwind: React.FC = ({ children, config }) => { if (hasNonInlineStylesToApply) { let hasAppliedNonInlineStyles = false as boolean; - mappedChildren = mapReactTree(mappedChildren, (node) => { + mappedChildren = await mapReactTree(mappedChildren, (node) => { if (hasAppliedNonInlineStyles) { return node; } From c9b22f8954803b9c9a8f41d06f1ff1dd95545741 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Tue, 19 Aug 2025 11:33:14 -0300 Subject: [PATCH 004/193] fix module resoltuion --- packages/tailwind/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/tailwind/tsconfig.json b/packages/tailwind/tsconfig.json index c04ef2d9bc..23405a05c7 100644 --- a/packages/tailwind/tsconfig.json +++ b/packages/tailwind/tsconfig.json @@ -10,6 +10,7 @@ "integrations/nextjs" ], "compilerOptions": { + "moduleResolution": "bundler", "strict": true, "noEmit": true } From 3162df525dee6827f80fd31ca6130333f1bcba0f Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Tue, 19 Aug 2025 11:33:28 -0300 Subject: [PATCH 005/193] update tailwind --- packages/tailwind/package.json | 2 +- pnpm-lock.yaml | 41 ++++++---------------------------- 2 files changed, 8 insertions(+), 35 deletions(-) diff --git a/packages/tailwind/package.json b/packages/tailwind/package.json index 689a5f7692..d46b6f7be0 100644 --- a/packages/tailwind/package.json +++ b/packages/tailwind/package.json @@ -60,7 +60,7 @@ "postcss-selector-parser": "7.1.0", "react-dom": "^19", "shelljs": "0.9.2", - "tailwindcss": "3.4.10", + "tailwindcss": "4.1.12", "tsconfig": "workspace:*", "tsup": "8.4.0", "typescript": "5.8.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dcbb0762b6..830feeec60 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -977,8 +977,8 @@ importers: specifier: 0.9.2 version: 0.9.2 tailwindcss: - specifier: 3.4.10 - version: 3.4.10 + specifier: 4.1.12 + version: 4.1.12 tsconfig: specifier: workspace:* version: link:../tsconfig @@ -8075,16 +8075,14 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - tailwindcss@3.4.10: - resolution: {integrity: sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==} - engines: {node: '>=14.0.0'} - hasBin: true - tailwindcss@3.4.3: resolution: {integrity: sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==} engines: {node: '>=14.0.0'} hasBin: true + tailwindcss@4.1.12: + resolution: {integrity: sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==} + tapable@2.2.1: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} @@ -17041,33 +17039,6 @@ snapshots: transitivePeerDependencies: - ts-node - tailwindcss@3.4.10: - dependencies: - '@alloc/quick-lru': 5.2.0 - arg: 5.0.2 - chokidar: 3.6.0 - didyoumean: 1.2.2 - dlv: 1.1.3 - fast-glob: 3.3.2 - glob-parent: 6.0.2 - is-glob: 4.0.3 - jiti: 1.21.7 - lilconfig: 2.1.0 - micromatch: 4.0.8 - normalize-path: 3.0.0 - object-hash: 3.0.0 - picocolors: 1.1.1 - postcss: 8.5.3 - postcss-import: 15.1.0(postcss@8.5.3) - postcss-js: 4.0.1(postcss@8.5.3) - postcss-load-config: 4.0.2(postcss@8.5.3) - postcss-nested: 6.2.0(postcss@8.5.3) - postcss-selector-parser: 6.1.2 - resolve: 1.22.9 - sucrase: 3.35.0 - transitivePeerDependencies: - - ts-node - tailwindcss@3.4.3: dependencies: '@alloc/quick-lru': 5.2.0 @@ -17095,6 +17066,8 @@ snapshots: transitivePeerDependencies: - ts-node + tailwindcss@4.1.12: {} + tapable@2.2.1: {} tar-fs@3.0.8: From 3d31304ee52b7e3598c3a15b46415acf62f6fd52 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Tue, 19 Aug 2025 14:05:16 -0300 Subject: [PATCH 006/193] use tsup --- packages/tailwind/package.json | 4 ++-- packages/tailwind/tsup.config.ts | 0 packages/tailwind/vite.config.ts | 33 -------------------------------- 3 files changed, 2 insertions(+), 35 deletions(-) create mode 100644 packages/tailwind/tsup.config.ts delete mode 100644 packages/tailwind/vite.config.ts diff --git a/packages/tailwind/package.json b/packages/tailwind/package.json index d46b6f7be0..8d1877f6d2 100644 --- a/packages/tailwind/package.json +++ b/packages/tailwind/package.json @@ -23,8 +23,8 @@ }, "license": "MIT", "scripts": { - "build": "tsc && NODE_ENV=production vite build --mode production && node ./copy-tailwind-types.mjs", - "build:watch": "vite build --watch", + "build": "tsup src/index.ts --format esm,cjs --dts --external react", + "build:watch": "tsup src/index.ts --format esm,cjs --dts --external react --watch", "clean": "rm -rf dist", "test": "vitest run", "test:watch": "vitest" diff --git a/packages/tailwind/tsup.config.ts b/packages/tailwind/tsup.config.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/tailwind/vite.config.ts b/packages/tailwind/vite.config.ts deleted file mode 100644 index c7fbbd5260..0000000000 --- a/packages/tailwind/vite.config.ts +++ /dev/null @@ -1,33 +0,0 @@ -import path from 'node:path'; -import { defineConfig } from 'vite'; -import dts from 'vite-plugin-dts'; - -export default defineConfig({ - plugins: [ - dts({ - include: ['src'], - compilerOptions: { - paths: { - tailwindcss: [path.resolve(__dirname, './dist/tailwindcss')], - }, - }, - rollupTypes: true, - outDir: 'dist', - }), - ], - build: { - rollupOptions: { - // in summary, this bundles the following since vite defaults to bundling - // - tailwindcss - // - postcss - // - postcss-selector-parser - external: ['react', /^react\/.*/, 'react-dom', /react-dom\/.*/], - }, - lib: { - entry: path.resolve(__dirname, 'src/index.ts'), - fileName: 'index', - formats: ['es', 'cjs'], - }, - outDir: 'dist', - }, -}); From 8418fe90e3f6437c3bec1fbaf637c0fbaf2a9f16 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Tue, 19 Aug 2025 14:05:31 -0300 Subject: [PATCH 007/193] attempt at fixing inline style resolution --- .../src/utils/css/make-inline-styles-for.spec.ts | 8 +++----- .../tailwind/src/utils/css/make-inline-styles-for.ts | 11 +++++++++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/tailwind/src/utils/css/make-inline-styles-for.spec.ts b/packages/tailwind/src/utils/css/make-inline-styles-for.spec.ts index 3fe80bb2fe..f623e21e40 100644 --- a/packages/tailwind/src/utils/css/make-inline-styles-for.spec.ts +++ b/packages/tailwind/src/utils/css/make-inline-styles-for.spec.ts @@ -1,12 +1,10 @@ -import { setupTailwind } from '../tailwindcss/setup-tailwind'; +import { generateRootForClasses } from '../tailwindcss/generate-root-for-classes'; import { makeInlineStylesFor } from './make-inline-styles-for'; -test('makeInlineStylesFor()', () => { - const tailwind = setupTailwind({}); - +test('makeInlineStylesFor()', async () => { const className = 'bg-red-500 sm:bg-blue-300 w-full md:max-w-[400px] my-custom-class'; - const tailwindStyles = tailwind.generateRootForClasses(className.split(' ')); + const tailwindStyles = await generateRootForClasses(className.split(' '), {}); expect(makeInlineStylesFor(className, tailwindStyles)).toEqual({ styles: { backgroundColor: 'rgb(239 68 68 / 1)', width: '100%' }, diff --git a/packages/tailwind/src/utils/css/make-inline-styles-for.ts b/packages/tailwind/src/utils/css/make-inline-styles-for.ts index 045e3e9ca2..969776edb8 100644 --- a/packages/tailwind/src/utils/css/make-inline-styles-for.ts +++ b/packages/tailwind/src/utils/css/make-inline-styles-for.ts @@ -1,11 +1,16 @@ -import type { Root, Rule } from 'postcss'; +import { AtRule, type Root, type Rule } from 'postcss'; import selectorParser from 'postcss-selector-parser'; import { convertCssPropertyToReactProperty } from '../compatibility/convert-css-property-to-react-property'; import { unescapeClass } from '../compatibility/unescape-class'; const walkInlinableRules = (root: Root, callback: (rule: Rule) => void) => { root.walkRules((rule) => { - if (rule.parent?.type === 'atrule') { + /// Only ignore AtRules that are not @layer, but can be @layer base + if ( + rule.parent instanceof AtRule && + rule.parent.name !== 'layer' && + rule.parent.params !== 'base' + ) { return; } @@ -26,6 +31,7 @@ export function makeInlineStylesFor( className: string, tailwindStylesRoot: Root, ) { + console.log(tailwindStylesRoot.toString()); const classes = className.split(' '); let residualClasses = [...classes]; @@ -44,6 +50,7 @@ export function makeInlineStylesFor( }); rule.walkDecls((declaration) => { + console.log(declaration.prop, declaration.value); styles[convertCssPropertyToReactProperty(declaration.prop)] = declaration.value + (declaration.important ? '!important' : ''); }); From 2df5e954f02886613d1ac4fac9d75dca4fc97fb8 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Wed, 20 Aug 2025 11:06:19 -0300 Subject: [PATCH 008/193] don't treat @layer at rules as media queries --- .../resolve-all-css-variables.spec.ts.snap | 9 +++++++++ .../utils/css/resolve-all-css-variables.spec.ts | 16 ++++++++++++++++ .../src/utils/css/resolve-all-css-variables.ts | 1 + 3 files changed, 26 insertions(+) create mode 100644 packages/tailwind/src/utils/css/__snapshots__/resolve-all-css-variables.spec.ts.snap diff --git a/packages/tailwind/src/utils/css/__snapshots__/resolve-all-css-variables.spec.ts.snap b/packages/tailwind/src/utils/css/__snapshots__/resolve-all-css-variables.spec.ts.snap new file mode 100644 index 0000000000..2ebd9f577e --- /dev/null +++ b/packages/tailwind/src/utils/css/__snapshots__/resolve-all-css-variables.spec.ts.snap @@ -0,0 +1,9 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`resolveAllCSSVariables > should work for variables across different CSS layers 1`] = ` +"@layer utilities { + .box { + width: 100px; + } + }" +`; diff --git a/packages/tailwind/src/utils/css/resolve-all-css-variables.spec.ts b/packages/tailwind/src/utils/css/resolve-all-css-variables.spec.ts index 7092ef6938..a5d154d328 100644 --- a/packages/tailwind/src/utils/css/resolve-all-css-variables.spec.ts +++ b/packages/tailwind/src/utils/css/resolve-all-css-variables.spec.ts @@ -16,6 +16,22 @@ describe('resolveAllCSSVariables', () => { }`); }); + it('should work for variables across different CSS layers', () => { + const root = parse(`@layer base { + :root { + --width: 100px; + } + } + + @layer utilities { + .box { + width: var(--width); + } + }`); + + expect(resolveAllCSSVariables(root).toString()).toMatchSnapshot(); + }); + it('should work with multiple variables in the same declaration', () => { const root = parse(`:root { --top: 101px; diff --git a/packages/tailwind/src/utils/css/resolve-all-css-variables.ts b/packages/tailwind/src/utils/css/resolve-all-css-variables.ts index 9411fa4c2f..825ff2ff75 100644 --- a/packages/tailwind/src/utils/css/resolve-all-css-variables.ts +++ b/packages/tailwind/src/utils/css/resolve-all-css-variables.ts @@ -48,6 +48,7 @@ export const resolveAllCSSVariables = (root: Root) => { ) { if ( otherDecl.parent?.parent instanceof AtRule && + otherDecl.parent?.parent.name !== 'layer' && otherDecl.parent !== declaration.parent ) { const atRule = otherDecl.parent.parent; From 6839d7471bd42b36ba2b71ff5166c0beb2654799 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Wed, 20 Aug 2025 11:06:36 -0300 Subject: [PATCH 009/193] filter for 'media' at ruels --- packages/tailwind/src/utils/css/resolve-all-css-variables.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tailwind/src/utils/css/resolve-all-css-variables.ts b/packages/tailwind/src/utils/css/resolve-all-css-variables.ts index 825ff2ff75..c7abfbcdef 100644 --- a/packages/tailwind/src/utils/css/resolve-all-css-variables.ts +++ b/packages/tailwind/src/utils/css/resolve-all-css-variables.ts @@ -48,7 +48,7 @@ export const resolveAllCSSVariables = (root: Root) => { ) { if ( otherDecl.parent?.parent instanceof AtRule && - otherDecl.parent?.parent.name !== 'layer' && + otherDecl.parent?.parent.name === 'media' && otherDecl.parent !== declaration.parent ) { const atRule = otherDecl.parent.parent; From 68d07e3bd06699261e6076839daa588f2c6a6d9d Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Mon, 25 Aug 2025 14:53:31 -0300 Subject: [PATCH 010/193] some initial work that runs at least --- .../src/utils/css/make-inline-styles-for.ts | 7 ++---- .../css/resolve-all-css-variables.spec.ts | 22 ++++++++++++++++++- .../utils/css/resolve-all-css-variables.ts | 12 ++++++---- .../tailwindcss/generate-root-for-classes.ts | 2 ++ 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/packages/tailwind/src/utils/css/make-inline-styles-for.ts b/packages/tailwind/src/utils/css/make-inline-styles-for.ts index 969776edb8..2659e698e2 100644 --- a/packages/tailwind/src/utils/css/make-inline-styles-for.ts +++ b/packages/tailwind/src/utils/css/make-inline-styles-for.ts @@ -5,11 +5,10 @@ import { unescapeClass } from '../compatibility/unescape-class'; const walkInlinableRules = (root: Root, callback: (rule: Rule) => void) => { root.walkRules((rule) => { - /// Only ignore AtRules that are not @layer, but can be @layer base + // Only ignore AtRules that are not @layer, but can be @layer base if ( rule.parent instanceof AtRule && - rule.parent.name !== 'layer' && - rule.parent.params !== 'base' + (rule.parent.name !== 'layer' || rule.parent.params === 'base') ) { return; } @@ -31,7 +30,6 @@ export function makeInlineStylesFor( className: string, tailwindStylesRoot: Root, ) { - console.log(tailwindStylesRoot.toString()); const classes = className.split(' '); let residualClasses = [...classes]; @@ -50,7 +48,6 @@ export function makeInlineStylesFor( }); rule.walkDecls((declaration) => { - console.log(declaration.prop, declaration.value); styles[convertCssPropertyToReactProperty(declaration.prop)] = declaration.value + (declaration.important ? '!important' : ''); }); diff --git a/packages/tailwind/src/utils/css/resolve-all-css-variables.spec.ts b/packages/tailwind/src/utils/css/resolve-all-css-variables.spec.ts index a5d154d328..412f472f9a 100644 --- a/packages/tailwind/src/utils/css/resolve-all-css-variables.spec.ts +++ b/packages/tailwind/src/utils/css/resolve-all-css-variables.spec.ts @@ -58,7 +58,7 @@ describe('resolveAllCSSVariables', () => { width: var(--width); }`); }); - + it('should work with variables set in the same rule', () => { const root = parse(`.box { --width: 200px; @@ -84,6 +84,26 @@ describe('resolveAllCSSVariables', () => { `); }); + it.only('should work with a variable set in a layer, and used in another through a media query', () => { + const root = parse(`@layer theme { + :root { + --color-blue-300: blue; + } +} + +.sm\:bg-blue-300 { + @media (width >= 40rem) { + background-color: var(--color-blue-300); + } +}`); + expect( + resolveAllCSSVariables(root).toString(), + ).toBe(`.sm\:bg-blue-300 { + @media (width >= 40rem) { + background-color: blue; +}`); + }); + it('should work with different values between media queries', () => { const root = parse(`:root { --width: 100px; diff --git a/packages/tailwind/src/utils/css/resolve-all-css-variables.ts b/packages/tailwind/src/utils/css/resolve-all-css-variables.ts index c7abfbcdef..32aaa62cd1 100644 --- a/packages/tailwind/src/utils/css/resolve-all-css-variables.ts +++ b/packages/tailwind/src/utils/css/resolve-all-css-variables.ts @@ -23,7 +23,7 @@ const doNodesMatch = (first: Node | undefined, second: Node | undefined) => { export const resolveAllCSSVariables = (root: Root) => { root.walkRules((rule) => { - const declarationsForAtRules = new Map>(); + const declarationsForMediaQueries = new Map>(); const valueReplacingInformation = new Set<{ declaration: Declaration; replacing: string; @@ -61,11 +61,12 @@ export const resolveAllCSSVariables = (root: Root) => { ); clonedDeclaration.important = declaration.important; - const declarationForAtRule = declarationsForAtRules.get(atRule); + const declarationForAtRule = + declarationsForMediaQueries.get(atRule); if (declarationForAtRule) { declarationForAtRule.add(clonedDeclaration); } else { - declarationsForAtRules.set( + declarationsForMediaQueries.set( otherDecl.parent.parent, new Set([clonedDeclaration]), ); @@ -92,7 +93,10 @@ export const resolveAllCSSVariables = (root: Root) => { declaration.value = declaration.value.replaceAll(replacing, replacement); } - for (const [atRule, declarations] of declarationsForAtRules.entries()) { + for (const [ + atRule, + declarations, + ] of declarationsForMediaQueries.entries()) { const equivalentRule = createRule(); equivalentRule.selector = rule.selector; equivalentRule.append(...declarations); diff --git a/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.ts b/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.ts index 8665ee27b0..66133dd934 100644 --- a/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.ts +++ b/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.ts @@ -66,7 +66,9 @@ export async function generateRootForClasses( }); const css = compiler.build(classes); const root = parse(css); + console.log(root.toString()); resolveAllCSSVariables(root); + console.log(root.toString()); return root; } From 3079d5afbcb63dc5f14566e903b1dd7a44121c00 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Mon, 25 Aug 2025 15:07:39 -0300 Subject: [PATCH 011/193] lint --- packages/tailwind/src/tailwind.tsx | 44 +++++++++++-------- .../css/resolve-all-css-variables.spec.ts | 6 +-- .../generate-root-for-classes.spec.ts | 10 +++-- .../tailwindcss/tailwind-stylesheets/theme.ts | 2 +- 4 files changed, 34 insertions(+), 28 deletions(-) diff --git a/packages/tailwind/src/tailwind.tsx b/packages/tailwind/src/tailwind.tsx index 97fd7a8dda..5c2482cf7e 100644 --- a/packages/tailwind/src/tailwind.tsx +++ b/packages/tailwind/src/tailwind.tsx @@ -76,32 +76,38 @@ export const pixelBasedPreset: TailwindConfig = { }, }; -export const Tailwind: React.FC = async ({ children, config }) => { +export const Tailwind: React.FC = async ({ + children, + config, +}) => { const nonInlineStylesRootToApply = new Root(); let mediaQueryClassesForAllElement: string[] = []; let hasNonInlineStylesToApply = false as boolean; - let mappedChildren: React.ReactNode = await mapReactTree(children, async (node) => { - if (React.isValidElement(node)) { - const { - elementWithInlinedStyles, - nonInlinableClasses, - nonInlineStyleNodes, - } = await cloneElementWithInlinedStyles(node, config ?? {}); - mediaQueryClassesForAllElement = - mediaQueryClassesForAllElement.concat(nonInlinableClasses); - nonInlineStylesRootToApply.append(nonInlineStyleNodes); - - if (nonInlinableClasses.length > 0 && !hasNonInlineStylesToApply) { - hasNonInlineStylesToApply = true; - } + let mappedChildren: React.ReactNode = await mapReactTree( + children, + async (node) => { + if (React.isValidElement(node)) { + const { + elementWithInlinedStyles, + nonInlinableClasses, + nonInlineStyleNodes, + } = await cloneElementWithInlinedStyles(node, config ?? {}); + mediaQueryClassesForAllElement = + mediaQueryClassesForAllElement.concat(nonInlinableClasses); + nonInlineStylesRootToApply.append(nonInlineStyleNodes); + + if (nonInlinableClasses.length > 0 && !hasNonInlineStylesToApply) { + hasNonInlineStylesToApply = true; + } - return elementWithInlinedStyles; - } + return elementWithInlinedStyles; + } - return node; - }); + return node; + }, + ); removeRuleDuplicatesFromRoot(nonInlineStylesRootToApply); diff --git a/packages/tailwind/src/utils/css/resolve-all-css-variables.spec.ts b/packages/tailwind/src/utils/css/resolve-all-css-variables.spec.ts index 412f472f9a..d314794aa5 100644 --- a/packages/tailwind/src/utils/css/resolve-all-css-variables.spec.ts +++ b/packages/tailwind/src/utils/css/resolve-all-css-variables.spec.ts @@ -58,7 +58,7 @@ describe('resolveAllCSSVariables', () => { width: var(--width); }`); }); - + it('should work with variables set in the same rule', () => { const root = parse(`.box { --width: 200px; @@ -96,9 +96,7 @@ describe('resolveAllCSSVariables', () => { background-color: var(--color-blue-300); } }`); - expect( - resolveAllCSSVariables(root).toString(), - ).toBe(`.sm\:bg-blue-300 { + expect(resolveAllCSSVariables(root).toString()).toBe(`.sm\:bg-blue-300 { @media (width >= 40rem) { background-color: blue; }`); diff --git a/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.spec.ts b/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.spec.ts index 0884c78750..b255ddb2e5 100644 --- a/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.spec.ts +++ b/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.spec.ts @@ -2,9 +2,11 @@ import { generateRootForClasses } from './generate-root-for-classes'; test("tailwind's generateRootForClasses()", async () => { expect( - (await generateRootForClasses( - ['text-red-500', 'sm:bg-blue-300', 'bg-slate-900'], - {}, - )).toString(), + ( + await generateRootForClasses( + ['text-red-500', 'sm:bg-blue-300', 'bg-slate-900'], + {}, + ) + ).toString(), ).toMatchSnapshot(); }); diff --git a/packages/tailwind/src/utils/tailwindcss/tailwind-stylesheets/theme.ts b/packages/tailwind/src/utils/tailwindcss/tailwind-stylesheets/theme.ts index f67d580c5f..ed1f7768ea 100644 --- a/packages/tailwind/src/utils/tailwindcss/tailwind-stylesheets/theme.ts +++ b/packages/tailwind/src/utils/tailwindcss/tailwind-stylesheets/theme.ts @@ -461,6 +461,6 @@ const css = ` --radius: 0.25rem; --max-width-prose: 65ch; } -` +`; export default css; From 73439df2d9a13fe6591103fde7e135636df6dd1a Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Mon, 25 Aug 2025 15:08:57 -0300 Subject: [PATCH 012/193] temporary change to build script for testing purposes --- .github/workflows/preview-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview-release.yml b/.github/workflows/preview-release.yml index 3bb18e489d..0ca3198fc6 100644 --- a/.github/workflows/preview-release.yml +++ b/.github/workflows/preview-release.yml @@ -38,7 +38,7 @@ jobs: cache-prefix: ${{ runner.os }}-turbo- provider: github - name: Run Build - run: pnpm build + run: pnpm turbo build --filter=./packages/* env: SPAM_ASSASSIN_HOST: ${{ secrets.SPAM_ASSASSIN_HOST }} SPAM_ASSASSIN_PORT: ${{ secrets.SPAM_ASSASSIN_PORT }} From 6ea656f221ea64853c56f40ac3619bcacad8f372 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Tue, 26 Aug 2025 16:18:07 -0300 Subject: [PATCH 013/193] refactored css variable resolution for simplicity and removing unecessary features --- .../resolve-all-css-variables.spec.ts.snap | 41 +++ .../css/resolve-all-css-variables.spec.ts | 91 +++--- .../utils/css/resolve-all-css-variables.ts | 258 +++++++++++------- 3 files changed, 244 insertions(+), 146 deletions(-) diff --git a/packages/tailwind/src/utils/css/__snapshots__/resolve-all-css-variables.spec.ts.snap b/packages/tailwind/src/utils/css/__snapshots__/resolve-all-css-variables.spec.ts.snap index 2ebd9f577e..faee81e82f 100644 --- a/packages/tailwind/src/utils/css/__snapshots__/resolve-all-css-variables.spec.ts.snap +++ b/packages/tailwind/src/utils/css/__snapshots__/resolve-all-css-variables.spec.ts.snap @@ -1,5 +1,11 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`resolveAllCSSVariables > should keep variable usages if it cant find their declaration 1`] = ` +".box { + width: var(--width); +}" +`; + exports[`resolveAllCSSVariables > should work for variables across different CSS layers 1`] = ` "@layer utilities { .box { @@ -7,3 +13,38 @@ exports[`resolveAllCSSVariables > should work for variables across different CSS } }" `; + +exports[`resolveAllCSSVariables > should work with a variable set in a layer, and used in another through a media query 1`] = ` +"@layer utilities { + .sm:bg-blue-300 { + @media (width >= 40rem) { + background-color: blue; + } + } +}" +`; + +exports[`resolveAllCSSVariables > should work with multiple variables in the same declaration 1`] = ` +".box { + margin: 101px 103px 102px 104px; + }" +`; + +exports[`resolveAllCSSVariables > should work with simple css variables on a :root 1`] = ` +".box { + width: 100px; +}" +`; + +exports[`resolveAllCSSVariables > should work with variables set in the same rule 1`] = ` +".box { + width: 200px; +} + +@media (min-width: 1280px) { + .xl\\:bg-green-500 { + background-color: rgb(34 197 94 / 1) + } +} +" +`; diff --git a/packages/tailwind/src/utils/css/resolve-all-css-variables.spec.ts b/packages/tailwind/src/utils/css/resolve-all-css-variables.spec.ts index d314794aa5..6d281c041f 100644 --- a/packages/tailwind/src/utils/css/resolve-all-css-variables.spec.ts +++ b/packages/tailwind/src/utils/css/resolve-all-css-variables.spec.ts @@ -11,9 +11,7 @@ describe('resolveAllCSSVariables', () => { width: var(--width); }`); - expect(resolveAllCSSVariables(root).toString()).toBe(`.box { - width: 100px; -}`); + expect(resolveAllCSSVariables(root).toString()).toMatchSnapshot(); }); it('should work for variables across different CSS layers', () => { @@ -44,9 +42,7 @@ describe('resolveAllCSSVariables', () => { margin: var(--top) var(--right) var(--bottom) var(--left); }`); - expect(resolveAllCSSVariables(root).toString()).toBe(`.box { - margin: 101px 103px 102px 104px; - }`); + expect(resolveAllCSSVariables(root).toString()).toMatchSnapshot(); }); it('should keep variable usages if it cant find their declaration', () => { @@ -54,9 +50,7 @@ describe('resolveAllCSSVariables', () => { width: var(--width); }`); - expect(resolveAllCSSVariables(root).toString()).toBe(`.box { - width: var(--width); -}`); + expect(resolveAllCSSVariables(root).toString()).toMatchSnapshot(); }); it('should work with variables set in the same rule', () => { @@ -72,60 +66,51 @@ describe('resolveAllCSSVariables', () => { } } `); - expect(resolveAllCSSVariables(root).toString()).toBe(`.box { - width: 200px; -} - -@media (min-width: 1280px) { - .xl\\:bg-green-500 { - background-color: rgb(34 197 94 / 1) - } -} -`); + expect(resolveAllCSSVariables(root).toString()).toMatchSnapshot(); }); - it.only('should work with a variable set in a layer, and used in another through a media query', () => { + it('should work with a variable set in a layer, and used in another through a media query', () => { const root = parse(`@layer theme { :root { --color-blue-300: blue; } } -.sm\:bg-blue-300 { - @media (width >= 40rem) { - background-color: var(--color-blue-300); +@layer utilities { + .sm\:bg-blue-300 { + @media (width >= 40rem) { + background-color: var(--color-blue-300); + } } }`); - expect(resolveAllCSSVariables(root).toString()).toBe(`.sm\:bg-blue-300 { - @media (width >= 40rem) { - background-color: blue; -}`); + expect(resolveAllCSSVariables(root).toString()).toMatchSnapshot(); }); - it('should work with different values between media queries', () => { - const root = parse(`:root { - --width: 100px; -} - -@media (max-width: 1000px) { - :root { - --width: 200px; - } -} - -.box { - width: var(--width); -}`); - expect( - resolveAllCSSVariables(root).toString(), - ).toBe(`@media (max-width: 1000px) { - .box { - width: 200px; - } -} - -.box { - width: 100px; -}`); - }); + // this behavior is not supported anymore, since it doesn't seem like tailwindcss actually generates any CSS that uses the pattern of defining css variables from inside media queries + // it.only('should work with different values between media queries', () => { + // const root = parse(`:root { + // --width: 100px; + // } + // + // @media (max-width: 1000px) { + // :root { + // --width: 200px; + // } + // } + // + // .box { + // width: var(--width); + // }`); + // expect( + // resolveAllCSSVariables(root).toString(), + // ).toBe(`@media (max-width: 1000px) { + // .box { + // width: 200px; + // } + // } + // + // .box { + // width: 100px; + // }`); + // }); }); diff --git a/packages/tailwind/src/utils/css/resolve-all-css-variables.ts b/packages/tailwind/src/utils/css/resolve-all-css-variables.ts index 32aaa62cd1..2d5209119c 100644 --- a/packages/tailwind/src/utils/css/resolve-all-css-variables.ts +++ b/packages/tailwind/src/utils/css/resolve-all-css-variables.ts @@ -1,119 +1,191 @@ import { AtRule, - decl as createDeclaration, - rule as createRule, type Declaration, - type Node, type Root, Rule, } from 'postcss'; import { removeIfEmptyRecursively } from './remove-if-empty-recursively'; -const doNodesMatch = (first: Node | undefined, second: Node | undefined) => { - if (first instanceof Rule && second instanceof Rule) { - return ( - first.selector === second.selector || - second.selector.includes('*') || - second.selector.includes(':root') - ); - } +interface VariableUse { + declaration: Declaration; + variable: string; +} - return first === second; -}; +interface VariableDefinition { + declaration: Declaration; + variable: string; + definition: string; +} -export const resolveAllCSSVariables = (root: Root) => { - root.walkRules((rule) => { - const declarationsForMediaQueries = new Map>(); - const valueReplacingInformation = new Set<{ - declaration: Declaration; - replacing: string; - replacement: string; - }>(); +const doSelectorsIntersect = (first: string, second: string): boolean => { + if (first === second) { + return true; + } - rule.walkDecls((declaration) => { - if (/var\(--[^\s)]+\)/.test(declaration.value)) { - /** - * @example ['var(--width)', 'var(--length)'] - */ - const variablesUsed = [ - ...declaration.value.matchAll(/var\(--[^\s)]+\)/gm), - ].map((match) => match.toString()); + if (first.includes(':root') || second.includes(':root')) { + return true; + } - root.walkDecls((otherDecl) => { - if (/--[^\s]+/.test(otherDecl.prop)) { - const variable = `var(${otherDecl.prop})`; - if ( - variablesUsed?.includes(variable) && - doNodesMatch(declaration.parent, otherDecl.parent) - ) { - if ( - otherDecl.parent?.parent instanceof AtRule && - otherDecl.parent?.parent.name === 'media' && - otherDecl.parent !== declaration.parent - ) { - const atRule = otherDecl.parent.parent; + if (first.includes('*') || second.includes('*')) { + return true; + } - const clonedDeclaration = createDeclaration(); - clonedDeclaration.prop = declaration.prop; - clonedDeclaration.value = declaration.value.replaceAll( - variable, - otherDecl.value, - ); - clonedDeclaration.important = declaration.important; + return false; +}; + +export const resolveAllCSSVariables = (root: Root) => { + const variableDefinitions = new Set(); + const variableUses = new Set(); - const declarationForAtRule = - declarationsForMediaQueries.get(atRule); - if (declarationForAtRule) { - declarationForAtRule.add(clonedDeclaration); - } else { - declarationsForMediaQueries.set( - otherDecl.parent.parent, - new Set([clonedDeclaration]), - ); - } - return; - } + root.walkDecls((declaration) => { + if (/--[^\s]+/.test(declaration.prop)) { + variableDefinitions.add({ + declaration, + variable: `var(${declaration.prop})`, + definition: declaration.value, + }); + } else { + const variablesUsed = [ + ...declaration.value.matchAll(/var\(--[^\s)]+\)/gm), + ].map((match) => match.toString()); - valueReplacingInformation.add({ - declaration, - replacing: variable, - replacement: otherDecl.value, - }); - } - } + for (const variable of variablesUsed) { + variableUses.add({ + declaration, + variable, }); } - }); - - for (const { - declaration, - replacing, - replacement, - } of valueReplacingInformation) { - declaration.value = declaration.value.replaceAll(replacing, replacement); } + }); - for (const [ - atRule, - declarations, - ] of declarationsForMediaQueries.entries()) { - const equivalentRule = createRule(); - equivalentRule.selector = rule.selector; - equivalentRule.append(...declarations); + for (const definition of variableDefinitions) { + for (const use of variableUses) { + if (use.variable !== definition.variable) { + continue; + } - atRule.append(equivalentRule); - } - }); + if ( + use.declaration.parent instanceof AtRule && + use.declaration.parent.parent instanceof Rule && + definition.declaration.parent instanceof Rule && + doSelectorsIntersect( + use.declaration.parent.parent.selector, + definition.declaration.parent.selector, + ) + ) { + use.declaration.value = use.declaration.value.replaceAll( + use.variable, + definition.definition, + ); + continue; + } - root.walkDecls((decl) => { - if (/--[^\s]+/.test(decl.prop)) { - const parent = decl.parent; - decl.remove(); - if (parent) { - removeIfEmptyRecursively(parent); + if ( + use.declaration.parent instanceof Rule && + definition.declaration.parent instanceof Rule && + doSelectorsIntersect( + use.declaration.parent.selector, + definition.declaration.parent.selector, + ) + ) { + use.declaration.value = use.declaration.value.replaceAll( + use.variable, + definition.definition, + ); } } - }); + } + + // root.walkRules((rule) => { + // const declarationsForMediaQueries = new Map>(); + // const valueReplacingInformation = new Set<{ + // declaration: Declaration; + // replacing: string; + // replacement: string; + // }>(); + // + // rule.walkDecls((declaration) => { + // if () { + // /** + // * @example ['var(--width)', 'var(--length)'] + // */ + // const variablesUsed = [ + // ...declaration.value.matchAll(/var\(--[^\s)]+\)/gm), + // ].map((match) => match.toString()); + // + // root.walkDecls((otherDecl) => { + // if () { + // const variable = `var(${otherDecl.prop})`; + // if ( + // variablesUsed?.includes(variable) && + // areSelectingTheSame(declaration.parent, otherDecl.parent) + // ) { + // if ( + // otherDecl.parent?.parent instanceof AtRule && + // otherDecl.parent?.parent.name === 'media' && + // otherDecl.parent !== declaration.parent + // ) { + // const atRule = otherDecl.parent.parent; + // + // const clonedDeclaration = createDeclaration(); + // clonedDeclaration.prop = declaration.prop; + // clonedDeclaration.value = declaration.value.replaceAll( + // variable, + // otherDecl.value, + // ); + // clonedDeclaration.important = declaration.important; + // + // const declarationForAtRule = + // declarationsForMediaQueries.get(atRule); + // if (declarationForAtRule) { + // declarationForAtRule.add(clonedDeclaration); + // } else { + // declarationsForMediaQueries.set( + // otherDecl.parent.parent, + // new Set([clonedDeclaration]), + // ); + // } + // return; + // } + // + // valueReplacingInformation.add({ + // declaration, + // replacing: variable, + // replacement: otherDecl.value, + // }); + // } + // } + // }); + // } + // }); + // + // for (const { + // declaration, + // replacing, + // replacement, + // } of valueReplacingInformation) { + // declaration.value = declaration.value.replaceAll(replacing, replacement); + // } + // + // for (const [ + // atRule, + // declarations, + // ] of declarationsForMediaQueries.entries()) { + // const equivalentRule = createRule(); + // equivalentRule.selector = rule.selector; + // equivalentRule.append(...declarations); + // + // atRule.append(equivalentRule); + // } + // }); + + for (const definition of variableDefinitions) { + const parent = definition.declaration.parent; + definition.declaration.remove(); + if (parent) { + removeIfEmptyRecursively(parent); + } + } return root; }; From e0a6f6e4d36b935ed32b5a757ee5081503715332 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Tue, 26 Aug 2025 16:18:16 -0300 Subject: [PATCH 014/193] update generateRootForClasses tests --- .../generate-root-for-classes.spec.ts.snap | 14 +++----------- .../utils/tailwindcss/generate-root-for-classes.ts | 2 -- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/packages/tailwind/src/utils/tailwindcss/__snapshots__/generate-root-for-classes.spec.ts.snap b/packages/tailwind/src/utils/tailwindcss/__snapshots__/generate-root-for-classes.spec.ts.snap index d265af9741..a9cd30c198 100644 --- a/packages/tailwind/src/utils/tailwindcss/__snapshots__/generate-root-for-classes.spec.ts.snap +++ b/packages/tailwind/src/utils/tailwindcss/__snapshots__/generate-root-for-classes.spec.ts.snap @@ -3,14 +3,6 @@ exports[`tailwind's generateRootForClasses() 1`] = ` "/*! tailwindcss v4.1.12 | MIT License | https://tailwindcss.com */ @layer theme, base, components, utilities; -@layer theme { - .bg-slate-900{ - background-color: oklch(20.8% 0.042 265.755); - } - .text-red-500{ - color: oklch(63.7% 0.237 25.331); - } -} @layer base { *, ::after, ::before, ::backdrop, ::file-selector-button { box-sizing: border-box; @@ -161,14 +153,14 @@ exports[`tailwind's generateRootForClasses() 1`] = ` } @layer utilities { .bg-slate-900 { - background-color: var(--color-slate-900); + background-color: oklch(20.8% 0.042 265.755); } .text-red-500 { - color: var(--color-red-500); + color: oklch(63.7% 0.237 25.331); } .sm\\:bg-blue-300 { @media (width >= 40rem) { - background-color: var(--color-blue-300); + background-color: oklch(80.9% 0.105 251.813); } } } diff --git a/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.ts b/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.ts index 66133dd934..8665ee27b0 100644 --- a/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.ts +++ b/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.ts @@ -66,9 +66,7 @@ export async function generateRootForClasses( }); const css = compiler.build(classes); const root = parse(css); - console.log(root.toString()); resolveAllCSSVariables(root); - console.log(root.toString()); return root; } From 8df61d651177516002c1049557da8ad2d01f99d5 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Wed, 27 Aug 2025 15:14:43 -0300 Subject: [PATCH 015/193] WIP sanitize declarations dealing with oklch --- .../utils/css/sanitize-declarations.spec.ts | 151 ++++++++++++++++++ .../src/utils/css/sanitize-declarations.ts | 55 ++++++- 2 files changed, 200 insertions(+), 6 deletions(-) create mode 100644 packages/tailwind/src/utils/css/sanitize-declarations.spec.ts diff --git a/packages/tailwind/src/utils/css/sanitize-declarations.spec.ts b/packages/tailwind/src/utils/css/sanitize-declarations.spec.ts new file mode 100644 index 0000000000..6399e97a70 --- /dev/null +++ b/packages/tailwind/src/utils/css/sanitize-declarations.spec.ts @@ -0,0 +1,151 @@ +import postcss from 'postcss'; +import { sanitizeDeclarations } from './sanitize-declarations'; + +describe('sanitizeDeclarations', () => { + describe('oklch to rgb conversion', () => { + it('converts oklch to rgb without alpha', () => { + const css = postcss.parse('div { color: oklch(50 0.2 180); }'); + const result = sanitizeDeclarations(css); + expect(result).toBe('div { color: rgb(0.3, 0.5, 0.5, 1); }'); + }); + + it('converts oklch to rgb with alpha', () => { + const css = postcss.parse('div { color: oklch(75 0.15 90 / 0.8); }'); + const result = sanitizeDeclarations(css); + expect(result).toBe('div { color: rgb(0.75, 0.9, 0.75, 0.8); }'); + }); + + it('converts oklch with decimal values', () => { + const css = postcss.parse('div { color: oklch(62.5 0.25 270 / 0.5); }'); + const result = sanitizeDeclarations(css); + // 270 degrees = 3π/2, cos(3π/2) = 0, sin(3π/2) = -1 + // l = 0.625, c = 0.25, h = 270° + // r = 0.625 + 0.25 * 0 = 0.625 + // g = 0.625 + 0.25 * (-1) = 0.375 + // b = 0.625 + expect(result).toBe('div { color: rgb(0.625, 0.375, 0.625, 0.5); }'); + }); + + it('converts oklch with zero hue', () => { + const css = postcss.parse('div { color: oklch(50 0.2 0); }'); + const result = sanitizeDeclarations(css); + expect(result).toBe('div { color: rgb(0.7, 0.5, 0.5, 1); }'); + }); + + it('converts oklch with 360 degree hue', () => { + const css = postcss.parse('div { color: oklch(50 0.2 360); }'); + const result = sanitizeDeclarations(css); + expect(result).toBe( + 'div { color: rgb(0.7, 0.49999999999999994, 0.5, 1); }', + ); + }); + + it('converts oklch with high chroma value', () => { + const css = postcss.parse('div { color: oklch(80 0.4 45); }'); + const result = sanitizeDeclarations(css); + expect(result).toBe( + 'div { color: rgb(1.0828427124746192, 1.0828427124746192, 0.8, 1); }', + ); + }); + }); + + describe('rgba space syntax to comma syntax conversion', () => { + it('converts rgb with space syntax to comma syntax', () => { + const css = postcss.parse('div { color: rgb(255 0 128); }'); + sanitizeDeclarations(css); + expect(css.toString()).toBe('div { color: rgb(255,0,128); }'); + }); + + it('converts rgb with space syntax and alpha to comma syntax', () => { + const css = postcss.parse('div { color: rgb(255 0 128 / 0.5); }'); + sanitizeDeclarations(css); + expect(css.toString()).toBe('div { color: rgb(255,0,128,0.5); }'); + }); + + it('converts rgb with space syntax and percentage alpha', () => { + const css = postcss.parse('div { color: rgb(255 0 128 / 50%); }'); + sanitizeDeclarations(css); + expect(css.toString()).toBe('div { color: rgb(255,0,128,50%); }'); + }); + + it('converts rgb with alpha value of 1 (omits alpha)', () => { + const css = postcss.parse('div { color: rgb(255 0 128 / 1); }'); + sanitizeDeclarations(css); + expect(css.toString()).toBe('div { color: rgb(255,0,128); }'); + }); + + it('handles multiple rgb values in single declaration', () => { + const css = postcss.parse( + 'div { background: linear-gradient(rgb(255 0 0), rgb(0 255 0 / 0.8)); }', + ); + sanitizeDeclarations(css); + expect(css.toString()).toBe( + 'div { background: linear-gradient(rgb(255,0,0), rgb(0,255,0,0.8)); }', + ); + }); + + it('handles rgb values with extra whitespace', () => { + const css = postcss.parse( + 'div { color: rgb( 255 0 128 / 0.7 ); }', + ); + sanitizeDeclarations(css); + expect(css.toString()).toBe('div { color: rgb(255,0,128,0.7); }'); + }); + + it('handles zero values', () => { + const css = postcss.parse('div { color: rgb(0 0 0); }'); + sanitizeDeclarations(css); + expect(css.toString()).toBe('div { color: rgb(0,0,0); }'); + }); + + it('preserves already comma-separated rgb values', () => { + const css = postcss.parse('div { color: rgb(255, 0, 128); }'); + sanitizeDeclarations(css); + expect(css.toString()).toBe('div { color: rgb(255, 0, 128); }'); + }); + }); + + describe('complex scenarios', () => { + it('handles multiple declarations with different rgb formats', () => { + const css = postcss.parse(` + div { + color: rgb(255 0 128 / 0.5); + background-color: rgb(0 255 0); + border-color: rgb(128, 128, 128); + } + `); + sanitizeDeclarations(css); + const result = css.toString(); + expect(result).toContain('color: rgb(255,0,128,0.5)'); + expect(result).toContain('background-color: rgb(0,255,0)'); + expect(result).toContain('border-color: rgb(128, 128, 128)'); + }); + + it('handles nested rules', () => { + const css = postcss.parse(` + @media (min-width: 768px) { + div { color: rgb(255 0 128 / 0.8); } + } + `); + sanitizeDeclarations(css); + expect(css.toString()).toContain('color: rgb(255,0,128,0.8)'); + }); + + it('processes at-rule declarations', () => { + const css = postcss.parse(` + @keyframes fade { + from { background: rgb(255 0 0 / 0); } + to { background: rgb(255 0 0 / 1); } + } + `); + sanitizeDeclarations(css); + const result = css.toString(); + expect(result).toBe(` + @keyframes fade { + from { background: rgb(255,0,0,0); } + to { background: rgb(255,0,0); } + } + `); + }); + }); +}); diff --git a/packages/tailwind/src/utils/css/sanitize-declarations.ts b/packages/tailwind/src/utils/css/sanitize-declarations.ts index 065b0fdb45..9b786ce6cd 100644 --- a/packages/tailwind/src/utils/css/sanitize-declarations.ts +++ b/packages/tailwind/src/utils/css/sanitize-declarations.ts @@ -1,5 +1,34 @@ import type { AtRule, Root, Rule } from 'postcss'; +function oklchToRgb(l: number, c: number, h: number) { + // Convert hue to radians + const hRad = (h * Math.PI) / 180; + + // Convert OKLCH to OKLAB + const a = c * Math.cos(hRad); + const b = c * Math.sin(hRad); + + // Convert OKLAB to linear RGB + const rLinear = +4.0767416621 * l - 3.3077115913 * a + 0.2309699292 * b; + const gLinear = -1.2684380046 * l + 2.6097574011 * a - 0.3413193965 * b; + const bLinear = -0.0041960863 * l - 0.7034186147 * a + 1.707614701 * b; + + // Convert linear RGB to sRGB + const toSrgb = (channel: number) => + channel <= 0.0031308 + ? 12.92 * channel + : 1.055 * channel ** (1 / 2.4) - 0.055; + + // Clamp values to [0, 1] + const clamp = (value: number) => Math.max(0, Math.min(1, value)); + + return { + r: clamp(toSrgb(rLinear)), + g: clamp(toSrgb(gLinear)), + b: clamp(toSrgb(bLinear)), + }; +} + /** * Meant to do all the things necessary, in a per-declaration basis, to have the best email client * support possible. @@ -13,13 +42,27 @@ export const sanitizeDeclarations = ( nodeContainingDeclarations.walkDecls((declaration) => { const rgbParserRegex = /rgb\(\s*(\d+)\s*(\d+)\s*(\d+)(?:\s*\/\s*([\d%.]+))?\s*\)/g; + const oklchParserRegex = + /oklch\(\s*([\d.]+)(%)?\s+([\d.]+)\s+([\d.]+)(?:\s*\/\s*([\d.]+)(%)?)?\s*\)/g; - declaration.value = declaration.value.replaceAll( - rgbParserRegex, - (_match, r, g, b, a) => { - const alpha = a === '1' || typeof a === 'undefined' ? '' : `,${a}`; + declaration.value = declaration.value + .replaceAll(rgbParserRegex, (_match, r, g, b, a) => { + const alpha = a === '1' || !a ? '' : `,${a}`; return `rgb(${r},${g},${b}${alpha})`; - }, - ); + }) + .replaceAll( + oklchParserRegex, + (_match, l, lPercentageSign: string, c, h, a, aPercentageSign) => { + console.log(l, lPercentageSign, c, h, a, aPercentageSign); + const rgb = oklchToRgb( + lPercentageSign ? Number(l) / 100 : Number(l), + Number(c), + Number(h), + ); + + const alphaString = a ? `,${a}${aPercentageSign}` : ''; + return `rgb(${rgb.r},${rgb.g},${rgb.b}${alphaString})`; + }, + ); }); }; From 06ea3e39f0f88e6c5e9f2b64e7743b5f937a3f00 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Wed, 27 Aug 2025 16:43:24 -0300 Subject: [PATCH 016/193] get oklch to rgba conversion --- .../utils/css/sanitize-declarations.spec.ts | 40 ++++---- .../src/utils/css/sanitize-declarations.ts | 93 +++++++++++++------ 2 files changed, 84 insertions(+), 49 deletions(-) diff --git a/packages/tailwind/src/utils/css/sanitize-declarations.spec.ts b/packages/tailwind/src/utils/css/sanitize-declarations.spec.ts index 6399e97a70..43e513b117 100644 --- a/packages/tailwind/src/utils/css/sanitize-declarations.spec.ts +++ b/packages/tailwind/src/utils/css/sanitize-declarations.spec.ts @@ -4,48 +4,44 @@ import { sanitizeDeclarations } from './sanitize-declarations'; describe('sanitizeDeclarations', () => { describe('oklch to rgb conversion', () => { it('converts oklch to rgb without alpha', () => { - const css = postcss.parse('div { color: oklch(50 0.2 180); }'); - const result = sanitizeDeclarations(css); - expect(result).toBe('div { color: rgb(0.3, 0.5, 0.5, 1); }'); + const css = postcss.parse('div { color: oklch(90.5% 0.2 180); }'); + sanitizeDeclarations(css); + expect(css.toString()).toBe('div { color: rgb(0,255,228.7); }'); }); it('converts oklch to rgb with alpha', () => { - const css = postcss.parse('div { color: oklch(75 0.15 90 / 0.8); }'); - const result = sanitizeDeclarations(css); - expect(result).toBe('div { color: rgb(0.75, 0.9, 0.75, 0.8); }'); + const css = postcss.parse('div { color: oklch(96.6% 0.147 107 / 80%); }'); + sanitizeDeclarations(css); + expect(css.toString()).toBe('div { color: rgb(255,250.7,125.6,0.8); }'); }); it('converts oklch with decimal values', () => { - const css = postcss.parse('div { color: oklch(62.5 0.25 270 / 0.5); }'); - const result = sanitizeDeclarations(css); + const css = postcss.parse('div { color: oklch(92.6% 0.0546 218 / 50%); }'); + sanitizeDeclarations(css); // 270 degrees = 3π/2, cos(3π/2) = 0, sin(3π/2) = -1 // l = 0.625, c = 0.25, h = 270° // r = 0.625 + 0.25 * 0 = 0.625 // g = 0.625 + 0.25 * (-1) = 0.375 // b = 0.625 - expect(result).toBe('div { color: rgb(0.625, 0.375, 0.625, 0.5); }'); + expect(css.toString()).toBe('div { color: rgb(190.5,240.2,255,0.5); }'); }); it('converts oklch with zero hue', () => { - const css = postcss.parse('div { color: oklch(50 0.2 0); }'); - const result = sanitizeDeclarations(css); - expect(result).toBe('div { color: rgb(0.7, 0.5, 0.5, 1); }'); + const css = postcss.parse('div { color: oklch(88.3% 0.102 329); }'); + sanitizeDeclarations(css); + expect(css.toString()).toBe('div { color: rgb(254.6,192.6,249.8); }'); }); it('converts oklch with 360 degree hue', () => { - const css = postcss.parse('div { color: oklch(50 0.2 360); }'); - const result = sanitizeDeclarations(css); - expect(result).toBe( - 'div { color: rgb(0.7, 0.49999999999999994, 0.5, 1); }', - ); + const css = postcss.parse('div { color: oklch(88.3% 0.102 329); }'); + sanitizeDeclarations(css); + expect(css.toString()).toBe('div { color: rgb(254.6,192.6,249.8); }'); }); it('converts oklch with high chroma value', () => { - const css = postcss.parse('div { color: oklch(80 0.4 45); }'); - const result = sanitizeDeclarations(css); - expect(result).toBe( - 'div { color: rgb(1.0828427124746192, 1.0828427124746192, 0.8, 1); }', - ); + const css = postcss.parse('div { color: oklch(69.3% 0.206 42.8); }'); + sanitizeDeclarations(css); + expect(css.toString()).toBe('div { color: rgb(255,99.8,0.1); }'); }); }); diff --git a/packages/tailwind/src/utils/css/sanitize-declarations.ts b/packages/tailwind/src/utils/css/sanitize-declarations.ts index 9b786ce6cd..9cb30d0b95 100644 --- a/packages/tailwind/src/utils/css/sanitize-declarations.ts +++ b/packages/tailwind/src/utils/css/sanitize-declarations.ts @@ -1,31 +1,69 @@ import type { AtRule, Root, Rule } from 'postcss'; -function oklchToRgb(l: number, c: number, h: number) { - // Convert hue to radians - const hRad = (h * Math.PI) / 180; +const LAB_TO_LMS = { + l: [0.3963377773761749, 0.2158037573099136], + m: [-0.1055613458156586, -0.0638541728258133], + s: [-0.0894841775298119, -1.2914855480194092], +}; +const LSM_TO_RGB = { + r: [4.0767416360759583, -3.3077115392580629, 0.2309699031821043], + g: [-1.2684379732850315, 2.6097573492876882, -0.341319376002657], + b: [-0.0041960761386756, -0.7034186179359362, 1.7076146940746117], +}; + +function lrgbToRgb(input: number) { + const absoluteNumber = Math.abs(input); + const sign = input < 0 ? -1 : 1; + + if (absoluteNumber > 0.0031308) { + return sign * (absoluteNumber ** (1 / 2.4) * 1.055 - 0.055); + } - // Convert OKLCH to OKLAB - const a = c * Math.cos(hRad); - const b = c * Math.sin(hRad); + return input * 12.92; +} + +function clamp(value: number, min: number, max: number) { + return Math.min(Math.max(value, min), max); +} + +function round(value: number, digits = 1) { + const factor = 10 ** digits; + return Math.round(value * factor) / factor; +} + +function oklchToOklab(oklch: { l: number; c: number; h: number }) { + return { + l: oklch.l, + a: oklch.c * Math.cos((oklch.h / 180) * Math.PI), + b: oklch.c * Math.sin((oklch.h / 180) * Math.PI), + }; +} - // Convert OKLAB to linear RGB - const rLinear = +4.0767416621 * l - 3.3077115913 * a + 0.2309699292 * b; - const gLinear = -1.2684380046 * l + 2.6097574011 * a - 0.3413193965 * b; - const bLinear = -0.0041960863 * l - 0.7034186147 * a + 1.707614701 * b; +/** Convert oklab to RGB */ +function oklchToRgb(oklch: { l: number; c: number; h: number }) { + const oklab = oklchToOklab(oklch); - // Convert linear RGB to sRGB - const toSrgb = (channel: number) => - channel <= 0.0031308 - ? 12.92 * channel - : 1.055 * channel ** (1 / 2.4) - 0.055; + const l = + (oklab.l + LAB_TO_LMS.l[0] * oklab.a + LAB_TO_LMS.l[1] * oklab.b) ** 3; + const m = + (oklab.l + LAB_TO_LMS.m[0] * oklab.a + LAB_TO_LMS.m[1] * oklab.b) ** 3; + const s = + (oklab.l + LAB_TO_LMS.s[0] * oklab.a + LAB_TO_LMS.s[1] * oklab.b) ** 3; - // Clamp values to [0, 1] - const clamp = (value: number) => Math.max(0, Math.min(1, value)); + const r = + 255 * + lrgbToRgb(LSM_TO_RGB.r[0] * l + LSM_TO_RGB.r[1] * m + LSM_TO_RGB.r[2] * s); + const g = + 255 * + lrgbToRgb(LSM_TO_RGB.g[0] * l + LSM_TO_RGB.g[1] * m + LSM_TO_RGB.g[2] * s); + const b = + 255 * + lrgbToRgb(LSM_TO_RGB.b[0] * l + LSM_TO_RGB.b[1] * m + LSM_TO_RGB.b[2] * s); return { - r: clamp(toSrgb(rLinear)), - g: clamp(toSrgb(gLinear)), - b: clamp(toSrgb(bLinear)), + r: round(clamp(r, 0, 255)), + g: round(clamp(g, 0, 255)), + b: round(clamp(b, 0, 255)), }; } @@ -53,14 +91,15 @@ export const sanitizeDeclarations = ( .replaceAll( oklchParserRegex, (_match, l, lPercentageSign: string, c, h, a, aPercentageSign) => { - console.log(l, lPercentageSign, c, h, a, aPercentageSign); - const rgb = oklchToRgb( - lPercentageSign ? Number(l) / 100 : Number(l), - Number(c), - Number(h), - ); + const rgb = oklchToRgb({ + l: lPercentageSign ? Number(l) / 100 : Number(l), + c: Number(c), + h: Number(h), + }); - const alphaString = a ? `,${a}${aPercentageSign}` : ''; + const alphaString = a + ? `,${aPercentageSign ? Number(a) / 100 : a}` + : ''; return `rgb(${rgb.r},${rgb.g},${rgb.b}${alphaString})`; }, ); From 4932b04b43f08d2545108fe79b56e85efc0f287d Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Wed, 27 Aug 2025 17:03:29 -0300 Subject: [PATCH 017/193] remove safelist test, since there's no safelist anymore --- packages/tailwind/src/tailwind.spec.tsx | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/packages/tailwind/src/tailwind.spec.tsx b/packages/tailwind/src/tailwind.spec.tsx index ca5d32b0d1..af387befd7 100644 --- a/packages/tailwind/src/tailwind.spec.tsx +++ b/packages/tailwind/src/tailwind.spec.tsx @@ -40,25 +40,6 @@ describe('Tailwind component', () => { expect(actualOutput).toMatchSnapshot(); }); - it('should warn about safelist not being supported', async () => { - const spy = vi.spyOn(console, 'warn'); - - const actualOutput = await render( - - - - - - , - { pretty: true }, - ); - - expect(spy).toHaveBeenCalled(); - expect(actualOutput).toMatchSnapshot(); - }); - it('should work with class manipulation done on components', async () => { const MyComponnt = (props: { className?: string; From cbbd4f4d6d0e04522b94df6b726123b5e924a9d8 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Wed, 27 Aug 2025 17:03:35 -0300 Subject: [PATCH 018/193] omit content from tailwind config --- packages/tailwind/src/tailwind.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tailwind/src/tailwind.tsx b/packages/tailwind/src/tailwind.tsx index 5c2482cf7e..5e8eb41c69 100644 --- a/packages/tailwind/src/tailwind.tsx +++ b/packages/tailwind/src/tailwind.tsx @@ -6,7 +6,7 @@ import { removeRuleDuplicatesFromRoot } from './utils/css/remove-rule-duplicates import { mapReactTree } from './utils/react/map-react-tree'; import { cloneElementWithInlinedStyles } from './utils/tailwindcss/clone-element-with-inlined-styles'; -export type TailwindConfig = Config; +export type TailwindConfig = Omit; export interface TailwindProps { children: React.ReactNode; From 32b8fddd80b943a6a684d50649c83ecce0f50b44 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Wed, 27 Aug 2025 17:18:07 -0300 Subject: [PATCH 019/193] use a snapshot for first test --- .../src/__snapshots__/tailwind.spec.tsx.snap | 2 ++ packages/tailwind/src/tailwind.spec.tsx | 16 +++++++--------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap b/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap index fc2d4fa1a1..2575ec41b4 100644 --- a/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap +++ b/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap @@ -28,6 +28,8 @@ exports[`Tailwind component > should preserve mso styles 1`] = `" should recognize custom responsive screen 1`] = `"
Test
Test
"`; +exports[`Tailwind component > should render children with inline Tailwind styles 1`] = `"
"`; + exports[`Tailwind component > should warn about safelist not being supported 1`] = ` " diff --git a/packages/tailwind/src/tailwind.spec.tsx b/packages/tailwind/src/tailwind.spec.tsx index af387befd7..2da28364c7 100644 --- a/packages/tailwind/src/tailwind.spec.tsx +++ b/packages/tailwind/src/tailwind.spec.tsx @@ -99,16 +99,14 @@ describe('Tailwind component', () => { expect(actualOutput).toMatchSnapshot(); }); - describe('Inline styles', () => { - it('should render children with inline Tailwind styles', async () => { - const actualOutput = await render( - -
- , - ); + it('should render children with inline Tailwind styles', async () => { + const actualOutput = await render( + +
+ , + ); - expect(actualOutput).not.toBeNull(); - }); + expect(actualOutput).toMatchSnapshot(); }); // test("with React context and custom components", () => { From e1a7164b240197efc6138c7adc2cfed79a11eeea Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Thu, 28 Aug 2025 09:14:57 -0300 Subject: [PATCH 020/193] fix layer at rules being ignored when inlining --- .../src/utils/css/sanitize-non-inlinable-classes.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/tailwind/src/utils/css/sanitize-non-inlinable-classes.ts b/packages/tailwind/src/utils/css/sanitize-non-inlinable-classes.ts index c2d3077049..e45c117668 100644 --- a/packages/tailwind/src/utils/css/sanitize-non-inlinable-classes.ts +++ b/packages/tailwind/src/utils/css/sanitize-non-inlinable-classes.ts @@ -9,8 +9,7 @@ import { sanitizeClassName } from '../compatibility/sanitize-class-name'; * * What it does is: * 1. Converts all declarations in all rules into being important ones - * 2. Sanitizes all the selectors of all rules in the media queries - * 3. Sanitizes all the selectors of all rules using pseudo classes + * 2. Sanitizes all the selectors of all non-inlinable rules * 4. Merges at rules that have equivalent parameters */ export const sanitizeNonInlinableClasses = (root: Root) => { @@ -21,6 +20,8 @@ export const sanitizeNonInlinableClasses = (root: Root) => { // Process rules within at-rules (like media queries) root.walkAtRules((atRule) => { + if (atRule.name !== 'media') return; + const sanitizedAtRule = atRule.clone(); sanitizedAtRule.walkRules((rule) => { @@ -51,7 +52,12 @@ export const sanitizeNonInlinableClasses = (root: Root) => { // Process top-level rules root.walkRules((rule) => { - if (rule.parent && rule.parent.type !== 'root') return; + if ( + rule.parent && + rule.parent instanceof AtRule && + rule.parent.name === 'media' + ) + return; const selectorRoot = selectorProcessor.astSync(rule.selector); From 21a40cc8a7c1889e1d248b4b4c84d6d1446d494b Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Thu, 28 Aug 2025 09:15:05 -0300 Subject: [PATCH 021/193] fix config not being loaded when generating styles --- .../src/utils/tailwindcss/generate-root-for-classes.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.ts b/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.ts index 8665ee27b0..8f1eb9bf39 100644 --- a/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.ts +++ b/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.ts @@ -6,7 +6,9 @@ import preflightCss from './tailwind-stylesheets/preflight'; import themeCss from './tailwind-stylesheets/theme'; import utilitiesCss from './tailwind-stylesheets/utilities'; -const baseCss = `@import "tailwindcss";`; +const baseCss = `@import "tailwindcss"; +@config; +`; export async function generateRootForClasses( classes: string[], From 90a4c6d49b16f4809e74c777a17c9adbdce85100 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Thu, 28 Aug 2025 09:52:36 -0300 Subject: [PATCH 022/193] disable preflight --- .../tailwindcss/generate-root-for-classes.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.ts b/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.ts index 8f1eb9bf39..383eebd1bf 100644 --- a/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.ts +++ b/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.ts @@ -6,7 +6,10 @@ import preflightCss from './tailwind-stylesheets/preflight'; import themeCss from './tailwind-stylesheets/theme'; import utilitiesCss from './tailwind-stylesheets/utilities'; -const baseCss = `@import "tailwindcss"; +const baseCss = ` +@layer theme, base, components, utilities; +@import "tailwindcss/theme.css" layer(theme); +@import "tailwindcss/utilities.css" layer(utilities); @config; `; @@ -37,26 +40,26 @@ export async function generateRootForClasses( }; } - if (id === 'tailwindcss/preflight') { + if (id === 'tailwindcss/preflight.css') { return { base, - path: 'tailwindcss/preflight.css', + path: id, content: preflightCss, }; } - if (id === 'tailwindcss/theme') { + if (id === 'tailwindcss/theme.css') { return { base, - path: 'tailwindcss/theme.css', + path: id, content: themeCss, }; } - if (id === 'tailwindcss/utilities') { + if (id === 'tailwindcss/utilities.css') { return { base, - path: 'tailwindcss/utilities.css', + path: id, content: utilitiesCss, }; } From aa161f39ec8cedaa12f2fc3a6e544227701d270d Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Thu, 28 Aug 2025 09:53:03 -0300 Subject: [PATCH 023/193] update sanitize non inlinable classes to work with the new media query placements --- .../css/sanitize-non-inlinable-classes.ts | 59 ++++--------------- 1 file changed, 12 insertions(+), 47 deletions(-) diff --git a/packages/tailwind/src/utils/css/sanitize-non-inlinable-classes.ts b/packages/tailwind/src/utils/css/sanitize-non-inlinable-classes.ts index e45c117668..8e737e18f2 100644 --- a/packages/tailwind/src/utils/css/sanitize-non-inlinable-classes.ts +++ b/packages/tailwind/src/utils/css/sanitize-non-inlinable-classes.ts @@ -1,4 +1,4 @@ -import { AtRule, type Root, type Rule } from 'postcss'; +import { AtRule, type Root, Rule } from 'postcss'; import selectorParser from 'postcss-selector-parser'; import { sanitizeClassName } from '../compatibility/sanitize-class-name'; @@ -18,62 +18,27 @@ export const sanitizeNonInlinableClasses = (root: Root) => { const selectorProcessor = selectorParser(); - // Process rules within at-rules (like media queries) - root.walkAtRules((atRule) => { - if (atRule.name !== 'media') return; - - const sanitizedAtRule = atRule.clone(); - - sanitizedAtRule.walkRules((rule) => { - const selectorRoot = selectorProcessor.astSync(rule.selector); - selectorRoot.walkClasses((className) => { - nonInlinableClasses.push(className.value); - sanitizeSelectorClassName(className); - }); - - const processedRule = rule.clone({ selector: selectorRoot.toString() }); - processedRule.walkDecls((decl) => { - decl.important = true; - }); - - rule.replaceWith(processedRule); - }); - - const equivalentRule = sanitizedRules.find( - (r) => r instanceof AtRule && r.params === sanitizedAtRule.params, - ); - - if (equivalentRule) { - equivalentRule.append(sanitizedAtRule.nodes); - } else { - sanitizedRules.push(sanitizedAtRule); - } - }); - - // Process top-level rules root.walkRules((rule) => { - if ( - rule.parent && - rule.parent instanceof AtRule && - rule.parent.name === 'media' - ) - return; - const selectorRoot = selectorProcessor.astSync(rule.selector); + let hasAtRuleInside = false; + rule.walkAtRules(() => { + hasAtRuleInside = true; + }); + let hasPseudoSelector = false as boolean; selectorRoot.walkPseudos(() => { hasPseudoSelector = true; }); - if (!hasPseudoSelector) return; + const isInlinable = !hasAtRuleInside && !hasPseudoSelector; - selectorRoot.walkClasses((className) => { - nonInlinableClasses.push(className.value); - sanitizeSelectorClassName(className); - }); + if (isInlinable) { + selectorRoot.walkClasses((className) => { + nonInlinableClasses.push(className.value); + sanitizeSelectorClassName(className); + }); - if (hasPseudoSelector) { const processedRule = rule.clone({ selector: selectorRoot.toString() }); processedRule.walkDecls((decl) => { decl.important = true; From e4c74759f71f1ecec7b31dac5a36d49895ab401a Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Thu, 28 Aug 2025 11:13:41 -0300 Subject: [PATCH 024/193] update style inlining with new checks for a rule being inlinable --- .../src/utils/css/is-rule-inlinable.ts | 24 ++++++++ .../src/utils/css/make-inline-styles-for.ts | 56 ++++++------------- .../css/sanitize-non-inlinable-classes.ts | 25 ++------- 3 files changed, 48 insertions(+), 57 deletions(-) create mode 100644 packages/tailwind/src/utils/css/is-rule-inlinable.ts diff --git a/packages/tailwind/src/utils/css/is-rule-inlinable.ts b/packages/tailwind/src/utils/css/is-rule-inlinable.ts new file mode 100644 index 0000000000..636adcbfc3 --- /dev/null +++ b/packages/tailwind/src/utils/css/is-rule-inlinable.ts @@ -0,0 +1,24 @@ +import type { Rule } from 'postcss'; +import selectorParser from 'postcss-selector-parser'; + +const selectorProcessor = selectorParser(); + +export function parseSelectors(rule: Rule): selectorParser.Root { + return selectorProcessor.astSync(rule.selector); +} + +export function isRuleInlinable(rule: Rule): boolean { + const selectorRoot = parseSelectors(rule); + + let hasAtRuleInside = false; + rule.walkAtRules(() => { + hasAtRuleInside = true; + }); + + let hasPseudoSelector = false as boolean; + selectorRoot.walkPseudos(() => { + hasPseudoSelector = true; + }); + + return !hasAtRuleInside && !hasPseudoSelector; +} diff --git a/packages/tailwind/src/utils/css/make-inline-styles-for.ts b/packages/tailwind/src/utils/css/make-inline-styles-for.ts index 2659e698e2..a9ef4a7fe4 100644 --- a/packages/tailwind/src/utils/css/make-inline-styles-for.ts +++ b/packages/tailwind/src/utils/css/make-inline-styles-for.ts @@ -1,30 +1,8 @@ -import { AtRule, type Root, type Rule } from 'postcss'; +import type { Root } from 'postcss'; import selectorParser from 'postcss-selector-parser'; import { convertCssPropertyToReactProperty } from '../compatibility/convert-css-property-to-react-property'; import { unescapeClass } from '../compatibility/unescape-class'; - -const walkInlinableRules = (root: Root, callback: (rule: Rule) => void) => { - root.walkRules((rule) => { - // Only ignore AtRules that are not @layer, but can be @layer base - if ( - rule.parent instanceof AtRule && - (rule.parent.name !== 'layer' || rule.parent.params === 'base') - ) { - return; - } - - selectorParser((selector) => { - let hasPseudoSelectors = false as boolean; - selector.walkPseudos(() => { - hasPseudoSelectors = true; - }); - - if (!hasPseudoSelectors) { - callback(rule); - } - }).processSync(rule.selector); - }); -}; +import { isRuleInlinable } from './is-rule-inlinable'; export function makeInlineStylesFor( className: string, @@ -35,22 +13,24 @@ export function makeInlineStylesFor( let residualClasses = [...classes]; const styles: Record = {}; - walkInlinableRules(tailwindStylesRoot, (rule) => { - const classesOnSelector: string[] = []; - selectorParser((selector) => { - selector.walkClasses((v) => { - classesOnSelector.push(unescapeClass(v.value)); + tailwindStylesRoot.walkRules((rule) => { + if (isRuleInlinable(rule)) { + const classesOnSelector: string[] = []; + selectorParser((selector) => { + selector.walkClasses((v) => { + classesOnSelector.push(unescapeClass(v.value)); + }); + }).processSync(rule.selector); + + residualClasses = residualClasses.filter((singleClass) => { + return !classesOnSelector.includes(singleClass); }); - }).processSync(rule.selector); - - residualClasses = residualClasses.filter((singleClass) => { - return !classesOnSelector.includes(singleClass); - }); - rule.walkDecls((declaration) => { - styles[convertCssPropertyToReactProperty(declaration.prop)] = - declaration.value + (declaration.important ? '!important' : ''); - }); + rule.walkDecls((declaration) => { + styles[convertCssPropertyToReactProperty(declaration.prop)] = + declaration.value + (declaration.important ? '!important' : ''); + }); + } }); return { diff --git a/packages/tailwind/src/utils/css/sanitize-non-inlinable-classes.ts b/packages/tailwind/src/utils/css/sanitize-non-inlinable-classes.ts index 8e737e18f2..3607ce5150 100644 --- a/packages/tailwind/src/utils/css/sanitize-non-inlinable-classes.ts +++ b/packages/tailwind/src/utils/css/sanitize-non-inlinable-classes.ts @@ -1,6 +1,7 @@ -import { AtRule, type Root, Rule } from 'postcss'; -import selectorParser from 'postcss-selector-parser'; +import type { AtRule, Root, Rule } from 'postcss'; +import type selectorParser from 'postcss-selector-parser'; import { sanitizeClassName } from '../compatibility/sanitize-class-name'; +import { isRuleInlinable, parseSelectors } from './is-rule-inlinable'; /** * This function goes through a few steps to ensure the best email client support and @@ -10,30 +11,16 @@ import { sanitizeClassName } from '../compatibility/sanitize-class-name'; * What it does is: * 1. Converts all declarations in all rules into being important ones * 2. Sanitizes all the selectors of all non-inlinable rules - * 4. Merges at rules that have equivalent parameters + * 3. Merges at rules that have equivalent parameters */ export const sanitizeNonInlinableClasses = (root: Root) => { const sanitizedRules: (Rule | AtRule)[] = []; const nonInlinableClasses: string[] = []; - const selectorProcessor = selectorParser(); - root.walkRules((rule) => { - const selectorRoot = selectorProcessor.astSync(rule.selector); - - let hasAtRuleInside = false; - rule.walkAtRules(() => { - hasAtRuleInside = true; - }); - - let hasPseudoSelector = false as boolean; - selectorRoot.walkPseudos(() => { - hasPseudoSelector = true; - }); - - const isInlinable = !hasAtRuleInside && !hasPseudoSelector; + if (!isRuleInlinable(rule)) { + const selectorRoot = parseSelectors(rule); - if (isInlinable) { selectorRoot.walkClasses((className) => { nonInlinableClasses.push(className.value); sanitizeSelectorClassName(className); From a8c031188345847afea62ec707f642836b055024 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Thu, 28 Aug 2025 11:13:55 -0300 Subject: [PATCH 025/193] update plugin usage in tests to v4's --- packages/tailwind/src/tailwind.spec.tsx | 39 +++++++++---------------- 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/packages/tailwind/src/tailwind.spec.tsx b/packages/tailwind/src/tailwind.spec.tsx index 2da28364c7..aa5bb380a5 100644 --- a/packages/tailwind/src/tailwind.spec.tsx +++ b/packages/tailwind/src/tailwind.spec.tsx @@ -7,7 +7,6 @@ import { Link } from '@react-email/link'; import { render } from '@react-email/render'; import { ResponsiveColumn, ResponsiveRow } from '@responsive-email/react-email'; import React from 'react'; -import { vi } from 'vitest'; import type { TailwindConfig } from '.'; import { Tailwind } from '.'; @@ -616,21 +615,23 @@ describe('Custom theme config', () => { }); describe('Custom plugins config', () => { - it('should be able to use custom plugins', async () => { - const config: TailwindConfig = { - plugins: [ - ({ addUtilities }: any) => { - const newUtilities = { + const config = { + plugins: [ + { + handler: (api) => { + api.addUtilities({ '.border-custom': { - border: '2px solid', + styles: { + border: '2px solid', + }, }, - }; - - addUtilities(newUtilities); + }); }, - ], - }; + }, + ], + } satisfies TailwindConfig; + it('should be able to use custom plugins', async () => { const actualOutput = await render(
@@ -641,20 +642,6 @@ describe('Custom plugins config', () => { }); it('should be able to use custom plugins with responsive styles', async () => { - const config: TailwindConfig = { - plugins: [ - ({ addUtilities }: any) => { - const newUtilities = { - '.border-custom': { - border: '2px solid', - }, - }; - - addUtilities(newUtilities); - }, - ], - }; - const actualOutput = await render( From b6c33a3061b5e82a1dd1f51d01e0663be4b12864 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Thu, 28 Aug 2025 11:13:58 -0300 Subject: [PATCH 026/193] update snap --- packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap b/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap index 2575ec41b4..80888cc338 100644 --- a/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap +++ b/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap @@ -2,7 +2,7 @@ exports[`Custom plugins config > should be able to use custom plugins 1`] = `"
"`; -exports[`Custom plugins config > should be able to use custom plugins with responsive styles 1`] = `"
"`; +exports[`Custom plugins config > should be able to use custom plugins with responsive styles 1`] = `"
"`; exports[`Custom theme config > should be able to use custom border radius 1`] = `"
"`; From 1411495e7dbca2694bc31c9a4a5ec91f70cdbb9e Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Thu, 28 Aug 2025 12:06:14 -0300 Subject: [PATCH 027/193] separate block-inline shorthand properties into respective left-right-top-bottom properties --- .../utils/css/sanitize-declarations.spec.ts | 147 +++++++++++++++++- .../src/utils/css/sanitize-declarations.ts | 97 +++++++++++- 2 files changed, 234 insertions(+), 10 deletions(-) diff --git a/packages/tailwind/src/utils/css/sanitize-declarations.spec.ts b/packages/tailwind/src/utils/css/sanitize-declarations.spec.ts index 43e513b117..7edf13b37f 100644 --- a/packages/tailwind/src/utils/css/sanitize-declarations.spec.ts +++ b/packages/tailwind/src/utils/css/sanitize-declarations.spec.ts @@ -16,13 +16,10 @@ describe('sanitizeDeclarations', () => { }); it('converts oklch with decimal values', () => { - const css = postcss.parse('div { color: oklch(92.6% 0.0546 218 / 50%); }'); + const css = postcss.parse( + 'div { color: oklch(92.6% 0.0546 218 / 50%); }', + ); sanitizeDeclarations(css); - // 270 degrees = 3π/2, cos(3π/2) = 0, sin(3π/2) = -1 - // l = 0.625, c = 0.25, h = 270° - // r = 0.625 + 0.25 * 0 = 0.625 - // g = 0.625 + 0.25 * (-1) = 0.375 - // b = 0.625 expect(css.toString()).toBe('div { color: rgb(190.5,240.2,255,0.5); }'); }); @@ -101,6 +98,144 @@ describe('sanitizeDeclarations', () => { }); }); + describe('hex to rgb conversion', () => { + it('converts 3-digit hex without alpha', () => { + const css = postcss.parse('div { color: #f0a; }'); + sanitizeDeclarations(css); + expect(css.toString()).toBe('div { color: rgb(255,0,170); }'); + }); + + it('converts 4-digit hex with alpha', () => { + const css = postcss.parse('div { color: #f0a8; }'); + sanitizeDeclarations(css); + expect(css.toString()).toBe( + 'div { color: rgb(255,0,170,0.5333333333333333); }', + ); + }); + + it('converts 6-digit hex without alpha', () => { + const css = postcss.parse('div { color: #ff00aa; }'); + sanitizeDeclarations(css); + expect(css.toString()).toBe('div { color: rgb(255,0,170); }'); + }); + + it('converts 8-digit hex with alpha', () => { + const css = postcss.parse('div { color: #ff00aa80; }'); + sanitizeDeclarations(css); + expect(css.toString()).toBe( + 'div { color: rgb(255,0,170,0.5019607843137255); }', + ); + }); + + it('converts black hex color', () => { + const css = postcss.parse('div { color: #000; }'); + sanitizeDeclarations(css); + expect(css.toString()).toBe('div { color: rgb(0,0,0); }'); + }); + + it('converts white hex color', () => { + const css = postcss.parse('div { color: #fff; }'); + sanitizeDeclarations(css); + expect(css.toString()).toBe('div { color: rgb(255,255,255); }'); + }); + + it('converts red hex color', () => { + const css = postcss.parse('div { color: #ff0000; }'); + sanitizeDeclarations(css); + expect(css.toString()).toBe('div { color: rgb(255,0,0); }'); + }); + + it('converts green hex color', () => { + const css = postcss.parse('div { color: #00ff00; }'); + sanitizeDeclarations(css); + expect(css.toString()).toBe('div { color: rgb(0,255,0); }'); + }); + + it('converts blue hex color', () => { + const css = postcss.parse('div { color: #0000ff; }'); + sanitizeDeclarations(css); + expect(css.toString()).toBe('div { color: rgb(0,0,255); }'); + }); + + it('converts hex with lowercase letters', () => { + const css = postcss.parse('div { color: #abcdef; }'); + sanitizeDeclarations(css); + expect(css.toString()).toBe('div { color: rgb(171,205,239); }'); + }); + + it('converts hex with uppercase letters', () => { + const css = postcss.parse('div { color: #ABCDEF; }'); + sanitizeDeclarations(css); + expect(css.toString()).toBe('div { color: rgb(171,205,239); }'); + }); + + it('converts hex with mixed case letters', () => { + const css = postcss.parse('div { color: #AbCdEf; }'); + sanitizeDeclarations(css); + expect(css.toString()).toBe('div { color: rgb(171,205,239); }'); + }); + + it('converts hex with full transparency', () => { + const css = postcss.parse('div { color: #ff000000; }'); + sanitizeDeclarations(css); + expect(css.toString()).toBe('div { color: rgb(255,0,0,0); }'); + }); + + it('converts hex with full opacity', () => { + const css = postcss.parse('div { color: #ff0000ff; }'); + sanitizeDeclarations(css); + expect(css.toString()).toBe('div { color: rgb(255,0,0,1); }'); + }); + + it('handles multiple hex colors in single declaration', () => { + const css = postcss.parse( + 'div { background: linear-gradient(#ff0000, #00ff00, #0000ff); }', + ); + sanitizeDeclarations(css); + expect(css.toString()).toBe( + 'div { background: linear-gradient(rgb(255,0,0), rgb(0,255,0), rgb(0,0,255)); }', + ); + }); + + it('handles hex colors mixed with other color formats', () => { + const css = postcss.parse( + 'div { background: linear-gradient(#ff0000, rgb(0 255 0), oklch(50% 0.2 240)); }', + ); + sanitizeDeclarations(css); + const result = css.toString(); + expect(result).toContain('rgb(255,0,0)'); + expect(result).toContain('rgb(0,255,0)'); + expect(result).toContain('rgb(0,'); + }); + + it('preserves non-hex hash values', () => { + const css = postcss.parse( + 'div { content: "Visit our site at example.com#section"; }', + ); + sanitizeDeclarations(css); + expect(css.toString()).toBe( + 'div { content: "Visit our site at example.com#section"; }', + ); + }); + + it('converts multiple hex values in different properties', () => { + const css = postcss.parse(` + div { + color: #ff0000; + background-color: #00ff00; + border-color: #0000ff; + box-shadow: 0 0 10px #333; + } + `); + sanitizeDeclarations(css); + const result = css.toString(); + expect(result).toContain('color: rgb(255,0,0)'); + expect(result).toContain('background-color: rgb(0,255,0)'); + expect(result).toContain('border-color: rgb(0,0,255)'); + expect(result).toContain('box-shadow: 0 0 10px rgb(51,51,51)'); + }); + }); + describe('complex scenarios', () => { it('handles multiple declarations with different rgb formats', () => { const css = postcss.parse(` diff --git a/packages/tailwind/src/utils/css/sanitize-declarations.ts b/packages/tailwind/src/utils/css/sanitize-declarations.ts index 9cb30d0b95..af12f5a68a 100644 --- a/packages/tailwind/src/utils/css/sanitize-declarations.ts +++ b/packages/tailwind/src/utils/css/sanitize-declarations.ts @@ -1,4 +1,9 @@ -import type { AtRule, Root, Rule } from 'postcss'; +import { + type AtRule, + decl as createDeclaration, + type Root, + type Rule, +} from 'postcss'; const LAB_TO_LMS = { l: [0.3963377773761749, 0.2158037573099136], @@ -71,8 +76,14 @@ function oklchToRgb(oklch: { l: number; c: number; h: number }) { * Meant to do all the things necessary, in a per-declaration basis, to have the best email client * support possible. * - * Currently it only converts all `rgb` declaration values that use a space-based syntax - * into a command based one since the space-based isn't well supported. + * Here's the transformations it does so far: + * - convert all `rgb` with space-based syntax into a comma based one; + * - convert all `oklch` values into `rgb`; + * - convert all hex values into `rgb`; + * - convert `padding-inline` into `padding-left` and `padding-right`; + * - convert `padding-block` into `padding-top` and `padding-bottom`; + * - convert `margin-inline` into `margin-left` and `margin-right`; + * - convert `margin-block` into `margin-top` and `margin-bottom`. */ export const sanitizeDeclarations = ( nodeContainingDeclarations: Rule | AtRule | Root, @@ -82,6 +93,7 @@ export const sanitizeDeclarations = ( /rgb\(\s*(\d+)\s*(\d+)\s*(\d+)(?:\s*\/\s*([\d%.]+))?\s*\)/g; const oklchParserRegex = /oklch\(\s*([\d.]+)(%)?\s+([\d.]+)\s+([\d.]+)(?:\s*\/\s*([\d.]+)(%)?)?\s*\)/g; + const hexParserRegex = /#([a-fA-F0-9]{3,8})/g; declaration.value = declaration.value .replaceAll(rgbParserRegex, (_match, r, g, b, a) => { @@ -102,6 +114,83 @@ export const sanitizeDeclarations = ( : ''; return `rgb(${rgb.r},${rgb.g},${rgb.b}${alphaString})`; }, - ); + ) + .replaceAll(hexParserRegex, (_match, hex: string) => { + if (hex.length === 3) { + const r = Number.parseInt(hex.charAt(0) + hex.charAt(0), 16); + const g = Number.parseInt(hex.charAt(1) + hex.charAt(1), 16); + const b = Number.parseInt(hex.charAt(2) + hex.charAt(2), 16); + return `rgb(${r},${g},${b})`; + } + if (hex.length === 4) { + const r = Number.parseInt(hex.charAt(0) + hex.charAt(0), 16); + const g = Number.parseInt(hex.charAt(1) + hex.charAt(1), 16); + const b = Number.parseInt(hex.charAt(2) + hex.charAt(2), 16); + const a = Number.parseInt(hex.charAt(3) + hex.charAt(3), 16) / 255; + return `rgb(${r},${g},${b},${a})`; + } + if (hex.length === 5) { + const r = Number.parseInt(hex.slice(0, 2), 16); + const g = Number.parseInt(hex.charAt(2) + hex.charAt(2), 16); + const b = Number.parseInt(hex.charAt(3) + hex.charAt(3), 16); + const a = Number.parseInt(hex.charAt(4) + hex.charAt(4), 16) / 255; + return `rgb(${r},${g},${b},${a})`; + } + if (hex.length === 6) { + const r = Number.parseInt(hex.slice(0, 2), 16); + const g = Number.parseInt(hex.slice(2, 4), 16); + const b = Number.parseInt(hex.slice(4, 6), 16); + return `rgb(${r},${g},${b})`; + } + if (hex.length === 7) { + const r = Number.parseInt(hex.slice(0, 2), 16); + const g = Number.parseInt(hex.slice(2, 4), 16); + const b = Number.parseInt(hex.slice(4, 6), 16); + const a = Number.parseInt(hex.charAt(6) + hex.charAt(6), 16) / 255; + return `rgb(${r},${g},${b},${a})`; + } + const r = Number.parseInt(hex.slice(0, 2), 16); + const g = Number.parseInt(hex.slice(2, 4), 16); + const b = Number.parseInt(hex.slice(4, 6), 16); + const a = Number.parseInt(hex.slice(6, 8), 16) / 255; + return `rgb(${r},${g},${b},${a})`; + }); + + if (declaration.prop === 'padding-inline') { + declaration.prop = 'padding-left'; + + const paddingRight = createDeclaration({ + prop: 'padding-right', + value: declaration.value, + }); + declaration.parent?.insertAfter(declaration, paddingRight); + } + if (declaration.prop === 'padding-block') { + declaration.prop = 'padding-top'; + + const paddingBottom = createDeclaration({ + prop: 'padding-bottom', + value: declaration.value, + }); + declaration.parent?.insertAfter(declaration, paddingBottom); + } + if (declaration.prop === 'margin-inline') { + declaration.prop = 'margin-left'; + + const marginRight = createDeclaration({ + prop: 'margin-right', + value: declaration.value, + }); + declaration.parent?.insertAfter(declaration, marginRight); + } + if (declaration.prop === 'margin-block') { + declaration.prop = 'margin-top'; + + const marginBottom = createDeclaration({ + prop: 'margin-bottom', + value: declaration.value, + }); + declaration.parent?.insertAfter(declaration, marginBottom); + } }); }; From 591b333d57bb5bfd91ee4b0614fc2e59961a4246 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Thu, 28 Aug 2025 12:06:26 -0300 Subject: [PATCH 028/193] update snaps, use pretty on all comparisons --- .../src/__snapshots__/tailwind.spec.tsx.snap | 118 ++++++++++++++++-- packages/tailwind/src/tailwind.spec.tsx | 31 +++-- 2 files changed, 122 insertions(+), 27 deletions(-) diff --git a/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap b/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap index 80888cc338..825ca6d421 100644 --- a/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap +++ b/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap @@ -1,18 +1,70 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`Custom plugins config > should be able to use custom plugins 1`] = `"
"`; +exports[`Custom plugins config > should be able to use custom plugins 1`] = ` +" + +
+ +" +`; -exports[`Custom plugins config > should be able to use custom plugins with responsive styles 1`] = `"
"`; +exports[`Custom plugins config > should be able to use custom plugins with responsive styles 1`] = ` +" + + + + + + +
+ + + +" +`; -exports[`Custom theme config > should be able to use custom border radius 1`] = `"
"`; +exports[`Custom theme config > should be able to use custom border radius 1`] = ` +" + +
+ +" +`; -exports[`Custom theme config > should be able to use custom colors 1`] = `"
"`; +exports[`Custom theme config > should be able to use custom colors 1`] = ` +" + +
+ +" +`; -exports[`Custom theme config > should be able to use custom fonts 1`] = `"
"`; +exports[`Custom theme config > should be able to use custom fonts 1`] = ` +" + +
+
+ +" +`; -exports[`Custom theme config > should be able to use custom spacing 1`] = `"
"`; +exports[`Custom theme config > should be able to use custom spacing 1`] = ` +" + +
+ +" +`; -exports[`Custom theme config > should be able to use custom text alignment 1`] = `"
"`; +exports[`Custom theme config > should be able to use custom text alignment 1`] = ` +" + +
+ +" +`; exports[`Tailwind component > - -" -`; - -exports[`Tailwind component > should work properly with 'no-underline' 1`] = `"

or copy and paste this URL into your browser: https://react.email

or copy and paste this URL into your browser: https://react.email

"`; +exports[`Tailwind component > should work properly with 'no-underline' 1`] = `"

or copy and paste this URL into your browser: https://react.email

or copy and paste this URL into your browser: https://react.email

"`; -exports[`Tailwind component > should work with Heading component 1`] = `"Hello

My testing heading

friends"`; +exports[`Tailwind component > should work with Heading component 1`] = `"Hello

My testing heading

friends"`; exports[`Tailwind component > should work with blocklist 1`] = ` " @@ -129,7 +47,7 @@ exports[`Tailwind component > should work with blocklist 1`] = ` @@ -144,7 +62,7 @@ exports[`Tailwind component > should work with calc() with + sign 1`] = `
should work with calc() with + sign 1`] = ` " `; -exports[`Tailwind component > should work with class manipulation done on components 1`] = `"
"`; - -exports[`Tailwind component > should work with components that return children 1`] = `"
Hello world

React Email

"`; +exports[`Tailwind component > should work with components that return children 1`] = `"
Hello world

React Email

"`; -exports[`Tailwind component > should work with components that use React.forwardRef 1`] = `"
Hello world

React Email

"`; +exports[`Tailwind component > should work with components that use React.forwardRef 1`] = `"
Hello world

React Email

"`; -exports[`Tailwind component > should work with custom components with fragment at the root 1`] = `"
Hello world

React Email

React Email

"`; +exports[`Tailwind component > should work with custom components with fragment at the root 1`] = `"
Hello world

React Email

React Email

"`; exports[`Tailwind component > with custom plugins config > should be able to use custom plugins 1`] = ` " @@ -249,23 +165,6 @@ exports[`Tailwind component > with non-inlinable styles > should add css to with non-inlinable styles > should not have duplicate media queries 1`] = ` -" - - - - - - - -
- - -" -`; - exports[`Tailwind component > with non-inlinable styles > should persist existing elements 1`] = ` " @@ -317,7 +216,7 @@ exports[`Tailwind component > with non-inlinable styles > should work with arbit @@ -330,7 +229,7 @@ exports[`Tailwind component > with non-inlinable styles > should work with arbit " `; -exports[`Tailwind component > with non-inlinable styles > should work with arbitrarily deep (in the React tree) elements 2`] = `"
"`; +exports[`Tailwind component > with non-inlinable styles > should work with arbitrarily deep (in the React tree) elements 2`] = `"
"`; exports[`Tailwind component > with non-inlinable styles > should work with relatively complex media query utilities 1`] = ` " @@ -348,134 +247,3 @@ exports[`Tailwind component > with non-inlinable styles > should work with relat " `; - -exports[`non-inlinable styles > should add css to and keep class names 1`] = ` -" - - - - - - -
- - - -" -`; - -exports[`non-inlinable styles > should not have duplicate media queries 1`] = ` -" - - - - - - - -
- - -" -`; - -exports[`non-inlinable styles > should persist existing elements 1`] = ` -" - - - - - - - - -
- - - -" -`; - -exports[`non-inlinable styles > should throw an error when used without a 1`] = ` -[Error: You are trying to use the following Tailwind classes that cannot be inlined: sm:bg-red-500. -For the media queries to work properly on rendering, they need to be added into a - - -
- - - -" -`; - -exports[`non-inlinable styles > should work with arbitrarily deep (in the React tree) elements 2`] = `"
"`; - -exports[`non-inlinable styles > should work with relatively complex media query utilities 1`] = ` -" - - - - - - -

- I am some text -

- -" -`; From 0b849ca486324c22a8d08c189a92e1b3dbd668d3 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Mon, 8 Sep 2025 15:49:31 -0300 Subject: [PATCH 058/193] remove debugging log --- .../tailwind/src/utils/css/sanitize-non-inlinable-classes.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/tailwind/src/utils/css/sanitize-non-inlinable-classes.ts b/packages/tailwind/src/utils/css/sanitize-non-inlinable-classes.ts index 26dab65327..06798680a5 100644 --- a/packages/tailwind/src/utils/css/sanitize-non-inlinable-classes.ts +++ b/packages/tailwind/src/utils/css/sanitize-non-inlinable-classes.ts @@ -36,7 +36,6 @@ export const sanitizeNonInlinableClasses = (node: CssNode) => { walk(ruleToChange.prelude, (node) => { if (node.type === 'ClassSelector') { const unescapedClassName = string.decode(node.name); - console.log(node.name, unescapedClassName); nonInlinableClasses.push(unescapedClassName); node.name = sanitizeClassName(unescapedClassName); } From 2854c9aa3bbbf6dca065fbdbeb6526ab5ecba16f Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Mon, 8 Sep 2025 16:34:22 -0300 Subject: [PATCH 059/193] update test snapshots --- .../src/__snapshots__/tailwind.spec.tsx.snap | 27 ++++++++++++++++--- packages/tailwind/src/tailwind.spec.tsx | 4 +-- packages/tailwind/src/tailwind.tsx | 7 +++-- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap b/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap index 06eefdfd41..dbc6113692 100644 --- a/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap +++ b/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap @@ -12,7 +12,26 @@ exports[`Tailwind component > should not override inline styles with Tailwind st exports[`Tailwind component > should override component styles with Tailwind styles 1`] = `"
"`; -exports[`Tailwind component > should preserve mso styles 1`] = `"
"`; +exports[`Tailwind component > should preserve mso styles 1`] = ` +" + + + + + + +
+ + +" +`; exports[`Tailwind component > should recognize custom responsive screen 1`] = ` " @@ -74,6 +93,8 @@ exports[`Tailwind component > should work with calc() with + sign 1`] = ` " `; +exports[`Tailwind component > should work with class manipulation done on components 1`] = `"
"`; + exports[`Tailwind component > should work with components that return children 1`] = `"
Hello world

React Email

"`; exports[`Tailwind component > should work with components that use React.forwardRef 1`] = `"
Hello world

React Email

"`; @@ -170,11 +191,11 @@ exports[`Tailwind component > with non-inlinable styles > should persist existin - - + +
{ props.style, 'Styles should be generated the same for a component', ).toEqual({ - color: 'rgb(96,165,250)', + color: 'rgb(80.5,162.1,255)', padding: '1rem', }); return ( @@ -290,7 +290,7 @@ describe('Tailwind component', () => { , ); - expect(actualOutput).toContain('width:3rem'); + expect(actualOutput).toMatchSnapshot(); }); // test.only('temporary', async () => { diff --git a/packages/tailwind/src/tailwind.tsx b/packages/tailwind/src/tailwind.tsx index a8bb442c8d..836f065fb2 100644 --- a/packages/tailwind/src/tailwind.tsx +++ b/packages/tailwind/src/tailwind.tsx @@ -98,8 +98,8 @@ export const Tailwind: React.FC = async ({ } = await cloneElementWithInlinedStyles(node, config ?? {}); mediaQueryClassesForAllElement = mediaQueryClassesForAllElement.concat(nonInlinableClasses); - for (const node of nonInlineStyleNodes) { - nonInlineStylesToApply.children.appendData(node); + for (const rule of nonInlineStyleNodes) { + nonInlineStylesToApply.children.appendData(rule); } if (nonInlinableClasses.length > 0 && !hasNonInlineStylesToApply) { @@ -125,7 +125,6 @@ export const Tailwind: React.FC = async ({ if (node.type === 'head') { hasAppliedNonInlineStyles = true; - /* only minify here since it is the only place that is going to be in the DOM */ const styleElement = ( ); @@ -133,8 +132,8 @@ export const Tailwind: React.FC = async ({ return React.cloneElement( node, node.props, - node.props.children, styleElement, + node.props.children, ); } } From 912be48643020b777c117159009909ea43366caf Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Mon, 8 Sep 2025 16:35:11 -0300 Subject: [PATCH 060/193] lint --- packages/tailwind/src/tailwind.tsx | 1 - packages/tailwind/src/utils/css/resolve-calc-expressions.ts | 4 ++-- .../tailwind/src/utils/css/sanitize-non-inlinable-classes.ts | 2 +- .../src/utils/tailwindcss/generate-root-for-classes.ts | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/tailwind/src/tailwind.tsx b/packages/tailwind/src/tailwind.tsx index 836f065fb2..0b9b9fa8a5 100644 --- a/packages/tailwind/src/tailwind.tsx +++ b/packages/tailwind/src/tailwind.tsx @@ -1,7 +1,6 @@ import { generate, List, type StyleSheet } from 'css-tree'; import * as React from 'react'; import type { Config } from 'tailwindcss'; -import { removeRuleDuplicatesFromRoot } from './utils/css/remove-rule-duplicates-from-root'; import { mapReactTree } from './utils/react/map-react-tree'; import { cloneElementWithInlinedStyles } from './utils/tailwindcss/clone-element-with-inlined-styles'; diff --git a/packages/tailwind/src/utils/css/resolve-calc-expressions.ts b/packages/tailwind/src/utils/css/resolve-calc-expressions.ts index e30eb4579d..9be44231fb 100644 --- a/packages/tailwind/src/utils/css/resolve-calc-expressions.ts +++ b/packages/tailwind/src/utils/css/resolve-calc-expressions.ts @@ -29,9 +29,9 @@ export const resolveCalcExpressions = (node: CssNode) => { const value = String( child.value === '*' ? Number.parseFloat(left.data.value) * - Number.parseFloat(right.data.value) + Number.parseFloat(right.data.value) : Number.parseFloat(left.data.value) / - Number.parseFloat(right.data.value), + Number.parseFloat(right.data.value), ); if ( left.data.type === 'Dimension' && diff --git a/packages/tailwind/src/utils/css/sanitize-non-inlinable-classes.ts b/packages/tailwind/src/utils/css/sanitize-non-inlinable-classes.ts index 06798680a5..ffc47eb25f 100644 --- a/packages/tailwind/src/utils/css/sanitize-non-inlinable-classes.ts +++ b/packages/tailwind/src/utils/css/sanitize-non-inlinable-classes.ts @@ -1,4 +1,4 @@ -import { type CssNode, type Rule, walk, string } from 'css-tree'; +import { type CssNode, type Rule, string, walk } from 'css-tree'; import { sanitizeClassName } from '../compatibility/sanitize-class-name'; import { clone } from './clone'; import { isRuleInlinable } from './is-rule-inlinable'; diff --git a/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.ts b/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.ts index c3e2ed290c..3bdb0b49a5 100644 --- a/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.ts +++ b/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.ts @@ -1,11 +1,11 @@ import { parse } from 'css-tree'; import { type Config, compile } from 'tailwindcss'; import { resolveAllCSSVariables } from '../css/resolve-all-css-variables'; +import { resolveCalcExpressions } from '../css/resolve-calc-expressions'; import indexCss from './tailwind-stylesheets/index'; import preflightCss from './tailwind-stylesheets/preflight'; import themeCss from './tailwind-stylesheets/theme'; import utilitiesCss from './tailwind-stylesheets/utilities'; -import { resolveCalcExpressions } from '../css/resolve-calc-expressions'; const baseCss = ` @layer theme, base, components, utilities; From b9e4a4b11193e38b11cfaa8054ed20b90a4335f5 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Mon, 8 Sep 2025 16:42:35 -0300 Subject: [PATCH 061/193] update remaining failing test snapshots, everything passes! --- .../__snapshots__/make-inline-styles-for.spec.ts.snap | 11 +++++++++++ .../sanitize-non-inlinable-classes.spec.ts.snap | 2 +- .../src/utils/css/make-inline-styles-for.spec.ts | 5 +---- 3 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 packages/tailwind/src/utils/css/__snapshots__/make-inline-styles-for.spec.ts.snap diff --git a/packages/tailwind/src/utils/css/__snapshots__/make-inline-styles-for.spec.ts.snap b/packages/tailwind/src/utils/css/__snapshots__/make-inline-styles-for.spec.ts.snap new file mode 100644 index 0000000000..75723cfdb0 --- /dev/null +++ b/packages/tailwind/src/utils/css/__snapshots__/make-inline-styles-for.spec.ts.snap @@ -0,0 +1,11 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`makeInlineStylesFor() 1`] = ` +{ + "residualClassName": "sm:bg-blue-300 md:max-w-[400px] my-custom-class", + "styles": { + "backgroundColor": "oklch(63.7%0.237 25.331)", + "width": "100%", + }, +} +`; diff --git a/packages/tailwind/src/utils/css/__snapshots__/sanitize-non-inlinable-classes.spec.ts.snap b/packages/tailwind/src/utils/css/__snapshots__/sanitize-non-inlinable-classes.spec.ts.snap index 8b494906e1..ef41b23353 100644 --- a/packages/tailwind/src/utils/css/__snapshots__/sanitize-non-inlinable-classes.spec.ts.snap +++ b/packages/tailwind/src/utils/css/__snapshots__/sanitize-non-inlinable-classes.spec.ts.snap @@ -15,4 +15,4 @@ exports[`sanitizeNonInlinableClasses() > should handle rules that can be inlined exports[`sanitizeNonInlinableClasses() > should handle rules that can be inlined 2`] = `[]`; -exports[`sanitizeNonInlinableClasses() > shuold work with basic media query rules 1`] = `":root,:host{}.sm_mx-auto{@media (width>=40rem){margin-inline:auto!important}}.sm_max-w-lg{@media (width>=40rem){max-width:32rem!important}}.sm_rounded-lg{@media (width>=40rem){border-radius:0.5rem!important}}.md_px-10{@media (width>=48rem){padding-inline:calc(0.25rem*10)!important}}.md_py-12{@media (width>=48rem){padding-block:calc(0.25rem*12)!important}}"`; +exports[`sanitizeNonInlinableClasses() > shuold work with basic media query rules 1`] = `":root,:host{}.sm_mx-auto{@media (width>=40rem){margin-inline:auto!important}}.sm_max-w-lg{@media (width>=40rem){max-width:32rem!important}}.sm_rounded-lg{@media (width>=40rem){border-radius:0.5rem!important}}.md_px-10{@media (width>=48rem){padding-inline:2.5rem!important}}.md_py-12{@media (width>=48rem){padding-block:3rem!important}}"`; diff --git a/packages/tailwind/src/utils/css/make-inline-styles-for.spec.ts b/packages/tailwind/src/utils/css/make-inline-styles-for.spec.ts index f623e21e40..dc060eef08 100644 --- a/packages/tailwind/src/utils/css/make-inline-styles-for.spec.ts +++ b/packages/tailwind/src/utils/css/make-inline-styles-for.spec.ts @@ -6,8 +6,5 @@ test('makeInlineStylesFor()', async () => { 'bg-red-500 sm:bg-blue-300 w-full md:max-w-[400px] my-custom-class'; const tailwindStyles = await generateRootForClasses(className.split(' '), {}); - expect(makeInlineStylesFor(className, tailwindStyles)).toEqual({ - styles: { backgroundColor: 'rgb(239 68 68 / 1)', width: '100%' }, - residualClassName: 'sm:bg-blue-300 md:max-w-[400px] my-custom-class', - }); + expect(makeInlineStylesFor(className, tailwindStyles)).toMatchSnapshot(); }); From 0cbea0c0d1f1af93b39c3a6ad736af704b03ebc9 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Mon, 8 Sep 2025 17:18:28 -0300 Subject: [PATCH 062/193] save parentList and parentListItem to nodes --- ...opulate-parents-for-node-tree.spec.ts.snap | 2018 ++++++++++++++++- packages/tailwind/src/utils/css/clone.ts | 2 +- .../css/populate-parents-for-node-tree.ts | 12 +- 3 files changed, 1981 insertions(+), 51 deletions(-) diff --git a/packages/tailwind/src/utils/css/__snapshots__/populate-parents-for-node-tree.spec.ts.snap b/packages/tailwind/src/utils/css/__snapshots__/populate-parents-for-node-tree.spec.ts.snap index f8de4dba87..48b790032b 100644 --- a/packages/tailwind/src/utils/css/__snapshots__/populate-parents-for-node-tree.spec.ts.snap +++ b/packages/tailwind/src/utils/css/__snapshots__/populate-parents-for-node-tree.spec.ts.snap @@ -10,6 +10,12 @@ exports[`populateParentsForNodeTree 1`] = ` "important": false, "loc": null, "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, "property": "color", "type": "Declaration", "value": { @@ -18,21 +24,511 @@ exports[`populateParentsForNodeTree 1`] = ` "loc": null, "name": "red", "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, "type": "Identifier", }, ], "loc": null, "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, "type": "Value", }, }, ], "loc": null, "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, "type": "Block", }, "loc": null, "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": { + "data": { + "block": { + "children": [ + { + "important": false, + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "property": "background", + "type": "Declaration", + "value": { + "children": [ + { + "loc": null, + "name": "blue", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "Identifier", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Value", + }, + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Block", + }, + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": [Circular], + "prelude": { + "children": [ + { + "children": [ + { + "loc": null, + "name": "bar", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "ClassSelector", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "Selector", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "SelectorList", + }, + "type": "Rule", + }, + "next": { + "data": { + "block": { + "children": [ + { + "block": { + "children": [ + { + "important": false, + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "property": "margin", + "type": "Declaration", + "value": { + "children": [ + { + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "Dimension", + "unit": "px", + "value": "10", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Value", + }, + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Block", + }, + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "prelude": { + "children": [ + { + "children": [ + { + "loc": null, + "name": "baz", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "ClassSelector", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "Selector", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "SelectorList", + }, + "type": "Rule", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Block", + }, + "loc": null, + "name": "media", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": [Circular], + "prelude": { + "children": [ + { + "children": [ + { + "condition": { + "children": [ + { + "kind": "media", + "loc": null, + "name": "min-width", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "Feature", + "value": { + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Dimension", + "unit": "px", + "value": "600", + }, + }, + ], + "kind": "media", + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Condition", + }, + "loc": null, + "mediaType": null, + "modifier": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "MediaQuery", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "MediaQueryList", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "AtrulePrelude", + }, + "type": "Atrule", + }, + "next": { + "data": { + "block": { + "children": [ + { + "block": { + "children": [ + { + "important": false, + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "property": "padding", + "type": "Declaration", + "value": { + "children": [ + { + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "Dimension", + "unit": "px", + "value": "5", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Value", + }, + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Block", + }, + "loc": null, + "name": "media", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "prelude": { + "children": [ + { + "children": [ + { + "condition": { + "children": [ + { + "kind": "media", + "loc": null, + "name": "hover", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "Feature", + "value": { + "loc": null, + "name": "hover", + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Identifier", + }, + }, + ], + "kind": "media", + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Condition", + }, + "loc": null, + "mediaType": null, + "modifier": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "MediaQuery", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "MediaQueryList", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "AtrulePrelude", + }, + "type": "Atrule", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Block", + }, + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": [Circular], + "prelude": { + "children": [ + { + "children": [ + { + "loc": null, + "name": "qux", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": { + "data": { + "children": null, + "loc": null, + "name": "hover", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": [Circular], + "type": "PseudoClassSelector", + }, + "next": null, + "prev": [Circular], + }, + "prev": null, + }, + "type": "ClassSelector", + }, + { + "children": null, + "loc": null, + "name": "hover", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": { + "data": { + "loc": null, + "name": "qux", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": [Circular], + "type": "ClassSelector", + }, + "next": [Circular], + "prev": null, + }, + }, + "type": "PseudoClassSelector", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "Selector", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "SelectorList", + }, + "type": "Rule", + }, + "next": null, + "prev": [Circular], + }, + "prev": [Circular], + }, + "prev": [Circular], + }, + "prev": null, + }, "prelude": { "children": [ { @@ -41,16 +537,30 @@ exports[`populateParentsForNodeTree 1`] = ` "loc": null, "name": "foo", "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, "type": "ClassSelector", }, ], "loc": null, "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, "type": "Selector", }, ], "loc": null, "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, "type": "SelectorList", }, "type": "Rule", @@ -62,115 +572,1054 @@ exports[`populateParentsForNodeTree 1`] = ` "important": false, "loc": null, "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, "property": "background", "type": "Declaration", "value": { "children": [ { "loc": null, - "name": "blue", + "name": "blue", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "Identifier", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Value", + }, + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Block", + }, + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": { + "data": { + "block": { + "children": [ + { + "block": { + "children": [ + { + "important": false, + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "property": "margin", + "type": "Declaration", + "value": { + "children": [ + { + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "Dimension", + "unit": "px", + "value": "10", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Value", + }, + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Block", + }, + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "prelude": { + "children": [ + { + "children": [ + { + "loc": null, + "name": "baz", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "ClassSelector", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "Selector", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "SelectorList", + }, + "type": "Rule", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Block", + }, + "loc": null, + "name": "media", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": [Circular], + "prelude": { + "children": [ + { + "children": [ + { + "condition": { + "children": [ + { + "kind": "media", + "loc": null, + "name": "min-width", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "Feature", + "value": { + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Dimension", + "unit": "px", + "value": "600", + }, + }, + ], + "kind": "media", + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Condition", + }, + "loc": null, + "mediaType": null, + "modifier": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "MediaQuery", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "MediaQueryList", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "AtrulePrelude", + }, + "type": "Atrule", + }, + "next": { + "data": { + "block": { + "children": [ + { + "block": { + "children": [ + { + "important": false, + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "property": "padding", + "type": "Declaration", + "value": { + "children": [ + { + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "Dimension", + "unit": "px", + "value": "5", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Value", + }, + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Block", + }, + "loc": null, + "name": "media", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "prelude": { + "children": [ + { + "children": [ + { + "condition": { + "children": [ + { + "kind": "media", + "loc": null, + "name": "hover", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "Feature", + "value": { + "loc": null, + "name": "hover", + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Identifier", + }, + }, + ], + "kind": "media", + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Condition", + }, + "loc": null, + "mediaType": null, + "modifier": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "MediaQuery", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "MediaQueryList", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "AtrulePrelude", + }, + "type": "Atrule", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Block", + }, + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": [Circular], + "prelude": { + "children": [ + { + "children": [ + { + "loc": null, + "name": "qux", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": { + "data": { + "children": null, + "loc": null, + "name": "hover", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": [Circular], + "type": "PseudoClassSelector", + }, + "next": null, + "prev": [Circular], + }, + "prev": null, + }, + "type": "ClassSelector", + }, + { + "children": null, + "loc": null, + "name": "hover", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": { + "data": { + "loc": null, + "name": "qux", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": [Circular], + "type": "ClassSelector", + }, + "next": [Circular], + "prev": null, + }, + }, + "type": "PseudoClassSelector", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "Selector", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "SelectorList", + }, + "type": "Rule", + }, + "next": null, + "prev": [Circular], + }, + "prev": [Circular], + }, + "prev": { + "data": { + "block": { + "children": [ + { + "important": false, + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "property": "color", + "type": "Declaration", + "value": { + "children": [ + { + "loc": null, + "name": "red", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "Identifier", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Value", + }, + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Block", + }, + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": [Circular], + "prelude": { + "children": [ + { + "children": [ + { + "loc": null, + "name": "foo", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "ClassSelector", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "Selector", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "SelectorList", + }, + "type": "Rule", + }, + "next": [Circular], + "prev": null, + }, + }, + "prelude": { + "children": [ + { + "children": [ + { + "loc": null, + "name": "bar", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "ClassSelector", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "Selector", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "SelectorList", + }, + "type": "Rule", + }, + { + "block": { + "children": [ + { + "block": { + "children": [ + { + "important": false, + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "property": "margin", + "type": "Declaration", + "value": { + "children": [ + { + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "Dimension", + "unit": "px", + "value": "10", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Value", + }, + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Block", + }, + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "prelude": { + "children": [ + { + "children": [ + { + "loc": null, + "name": "baz", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "ClassSelector", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "Selector", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "SelectorList", + }, + "type": "Rule", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Block", + }, + "loc": null, + "name": "media", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": { + "data": { + "block": { + "children": [ + { + "block": { + "children": [ + { + "important": false, + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "property": "padding", + "type": "Declaration", + "value": { + "children": [ + { + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "Dimension", + "unit": "px", + "value": "5", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Value", + }, + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Block", + }, + "loc": null, + "name": "media", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "prelude": { + "children": [ + { + "children": [ + { + "condition": { + "children": [ + { + "kind": "media", + "loc": null, + "name": "hover", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "Feature", + "value": { + "loc": null, + "name": "hover", + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Identifier", + }, + }, + ], + "kind": "media", + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Condition", + }, + "loc": null, + "mediaType": null, + "modifier": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "MediaQuery", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "MediaQueryList", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "AtrulePrelude", + }, + "type": "Atrule", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Block", + }, + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": [Circular], + "prelude": { + "children": [ + { + "children": [ + { + "loc": null, + "name": "qux", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": { + "data": { + "children": null, + "loc": null, + "name": "hover", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": [Circular], + "type": "PseudoClassSelector", + }, + "next": null, + "prev": [Circular], + }, + "prev": null, + }, + "type": "ClassSelector", + }, + { + "children": null, + "loc": null, + "name": "hover", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": { + "data": { + "loc": null, + "name": "qux", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": [Circular], + "type": "ClassSelector", + }, + "next": [Circular], + "prev": null, + }, + }, + "type": "PseudoClassSelector", + }, + ], + "loc": null, "parent": [Circular], - "type": "Identifier", + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "Selector", }, ], "loc": null, "parent": [Circular], - "type": "Value", + "parentList": undefined, + "parentListItem": undefined, + "type": "SelectorList", }, + "type": "Rule", }, - ], - "loc": null, - "parent": [Circular], - "type": "Block", - }, - "loc": null, - "parent": [Circular], - "prelude": { - "children": [ - { - "children": [ - { - "loc": null, - "name": "bar", - "parent": [Circular], - "type": "ClassSelector", - }, - ], - "loc": null, - "parent": [Circular], - "type": "Selector", - }, - ], - "loc": null, - "parent": [Circular], - "type": "SelectorList", - }, - "type": "Rule", - }, - { - "block": { - "children": [ - { + "next": null, + "prev": [Circular], + }, + "prev": { + "data": { "block": { "children": [ { "important": false, "loc": null, "parent": [Circular], - "property": "margin", + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "property": "background", "type": "Declaration", "value": { "children": [ { "loc": null, + "name": "blue", "parent": [Circular], - "type": "Dimension", - "unit": "px", - "value": "10", + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "Identifier", }, ], "loc": null, "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, "type": "Value", }, }, ], "loc": null, "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, "type": "Block", }, "loc": null, "parent": [Circular], + "parentList": [Circular], + "parentListItem": [Circular], "prelude": { "children": [ { "children": [ { "loc": null, - "name": "baz", + "name": "bar", "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, "type": "ClassSelector", }, ], "loc": null, "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, "type": "Selector", }, ], "loc": null, "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, "type": "SelectorList", }, "type": "Rule", }, - ], - "loc": null, - "parent": [Circular], - "type": "Block", + "next": [Circular], + "prev": { + "data": { + "block": { + "children": [ + { + "important": false, + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "property": "color", + "type": "Declaration", + "value": { + "children": [ + { + "loc": null, + "name": "red", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "Identifier", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Value", + }, + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Block", + }, + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": [Circular], + "prelude": { + "children": [ + { + "children": [ + { + "loc": null, + "name": "foo", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "ClassSelector", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "Selector", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "SelectorList", + }, + "type": "Rule", + }, + "next": [Circular], + "prev": null, + }, + }, }, - "loc": null, - "name": "media", - "parent": [Circular], "prelude": { "children": [ { @@ -183,10 +1632,18 @@ exports[`populateParentsForNodeTree 1`] = ` "loc": null, "name": "min-width", "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, "type": "Feature", "value": { "loc": null, "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, "type": "Dimension", "unit": "px", "value": "600", @@ -196,22 +1653,38 @@ exports[`populateParentsForNodeTree 1`] = ` "kind": "media", "loc": null, "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, "type": "Condition", }, "loc": null, "mediaType": null, "modifier": null, "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, "type": "MediaQuery", }, ], "loc": null, "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, "type": "MediaQueryList", }, ], "loc": null, "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, "type": "AtrulePrelude", }, "type": "Atrule", @@ -226,6 +1699,12 @@ exports[`populateParentsForNodeTree 1`] = ` "important": false, "loc": null, "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, "property": "padding", "type": "Declaration", "value": { @@ -233,6 +1712,12 @@ exports[`populateParentsForNodeTree 1`] = ` { "loc": null, "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, "type": "Dimension", "unit": "px", "value": "5", @@ -240,17 +1725,27 @@ exports[`populateParentsForNodeTree 1`] = ` ], "loc": null, "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, "type": "Value", }, }, ], "loc": null, "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, "type": "Block", }, "loc": null, "name": "media", "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, "prelude": { "children": [ { @@ -263,11 +1758,19 @@ exports[`populateParentsForNodeTree 1`] = ` "loc": null, "name": "hover", "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, "type": "Feature", "value": { "loc": null, "name": "hover", "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, "type": "Identifier", }, }, @@ -275,22 +1778,38 @@ exports[`populateParentsForNodeTree 1`] = ` "kind": "media", "loc": null, "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, "type": "Condition", }, "loc": null, "mediaType": null, "modifier": null, "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, "type": "MediaQuery", }, ], "loc": null, "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, "type": "MediaQueryList", }, ], "loc": null, "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, "type": "AtrulePrelude", }, "type": "Atrule", @@ -298,10 +1817,368 @@ exports[`populateParentsForNodeTree 1`] = ` ], "loc": null, "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, "type": "Block", }, "loc": null, "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": { + "data": { + "block": { + "children": [ + { + "block": { + "children": [ + { + "important": false, + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "property": "margin", + "type": "Declaration", + "value": { + "children": [ + { + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "Dimension", + "unit": "px", + "value": "10", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Value", + }, + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Block", + }, + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "prelude": { + "children": [ + { + "children": [ + { + "loc": null, + "name": "baz", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "ClassSelector", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "Selector", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "SelectorList", + }, + "type": "Rule", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Block", + }, + "loc": null, + "name": "media", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": [Circular], + "prelude": { + "children": [ + { + "children": [ + { + "condition": { + "children": [ + { + "kind": "media", + "loc": null, + "name": "min-width", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "Feature", + "value": { + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Dimension", + "unit": "px", + "value": "600", + }, + }, + ], + "kind": "media", + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Condition", + }, + "loc": null, + "mediaType": null, + "modifier": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "MediaQuery", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "MediaQueryList", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "AtrulePrelude", + }, + "type": "Atrule", + }, + "next": [Circular], + "prev": { + "data": { + "block": { + "children": [ + { + "important": false, + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "property": "background", + "type": "Declaration", + "value": { + "children": [ + { + "loc": null, + "name": "blue", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "Identifier", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Value", + }, + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Block", + }, + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": [Circular], + "prelude": { + "children": [ + { + "children": [ + { + "loc": null, + "name": "bar", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "ClassSelector", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "Selector", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "SelectorList", + }, + "type": "Rule", + }, + "next": [Circular], + "prev": { + "data": { + "block": { + "children": [ + { + "important": false, + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "property": "color", + "type": "Declaration", + "value": { + "children": [ + { + "loc": null, + "name": "red", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "Identifier", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Value", + }, + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "Block", + }, + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": [Circular], + "prelude": { + "children": [ + { + "children": [ + { + "loc": null, + "name": "foo", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "ClassSelector", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, + "type": "Selector", + }, + ], + "loc": null, + "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, + "type": "SelectorList", + }, + "type": "Rule", + }, + "next": [Circular], + "prev": null, + }, + }, + }, + }, "prelude": { "children": [ { @@ -310,6 +2187,24 @@ exports[`populateParentsForNodeTree 1`] = ` "loc": null, "name": "qux", "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": { + "data": { + "children": null, + "loc": null, + "name": "hover", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": [Circular], + "type": "PseudoClassSelector", + }, + "next": null, + "prev": [Circular], + }, + "prev": null, + }, "type": "ClassSelector", }, { @@ -317,16 +2212,41 @@ exports[`populateParentsForNodeTree 1`] = ` "loc": null, "name": "hover", "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": { + "data": { + "loc": null, + "name": "qux", + "parent": [Circular], + "parentList": [Circular], + "parentListItem": [Circular], + "type": "ClassSelector", + }, + "next": [Circular], + "prev": null, + }, + }, "type": "PseudoClassSelector", }, ], "loc": null, "parent": [Circular], + "parentList": [Circular], + "parentListItem": { + "data": [Circular], + "next": null, + "prev": null, + }, "type": "Selector", }, ], "loc": null, "parent": [Circular], + "parentList": undefined, + "parentListItem": undefined, "type": "SelectorList", }, "type": "Rule", @@ -334,6 +2254,8 @@ exports[`populateParentsForNodeTree 1`] = ` ], "loc": null, "parent": undefined, + "parentList": undefined, + "parentListItem": undefined, "type": "StyleSheet", } `; diff --git a/packages/tailwind/src/utils/css/clone.ts b/packages/tailwind/src/utils/css/clone.ts index d0fe2988a6..bbce880004 100644 --- a/packages/tailwind/src/utils/css/clone.ts +++ b/packages/tailwind/src/utils/css/clone.ts @@ -8,7 +8,7 @@ export function clone(node: T): T { for (const key of Object.keys(node)) { let value: any = node[key as keyof CssNode]; - if (key === 'parent') { + if (key === 'parent' || key === 'parentList' || key === 'parentListItem') { continue; } diff --git a/packages/tailwind/src/utils/css/populate-parents-for-node-tree.ts b/packages/tailwind/src/utils/css/populate-parents-for-node-tree.ts index 7d1c3b893f..135220f690 100644 --- a/packages/tailwind/src/utils/css/populate-parents-for-node-tree.ts +++ b/packages/tailwind/src/utils/css/populate-parents-for-node-tree.ts @@ -1,17 +1,25 @@ -import { type CssNode, walk } from 'css-tree'; +import { type CssNode, type List, type ListItem, walk } from 'css-tree'; // This expands the definition for CssNode so that we can add a parent property to all nodes declare module 'css-tree' { interface CssNodeCommon { parent?: CssNode; + parentListItem?: ListItem; + parentList?: List; } } export function populateParentsForNodeTree(node: CssNode): void { const parentPath: CssNode[] = []; walk(node, { - enter(child: CssNode) { + enter( + child: CssNode, + parentListItem: ListItem, + parentList: List, + ) { child.parent = parentPath[0]; + child.parentListItem = parentListItem; + child.parentList = parentList; parentPath.unshift(child); }, leave() { From 74cdbdd204dc119e4ad0aa9f648cc1da4e5c95df Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Mon, 8 Sep 2025 17:48:40 -0300 Subject: [PATCH 063/193] remove empty parents when resolving variable uses and removing definitions --- .../resolve-all-css-variables.spec.ts.snap | 12 +++---- .../css/populate-parents-for-node-tree.ts | 8 ++--- .../utils/css/remove-if-empty-recursively.ts | 11 ------- .../css/resolve-all-css-variables.spec.ts | 33 ++++++++++--------- .../utils/css/resolve-all-css-variables.ts | 22 ++++++++++--- 5 files changed, 45 insertions(+), 41 deletions(-) delete mode 100644 packages/tailwind/src/utils/css/remove-if-empty-recursively.ts diff --git a/packages/tailwind/src/utils/css/__snapshots__/resolve-all-css-variables.spec.ts.snap b/packages/tailwind/src/utils/css/__snapshots__/resolve-all-css-variables.spec.ts.snap index 19ba9ed47a..bf5c04ee18 100644 --- a/packages/tailwind/src/utils/css/__snapshots__/resolve-all-css-variables.spec.ts.snap +++ b/packages/tailwind/src/utils/css/__snapshots__/resolve-all-css-variables.spec.ts.snap @@ -1,19 +1,19 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`resolveAllCSSVariables > should handle deeply nested var() functions with complex parentheses 1`] = `":root{}.box{color:blue;width:20px;border:1px solid black;background:rgb(100,0,10)}"`; +exports[`resolveAllCSSVariables > should handle deeply nested var() functions with complex parentheses 1`] = `".box{color:blue;width:20px;border:1px solid black;background:rgb(100,0,10)}"`; -exports[`resolveAllCSSVariables > should handle nested var() functions in fallbacks 1`] = `":root{}.box{width:300px;height:250px}"`; +exports[`resolveAllCSSVariables > should handle nested var() functions in fallbacks 1`] = `".box{width:300px;height:250px}"`; exports[`resolveAllCSSVariables > should keep variable usages if it cant find their declaration 1`] = `".box{width:var(--width)}"`; exports[`resolveAllCSSVariables > should use fallback values when variable definition is not found 1`] = `".box{width:150px;height:200px;margin:10px 20px}"`; -exports[`resolveAllCSSVariables > should work for variables across different CSS layers 1`] = `"@layer base{:root{}}@layer utilities{.box{width:100px}}"`; +exports[`resolveAllCSSVariables > should work for variables across different CSS layers 1`] = `"@layer utilities{.box{width:100px}}"`; -exports[`resolveAllCSSVariables > should work with a variable set in a layer, and used in another through a media query 1`] = `"@layer theme{:root{}}@layer utilities{.sm:bg-blue-300{@media (width>=40rem){background-color:blue}}}"`; +exports[`resolveAllCSSVariables > should work with a variable set in a layer, and used in another through a media query 1`] = `"@layer utilities{.sm:bg-blue-300{@media (width>=40rem){background-color:blue}}}"`; -exports[`resolveAllCSSVariables > should work with multiple variables in the same declaration 1`] = `":root{}.box{margin:101px 103px 102px 104px}"`; +exports[`resolveAllCSSVariables > should work with multiple variables in the same declaration 1`] = `".box{margin:101px 103px 102px 104px}"`; -exports[`resolveAllCSSVariables > should work with simple css variables on a :root 1`] = `":root{}.box{width:100px}"`; +exports[`resolveAllCSSVariables > should work with simple css variables on a :root 1`] = `".box{width:100px}"`; exports[`resolveAllCSSVariables > should work with variables set in the same rule 1`] = `".box{width:200px}@media (min-width:1280px){.xl\\:bg-green-500{background-color:rgb(34 197 94/1)}}"`; diff --git a/packages/tailwind/src/utils/css/populate-parents-for-node-tree.ts b/packages/tailwind/src/utils/css/populate-parents-for-node-tree.ts index 135220f690..6bf8d8ca0a 100644 --- a/packages/tailwind/src/utils/css/populate-parents-for-node-tree.ts +++ b/packages/tailwind/src/utils/css/populate-parents-for-node-tree.ts @@ -4,8 +4,8 @@ import { type CssNode, type List, type ListItem, walk } from 'css-tree'; declare module 'css-tree' { interface CssNodeCommon { parent?: CssNode; - parentListItem?: ListItem; - parentList?: List; + containingItem?: ListItem; + containedIn?: List; } } @@ -18,8 +18,8 @@ export function populateParentsForNodeTree(node: CssNode): void { parentList: List, ) { child.parent = parentPath[0]; - child.parentListItem = parentListItem; - child.parentList = parentList; + child.containingItem = parentListItem; + child.containedIn = parentList; parentPath.unshift(child); }, leave() { diff --git a/packages/tailwind/src/utils/css/remove-if-empty-recursively.ts b/packages/tailwind/src/utils/css/remove-if-empty-recursively.ts deleted file mode 100644 index 90fa5feffc..0000000000 --- a/packages/tailwind/src/utils/css/remove-if-empty-recursively.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Container, Document } from 'postcss'; - -export const removeIfEmptyRecursively = (node: Container | Document) => { - if (node.first === undefined) { - const parent = node.parent; - if (parent) { - node.remove(); - removeIfEmptyRecursively(parent); - } - } -}; diff --git a/packages/tailwind/src/utils/css/resolve-all-css-variables.spec.ts b/packages/tailwind/src/utils/css/resolve-all-css-variables.spec.ts index 1bb0e63943..aed4517920 100644 --- a/packages/tailwind/src/utils/css/resolve-all-css-variables.spec.ts +++ b/packages/tailwind/src/utils/css/resolve-all-css-variables.spec.ts @@ -10,8 +10,8 @@ describe('resolveAllCSSVariables', () => { .box { width: var(--width); }`); - - expect(generate(resolveAllCSSVariables(root))).toMatchSnapshot(); + resolveAllCSSVariables(root); + expect(generate(root)).toMatchSnapshot(); }); it('should work for variables across different CSS layers', () => { @@ -26,8 +26,8 @@ describe('resolveAllCSSVariables', () => { width: var(--width); } }`); - - expect(generate(resolveAllCSSVariables(root))).toMatchSnapshot(); + resolveAllCSSVariables(root); + expect(generate(root)).toMatchSnapshot(); }); it('should work with multiple variables in the same declaration', () => { @@ -42,15 +42,16 @@ describe('resolveAllCSSVariables', () => { margin: var(--top) var(--right) var(--bottom) var(--left); }`); - expect(generate(resolveAllCSSVariables(root))).toMatchSnapshot(); + resolveAllCSSVariables(root); + expect(generate(root)).toMatchSnapshot(); }); it('should keep variable usages if it cant find their declaration', () => { const root = parse(`.box { width: var(--width); }`); - - expect(generate(resolveAllCSSVariables(root))).toMatchSnapshot(); + resolveAllCSSVariables(root); + expect(generate(root)).toMatchSnapshot(); }); it('should work with variables set in the same rule', () => { @@ -66,7 +67,8 @@ describe('resolveAllCSSVariables', () => { } } `); - expect(generate(resolveAllCSSVariables(root))).toMatchSnapshot(); + resolveAllCSSVariables(root); + expect(generate(root)).toMatchSnapshot(); }); it('should work with a variable set in a layer, and used in another through a media query', () => { @@ -83,7 +85,8 @@ describe('resolveAllCSSVariables', () => { } } }`); - expect(generate(resolveAllCSSVariables(root))).toMatchSnapshot(); + resolveAllCSSVariables(root); + expect(generate(root)).toMatchSnapshot(); }); it('should use fallback values when variable definition is not found', () => { @@ -92,8 +95,8 @@ describe('resolveAllCSSVariables', () => { height: var(--undefined-height, 200px); margin: var(--undefined-margin, 10px 20px); }`); - - expect(generate(resolveAllCSSVariables(root))).toMatchSnapshot(); + resolveAllCSSVariables(root); + expect(generate(root)).toMatchSnapshot(); }); it('should handle nested var() functions in fallbacks', () => { @@ -105,8 +108,8 @@ describe('resolveAllCSSVariables', () => { width: var(--undefined-width, var(--fallback-width)); height: var(--undefined-height, var(--also-undefined, 250px)); }`); - - expect(generate(resolveAllCSSVariables(root))).toMatchSnapshot(); + resolveAllCSSVariables(root); + expect(generate(root)).toMatchSnapshot(); }); it('should handle deeply nested var() functions with complex parentheses', () => { @@ -125,8 +128,8 @@ describe('resolveAllCSSVariables', () => { --b: 10; background: var(--bg-color, rgb(var(--r, 255), var(--g, 0), var(--b, 0))); }`); - - expect(generate(resolveAllCSSVariables(root))).toMatchSnapshot(); + resolveAllCSSVariables(root); + expect(generate(root)).toMatchSnapshot(); }); // this behavior is not supported anymore, since it doesn't seem like tailwindcss actually generates any CSS that uses the pattern of defining css variables from inside media queries diff --git a/packages/tailwind/src/utils/css/resolve-all-css-variables.ts b/packages/tailwind/src/utils/css/resolve-all-css-variables.ts index 8c4ca4be18..e40480994f 100644 --- a/packages/tailwind/src/utils/css/resolve-all-css-variables.ts +++ b/packages/tailwind/src/utils/css/resolve-all-css-variables.ts @@ -2,6 +2,7 @@ import { type CssNode, type Declaration, generate, + List, parse, type Raw, type Value, @@ -40,6 +41,20 @@ const doSelectorsIntersect = (first: string, second: string): boolean => { return false; }; +const removeAndRepeatIfEmptyRecursively = (node: CssNode) => { + if (node.parent) { + if (node.containedIn && node.containingItem) { + node.containedIn.remove(node.containingItem); + if (node.containedIn.isEmpty) { + removeAndRepeatIfEmptyRecursively(node.parent); + } + } else { + // The node might not have any list of children, but the parent can (e.g. a Block) + removeAndRepeatIfEmptyRecursively(node.parent); + } + } +}; + export const resolveAllCSSVariables = (node: CssNode) => { populateParentsForNodeTree(node); const variableDefinitions = new Set(); @@ -47,15 +62,14 @@ export const resolveAllCSSVariables = (node: CssNode) => { walk(node, { visit: 'Declaration', - enter(declaration, declarationItem, ruleDeclarationList) { + enter(declaration) { if (/--[\S]+/.test(declaration.property)) { variableDefinitions.add({ declaration, variableName: `${declaration.property}`, definition: generate(declaration.value), remove() { - ruleDeclarationList.remove(declarationItem); - // TODO: recursively remove the parent rule if it is empty + removeAndRepeatIfEmptyRecursively(declaration); }, }); } else { @@ -163,6 +177,4 @@ export const resolveAllCSSVariables = (node: CssNode) => { for (const definition of variableDefinitions) { definition.remove(); } - - return node; }; From 95b80bd1ca0098abfe1aa4a004e30ccd3394e890 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Mon, 8 Sep 2025 17:49:47 -0300 Subject: [PATCH 064/193] fix clone's infinite loops --- packages/tailwind/src/utils/css/clone.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tailwind/src/utils/css/clone.ts b/packages/tailwind/src/utils/css/clone.ts index bbce880004..660b1f5eb6 100644 --- a/packages/tailwind/src/utils/css/clone.ts +++ b/packages/tailwind/src/utils/css/clone.ts @@ -8,7 +8,7 @@ export function clone(node: T): T { for (const key of Object.keys(node)) { let value: any = node[key as keyof CssNode]; - if (key === 'parent' || key === 'parentList' || key === 'parentListItem') { + if (key === 'parent' || key === 'containedIn' || key === 'containingItem') { continue; } From 84b0ef9a984ba30549991ebd0379dceb0faf1401 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Mon, 8 Sep 2025 17:49:51 -0300 Subject: [PATCH 065/193] update snap --- ...opulate-parents-for-node-tree.spec.ts.snap | 1398 ++++++++--------- 1 file changed, 699 insertions(+), 699 deletions(-) diff --git a/packages/tailwind/src/utils/css/__snapshots__/populate-parents-for-node-tree.spec.ts.snap b/packages/tailwind/src/utils/css/__snapshots__/populate-parents-for-node-tree.spec.ts.snap index 48b790032b..07dd927bc6 100644 --- a/packages/tailwind/src/utils/css/__snapshots__/populate-parents-for-node-tree.spec.ts.snap +++ b/packages/tailwind/src/utils/css/__snapshots__/populate-parents-for-node-tree.spec.ts.snap @@ -7,132 +7,130 @@ exports[`populateParentsForNodeTree 1`] = ` "block": { "children": [ { - "important": false, - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "important": false, + "loc": null, + "parent": [Circular], "property": "color", "type": "Declaration", "value": { "children": [ { - "loc": null, - "name": "red", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "name": "red", + "parent": [Circular], "type": "Identifier", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Value", }, }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Block", }, - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": { "data": { "block": { "children": [ { - "important": false, - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "important": false, + "loc": null, + "parent": [Circular], "property": "background", "type": "Declaration", "value": { "children": [ { - "loc": null, - "name": "blue", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "name": "blue", + "parent": [Circular], "type": "Identifier", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Value", }, }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Block", }, + "containedIn": [Circular], + "containingItem": [Circular], "loc": null, "parent": [Circular], - "parentList": [Circular], - "parentListItem": [Circular], "prelude": { "children": [ { "children": [ { - "loc": null, - "name": "bar", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "name": "bar", + "parent": [Circular], "type": "ClassSelector", }, ], - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "type": "Selector", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "SelectorList", }, "type": "Rule", @@ -145,103 +143,103 @@ exports[`populateParentsForNodeTree 1`] = ` "block": { "children": [ { - "important": false, - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "important": false, + "loc": null, + "parent": [Circular], "property": "margin", "type": "Declaration", "value": { "children": [ { - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "type": "Dimension", "unit": "px", "value": "10", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Value", }, }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Block", }, - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "prelude": { "children": [ { "children": [ { - "loc": null, - "name": "baz", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "name": "baz", + "parent": [Circular], "type": "ClassSelector", }, ], - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "type": "Selector", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "SelectorList", }, "type": "Rule", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Block", }, + "containedIn": [Circular], + "containingItem": [Circular], "loc": null, "name": "media", "parent": [Circular], - "parentList": [Circular], - "parentListItem": [Circular], "prelude": { "children": [ { @@ -250,63 +248,63 @@ exports[`populateParentsForNodeTree 1`] = ` "condition": { "children": [ { - "kind": "media", - "loc": null, - "name": "min-width", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "kind": "media", + "loc": null, + "name": "min-width", + "parent": [Circular], "type": "Feature", "value": { + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Dimension", "unit": "px", "value": "600", }, }, ], + "containedIn": undefined, + "containingItem": undefined, "kind": "media", "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Condition", }, - "loc": null, - "mediaType": null, - "modifier": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "mediaType": null, + "modifier": null, + "parent": [Circular], "type": "MediaQuery", }, ], - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "type": "MediaQueryList", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "AtrulePrelude", }, "type": "Atrule", @@ -319,56 +317,56 @@ exports[`populateParentsForNodeTree 1`] = ` "block": { "children": [ { - "important": false, - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "important": false, + "loc": null, + "parent": [Circular], "property": "padding", "type": "Declaration", "value": { "children": [ { - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "type": "Dimension", "unit": "px", "value": "5", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Value", }, }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Block", }, - "loc": null, - "name": "media", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "name": "media", + "parent": [Circular], "prelude": { "children": [ { @@ -377,96 +375,93 @@ exports[`populateParentsForNodeTree 1`] = ` "condition": { "children": [ { - "kind": "media", - "loc": null, - "name": "hover", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "kind": "media", + "loc": null, + "name": "hover", + "parent": [Circular], "type": "Feature", "value": { + "containedIn": undefined, + "containingItem": undefined, "loc": null, "name": "hover", "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Identifier", }, }, ], + "containedIn": undefined, + "containingItem": undefined, "kind": "media", "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Condition", }, - "loc": null, - "mediaType": null, - "modifier": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "mediaType": null, + "modifier": null, + "parent": [Circular], "type": "MediaQuery", }, ], - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "type": "MediaQueryList", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "AtrulePrelude", }, "type": "Atrule", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Block", }, + "containedIn": [Circular], + "containingItem": [Circular], "loc": null, "parent": [Circular], - "parentList": [Circular], - "parentListItem": [Circular], "prelude": { "children": [ { "children": [ { - "loc": null, - "name": "qux", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": { "data": { "children": null, + "containedIn": [Circular], + "containingItem": [Circular], "loc": null, "name": "hover", "parent": [Circular], - "parentList": [Circular], - "parentListItem": [Circular], "type": "PseudoClassSelector", }, "next": null, @@ -474,48 +469,51 @@ exports[`populateParentsForNodeTree 1`] = ` }, "prev": null, }, + "loc": null, + "name": "qux", + "parent": [Circular], "type": "ClassSelector", }, { "children": null, - "loc": null, - "name": "hover", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": { "data": { + "containedIn": [Circular], + "containingItem": [Circular], "loc": null, "name": "qux", "parent": [Circular], - "parentList": [Circular], - "parentListItem": [Circular], "type": "ClassSelector", }, "next": [Circular], "prev": null, }, }, + "loc": null, + "name": "hover", + "parent": [Circular], "type": "PseudoClassSelector", }, ], - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "type": "Selector", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "SelectorList", }, "type": "Rule", @@ -529,38 +527,40 @@ exports[`populateParentsForNodeTree 1`] = ` }, "prev": null, }, + "loc": null, + "parent": [Circular], "prelude": { "children": [ { "children": [ { - "loc": null, - "name": "foo", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "name": "foo", + "parent": [Circular], "type": "ClassSelector", }, ], - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "type": "Selector", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "SelectorList", }, "type": "Rule", @@ -569,50 +569,48 @@ exports[`populateParentsForNodeTree 1`] = ` "block": { "children": [ { - "important": false, - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "important": false, + "loc": null, + "parent": [Circular], "property": "background", "type": "Declaration", "value": { "children": [ { - "loc": null, - "name": "blue", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "name": "blue", + "parent": [Circular], "type": "Identifier", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Value", }, }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Block", }, - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": { "data": { @@ -622,103 +620,103 @@ exports[`populateParentsForNodeTree 1`] = ` "block": { "children": [ { - "important": false, - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "important": false, + "loc": null, + "parent": [Circular], "property": "margin", "type": "Declaration", "value": { "children": [ { - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "type": "Dimension", "unit": "px", "value": "10", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Value", }, }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Block", }, - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "prelude": { "children": [ { "children": [ { - "loc": null, - "name": "baz", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "name": "baz", + "parent": [Circular], "type": "ClassSelector", }, ], - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "type": "Selector", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "SelectorList", }, "type": "Rule", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Block", }, + "containedIn": [Circular], + "containingItem": [Circular], "loc": null, "name": "media", "parent": [Circular], - "parentList": [Circular], - "parentListItem": [Circular], "prelude": { "children": [ { @@ -727,63 +725,63 @@ exports[`populateParentsForNodeTree 1`] = ` "condition": { "children": [ { - "kind": "media", - "loc": null, - "name": "min-width", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "kind": "media", + "loc": null, + "name": "min-width", + "parent": [Circular], "type": "Feature", "value": { + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Dimension", "unit": "px", "value": "600", }, }, ], + "containedIn": undefined, + "containingItem": undefined, "kind": "media", "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Condition", }, - "loc": null, - "mediaType": null, - "modifier": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "mediaType": null, + "modifier": null, + "parent": [Circular], "type": "MediaQuery", }, ], - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "type": "MediaQueryList", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "AtrulePrelude", }, "type": "Atrule", @@ -796,56 +794,56 @@ exports[`populateParentsForNodeTree 1`] = ` "block": { "children": [ { - "important": false, - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "important": false, + "loc": null, + "parent": [Circular], "property": "padding", "type": "Declaration", "value": { "children": [ { - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "type": "Dimension", "unit": "px", "value": "5", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Value", }, }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Block", }, - "loc": null, - "name": "media", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "name": "media", + "parent": [Circular], "prelude": { "children": [ { @@ -854,96 +852,93 @@ exports[`populateParentsForNodeTree 1`] = ` "condition": { "children": [ { - "kind": "media", - "loc": null, - "name": "hover", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "kind": "media", + "loc": null, + "name": "hover", + "parent": [Circular], "type": "Feature", "value": { + "containedIn": undefined, + "containingItem": undefined, "loc": null, "name": "hover", "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Identifier", }, }, ], + "containedIn": undefined, + "containingItem": undefined, "kind": "media", "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Condition", }, - "loc": null, - "mediaType": null, - "modifier": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "mediaType": null, + "modifier": null, + "parent": [Circular], "type": "MediaQuery", }, ], - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "type": "MediaQueryList", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "AtrulePrelude", }, "type": "Atrule", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Block", }, + "containedIn": [Circular], + "containingItem": [Circular], "loc": null, "parent": [Circular], - "parentList": [Circular], - "parentListItem": [Circular], "prelude": { "children": [ { "children": [ { - "loc": null, - "name": "qux", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": { "data": { "children": null, + "containedIn": [Circular], + "containingItem": [Circular], "loc": null, "name": "hover", "parent": [Circular], - "parentList": [Circular], - "parentListItem": [Circular], "type": "PseudoClassSelector", }, "next": null, @@ -951,48 +946,51 @@ exports[`populateParentsForNodeTree 1`] = ` }, "prev": null, }, + "loc": null, + "name": "qux", + "parent": [Circular], "type": "ClassSelector", }, { "children": null, - "loc": null, - "name": "hover", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": { "data": { + "containedIn": [Circular], + "containingItem": [Circular], "loc": null, "name": "qux", "parent": [Circular], - "parentList": [Circular], - "parentListItem": [Circular], "type": "ClassSelector", }, "next": [Circular], "prev": null, }, }, + "loc": null, + "name": "hover", + "parent": [Circular], "type": "PseudoClassSelector", }, ], - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "type": "Selector", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "SelectorList", }, "type": "Rule", @@ -1007,82 +1005,82 @@ exports[`populateParentsForNodeTree 1`] = ` "block": { "children": [ { - "important": false, - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "important": false, + "loc": null, + "parent": [Circular], "property": "color", "type": "Declaration", "value": { "children": [ { - "loc": null, - "name": "red", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "name": "red", + "parent": [Circular], "type": "Identifier", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Value", }, }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Block", }, + "containedIn": [Circular], + "containingItem": [Circular], "loc": null, "parent": [Circular], - "parentList": [Circular], - "parentListItem": [Circular], "prelude": { "children": [ { "children": [ { - "loc": null, - "name": "foo", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "name": "foo", + "parent": [Circular], "type": "ClassSelector", }, ], - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "type": "Selector", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "SelectorList", }, "type": "Rule", @@ -1091,38 +1089,40 @@ exports[`populateParentsForNodeTree 1`] = ` "prev": null, }, }, + "loc": null, + "parent": [Circular], "prelude": { "children": [ { "children": [ { - "loc": null, - "name": "bar", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "name": "bar", + "parent": [Circular], "type": "ClassSelector", }, ], - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "type": "Selector", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "SelectorList", }, "type": "Rule", @@ -1134,103 +1134,100 @@ exports[`populateParentsForNodeTree 1`] = ` "block": { "children": [ { - "important": false, - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "important": false, + "loc": null, + "parent": [Circular], "property": "margin", "type": "Declaration", "value": { "children": [ { - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "type": "Dimension", "unit": "px", "value": "10", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Value", }, }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Block", }, - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "prelude": { "children": [ { "children": [ { - "loc": null, - "name": "baz", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "name": "baz", + "parent": [Circular], "type": "ClassSelector", }, ], - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "type": "Selector", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "SelectorList", }, "type": "Rule", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Block", }, - "loc": null, - "name": "media", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": { "data": { @@ -1240,56 +1237,56 @@ exports[`populateParentsForNodeTree 1`] = ` "block": { "children": [ { - "important": false, - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "important": false, + "loc": null, + "parent": [Circular], "property": "padding", "type": "Declaration", "value": { "children": [ { - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "type": "Dimension", "unit": "px", "value": "5", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Value", }, }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Block", }, - "loc": null, - "name": "media", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "name": "media", + "parent": [Circular], "prelude": { "children": [ { @@ -1298,96 +1295,93 @@ exports[`populateParentsForNodeTree 1`] = ` "condition": { "children": [ { - "kind": "media", - "loc": null, - "name": "hover", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "kind": "media", + "loc": null, + "name": "hover", + "parent": [Circular], "type": "Feature", "value": { + "containedIn": undefined, + "containingItem": undefined, "loc": null, "name": "hover", "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Identifier", }, }, ], + "containedIn": undefined, + "containingItem": undefined, "kind": "media", "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Condition", }, - "loc": null, - "mediaType": null, - "modifier": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "mediaType": null, + "modifier": null, + "parent": [Circular], "type": "MediaQuery", }, ], - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "type": "MediaQueryList", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "AtrulePrelude", }, "type": "Atrule", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Block", }, + "containedIn": [Circular], + "containingItem": [Circular], "loc": null, "parent": [Circular], - "parentList": [Circular], - "parentListItem": [Circular], "prelude": { "children": [ { "children": [ { - "loc": null, - "name": "qux", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": { "data": { "children": null, + "containedIn": [Circular], + "containingItem": [Circular], "loc": null, "name": "hover", "parent": [Circular], - "parentList": [Circular], - "parentListItem": [Circular], "type": "PseudoClassSelector", }, "next": null, @@ -1395,48 +1389,51 @@ exports[`populateParentsForNodeTree 1`] = ` }, "prev": null, }, + "loc": null, + "name": "qux", + "parent": [Circular], "type": "ClassSelector", }, { "children": null, - "loc": null, - "name": "hover", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": { "data": { + "containedIn": [Circular], + "containingItem": [Circular], "loc": null, "name": "qux", "parent": [Circular], - "parentList": [Circular], - "parentListItem": [Circular], "type": "ClassSelector", }, "next": [Circular], "prev": null, }, }, + "loc": null, + "name": "hover", + "parent": [Circular], "type": "PseudoClassSelector", }, ], - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "type": "Selector", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "SelectorList", }, "type": "Rule", @@ -1449,82 +1446,82 @@ exports[`populateParentsForNodeTree 1`] = ` "block": { "children": [ { - "important": false, - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "important": false, + "loc": null, + "parent": [Circular], "property": "background", "type": "Declaration", "value": { "children": [ { - "loc": null, - "name": "blue", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "name": "blue", + "parent": [Circular], "type": "Identifier", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Value", }, }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Block", }, + "containedIn": [Circular], + "containingItem": [Circular], "loc": null, "parent": [Circular], - "parentList": [Circular], - "parentListItem": [Circular], "prelude": { "children": [ { "children": [ { - "loc": null, - "name": "bar", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "name": "bar", + "parent": [Circular], "type": "ClassSelector", }, ], - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "type": "Selector", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "SelectorList", }, "type": "Rule", @@ -1535,82 +1532,82 @@ exports[`populateParentsForNodeTree 1`] = ` "block": { "children": [ { - "important": false, - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "important": false, + "loc": null, + "parent": [Circular], "property": "color", "type": "Declaration", "value": { "children": [ { - "loc": null, - "name": "red", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "name": "red", + "parent": [Circular], "type": "Identifier", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Value", }, }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Block", }, + "containedIn": [Circular], + "containingItem": [Circular], "loc": null, "parent": [Circular], - "parentList": [Circular], - "parentListItem": [Circular], "prelude": { "children": [ { "children": [ { - "loc": null, - "name": "foo", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "name": "foo", + "parent": [Circular], "type": "ClassSelector", }, ], - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "type": "Selector", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "SelectorList", }, "type": "Rule", @@ -1620,6 +1617,9 @@ exports[`populateParentsForNodeTree 1`] = ` }, }, }, + "loc": null, + "name": "media", + "parent": [Circular], "prelude": { "children": [ { @@ -1628,63 +1628,63 @@ exports[`populateParentsForNodeTree 1`] = ` "condition": { "children": [ { - "kind": "media", - "loc": null, - "name": "min-width", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "kind": "media", + "loc": null, + "name": "min-width", + "parent": [Circular], "type": "Feature", "value": { + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Dimension", "unit": "px", "value": "600", }, }, ], + "containedIn": undefined, + "containingItem": undefined, "kind": "media", "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Condition", }, - "loc": null, - "mediaType": null, - "modifier": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "mediaType": null, + "modifier": null, + "parent": [Circular], "type": "MediaQuery", }, ], - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "type": "MediaQueryList", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "AtrulePrelude", }, "type": "Atrule", @@ -1696,56 +1696,56 @@ exports[`populateParentsForNodeTree 1`] = ` "block": { "children": [ { - "important": false, - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "important": false, + "loc": null, + "parent": [Circular], "property": "padding", "type": "Declaration", "value": { "children": [ { - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "type": "Dimension", "unit": "px", "value": "5", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Value", }, }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Block", }, - "loc": null, - "name": "media", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "name": "media", + "parent": [Circular], "prelude": { "children": [ { @@ -1754,77 +1754,75 @@ exports[`populateParentsForNodeTree 1`] = ` "condition": { "children": [ { - "kind": "media", - "loc": null, - "name": "hover", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "kind": "media", + "loc": null, + "name": "hover", + "parent": [Circular], "type": "Feature", "value": { + "containedIn": undefined, + "containingItem": undefined, "loc": null, "name": "hover", "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Identifier", }, }, ], + "containedIn": undefined, + "containingItem": undefined, "kind": "media", "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Condition", }, - "loc": null, - "mediaType": null, - "modifier": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "mediaType": null, + "modifier": null, + "parent": [Circular], "type": "MediaQuery", }, ], - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "type": "MediaQueryList", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "AtrulePrelude", }, "type": "Atrule", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Block", }, - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": { @@ -1835,103 +1833,103 @@ exports[`populateParentsForNodeTree 1`] = ` "block": { "children": [ { - "important": false, - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "important": false, + "loc": null, + "parent": [Circular], "property": "margin", "type": "Declaration", "value": { "children": [ { - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "type": "Dimension", "unit": "px", "value": "10", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Value", }, }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Block", }, - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "prelude": { "children": [ { "children": [ { - "loc": null, - "name": "baz", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "name": "baz", + "parent": [Circular], "type": "ClassSelector", }, ], - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "type": "Selector", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "SelectorList", }, "type": "Rule", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Block", }, + "containedIn": [Circular], + "containingItem": [Circular], "loc": null, "name": "media", "parent": [Circular], - "parentList": [Circular], - "parentListItem": [Circular], "prelude": { "children": [ { @@ -1940,63 +1938,63 @@ exports[`populateParentsForNodeTree 1`] = ` "condition": { "children": [ { - "kind": "media", - "loc": null, - "name": "min-width", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "kind": "media", + "loc": null, + "name": "min-width", + "parent": [Circular], "type": "Feature", "value": { + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Dimension", "unit": "px", "value": "600", }, }, ], + "containedIn": undefined, + "containingItem": undefined, "kind": "media", "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Condition", }, - "loc": null, - "mediaType": null, - "modifier": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "mediaType": null, + "modifier": null, + "parent": [Circular], "type": "MediaQuery", }, ], - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "type": "MediaQueryList", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "AtrulePrelude", }, "type": "Atrule", @@ -2007,82 +2005,82 @@ exports[`populateParentsForNodeTree 1`] = ` "block": { "children": [ { - "important": false, - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "important": false, + "loc": null, + "parent": [Circular], "property": "background", "type": "Declaration", "value": { "children": [ { - "loc": null, - "name": "blue", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "name": "blue", + "parent": [Circular], "type": "Identifier", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Value", }, }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Block", }, + "containedIn": [Circular], + "containingItem": [Circular], "loc": null, "parent": [Circular], - "parentList": [Circular], - "parentListItem": [Circular], "prelude": { "children": [ { "children": [ { - "loc": null, - "name": "bar", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "name": "bar", + "parent": [Circular], "type": "ClassSelector", }, ], - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "type": "Selector", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "SelectorList", }, "type": "Rule", @@ -2093,82 +2091,82 @@ exports[`populateParentsForNodeTree 1`] = ` "block": { "children": [ { - "important": false, - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "important": false, + "loc": null, + "parent": [Circular], "property": "color", "type": "Declaration", "value": { "children": [ { - "loc": null, - "name": "red", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "name": "red", + "parent": [Circular], "type": "Identifier", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Value", }, }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "Block", }, + "containedIn": [Circular], + "containingItem": [Circular], "loc": null, "parent": [Circular], - "parentList": [Circular], - "parentListItem": [Circular], "prelude": { "children": [ { "children": [ { - "loc": null, - "name": "foo", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "name": "foo", + "parent": [Circular], "type": "ClassSelector", }, ], - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "type": "Selector", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "SelectorList", }, "type": "Rule", @@ -2179,25 +2177,24 @@ exports[`populateParentsForNodeTree 1`] = ` }, }, }, + "loc": null, + "parent": [Circular], "prelude": { "children": [ { "children": [ { - "loc": null, - "name": "qux", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": { "data": { "children": null, + "containedIn": [Circular], + "containingItem": [Circular], "loc": null, "name": "hover", "parent": [Circular], - "parentList": [Circular], - "parentListItem": [Circular], "type": "PseudoClassSelector", }, "next": null, @@ -2205,57 +2202,60 @@ exports[`populateParentsForNodeTree 1`] = ` }, "prev": null, }, + "loc": null, + "name": "qux", + "parent": [Circular], "type": "ClassSelector", }, { "children": null, - "loc": null, - "name": "hover", - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": { "data": { + "containedIn": [Circular], + "containingItem": [Circular], "loc": null, "name": "qux", "parent": [Circular], - "parentList": [Circular], - "parentListItem": [Circular], "type": "ClassSelector", }, "next": [Circular], "prev": null, }, }, + "loc": null, + "name": "hover", + "parent": [Circular], "type": "PseudoClassSelector", }, ], - "loc": null, - "parent": [Circular], - "parentList": [Circular], - "parentListItem": { + "containedIn": [Circular], + "containingItem": { "data": [Circular], "next": null, "prev": null, }, + "loc": null, + "parent": [Circular], "type": "Selector", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": [Circular], - "parentList": undefined, - "parentListItem": undefined, "type": "SelectorList", }, "type": "Rule", }, ], + "containedIn": undefined, + "containingItem": undefined, "loc": null, "parent": undefined, - "parentList": undefined, - "parentListItem": undefined, "type": "StyleSheet", } `; From 5f662313ef5fb471ce3346c59f9feb9c520bbc5e Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Mon, 8 Sep 2025 17:51:31 -0300 Subject: [PATCH 066/193] update snapshots --- .../src/__snapshots__/tailwind.spec.tsx.snap | 18 +++++++++--------- ...sanitize-non-inlinable-classes.spec.ts.snap | 4 ++-- .../generate-root-for-classes.spec.ts.snap | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap b/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap index dbc6113692..ebc9d1ab5b 100644 --- a/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap +++ b/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap @@ -20,7 +20,7 @@ exports[`Tailwind component > should preserve mso styles 1`] = ` should recognize custom responsive screen 1`] = `
@@ -66,7 +66,7 @@ exports[`Tailwind component > should work with blocklist 1`] = ` @@ -81,7 +81,7 @@ exports[`Tailwind component > should work with calc() with + sign 1`] = `
with non-inlinable styles > should add css to @@ -192,7 +192,7 @@ exports[`Tailwind component > with non-inlinable styles > should persist existin @@ -237,7 +237,7 @@ exports[`Tailwind component > with non-inlinable styles > should work with arbit @@ -250,7 +250,7 @@ exports[`Tailwind component > with non-inlinable styles > should work with arbit " `; -exports[`Tailwind component > with non-inlinable styles > should work with arbitrarily deep (in the React tree) elements 2`] = `"
"`; +exports[`Tailwind component > with non-inlinable styles > should work with arbitrarily deep (in the React tree) elements 2`] = `"
"`; exports[`Tailwind component > with non-inlinable styles > should work with relatively complex media query utilities 1`] = ` " @@ -259,7 +259,7 @@ exports[`Tailwind component > with non-inlinable styles > should work with relat

diff --git a/packages/tailwind/src/utils/css/__snapshots__/sanitize-non-inlinable-classes.spec.ts.snap b/packages/tailwind/src/utils/css/__snapshots__/sanitize-non-inlinable-classes.spec.ts.snap index ef41b23353..e3a686eedf 100644 --- a/packages/tailwind/src/utils/css/__snapshots__/sanitize-non-inlinable-classes.spec.ts.snap +++ b/packages/tailwind/src/utils/css/__snapshots__/sanitize-non-inlinable-classes.spec.ts.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`sanitizeNonInlinableClasses() > should css nesting in hover pseudo styles 1`] = `":root,:host{}.hover_text-sky-600{&:hover{@media (hover:hover){color:oklch(58.8%0.158 241.966)!important}}}.sm_focus_outline-none{@media (width>=40rem){&:focus{outline-style:none!important}}}.md_hover_bg-gray-100{@media (width>=48rem){&:hover{@media (hover:hover){background-color:oklch(96.7%0.003 264.542)!important}}}}.lg_focus_underline{@media (width>=64rem){&:focus{text-decoration-line:underline!important}}}"`; +exports[`sanitizeNonInlinableClasses() > should css nesting in hover pseudo styles 1`] = `".hover_text-sky-600{&:hover{@media (hover:hover){color:oklch(58.8%0.158 241.966)!important}}}.sm_focus_outline-none{@media (width>=40rem){&:focus{outline-style:none!important}}}.md_hover_bg-gray-100{@media (width>=48rem){&:hover{@media (hover:hover){background-color:oklch(96.7%0.003 264.542)!important}}}}.lg_focus_underline{@media (width>=64rem){&:focus{text-decoration-line:underline!important}}}"`; exports[`sanitizeNonInlinableClasses() > should css nesting in hover pseudo styles 2`] = ` [ @@ -15,4 +15,4 @@ exports[`sanitizeNonInlinableClasses() > should handle rules that can be inlined exports[`sanitizeNonInlinableClasses() > should handle rules that can be inlined 2`] = `[]`; -exports[`sanitizeNonInlinableClasses() > shuold work with basic media query rules 1`] = `":root,:host{}.sm_mx-auto{@media (width>=40rem){margin-inline:auto!important}}.sm_max-w-lg{@media (width>=40rem){max-width:32rem!important}}.sm_rounded-lg{@media (width>=40rem){border-radius:0.5rem!important}}.md_px-10{@media (width>=48rem){padding-inline:2.5rem!important}}.md_py-12{@media (width>=48rem){padding-block:3rem!important}}"`; +exports[`sanitizeNonInlinableClasses() > shuold work with basic media query rules 1`] = `".sm_mx-auto{@media (width>=40rem){margin-inline:auto!important}}.sm_max-w-lg{@media (width>=40rem){max-width:32rem!important}}.sm_rounded-lg{@media (width>=40rem){border-radius:0.5rem!important}}.md_px-10{@media (width>=48rem){padding-inline:2.5rem!important}}.md_py-12{@media (width>=48rem){padding-block:3rem!important}}"`; diff --git a/packages/tailwind/src/utils/tailwindcss/__snapshots__/generate-root-for-classes.spec.ts.snap b/packages/tailwind/src/utils/tailwindcss/__snapshots__/generate-root-for-classes.spec.ts.snap index a7d61edb0a..c6a74fa9f2 100644 --- a/packages/tailwind/src/utils/tailwindcss/__snapshots__/generate-root-for-classes.spec.ts.snap +++ b/packages/tailwind/src/utils/tailwindcss/__snapshots__/generate-root-for-classes.spec.ts.snap @@ -1,3 +1,3 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`tailwind's generateRootForClasses() 1`] = `"/*! tailwindcss v4.1.12 | MIT License | https://tailwindcss.com */@layer theme,base,components,utilities;@layer theme{:root,:host{}}@layer utilities{.bg-slate-900{background-color:oklch(20.8%0.042 265.755)}.text-red-500{color:oklch(63.7%0.237 25.331)}.sm\\:bg-blue-300{@media (width>=40rem){background-color:oklch(80.9%0.105 251.813)}}}"`; +exports[`tailwind's generateRootForClasses() 1`] = `"/*! tailwindcss v4.1.12 | MIT License | https://tailwindcss.com */@layer theme,base,components,utilities;@layer utilities{.bg-slate-900{background-color:oklch(20.8%0.042 265.755)}.text-red-500{color:oklch(63.7%0.237 25.331)}.sm\\:bg-blue-300{@media (width>=40rem){background-color:oklch(80.9%0.105 251.813)}}}"`; From bfcd9ac2f8bea6a6ca5f90b20f5f71d0fdd9ef47 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Mon, 8 Sep 2025 17:52:32 -0300 Subject: [PATCH 067/193] lint --- packages/tailwind/src/utils/css/resolve-all-css-variables.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/tailwind/src/utils/css/resolve-all-css-variables.ts b/packages/tailwind/src/utils/css/resolve-all-css-variables.ts index e40480994f..55eb1714af 100644 --- a/packages/tailwind/src/utils/css/resolve-all-css-variables.ts +++ b/packages/tailwind/src/utils/css/resolve-all-css-variables.ts @@ -2,7 +2,6 @@ import { type CssNode, type Declaration, generate, - List, parse, type Raw, type Value, From 224277111fd5ec59cc33fe366e435a92eb36e134 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Mon, 8 Sep 2025 17:54:20 -0300 Subject: [PATCH 068/193] update build process and dependencies placement --- packages/tailwind/copy-tailwind-types.mjs | 13 ----- packages/tailwind/package.json | 10 ++-- pnpm-lock.yaml | 61 ++--------------------- 3 files changed, 8 insertions(+), 76 deletions(-) delete mode 100644 packages/tailwind/copy-tailwind-types.mjs diff --git a/packages/tailwind/copy-tailwind-types.mjs b/packages/tailwind/copy-tailwind-types.mjs deleted file mode 100644 index 6fe7008614..0000000000 --- a/packages/tailwind/copy-tailwind-types.mjs +++ /dev/null @@ -1,13 +0,0 @@ -import { promises as fs } from 'node:fs'; -import path from 'node:path'; -import url from 'node:url'; - -const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); - -await fs.cp( - path.resolve(__dirname, './node_modules/tailwindcss/types'), - path.resolve(__dirname, './dist/tailwindcss'), - { - recursive: true, - }, -); diff --git a/packages/tailwind/package.json b/packages/tailwind/package.json index 59c249b279..95363ec6a3 100644 --- a/packages/tailwind/package.json +++ b/packages/tailwind/package.json @@ -23,8 +23,8 @@ }, "license": "MIT", "scripts": { - "build": "tsup src/index.ts --format esm,cjs --dts --external react", - "build:watch": "tsup src/index.ts --format esm,cjs --dts --external react --watch", + "build": "tsup-node src/index.ts --format esm,cjs --dts", + "build:watch": "tsup-node src/index.ts --format esm,cjs --dts--watch", "clean": "rm -rf dist", "test": "vitest run", "test:watch": "vitest" @@ -57,11 +57,8 @@ "@types/css-tree": "2.3.10", "@types/shelljs": "0.8.15", "@vitejs/plugin-react": "4.4.1", - "postcss": "8.5.3", - "postcss-selector-parser": "7.1.0", "react-dom": "^19", "shelljs": "0.9.2", - "tailwindcss": "4.1.12", "tsconfig": "workspace:*", "tsup": "8.4.0", "typescript": "5.8.3", @@ -73,6 +70,7 @@ "access": "public" }, "dependencies": { - "css-tree": "3.1.0" + "css-tree": "3.1.0", + "tailwindcss": "4.1.12" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8edf06f7df..349942bb6a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -936,6 +936,9 @@ importers: react: specifier: ^19.0.0 version: 19.0.0 + tailwindcss: + specifier: 4.1.12 + version: 4.1.12 devDependencies: '@react-email/button': specifier: workspace:^ @@ -970,27 +973,18 @@ importers: '@vitejs/plugin-react': specifier: 4.4.1 version: 4.4.1(vite@6.3.4(@types/node@22.14.1)(jiti@2.4.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.6.1)) - postcss: - specifier: 8.5.3 - version: 8.5.3 - postcss-selector-parser: - specifier: 7.1.0 - version: 7.1.0 react-dom: specifier: ^19.0.0 version: 19.0.0(react@19.0.0) shelljs: specifier: 0.9.2 version: 0.9.2 - tailwindcss: - specifier: 4.1.12 - version: 4.1.12 tsconfig: specifier: workspace:* version: link:../tsconfig tsup: specifier: 8.4.0 - version: 8.4.0(@microsoft/api-extractor@7.52.4(@types/node@22.14.1))(@swc/core@1.11.21)(jiti@2.4.2)(postcss@8.5.3)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.6.1) + version: 8.4.0(@microsoft/api-extractor@7.52.4(@types/node@22.14.1))(@swc/core@1.11.21)(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.6.1) typescript: specifier: 5.8.3 version: 5.8.3 @@ -7267,10 +7261,6 @@ packages: resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} engines: {node: '>=4'} - postcss-selector-parser@7.1.0: - resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==} - engines: {node: '>=4'} - postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} @@ -15767,15 +15757,6 @@ snapshots: optionalDependencies: postcss: 8.5.3 - postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.5.3)(tsx@4.20.3)(yaml@2.6.1): - dependencies: - lilconfig: 3.1.3 - optionalDependencies: - jiti: 2.4.2 - postcss: 8.5.3 - tsx: 4.20.3 - yaml: 2.6.1 - postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(yaml@2.6.1): dependencies: lilconfig: 3.1.3 @@ -15800,11 +15781,6 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-selector-parser@7.1.0: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - postcss-value-parser@4.2.0: {} postcss@8.4.31: @@ -17253,35 +17229,6 @@ snapshots: tslib@2.8.1: {} - tsup@8.4.0(@microsoft/api-extractor@7.52.4(@types/node@22.14.1))(@swc/core@1.11.21)(jiti@2.4.2)(postcss@8.5.3)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.6.1): - dependencies: - bundle-require: 5.1.0(esbuild@0.25.0) - cac: 6.7.14 - chokidar: 4.0.3 - consola: 3.4.2 - debug: 4.4.0 - esbuild: 0.25.0 - joycon: 3.1.1 - picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.4.2)(postcss@8.5.3)(tsx@4.20.3)(yaml@2.6.1) - resolve-from: 5.0.0 - rollup: 4.39.0 - source-map: 0.8.0-beta.0 - sucrase: 3.35.0 - tinyexec: 0.3.2 - tinyglobby: 0.2.13 - tree-kill: 1.2.2 - optionalDependencies: - '@microsoft/api-extractor': 7.52.4(@types/node@22.14.1) - '@swc/core': 1.11.21 - postcss: 8.5.3 - typescript: 5.8.3 - transitivePeerDependencies: - - jiti - - supports-color - - tsx - - yaml - tsup@8.4.0(@microsoft/api-extractor@7.52.4(@types/node@22.14.1))(@swc/core@1.11.21)(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.6.1): dependencies: bundle-require: 5.1.0(esbuild@0.25.0) From 7f23250ab75bbd195cc3e594227f370b13bc745f Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Tue, 9 Sep 2025 10:58:55 -0300 Subject: [PATCH 069/193] add import.meta to global on preview server running environment --- packages/preview-server/src/utils/run-bundled-code.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/preview-server/src/utils/run-bundled-code.ts b/packages/preview-server/src/utils/run-bundled-code.ts index e353e7e0f0..88773fccd2 100644 --- a/packages/preview-server/src/utils/run-bundled-code.ts +++ b/packages/preview-server/src/utils/run-bundled-code.ts @@ -1,9 +1,17 @@ import path from 'node:path'; +import url from 'node:url'; import vm from 'node:vm'; import { err, ok, type Result } from './result'; import { staticNodeModulesForVM } from './static-node-modules-for-vm'; export const createContext = (filename: string): vm.Context => { + const import_ = (specifier: string) => import(specifier); + + import_.meta = { + url: url.pathToFileURL(filename), + filename: filename, + dirname: path.dirname(filename), + }; return { ...global, console, @@ -26,6 +34,7 @@ export const createContext = (filename: string): vm.Context => { module: { exports: {}, }, + import: import_, __filename: filename, __dirname: path.dirname(filename), require: (specifiedModule: string) => { From 39f86a63d701aa3004ef1319772569a057946fe0 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Mon, 15 Sep 2025 14:07:45 -0300 Subject: [PATCH 070/193] undo workflow change --- .github/workflows/preview-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview-release.yml b/.github/workflows/preview-release.yml index 0ca3198fc6..3bb18e489d 100644 --- a/.github/workflows/preview-release.yml +++ b/.github/workflows/preview-release.yml @@ -38,7 +38,7 @@ jobs: cache-prefix: ${{ runner.os }}-turbo- provider: github - name: Run Build - run: pnpm turbo build --filter=./packages/* + run: pnpm build env: SPAM_ASSASSIN_HOST: ${{ secrets.SPAM_ASSASSIN_HOST }} SPAM_ASSASSIN_PORT: ${{ secrets.SPAM_ASSASSIN_PORT }} From 9d3674e8756fdab4bc8b1723937a5c3a4a6cc1c5 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Mon, 15 Sep 2025 14:08:23 -0300 Subject: [PATCH 071/193] remove tests to fix csstree issue --- packages/preview-server/src/utils/run-bundled-code.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/preview-server/src/utils/run-bundled-code.ts b/packages/preview-server/src/utils/run-bundled-code.ts index 88773fccd2..e353e7e0f0 100644 --- a/packages/preview-server/src/utils/run-bundled-code.ts +++ b/packages/preview-server/src/utils/run-bundled-code.ts @@ -1,17 +1,9 @@ import path from 'node:path'; -import url from 'node:url'; import vm from 'node:vm'; import { err, ok, type Result } from './result'; import { staticNodeModulesForVM } from './static-node-modules-for-vm'; export const createContext = (filename: string): vm.Context => { - const import_ = (specifier: string) => import(specifier); - - import_.meta = { - url: url.pathToFileURL(filename), - filename: filename, - dirname: path.dirname(filename), - }; return { ...global, console, @@ -34,7 +26,6 @@ export const createContext = (filename: string): vm.Context => { module: { exports: {}, }, - import: import_, __filename: filename, __dirname: path.dirname(filename), require: (specifiedModule: string) => { From 6bc4552b44465f5ae4e578a472e6933096c34bb9 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Mon, 15 Sep 2025 14:14:40 -0300 Subject: [PATCH 072/193] use nodeNext as module resolution instead of bundler --- packages/tailwind/tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/tailwind/tsconfig.json b/packages/tailwind/tsconfig.json index 23405a05c7..921500b54e 100644 --- a/packages/tailwind/tsconfig.json +++ b/packages/tailwind/tsconfig.json @@ -10,7 +10,8 @@ "integrations/nextjs" ], "compilerOptions": { - "moduleResolution": "bundler", + "moduleResolution": "nodenext", + "module": "nodenext", "strict": true, "noEmit": true } From 4459b4bcf46d8fd0e7e9f8b74c0b89e090a2eed7 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Mon, 15 Sep 2025 14:14:43 -0300 Subject: [PATCH 073/193] remove dangling tsup.config --- packages/tailwind/tsup.config.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 packages/tailwind/tsup.config.ts diff --git a/packages/tailwind/tsup.config.ts b/packages/tailwind/tsup.config.ts deleted file mode 100644 index e69de29bb2..0000000000 From 412cdf7bc630c809044434d9676eb6a2d6c8ebf7 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Tue, 16 Sep 2025 15:40:09 -0300 Subject: [PATCH 074/193] bundle css tree and hoist JSON imports throuhg a rolldown plugin --- packages/tailwind/package.json | 4 +-- packages/tailwind/tsdown.config.ts | 40 ++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 packages/tailwind/tsdown.config.ts diff --git a/packages/tailwind/package.json b/packages/tailwind/package.json index c26a0bcc4e..cf54552146 100644 --- a/packages/tailwind/package.json +++ b/packages/tailwind/package.json @@ -23,8 +23,8 @@ }, "license": "MIT", "scripts": { - "build": "tsup-node src/index.ts --format esm,cjs --dts", - "build:watch": "tsup-node src/index.ts --format esm,cjs --dts--watch", + "build": "tsdown", + "build:watch": "tsdown --watch", "clean": "rm -rf dist", "test": "vitest run", "test:watch": "vitest" diff --git a/packages/tailwind/tsdown.config.ts b/packages/tailwind/tsdown.config.ts new file mode 100644 index 0000000000..21af50b807 --- /dev/null +++ b/packages/tailwind/tsdown.config.ts @@ -0,0 +1,40 @@ +import { createRequire } from 'node:module'; +import url from 'node:url'; +import { defineConfig } from 'tsdown/config'; + +export default defineConfig({ + entry: './src/index.ts', + format: ['cjs', 'esm'], + dts: true, + noExternal: ['css-tree'], + plugins: [ + { + name: 'hoist-create-require-imports', + async transform(code, id, meta) { + if (id.includes('css-tree')) { + const localizedRequire = createRequire(url.pathToFileURL(id)); + + return { + code: code + .replaceAll("import { createRequire } from 'module';", '') + .replaceAll( + /(const|var|let)\s+require\s*=\s*createRequire\s*\([^)]*\)\s*;?/g, + '', + ) + .replaceAll( + /require\s*\(\s*(['"])([^)]+?\.json)\1\s*\)/gm, + (_match, _quote, jsonSpecifier) => { + return JSON.stringify( + localizedRequire(localizedRequire.resolve(jsonSpecifier)), + null, + 2, + ); + }, + ), + ...meta, + }; + } + }, + }, + ], +}); From 49814c0d676921e273473a01d60eee7f923efa16 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Wed, 17 Sep 2025 16:39:18 -0300 Subject: [PATCH 075/193] only use a single compiler from tailwindcss, and also reuse the same CSS for it --- ...xtract-rules-matching-classes.spec.ts.snap | 23 +++++++ .../make-inline-styles-for.spec.ts.snap | 7 +- ...anitize-non-inlinable-classes.spec.ts.snap | 17 +---- .../sanitize-non-inlinable-rules.spec.ts.snap | 7 ++ .../extract-rules-matching-classes.spec.ts | 56 ++++++++++++++++ .../css/extract-rules-matching-classes.ts | 44 ++++++++++++ .../utils/css/make-inline-styles-for.spec.ts | 13 ++-- .../src/utils/css/make-inline-styles-for.ts | 52 ++++---------- .../sanitize-non-inlinable-classes.spec.ts | 67 ------------------- .../css/sanitize-non-inlinable-rules.spec.ts | 45 +++++++++++++ ...ses.ts => sanitize-non-inlinable-rules.ts} | 35 ++-------- .../generate-root-for-classes.spec.ts.snap | 3 - .../__snapshots__/setup-tailwind.spec.ts.snap | 5 ++ .../clone-element-with-inlined-styles.ts | 65 ++++++++++-------- .../generate-root-for-classes.spec.ts | 13 ---- .../utils/tailwindcss/setup-tailwind.spec.ts | 18 +++++ ...-root-for-classes.ts => setup-tailwind.ts} | 35 ++++++---- 17 files changed, 287 insertions(+), 218 deletions(-) create mode 100644 packages/tailwind/src/utils/css/__snapshots__/extract-rules-matching-classes.spec.ts.snap create mode 100644 packages/tailwind/src/utils/css/__snapshots__/sanitize-non-inlinable-rules.spec.ts.snap create mode 100644 packages/tailwind/src/utils/css/extract-rules-matching-classes.spec.ts create mode 100644 packages/tailwind/src/utils/css/extract-rules-matching-classes.ts delete mode 100644 packages/tailwind/src/utils/css/sanitize-non-inlinable-classes.spec.ts create mode 100644 packages/tailwind/src/utils/css/sanitize-non-inlinable-rules.spec.ts rename packages/tailwind/src/utils/css/{sanitize-non-inlinable-classes.ts => sanitize-non-inlinable-rules.ts} (52%) delete mode 100644 packages/tailwind/src/utils/tailwindcss/__snapshots__/generate-root-for-classes.spec.ts.snap create mode 100644 packages/tailwind/src/utils/tailwindcss/__snapshots__/setup-tailwind.spec.ts.snap delete mode 100644 packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.spec.ts create mode 100644 packages/tailwind/src/utils/tailwindcss/setup-tailwind.spec.ts rename packages/tailwind/src/utils/tailwindcss/{generate-root-for-classes.ts => setup-tailwind.ts} (63%) diff --git a/packages/tailwind/src/utils/css/__snapshots__/extract-rules-matching-classes.spec.ts.snap b/packages/tailwind/src/utils/css/__snapshots__/extract-rules-matching-classes.spec.ts.snap new file mode 100644 index 0000000000..db442b075e --- /dev/null +++ b/packages/tailwind/src/utils/css/__snapshots__/extract-rules-matching-classes.spec.ts.snap @@ -0,0 +1,23 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`extractRulesMatchingClasses() > should work just inlinable utilities 1`] = ` +{ + "bg-red-500": ".bg-red-500{background-color:rgb(250.6,43.8,54.3)}", + "text-center": ".text-center{text-align:center}", +} +`; + +exports[`extractRulesMatchingClasses() > should work with a mix of inlinable and non-inlinable utilities 1`] = ` +{ + "bg-red-500": ".bg-red-500{background-color:rgb(250.6,43.8,54.3)}", + "lg:w-1/2": ".lg_w-1_2{@media (width>=64rem){width:calc(0.5*100%)!important}}", + "text-center": ".text-center{text-align:center}", + "w-full": ".w-full{width:100%}", +} +`; + +exports[`extractRulesMatchingClasses() > should work with non-inlinable utilities 1`] = ` +{ + "lg:w-1/2": ".lg_w-1_2{@media (width>=64rem){width:calc(0.5*100%)!important}}", +} +`; diff --git a/packages/tailwind/src/utils/css/__snapshots__/make-inline-styles-for.spec.ts.snap b/packages/tailwind/src/utils/css/__snapshots__/make-inline-styles-for.spec.ts.snap index 75723cfdb0..a8d9b53aa9 100644 --- a/packages/tailwind/src/utils/css/__snapshots__/make-inline-styles-for.spec.ts.snap +++ b/packages/tailwind/src/utils/css/__snapshots__/make-inline-styles-for.spec.ts.snap @@ -2,10 +2,7 @@ exports[`makeInlineStylesFor() 1`] = ` { - "residualClassName": "sm:bg-blue-300 md:max-w-[400px] my-custom-class", - "styles": { - "backgroundColor": "oklch(63.7%0.237 25.331)", - "width": "100%", - }, + "backgroundColor": "#f56565", + "width": "100%", } `; diff --git a/packages/tailwind/src/utils/css/__snapshots__/sanitize-non-inlinable-classes.spec.ts.snap b/packages/tailwind/src/utils/css/__snapshots__/sanitize-non-inlinable-classes.spec.ts.snap index e3a686eedf..7b70a75815 100644 --- a/packages/tailwind/src/utils/css/__snapshots__/sanitize-non-inlinable-classes.spec.ts.snap +++ b/packages/tailwind/src/utils/css/__snapshots__/sanitize-non-inlinable-classes.spec.ts.snap @@ -1,18 +1,7 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`sanitizeNonInlinableClasses() > should css nesting in hover pseudo styles 1`] = `".hover_text-sky-600{&:hover{@media (hover:hover){color:oklch(58.8%0.158 241.966)!important}}}.sm_focus_outline-none{@media (width>=40rem){&:focus{outline-style:none!important}}}.md_hover_bg-gray-100{@media (width>=48rem){&:hover{@media (hover:hover){background-color:oklch(96.7%0.003 264.542)!important}}}}.lg_focus_underline{@media (width>=64rem){&:focus{text-decoration-line:underline!important}}}"`; +exports[`sanitizeNonInlinableClasses() > should css nesting in hover pseudo styles 1`] = `"/*! tailwindcss v4.1.12 | MIT License | https://tailwindcss.com */@layer theme,base,components,utilities;@layer theme{:root,:host{--color-sky-600: oklch(58.8% 0.158 241.966)!important;--color-gray-100: oklch(96.7% 0.003 264.542)!important}}@layer utilities{.hover_text-sky-600{&:hover{@media (hover:hover){color:var(--color-sky-600)!important}}}.sm_focus_outline-none{@media (width>=40rem){&:focus{--tw-outline-style: none!important;outline-style:none!important}}}.md_hover_bg-gray-100{@media (width>=48rem){&:hover{@media (hover:hover){background-color:var(--color-gray-100)!important}}}}.lg_focus_underline{@media (width>=64rem){&:focus{text-decoration-line:underline!important}}}}"`; -exports[`sanitizeNonInlinableClasses() > should css nesting in hover pseudo styles 2`] = ` -[ - "hover:text-sky-600", - "sm:focus:outline-none", - "md:hover:bg-gray-100", - "lg:focus:underline", -] -`; +exports[`sanitizeNonInlinableClasses() > should handle rules that can be inlined 1`] = `"/*! tailwindcss v4.1.12 | MIT License | https://tailwindcss.com */@layer theme,base,components,utilities;@layer theme{:root,:host{--color-red-300: oklch(80.8% 0.114 19.571)!important;--color-gray-900: oklch(21% 0.034 264.665)!important;--text-lg: 1.125rem!important;--text-lg--line-height: calc(1.75 / 1.125)!important}}@layer utilities{.bg-gray-900{background-color:var(--color-gray-900)}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading, var(--text-lg--line-height))}.text-red-300{color:var(--color-red-300)}}"`; -exports[`sanitizeNonInlinableClasses() > should handle rules that can be inlined 1`] = `""`; - -exports[`sanitizeNonInlinableClasses() > should handle rules that can be inlined 2`] = `[]`; - -exports[`sanitizeNonInlinableClasses() > shuold work with basic media query rules 1`] = `".sm_mx-auto{@media (width>=40rem){margin-inline:auto!important}}.sm_max-w-lg{@media (width>=40rem){max-width:32rem!important}}.sm_rounded-lg{@media (width>=40rem){border-radius:0.5rem!important}}.md_px-10{@media (width>=48rem){padding-inline:2.5rem!important}}.md_py-12{@media (width>=48rem){padding-block:3rem!important}}"`; +exports[`sanitizeNonInlinableClasses() > shuold work with basic media query rules 1`] = `"/*! tailwindcss v4.1.12 | MIT License | https://tailwindcss.com */@layer theme,base,components,utilities;@layer theme{:root,:host{--spacing: 0.25rem!important;--container-lg: 32rem!important;--radius-lg: 0.5rem!important}}@layer utilities{.sm_mx-auto{@media (width>=40rem){margin-inline:auto!important}}.sm_max-w-lg{@media (width>=40rem){max-width:var(--container-lg)!important}}.sm_rounded-lg{@media (width>=40rem){border-radius:var(--radius-lg)!important}}.md_px-10{@media (width>=48rem){padding-inline:calc(var(--spacing)*10)!important}}.md_py-12{@media (width>=48rem){padding-block:calc(var(--spacing)*12)!important}}}"`; diff --git a/packages/tailwind/src/utils/css/__snapshots__/sanitize-non-inlinable-rules.spec.ts.snap b/packages/tailwind/src/utils/css/__snapshots__/sanitize-non-inlinable-rules.spec.ts.snap new file mode 100644 index 0000000000..222406246c --- /dev/null +++ b/packages/tailwind/src/utils/css/__snapshots__/sanitize-non-inlinable-rules.spec.ts.snap @@ -0,0 +1,7 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`sanitizeNonInlinableRules() > should css nesting in hover pseudo styles 1`] = `"/*! tailwindcss v4.1.12 | MIT License | https://tailwindcss.com */@layer theme,base,components,utilities;@layer theme{:root,:host{--color-sky-600: oklch(58.8% 0.158 241.966)!important;--color-gray-100: oklch(96.7% 0.003 264.542)!important}}@layer utilities{.hover_text-sky-600{&:hover{@media (hover:hover){color:var(--color-sky-600)!important}}}.sm_focus_outline-none{@media (width>=40rem){&:focus{--tw-outline-style: none!important;outline-style:none!important}}}.md_hover_bg-gray-100{@media (width>=48rem){&:hover{@media (hover:hover){background-color:var(--color-gray-100)!important}}}}.lg_focus_underline{@media (width>=64rem){&:focus{text-decoration-line:underline!important}}}}"`; + +exports[`sanitizeNonInlinableRules() > should handle rules that can be inlined 1`] = `"/*! tailwindcss v4.1.12 | MIT License | https://tailwindcss.com */@layer theme,base,components,utilities;@layer theme{:root,:host{--color-red-300: oklch(80.8% 0.114 19.571)!important;--color-gray-900: oklch(21% 0.034 264.665)!important;--text-lg: 1.125rem!important;--text-lg--line-height: calc(1.75 / 1.125)!important}}@layer utilities{.bg-gray-900{background-color:var(--color-gray-900)}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading, var(--text-lg--line-height))}.text-red-300{color:var(--color-red-300)}}"`; + +exports[`sanitizeNonInlinableRules() > shuold work with basic media query rules 1`] = `"/*! tailwindcss v4.1.12 | MIT License | https://tailwindcss.com */@layer theme,base,components,utilities;@layer theme{:root,:host{--spacing: 0.25rem!important;--container-lg: 32rem!important;--radius-lg: 0.5rem!important}}@layer utilities{.sm_mx-auto{@media (width>=40rem){margin-inline:auto!important}}.sm_max-w-lg{@media (width>=40rem){max-width:var(--container-lg)!important}}.sm_rounded-lg{@media (width>=40rem){border-radius:var(--radius-lg)!important}}.md_px-10{@media (width>=48rem){padding-inline:calc(var(--spacing)*10)!important}}.md_py-12{@media (width>=48rem){padding-block:calc(var(--spacing)*12)!important}}}"`; diff --git a/packages/tailwind/src/utils/css/extract-rules-matching-classes.spec.ts b/packages/tailwind/src/utils/css/extract-rules-matching-classes.spec.ts new file mode 100644 index 0000000000..a57a53ceb8 --- /dev/null +++ b/packages/tailwind/src/utils/css/extract-rules-matching-classes.spec.ts @@ -0,0 +1,56 @@ +import { generate } from 'css-tree'; +import { setupTailwind } from '../tailwindcss/setup-tailwind'; +import { extractRulesMatchingStyles } from './extract-rules-matching-classes'; + +describe('extractRulesMatchingClasses()', async () => { + it('should work just inlinable utilities', async () => { + const tailwind = await setupTailwind({}); + const classes = ['text-center', 'bg-red-500']; + const cssNode = tailwind.aggregateIntoCss(classes); + tailwind.dealWithCompatibilityIssues(cssNode); + const rules = extractRulesMatchingStyles(classes, cssNode); + const stringifiedRulesMap = Object.fromEntries( + rules + .entries() + .map(([className, { rule }]) => [className, generate(rule)]), + ); + + expect(stringifiedRulesMap).toMatchSnapshot(); + }); + + it('should work with non-inlinable utilities', async () => { + const tailwind = await setupTailwind({}); + const classes = ['lg:w-1/2']; + const cssNode = tailwind.aggregateIntoCss(classes); + tailwind.dealWithCompatibilityIssues(cssNode); + const rules = extractRulesMatchingStyles(classes, cssNode); + const stringifiedRulesMap = Object.fromEntries( + rules + .entries() + .map(([className, { rule }]) => [className, generate(rule)]), + ); + + expect(stringifiedRulesMap).toMatchSnapshot(); + }); + + it('should work with a mix of inlinable and non-inlinable utilities', async () => { + const tailwind = await setupTailwind({}); + const classes = [ + 'text-center', + 'bg-red-500', + 'some-other-class', // should be ignored + 'w-full', + 'lg:w-1/2', + ]; + const cssNode = tailwind.aggregateIntoCss(classes); + tailwind.dealWithCompatibilityIssues(cssNode); + const rules = extractRulesMatchingStyles(classes, cssNode); + const stringifiedRulesMap = Object.fromEntries( + rules + .entries() + .map(([className, { rule }]) => [className, generate(rule)]), + ); + + expect(stringifiedRulesMap).toMatchSnapshot(); + }); +}); diff --git a/packages/tailwind/src/utils/css/extract-rules-matching-classes.ts b/packages/tailwind/src/utils/css/extract-rules-matching-classes.ts new file mode 100644 index 0000000000..a9a6ead9bf --- /dev/null +++ b/packages/tailwind/src/utils/css/extract-rules-matching-classes.ts @@ -0,0 +1,44 @@ +import { type CssNode, generate, string, walk } from 'css-tree'; +import { sanitizeClassName } from '../compatibility/sanitize-class-name'; +import { isRuleInlinable } from './is-rule-inlinable'; + +export const extractRulesMatchingStyles = ( + classes: string[], + root: CssNode, +) => { + const rules = new Map< + string, + { + rule: CssNode; + inlinable: boolean; + } + >(); + walk(root, { + visit: 'Rule', + enter(rule) { + const selector = generate(rule.prelude); + if (isRuleInlinable(rule)) { + const matchingClass = classes.find((cls) => + string.decode(selector).includes(`.${cls}`), + ); + if (matchingClass) { + rules.set(matchingClass, { + rule, + inlinable: true, + }); + } + } else { + const matchingClass = classes.find((cls) => + string.decode(selector).includes(`.${sanitizeClassName(cls)}`), + ); + if (matchingClass) { + rules.set(matchingClass, { + rule, + inlinable: false, + }); + } + } + }, + }); + return rules; +}; diff --git a/packages/tailwind/src/utils/css/make-inline-styles-for.spec.ts b/packages/tailwind/src/utils/css/make-inline-styles-for.spec.ts index dc060eef08..7c6aa50295 100644 --- a/packages/tailwind/src/utils/css/make-inline-styles-for.spec.ts +++ b/packages/tailwind/src/utils/css/make-inline-styles-for.spec.ts @@ -1,10 +1,13 @@ -import { generateRootForClasses } from '../tailwindcss/generate-root-for-classes'; +import { parse, type StyleSheet } from 'css-tree'; import { makeInlineStylesFor } from './make-inline-styles-for'; test('makeInlineStylesFor()', async () => { - const className = - 'bg-red-500 sm:bg-blue-300 w-full md:max-w-[400px] my-custom-class'; - const tailwindStyles = await generateRootForClasses(className.split(' '), {}); + const tailwindStyles = parse(` + .bg-red-500 { background-color: #f56565; } + .w-full { width: 100%; } + `) as StyleSheet; - expect(makeInlineStylesFor(className, tailwindStyles)).toMatchSnapshot(); + expect( + makeInlineStylesFor(tailwindStyles.children.toArray()), + ).toMatchSnapshot(); }); diff --git a/packages/tailwind/src/utils/css/make-inline-styles-for.ts b/packages/tailwind/src/utils/css/make-inline-styles-for.ts index 1fe535acb9..d4f84104b1 100644 --- a/packages/tailwind/src/utils/css/make-inline-styles-for.ts +++ b/packages/tailwind/src/utils/css/make-inline-styles-for.ts @@ -1,47 +1,19 @@ import { type CssNode, generate, walk } from 'css-tree'; import { convertCssPropertyToReactProperty } from '../compatibility/convert-css-property-to-react-property'; -import { unescapeClass } from '../compatibility/unescape-class'; -import { isRuleInlinable } from './is-rule-inlinable'; -export function makeInlineStylesFor( - className: string, - tailwindStyles: CssNode, -) { - const classes = className.split(' '); - - let residualClasses = [...classes]; +export function makeInlineStylesFor(inlinableRules: CssNode[]) { const styles: Record = {}; - walk(tailwindStyles, { - visit: 'Rule', - enter(rule) { - if (isRuleInlinable(rule)) { - const classesOnSelector: string[] = []; - walk(rule.prelude, { - visit: 'ClassSelector', - enter(classNode) { - classesOnSelector.push(unescapeClass(classNode.name)); - }, - }); - - residualClasses = residualClasses.filter((singleClass) => { - return !classesOnSelector.includes(singleClass); - }); - - walk(rule, { - visit: 'Declaration', - enter(declaration) { - styles[convertCssPropertyToReactProperty(declaration.property)] = - generate(declaration.value) + - (declaration.important ? '!important' : ''); - }, - }); - } - }, - }); + for (const rule of inlinableRules) { + walk(rule, { + visit: 'Declaration', + enter(declaration) { + styles[convertCssPropertyToReactProperty(declaration.property)] = + generate(declaration.value) + + (declaration.important ? '!important' : ''); + }, + }); + } - return { - styles, - residualClassName: residualClasses.join(' '), - }; + return styles; } diff --git a/packages/tailwind/src/utils/css/sanitize-non-inlinable-classes.spec.ts b/packages/tailwind/src/utils/css/sanitize-non-inlinable-classes.spec.ts deleted file mode 100644 index af1ff807ed..0000000000 --- a/packages/tailwind/src/utils/css/sanitize-non-inlinable-classes.spec.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { type CssNode, generate, List, type StyleSheet } from 'css-tree'; -import { generateRootForClasses } from '../tailwindcss/generate-root-for-classes'; -import { sanitizeNonInlinableClasses } from './sanitize-non-inlinable-classes'; - -describe('sanitizeNonInlinableClasses()', async () => { - it('should handle rules that can be inlined', async () => { - const stylesheet = await generateRootForClasses( - ['bg-gray-900 text-red-300 text-lg'], - {}, - ); - - const { nonInlinableClasses, sanitizedRules } = - sanitizeNonInlinableClasses(stylesheet); - expect( - generate({ - type: 'StyleSheet', - children: new List().fromArray(sanitizedRules), - } satisfies StyleSheet), - ).toMatchSnapshot(); - expect(nonInlinableClasses).toMatchSnapshot(); - }); - - it('should css nesting in hover pseudo styles', async () => { - const stylesheet = await generateRootForClasses( - [ - 'hover:text-sky-600', - 'sm:focus:outline-none', - 'md:hover:bg-gray-100', - 'lg:focus:underline', - ], - {}, - ); - - const { nonInlinableClasses, sanitizedRules } = - sanitizeNonInlinableClasses(stylesheet); - expect( - generate({ - type: 'StyleSheet', - children: new List().fromArray(sanitizedRules), - } satisfies StyleSheet), - ).toMatchSnapshot(); - expect(nonInlinableClasses).toMatchSnapshot(); - }); - - it('shuold work with basic media query rules', async () => { - const stylesheet = await generateRootForClasses( - ['sm:mx-auto', 'sm:max-w-lg', 'sm:rounded-lg', 'md:px-10', 'md:py-12'], - {}, - ); - - const { nonInlinableClasses, sanitizedRules } = - sanitizeNonInlinableClasses(stylesheet); - expect( - generate({ - type: 'StyleSheet', - children: new List().fromArray(sanitizedRules), - } satisfies StyleSheet), - ).toMatchSnapshot(); - expect(nonInlinableClasses).toEqual([ - 'sm:mx-auto', - 'sm:max-w-lg', - 'sm:rounded-lg', - 'md:px-10', - 'md:py-12', - ]); - }); -}); diff --git a/packages/tailwind/src/utils/css/sanitize-non-inlinable-rules.spec.ts b/packages/tailwind/src/utils/css/sanitize-non-inlinable-rules.spec.ts new file mode 100644 index 0000000000..399ec0752e --- /dev/null +++ b/packages/tailwind/src/utils/css/sanitize-non-inlinable-rules.spec.ts @@ -0,0 +1,45 @@ +import { generate } from 'css-tree'; +import { setupTailwind } from '../tailwindcss/setup-tailwind'; +import { sanitizeNonInlinableRules } from './sanitize-non-inlinable-rules'; + +describe('sanitizeNonInlinableRules()', () => { + it('should handle rules that can be inlined', async () => { + const tailwind = await setupTailwind({}); + const stylesheet = tailwind.aggregateIntoCss([ + 'bg-gray-900', + 'text-red-300', + 'text-lg', + ]); + + sanitizeNonInlinableRules(stylesheet); + expect(generate(stylesheet)).toMatchSnapshot(); + }); + + it('should css nesting in hover pseudo styles', async () => { + const tailwind = await setupTailwind({}); + const stylesheet = tailwind.aggregateIntoCss([ + 'hover:text-sky-600', + 'sm:focus:outline-none', + 'md:hover:bg-gray-100', + 'lg:focus:underline', + ]); + + sanitizeNonInlinableRules(stylesheet); + expect(generate(stylesheet)).toMatchSnapshot(); + }); + + it('shuold work with basic media query rules', async () => { + const tailwind = await setupTailwind({}); + const stylesheet = tailwind.aggregateIntoCss([ + 'sm:mx-auto', + 'sm:max-w-lg', + 'sm:rounded-lg', + 'md:px-10', + 'md:py-12', + ]); + + sanitizeNonInlinableRules(stylesheet); + + expect(generate(stylesheet)).toMatchSnapshot(); + }); +}); diff --git a/packages/tailwind/src/utils/css/sanitize-non-inlinable-classes.ts b/packages/tailwind/src/utils/css/sanitize-non-inlinable-rules.ts similarity index 52% rename from packages/tailwind/src/utils/css/sanitize-non-inlinable-classes.ts rename to packages/tailwind/src/utils/css/sanitize-non-inlinable-rules.ts index ffc47eb25f..f846396481 100644 --- a/packages/tailwind/src/utils/css/sanitize-non-inlinable-classes.ts +++ b/packages/tailwind/src/utils/css/sanitize-non-inlinable-rules.ts @@ -1,6 +1,5 @@ -import { type CssNode, type Rule, string, walk } from 'css-tree'; +import { type CssNode, string, walk } from 'css-tree'; import { sanitizeClassName } from '../compatibility/sanitize-class-name'; -import { clone } from './clone'; import { isRuleInlinable } from './is-rule-inlinable'; /** @@ -11,52 +10,26 @@ import { isRuleInlinable } from './is-rule-inlinable'; * What it does is: * 1. Converts all declarations in all rules into being important ones * 2. Sanitizes all the selectors of all non-inlinable rules - * 3. Merges at rules that have equivalent parameters */ -export const sanitizeNonInlinableClasses = (node: CssNode) => { - const sanitizedRules: Rule[] = []; - const nonInlinableClasses: string[] = []; - - let rootRule: Rule | undefined; - +export const sanitizeNonInlinableRules = (node: CssNode) => { walk(node, { visit: 'Rule', enter(rule) { - if (!rootRule) { - rootRule = rule; - } - }, - leave(rule) { - if (rootRule === rule) { - rootRule = undefined; - } if (!isRuleInlinable(rule)) { - const ruleToChange = !rootRule ? (clone(rule) as Rule) : rule; - - walk(ruleToChange.prelude, (node) => { + walk(rule.prelude, (node) => { if (node.type === 'ClassSelector') { const unescapedClassName = string.decode(node.name); - nonInlinableClasses.push(unescapedClassName); node.name = sanitizeClassName(unescapedClassName); } }); - walk(ruleToChange, { + walk(rule, { visit: 'Declaration', enter(declaration) { declaration.important = true; }, }); - - if (!rootRule) { - sanitizedRules.push(ruleToChange); - } } }, }); - - return { - nonInlinableClasses, - sanitizedRules, - }; }; diff --git a/packages/tailwind/src/utils/tailwindcss/__snapshots__/generate-root-for-classes.spec.ts.snap b/packages/tailwind/src/utils/tailwindcss/__snapshots__/generate-root-for-classes.spec.ts.snap deleted file mode 100644 index c6a74fa9f2..0000000000 --- a/packages/tailwind/src/utils/tailwindcss/__snapshots__/generate-root-for-classes.spec.ts.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`tailwind's generateRootForClasses() 1`] = `"/*! tailwindcss v4.1.12 | MIT License | https://tailwindcss.com */@layer theme,base,components,utilities;@layer utilities{.bg-slate-900{background-color:oklch(20.8%0.042 265.755)}.text-red-500{color:oklch(63.7%0.237 25.331)}.sm\\:bg-blue-300{@media (width>=40rem){background-color:oklch(80.9%0.105 251.813)}}}"`; diff --git a/packages/tailwind/src/utils/tailwindcss/__snapshots__/setup-tailwind.spec.ts.snap b/packages/tailwind/src/utils/tailwindcss/__snapshots__/setup-tailwind.spec.ts.snap new file mode 100644 index 0000000000..f921bbfe72 --- /dev/null +++ b/packages/tailwind/src/utils/tailwindcss/__snapshots__/setup-tailwind.spec.ts.snap @@ -0,0 +1,5 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`setupTailwind() and generateCssFrom() 1`] = `"/*! tailwindcss v4.1.12 | MIT License | https://tailwindcss.com */@layer theme,base,components,utilities;@layer theme{:root,:host{--color-red-500: oklch(63.7% 0.237 25.331);--color-blue-300: oklch(80.9% 0.105 251.813);--color-slate-900: oklch(20.8% 0.042 265.755)}}@layer utilities{.bg-slate-900{background-color:var(--color-slate-900)}.text-red-500{color:var(--color-red-500)}.sm\\:bg-blue-300{@media (width>=40rem){background-color:var(--color-blue-300)}}}"`; + +exports[`setupTailwind() and generateCssFrom() 2`] = `"/*! tailwindcss v4.1.12 | MIT License | https://tailwindcss.com */@layer theme,base,components,utilities;@layer theme{:root,:host{--color-red-100: oklch(93.6% 0.032 17.717);--color-red-500: oklch(63.7% 0.237 25.331);--color-blue-300: oklch(80.9% 0.105 251.813);--color-slate-900: oklch(20.8% 0.042 265.755)}}@layer utilities{.bg-red-100{background-color:var(--color-red-100)}.bg-slate-900{background-color:var(--color-slate-900)}.text-red-500{color:var(--color-red-500)}.sm\\:bg-blue-300{@media (width>=40rem){background-color:var(--color-blue-300)}}}"`; diff --git a/packages/tailwind/src/utils/tailwindcss/clone-element-with-inlined-styles.ts b/packages/tailwind/src/utils/tailwindcss/clone-element-with-inlined-styles.ts index ad5bcec4b4..c8c428098c 100644 --- a/packages/tailwind/src/utils/tailwindcss/clone-element-with-inlined-styles.ts +++ b/packages/tailwind/src/utils/tailwindcss/clone-element-with-inlined-styles.ts @@ -1,47 +1,56 @@ -import type { CssNode } from 'css-tree'; +import { generate, type CssNode } from 'css-tree'; import React from 'react'; -import type { Config } from 'tailwindcss'; import type { EmailElementProps } from '../../tailwind'; import { sanitizeClassName } from '../compatibility/sanitize-class-name'; import { makeInlineStylesFor } from '../css/make-inline-styles-for'; -import { sanitizeDeclarations } from '../css/sanitize-declarations'; -import { sanitizeNonInlinableClasses } from '../css/sanitize-non-inlinable-classes'; import { isComponent } from '../react/is-component'; -import { generateRootForClasses } from './generate-root-for-classes'; +import type { TailwindSetup } from './setup-tailwind'; +import { extractRulesMatchingStyles } from '../css/extract-rules-matching-classes'; -export const cloneElementWithInlinedStyles = async ( +export const cloneElementWithInlinedStyles = ( element: React.ReactElement, - config: Config, + tailwindSetup: TailwindSetup, ) => { const propsToOverwrite: Partial = {}; - let nonInlinableClasses: string[] = []; - let nonInlineStyleNodes: CssNode[] = []; + const nonInlinableClasses: string[] = []; + const nonInlineRules: CssNode[] = []; if (element.props.className) { - const rootForClasses = await generateRootForClasses( - element.props.className.split(' '), - config, - ); - sanitizeDeclarations(rootForClasses); + const classes = element.props.className.split(' '); + const cssNode = tailwindSetup.aggregateIntoCss(classes); + tailwindSetup.dealWithCompatibilityIssues(cssNode); - ({ - sanitizedRules: nonInlineStyleNodes, - nonInlinableClasses: nonInlinableClasses, - } = sanitizeNonInlinableClasses(rootForClasses)); + const rulePerClass = extractRulesMatchingStyles(classes, cssNode); - const { styles, residualClassName } = makeInlineStylesFor( - element.props.className, - rootForClasses, - ); + console.log(rulePerClass); + + /** Includes both user-defined classes that we need to persist, as well as classes for non-inlinable rules */ + const residualClasses: string[] = []; + for (const className of classes) { + const rule = rulePerClass.get(className); + if (rule === undefined || !rule.inlinable) { + residualClasses.push(className); + } + if (rule && !rule.inlinable) { + nonInlinableClasses.push(className); + } + } + console.log(residualClasses, nonInlinableClasses); + const inlinableRules: CssNode[] = rulePerClass + .values() + .filter((r) => r.inlinable) + .map((r) => r.rule) + .toArray(); + const styles = makeInlineStylesFor(inlinableRules); propsToOverwrite.style = { ...styles, ...element.props.style, }; if (!isComponent(element)) { - if (residualClassName.trim().length > 0) { - propsToOverwrite.className = residualClassName; + if (residualClasses.length > 0) { + propsToOverwrite.className = residualClasses.join(' '); /* We sanitize only the class names of Tailwind classes that we are not going to inline @@ -49,10 +58,10 @@ export const cloneElementWithInlinedStyles = async ( a user-defined class could end up also being sanitized which would lead to unexpected behavior and bugs that are hard to track. */ - for (const singleClass of nonInlinableClasses) { + for (const className of nonInlinableClasses) { propsToOverwrite.className = propsToOverwrite.className.replace( - singleClass, - sanitizeClassName(singleClass), + className, + sanitizeClassName(className), ); } } else { @@ -74,6 +83,6 @@ export const cloneElementWithInlinedStyles = async ( ), nonInlinableClasses, - nonInlineStyleNodes, + nonInlineRules, }; }; diff --git a/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.spec.ts b/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.spec.ts deleted file mode 100644 index 5538dfedd6..0000000000 --- a/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { generate } from 'css-tree'; -import { generateRootForClasses } from './generate-root-for-classes'; - -test("tailwind's generateRootForClasses()", async () => { - expect( - generate( - await generateRootForClasses( - ['text-red-500', 'sm:bg-blue-300', 'bg-slate-900'], - {}, - ), - ), - ).toMatchSnapshot(); -}); diff --git a/packages/tailwind/src/utils/tailwindcss/setup-tailwind.spec.ts b/packages/tailwind/src/utils/tailwindcss/setup-tailwind.spec.ts new file mode 100644 index 0000000000..6c79fbc35b --- /dev/null +++ b/packages/tailwind/src/utils/tailwindcss/setup-tailwind.spec.ts @@ -0,0 +1,18 @@ +import { generate } from 'css-tree'; +import { setupTailwind } from './setup-tailwind'; + +test("setupTailwind() and generateCssFrom()", async () => { + const { aggregateIntoCss } = await setupTailwind({}); + + expect( + generate( + aggregateIntoCss(['text-red-500', 'sm:bg-blue-300', 'bg-slate-900']), + ), + ).toMatchSnapshot(); + + expect( + generate( + aggregateIntoCss(['bg-red-100']), + ), + ).toMatchSnapshot(); +}); diff --git a/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.ts b/packages/tailwind/src/utils/tailwindcss/setup-tailwind.ts similarity index 63% rename from packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.ts rename to packages/tailwind/src/utils/tailwindcss/setup-tailwind.ts index 3bdb0b49a5..cd3be3c522 100644 --- a/packages/tailwind/src/utils/tailwindcss/generate-root-for-classes.ts +++ b/packages/tailwind/src/utils/tailwindcss/setup-tailwind.ts @@ -1,23 +1,23 @@ -import { parse } from 'css-tree'; +import { type CssNode, parse } from 'css-tree'; import { type Config, compile } from 'tailwindcss'; import { resolveAllCSSVariables } from '../css/resolve-all-css-variables'; import { resolveCalcExpressions } from '../css/resolve-calc-expressions'; +import { sanitizeDeclarations } from '../css/sanitize-declarations'; +import { sanitizeNonInlinableRules } from '../css/sanitize-non-inlinable-rules'; import indexCss from './tailwind-stylesheets/index'; import preflightCss from './tailwind-stylesheets/preflight'; import themeCss from './tailwind-stylesheets/theme'; import utilitiesCss from './tailwind-stylesheets/utilities'; -const baseCss = ` +export type TailwindSetup = Awaited>; + +export async function setupTailwind(config: Config) { + const baseCss = ` @layer theme, base, components, utilities; @import "tailwindcss/theme.css" layer(theme); @import "tailwindcss/utilities.css" layer(utilities); @config; `; - -export async function generateRootForClasses( - classes: string[], - config: Config, -) { const compiler = await compile(baseCss, { async loadModule(id, base, resourceHint) { if (resourceHint === 'config') { @@ -70,10 +70,21 @@ export async function generateRootForClasses( ); }, }); - const css = compiler.build(classes); - const root = parse(css); - resolveAllCSSVariables(root); - resolveCalcExpressions(root); - return root; + return { + /** + * @description Given a list of Tailwind classes, it generates the corresponding CSS. Also resolves simple `calc` functions, and css variables. + * The returned CSS also includes the CSS generated from all previous calls to this function, this is internal to Tailwind. + */ + aggregateIntoCss(classes: string[]) { + return parse(compiler.build(classes)); + }, + dealWithCompatibilityIssues(root: CssNode) { + resolveAllCSSVariables(root); + resolveCalcExpressions(root); + sanitizeDeclarations(root); + sanitizeNonInlinableRules(root); + return root; + }, + }; } From d18142c8b97539f96cd0228fa39df9f78a6f8bac Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Wed, 17 Sep 2025 16:39:51 -0300 Subject: [PATCH 076/193] remove console.logs --- .../src/utils/tailwindcss/clone-element-with-inlined-styles.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/tailwind/src/utils/tailwindcss/clone-element-with-inlined-styles.ts b/packages/tailwind/src/utils/tailwindcss/clone-element-with-inlined-styles.ts index c8c428098c..acf2a4b798 100644 --- a/packages/tailwind/src/utils/tailwindcss/clone-element-with-inlined-styles.ts +++ b/packages/tailwind/src/utils/tailwindcss/clone-element-with-inlined-styles.ts @@ -23,8 +23,6 @@ export const cloneElementWithInlinedStyles = ( const rulePerClass = extractRulesMatchingStyles(classes, cssNode); - console.log(rulePerClass); - /** Includes both user-defined classes that we need to persist, as well as classes for non-inlinable rules */ const residualClasses: string[] = []; for (const className of classes) { @@ -36,7 +34,6 @@ export const cloneElementWithInlinedStyles = ( nonInlinableClasses.push(className); } } - console.log(residualClasses, nonInlinableClasses); const inlinableRules: CssNode[] = rulePerClass .values() .filter((r) => r.inlinable) From b9d55d5564eaec674a9b3946e8ce5c462b56ba90 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Wed, 17 Sep 2025 16:41:31 -0300 Subject: [PATCH 077/193] fix non inline rules not being returned --- .../utils/tailwindcss/clone-element-with-inlined-styles.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/tailwind/src/utils/tailwindcss/clone-element-with-inlined-styles.ts b/packages/tailwind/src/utils/tailwindcss/clone-element-with-inlined-styles.ts index acf2a4b798..272b464d29 100644 --- a/packages/tailwind/src/utils/tailwindcss/clone-element-with-inlined-styles.ts +++ b/packages/tailwind/src/utils/tailwindcss/clone-element-with-inlined-styles.ts @@ -26,12 +26,13 @@ export const cloneElementWithInlinedStyles = ( /** Includes both user-defined classes that we need to persist, as well as classes for non-inlinable rules */ const residualClasses: string[] = []; for (const className of classes) { - const rule = rulePerClass.get(className); - if (rule === undefined || !rule.inlinable) { + const item = rulePerClass.get(className); + if (item === undefined || !item.inlinable) { residualClasses.push(className); } - if (rule && !rule.inlinable) { + if (item && !item.inlinable) { nonInlinableClasses.push(className); + nonInlineRules.push(item.rule); } } const inlinableRules: CssNode[] = rulePerClass From 32c3af03fa8ee3bbc640dd859479fc30cdb87d0c Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Wed, 17 Sep 2025 16:41:49 -0300 Subject: [PATCH 078/193] don't use async in mapReactTree anymore this should avoid the key issues, I think? This was the only change to the only portion that could be causing them --- .../src/utils/react/map-react-tree.ts | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/tailwind/src/utils/react/map-react-tree.ts b/packages/tailwind/src/utils/react/map-react-tree.ts index 48999cc468..001517fff0 100644 --- a/packages/tailwind/src/utils/react/map-react-tree.ts +++ b/packages/tailwind/src/utils/react/map-react-tree.ts @@ -16,16 +16,16 @@ import { isComponent } from './is-component'; export function mapReactTree( value: React.ReactNode, process: (node: React.ReactNode) => React.ReactNode, -): Promise { - const mapped = React.Children.map(value, async (node) => { +): React.ReactNode { + const mapped = React.Children.map(value, (node) => { if (React.isValidElement<{ children?: React.ReactNode }>(node)) { const newProps = { ...node.props }; if (node.props.children && !isComponent(node)) { - newProps.children = await mapReactTree(node.props.children, process); + newProps.children = mapReactTree(node.props.children, process); } - const processed = await process( + const processed = process( React.cloneElement(node, newProps, newProps.children), ); @@ -38,11 +38,11 @@ export function mapReactTree( const OriginalComponent = typeof processed.type === 'object' ? // @ts-expect-error - we know this is a component with a render function - (processed.type.render as React.FC) + (processed.type.render as React.FC) : (processed.type as React.FC); - const rendered = await OriginalComponent(processed.props); - const mappedRenderedNode = await mapReactTree(rendered, process); + const rendered = OriginalComponent(processed.props); + const mappedRenderedNode = mapReactTree(rendered, process); return mappedRenderedNode; } @@ -53,9 +53,5 @@ export function mapReactTree( return process(node); }); - if (mapped) { - return mapped.length === 1 ? mapped[0] : Promise.all(mapped); - } - - return Promise.resolve(mapped); + return mapped && mapped.length === 1 ? mapped[0] : mapped; } From d7de09cdbfa7f3221e76a8df4c955fb736a0f3e1 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Wed, 17 Sep 2025 16:42:16 -0300 Subject: [PATCH 079/193] use the new setupTailwind, and new functions for a speed up --- packages/tailwind/src/tailwind.tsx | 41 ++++++++++++++---------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/packages/tailwind/src/tailwind.tsx b/packages/tailwind/src/tailwind.tsx index 0b9b9fa8a5..e0b689d943 100644 --- a/packages/tailwind/src/tailwind.tsx +++ b/packages/tailwind/src/tailwind.tsx @@ -3,6 +3,7 @@ import * as React from 'react'; import type { Config } from 'tailwindcss'; import { mapReactTree } from './utils/react/map-react-tree'; import { cloneElementWithInlinedStyles } from './utils/tailwindcss/clone-element-with-inlined-styles'; +import { setupTailwind } from './utils/tailwindcss/setup-tailwind'; export type TailwindConfig = Omit; @@ -78,6 +79,8 @@ export const Tailwind: React.FC = async ({ children, config, }) => { + const tailwindSetup = await setupTailwind(config ?? {}); + const nonInlineStylesToApply: StyleSheet = { type: 'StyleSheet', children: new List(), @@ -86,31 +89,25 @@ export const Tailwind: React.FC = async ({ let hasNonInlineStylesToApply = false as boolean; - let mappedChildren: React.ReactNode = await mapReactTree( - children, - async (node) => { - if (React.isValidElement(node)) { - const { - elementWithInlinedStyles, - nonInlinableClasses, - nonInlineStyleNodes, - } = await cloneElementWithInlinedStyles(node, config ?? {}); - mediaQueryClassesForAllElement = - mediaQueryClassesForAllElement.concat(nonInlinableClasses); - for (const rule of nonInlineStyleNodes) { - nonInlineStylesToApply.children.appendData(rule); - } - - if (nonInlinableClasses.length > 0 && !hasNonInlineStylesToApply) { - hasNonInlineStylesToApply = true; - } + let mappedChildren: React.ReactNode = mapReactTree(children, (node) => { + if (React.isValidElement(node)) { + const { elementWithInlinedStyles, nonInlinableClasses, nonInlineRules } = + cloneElementWithInlinedStyles(node, tailwindSetup); + mediaQueryClassesForAllElement = + mediaQueryClassesForAllElement.concat(nonInlinableClasses); + for (const rule of nonInlineRules) { + nonInlineStylesToApply.children.appendData(rule); + } - return elementWithInlinedStyles; + if (nonInlinableClasses.length > 0 && !hasNonInlineStylesToApply) { + hasNonInlineStylesToApply = true; } - return node; - }, - ); + return elementWithInlinedStyles; + } + + return node; + }); if (hasNonInlineStylesToApply) { let hasAppliedNonInlineStyles = false as boolean; From e5f5730052002802966bff1f72d426fdb527b377 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Wed, 17 Sep 2025 17:04:10 -0300 Subject: [PATCH 080/193] lint --- packages/tailwind/src/utils/react/map-react-tree.ts | 2 +- .../tailwindcss/clone-element-with-inlined-styles.ts | 4 ++-- .../tailwind/src/utils/tailwindcss/setup-tailwind.spec.ts | 8 ++------ 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/tailwind/src/utils/react/map-react-tree.ts b/packages/tailwind/src/utils/react/map-react-tree.ts index 001517fff0..492dcac986 100644 --- a/packages/tailwind/src/utils/react/map-react-tree.ts +++ b/packages/tailwind/src/utils/react/map-react-tree.ts @@ -38,7 +38,7 @@ export function mapReactTree( const OriginalComponent = typeof processed.type === 'object' ? // @ts-expect-error - we know this is a component with a render function - (processed.type.render as React.FC) + (processed.type.render as React.FC) : (processed.type as React.FC); const rendered = OriginalComponent(processed.props); diff --git a/packages/tailwind/src/utils/tailwindcss/clone-element-with-inlined-styles.ts b/packages/tailwind/src/utils/tailwindcss/clone-element-with-inlined-styles.ts index 272b464d29..6b9a165e82 100644 --- a/packages/tailwind/src/utils/tailwindcss/clone-element-with-inlined-styles.ts +++ b/packages/tailwind/src/utils/tailwindcss/clone-element-with-inlined-styles.ts @@ -1,11 +1,11 @@ -import { generate, type CssNode } from 'css-tree'; +import type { CssNode } from 'css-tree'; import React from 'react'; import type { EmailElementProps } from '../../tailwind'; import { sanitizeClassName } from '../compatibility/sanitize-class-name'; +import { extractRulesMatchingStyles } from '../css/extract-rules-matching-classes'; import { makeInlineStylesFor } from '../css/make-inline-styles-for'; import { isComponent } from '../react/is-component'; import type { TailwindSetup } from './setup-tailwind'; -import { extractRulesMatchingStyles } from '../css/extract-rules-matching-classes'; export const cloneElementWithInlinedStyles = ( element: React.ReactElement, diff --git a/packages/tailwind/src/utils/tailwindcss/setup-tailwind.spec.ts b/packages/tailwind/src/utils/tailwindcss/setup-tailwind.spec.ts index 6c79fbc35b..5db70f88f1 100644 --- a/packages/tailwind/src/utils/tailwindcss/setup-tailwind.spec.ts +++ b/packages/tailwind/src/utils/tailwindcss/setup-tailwind.spec.ts @@ -1,7 +1,7 @@ import { generate } from 'css-tree'; import { setupTailwind } from './setup-tailwind'; -test("setupTailwind() and generateCssFrom()", async () => { +test('setupTailwind() and generateCssFrom()', async () => { const { aggregateIntoCss } = await setupTailwind({}); expect( @@ -10,9 +10,5 @@ test("setupTailwind() and generateCssFrom()", async () => { ), ).toMatchSnapshot(); - expect( - generate( - aggregateIntoCss(['bg-red-100']), - ), - ).toMatchSnapshot(); + expect(generate(aggregateIntoCss(['bg-red-100']))).toMatchSnapshot(); }); From 060c2ca5b1ac7ee080b1d60bb1e09d881dd31b54 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Thu, 18 Sep 2025 11:21:15 -0300 Subject: [PATCH 081/193] remove now unused clone --- packages/tailwind/src/utils/css/clone.ts | 29 ------------------------ 1 file changed, 29 deletions(-) delete mode 100644 packages/tailwind/src/utils/css/clone.ts diff --git a/packages/tailwind/src/utils/css/clone.ts b/packages/tailwind/src/utils/css/clone.ts deleted file mode 100644 index 660b1f5eb6..0000000000 --- a/packages/tailwind/src/utils/css/clone.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { type CssNode, List } from 'css-tree'; - -/** - * Slightly modified version of csstree's clone, with the difference that it ignores the `parent` property that we add in. - */ -export function clone(node: T): T { - const result: Record = {}; - - for (const key of Object.keys(node)) { - let value: any = node[key as keyof CssNode]; - if (key === 'parent' || key === 'containedIn' || key === 'containingItem') { - continue; - } - - if (value) { - if (value instanceof List) { - value = value.map(clone); - } else if (Array.isArray(value)) { - value = value.map(clone); - } else if (value.constructor === Object) { - value = clone(value); - } - } - - result[key] = value; - } - - return result as T; -} From 0511f1e4a5e7a74263605d599ba8b87f229ba8e1 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Thu, 18 Sep 2025 18:02:42 -0300 Subject: [PATCH 082/193] use css tree's parsing to sanitize declarations for performance --- .../sanitize-declarations.spec.ts.snap | 2 +- .../src/utils/css/sanitize-declarations.ts | 240 +++++++++++++----- 2 files changed, 176 insertions(+), 66 deletions(-) diff --git a/packages/tailwind/src/utils/css/__snapshots__/sanitize-declarations.spec.ts.snap b/packages/tailwind/src/utils/css/__snapshots__/sanitize-declarations.spec.ts.snap index dbed89aad8..c9a5a504fc 100644 --- a/packages/tailwind/src/utils/css/__snapshots__/sanitize-declarations.spec.ts.snap +++ b/packages/tailwind/src/utils/css/__snapshots__/sanitize-declarations.spec.ts.snap @@ -58,7 +58,7 @@ exports[`sanitizeDeclarations > rgba space syntax to comma syntax conversion > c exports[`sanitizeDeclarations > rgba space syntax to comma syntax conversion > converts rgb with space syntax and alpha to comma syntax 1`] = `"div{color:rgb(255,0,128,0.5)}"`; -exports[`sanitizeDeclarations > rgba space syntax to comma syntax conversion > converts rgb with space syntax and percentage alpha 1`] = `"div{color:rgb(255,0,128,50%)}"`; +exports[`sanitizeDeclarations > rgba space syntax to comma syntax conversion > converts rgb with space syntax and percentage alpha 1`] = `"div{color:rgb(255,0,128,0.5)}"`; exports[`sanitizeDeclarations > rgba space syntax to comma syntax conversion > converts rgb with space syntax to comma syntax 1`] = `"div{color:rgb(255,0,128)}"`; diff --git a/packages/tailwind/src/utils/css/sanitize-declarations.ts b/packages/tailwind/src/utils/css/sanitize-declarations.ts index a808c38afc..823cdf2140 100644 --- a/packages/tailwind/src/utils/css/sanitize-declarations.ts +++ b/packages/tailwind/src/utils/css/sanitize-declarations.ts @@ -88,81 +88,191 @@ function oklchToRgb(oklch: { l: number; c: number; h: number }) { * - convert `margin-inline` into `margin-left` and `margin-right`; * - convert `margin-block` into `margin-top` and `margin-bottom`. */ -export const sanitizeDeclarations = (nodeContainingDeclarations: CssNode) => { +export function sanitizeDeclarations(nodeContainingDeclarations: CssNode) { walk(nodeContainingDeclarations, { visit: 'Declaration', enter(declaration, item, list) { - const rgbParserRegex = - /rgb\(\s*(\d+)\s*(\d+)\s*(\d+)(?:\s*\/\s*([\d%.]+))?\s*\)/g; - const oklchParserRegex = - /oklch\(\s*([\d.]+)(%)?\s*([\d.]+)\s*([\d.]+)(?:\s*\/\s*([\d.]+)(%)?)?\s*\)/g; - const hexParserRegex = /#([a-fA-F0-9]{3,8})/g; - - declaration.value = parse( - generate(declaration.value) - .replaceAll(rgbParserRegex, (_match, r, g, b, a) => { - const alpha = a === '1' || !a ? '' : `,${a}`; - return `rgb(${r},${g},${b}${alpha})`; - }) - .replaceAll( - oklchParserRegex, - (_match, l, lPercentageSign: string, c, h, a, aPercentageSign) => { - const rgb = oklchToRgb({ - l: lPercentageSign ? Number(l) / 100 : Number(l), - c: Number(c), - h: Number(h), - }); - - const alphaString = a - ? `,${aPercentageSign ? Number(a) / 100 : a}` - : ''; - return `rgb(${rgb.r},${rgb.g},${rgb.b}${alphaString})`; - }, - ) - .replaceAll(hexParserRegex, (_match, hex: string) => { - if (hex.length === 3) { - const r = Number.parseInt(hex.charAt(0) + hex.charAt(0), 16); - const g = Number.parseInt(hex.charAt(1) + hex.charAt(1), 16); - const b = Number.parseInt(hex.charAt(2) + hex.charAt(2), 16); - return `rgb(${r},${g},${b})`; + walk(declaration, { + visit: 'Function', + enter(func, funcParentListItem) { + const children = func.children.toArray(); + if (func.name === 'oklch') { + let l: number | undefined; + let c: number | undefined; + let h: number | undefined; + let a: number | undefined; + for (const child of children) { + if (child.type === 'Number') { + if (l === undefined) { + l = Number.parseFloat(child.value); + continue; + } + if (c === undefined) { + c = Number.parseFloat(child.value); + continue; + } + if (h === undefined) { + h = Number.parseFloat(child.value); + continue; + } + if (a === undefined) { + a = Number.parseFloat(child.value); + continue; + } + } + if (child.type === 'Percentage') { + if (l === undefined) { + l = Number.parseFloat(child.value) / 100; + continue; + } + if (a === undefined) { + a = Number.parseFloat(child.value) / 100; + } + } } - if (hex.length === 4) { - const r = Number.parseInt(hex.charAt(0) + hex.charAt(0), 16); - const g = Number.parseInt(hex.charAt(1) + hex.charAt(1), 16); - const b = Number.parseInt(hex.charAt(2) + hex.charAt(2), 16); - const a = - Number.parseInt(hex.charAt(3) + hex.charAt(3), 16) / 255; - return `rgb(${r},${g},${b},${a.toFixed(1)})`; + + if (l === undefined || c === undefined || h === undefined) { + throw new Error( + 'Could not determine the parameters of an oklch() function.', + { + cause: declaration, + }, + ); } - if (hex.length === 5) { - const r = Number.parseInt(hex.slice(0, 2), 16); - const g = Number.parseInt(hex.charAt(2) + hex.charAt(2), 16); - const b = Number.parseInt(hex.charAt(3) + hex.charAt(3), 16); - const a = - Number.parseInt(hex.charAt(4) + hex.charAt(4), 16) / 255; - return `rgb(${r},${g},${b},${a.toFixed(1)})`; + + const rgb = oklchToRgb({ + l, + c, + h, + }); + + const alphaString = a ? `,${a}` : ''; + + funcParentListItem.data = parse( + `rgb(${rgb.r},${rgb.g},${rgb.b}${alphaString})`, + { + context: 'value', + }, + ); + } + + if (func.name === 'rgb') { + let r: number | undefined; + let g: number | undefined; + let b: number | undefined; + let a: number | undefined; + for (const child of children) { + if (child.type === 'Number') { + if (r === undefined) { + r = Number.parseFloat(child.value); + continue; + } + if (g === undefined) { + g = Number.parseFloat(child.value); + continue; + } + if (b === undefined) { + b = Number.parseFloat(child.value); + continue; + } + if (a === undefined) { + a = Number.parseFloat(child.value); + continue; + } + } + if (child.type === 'Percentage') { + if (a === undefined) { + a = Number.parseFloat(child.value) / 100; + } + } } - if (hex.length === 6) { - const r = Number.parseInt(hex.slice(0, 2), 16); - const g = Number.parseInt(hex.slice(2, 4), 16); - const b = Number.parseInt(hex.slice(4, 6), 16); - return `rgb(${r},${g},${b})`; + + if (r === undefined || g === undefined || b === undefined) { + throw new Error( + 'Could not determine the parameters of an rgb() function.', + { + cause: declaration, + }, + ); } - if (hex.length === 7) { - const r = Number.parseInt(hex.slice(0, 2), 16); - const g = Number.parseInt(hex.slice(2, 4), 16); - const b = Number.parseInt(hex.slice(4, 6), 16); - const a = - Number.parseInt(hex.charAt(6) + hex.charAt(6), 16) / 255; - return `rgb(${r},${g},${b},${a.toFixed(1)})`; + + if (a === undefined || a === 1) { + funcParentListItem.data = parse(`rgb(${r},${g},${b})`, { + context: 'value', + }); + } else { + funcParentListItem.data = parse(`rgb(${r},${g},${b},${a})`, { + context: 'value', + }); } + } + }, + }); + walk(declaration, { + visit: 'Hash', + enter(hash, hashParentListItem) { + const hex = hash.value.trim(); + if (hex.length === 3) { + const r = Number.parseInt(hex.charAt(0) + hex.charAt(0), 16); + const g = Number.parseInt(hex.charAt(1) + hex.charAt(1), 16); + const b = Number.parseInt(hex.charAt(2) + hex.charAt(2), 16); + hashParentListItem.data = parse(`rgb(${r},${g},${b})`, { + context: 'value', + }); + return; + } + if (hex.length === 4) { + const r = Number.parseInt(hex.charAt(0) + hex.charAt(0), 16); + const g = Number.parseInt(hex.charAt(1) + hex.charAt(1), 16); + const b = Number.parseInt(hex.charAt(2) + hex.charAt(2), 16); + const a = Number.parseInt(hex.charAt(3) + hex.charAt(3), 16) / 255; + hashParentListItem.data = parse( + `rgb(${r},${g},${b},${a.toFixed(1)})`, + { context: 'value' }, + ); + return; + } + if (hex.length === 5) { + const r = Number.parseInt(hex.slice(0, 2), 16); + const g = Number.parseInt(hex.charAt(2) + hex.charAt(2), 16); + const b = Number.parseInt(hex.charAt(3) + hex.charAt(3), 16); + const a = Number.parseInt(hex.charAt(4) + hex.charAt(4), 16) / 255; + hashParentListItem.data = parse( + `rgb(${r},${g},${b},${a.toFixed(1)})`, + { context: 'value' }, + ); + return; + } + if (hex.length === 6) { + const r = Number.parseInt(hex.slice(0, 2), 16); + const g = Number.parseInt(hex.slice(2, 4), 16); + const b = Number.parseInt(hex.slice(4, 6), 16); + hashParentListItem.data = parse(`rgb(${r},${g},${b})`, { + context: 'value', + }); + return; + } + if (hex.length === 7) { const r = Number.parseInt(hex.slice(0, 2), 16); const g = Number.parseInt(hex.slice(2, 4), 16); const b = Number.parseInt(hex.slice(4, 6), 16); - const a = Number.parseInt(hex.slice(6, 8), 16) / 255; - return `rgb(${r},${g},${b},${a.toFixed(1)})`; - }), - ) as Raw | Value; + const a = Number.parseInt(hex.charAt(6) + hex.charAt(6), 16) / 255; + hashParentListItem.data = parse( + `rgb(${r},${g},${b},${a.toFixed(1)})`, + { context: 'value' }, + ); + return; + } + const r = Number.parseInt(hex.slice(0, 2), 16); + const g = Number.parseInt(hex.slice(2, 4), 16); + const b = Number.parseInt(hex.slice(4, 6), 16); + const a = Number.parseInt(hex.slice(6, 8), 16) / 255; + hashParentListItem.data = parse( + `rgb(${r},${g},${b},${a.toFixed(1)})`, + { context: 'value' }, + ); + }, + }); if (declaration.property === 'padding-inline') { declaration.property = 'padding-left'; @@ -210,4 +320,4 @@ export const sanitizeDeclarations = (nodeContainingDeclarations: CssNode) => { } }, }); -}; +} From 12a65fb7c64fd7736980111e5b1798e67594b8ed Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 19 Sep 2025 10:28:38 -0300 Subject: [PATCH 083/193] remove hooks --- .../__snapshots__/use-tailwind.spec.ts.snap | 16 --------- .../src/hooks/use-suspensed-promise.ts | 35 ------------------- 2 files changed, 51 deletions(-) delete mode 100644 packages/tailwind/src/hooks/__snapshots__/use-tailwind.spec.ts.snap delete mode 100644 packages/tailwind/src/hooks/use-suspensed-promise.ts diff --git a/packages/tailwind/src/hooks/__snapshots__/use-tailwind.spec.ts.snap b/packages/tailwind/src/hooks/__snapshots__/use-tailwind.spec.ts.snap deleted file mode 100644 index baebff2970..0000000000 --- a/packages/tailwind/src/hooks/__snapshots__/use-tailwind.spec.ts.snap +++ /dev/null @@ -1,16 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`useTailwind() 1`] = ` -".text-red-500 { - color: rgb(239 68 68 / 1) -} - @media (min-width: 640px) { - .sm\\:bg-blue-300 { - background-color: rgb(147 197 253 / 1) - } -} - .bg-slate-900 { - background-color: rgb(15 23 42 / 1) -} -" -`; diff --git a/packages/tailwind/src/hooks/use-suspensed-promise.ts b/packages/tailwind/src/hooks/use-suspensed-promise.ts deleted file mode 100644 index 0a22615e84..0000000000 --- a/packages/tailwind/src/hooks/use-suspensed-promise.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* eslint-disable @typescript-eslint/no-throw-literal */ -interface PromiseState { - promise: Promise; - error?: unknown; - result?: unknown; -} - -const promiseStates = new Map(); - -export const useSuspensedPromise = ( - promiseFn: () => Promise, - key: string, -) => { - const previousState = promiseStates.get(key); - if (previousState) { - if ('error' in previousState) { - throw previousState.error; - } - - if ('result' in previousState) { - return previousState.result as Result; - } - - throw previousState.promise; - } - - const state: PromiseState = { - promise: promiseFn() - .then((result) => (state.result = result)) - .catch((error) => (state.error = error as unknown)), - }; - promiseStates.set(key, state); - - throw state.promise; -}; From 115b85bacf8d5fd1af7291fd43164c60d778eb0e Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 19 Sep 2025 10:31:20 -0300 Subject: [PATCH 084/193] update to using the new compat option that's cached --- .../extract-rules-matching-classes.spec.ts | 9 ++---- .../css/sanitize-non-inlinable-rules.spec.ts | 12 +++---- .../__snapshots__/setup-tailwind.spec.ts.snap | 4 +-- .../clone-element-with-inlined-styles.ts | 3 +- .../utils/tailwindcss/setup-tailwind.spec.ts | 10 +++--- .../src/utils/tailwindcss/setup-tailwind.ts | 31 +++++++++++++------ 6 files changed, 38 insertions(+), 31 deletions(-) diff --git a/packages/tailwind/src/utils/css/extract-rules-matching-classes.spec.ts b/packages/tailwind/src/utils/css/extract-rules-matching-classes.spec.ts index a57a53ceb8..c8c7fea86e 100644 --- a/packages/tailwind/src/utils/css/extract-rules-matching-classes.spec.ts +++ b/packages/tailwind/src/utils/css/extract-rules-matching-classes.spec.ts @@ -6,8 +6,7 @@ describe('extractRulesMatchingClasses()', async () => { it('should work just inlinable utilities', async () => { const tailwind = await setupTailwind({}); const classes = ['text-center', 'bg-red-500']; - const cssNode = tailwind.aggregateIntoCss(classes); - tailwind.dealWithCompatibilityIssues(cssNode); + const cssNode = tailwind.addUtilities(classes); const rules = extractRulesMatchingStyles(classes, cssNode); const stringifiedRulesMap = Object.fromEntries( rules @@ -21,8 +20,7 @@ describe('extractRulesMatchingClasses()', async () => { it('should work with non-inlinable utilities', async () => { const tailwind = await setupTailwind({}); const classes = ['lg:w-1/2']; - const cssNode = tailwind.aggregateIntoCss(classes); - tailwind.dealWithCompatibilityIssues(cssNode); + const cssNode = tailwind.addUtilities(classes); const rules = extractRulesMatchingStyles(classes, cssNode); const stringifiedRulesMap = Object.fromEntries( rules @@ -42,8 +40,7 @@ describe('extractRulesMatchingClasses()', async () => { 'w-full', 'lg:w-1/2', ]; - const cssNode = tailwind.aggregateIntoCss(classes); - tailwind.dealWithCompatibilityIssues(cssNode); + const cssNode = tailwind.addUtilities(classes); const rules = extractRulesMatchingStyles(classes, cssNode); const stringifiedRulesMap = Object.fromEntries( rules diff --git a/packages/tailwind/src/utils/css/sanitize-non-inlinable-rules.spec.ts b/packages/tailwind/src/utils/css/sanitize-non-inlinable-rules.spec.ts index 399ec0752e..fb72a8e2f2 100644 --- a/packages/tailwind/src/utils/css/sanitize-non-inlinable-rules.spec.ts +++ b/packages/tailwind/src/utils/css/sanitize-non-inlinable-rules.spec.ts @@ -5,11 +5,11 @@ import { sanitizeNonInlinableRules } from './sanitize-non-inlinable-rules'; describe('sanitizeNonInlinableRules()', () => { it('should handle rules that can be inlined', async () => { const tailwind = await setupTailwind({}); - const stylesheet = tailwind.aggregateIntoCss([ + const stylesheet = tailwind.addUtilities([ 'bg-gray-900', 'text-red-300', 'text-lg', - ]); + ], { compatibilityFixes: false }); sanitizeNonInlinableRules(stylesheet); expect(generate(stylesheet)).toMatchSnapshot(); @@ -17,12 +17,12 @@ describe('sanitizeNonInlinableRules()', () => { it('should css nesting in hover pseudo styles', async () => { const tailwind = await setupTailwind({}); - const stylesheet = tailwind.aggregateIntoCss([ + const stylesheet = tailwind.addUtilities([ 'hover:text-sky-600', 'sm:focus:outline-none', 'md:hover:bg-gray-100', 'lg:focus:underline', - ]); + ], { compatibilityFixes: false }); sanitizeNonInlinableRules(stylesheet); expect(generate(stylesheet)).toMatchSnapshot(); @@ -30,13 +30,13 @@ describe('sanitizeNonInlinableRules()', () => { it('shuold work with basic media query rules', async () => { const tailwind = await setupTailwind({}); - const stylesheet = tailwind.aggregateIntoCss([ + const stylesheet = tailwind.addUtilities([ 'sm:mx-auto', 'sm:max-w-lg', 'sm:rounded-lg', 'md:px-10', 'md:py-12', - ]); + ], { compatibilityFixes: false }); sanitizeNonInlinableRules(stylesheet); diff --git a/packages/tailwind/src/utils/tailwindcss/__snapshots__/setup-tailwind.spec.ts.snap b/packages/tailwind/src/utils/tailwindcss/__snapshots__/setup-tailwind.spec.ts.snap index f921bbfe72..afcb89585f 100644 --- a/packages/tailwind/src/utils/tailwindcss/__snapshots__/setup-tailwind.spec.ts.snap +++ b/packages/tailwind/src/utils/tailwindcss/__snapshots__/setup-tailwind.spec.ts.snap @@ -1,5 +1,5 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`setupTailwind() and generateCssFrom() 1`] = `"/*! tailwindcss v4.1.12 | MIT License | https://tailwindcss.com */@layer theme,base,components,utilities;@layer theme{:root,:host{--color-red-500: oklch(63.7% 0.237 25.331);--color-blue-300: oklch(80.9% 0.105 251.813);--color-slate-900: oklch(20.8% 0.042 265.755)}}@layer utilities{.bg-slate-900{background-color:var(--color-slate-900)}.text-red-500{color:var(--color-red-500)}.sm\\:bg-blue-300{@media (width>=40rem){background-color:var(--color-blue-300)}}}"`; +exports[`setupTailwind() and addUtilities() 1`] = `"/*! tailwindcss v4.1.12 | MIT License | https://tailwindcss.com */@layer theme,base,components,utilities;@layer utilities{.bg-slate-900{background-color:rgb(14.6,23,43)}.text-red-500{color:rgb(250.6,43.8,54.3)}.sm_bg-blue-300{@media (width>=40rem){background-color:rgb(141.9,197.2,255)!important}}}"`; -exports[`setupTailwind() and generateCssFrom() 2`] = `"/*! tailwindcss v4.1.12 | MIT License | https://tailwindcss.com */@layer theme,base,components,utilities;@layer theme{:root,:host{--color-red-100: oklch(93.6% 0.032 17.717);--color-red-500: oklch(63.7% 0.237 25.331);--color-blue-300: oklch(80.9% 0.105 251.813);--color-slate-900: oklch(20.8% 0.042 265.755)}}@layer utilities{.bg-red-100{background-color:var(--color-red-100)}.bg-slate-900{background-color:var(--color-slate-900)}.text-red-500{color:var(--color-red-500)}.sm\\:bg-blue-300{@media (width>=40rem){background-color:var(--color-blue-300)}}}"`; +exports[`setupTailwind() and addUtilities() 2`] = `"/*! tailwindcss v4.1.12 | MIT License | https://tailwindcss.com */@layer theme,base,components,utilities;@layer utilities{.bg-red-100{background-color:rgb(254.8,225.8,225.8)}.bg-slate-900{background-color:rgb(14.6,23,43)}.text-red-500{color:rgb(250.6,43.8,54.3)}.sm_bg-blue-300{@media (width>=40rem){background-color:rgb(141.9,197.2,255)!important}}}"`; diff --git a/packages/tailwind/src/utils/tailwindcss/clone-element-with-inlined-styles.ts b/packages/tailwind/src/utils/tailwindcss/clone-element-with-inlined-styles.ts index 6b9a165e82..0f4781d893 100644 --- a/packages/tailwind/src/utils/tailwindcss/clone-element-with-inlined-styles.ts +++ b/packages/tailwind/src/utils/tailwindcss/clone-element-with-inlined-styles.ts @@ -18,8 +18,7 @@ export const cloneElementWithInlinedStyles = ( if (element.props.className) { const classes = element.props.className.split(' '); - const cssNode = tailwindSetup.aggregateIntoCss(classes); - tailwindSetup.dealWithCompatibilityIssues(cssNode); + const cssNode = tailwindSetup.addUtilities(classes); const rulePerClass = extractRulesMatchingStyles(classes, cssNode); diff --git a/packages/tailwind/src/utils/tailwindcss/setup-tailwind.spec.ts b/packages/tailwind/src/utils/tailwindcss/setup-tailwind.spec.ts index 5db70f88f1..f9e3a107f4 100644 --- a/packages/tailwind/src/utils/tailwindcss/setup-tailwind.spec.ts +++ b/packages/tailwind/src/utils/tailwindcss/setup-tailwind.spec.ts @@ -1,14 +1,12 @@ import { generate } from 'css-tree'; import { setupTailwind } from './setup-tailwind'; -test('setupTailwind() and generateCssFrom()', async () => { - const { aggregateIntoCss } = await setupTailwind({}); +test('setupTailwind() and addUtilities()', async () => { + const { addUtilities } = await setupTailwind({}); expect( - generate( - aggregateIntoCss(['text-red-500', 'sm:bg-blue-300', 'bg-slate-900']), - ), + generate(addUtilities(['text-red-500', 'sm:bg-blue-300', 'bg-slate-900'])), ).toMatchSnapshot(); - expect(generate(aggregateIntoCss(['bg-red-100']))).toMatchSnapshot(); + expect(generate(addUtilities(['bg-red-100']))).toMatchSnapshot(); }); diff --git a/packages/tailwind/src/utils/tailwindcss/setup-tailwind.ts b/packages/tailwind/src/utils/tailwindcss/setup-tailwind.ts index cd3be3c522..409c15226c 100644 --- a/packages/tailwind/src/utils/tailwindcss/setup-tailwind.ts +++ b/packages/tailwind/src/utils/tailwindcss/setup-tailwind.ts @@ -32,6 +32,7 @@ export async function setupTailwind(config: Config) { `NO-OP: should we implement support for ${resourceHint}?`, ); }, + polyfills: 3, // All async loadStylesheet(id, base) { if (id === 'tailwindcss') { return { @@ -71,20 +72,32 @@ export async function setupTailwind(config: Config) { }, }); + let addedUtilities: string[] = []; + + let root: CssNode | undefined; + return { /** * @description Given a list of Tailwind classes, it generates the corresponding CSS. Also resolves simple `calc` functions, and css variables. * The returned CSS also includes the CSS generated from all previous calls to this function, this is internal to Tailwind. */ - aggregateIntoCss(classes: string[]) { - return parse(compiler.build(classes)); - }, - dealWithCompatibilityIssues(root: CssNode) { - resolveAllCSSVariables(root); - resolveCalcExpressions(root); - sanitizeDeclarations(root); - sanitizeNonInlinableRules(root); - return root; + addUtilities: function addUtilities( + classes: string[], + { compatibilityFixes = true } = {}, + ) { + if (!compatibilityFixes) { + return parse(compiler.build(classes)); + } + + if (classes.some((className) => !addedUtilities.includes(className))) { + addedUtilities = [...addedUtilities, ...classes]; + root = parse(compiler.build(classes)); + resolveAllCSSVariables(root); + resolveCalcExpressions(root); + sanitizeDeclarations(root); + sanitizeNonInlinableRules(root); + } + return root!; }, }; } From 90a1b74b7397719fae3920c48ceaa8f996c21685 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 19 Sep 2025 11:30:23 -0300 Subject: [PATCH 085/193] don't use arrow functions for easier profiling --- packages/tailwind/src/tailwind.tsx | 9 +++------ .../convert-css-property-to-react-property.ts | 2 +- .../src/utils/compatibility/escape-class-name.ts | 2 +- .../src/utils/compatibility/sanitize-class-name.ts | 2 +- .../src/utils/css/extract-rules-matching-classes.ts | 7 ++----- .../src/utils/css/resolve-all-css-variables.ts | 12 ++++++------ .../src/utils/css/resolve-calc-expressions.ts | 8 ++++---- .../src/utils/css/sanitize-non-inlinable-rules.ts | 2 +- .../tailwindcss/clone-element-with-inlined-styles.ts | 4 ++-- 9 files changed, 21 insertions(+), 27 deletions(-) diff --git a/packages/tailwind/src/tailwind.tsx b/packages/tailwind/src/tailwind.tsx index e0b689d943..0447c52fcd 100644 --- a/packages/tailwind/src/tailwind.tsx +++ b/packages/tailwind/src/tailwind.tsx @@ -75,10 +75,7 @@ export const pixelBasedPreset: TailwindConfig = { }, }; -export const Tailwind: React.FC = async ({ - children, - config, -}) => { +export async function Tailwind({ children, config }: TailwindProps) { const tailwindSetup = await setupTailwind(config ?? {}); const nonInlineStylesToApply: StyleSheet = { @@ -112,7 +109,7 @@ export const Tailwind: React.FC = async ({ if (hasNonInlineStylesToApply) { let hasAppliedNonInlineStyles = false as boolean; - mappedChildren = await mapReactTree(mappedChildren, (node) => { + mappedChildren = mapReactTree(mappedChildren, (node) => { if (hasAppliedNonInlineStyles) { return node; } @@ -155,4 +152,4 @@ please file a bug https://github.com/resend/react-email/issues/new?assignees=&la } return mappedChildren; -}; +} diff --git a/packages/tailwind/src/utils/compatibility/convert-css-property-to-react-property.ts b/packages/tailwind/src/utils/compatibility/convert-css-property-to-react-property.ts index bd8ba5ca49..b3d0cac152 100644 --- a/packages/tailwind/src/utils/compatibility/convert-css-property-to-react-property.ts +++ b/packages/tailwind/src/utils/compatibility/convert-css-property-to-react-property.ts @@ -1,6 +1,6 @@ import { fromDashCaseToCamelCase } from '../text/from-dash-case-to-camel-case'; -export const convertCssPropertyToReactProperty = (prop: string) => { +export function convertCssPropertyToReactProperty(prop: string) { const modifiedProp = prop.toLowerCase(); if (modifiedProp.startsWith('--')) { diff --git a/packages/tailwind/src/utils/compatibility/escape-class-name.ts b/packages/tailwind/src/utils/compatibility/escape-class-name.ts index 8f66c03bb7..ca813592e9 100644 --- a/packages/tailwind/src/utils/compatibility/escape-class-name.ts +++ b/packages/tailwind/src/utils/compatibility/escape-class-name.ts @@ -5,7 +5,7 @@ * Also does a bit more trickery to avoid escaping already * escaped characters. */ -export const escapeClassName = (className: string) => { +export function escapeClassName(className: string) { return className.replace( /* we need this look ahead capturing group to avoid using negative look behinds */ /([^\\]|^)(?=([^a-zA-Z0-9\-_]))/g, diff --git a/packages/tailwind/src/utils/compatibility/sanitize-class-name.ts b/packages/tailwind/src/utils/compatibility/sanitize-class-name.ts index bbd0204dfd..43fde2e2dc 100644 --- a/packages/tailwind/src/utils/compatibility/sanitize-class-name.ts +++ b/packages/tailwind/src/utils/compatibility/sanitize-class-name.ts @@ -17,7 +17,7 @@ const digitToNameMap = { * @param className - This should not come with any escaped charcters, it should come the same * as is on the `className` attribute on React elements. */ -export const sanitizeClassName = (className: string) => { +export function sanitizeClassName(className: string) { return className .replaceAll('+', 'plus') .replaceAll('[', '') diff --git a/packages/tailwind/src/utils/css/extract-rules-matching-classes.ts b/packages/tailwind/src/utils/css/extract-rules-matching-classes.ts index a9a6ead9bf..7dfac092f2 100644 --- a/packages/tailwind/src/utils/css/extract-rules-matching-classes.ts +++ b/packages/tailwind/src/utils/css/extract-rules-matching-classes.ts @@ -2,10 +2,7 @@ import { type CssNode, generate, string, walk } from 'css-tree'; import { sanitizeClassName } from '../compatibility/sanitize-class-name'; import { isRuleInlinable } from './is-rule-inlinable'; -export const extractRulesMatchingStyles = ( - classes: string[], - root: CssNode, -) => { +export function extractRulesMatchingStyles(classes: string[], root: CssNode) { const rules = new Map< string, { @@ -41,4 +38,4 @@ export const extractRulesMatchingStyles = ( }, }); return rules; -}; +} diff --git a/packages/tailwind/src/utils/css/resolve-all-css-variables.ts b/packages/tailwind/src/utils/css/resolve-all-css-variables.ts index 55eb1714af..f92bd322ea 100644 --- a/packages/tailwind/src/utils/css/resolve-all-css-variables.ts +++ b/packages/tailwind/src/utils/css/resolve-all-css-variables.ts @@ -24,7 +24,7 @@ interface VariableDefinition { remove(): void; } -const doSelectorsIntersect = (first: string, second: string): boolean => { +function doSelectorsIntersect(first: string, second: string): boolean { if (first === second) { return true; } @@ -38,9 +38,9 @@ const doSelectorsIntersect = (first: string, second: string): boolean => { } return false; -}; +} -const removeAndRepeatIfEmptyRecursively = (node: CssNode) => { +function removeAndRepeatIfEmptyRecursively(node: CssNode) { if (node.parent) { if (node.containedIn && node.containingItem) { node.containedIn.remove(node.containingItem); @@ -52,9 +52,9 @@ const removeAndRepeatIfEmptyRecursively = (node: CssNode) => { removeAndRepeatIfEmptyRecursively(node.parent); } } -}; +} -export const resolveAllCSSVariables = (node: CssNode) => { +export function resolveAllCSSVariables(node: CssNode) { populateParentsForNodeTree(node); const variableDefinitions = new Set(); const variableUses = new Set(); @@ -176,4 +176,4 @@ export const resolveAllCSSVariables = (node: CssNode) => { for (const definition of variableDefinitions) { definition.remove(); } -}; +} diff --git a/packages/tailwind/src/utils/css/resolve-calc-expressions.ts b/packages/tailwind/src/utils/css/resolve-calc-expressions.ts index 9be44231fb..38d55a8733 100644 --- a/packages/tailwind/src/utils/css/resolve-calc-expressions.ts +++ b/packages/tailwind/src/utils/css/resolve-calc-expressions.ts @@ -3,7 +3,7 @@ import { type CssNode, walk } from 'css-tree'; /** * Intentionally only resolves `*` and `/` operations without dealing with parenthesis, because this is the only thing required to run Tailwind v4 */ -export const resolveCalcExpressions = (node: CssNode) => { +export function resolveCalcExpressions(node: CssNode) { walk(node, { visit: 'Function', enter(func, funcListItem) { @@ -29,9 +29,9 @@ export const resolveCalcExpressions = (node: CssNode) => { const value = String( child.value === '*' ? Number.parseFloat(left.data.value) * - Number.parseFloat(right.data.value) + Number.parseFloat(right.data.value) : Number.parseFloat(left.data.value) / - Number.parseFloat(right.data.value), + Number.parseFloat(right.data.value), ); if ( left.data.type === 'Dimension' && @@ -87,4 +87,4 @@ export const resolveCalcExpressions = (node: CssNode) => { } }, }); -}; +} diff --git a/packages/tailwind/src/utils/css/sanitize-non-inlinable-rules.ts b/packages/tailwind/src/utils/css/sanitize-non-inlinable-rules.ts index f846396481..e7237c911e 100644 --- a/packages/tailwind/src/utils/css/sanitize-non-inlinable-rules.ts +++ b/packages/tailwind/src/utils/css/sanitize-non-inlinable-rules.ts @@ -11,7 +11,7 @@ import { isRuleInlinable } from './is-rule-inlinable'; * 1. Converts all declarations in all rules into being important ones * 2. Sanitizes all the selectors of all non-inlinable rules */ -export const sanitizeNonInlinableRules = (node: CssNode) => { +export function sanitizeNonInlinableRules(node: CssNode) { walk(node, { visit: 'Rule', enter(rule) { diff --git a/packages/tailwind/src/utils/tailwindcss/clone-element-with-inlined-styles.ts b/packages/tailwind/src/utils/tailwindcss/clone-element-with-inlined-styles.ts index 0f4781d893..26710689c2 100644 --- a/packages/tailwind/src/utils/tailwindcss/clone-element-with-inlined-styles.ts +++ b/packages/tailwind/src/utils/tailwindcss/clone-element-with-inlined-styles.ts @@ -7,10 +7,10 @@ import { makeInlineStylesFor } from '../css/make-inline-styles-for'; import { isComponent } from '../react/is-component'; import type { TailwindSetup } from './setup-tailwind'; -export const cloneElementWithInlinedStyles = ( +export function cloneElementWithInlinedStyles( element: React.ReactElement, tailwindSetup: TailwindSetup, -) => { +) { const propsToOverwrite: Partial = {}; const nonInlinableClasses: string[] = []; From dff22d34f6e1de122497fc697739f992dc690fcb Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 19 Sep 2025 11:46:18 -0300 Subject: [PATCH 086/193] lint --- .../convert-css-property-to-react-property.ts | 2 +- .../utils/compatibility/escape-class-name.ts | 2 +- .../compatibility/sanitize-class-name.ts | 2 +- .../src/utils/css/resolve-calc-expressions.ts | 4 +-- .../src/utils/css/sanitize-declarations.ts | 10 +----- .../css/sanitize-non-inlinable-rules.spec.ts | 35 +++++++++---------- .../utils/css/sanitize-non-inlinable-rules.ts | 2 +- .../clone-element-with-inlined-styles.ts | 2 +- 8 files changed, 25 insertions(+), 34 deletions(-) diff --git a/packages/tailwind/src/utils/compatibility/convert-css-property-to-react-property.ts b/packages/tailwind/src/utils/compatibility/convert-css-property-to-react-property.ts index b3d0cac152..2f8a91d3df 100644 --- a/packages/tailwind/src/utils/compatibility/convert-css-property-to-react-property.ts +++ b/packages/tailwind/src/utils/compatibility/convert-css-property-to-react-property.ts @@ -12,4 +12,4 @@ export function convertCssPropertyToReactProperty(prop: string) { } return fromDashCaseToCamelCase(modifiedProp); -}; +} diff --git a/packages/tailwind/src/utils/compatibility/escape-class-name.ts b/packages/tailwind/src/utils/compatibility/escape-class-name.ts index ca813592e9..6525a50234 100644 --- a/packages/tailwind/src/utils/compatibility/escape-class-name.ts +++ b/packages/tailwind/src/utils/compatibility/escape-class-name.ts @@ -15,4 +15,4 @@ export function escapeClassName(className: string) { return `${prefixCharacter}\\`; }, ); -}; +} diff --git a/packages/tailwind/src/utils/compatibility/sanitize-class-name.ts b/packages/tailwind/src/utils/compatibility/sanitize-class-name.ts index 43fde2e2dc..424c6ad629 100644 --- a/packages/tailwind/src/utils/compatibility/sanitize-class-name.ts +++ b/packages/tailwind/src/utils/compatibility/sanitize-class-name.ts @@ -33,4 +33,4 @@ export function sanitizeClassName(className: string) { return digitToNameMap[digit as keyof typeof digitToNameMap]; }) .replace(/[^a-zA-Z0-9\-_]/g, '_'); -}; +} diff --git a/packages/tailwind/src/utils/css/resolve-calc-expressions.ts b/packages/tailwind/src/utils/css/resolve-calc-expressions.ts index 38d55a8733..54bad1e0ec 100644 --- a/packages/tailwind/src/utils/css/resolve-calc-expressions.ts +++ b/packages/tailwind/src/utils/css/resolve-calc-expressions.ts @@ -29,9 +29,9 @@ export function resolveCalcExpressions(node: CssNode) { const value = String( child.value === '*' ? Number.parseFloat(left.data.value) * - Number.parseFloat(right.data.value) + Number.parseFloat(right.data.value) : Number.parseFloat(left.data.value) / - Number.parseFloat(right.data.value), + Number.parseFloat(right.data.value), ); if ( left.data.type === 'Dimension' && diff --git a/packages/tailwind/src/utils/css/sanitize-declarations.ts b/packages/tailwind/src/utils/css/sanitize-declarations.ts index 823cdf2140..e71676b621 100644 --- a/packages/tailwind/src/utils/css/sanitize-declarations.ts +++ b/packages/tailwind/src/utils/css/sanitize-declarations.ts @@ -1,12 +1,4 @@ -import { - type CssNode, - type Declaration, - generate, - parse, - type Raw, - type Value, - walk, -} from 'css-tree'; +import { type CssNode, type Declaration, parse, walk } from 'css-tree'; const LAB_TO_LMS = { l: [0.3963377773761749, 0.2158037573099136], diff --git a/packages/tailwind/src/utils/css/sanitize-non-inlinable-rules.spec.ts b/packages/tailwind/src/utils/css/sanitize-non-inlinable-rules.spec.ts index fb72a8e2f2..9920ff44a5 100644 --- a/packages/tailwind/src/utils/css/sanitize-non-inlinable-rules.spec.ts +++ b/packages/tailwind/src/utils/css/sanitize-non-inlinable-rules.spec.ts @@ -5,11 +5,10 @@ import { sanitizeNonInlinableRules } from './sanitize-non-inlinable-rules'; describe('sanitizeNonInlinableRules()', () => { it('should handle rules that can be inlined', async () => { const tailwind = await setupTailwind({}); - const stylesheet = tailwind.addUtilities([ - 'bg-gray-900', - 'text-red-300', - 'text-lg', - ], { compatibilityFixes: false }); + const stylesheet = tailwind.addUtilities( + ['bg-gray-900', 'text-red-300', 'text-lg'], + { compatibilityFixes: false }, + ); sanitizeNonInlinableRules(stylesheet); expect(generate(stylesheet)).toMatchSnapshot(); @@ -17,12 +16,15 @@ describe('sanitizeNonInlinableRules()', () => { it('should css nesting in hover pseudo styles', async () => { const tailwind = await setupTailwind({}); - const stylesheet = tailwind.addUtilities([ - 'hover:text-sky-600', - 'sm:focus:outline-none', - 'md:hover:bg-gray-100', - 'lg:focus:underline', - ], { compatibilityFixes: false }); + const stylesheet = tailwind.addUtilities( + [ + 'hover:text-sky-600', + 'sm:focus:outline-none', + 'md:hover:bg-gray-100', + 'lg:focus:underline', + ], + { compatibilityFixes: false }, + ); sanitizeNonInlinableRules(stylesheet); expect(generate(stylesheet)).toMatchSnapshot(); @@ -30,13 +32,10 @@ describe('sanitizeNonInlinableRules()', () => { it('shuold work with basic media query rules', async () => { const tailwind = await setupTailwind({}); - const stylesheet = tailwind.addUtilities([ - 'sm:mx-auto', - 'sm:max-w-lg', - 'sm:rounded-lg', - 'md:px-10', - 'md:py-12', - ], { compatibilityFixes: false }); + const stylesheet = tailwind.addUtilities( + ['sm:mx-auto', 'sm:max-w-lg', 'sm:rounded-lg', 'md:px-10', 'md:py-12'], + { compatibilityFixes: false }, + ); sanitizeNonInlinableRules(stylesheet); diff --git a/packages/tailwind/src/utils/css/sanitize-non-inlinable-rules.ts b/packages/tailwind/src/utils/css/sanitize-non-inlinable-rules.ts index e7237c911e..f4947b63de 100644 --- a/packages/tailwind/src/utils/css/sanitize-non-inlinable-rules.ts +++ b/packages/tailwind/src/utils/css/sanitize-non-inlinable-rules.ts @@ -32,4 +32,4 @@ export function sanitizeNonInlinableRules(node: CssNode) { } }, }); -}; +} diff --git a/packages/tailwind/src/utils/tailwindcss/clone-element-with-inlined-styles.ts b/packages/tailwind/src/utils/tailwindcss/clone-element-with-inlined-styles.ts index 26710689c2..2542c79399 100644 --- a/packages/tailwind/src/utils/tailwindcss/clone-element-with-inlined-styles.ts +++ b/packages/tailwind/src/utils/tailwindcss/clone-element-with-inlined-styles.ts @@ -82,4 +82,4 @@ export function cloneElementWithInlinedStyles( nonInlinableClasses, nonInlineRules, }; -}; +} From bf8793caebaebc9b2daa517f57b3e1ecea699273 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 19 Sep 2025 11:50:49 -0300 Subject: [PATCH 087/193] update test snapshot --- .../get-email-component.spec.ts.snap | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/packages/preview-server/src/utils/__snapshots__/get-email-component.spec.ts.snap b/packages/preview-server/src/utils/__snapshots__/get-email-component.spec.ts.snap index a6e818a9d9..c36b698cc9 100644 --- a/packages/preview-server/src/utils/__snapshots__/get-email-component.spec.ts.snap +++ b/packages/preview-server/src/utils/__snapshots__/get-email-component.spec.ts.snap @@ -13,7 +13,7 @@ exports[`getEmailComponent() > with a demo email template 1`] = ` + style='margin-right:auto;margin-left:auto;margin-bottom:auto;margin-top:auto;background-color:rgb(255,255,255);padding-right:8px;padding-left:8px;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"'> with a demo email template 1`] = ` @@ -153,7 +154,7 @@ exports[`getEmailComponent() > with a demo email template 1`] = `
+ style='margin-right:auto;margin-left:auto;margin-bottom:auto;margin-top:auto;background-color:rgb(255,255,255);padding-right:8px;padding-left:8px;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"'>
@@ -36,11 +36,12 @@ exports[`getEmailComponent() > with a demo email template 1`] = ` + style="margin-right:auto;margin-left:auto;margin-bottom:40px;margin-top:40px;max-width:465px;border-radius:0.25rem;border-color:rgb(234,234,234);padding:20px">

+ style="margin-right:0;margin-left:0;margin-bottom:30px;margin-top:30px;padding:0;text-align:center;font-size:24px;font-weight:400;color:rgb(0,0,0)"> Join Enigma on Vercel

+ style="font-size:14px;line-height:24px;color:rgb(0,0,0);margin-top:16px;margin-bottom:16px"> Hello alanturing,

+ style="font-size:14px;line-height:24px;color:rgb(0,0,0);margin-top:16px;margin-bottom:16px"> Alan (alan.turing@example.com) has invited you to the Enigma team on @@ -110,7 +111,7 @@ exports[`getEmailComponent() > with a demo email template 1`] = ` alt="alanturing's profile picture" height="64" src="/static/vercel-user.png" - style="border-radius:9999px;display:block;outline:none;border:none;text-decoration:none" + style="border-radius:calc(infinity*1px);display:block;outline:none;border:none;text-decoration:none" width="64" />

with a demo email template 1`] = ` alt="Enigma team logo" height="64" src="/static/vercel-team.png" - style="border-radius:9999px;display:block;outline:none;border:none;text-decoration:none" + style="border-radius:calc(infinity*1px);display:block;outline:none;border:none;text-decoration:none" width="64" />
with a demo email template 1`] = `

+ style="font-size:14px;line-height:24px;color:rgb(0,0,0);margin-top:16px;margin-bottom:16px"> or copy and paste this URL into your browser: https://vercel.com


+ class="border-[#eaeaea] border-solid" + style="margin-right:0;margin-left:0;margin-bottom:26px;margin-top:26px;width:100%;border-color:rgb(234,234,234);border:none;border-top:1px solid #eaeaea" />

+ style="font-size:12px;line-height:24px;color:rgb(102,102,102);margin-top:16px;margin-bottom:16px"> This invitation was intended for alanturing. This invite was sent from From 8d72cb9955fa5a07a13c2982ee7d11ab40d1a686 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 19 Sep 2025 12:13:21 -0300 Subject: [PATCH 088/193] update snap --- .../testing/__snapshots__/export.spec.ts.snap | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/packages/react-email/src/commands/testing/__snapshots__/export.spec.ts.snap b/packages/react-email/src/commands/testing/__snapshots__/export.spec.ts.snap index 615acfb5ad..75656a5cae 100644 --- a/packages/react-email/src/commands/testing/__snapshots__/export.spec.ts.snap +++ b/packages/react-email/src/commands/testing/__snapshots__/export.spec.ts.snap @@ -11,7 +11,7 @@ exports[`email export 1`] = ` + style='margin-right:auto;margin-left:auto;margin-bottom:auto;margin-top:auto;background-color:rgb(255,255,255);padding-right:8px;padding-left:8px;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"'> @@ -147,7 +148,7 @@ exports[`email export 1`] = `
+ style='margin-right:auto;margin-left:auto;margin-bottom:auto;margin-top:auto;background-color:rgb(255,255,255);padding-right:8px;padding-left:8px;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"'>
@@ -34,11 +34,12 @@ exports[`email export 1`] = ` + style="margin-right:auto;margin-left:auto;margin-bottom:40px;margin-top:40px;max-width:465px;border-radius:0.25rem;border-color:rgb(234,234,234);padding:20px">

+ style="margin-right:0;margin-left:0;margin-bottom:30px;margin-top:30px;padding:0;text-align:center;font-size:24px;font-weight:400;color:rgb(0,0,0)"> Join on Vercel

+ style="font-size:14px;line-height:24px;color:rgb(0,0,0);margin-top:16px;margin-bottom:16px"> Hello ,

+ style="font-size:14px;line-height:24px;color:rgb(0,0,0);margin-top:16px;margin-bottom:16px"> () has invited you to the team on Vercel. @@ -106,7 +107,7 @@ exports[`email export 1`] = ` undefined's profile picture

+ style="font-size:14px;line-height:24px;color:rgb(0,0,0);margin-top:16px;margin-bottom:16px"> or copy and paste this URL into your browser:


+ class="border-[#eaeaea] border-solid" + style="margin-right:0;margin-left:0;margin-bottom:26px;margin-top:26px;width:100%;border-color:rgb(234,234,234);border:none;border-top:1px solid #eaeaea" />

+ style="font-size:12px;line-height:24px;color:rgb(102,102,102);margin-top:16px;margin-bottom:16px"> This invitation was intended for . This invite was sent from From 6c38251d2cae4215f9f3414ec168e5753e8df23d Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 19 Sep 2025 16:47:14 -0300 Subject: [PATCH 089/193] remove stale test --- packages/tailwind/src/tailwind.spec.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/tailwind/src/tailwind.spec.tsx b/packages/tailwind/src/tailwind.spec.tsx index 77a0dca5ff..69ee31c49f 100644 --- a/packages/tailwind/src/tailwind.spec.tsx +++ b/packages/tailwind/src/tailwind.spec.tsx @@ -293,14 +293,6 @@ describe('Tailwind component', () => { expect(actualOutput).toMatchSnapshot(); }); - // test.only('temporary', async () => { - // await render( - // - //

- // , - // ); - // }); - it('should preserve mso styles', async () => { const actualOutput = await render( From 6521fc4f61affaa65ff33e19d3e445c11f3cd0ff Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 19 Sep 2025 16:47:27 -0300 Subject: [PATCH 090/193] first iteration of trying to only parse CSS once --- packages/tailwind/src/tailwind.tsx | 97 +- .../extract-rules-per-class.spec.ts.snap | 178430 +++++++++++++++ .../extract-rules-matching-classes.spec.ts | 53 - .../css/extract-rules-matching-classes.ts | 41 - .../utils/css/extract-rules-per-class.spec.ts | 47 + .../src/utils/css/extract-rules-per-class.ts | 32 + .../css/sanitize-non-inlinable-rules.spec.ts | 35 +- .../clone-element-with-inlined-styles.ts | 67 +- .../utils/tailwindcss/setup-tailwind.spec.ts | 12 +- .../src/utils/tailwindcss/setup-tailwind.ts | 25 +- 10 files changed, 178611 insertions(+), 228 deletions(-) create mode 100644 packages/tailwind/src/utils/css/__snapshots__/extract-rules-per-class.spec.ts.snap delete mode 100644 packages/tailwind/src/utils/css/extract-rules-matching-classes.spec.ts delete mode 100644 packages/tailwind/src/utils/css/extract-rules-matching-classes.ts create mode 100644 packages/tailwind/src/utils/css/extract-rules-per-class.spec.ts create mode 100644 packages/tailwind/src/utils/css/extract-rules-per-class.ts diff --git a/packages/tailwind/src/tailwind.tsx b/packages/tailwind/src/tailwind.tsx index 0447c52fcd..41a71072ad 100644 --- a/packages/tailwind/src/tailwind.tsx +++ b/packages/tailwind/src/tailwind.tsx @@ -1,6 +1,7 @@ -import { generate, List, type StyleSheet } from 'css-tree'; +import { type CssNode, generate, List, type StyleSheet } from 'css-tree'; import * as React from 'react'; import type { Config } from 'tailwindcss'; +import { extractRulesPerClass } from './utils/css/extract-rules-per-class'; import { mapReactTree } from './utils/react/map-react-tree'; import { cloneElementWithInlinedStyles } from './utils/tailwindcss/clone-element-with-inlined-styles'; import { setupTailwind } from './utils/tailwindcss/setup-tailwind'; @@ -78,26 +79,50 @@ export const pixelBasedPreset: TailwindConfig = { export async function Tailwind({ children, config }: TailwindProps) { const tailwindSetup = await setupTailwind(config ?? {}); - const nonInlineStylesToApply: StyleSheet = { + let mappedChildren: React.ReactNode = mapReactTree(children, (node) => { + if (React.isValidElement(node)) { + if (node.props.className) { + tailwindSetup.addUtilities(node.props.className?.split(' ')); + } + } + + return node; + }); + + const styleSheet = tailwindSetup.getStyleSheet(); + + const { inlinable: inlinableRules, nonInlinable: nonInlinableRules } = + extractRulesPerClass(styleSheet); + + const nonInlineStyles: StyleSheet = { type: 'StyleSheet', - children: new List(), + children: new List().fromArray( + nonInlinableRules.values().toArray(), + ), }; - let mediaQueryClassesForAllElement: string[] = []; - let hasNonInlineStylesToApply = false as boolean; + const hasNonInlineStylesToApply = nonInlinableRules.size > 0; + let appliedNonInlineStyles = false as boolean; - let mappedChildren: React.ReactNode = mapReactTree(children, (node) => { + mappedChildren = mapReactTree(mappedChildren, (node) => { if (React.isValidElement(node)) { - const { elementWithInlinedStyles, nonInlinableClasses, nonInlineRules } = - cloneElementWithInlinedStyles(node, tailwindSetup); - mediaQueryClassesForAllElement = - mediaQueryClassesForAllElement.concat(nonInlinableClasses); - for (const rule of nonInlineRules) { - nonInlineStylesToApply.children.appendData(rule); - } + const elementWithInlinedStyles = cloneElementWithInlinedStyles( + node, + inlinableRules, + nonInlinableRules, + ); + + if (elementWithInlinedStyles.type === 'head') { + appliedNonInlineStyles = true; + + const styleElement = ; - if (nonInlinableClasses.length > 0 && !hasNonInlineStylesToApply) { - hasNonInlineStylesToApply = true; + return React.cloneElement( + elementWithInlinedStyles, + elementWithInlinedStyles.props, + styleElement, + elementWithInlinedStyles.props.children, + ); } return elementWithInlinedStyles; @@ -106,39 +131,12 @@ export async function Tailwind({ children, config }: TailwindProps) { return node; }); - if (hasNonInlineStylesToApply) { - let hasAppliedNonInlineStyles = false as boolean; - - mappedChildren = mapReactTree(mappedChildren, (node) => { - if (hasAppliedNonInlineStyles) { - return node; - } - - if (React.isValidElement(node)) { - if (node.type === 'head') { - hasAppliedNonInlineStyles = true; - - const styleElement = ( - - ); - - return React.cloneElement( - node, - node.props, - styleElement, - node.props.children, - ); - } - } - - return node; - }); - - if (!hasAppliedNonInlineStyles) { - throw new Error( - `You are trying to use the following Tailwind classes that cannot be inlined: ${mediaQueryClassesForAllElement.join( - ' ', - )}. + if (hasNonInlineStylesToApply && !appliedNonInlineStyles) { + throw new Error( + `You are trying to use the following Tailwind classes that cannot be inlined: ${nonInlinableRules + .keys() + .toArray() + .join(' ')}. For the media queries to work properly on rendering, they need to be added into a ; - const styleElement = ; + return React.cloneElement( + elementWithInlinedStyles, + elementWithInlinedStyles.props, + styleElement, + elementWithInlinedStyles.props.children, + ); + } - return React.cloneElement( - elementWithInlinedStyles, - elementWithInlinedStyles.props, - styleElement, - elementWithInlinedStyles.props.children, - ); + return elementWithInlinedStyles; } - return elementWithInlinedStyles; - } - - return node; - }); + return node; + }); - if (hasNonInlineStylesToApply && !appliedNonInlineStyles) { - throw new Error( - `You are trying to use the following Tailwind classes that cannot be inlined: ${nonInlinableRules - .keys() - .toArray() - .join(' ')}. + if (hasNonInlineStylesToApply && !appliedNonInlineStyles) { + throw new Error( + `You are trying to use the following Tailwind classes that cannot be inlined: ${nonInlinableRules + .keys() + .toArray() + .join(' ')}. For the media queries to work properly on rendering, they need to be added into a ; + if (elementWithInlinedStyles.type === 'head') { + appliedNonInlineStyles = true; - return React.cloneElement( - elementWithInlinedStyles, - elementWithInlinedStyles.props, - styleElement, - elementWithInlinedStyles.props.children, - ); - } + const styleElement = ; - return elementWithInlinedStyles; + return React.cloneElement( + elementWithInlinedStyles, + elementWithInlinedStyles.props, + styleElement, + elementWithInlinedStyles.props.children, + ); } - return node; - }); + return elementWithInlinedStyles; + } + + return node; + }); - if (hasNonInlineStylesToApply && !appliedNonInlineStyles) { - throw new Error( - `You are trying to use the following Tailwind classes that cannot be inlined: ${nonInlinableRules - .keys() - .toArray() - .join(' ')}. + if (hasNonInlineStylesToApply && !appliedNonInlineStyles) { + throw new Error( + `You are trying to use the following Tailwind classes that cannot be inlined: ${nonInlinableRules + .keys() + .toArray() + .join(' ')}. For the media queries to work properly on rendering, they need to be added into a should recognize custom responsive screen 1`] = ` -
+
Test
Test
@@ -55,7 +55,7 @@ exports[`Tailwind component > should recognize custom responsive screen 1`] = ` exports[`Tailwind component > should render children with inline Tailwind styles 1`] = `"
"`; -exports[`Tailwind component > should work properly with 'no-underline' 1`] = `"

or copy and paste this URL into your browser: https://react.email

or copy and paste this URL into your browser: https://react.email

"`; +exports[`Tailwind component > should work properly with 'no-underline' 1`] = `"

or copy and paste this URL into your browser: https://react.email

or copy and paste this URL into your browser: https://react.email

"`; exports[`Tailwind component > should work with Heading component 1`] = `"Hello

My testing heading

friends"`; @@ -86,14 +86,14 @@ exports[`Tailwind component > should work with calc() with + sign 1`] = `
+ style="max-height:calc(50px + 3rem);background-color:rgb(255,226,226)">
something tall
" `; -exports[`Tailwind component > should work with class manipulation done on components 1`] = `"
"`; +exports[`Tailwind component > should work with class manipulation done on components 1`] = `"
"`; exports[`Tailwind component > should work with components that return children 1`] = `"
Hello world

React Email

"`; @@ -173,13 +173,13 @@ exports[`Tailwind component > with non-inlinable styles > should add css to
+ style="background-color:rgb(255,201,201)">
@@ -192,15 +192,13 @@ exports[`Tailwind component > with non-inlinable styles > should persist existin -
+
@@ -237,20 +235,20 @@ exports[`Tailwind component > with non-inlinable styles > should work with arbit
+ style="background-color:rgb(255,201,201)">
" `; -exports[`Tailwind component > with non-inlinable styles > should work with arbitrarily deep (in the React tree) elements 2`] = `"
"`; +exports[`Tailwind component > with non-inlinable styles > should work with arbitrarily deep (in the React tree) elements 2`] = `"
"`; exports[`Tailwind component > with non-inlinable styles > should work with relatively complex media query utilities 1`] = ` " @@ -259,12 +257,10 @@ exports[`Tailwind component > with non-inlinable styles > should work with relat -

- I am some text -

+

I am some text

" `; diff --git a/packages/tailwind/src/hooks/use-suspensed-promise.spec.ts b/packages/tailwind/src/hooks/use-suspensed-promise.spec.ts index a43199c247..2063a1ab49 100644 --- a/packages/tailwind/src/hooks/use-suspensed-promise.spec.ts +++ b/packages/tailwind/src/hooks/use-suspensed-promise.spec.ts @@ -1,5 +1,5 @@ /** biome-ignore-all lint/correctness/useHookAtTopLevel: function is not a React hook */ -import { useSuspensedPromise } from './use-suspensed-promise'; +import { useSuspensedPromise } from './use-suspended-promise'; describe('useSuspensedPromise', () => { beforeEach(() => {}); diff --git a/packages/tailwind/src/utils/css/__snapshots__/resolve-calc-expressions.spec.ts.snap b/packages/tailwind/src/utils/css/__snapshots__/resolve-calc-expressions.spec.ts.snap index 29eb607d54..10a88869eb 100644 --- a/packages/tailwind/src/utils/css/__snapshots__/resolve-calc-expressions.spec.ts.snap +++ b/packages/tailwind/src/utils/css/__snapshots__/resolve-calc-expressions.spec.ts.snap @@ -1,5 +1,7 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`resolveCalcExpressions() > resolves calc expressions repeating decimals 1`] = `".w-1/3{width:33.33333333333333%}"`; + exports[`resolveCalcExpressions() > should not do anything to complex calc expressions 1`] = `".px-3{padding-inline:calc(0.25rem*(3 + 1px))}.py-2{padding-block:calc(0.25rem*(2 + 1px))}"`; exports[`resolveCalcExpressions() > should resolve spacing calc expressions from tailwind v4 1`] = `".px-3{padding-inline:0.75rem}.py-2{padding-block:0.5rem}"`; diff --git a/packages/tailwind/src/utils/css/__snapshots__/sanitize-declarations.spec.ts.snap b/packages/tailwind/src/utils/css/__snapshots__/sanitize-declarations.spec.ts.snap index 90772f2c8a..bac00f3205 100644 --- a/packages/tailwind/src/utils/css/__snapshots__/sanitize-declarations.spec.ts.snap +++ b/packages/tailwind/src/utils/css/__snapshots__/sanitize-declarations.spec.ts.snap @@ -6,7 +6,7 @@ exports[`sanitizeDeclarations > complex scenarios > handles nested rules 1`] = ` exports[`sanitizeDeclarations > complex scenarios > processes at-rule declarations 1`] = `"@keyframes fade{from{background:rgb(255,0,0,0)}to{background:rgb(255,0,0)}}"`; -exports[`sanitizeDeclarations > handles transparency generated with color-mix 1`] = `".bg-blue-600/50{background-color:rgb(21.3,92.9,251.5,0.6)}"`; +exports[`sanitizeDeclarations > handles transparency generated with color-mix 1`] = `".bg-blue-600/50{background-color:rgb(21,93,252,0.6)}"`; exports[`sanitizeDeclarations > hex to rgb conversion 1`] = `"div{color:rgb(255,0,170)}"`; @@ -38,23 +38,23 @@ exports[`sanitizeDeclarations > hex to rgb conversion 14`] = `"div{color:rgb(255 exports[`sanitizeDeclarations > hex to rgb conversion 15`] = `"div{background:linear-gradient(rgb(255,0,0),rgb(0,255,0),rgb(0,0,255))}"`; -exports[`sanitizeDeclarations > hex to rgb conversion 16`] = `"div{background:linear-gradient(rgb(255,0,0),rgb(0,255,0),rgb(0,104.6,199))}"`; +exports[`sanitizeDeclarations > hex to rgb conversion 16`] = `"div{background:linear-gradient(rgb(255,0,0),rgb(0,255,0),rgb(0,105,199))}"`; exports[`sanitizeDeclarations > hex to rgb conversion 17`] = `"div{content:"Visit our site at example.com#section"}"`; exports[`sanitizeDeclarations > hex to rgb conversion 18`] = `"div{color:rgb(255,0,0);background-color:rgb(0,255,0);border-color:rgb(0,0,255);box-shadow:0 0 10px rgb(51,51,51)}"`; -exports[`sanitizeDeclarations > oklch to rgb conversion 1`] = `"div{color:rgb(0,255,228.7)}"`; +exports[`sanitizeDeclarations > oklch to rgb conversion 1`] = `"div{color:rgb(0,255,229)}"`; -exports[`sanitizeDeclarations > oklch to rgb conversion 2`] = `"div{color:rgb(255,250.7,125.6,0.8)}"`; +exports[`sanitizeDeclarations > oklch to rgb conversion 2`] = `"div{color:rgb(255,251,126,0.8)}"`; -exports[`sanitizeDeclarations > oklch to rgb conversion 3`] = `"div{color:rgb(255,250.7,125.6,0.8)}"`; +exports[`sanitizeDeclarations > oklch to rgb conversion 3`] = `"div{color:rgb(255,251,126,0.8)}"`; -exports[`sanitizeDeclarations > oklch to rgb conversion 4`] = `"div{color:rgb(190.5,240.2,255,0.5)}"`; +exports[`sanitizeDeclarations > oklch to rgb conversion 4`] = `"div{color:rgb(191,240,255,0.5)}"`; -exports[`sanitizeDeclarations > oklch to rgb conversion 5`] = `"div{color:rgb(254.6,192.6,249.8)}"`; +exports[`sanitizeDeclarations > oklch to rgb conversion 5`] = `"div{color:rgb(255,193,250)}"`; -exports[`sanitizeDeclarations > oklch to rgb conversion 6`] = `"div{color:rgb(255,99.8,0.1)}"`; +exports[`sanitizeDeclarations > oklch to rgb conversion 6`] = `"div{color:rgb(255,100,0)}"`; exports[`sanitizeDeclarations > rgba space syntax to comma syntax conversion 1`] = `"div{color:rgb(255,0,128)}"`; diff --git a/packages/tailwind/src/utils/css/sanitize-declarations.ts b/packages/tailwind/src/utils/css/sanitize-declarations.ts index cb58f31a4d..8f89b951b7 100644 --- a/packages/tailwind/src/utils/css/sanitize-declarations.ts +++ b/packages/tailwind/src/utils/css/sanitize-declarations.ts @@ -37,11 +37,6 @@ function clamp(value: number, min: number, max: number) { return Math.min(Math.max(value, min), max); } -function round(value: number, digits = 1) { - const factor = 10 ** digits; - return Math.round(value * factor) / factor; -} - function oklchToOklab(oklch: { l: number; c: number; h: number }) { return { l: oklch.l, @@ -72,9 +67,9 @@ function oklchToRgb(oklch: { l: number; c: number; h: number }) { lrgbToRgb(LSM_TO_RGB.b[0] * l + LSM_TO_RGB.b[1] * m + LSM_TO_RGB.b[2] * s); return { - r: round(clamp(r, 0, 255)), - g: round(clamp(g, 0, 255)), - b: round(clamp(b, 0, 255)), + r: clamp(r, 0, 255).toFixed(0), + g: clamp(g, 0, 255).toFixed(0), + b: clamp(b, 0, 255).toFixed(0), }; } From d95f0ed14ab6f050dca938fab8fe3723bf04be91 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 3 Oct 2025 14:05:23 -0300 Subject: [PATCH 148/193] lint --- .../src/utils/css/resolve-calc-expressions.ts | 4 ++-- .../src/utils/css/sanitize-declarations.ts | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/tailwind/src/utils/css/resolve-calc-expressions.ts b/packages/tailwind/src/utils/css/resolve-calc-expressions.ts index 212e5fedea..13b2417d51 100644 --- a/packages/tailwind/src/utils/css/resolve-calc-expressions.ts +++ b/packages/tailwind/src/utils/css/resolve-calc-expressions.ts @@ -35,7 +35,7 @@ export function resolveCalcExpressions(node: CssNode) { if (child.value === '*') { return String( Number.parseFloat(left.data.value) * - Number.parseFloat(right.data.value), + Number.parseFloat(right.data.value), ); } if (right.data.value === '0') { @@ -43,7 +43,7 @@ export function resolveCalcExpressions(node: CssNode) { } return String( Number.parseFloat(left.data.value) / - Number.parseFloat(right.data.value), + Number.parseFloat(right.data.value), ); })(); if ( diff --git a/packages/tailwind/src/utils/css/sanitize-declarations.ts b/packages/tailwind/src/utils/css/sanitize-declarations.ts index 8f89b951b7..4881ddac5f 100644 --- a/packages/tailwind/src/utils/css/sanitize-declarations.ts +++ b/packages/tailwind/src/utils/css/sanitize-declarations.ts @@ -82,13 +82,13 @@ function separteShorthandDeclaration( const values = shorthandToReplace.value.type === 'Value' ? shorthandToReplace.value.children - .toArray() - .filter( - (child) => - child.type === 'Dimension' || - child.type === 'Number' || - child.type === 'Percentage', - ) + .toArray() + .filter( + (child) => + child.type === 'Dimension' || + child.type === 'Number' || + child.type === 'Percentage', + ) : [shorthandToReplace.value]; let endValue = shorthandToReplace.value; if (values.length === 2) { From 9c62259edf41a5dd89be334596905bc0858454c5 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 3 Oct 2025 14:26:48 -0300 Subject: [PATCH 149/193] use extend for pixelBasedPreset --- packages/tailwind/src/tailwind.tsx | 104 +++++++++++++++-------------- 1 file changed, 53 insertions(+), 51 deletions(-) diff --git a/packages/tailwind/src/tailwind.tsx b/packages/tailwind/src/tailwind.tsx index 849f30029f..142a9558b2 100644 --- a/packages/tailwind/src/tailwind.tsx +++ b/packages/tailwind/src/tailwind.tsx @@ -26,57 +26,59 @@ export interface EmailElementProps { export const pixelBasedPreset: TailwindConfig = { theme: { - fontSize: { - xs: ['12px', { lineHeight: '16px' }], - sm: ['14px', { lineHeight: '20px' }], - base: ['16px', { lineHeight: '24px' }], - lg: ['18px', { lineHeight: '28px' }], - xl: ['20px', { lineHeight: '28px' }], - '2xl': ['24px', { lineHeight: '32px' }], - '3xl': ['30px', { lineHeight: '36px' }], - '4xl': ['36px', { lineHeight: '36px' }], - '5xl': ['48px', { lineHeight: '1' }], - '6xl': ['60px', { lineHeight: '1' }], - '7xl': ['72px', { lineHeight: '1' }], - '8xl': ['96px', { lineHeight: '1' }], - '9xl': ['144px', { lineHeight: '1' }], - }, - spacing: { - px: '1px', - 0: '0', - 0.5: '2px', - 1: '4px', - 1.5: '6px', - 2: '8px', - 2.5: '10px', - 3: '12px', - 3.5: '14px', - 4: '16px', - 5: '20px', - 6: '24px', - 7: '28px', - 8: '32px', - 9: '36px', - 10: '40px', - 11: '44px', - 12: '48px', - 14: '56px', - 16: '64px', - 20: '80px', - 24: '96px', - 28: '112px', - 32: '128px', - 36: '144px', - 40: '160px', - 44: '176px', - 48: '192px', - 52: '208px', - 56: '224px', - 60: '240px', - 64: '256px', - 72: '288px', - 80: '320px', - 96: '384px', + extend: { + fontSize: { + xs: ['12px', { lineHeight: '16px' }], + sm: ['14px', { lineHeight: '20px' }], + base: ['16px', { lineHeight: '24px' }], + lg: ['18px', { lineHeight: '28px' }], + xl: ['20px', { lineHeight: '28px' }], + '2xl': ['24px', { lineHeight: '32px' }], + '3xl': ['30px', { lineHeight: '36px' }], + '4xl': ['36px', { lineHeight: '36px' }], + '5xl': ['48px', { lineHeight: '1' }], + '6xl': ['60px', { lineHeight: '1' }], + '7xl': ['72px', { lineHeight: '1' }], + '8xl': ['96px', { lineHeight: '1' }], + '9xl': ['144px', { lineHeight: '1' }], + }, + spacing: { + px: '1px', + 0: '0', + 0.5: '2px', + 1: '4px', + 1.5: '6px', + 2: '8px', + 2.5: '10px', + 3: '12px', + 3.5: '14px', + 4: '16px', + 5: '20px', + 6: '24px', + 7: '28px', + 8: '32px', + 9: '36px', + 10: '40px', + 11: '44px', + 12: '48px', + 14: '56px', + 16: '64px', + 20: '80px', + 24: '96px', + 28: '112px', + 32: '128px', + 36: '144px', + 40: '160px', + 44: '176px', + 48: '192px', + 52: '208px', + 56: '224px', + 60: '240px', + 64: '256px', + 72: '288px', + 80: '320px', + 96: '384px', + }, }, }, }; From 0545536166b7cdbe85f917c74dbb1efc6f5fcdd5 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 3 Oct 2025 14:48:22 -0300 Subject: [PATCH 150/193] update snaps --- .../get-email-component.spec.ts.snap | 31 ++++++++++--------- .../testing/__snapshots__/export.spec.ts.snap | 31 ++++++++++--------- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/packages/preview-server/src/utils/__snapshots__/get-email-component.spec.ts.snap b/packages/preview-server/src/utils/__snapshots__/get-email-component.spec.ts.snap index a9f6569a7a..a6e818a9d9 100644 --- a/packages/preview-server/src/utils/__snapshots__/get-email-component.spec.ts.snap +++ b/packages/preview-server/src/utils/__snapshots__/get-email-component.spec.ts.snap @@ -12,7 +12,8 @@ exports[`getEmailComponent() > with a demo email template 1`] = ` - + with a demo email template 1`] = `
+ style='margin-left:auto;margin-right:auto;margin-top:auto;margin-bottom:auto;background-color:rgb(255,255,255);padding-left:8px;padding-right:8px;font-family:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'>
@@ -39,7 +40,7 @@ exports[`getEmailComponent() > with a demo email template 1`] = ` cellpadding="0" cellspacing="0" role="presentation" - style="max-width:465px;margin-right:auto;margin-left:auto;margin-bottom:40px;margin-top:40px;border-radius:0.25rem;border-style:solid;border-width:1px;border-color:rgb(234,234,234);padding:20px"> + style="margin-left:auto;margin-right:auto;margin-top:40px;margin-bottom:40px;max-width:465px;border-radius:0.25rem;border-width:1px;border-color:rgb(234,234,234);border-style:solid;padding:20px">
@@ -58,26 +59,26 @@ exports[`getEmailComponent() > with a demo email template 1`] = ` alt="Vercel Logo" height="37" src="/static/vercel-logo.png" - style="display:block;outline:none;border:none;text-decoration:none;margin-right:auto;margin-left:auto;margin-bottom:0;margin-top:0" + style="margin-left:auto;margin-right:auto;margin-top:0;margin-bottom:0;display:block;outline:none;border:none;text-decoration:none" width="40" />

+ style="margin-left:0;margin-right:0;margin-top:30px;margin-bottom:30px;padding:0;text-align:center;font-weight:400;font-size:24px;color:rgb(0,0,0)"> Join Enigma on Vercel

+ style="font-size:14px;color:rgb(0,0,0);line-height:24px;margin-top:16px;margin-bottom:16px"> Hello alanturing,

+ style="font-size:14px;color:rgb(0,0,0);line-height:24px;margin-top:16px;margin-bottom:16px"> Alan (alan.turing@example.com) has invited you to the Enigma team on @@ -109,7 +110,7 @@ exports[`getEmailComponent() > with a demo email template 1`] = ` alt="alanturing's profile picture" height="64" src="/static/vercel-user.png" - style="display:block;outline:none;border:none;text-decoration:none;border-radius:9999px" + style="border-radius:9999px;display:block;outline:none;border:none;text-decoration:none" width="64" /> with a demo email template 1`] = ` alt="Enigma team logo" height="64" src="/static/vercel-team.png" - style="display:block;outline:none;border:none;text-decoration:none;border-radius:9999px" + style="border-radius:9999px;display:block;outline:none;border:none;text-decoration:none" width="64" /> @@ -152,7 +153,7 @@ exports[`getEmailComponent() > with a demo email template 1`] = ` with a demo email template 1`] = `

+ style="font-size:14px;color:rgb(0,0,0);line-height:24px;margin-top:16px;margin-bottom:16px"> or copy and paste this URL into your browser: https://vercel.com


+ style="margin-left:0;margin-right:0;margin-top:26px;margin-bottom:26px;width:100%;border-width:1px;border-color:rgb(234,234,234);border-style:solid;border:none;border-top:1px solid #eaeaea" />

+ style="color:rgb(102,102,102);font-size:12px;line-height:24px;margin-top:16px;margin-bottom:16px"> This invitation was intended for alanturing. This invite was sent from diff --git a/packages/react-email/src/commands/testing/__snapshots__/export.spec.ts.snap b/packages/react-email/src/commands/testing/__snapshots__/export.spec.ts.snap index 0a6c58f058..615acfb5ad 100644 --- a/packages/react-email/src/commands/testing/__snapshots__/export.spec.ts.snap +++ b/packages/react-email/src/commands/testing/__snapshots__/export.spec.ts.snap @@ -10,7 +10,8 @@ exports[`email export 1`] = ` - +
+ style='margin-left:auto;margin-right:auto;margin-top:auto;margin-bottom:auto;background-color:rgb(255,255,255);padding-left:8px;padding-right:8px;font-family:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'>
@@ -37,7 +38,7 @@ exports[`email export 1`] = ` cellpadding="0" cellspacing="0" role="presentation" - style="max-width:465px;margin-right:auto;margin-left:auto;margin-bottom:40px;margin-top:40px;border-radius:0.25rem;border-style:solid;border-width:1px;border-color:rgb(234,234,234);padding:20px"> + style="margin-left:auto;margin-right:auto;margin-top:40px;margin-bottom:40px;max-width:465px;border-radius:0.25rem;border-width:1px;border-color:rgb(234,234,234);border-style:solid;padding:20px">
@@ -56,26 +57,26 @@ exports[`email export 1`] = ` alt="Vercel Logo" height="37" src="/static/vercel-logo.png" - style="display:block;outline:none;border:none;text-decoration:none;margin-right:auto;margin-left:auto;margin-bottom:0;margin-top:0" + style="margin-left:auto;margin-right:auto;margin-top:0;margin-bottom:0;display:block;outline:none;border:none;text-decoration:none" width="40" />

+ style="margin-left:0;margin-right:0;margin-top:30px;margin-bottom:30px;padding:0;text-align:center;font-weight:400;font-size:24px;color:rgb(0,0,0)"> Join on Vercel

+ style="font-size:14px;color:rgb(0,0,0);line-height:24px;margin-top:16px;margin-bottom:16px"> Hello ,

+ style="font-size:14px;color:rgb(0,0,0);line-height:24px;margin-top:16px;margin-bottom:16px"> () has invited you to the team on Vercel. @@ -105,7 +106,7 @@ exports[`email export 1`] = ` undefined's profile picture @@ -146,7 +147,7 @@ exports[`email export 1`] = `

+ style="font-size:14px;color:rgb(0,0,0);line-height:24px;margin-top:16px;margin-bottom:16px"> or copy and paste this URL into your browser:


+ style="margin-left:0;margin-right:0;margin-top:26px;margin-bottom:26px;width:100%;border-width:1px;border-color:rgb(234,234,234);border-style:solid;border:none;border-top:1px solid #eaeaea" />

+ style="color:rgb(102,102,102);font-size:12px;line-height:24px;margin-top:16px;margin-bottom:16px"> This invitation was intended for . This invite was sent from From c79502bd70e6d0d33b258d25079bea9d6c6c2af0 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 3 Oct 2025 16:50:07 -0300 Subject: [PATCH 151/193] bump versions with fixes --- packages/components/package.json | 2 +- packages/tailwind/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/components/package.json b/packages/components/package.json index 73fd3d938b..09e67255a1 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@react-email/components", - "version": "1.0.0-tailwindv4.3", + "version": "1.0.0-tailwindv4.4", "description": "A collection of all components React Email.", "sideEffects": false, "main": "./dist/index.js", diff --git a/packages/tailwind/package.json b/packages/tailwind/package.json index 06a7eed3b3..1ab9e958b0 100644 --- a/packages/tailwind/package.json +++ b/packages/tailwind/package.json @@ -1,6 +1,6 @@ { "name": "@react-email/tailwind", - "version": "2.0.0-tailwindv4.1", + "version": "2.0.0-tailwindv4.2", "description": "A React component to wrap emails with Tailwind CSS", "sideEffects": false, "main": "./dist/index.js", From 438d70394a83e07674f7f5e3003ccfc776c13b0a Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Fri, 3 Oct 2025 16:53:10 -0300 Subject: [PATCH 152/193] update snaps --- .../get-email-component.spec.ts.snap | 31 +++++++++---------- .../testing/__snapshots__/export.spec.ts.snap | 31 +++++++++---------- 2 files changed, 30 insertions(+), 32 deletions(-) diff --git a/packages/preview-server/src/utils/__snapshots__/get-email-component.spec.ts.snap b/packages/preview-server/src/utils/__snapshots__/get-email-component.spec.ts.snap index a6e818a9d9..3886d5c63b 100644 --- a/packages/preview-server/src/utils/__snapshots__/get-email-component.spec.ts.snap +++ b/packages/preview-server/src/utils/__snapshots__/get-email-component.spec.ts.snap @@ -12,8 +12,7 @@ exports[`getEmailComponent() > with a demo email template 1`] = ` - + with a demo email template 1`] = `
+ style='margin-right:auto;margin-left:auto;margin-bottom:auto;margin-top:auto;background-color:rgb(255,255,255);padding-right:8px;padding-left:8px;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"'>
@@ -40,7 +39,7 @@ exports[`getEmailComponent() > with a demo email template 1`] = ` cellpadding="0" cellspacing="0" role="presentation" - style="margin-left:auto;margin-right:auto;margin-top:40px;margin-bottom:40px;max-width:465px;border-radius:0.25rem;border-width:1px;border-color:rgb(234,234,234);border-style:solid;padding:20px"> + style="max-width:465px;margin-right:auto;margin-left:auto;margin-bottom:40px;margin-top:40px;border-radius:0.25rem;border-style:solid;border-width:1px;border-color:rgb(234,234,234);padding:20px">
@@ -59,26 +58,26 @@ exports[`getEmailComponent() > with a demo email template 1`] = ` alt="Vercel Logo" height="37" src="/static/vercel-logo.png" - style="margin-left:auto;margin-right:auto;margin-top:0;margin-bottom:0;display:block;outline:none;border:none;text-decoration:none" + style="display:block;outline:none;border:none;text-decoration:none;margin-right:auto;margin-left:auto;margin-bottom:0;margin-top:0" width="40" />

+ style="margin-right:0;margin-left:0;margin-bottom:30px;margin-top:30px;padding:0;text-align:center;font-weight:400;font-size:24px;color:rgb(0,0,0)"> Join Enigma on Vercel

+ style="font-size:14px;line-height:24px;color:rgb(0,0,0);margin-top:16px;margin-bottom:16px"> Hello alanturing,

+ style="font-size:14px;line-height:24px;color:rgb(0,0,0);margin-top:16px;margin-bottom:16px"> Alan (alan.turing@example.com) has invited you to the Enigma team on @@ -110,7 +109,7 @@ exports[`getEmailComponent() > with a demo email template 1`] = ` alt="alanturing's profile picture" height="64" src="/static/vercel-user.png" - style="border-radius:9999px;display:block;outline:none;border:none;text-decoration:none" + style="display:block;outline:none;border:none;text-decoration:none;border-radius:9999px" width="64" /> with a demo email template 1`] = ` alt="Enigma team logo" height="64" src="/static/vercel-team.png" - style="border-radius:9999px;display:block;outline:none;border:none;text-decoration:none" + style="display:block;outline:none;border:none;text-decoration:none;border-radius:9999px" width="64" /> @@ -153,7 +152,7 @@ exports[`getEmailComponent() > with a demo email template 1`] = ` with a demo email template 1`] = `

+ style="font-size:14px;line-height:24px;color:rgb(0,0,0);margin-top:16px;margin-bottom:16px"> or copy and paste this URL into your browser: https://vercel.com


+ style="width:100%;border:none;border-top:1px solid #eaeaea;margin-right:0;margin-left:0;margin-bottom:26px;margin-top:26px;border-style:solid;border-width:1px;border-color:rgb(234,234,234)" />

+ style="font-size:12px;line-height:24px;color:rgb(102,102,102);margin-top:16px;margin-bottom:16px"> This invitation was intended for alanturing. This invite was sent from diff --git a/packages/react-email/src/commands/testing/__snapshots__/export.spec.ts.snap b/packages/react-email/src/commands/testing/__snapshots__/export.spec.ts.snap index 615acfb5ad..7e4c1a61e7 100644 --- a/packages/react-email/src/commands/testing/__snapshots__/export.spec.ts.snap +++ b/packages/react-email/src/commands/testing/__snapshots__/export.spec.ts.snap @@ -10,8 +10,7 @@ exports[`email export 1`] = ` - +
+ style='margin-right:auto;margin-left:auto;margin-bottom:auto;margin-top:auto;background-color:rgb(255,255,255);padding-right:8px;padding-left:8px;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"'>
@@ -38,7 +37,7 @@ exports[`email export 1`] = ` cellpadding="0" cellspacing="0" role="presentation" - style="margin-left:auto;margin-right:auto;margin-top:40px;margin-bottom:40px;max-width:465px;border-radius:0.25rem;border-width:1px;border-color:rgb(234,234,234);border-style:solid;padding:20px"> + style="max-width:465px;margin-right:auto;margin-left:auto;margin-bottom:40px;margin-top:40px;border-radius:0.25rem;border-style:solid;border-width:1px;border-color:rgb(234,234,234);padding:20px">
@@ -57,26 +56,26 @@ exports[`email export 1`] = ` alt="Vercel Logo" height="37" src="/static/vercel-logo.png" - style="margin-left:auto;margin-right:auto;margin-top:0;margin-bottom:0;display:block;outline:none;border:none;text-decoration:none" + style="display:block;outline:none;border:none;text-decoration:none;margin-right:auto;margin-left:auto;margin-bottom:0;margin-top:0" width="40" />

+ style="margin-right:0;margin-left:0;margin-bottom:30px;margin-top:30px;padding:0;text-align:center;font-weight:400;font-size:24px;color:rgb(0,0,0)"> Join on Vercel

+ style="font-size:14px;line-height:24px;color:rgb(0,0,0);margin-top:16px;margin-bottom:16px"> Hello ,

+ style="font-size:14px;line-height:24px;color:rgb(0,0,0);margin-top:16px;margin-bottom:16px"> () has invited you to the team on Vercel. @@ -106,7 +105,7 @@ exports[`email export 1`] = ` undefined's profile picture @@ -147,7 +146,7 @@ exports[`email export 1`] = `

+ style="font-size:14px;line-height:24px;color:rgb(0,0,0);margin-top:16px;margin-bottom:16px"> or copy and paste this URL into your browser:


+ style="width:100%;border:none;border-top:1px solid #eaeaea;margin-right:0;margin-left:0;margin-bottom:26px;margin-top:26px;border-style:solid;border-width:1px;border-color:rgb(234,234,234)" />

+ style="font-size:12px;line-height:24px;color:rgb(102,102,102);margin-top:16px;margin-bottom:16px"> This invitation was intended for . This invite was sent from From 0486c658d62a85844ffb25ebc429a474a216827e Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Mon, 6 Oct 2025 11:08:48 -0300 Subject: [PATCH 153/193] add variable resolution to make-inline-styles --- .../make-inline-styles-for.spec.ts.snap | 7 ++++ .../utils/css/make-inline-styles-for.spec.ts | 37 ++++++++++++++----- .../src/utils/css/make-inline-styles-for.ts | 29 ++++++++++++++- 3 files changed, 62 insertions(+), 11 deletions(-) diff --git a/packages/tailwind/src/utils/css/__snapshots__/make-inline-styles-for.spec.ts.snap b/packages/tailwind/src/utils/css/__snapshots__/make-inline-styles-for.spec.ts.snap index a8d9b53aa9..fd1f76febb 100644 --- a/packages/tailwind/src/utils/css/__snapshots__/make-inline-styles-for.spec.ts.snap +++ b/packages/tailwind/src/utils/css/__snapshots__/make-inline-styles-for.spec.ts.snap @@ -1,5 +1,12 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`makeInlineStylesFor() > works in simple use case 1`] = ` +{ + "backgroundColor": "#f56565", + "width": "100%", +} +`; + exports[`makeInlineStylesFor() 1`] = ` { "backgroundColor": "#f56565", diff --git a/packages/tailwind/src/utils/css/make-inline-styles-for.spec.ts b/packages/tailwind/src/utils/css/make-inline-styles-for.spec.ts index 7c6aa50295..039d9fb24c 100644 --- a/packages/tailwind/src/utils/css/make-inline-styles-for.spec.ts +++ b/packages/tailwind/src/utils/css/make-inline-styles-for.spec.ts @@ -1,13 +1,32 @@ import { parse, type StyleSheet } from 'css-tree'; import { makeInlineStylesFor } from './make-inline-styles-for'; -test('makeInlineStylesFor()', async () => { - const tailwindStyles = parse(` - .bg-red-500 { background-color: #f56565; } - .w-full { width: 100%; } - `) as StyleSheet; - - expect( - makeInlineStylesFor(tailwindStyles.children.toArray()), - ).toMatchSnapshot(); +describe('makeInlineStylesFor()', async () => { + it('works in simple use case', () => { + const tailwindStyles = parse(` + .bg-red-500 { background-color: #f56565; } + .w-full { width: 100%; } + `) as StyleSheet; + + expect( + makeInlineStylesFor(tailwindStyles.children.toArray()), + ).toMatchSnapshot(); + }); + + it('does basic local variable resolution', () => { + const tailwindStyles = parse(` + .btn { + --btn-bg: #3490dc; + --btn-text: #fff; + background-color: var(--btn-bg); + color: var(--btn-text); + padding: 0.5rem 1rem; + border-radius: 0.25rem; + } + `) as StyleSheet; + + expect( + makeInlineStylesFor(tailwindStyles.children.toArray()), + ).toMatchSnapshot(); + }); }); diff --git a/packages/tailwind/src/utils/css/make-inline-styles-for.ts b/packages/tailwind/src/utils/css/make-inline-styles-for.ts index d4f84104b1..4f323b62e4 100644 --- a/packages/tailwind/src/utils/css/make-inline-styles-for.ts +++ b/packages/tailwind/src/utils/css/make-inline-styles-for.ts @@ -1,16 +1,41 @@ import { type CssNode, generate, walk } from 'css-tree'; -import { convertCssPropertyToReactProperty } from '../compatibility/convert-css-property-to-react-property'; +import { getReactProperty } from '../compatibility/get-react-property'; +import type { VariableDefinition } from './resolve-all-css-variables'; export function makeInlineStylesFor(inlinableRules: CssNode[]) { const styles: Record = {}; for (const rule of inlinableRules) { + const localVariableDefinitions = new Set(); walk(rule, { visit: 'Declaration', enter(declaration) { - styles[convertCssPropertyToReactProperty(declaration.property)] = + if (declaration.property.startsWith('--')) { + localVariableDefinitions.add({ + declaration, + definition: generate(declaration.value).trim(), + variableName: declaration.property.trim(), + }); + } + }, + }); + + walk(rule, { + visit: 'Declaration', + enter(declaration) { + if (declaration.property.startsWith('--')) { + return; + } + let value = generate(declaration.value) + (declaration.important ? '!important' : ''); + for (const localVarDef of localVariableDefinitions) { + const varUsage = `var(${localVarDef.variableName})`; + if (value.includes(varUsage)) { + value = value.replaceAll(varUsage, localVarDef.definition); + } + } + styles[getReactProperty(declaration.property)] = value; }, }); } From afb3786bb2bfc090f56373d1195173b5bfca9410 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Mon, 6 Oct 2025 11:09:02 -0300 Subject: [PATCH 154/193] don't remove variable definition from rules so that they can be resolved locally too --- .../utils/css/resolve-all-css-variables.ts | 25 +------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/packages/tailwind/src/utils/css/resolve-all-css-variables.ts b/packages/tailwind/src/utils/css/resolve-all-css-variables.ts index c4991466ce..7f2f22e46e 100644 --- a/packages/tailwind/src/utils/css/resolve-all-css-variables.ts +++ b/packages/tailwind/src/utils/css/resolve-all-css-variables.ts @@ -17,12 +17,10 @@ interface VariableUse { raw: string; } -interface VariableDefinition { +export interface VariableDefinition { declaration: Declaration; variableName: string; definition: string; - - remove(): void; } function doSelectorsIntersect( @@ -59,20 +57,6 @@ function doSelectorsIntersect( return false; } -function removeAndRepeatIfEmptyRecursively(node: CssNode) { - if (node.parent) { - if (node.containedIn && node.containingItem) { - node.containedIn.remove(node.containingItem); - if (node.containedIn.isEmpty) { - removeAndRepeatIfEmptyRecursively(node.parent); - } - } else { - // The node might not have any list of children, but the parent can (e.g. a Block) - removeAndRepeatIfEmptyRecursively(node.parent); - } - } -} - function someParent( node: CssNode, predicate: (ancestor: CssNode) => boolean, @@ -113,9 +97,6 @@ export function resolveAllCssVariables(node: CssNode) { declaration, variableName: `${declaration.property}`, definition: generate(declaration.value), - remove() { - removeAndRepeatIfEmptyRecursively(declaration); - }, }); } else { function parseVariableUsesFrom(node: CssNode) { @@ -218,8 +199,4 @@ export function resolveAllCssVariables(node: CssNode) { ) as Raw | Value; } } - - for (const definition of variableDefinitions) { - definition.remove(); - } } From f42e1b36e884af9da64070a50cd8e9f0677bcb22 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Mon, 6 Oct 2025 11:09:23 -0300 Subject: [PATCH 155/193] better naming --- ...-css-property-to-react-property.ts => get-react-property.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename packages/tailwind/src/utils/compatibility/{convert-css-property-to-react-property.ts => get-react-property.ts} (84%) diff --git a/packages/tailwind/src/utils/compatibility/convert-css-property-to-react-property.ts b/packages/tailwind/src/utils/compatibility/get-react-property.ts similarity index 84% rename from packages/tailwind/src/utils/compatibility/convert-css-property-to-react-property.ts rename to packages/tailwind/src/utils/compatibility/get-react-property.ts index 2f8a91d3df..574b529fc8 100644 --- a/packages/tailwind/src/utils/compatibility/convert-css-property-to-react-property.ts +++ b/packages/tailwind/src/utils/compatibility/get-react-property.ts @@ -1,6 +1,6 @@ import { fromDashCaseToCamelCase } from '../text/from-dash-case-to-camel-case'; -export function convertCssPropertyToReactProperty(prop: string) { +export function getReactProperty(prop: string) { const modifiedProp = prop.toLowerCase(); if (modifiedProp.startsWith('--')) { From 1fa7ef5d04cebf55e9d1bec64c41b7910ba3cd96 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Mon, 6 Oct 2025 12:39:53 -0300 Subject: [PATCH 156/193] lint --- .../src/utils/css/make-inline-styles-for.ts | 44 ++++++++++++------- .../utils/css/resolve-all-css-variables.ts | 2 +- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/packages/tailwind/src/utils/css/make-inline-styles-for.ts b/packages/tailwind/src/utils/css/make-inline-styles-for.ts index 4f323b62e4..2a4053e79a 100644 --- a/packages/tailwind/src/utils/css/make-inline-styles-for.ts +++ b/packages/tailwind/src/utils/css/make-inline-styles-for.ts @@ -1,21 +1,42 @@ -import { type CssNode, generate, walk } from 'css-tree'; +import { type CssNode, type Declaration, generate, walk } from 'css-tree'; import { getReactProperty } from '../compatibility/get-react-property'; -import type { VariableDefinition } from './resolve-all-css-variables'; export function makeInlineStylesFor(inlinableRules: CssNode[]) { const styles: Record = {}; + const localVariableDeclarations = new Set(); for (const rule of inlinableRules) { - const localVariableDefinitions = new Set(); walk(rule, { visit: 'Declaration', enter(declaration) { if (declaration.property.startsWith('--')) { - localVariableDefinitions.add({ - declaration, - definition: generate(declaration.value).trim(), - variableName: declaration.property.trim(), + localVariableDeclarations.add(declaration); + } + }, + }); + } + + for (const rule of inlinableRules) { + walk(rule, { + visit: 'Function', + enter(func, funcParentListItem) { + if (func.name === 'var') { + let variableName: string | undefined; + walk(func, { + visit: 'Identifier', + enter(identifier) { + variableName = identifier.name; + return this.break; + }, }); + if (variableName) { + const definition = Array.from(localVariableDeclarations).find( + (declaration) => variableName === declaration.property, + ); + if (definition) { + funcParentListItem.data = definition.value; + } + } } }, }); @@ -26,16 +47,9 @@ export function makeInlineStylesFor(inlinableRules: CssNode[]) { if (declaration.property.startsWith('--')) { return; } - let value = + styles[getReactProperty(declaration.property)] = generate(declaration.value) + (declaration.important ? '!important' : ''); - for (const localVarDef of localVariableDefinitions) { - const varUsage = `var(${localVarDef.variableName})`; - if (value.includes(varUsage)) { - value = value.replaceAll(varUsage, localVarDef.definition); - } - } - styles[getReactProperty(declaration.property)] = value; }, }); } diff --git a/packages/tailwind/src/utils/css/resolve-all-css-variables.ts b/packages/tailwind/src/utils/css/resolve-all-css-variables.ts index 7f2f22e46e..3354cf0b0a 100644 --- a/packages/tailwind/src/utils/css/resolve-all-css-variables.ts +++ b/packages/tailwind/src/utils/css/resolve-all-css-variables.ts @@ -95,7 +95,7 @@ export function resolveAllCssVariables(node: CssNode) { if (/--[\S]+/.test(declaration.property)) { variableDefinitions.add({ declaration, - variableName: `${declaration.property}`, + variableName: declaration.property, definition: generate(declaration.value), }); } else { From 0316ed2fa02ee2ea5448606ed9a6a840deb9ab7a Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Tue, 7 Oct 2025 09:31:03 -0300 Subject: [PATCH 157/193] update tests --- .../make-inline-styles-for.spec.ts.snap | 9 +++++++++ .../resolve-all-css-variables.spec.ts.snap | 16 ++++++++-------- .../utils/css/resolve-all-css-variables.spec.ts | 4 +--- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/packages/tailwind/src/utils/css/__snapshots__/make-inline-styles-for.spec.ts.snap b/packages/tailwind/src/utils/css/__snapshots__/make-inline-styles-for.spec.ts.snap index fd1f76febb..ed4d537e7a 100644 --- a/packages/tailwind/src/utils/css/__snapshots__/make-inline-styles-for.spec.ts.snap +++ b/packages/tailwind/src/utils/css/__snapshots__/make-inline-styles-for.spec.ts.snap @@ -1,5 +1,14 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`makeInlineStylesFor() > does basic local variable resolution 1`] = ` +{ + "backgroundColor": " #3490dc", + "borderRadius": "0.25rem", + "color": " #fff", + "padding": "0.5rem 1rem", +} +`; + exports[`makeInlineStylesFor() > works in simple use case 1`] = ` { "backgroundColor": "#f56565", diff --git a/packages/tailwind/src/utils/css/__snapshots__/resolve-all-css-variables.spec.ts.snap b/packages/tailwind/src/utils/css/__snapshots__/resolve-all-css-variables.spec.ts.snap index dc121c1726..fc83479864 100644 --- a/packages/tailwind/src/utils/css/__snapshots__/resolve-all-css-variables.spec.ts.snap +++ b/packages/tailwind/src/utils/css/__snapshots__/resolve-all-css-variables.spec.ts.snap @@ -1,21 +1,21 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`resolveAllCSSVariables > handles deeply nested var() functions with complex parentheses 1`] = `".box{color:blue;width:20px;border:1px solid black;background:rgb(100,0,10)}"`; +exports[`resolveAllCSSVariables > handles deeply nested var() functions with complex parentheses 1`] = `":root{--primary: blue;--secondary: red;--fallback: green;--size: 20px}.box{color:blue;width:20px;border:1px solid black;--r: 100;--b: 10;background:rgb(100,0,10)}"`; -exports[`resolveAllCSSVariables > handles nested var() functions in fallbacks 1`] = `".box{width:300px;height:250px}"`; +exports[`resolveAllCSSVariables > handles nested var() functions in fallbacks 1`] = `":root{--fallback-width: 300px}.box{width:300px;height:250px}"`; -exports[`resolveAllCSSVariables > ignores @layer (properties) defined for browser compatibility 1`] = `"/*! tailwindcss v4.1.12 | MIT License | https://tailwindcss.com */@layer properties;@layer theme,base,components,utilities;@layer utilities{.mt-8{margin-top:calc(0.25rem*8)}.rounded-md{border-radius:0.375rem}.bg-blue-600{background-color:oklch(54.6%0.245 262.881)}.bg-red-500{background-color:oklch(63.7%0.237 25.331)}.bg-white{background-color:#fff}.p-4{padding:calc(0.25rem*4)}.px-3{padding-inline:calc(0.25rem*3)}.py-2{padding-block:calc(0.25rem*2)}.text-sm{font-size:0.875rem;line-height:calc(1.25/0.875)}.text-[14px]{font-size:14px}.leading-[24px]{line-height:24px}.text-black{color:#000}.text-blue-400{color:oklch(70.7%0.165 254.624)}.text-blue-600{color:oklch(54.6%0.245 262.881)}.text-gray-200{color:oklch(92.8%0.006 264.531)}.no-underline{text-decoration-line:none}}@property --tw-leading{syntax:"*";inherits:false}@layer properties{@supports ((-webkit-hyphens:none) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,::before,::after,::backdrop{--tw-leading: initial}}}"`; +exports[`resolveAllCSSVariables > ignores @layer (properties) defined for browser compatibility 1`] = `"/*! tailwindcss v4.1.12 | MIT License | https://tailwindcss.com */@layer properties;@layer theme,base,components,utilities;@layer theme{:root,:host{--color-red-500: oklch(63.7% 0.237 25.331);--color-blue-400: oklch(70.7% 0.165 254.624);--color-blue-600: oklch(54.6% 0.245 262.881);--color-gray-200: oklch(92.8% 0.006 264.531);--color-black: #000;--color-white: #fff;--spacing: 0.25rem;--text-sm: 0.875rem;--text-sm--line-height: calc(1.25 / 0.875);--radius-md: 0.375rem}}@layer utilities{.mt-8{margin-top:calc(0.25rem*8)}.rounded-md{border-radius:0.375rem}.bg-blue-600{background-color:oklch(54.6%0.245 262.881)}.bg-red-500{background-color:oklch(63.7%0.237 25.331)}.bg-white{background-color:#fff}.p-4{padding:calc(0.25rem*4)}.px-3{padding-inline:calc(0.25rem*3)}.py-2{padding-block:calc(0.25rem*2)}.text-sm{font-size:0.875rem;line-height:calc(1.25/0.875)}.text-[14px]{font-size:14px}.leading-[24px]{--tw-leading: 24px;line-height:24px}.text-black{color:#000}.text-blue-400{color:oklch(70.7%0.165 254.624)}.text-blue-600{color:oklch(54.6%0.245 262.881)}.text-gray-200{color:oklch(92.8%0.006 264.531)}.no-underline{text-decoration-line:none}}@property --tw-leading{syntax:"*";inherits:false}@layer properties{@supports ((-webkit-hyphens:none) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,::before,::after,::backdrop{--tw-leading: initial}}}"`; exports[`resolveAllCSSVariables > keeps variable usages if it cant find their declaration 1`] = `".box{width:var(--width)}"`; exports[`resolveAllCSSVariables > uses fallback values when variable definition is not found 1`] = `".box{width:150px;height:200px;margin:10px 20px}"`; -exports[`resolveAllCSSVariables > works for variables across different CSS layers 1`] = `"@layer utilities{.box{width:100px}}"`; +exports[`resolveAllCSSVariables > works for variables across different CSS layers 1`] = `"@layer base{:root{--width: 100px}}@layer utilities{.box{width:100px}}"`; -exports[`resolveAllCSSVariables > works with a variable set in a layer, and used in another through a media query 1`] = `"@layer utilities{.sm:bg-blue-300{@media (width>=40rem){background-color:blue}}}"`; +exports[`resolveAllCSSVariables > works with a variable set in a layer, and used in another through a media query 1`] = `"@layer theme{:root{--color-blue-300: blue}}@layer utilities{.sm:bg-blue-300{@media (width>=40rem){background-color:blue}}}"`; -exports[`resolveAllCSSVariables > works with multiple variables in the same declaration 1`] = `".box{margin:101px 103px 102px 104px}"`; +exports[`resolveAllCSSVariables > works with multiple variables in the same declaration 1`] = `":root{--top: 101px;--bottom: 102px;--right: 103px;--left: 104px}.box{margin:101px 103px 102px 104px}"`; -exports[`resolveAllCSSVariables > works with simple css variables on a :root 1`] = `".box{width:100px}"`; +exports[`resolveAllCSSVariables > works with simple css variables on a :root 1`] = `":root{--width: 100px}.box{width:100px}"`; -exports[`resolveAllCSSVariables > works with variables set in the same rule 1`] = `".box{width:200px}@media (min-width:1280px){.xl\\:bg-green-500{background-color:rgb(34 197 94/1)}}"`; +exports[`resolveAllCSSVariables > works with variables set in the same rule 1`] = `".box{--width: 200px;width:200px}@media (min-width:1280px){.xl\\:bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94/1)}}"`; diff --git a/packages/tailwind/src/utils/css/resolve-all-css-variables.spec.ts b/packages/tailwind/src/utils/css/resolve-all-css-variables.spec.ts index be2ceaefa6..54563eb5ef 100644 --- a/packages/tailwind/src/utils/css/resolve-all-css-variables.spec.ts +++ b/packages/tailwind/src/utils/css/resolve-all-css-variables.spec.ts @@ -254,10 +254,8 @@ div:nth-child(2*n+1) { expect(result).toContain('.test[data-attr="value*test"]{border-color:red}'); // Variables from *.universal-with-class should resolve within the same selector and to .normal - expect(result).toContain('.universal-with-class-*{text-decoration:blue}'); + expect(result).toContain('.universal-with-class-*{--class-color: blue;text-decoration:blue}'); expect(result).toContain('.normal{color:var(--class-color)}'); - - expect(result).not.toContain('var(--global-color)'); }); // this behavior is not supported anymore, since it doesn't seem like tailwindcss actually generates any CSS that uses the pattern of defining css variables from inside media queries From 05e76a1e533643b70294db96f7127a4b934727c3 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Tue, 7 Oct 2025 09:31:25 -0300 Subject: [PATCH 158/193] lint --- .../tailwind/src/utils/css/resolve-all-css-variables.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/tailwind/src/utils/css/resolve-all-css-variables.spec.ts b/packages/tailwind/src/utils/css/resolve-all-css-variables.spec.ts index 54563eb5ef..3026f71a12 100644 --- a/packages/tailwind/src/utils/css/resolve-all-css-variables.spec.ts +++ b/packages/tailwind/src/utils/css/resolve-all-css-variables.spec.ts @@ -254,7 +254,9 @@ div:nth-child(2*n+1) { expect(result).toContain('.test[data-attr="value*test"]{border-color:red}'); // Variables from *.universal-with-class should resolve within the same selector and to .normal - expect(result).toContain('.universal-with-class-*{--class-color: blue;text-decoration:blue}'); + expect(result).toContain( + '.universal-with-class-*{--class-color: blue;text-decoration:blue}', + ); expect(result).toContain('.normal{color:var(--class-color)}'); }); From 0cd34f40fb1056821f6c3b808db5753b544eb81e Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Tue, 7 Oct 2025 09:32:06 -0300 Subject: [PATCH 159/193] bump tailwind & components --- packages/components/package.json | 4 ++-- packages/tailwind/package.json | 2 +- pnpm-lock.yaml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/components/package.json b/packages/components/package.json index b20236ff2a..416cf9e653 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@react-email/components", - "version": "1.0.0-tailwindv4.4", + "version": "1.0.0-tailwindv4.5", "description": "A collection of all components React Email.", "sideEffects": false, "main": "./dist/index.js", @@ -58,7 +58,7 @@ "@react-email/render": "workspace:1.3.2", "@react-email/row": "workspace:0.0.12", "@react-email/section": "workspace:0.0.16", - "@react-email/tailwind": "workspace:2.0.0-tailwindv4.1", + "@react-email/tailwind": "workspace:2.0.0-tailwindv4.3", "@react-email/text": "workspace:0.1.6-tailwindv4.0" }, "peerDependencies": { diff --git a/packages/tailwind/package.json b/packages/tailwind/package.json index 1ab9e958b0..5853d91b00 100644 --- a/packages/tailwind/package.json +++ b/packages/tailwind/package.json @@ -1,6 +1,6 @@ { "name": "@react-email/tailwind", - "version": "2.0.0-tailwindv4.2", + "version": "2.0.0-tailwindv4.3", "description": "A React component to wrap emails with Tailwind CSS", "sideEffects": false, "main": "./dist/index.js", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6515d2d0a1..4471f03471 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -417,7 +417,7 @@ importers: specifier: workspace:0.0.16 version: link:../section '@react-email/tailwind': - specifier: workspace:2.0.0-tailwindv4.1 + specifier: workspace:2.0.0-tailwindv4.3 version: link:../tailwind '@react-email/text': specifier: workspace:0.1.6-tailwindv4.0 From 8888f61dc6087e807975769d26149eddb60966da Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Wed, 8 Oct 2025 09:19:31 -0300 Subject: [PATCH 160/193] check for our components by hand in Tailwind instead of using a property for them --- packages/tailwind/package.json | 44 ++++++++++- .../tailwind/src/utils/react/is-component.ts | 27 ++++++- pnpm-lock.yaml | 78 +++++++++++++++++++ 3 files changed, 146 insertions(+), 3 deletions(-) diff --git a/packages/tailwind/package.json b/packages/tailwind/package.json index 5853d91b00..14a68c8f42 100644 --- a/packages/tailwind/package.json +++ b/packages/tailwind/package.json @@ -43,7 +43,49 @@ "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.0 || ^19.0 || ^19.0.0-rc" + "react": "^18.0 || ^19.0 || ^19.0.0-rc", + "@react-email/button": "*", + "@react-email/body": "*", + "@react-email/code-block": "*", + "@react-email/code-inline": "*", + "@react-email/container": "*", + "@react-email/heading": "*", + "@react-email/hr": "*", + "@react-email/img": "*", + "@react-email/link": "*", + "@react-email/preview": "*" + }, + "peerDependenciesMeta": { + "@react-email/button": { + "optional": true + }, + "@react-email/body": { + "optional": true + }, + "@react-email/code-block": { + "optional": true + }, + "@react-email/code-inline": { + "optional": true + }, + "@react-email/container": { + "optional": true + }, + "@react-email/heading": { + "optional": true + }, + "@react-email/hr": { + "optional": true + }, + "@react-email/img": { + "optional": true + }, + "@react-email/link": { + "optional": true + }, + "@react-email/preview": { + "optional": true + } }, "devDependencies": { "@react-email/button": "workspace:^", diff --git a/packages/tailwind/src/utils/react/is-component.ts b/packages/tailwind/src/utils/react/is-component.ts index 431a8243c5..db630c0a51 100644 --- a/packages/tailwind/src/utils/react/is-component.ts +++ b/packages/tailwind/src/utils/react/is-component.ts @@ -1,3 +1,27 @@ +import { Body } from '@react-email/body'; +import { Button } from '@react-email/button'; +import { CodeBlock } from '@react-email/code-block'; +import { CodeInline } from '@react-email/code-inline'; +import { Container } from '@react-email/container'; +import { Heading } from '@react-email/heading'; +import { Hr } from '@react-email/hr'; +import { Img } from '@react-email/img'; +import { Link } from '@react-email/link'; +import { Preview } from '@react-email/preview'; + +const componentsToTreatAsElements: React.ReactElement['type'][] = [ + Body, + Button, + CodeBlock, + CodeInline, + Container, + Heading, + Hr, + Img, + Link, + Preview, +]; + export const isComponent = ( element: React.ReactElement, ): element is React.ReactElement> => { @@ -5,7 +29,6 @@ export const isComponent = ( (typeof element.type === 'function' || // @ts-expect-error - we know this is a component that may have a render function element.type.render !== undefined) && - // @ts-expect-error this is set for our components - element.type.tailwindTreatAsElement !== true + !componentsToTreatAsElements.includes(element.type) ); }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4471f03471..0c2c86cd30 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -927,6 +927,24 @@ importers: packages/tailwind: dependencies: + '@react-email/body': + specifier: '*' + version: 0.1.0(react@19.0.0) + '@react-email/code-block': + specifier: '*' + version: 0.1.0(react@19.0.0) + '@react-email/code-inline': + specifier: '*' + version: 0.0.5(react@19.0.0) + '@react-email/container': + specifier: '*' + version: 0.0.15(react@19.0.0) + '@react-email/img': + specifier: '*' + version: 0.0.11(react@19.0.0) + '@react-email/preview': + specifier: '*' + version: 0.0.13(react@19.0.0) react: specifier: ^19.0.0 version: 19.0.0 @@ -3506,6 +3524,41 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@react-email/body@0.1.0': + resolution: {integrity: sha512-o1bcSAmDYNNHECbkeyceCVPGmVsYvT+O3sSO/Ct7apKUu3JphTi31hu+0Nwqr/pgV5QFqdoT5vdS3SW5DJFHgQ==} + peerDependencies: + react: ^19.0.0 + + '@react-email/code-block@0.1.0': + resolution: {integrity: sha512-jSpHFsgqnQXxDIssE4gvmdtFncaFQz5D6e22BnVjcCPk/udK+0A9jRwGFEG8JD2si9ZXBmU4WsuqQEczuZn4ww==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^19.0.0 + + '@react-email/code-inline@0.0.5': + resolution: {integrity: sha512-MmAsOzdJpzsnY2cZoPHFPk6uDO/Ncpb4Kh1hAt9UZc1xOW3fIzpe1Pi9y9p6wwUmpaeeDalJxAxH6/fnTquinA==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^19.0.0 + + '@react-email/container@0.0.15': + resolution: {integrity: sha512-Qo2IQo0ru2kZq47REmHW3iXjAQaKu4tpeq/M8m1zHIVwKduL2vYOBQWbC2oDnMtWPmkBjej6XxgtZByxM6cCFg==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^19.0.0 + + '@react-email/img@0.0.11': + resolution: {integrity: sha512-aGc8Y6U5C3igoMaqAJKsCpkbm1XjguQ09Acd+YcTKwjnC2+0w3yGUJkjWB2vTx4tN8dCqQCXO8FmdJpMfOA9EQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^19.0.0 + + '@react-email/preview@0.0.13': + resolution: {integrity: sha512-F7j9FJ0JN/A4d7yr+aw28p4uX7VLWs7hTHtLo7WRyw4G+Lit6Zucq4UWKRxJC8lpsUdzVmG7aBJnKOT+urqs/w==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^19.0.0 + '@react-email/render@1.0.6': resolution: {integrity: sha512-zNueW5Wn/4jNC1c5LFgXzbUdv5Lhms+FWjOvWAhal7gx5YVf0q6dPJ0dnR70+ifo59gcMLwCZEaTS9EEuUhKvQ==} engines: {node: '>=18.0.0'} @@ -11564,6 +11617,31 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@react-email/body@0.1.0(react@19.0.0)': + dependencies: + react: 19.0.0 + + '@react-email/code-block@0.1.0(react@19.0.0)': + dependencies: + prismjs: 1.30.0 + react: 19.0.0 + + '@react-email/code-inline@0.0.5(react@19.0.0)': + dependencies: + react: 19.0.0 + + '@react-email/container@0.0.15(react@19.0.0)': + dependencies: + react: 19.0.0 + + '@react-email/img@0.0.11(react@19.0.0)': + dependencies: + react: 19.0.0 + + '@react-email/preview@0.0.13(react@19.0.0)': + dependencies: + react: 19.0.0 + '@react-email/render@1.0.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: html-to-text: 9.0.5 From 568ef31213c11f12c5d06541e9d2822c134d7c27 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Wed, 8 Oct 2025 09:19:36 -0300 Subject: [PATCH 161/193] update snapshots --- packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap | 6 +++--- .../css/__snapshots__/make-inline-styles-for.spec.ts.snap | 7 ------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap b/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap index c97688b214..7ffb5d28fc 100644 --- a/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap +++ b/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`Tailwind component >