Merge branch 'main' into fix-max-width-mapping

This commit is contained in:
Gourav N S S 2026-03-30 21:59:08 +05:30 • committed by GitHub
commit a02acb94bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
56 changed files with 2575 additions and 855 deletions

View file

@ -13,13 +13,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Guard object lookups against inherited prototype properties ([#19725](https://github.com/tailwindlabs/tailwindcss/pull/19725))
- Canonicalization: improve canonicalizations for `tracking-*` utilities by preferring non-negative utilities (e.g. `-tracking-tighter` → `tracking-wider`) ([#19827](https://github.com/tailwindlabs/tailwindcss/pull/19827))
- Fix crash due to invalid characters in candidate ([#19829](https://github.com/tailwindlabs/tailwindcss/pull/19829))
- Ensure query params in imports are considered unique resources when using `@tailwindcss/webpack` ([#19723](https://github.com/tailwindlabs/tailwindcss/pull/19723))
- Canonicalization: collapse arbitrary values into shorthand utilities (e.g. `px-[1.2rem] py-[1.2rem]` → `p-[1.2rem]`) ([#19837](https://github.com/tailwindlabs/tailwindcss/pull/19837))
- Canonicalization: collapse `border-{t,b}-*` into `border-y-*`, `border-{l,r}-*` into `border-x-*`, and `border-{t,r,b,l}-*` into `border-*` ([#19842](https://github.com/tailwindlabs/tailwindcss/pull/19842))
- Canonicalization: collapse `scroll-m{t,b}-*` into `scroll-my-*`, `scroll-m{l,r}-*` into `scroll-mx-*`, and `scroll-m{t,r,b,l}-*` into `scroll-m-*` ([#19842](https://github.com/tailwindlabs/tailwindcss/pull/19842))
- Canonicalization: collapse `scroll-p{t,b}-*` into `scroll-py-*`, `scroll-p{l,r}-*` into `scroll-px-*`, and `scroll-p{t,r,b,l}-*` into `scroll-p-*` ([#19842](https://github.com/tailwindlabs/tailwindcss/pull/19842))
- Canonicalization: collapse `overflow-{x,y}-*` into `overflow-*` ([#19842](https://github.com/tailwindlabs/tailwindcss/pull/19842))
- Canonicalization: collapse `overscroll-{x,y}-*` into `overscroll-*` ([#19842](https://github.com/tailwindlabs/tailwindcss/pull/19842))
- Read from `--placeholder-color` instead of `--background-color` for `placeholder-*` utilities ([#19843](https://github.com/tailwindlabs/tailwindcss/pull/19843))
- Upgrade: Ensure files are not emptied out when killing the upgrade process while it's running ([#19846](https://github.com/tailwindlabs/tailwindcss/pull/19846))
- Upgrade: Use `config.content` when migrating from Tailwind CSS v3 to Tailwind CSS v4 ([#19846](https://github.com/tailwindlabs/tailwindcss/pull/19846))
- Upgrade: Never migrate files that are ignored by git ([#19846](https://github.com/tailwindlabs/tailwindcss/pull/19846))
- Add `.env` and `.env.*` to default ignored content files ([#19846](https://github.com/tailwindlabs/tailwindcss/pull/19846))
- Canonicalization: migrate `overflow-ellipsis` into `text-ellipsis` ([#19849](https://github.com/tailwindlabs/tailwindcss/pull/19849))
- Canonicalization: migrate `start-full` → `inset-s-full`, `start-auto` → `inset-s-auto`, `start-px` → `inset-s-px`, and `start-<number>` → `inset-s-<number>` as well as negative versions ([#19849](https://github.com/tailwindlabs/tailwindcss/pull/19849))
- Canonicalization: migrate `end-full` → `inset-e-full`, `end-auto` → `inset-e-auto`, `end-px` → `inset-e-px`, and `end-<number>` → `inset-e-<number>` as well as negative versions ([#19849](https://github.com/tailwindlabs/tailwindcss/pull/19849))
- Canonicalization: move the `-` sign inside the arbitrary value `-left-[9rem]` → `left-[-9rem]` ([#19858](https://github.com/tailwindlabs/tailwindcss/pull/19858))
- Canonicalization: move the `-` sign outside the arbitrary value `ml-[calc(-1*var(--width))]` → `-ml-(--width)` ([#19858](https://github.com/tailwindlabs/tailwindcss/pull/19858))
- Improve performance when scanning JSONL / NDJSON files ([#19862](https://github.com/tailwindlabs/tailwindcss/pull/19862))
## [4.2.2] - 2026-03-18
### Fixed
- Don't crash when candidates contain prototype properties like `row-constructor` ([#19725](https://github.com/tailwindlabs/tailwindcss/pull/19725))
- Canonicalize `calc(var(--spacing)*…)` expressions into `--spacing(…)` ([#19769](https://github.com/tailwindlabs/tailwindcss/pull/19769))
- Fix crash in canonicalization step when handling utilities with empty property maps ([#19727](https://github.com/tailwindlabs/tailwindcss/pull/19727))
- Fix crash in canonicalization step when handling utilities containing `@property` at-rules (e.g. `shadow-sm border`) ([#19727](https://github.com/tailwindlabs/tailwindcss/pull/19727))
- Skip full reload for server only modules scanned by client CSS when using `@tailwindcss/vite` ([#19745](https://github.com/tailwindlabs/tailwindcss/pull/19745))
- Add support for Vite 8 in `@tailwindcss/vite` ([#19790](https://github.com/tailwindlabs/tailwindcss/pull/19790))
- Improve canonicalization for bare values exceeding default spacing scale suggestions (e.g. `w-1234 h-1234` → `size-1234`) ([#19809](https://github.com/tailwindlabs/tailwindcss/pull/19809))
- Fix canonicalization resulting in empty list (e.g. `w-5 h-5 size-5` → `` instead of `size-5`) ([#19812](https://github.com/tailwindlabs/tailwindcss/pull/19812))
- Fix canonicalization resulting in empty list (e.g. `w-5 h-5 size-5` → `''` instead of `size-5`) ([#19812](https://github.com/tailwindlabs/tailwindcss/pull/19812))
- Resolve tsconfig paths to allow for `@import '@/path/to/file';` when using `@tailwindcss/vite` ([#19803](https://github.com/tailwindlabs/tailwindcss/pull/19803))
## [4.2.1] - 2026-02-23
@ -3958,7 +3984,8 @@ No release notes
- Everything!
[unreleased]: https://github.com/tailwindlabs/tailwindcss/compare/v4.2.1...HEAD
[unreleased]: https://github.com/tailwindlabs/tailwindcss/compare/v4.2.2...HEAD
[4.2.2]: https://github.com/tailwindlabs/tailwindcss/compare/v4.2.1...v4.2.2
[4.2.1]: https://github.com/tailwindlabs/tailwindcss/compare/v4.2.0...v4.2.1
[4.2.0]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.18...v4.2.0
[4.1.18]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.17...v4.1.18

View file

@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-android-arm-eabi",
"version": "4.2.1",
"version": "4.2.2",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",

View file

@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-android-arm64",
"version": "4.2.1",
"version": "4.2.2",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",

View file

@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-darwin-arm64",
"version": "4.2.1",
"version": "4.2.2",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",

View file

@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-darwin-x64",
"version": "4.2.1",
"version": "4.2.2",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",

View file

@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-freebsd-x64",
"version": "4.2.1",
"version": "4.2.2",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",

View file

@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-linux-arm-gnueabihf",
"version": "4.2.1",
"version": "4.2.2",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",

View file

@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-linux-arm64-gnu",
"version": "4.2.1",
"version": "4.2.2",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",

View file

@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-linux-arm64-musl",
"version": "4.2.1",
"version": "4.2.2",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",

View file

@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-linux-x64-gnu",
"version": "4.2.1",
"version": "4.2.2",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",

View file

@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-linux-x64-musl",
"version": "4.2.1",
"version": "4.2.2",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",

View file

@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-wasm32-wasi",
"version": "4.2.1",
"version": "4.2.2",
"cpu": [
"wasm32"
],

View file

@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-win32-arm64-msvc",
"version": "4.2.1",
"version": "4.2.2",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",

View file

@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-win32-x64-msvc",
"version": "4.2.1",
"version": "4.2.2",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",

View file

@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide",
"version": "4.2.1",
"version": "4.2.2",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",

View file

@ -2,3 +2,5 @@ package-lock.json
pnpm-lock.yaml
bun.lockb
.gitignore
.env
.env.*

View file

@ -474,7 +474,7 @@ pub fn pre_process_input(content: Vec<u8>, extension: &str) -> Vec<u8> {
"heex" | "eex" | "ex" | "exs" => Elixir.process(&content),
"cshtml" | "razor" => Razor.process(&content),
"haml" => Haml.process(&content),
"json" => Json.process(&content),
"json" | "jsonl" | "ndjson" => Json.process(&content),
"md" | "mdx" => Markdown.process(&content),
"pug" => Pug.process(&content),
"rb" | "erb" => Ruby.process(&content),

View file

@ -1,6 +1,6 @@
import path from 'node:path'
import { isRepoDirty } from '../../packages/@tailwindcss-upgrade/src/utils/git'
import { candidate, css, html, js, json, test, ts, yaml } from '../utils'
import { candidate, css, html, js, json, test, ts, txt, yaml } from '../utils'
test(
'error when no CSS file with @tailwind is used',
@ -171,6 +171,56 @@ test(
},
)
test(
'only migrates files matched by `config.content` when upgrading from v3 to v4',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.js': js`
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.html'],
}
`,
'src/index.html': html`
<div class="order-[0] bg-[--my-red]"></div>
`,
'src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
'templates/email.php': html`
<div class="order-[0] bg-[--my-red]"></div>
`,
'notes/unrelated.txt': `order-[0] bg-[--my-red]`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('./**/*.{html,php,txt}')).toMatchInlineSnapshot(`
"
--- notes/unrelated.txt ---
order-[0] bg-[--my-red]
--- src/index.html ---
<div class="order-0 bg-(--my-red)"></div>
--- templates/email.php ---
<div class="order-[0] bg-[--my-red]"></div>
"
`)
},
)
test(
`upgrades a v3 project with prefixes to v4`,
{
@ -2972,6 +3022,198 @@ test(
},
)
test(
'v4 ignores .env files during template migration',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^4",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'src/app.css': css`@import 'tailwindcss';`,
'src/index.html': html`
<div class="order-[0]"></div>
`,
'src/.env': `TW_TEST_CLASS=order-[0]`,
'src/.env.production': `TW_TEST_CLASS=order-[0]`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('./src/**/{*,.env,.env.*}')).toMatchInlineSnapshot(`
"
--- ./src/index.html ---
<div class="order-0"></div>
--- ./src/.env ---
TW_TEST_CLASS=order-[0]
--- ./src/.env.production ---
TW_TEST_CLASS=order-[0]
--- ./src/app.css ---
@import 'tailwindcss';
"
`)
},
)
test(
'v4 linked configs respect `content` and still ignore gitignored files',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^4",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.js': js`
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/*.html', './src/*.less'],
}
`,
'src/app.css': css`
@import 'tailwindcss';
@config '../tailwind.config.js';
`,
// Ignore all .html files
'src/.gitignore': txt`
*.html
`,
// HTML files are in .gitignore, even though they are explicitly mentioned
// in the `content` array. Still ignore them
'src/do-not-migrate-me.html': html`
<div class="order-[0]"></div>
`,
// Should be picked up by auto-content detection
'templates/migrate-me.php': html`
<div class="order-[0]"></div>
`,
// Does not get picked up by auto content detection (because it's a less
// file), but was explicitly listed in the `content` array.
//
// A bit of a hacky way, I admit, but it allows us to differentiate
// between git ignored files, auto content detection and explicitly listed
// files.
'src/migrate-me.less': html`
<div class="order-[0]"></div>
`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('./{src,templates}/**/*')).toMatchInlineSnapshot(`
"
--- ./src/app.css ---
@import 'tailwindcss';
@config '../tailwind.config.js';
--- ./src/do-not-migrate-me.html ---
<div class="order-[0]"></div>
--- ./src/migrate-me.less ---
<div class="order-0"></div>
--- ./templates/migrate-me.php ---
<div class="order-0"></div>
"
`)
},
)
test(
'interrupting template migration does not truncate files',
{
timeout: 180_000,
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^4",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'src/app.css': css` @import 'tailwindcss'; `,
'src/index.html': html`
<div class="order-[0]"></div>
`,
'hook.cjs': js`
let fs = require('node:fs/promises')
let path = require('node:path')
let originalWriteFile = fs.writeFile.bind(fs)
fs.writeFile = async (file, contents, ...rest) => {
// Mimic a bad write
await originalWriteFile(file, '') // As-if we truncated first
console.error('__TRUNCATED_TARGET__')
await new Promise((r) => setTimeout(r, 50)) // Wait 50ms to allow us to kill the process
await originalWriteFile(file, contents, ...rest) // Write the actual contents
}
`,
'src/keep.php': `
<?php
return [
'keep' => 'this file should never be truncated',
];
`,
},
},
async ({ spawn, fs, expect }) => {
let repeatedCandidates = Array.from(
{ length: 250 },
() => '<div class="order-[0]"></div>',
).join('\n')
for (let i = 0; i < 100; i++) {
await fs.write(
`src/templates/template-${i}.php`,
`<?php\n\n${repeatedCandidates}\n\nreturn ['template' => ${i}];\n`,
)
}
let originalKeepFile = await fs.read('src/keep.php')
let originalTemplate = await fs.read('src/templates/template-0.php')
let process = await spawn('npx @tailwindcss/upgrade --force', {
env: {
NODE_OPTIONS: '--require=./hook.cjs',
},
})
// We're only interested once we start migrating the templates
await process.onStderr((message) => message.includes('Migrating templates'))
// Wait for the trigger that we are mid-write
await process.onStderr((message) => message === '__TRUNCATED_TARGET__')
// Kill the process
await process.dispose()
expect(await fs.read('src/keep.php')).toBe(originalKeepFile)
expect(await fs.read('src/templates/template-0.php')).toBe(originalTemplate)
for (let [file, contents] of await fs.glob('src/**/*.{html,php,css}')) {
expect(contents.trim(), `${file} should not be empty after interruption`).not.toBe('')
}
},
)
test(
'upgrades can run in a pnpm workspace',
{

View file

@ -1,6 +1,6 @@
import path from 'node:path'
import { describe } from 'vitest'
import { css, html, json, test, ts } from '../utils'
import { css, html, json, test, ts, txt } from '../utils'
test(
`upgrade JS config files with flat theme values, darkMode, and content fields`,
@ -322,6 +322,105 @@ test(
},
)
test(
'skips gitignored template files even when they are explicitly referenced in `content`',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'.gitignore': txt`
node_modules
src/ignored
`,
'tailwind.config.ts': ts`
import { type Config } from 'tailwindcss'
module.exports = {
content: [
'./src/migrate-me.html',
'./src/ignored/do-not-migrate-me.html',
'./node_modules/my-external-lib/template.html',
],
theme: {},
plugins: [],
} satisfies Config
`,
'src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
'src/migrate-me.html': html`
<div class="order-[0]"></div>
`,
'src/ignored/do-not-migrate-me.html': html`
<div class="order-[0]"></div>
`,
'node_modules/my-external-lib/template.html': html`
<div
class="order-[0]"
></div>
`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(
await fs.dumpFiles(
'{src/**/*.{css,html},node_modules/my-external-lib/template.html,.gitignore}',
),
).toMatchInlineSnapshot(`
"
--- .gitignore ---
node_modules
src/ignored
--- src/input.css ---
@import 'tailwindcss';
@source './ignored/do-not-migrate-me.html';
@source '../node_modules/my-external-lib/template.html';
/*
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
--- src/migrate-me.html ---
<div class="order-0"></div>
--- node_modules/my-external-lib/template.html ---
<div
class="order-[0]"
></div>
--- src/ignored/do-not-migrate-me.html ---
<div class="order-[0]"></div>
"
`)
},
)
test(
'upgrades JS config files with plugins',
{

View file

@ -16,7 +16,7 @@ const PUBLIC_PACKAGES = (await fs.readdir(path.join(REPO_ROOT, 'dist'))).map((na
)
interface SpawnedProcess {
dispose: () => void
dispose: () => Promise<void>
flush: () => void
onStdout: (predicate: (message: string) => boolean) => Promise<void>
onStderr: (predicate: (message: string) => boolean) => Promise<void>
@ -194,6 +194,7 @@ export function test(
return disposePromise
}
disposables.push(dispose)
function onExit() {
resolveDisposal?.()
}

View file

@ -1,5 +1,168 @@
import { describe } from 'vitest'
import { candidate, css, fetchStyles, html, js, retryAssertion, test, ts, txt } from '../utils'
import {
candidate,
css,
fetchStyles,
html,
js,
json,
retryAssertion,
test,
ts,
txt,
} from '../utils'
test(
'resolves tsconfig paths in production build',
{
fs: {
'package.json': json`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"tailwindcss": "workspace:^"
},
"devDependencies": {
"vite": "^8"
}
}
`,
'tsconfig.json': json`
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
`,
'vite.config.ts': ts`
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'
export default defineConfig({
build: { cssMinify: false },
plugins: [tailwindcss()],
resolve: {
tsconfigPaths: true,
},
})
`,
'index.html': html`
<head>
<link rel="stylesheet" href="./src/index.css" />
</head>
<body>
<div class="underline custom-underline">Hello, world!</div>
</body>
`,
'src/index.css': css`
@import '@/styles/base.css';
@plugin '@/plugin.js';
`,
'src/styles/base.css': css`
@reference 'tailwindcss/theme';
@import 'tailwindcss/utilities';
`,
'src/plugin.js': js`
export default function ({ addUtilities }) {
addUtilities({ '.custom-underline': { 'border-bottom': '1px solid green' } })
}
`,
},
},
async ({ fs, exec, expect }) => {
await exec('pnpm vite build')
let files = await fs.glob('dist/**/*.css')
expect(files).toHaveLength(1)
let [filename] = files[0]
await fs.expectFileToContain(filename, [candidate`underline`, candidate`custom-underline`])
},
)
test(
'resolves tsconfig paths in dev mode',
{
fs: {
'package.json': json`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"tailwindcss": "workspace:^"
},
"devDependencies": {
"vite": "^8"
}
}
`,
'tsconfig.json': json`
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
`,
'vite.config.ts': ts`
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'
export default defineConfig({
build: { cssMinify: false },
plugins: [tailwindcss()],
resolve: {
tsconfigPaths: true,
},
})
`,
'index.html': html`
<head>
<link rel="stylesheet" href="./src/index.css" />
</head>
<body>
<div class="underline custom-underline">Hello, world!</div>
</body>
`,
'src/index.css': css`
@import '@/styles/base.css';
@plugin '@/plugin.js';
`,
'src/styles/base.css': css`
@reference 'tailwindcss/theme';
@import 'tailwindcss/utilities';
`,
'src/plugin.js': js`
export default function ({ addUtilities }) {
addUtilities({ '.custom-underline': { 'border-bottom': '1px solid green' } })
}
`,
},
},
async ({ spawn, expect }) => {
let process = await spawn('pnpm vite dev')
await process.onStdout((m) => m.includes('ready in'))
let url = ''
await process.onStdout((m) => {
let match = /Local:\s*(http.*)\//.exec(m)
if (match) url = match[1]
return Boolean(url)
})
await retryAssertion(async () => {
let styles = await fetchStyles(url, '/index.html')
expect(styles).toContain(candidate`underline`)
expect(styles).toContain(candidate`custom-underline`)
})
},
)
describe.each(['postcss', 'lightningcss'])('%s', (transformer) => {
test(

View file

@ -418,3 +418,78 @@ test(
`)
},
)
test(
'@tailwindcss/webpack loader isolates cache by resource including query',
{
fs: {
'package.json': json`
{
"main": "./src/index.js",
"browser": "./src/index.js",
"dependencies": {
"css-loader": "^6",
"webpack": "^5",
"webpack-cli": "^5",
"mini-css-extract-plugin": "^2",
"tailwindcss": "workspace:^",
"@tailwindcss/webpack": "workspace:^"
}
}
`,
'webpack.config.js': js`
let MiniCssExtractPlugin = require('mini-css-extract-plugin')
let path = require('node:path')
module.exports = {
mode: 'development',
entry: {
a: './src/a.js',
b: './src/b.js',
},
output: {
clean: true,
},
plugins: [new MiniCssExtractPlugin()],
module: {
rules: [
{
test: /.css$/i,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'@tailwindcss/webpack',
path.resolve(__dirname, 'query-loader.js'),
],
},
],
},
}
`,
'query-loader.js': js`
module.exports = function (source) {
if (this.resourceQuery.includes('a')) {
return '@import "tailwindcss/utilities"; @utility only-a {color: var(--color-red-500);}'
}
if (this.resourceQuery.includes('b')) {
return '@import "tailwindcss/utilities"; @utility only-b {color: var(--color-blue-500);}'
}
return source
}
`,
'src/a.js': js`import './index.css?a'`,
'src/b.js': js`import './index.css?b'`,
'src/index.css': css``,
},
},
async ({ fs, exec }) => {
await exec('pnpm webpack --mode=development')
await fs.expectFileToContain('dist/a.css', ['only-a', '--color-red-500'])
await fs.expectFileToContain('dist/b.css', ['only-b', '--color-blue-500'])
await fs.expectFileNotToContain('dist/a.css', ['only-b', '--color-blue-500'])
await fs.expectFileNotToContain('dist/b.css', ['only-a', '--color-red-500'])
},
)

View file

@ -63,8 +63,8 @@
"packageManager": "pnpm@9.6.0",
"pnpm": {
"patchedDependencies": {
"lightningcss@1.31.1": "patches/lightningcss@1.31.1.patch",
"@parcel/watcher@2.5.1": "patches/@parcel__watcher@2.5.1.patch"
"@parcel/watcher@2.5.1": "patches/@parcel__watcher@2.5.1.patch",
"lightningcss@1.32.0": "patches/lightningcss@1.32.0.patch"
}
}
}

View file

@ -1,6 +1,6 @@
{
"name": "@tailwindcss/browser",
"version": "4.2.1",
"version": "4.2.2",
"description": "A utility-first CSS framework for rapidly building custom user interfaces.",
"license": "MIT",
"main": "./dist/index.global.js",

View file

@ -1,6 +1,6 @@
{
"name": "@tailwindcss/cli",
"version": "4.2.1",
"version": "4.2.2",
"description": "A utility-first CSS framework for rapidly building custom user interfaces.",
"license": "MIT",
"repository": {

View file

@ -1,6 +1,6 @@
{
"name": "@tailwindcss/node",
"version": "4.2.1",
"version": "4.2.2",
"description": "A utility-first CSS framework for rapidly building custom user interfaces.",
"license": "MIT",
"repository": {

View file

@ -1,6 +1,6 @@
{
"name": "@tailwindcss/postcss",
"version": "4.2.1",
"version": "4.2.2",
"description": "PostCSS plugin for Tailwind CSS, a utility-first CSS framework for rapidly building custom user interfaces",
"license": "MIT",
"repository": {

View file

@ -1,6 +1,6 @@
{
"name": "@tailwindcss/standalone",
"version": "4.2.1",
"version": "4.2.2",
"private": true,
"description": "Standalone CLI for Tailwind CSS",
"license": "MIT",

View file

@ -1,6 +1,6 @@
{
"name": "@tailwindcss/upgrade",
"version": "4.2.1",
"version": "4.2.2",
"description": "A utility-first CSS framework for rapidly building custom user interfaces.",
"license": "MIT",
"repository": {

View file

@ -2,6 +2,7 @@ import fs from 'node:fs/promises'
import path from 'node:path'
import { pkg } from '../../utils/packages'
import { highlight, info, relative, success, warn } from '../../utils/renderer'
import { writeFileSafely } from '../../utils/write-file-safely'
// Migrates simple PostCSS setups. This is to cover non-dynamic config files
// similar to the ones we have all over our docs:
@ -50,7 +51,7 @@ export async function migratePostCSSConfig(base: string) {
ranMigration = true
if (result) {
await fs.writeFile(
await writeFileSafely(
packageJsonPath,
JSON.stringify({ ...packageJson, postcss: result?.json }, null, 2),
)
@ -76,7 +77,7 @@ export async function migratePostCSSConfig(base: string) {
ranMigration = true
if (result) {
await fs.writeFile(jsonConfigPath, JSON.stringify(result.json, null, 2))
await writeFileSafely(jsonConfigPath, JSON.stringify(result.json, null, 2))
didMigrate = true
didAddPostcssClient = result.didAddPostcssClient
@ -206,7 +207,7 @@ async function migratePostCSSJSConfig(configPath: string): Promise<{
newLines.push(line)
}
}
await fs.writeFile(configPath, newLines.join('\n'))
await writeFileSafely(configPath, newLines.join('\n'))
return { didAddPostcssClient, didRemoveAutoprefixer, didRemovePostCSSImport }
}

View file

@ -9,6 +9,7 @@ import type { Config } from '../../../../tailwindcss/src/compat/plugin-api'
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map'
import { spliceChangesIntoString, type StringChange } from '../../utils/splice-changes-into-string'
import { writeFileSafely } from '../../utils/write-file-safely'
import { extractRawCandidates } from './candidates'
import { isSafeMigration } from './is-safe-migration'
import { migrateAutomaticVarInjection } from './migrate-automatic-var-injection'
@ -119,12 +120,18 @@ export default async function migrateContents(
return spliceChangesIntoString(contents, changes)
}
export async function migrate(designSystem: DesignSystem, userConfig: Config | null, file: string) {
export async function migrate(
designSystem: DesignSystem,
userConfig: Config | null,
file: string,
): Promise<boolean> {
let fullPath = path.isAbsolute(file) ? file : path.resolve(process.cwd(), file)
let contents = await fs.readFile(fullPath, 'utf-8')
await fs.writeFile(
fullPath,
await migrateContents(designSystem, userConfig, contents, extname(file)),
)
let migrated = await migrateContents(designSystem, userConfig, contents, extname(file))
if (migrated === contents) return false // Nothing changed
if (migrated.trim() === '') return false // Emptied out, something went horribly wrong
await writeFileSafely(fullPath, migrated)
return true
}

View file

@ -1,7 +1,7 @@
#!/usr/bin/env node
import { Scanner } from '@tailwindcss/oxide'
import { globby } from 'globby'
import { globby, isGitIgnored } from 'globby'
import fs from 'node:fs/promises'
import path from 'node:path'
import pc from 'picocolors'
@ -23,6 +23,7 @@ import { isRepoDirty } from './utils/git'
import { pkg } from './utils/packages'
import { eprintln, error, header, highlight, info, relative, success } from './utils/renderer'
import * as version from './utils/version'
import { writeFileSafely } from './utils/write-file-safely'
const options = {
'--config': { type: 'string', description: 'Path to the configuration file', alias: '-c' },
@ -42,6 +43,7 @@ if (flags['--help']) {
async function run() {
let base = process.cwd()
let isIgnored = await isGitIgnored({ cwd: base })
eprintln(header())
eprintln()
@ -95,6 +97,7 @@ async function run() {
info('Searching for CSS files in the current directory and its subdirectories…')
files = await globby(['**/*.css'], {
cwd: base,
absolute: true,
gitignore: true,
// gitignore: true will first search for all .gitignore including node_modules folders, this makes the initial search much faster
@ -250,7 +253,7 @@ async function run() {
for (let sheet of stylesheets) {
if (!sheet.file) continue
await fs.writeFile(sheet.file, sheet.root.toString())
await writeFileSafely(sheet.file, sheet.root.toString())
if (sheet.isTailwindRoot) {
success(`Migrated stylesheet: ${highlight(relative(sheet.file, base))}`, { prefix: '↳ ' })
@ -293,6 +296,8 @@ async function run() {
let designSystem = await sheet.designSystem()
if (!designSystem) continue
let config = configBySheet.get(sheet)
// Figure out the source files to migrate
let sources = (() => {
// Disable auto source detection
@ -300,43 +305,121 @@ async function run() {
return []
}
// No root specified, use the base directory
// No root specified
if (compiler.root === null) {
// When coming from Tailwind CSS v3, we have to use the
// `config.sources` (which came from `config.content` originally)
if (version.isMajor(3)) {
if (config?.sources) {
return config.sources.map((source) => ({ ...source, negated: false }))
}
// When we don't have any sources, then we have to fallback to no
// sources at all. We cannot fallback to the `**/*` pattern.
return []
}
// When we are upgrading a Tailwind CSS v4 and up version, we use
// the default `**/*` pattern. All custom `@source` directives will
// be attached later as sources.
return [{ base, pattern: '**/*', negated: false }]
}
// Use the specified root
return [{ ...compiler.root, negated: false }]
})().concat(compiler.sources)
let config = configBySheet.get(sheet)
let scanner = new Scanner({ sources })
let filesToMigrate = []
let ignoredPaths = new Set<string>()
for (let file of scanner.files) {
file = await fs.realpath(file).catch(() => file) // Ensure we are dealing with the real path, not symlinks
if (file.endsWith('.css')) continue
// When a file is git ignored, then we don't want to migrate it even
// if it was listed in the `config.content` array or part of any
// `@source` directives.
//
// We can make this an option later to explicitly allow this, but
// this should be the default. This guarantees that:
//
// 1. Files coming from node_modules aren't touched
// 2. Generated files aren't changed (the source should update, not the target)
// 3. You can see all the changes that happened
try {
if (isIgnored(file)) {
let culprit = file
// To prevent print all ignored files, we can also walk up the
// parent tree and log those instead _if_ they are:
//
// 1. Also git ignored
// 2. Are not going outside of the current repo
let parent = path.dirname(file)
do {
try {
if (isIgnored(parent)) {
culprit = parent
}
} catch {
// Escaping the current repo
break
}
parent = path.dirname(parent)
} while (parent)
if (ignoredPaths.has(culprit)) continue // Already logged, skip
ignoredPaths.add(culprit)
if (culprit === file) {
info(`Git ignored, skipping: ${highlight(relative(culprit, base))}`, {
prefix: '↳ ',
})
} else {
info(`Git ignored folder, skipping: ${highlight(relative(culprit, base))}`, {
prefix: '↳ ',
})
}
continue
}
} catch (err) {
info(`Outside repository, skipping: ${highlight(relative(file, base))}`, {
prefix: '↳ ',
})
// Skip this file when we run into errors. E.g.: when the current
// file is not part of the current git repo it will throw an error.
continue
}
if (seenFiles.has(file)) continue
seenFiles.add(file)
filesToMigrate.push(file)
}
// Migrate each file
let changes = 0
await Promise.allSettled(
filesToMigrate.map((file) =>
migrateTemplate(designSystem, config?.userConfig ?? null, file),
),
filesToMigrate.map(async (file) => {
let changed = await migrateTemplate(designSystem, config?.userConfig ?? null, file)
if (changed) {
changes++
info(`Migrated ${highlight(relative(file, base))}`, { prefix: '↳ ' })
}
}),
)
if (config?.configFilePath) {
success(
`Migrated templates for configuration file: ${highlight(relative(config.configFilePath, base))}`,
`Migrated templates for configuration file: ${highlight(relative(config.configFilePath, base))} (${changes} file${changes === 1 ? '' : 's'} changed)`,
{ prefix: '↳ ' },
)
} else {
success(
`Migrated templates for: ${highlight(relative(sheet.file ?? '<unknown>', base))}`,
{
prefix: '↳ ',
},
`Migrated templates for: ${highlight(relative(sheet.file ?? '<unknown>', base))} (${changes} file${changes === 1 ? '' : 's'} changed)`,
{ prefix: '↳ ' },
)
}
}

View file

@ -0,0 +1,49 @@
import { randomUUID } from 'node:crypto'
import fs from 'node:fs/promises'
import path from 'node:path'
export async function writeFileSafely(file: string, contents: string) {
// Resolve symlinks so the rename replaces the actual target file rather than
// replacing the symlink itself with a regular file.
let realFile = await fs.realpath(file).catch(() => file)
// Start by creating a new file in the current directory that is guaranteed to
// be unique (via `uuid`). We can embed the `process.id` in case we need to
// debug things later.
//
// While we can write this to a more global `/tmp` folder, I want to be 100%
// sure that we are on the same file system (same drive) so the rename
// operation is atomic. Once the file is written, we will rename the file. If
// this fails, the old file is still intact, if it works we have an updated
// file.
//
// If this still causes problems (but it will slow things down):
// 1. We could make sure that we inherit the file permissions
// 2. Use an explicit fsync to force a flush to disk
let temporaryFile = path.join(
path.dirname(realFile),
`.${path.basename(realFile)}.tailwind-upgrade.${process.pid}.${randomUUID()}.tmp`,
)
// Write file uses the `w` flag by default, which is defined as:
// > Open file for writing. The file is created (if it does not exist) or truncated (if it exists).
// > https://nodejs.org/api/fs.html#file-system-flags
//
// Which means that if this function is actively running, and you cancel the
// process at the wrong time, then the truncated files are present. Since all
// these migrations happen in parallel, multiple files are open and available
// to be written to, it could mean in multiple truncated files.
//
// Writing to a temp file first means that if the process is cancelled at this
// point, that the old original file is still correct.
//
// The rename part should be atomic (especially because we guarantee it to be
// on the same file system) so this either succeeds or doesn't happen.
try {
await fs.writeFile(temporaryFile, contents, 'utf8')
await fs.rename(temporaryFile, realFile)
} catch (error) {
await fs.unlink(temporaryFile).catch(() => {})
throw error
}
}

View file

@ -1,6 +1,6 @@
{
"name": "@tailwindcss/vite",
"version": "4.2.1",
"version": "4.2.2",
"description": "A utility-first CSS framework for rapidly building custom user interfaces.",
"license": "MIT",
"repository": {

View file

@ -62,8 +62,12 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] {
let jsResolver = config!.createResolver(config!.resolve)
customCssResolver = (id: string, base: string) => cssResolver(id, base, true, isSSR)
customJsResolver = (id: string, base: string) => jsResolver(id, base, true, isSSR)
customCssResolver = async (id: string, base: string) => {
let resolved = await cssResolver(id, base, false, isSSR)
if (resolved && !resolved.endsWith('.css')) return undefined
return resolved
}
customJsResolver = (id: string, base: string) => jsResolver(id, base, false, isSSR)
} else {
type ResolveIdFn = (
environment: Environment,
@ -105,8 +109,12 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] {
let jsResolver = createBackCompatIdResolver(env.config, env.config.resolve)
customCssResolver = (id: string, base: string) => cssResolver(env, id, base, true)
customJsResolver = (id: string, base: string) => jsResolver(env, id, base, true)
customCssResolver = async (id: string, base: string) => {
let resolved = await cssResolver(env, id, base, false)
if (resolved && !resolved.endsWith('.css')) return undefined
return resolved
}
customJsResolver = (id: string, base: string) => jsResolver(env, id, base, false)
}
return new Root(

View file

@ -1,6 +1,6 @@
{
"name": "@tailwindcss/webpack",
"version": "4.2.1",
"version": "4.2.2",
"description": "A webpack loader for Tailwind CSS v4.",
"license": "MIT",
"repository": {

View file

@ -40,8 +40,12 @@ interface CacheEntry {
const cache = new QuickLRU<string, CacheEntry>({ maxSize: 50 })
function getContextFromCache(inputFile: string, opts: LoaderOptions): CacheEntry {
let key = `${inputFile}:${opts.base ?? ''}:${JSON.stringify(opts.optimize)}`
function getCacheKey(resourceId: string, opts: LoaderOptions): string {
return `${resourceId}:${opts.base ?? ''}:${JSON.stringify(opts.optimize)}`
}
function getContextFromCache(resourceId: string, opts: LoaderOptions): CacheEntry {
let key = getCacheKey(resourceId, opts)
if (cache.has(key)) return cache.get(key)!
let entry: CacheEntry = {
mtimes: new Map<string, number>(),
@ -61,6 +65,7 @@ export default async function tailwindLoader(
let callback = this.async()
let options = this.getOptions() ?? {}
let inputFile = this.resourcePath
let resourceId = this.resource
let base = options.base ?? process.cwd()
let shouldOptimize = options.optimize ?? process.env.NODE_ENV === 'production'
let isCSSModuleFile = inputFile.endsWith('.module.css')
@ -83,7 +88,7 @@ export default async function tailwindLoader(
}
try {
let context = getContextFromCache(inputFile, options)
let context = getContextFromCache(resourceId, options)
let inputBasePath = path.dirname(path.resolve(inputFile))
// Whether this is the first build or not
@ -273,7 +278,7 @@ export default async function tailwindLoader(
callback(null, result)
} catch (error) {
// Clear the cache entry on error to force a full rebuild next time
let key = `${inputFile}:${options.base ?? ''}:${JSON.stringify(options.optimize)}`
let key = getCacheKey(resourceId, options)
cache.delete(key)
DEBUG && I.end(`[@tailwindcss/webpack] ${path.relative(base, inputFile)}`)

View file

@ -1,6 +1,6 @@
{
"name": "tailwindcss",
"version": "4.2.1",
"version": "4.2.2",
"description": "A utility-first CSS framework for rapidly building custom user interfaces.",
"license": "MIT",
"repository": {

View file

@ -0,0 +1,24 @@
import { expect, it } from 'vitest'
import { canonicalizeCalcExpressions } from './canonicalize-calc-expressions'
it.each([
['calc(-1 * var(--foo))', 'calc(var(--foo) * -1)'],
['calc(1rem + var(--foo))', 'calc(var(--foo) + 1rem)'],
['calc(2rem * calc(3px * var(--foo)))', 'calc(calc(var(--foo) * 3px) * 2rem)'],
['calc(var(--b) + var(--a))', 'calc(var(--a) + var(--b))'],
['calc(3px * 2rem)', 'calc(2rem * 3px)'],
['calc(5px * 3px)', 'calc(3px * 5px)'],
['calc(1 * 1rem)', 'calc(1rem * 1)'],
['calc(10 + 2)', 'calc(2 + 10)'],
])('`%s` → `%s` (%#)', (input, expected) => {
expect(canonicalizeCalcExpressions(input)).toBe(expected)
})
it.each([
['calc(1rem - var(--foo))'],
['calc(1rem / 2)'],
['calc(var(--a) + 1rem)'],
['calc(2rem * 3px)'],
])('should keep `%s` (%#)', (input) => {
expect(canonicalizeCalcExpressions(input)).toBe(input)
})

View file

@ -0,0 +1,82 @@
import { dimensions } from './utils/dimensions'
import * as ValueParser from './value-parser'
import { walk, WalkAction } from './walk'
// Assumption: We already assume that we receive somewhat valid `calc()`
// expressions. So we will see `calc(1 + 1)` and not `calc(1+1)`
export function canonicalizeCalcExpressions(input: string): string {
let [canonicalized, valueAst] = canonicalizeCalcExpressionsAst(ValueParser.parse(input))
return canonicalized ? ValueParser.toCss(valueAst) : input
}
export function canonicalizeCalcExpressionsAst(
ast: ValueParser.ValueAstNode[],
): [canonicalized: boolean, ast: ValueParser.ValueAstNode[]] {
let canonicalized = false
walk(ast, {
exit(valueNode) {
// We are only interested in binary expressions in `calc(…)` and `(…)`,
// and only with the `*` or `+` operators.
if (valueNode.kind !== 'function') return
if (valueNode.value !== 'calc' && valueNode.value !== '') return
if (valueNode.nodes.length !== 5) return
if (valueNode.nodes[2].kind !== 'word') return
if (valueNode.nodes[2].value !== '*' && valueNode.nodes[2].value !== '+') return
let lhs = valueNode.nodes[0]
let rhs = valueNode.nodes[4]
if (shouldSwap(lhs, rhs)) {
canonicalized = true
let replacement: ValueParser.ValueFunctionNode = {
kind: 'function',
value: valueNode.value,
nodes: [
rhs, // Now lhs
valueNode.nodes[1], // Separator
valueNode.nodes[2], // Operator
valueNode.nodes[3], // Separator
lhs, // Now rhs
],
}
return WalkAction.ReplaceSkip(replacement)
}
},
})
return [canonicalized, ast]
}
function shouldSwap(lhs: ValueParser.ValueAstNode, rhs: ValueParser.ValueAstNode): boolean {
let lhsDimension = lhs.kind === 'word' ? dimensions.get(lhs.value) : null
let rhsDimension = rhs.kind === 'word' ? dimensions.get(rhs.value) : null
if (lhsDimension !== null && rhsDimension === null) return true
if (lhsDimension === null && rhsDimension !== null) return false
if (lhsDimension !== null && rhsDimension !== null) {
let [lhsValue, lhsUnit] = lhsDimension
let [rhsValue, rhsUnit] = rhsDimension
// Within dimensions, keep unit-bearing values ahead of unitless numbers so
// `1rem` sorts before `1`.
if (lhsUnit === null && rhsUnit !== null) return true
if (lhsUnit !== null && rhsUnit === null) return false
// Then sort dimensions numerically, and finally by unit for ties.
if (lhsValue !== rhsValue) {
return lhsValue - rhsValue > 0
}
if (lhsUnit !== rhsUnit) {
return (lhsUnit ?? '').localeCompare(rhsUnit ?? '') > 0
}
}
// Both nodes are unknown values (not dimensions), sort them as strings
return ValueParser.toCss([lhs]).localeCompare(ValueParser.toCss([rhs])) > 0
}

View file

@ -1,3 +1,4 @@
import { randomUUID } from 'node:crypto'
import fs from 'node:fs'
import path from 'node:path'
import { describe, expect, test } from 'vitest'
@ -92,30 +93,6 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
}
async function expectCanonicalization(
input: string,
candidate: string,
expected: string,
options: CanonicalizeOptions = DEFAULT_CANONICALIZATION_OPTIONS,
) {
candidate = prepare(candidate)
expected = prepare(expected)
if (strategy === 'prefix') {
input = input.replace("@import 'tailwindcss';", "@import 'tailwindcss' prefix(tw);")
}
let designSystem = await designSystems.get(__dirname).get(input)
let [actual] = designSystem.canonicalizeCandidates([candidate], options)
try {
expect(actual).toBe(expected)
} catch (err) {
if (err instanceof Error) Error.captureStackTrace(err, expectCanonicalization)
throw err
}
}
async function expectCombinedCanonicalization(
input: string,
candidates: string,
expected: string,
@ -134,179 +111,190 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
try {
expect(actual).toEqual(preparedExpected)
} catch (err) {
if (err instanceof Error) Error.captureStackTrace(err, expectCombinedCanonicalization)
if (err instanceof Error) Error.captureStackTrace(err, expectCanonicalization)
throw err
}
}
/// ----------------------------------
test.each([
/// Legacy bg-gradient-* → bg-linear-*
['bg-gradient-to-t', 'bg-linear-to-t'],
['bg-gradient-to-tr', 'bg-linear-to-tr'],
['bg-gradient-to-r', 'bg-linear-to-r'],
['bg-gradient-to-br', 'bg-linear-to-br'],
['bg-gradient-to-b', 'bg-linear-to-b'],
['bg-gradient-to-bl', 'bg-linear-to-bl'],
['bg-gradient-to-l', 'bg-linear-to-l'],
['bg-gradient-to-tl', 'bg-linear-to-tl'],
/// theme(…) to `var(…)`
// Keep candidates that don't contain `theme(…)` or `theme(…, …)`
['[color:red]', 'text-[red]'],
// Handle special cases around `.1` in the `theme(…)`
['[--value:theme(spacing.1)]', '[--value:--spacing(1)]'],
['[--value:theme(fontSize.xs.1.lineHeight)]', '[--value:var(--text-xs--line-height)]'],
['[--value:theme(spacing[1.25])]', '[--value:--spacing(1.25)]'],
// Should not convert invalid spacing values to calc
['[--value:theme(spacing[1.1])]', '[--value:theme(spacing[1.1])]'],
// Convert to `var(…)` if we can resolve the path
['[color:theme(colors.red.500)]', 'text-red-500'], // Arbitrary property
['[color:theme(colors.red.500)]/50', 'text-red-500/50'], // Arbitrary property + modifier
['bg-[theme(colors.red.500)]', 'bg-red-500'], // Arbitrary value
['bg-[size:theme(spacing.4)]', 'bg-size-[--spacing(4)]'], // Arbitrary value + data type hint
// Pretty print CSS functions preceded by an operator to prevent consecutive
// operator characters.
['w-[calc(100dvh-theme(spacing.2))]', 'w-[calc(100dvh-(--spacing(2)))]'],
['w-[calc(100dvh+theme(spacing.2))]', 'w-[calc(100dvh+(--spacing(2)))]'],
['w-[calc(100dvh/theme(spacing.2))]', 'w-[calc(100dvh/(--spacing(2)))]'],
['w-[calc(100dvh*theme(spacing.2))]', 'w-[calc(100dvh*(--spacing(2)))]'],
// Convert to `var(…)` if we can resolve the path, but keep fallback values
['bg-[theme(colors.red.500,red)]', 'bg-(--color-red-500,red)'],
// Keep `theme(…)` if we can't resolve the path
['bg-[theme(colors.foo.1000)]', 'bg-[theme(colors.foo.1000)]'],
// Keep `theme(…)` if we can't resolve the path, but still try to convert the
// fallback value.
['bg-[theme(colors.foo.1000,theme(colors.red.500))]', 'bg-red-500'],
// Use `theme(…)` (deeply nested) inside of a `calc(…)` function
['text-[calc(theme(fontSize.xs)*2)]', 'text-[calc(var(--text-xs)*2)]'],
// Multiple `theme(… / …)` calls should result in modern syntax of `theme(…)`
// - Can't convert to `var(…)` because that would lose the modifier.
// - Can't convert to a candidate modifier because there are multiple
// `theme(…)` calls.
//
// If we really want to, we can make a fancy migration that tries to move it
// to a candidate modifier _if_ all `theme(…)` calls use the same modifier.
[
'[color:theme(colors.red.500/50,theme(colors.blue.500/50))]',
'text-[--theme(--color-red-500/50,--theme(--color-blue-500/50))]',
],
[
'[color:theme(colors.red.500/50,theme(colors.blue.500/50))]/50',
'text-[--theme(--color-red-500/50,--theme(--color-blue-500/50))]/50',
],
// Convert the `theme(…)`, but try to move the inline modifier (e.g. `50%`),
// to a candidate modifier.
// Arbitrary property, with simple percentage modifier
['[color:theme(colors.red.500/75%)]', 'text-red-500/75'],
// Arbitrary property, with numbers (0-1) without a unit
['[color:theme(colors.red.500/.12)]', 'text-red-500/12'],
['[color:theme(colors.red.500/0.12)]', 'text-red-500/12'],
// Arbitrary property, with more complex modifier (we only allow whole numbers
// as bare modifiers). Convert the complex numbers to arbitrary values instead.
['[color:theme(colors.red.500/12.34%)]', 'text-red-500/[12.34%]'],
['[color:theme(colors.red.500/var(--opacity))]', 'text-red-500/(--opacity)'],
['[color:theme(colors.red.500/.12345)]', 'text-red-500/1234.5'],
['[color:theme(colors.red.500/50.25%)]', 'text-red-500/50.25'],
// Arbitrary value
['bg-[theme(colors.red.500/75%)]', 'bg-red-500/75'],
['bg-[theme(colors.red.500/12.34%)]', 'bg-red-500/[12.34%]'],
// Arbitrary property that already contains a modifier
['[color:theme(colors.red.500/50%)]/50', 'text-[--theme(--color-red-500/50%)]/50'],
// Values that don't contain only `theme(…)` calls should not be converted to
// use a modifier since the color is not the whole value.
[
'shadow-[shadow:inset_0px_1px_theme(colors.white/15%)]',
'shadow-[inset_0px_1px_--theme(--color-white/15%)]',
],
// Arbitrary value, where the candidate already contains a modifier
// This should still migrate the `theme(…)` syntax to the modern syntax.
['bg-[theme(colors.red.500/50%)]/50', 'bg-[--theme(--color-red-500/50%)]/50'],
// Variants, we can't use `var(…)` especially inside of `@media(…)`. We can
// still upgrade the `theme(…)` to the modern syntax.
['max-[theme(screens.lg)]:flex', 'max-[--theme(--breakpoint-lg)]:flex'],
// There are no variables for `--spacing` multiples, so we can't convert this
['max-[theme(spacing.4)]:flex', 'max-[theme(spacing.4)]:flex'],
// This test in itself doesn't make much sense. But we need to make sure
// that this doesn't end up as the modifier in the candidate itself.
['max-[theme(spacing.4/50)]:flex', 'max-[theme(spacing.4/50)]:flex'],
// `theme(…)` calls in another CSS function is replaced correctly.
// Additionally we remove unnecessary whitespace.
['grid-cols-[min(50%_,_theme(spacing.80))_auto]', 'grid-cols-[min(50%,--spacing(80))_auto]'],
// `calc(var(--spacing)*…)` to `--spacing(…)`
['pt-[min(20%,calc(var(--spacing)*8))]', 'pt-[min(20%,--spacing(8))]'],
['pt-[min(20%,calc(var(--spacing)*var(--other)))]', 'pt-[min(20%,--spacing(var(--other)))]'],
['pt-[calc(var(--spacing)*8)]', 'pt-8'],
['pt-[calc(var(--spacing)*var(--other))]', 'pt-[--spacing(var(--other))]'],
['[padding-top:min(20%,calc(var(--spacing)*8))]', 'pt-[min(20%,--spacing(8))]'],
[
'[padding-top:min(20%,calc(var(--spacing)*var(--other)))]',
'pt-[min(20%,--spacing(var(--other)))]',
],
['[padding-top:calc(var(--spacing)*8)]', 'pt-8'],
['[padding-top:calc(var(--spacing)*var(--other))]', 'pt-[--spacing(var(--other))]'],
// `theme(…)` calls valid in v3, but not in v4 should still be converted.
['[--foo:theme(transitionDuration.500)]', '[--foo:theme(transitionDuration.500)]'],
// Renamed theme keys
['max-w-[theme(screens.md)]', 'max-w-(--breakpoint-md)'],
['w-[theme(maxWidth.md)]', 'w-md'],
// Invalid cases
['[--foo:theme(colors.red.500/50/50)]', '[--foo:theme(colors.red.500/50/50)]'],
['[--foo:theme(colors.red.500/50/50)]/50', '[--foo:theme(colors.red.500/50/50)]/50'],
// Partially invalid cases
[
'[--foo:theme(colors.red.500/50/50)_theme(colors.blue.200)]',
'[--foo:theme(colors.red.500/50/50)_var(--color-blue-200)]',
],
[
'[--foo:theme(colors.red.500/50/50)_theme(colors.blue.200)]/50',
'[--foo:theme(colors.red.500/50/50)_var(--color-blue-200)]/50',
],
// If a utility sets `property` and `--tw-{property}` with the same value,
// we can ignore the `--tw-{property}`. This is just here for composition.
// This means that we should be able to upgrade the one _without_ to the one
// _with_ the variable
['[font-weight:400]', 'font-normal'],
['[line-height:0]', 'leading-0'],
['[border-style:solid]', 'border-solid'],
])(testName, { timeout }, async (candidate, expected) => {
await expectCanonicalization(
css`
describe('deprecated utilities', () => {
test.each([
/// Legacy bg-gradient-* → bg-linear-*
['bg-gradient-to-t', 'bg-linear-to-t'],
['bg-gradient-to-tr', 'bg-linear-to-tr'],
['bg-gradient-to-r', 'bg-linear-to-r'],
['bg-gradient-to-br', 'bg-linear-to-br'],
['bg-gradient-to-b', 'bg-linear-to-b'],
['bg-gradient-to-bl', 'bg-linear-to-bl'],
['bg-gradient-to-l', 'bg-linear-to-l'],
['bg-gradient-to-tl', 'bg-linear-to-tl'],
])(testName, { timeout }, async (candidate, expected) => {
let input = css`
@import 'tailwindcss';
`,
candidate,
expected,
)
`
await expectCanonicalization(input, candidate, expected)
})
let deprecated: [string, string][] = [
['order-none', 'order-0'],
['break-words', 'wrap-break-word'],
['overflow-ellipsis', 'text-ellipsis'],
['start-full', 'inset-s-full'],
['-start-full', '-inset-s-full'],
['start-auto', 'inset-s-auto'],
['start-px', 'inset-s-px'],
['-start-px', '-inset-s-px'],
['start-8', 'inset-s-8'], // Within default spacing scale
['-start-8', '-inset-s-8'], // Within default spacing scale
['start-123', 'inset-s-123'], // Outside of default spacing scale
['-start-123', '-inset-s-123'], // Outside of default spacing scale
['end-full', 'inset-e-full'],
['-end-full', '-inset-e-full'],
['end-auto', 'inset-e-auto'],
['end-px', 'inset-e-px'],
['-end-px', '-inset-e-px'],
['end-8', 'inset-e-8'], // Within default spacing scale
['-end-8', '-inset-e-8'], // Within default spacing scale
['end-123', 'inset-e-123'], // Outside of default spacing scale
['-end-123', '-inset-e-123'], // Outside of default spacing scale
]
test.each(deprecated)(testName, { timeout }, async (candidate, expected) => {
let input = css`
@import 'tailwindcss';
`
await expectCanonicalization(input, candidate, expected)
})
describe('With custom implementation', () => {
// Creating a shared CSS file such that we can re-use the same design
// system for all of these.
let customImplementations = deprecated
.map(
([candidate]) => css`
@utility ${candidate} {
--custom-${randomUUID()}: implementation;
}
`,
)
.join('\n')
// Keep the current utility because of the custom implementation
test.each(deprecated.map(([candidate]) => [candidate, candidate]))(
testName,
{ timeout },
async (candidate, expected) => {
let input = css`
@import 'tailwindcss';
${customImplementations}
`
await expectCanonicalization(input, candidate, expected)
},
)
})
})
describe('arbitrary utilities', () => {
describe('arbitrary properties', () => {
test.each([
/// theme(…) to `var(…)`
// Keep candidates that don't contain `theme(…)` or `theme(…, …)`
['[color:red]', 'text-[red]'],
// Handle special cases around `.1` in the `theme(…)`
['[--value:theme(spacing.1)]', '[--value:--spacing(1)]'],
['[--value:theme(fontSize.xs.1.lineHeight)]', '[--value:var(--text-xs--line-height)]'],
['[--value:theme(spacing[1.25])]', '[--value:--spacing(1.25)]'],
// Should not convert invalid spacing values to calc
['[--value:theme(spacing[1.1])]', '[--value:theme(spacing[1.1])]'],
// Convert to `var(…)` if we can resolve the path
['[color:theme(colors.red.500)]', 'text-red-500'], // Arbitrary property
['[color:theme(colors.red.500)]/50', 'text-red-500/50'], // Arbitrary property + modifier
// Multiple `theme(… / …)` calls should result in modern syntax of `theme(…)`
// - Can't convert to `var(…)` because that would lose the modifier.
// - Can't convert to a candidate modifier because there are multiple
// `theme(…)` calls.
//
// If we really want to, we can make a fancy migration that tries to move it
// to a candidate modifier _if_ all `theme(…)` calls use the same modifier.
[
'[color:theme(colors.red.500/50,theme(colors.blue.500/50))]',
'text-[--theme(--color-red-500/50,--theme(--color-blue-500/50))]',
],
[
'[color:theme(colors.red.500/50,theme(colors.blue.500/50))]/50',
'text-[--theme(--color-red-500/50,--theme(--color-blue-500/50))]/50',
],
// Convert the `theme(…)`, but try to move the inline modifier (e.g. `50%`),
// to a candidate modifier.
// Arbitrary property, with simple percentage modifier
['[color:theme(colors.red.500/75%)]', 'text-red-500/75'],
// Arbitrary property, with numbers (0-1) without a unit
['[color:theme(colors.red.500/.12)]', 'text-red-500/12'],
['[color:theme(colors.red.500/0.12)]', 'text-red-500/12'],
// Arbitrary property, with more complex modifier (we only allow whole numbers
// as bare modifiers). Convert the complex numbers to arbitrary values instead.
['[color:theme(colors.red.500/12.34%)]', 'text-red-500/[12.34%]'],
['[color:theme(colors.red.500/var(--opacity))]', 'text-red-500/(--opacity)'],
['[color:theme(colors.red.500/.12345)]', 'text-red-500/1234.5'],
['[color:theme(colors.red.500/50.25%)]', 'text-red-500/50.25'],
// Arbitrary property that already contains a modifier
['[color:theme(colors.red.500/50%)]/50', 'text-[--theme(--color-red-500/50%)]/50'],
// `calc(var(--spacing)*…)` to `--spacing(…)`
['[padding-top:min(20%,calc(var(--spacing)*8))]', 'pt-[min(20%,--spacing(8))]'],
[
'[padding-top:min(20%,calc(var(--spacing)*var(--other)))]',
'pt-[min(20%,--spacing(var(--other)))]',
],
['[padding-top:calc(var(--spacing)*8)]', 'pt-8'],
['[padding-top:calc(var(--spacing)*var(--other))]', 'pt-[--spacing(var(--other))]'],
// `theme(…)` calls valid in v3, but not in v4 should still be converted.
['[--foo:theme(transitionDuration.500)]', '[--foo:theme(transitionDuration.500)]'],
// Invalid cases
['[--foo:theme(colors.red.500/50/50)]', '[--foo:theme(colors.red.500/50/50)]'],
['[--foo:theme(colors.red.500/50/50)]/50', '[--foo:theme(colors.red.500/50/50)]/50'],
// Partially invalid cases
[
'[--foo:theme(colors.red.500/50/50)_theme(colors.blue.200)]',
'[--foo:theme(colors.red.500/50/50)_var(--color-blue-200)]',
],
[
'[--foo:theme(colors.red.500/50/50)_theme(colors.blue.200)]/50',
'[--foo:theme(colors.red.500/50/50)_var(--color-blue-200)]/50',
],
// If a utility sets `property` and `--tw-{property}` with the same value,
// we can ignore the `--tw-{property}`. This is just here for composition.
// This means that we should be able to upgrade the one _without_ to the one
// _with_ the variable
['[font-weight:400]', 'font-normal'],
['[line-height:0]', 'leading-0'],
['[border-style:solid]', 'border-solid'],
])(testName, { timeout }, async (candidate, expected) => {
let input = css`
@import 'tailwindcss';
`
await expectCanonicalization(input, candidate, expected)
})
test.each([
// Arbitrary property to static utility
['[text-wrap:balance]', 'text-balance'],
@ -317,10 +305,6 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
['[display:_flex]', 'flex'],
['[display:flex_]', 'flex'],
// Arbitrary property to static utility
// Map number to keyword-like value
['leading-[1]', 'leading-none'],
// Arbitrary property to named functional utility
['[color:var(--color-red-500)]', 'text-red-500'],
['[background-color:var(--color-red-500)]', 'bg-red-500'],
@ -348,20 +332,6 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
['[grid-column:2]', 'col-2'],
['[grid-column:1234]', 'col-1234'],
// Arbitrary value to bare value
['border-[2px]', 'border-2'],
['border-[1234px]', 'border-1234'],
// Arbitrary value with data type, to more specific arbitrary value
['bg-[position:123px]', 'bg-position-[123px]'],
['bg-[size:123px]', 'bg-size-[123px]'],
// Arbitrary value with inferred data type, to more specific arbitrary value
['bg-[123px]', 'bg-position-[123px]'],
// Arbitrary value with spacing mul
['w-[64rem]', 'w-256'],
// Complex arbitrary property to arbitrary value
[
'[grid-template-columns:repeat(2,minmax(100px,1fr))]',
@ -369,13 +339,6 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
],
// Complex arbitrary property to bare value
['[grid-template-columns:repeat(2,minmax(0,1fr))]', 'grid-cols-2'],
// Arbitrary value to bare value with percentage
['from-[25%]', 'from-25%'],
// Arbitrary percentage value must be a whole number. Should not migrate to
// a bare value.
['from-[2.5%]', 'from-[2.5%]'],
])(testName, { timeout }, async (candidate, expected) => {
let input = css`
@import 'tailwindcss';
@ -392,7 +355,129 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
await expectCanonicalization(input, candidate, expected)
})
})
describe('arbitrary values', () => {
test.each([
// Convert to `var(…)` if we can resolve the path
['bg-[theme(colors.red.500)]', 'bg-red-500'], // Arbitrary value
['bg-[size:theme(spacing.4)]', 'bg-size-[--spacing(4)]'], // Arbitrary value + data type hint
// Pretty print CSS functions preceded by an operator to prevent consecutive
// operator characters.
['w-[calc(100dvh-theme(spacing.2))]', 'w-[calc(100dvh-(--spacing(2)))]'],
['w-[calc(100dvh+theme(spacing.2))]', 'w-[calc(100dvh+(--spacing(2)))]'],
['w-[calc(100dvh/theme(spacing.2))]', 'w-[calc(100dvh/(--spacing(2)))]'],
['w-[calc(100dvh*theme(spacing.2))]', 'w-[calc(100dvh*(--spacing(2)))]'],
// Convert to `var(…)` if we can resolve the path, but keep fallback values
['bg-[theme(colors.red.500,red)]', 'bg-(--color-red-500,red)'],
// Keep `theme(…)` if we can't resolve the path
['bg-[theme(colors.foo.1000)]', 'bg-[theme(colors.foo.1000)]'],
// Keep `theme(…)` if we can't resolve the path, but still try to convert the
// fallback value.
['bg-[theme(colors.foo.1000,theme(colors.red.500))]', 'bg-red-500'],
// Use `theme(…)` (deeply nested) inside of a `calc(…)` function
['text-[calc(theme(fontSize.xs)*2)]', 'text-[calc(var(--text-xs)*2)]'],
// Arbitrary value
['bg-[theme(colors.red.500/75%)]', 'bg-red-500/75'],
['bg-[theme(colors.red.500/12.34%)]', 'bg-red-500/[12.34%]'],
// Values that don't contain only `theme(…)` calls should not be converted to
// use a modifier since the color is not the whole value.
[
'shadow-[shadow:inset_0px_1px_theme(colors.white/15%)]',
'shadow-[inset_0px_1px_--theme(--color-white/15%)]',
],
// Arbitrary value, where the candidate already contains a modifier
// This should still migrate the `theme(…)` syntax to the modern syntax.
['bg-[theme(colors.red.500/50%)]/50', 'bg-[--theme(--color-red-500/50%)]/50'],
// Variants, we can't use `var(…)` especially inside of `@media(…)`. We can
// still upgrade the `theme(…)` to the modern syntax.
['max-[theme(screens.lg)]:flex', 'max-[--theme(--breakpoint-lg)]:flex'],
// There are no variables for `--spacing` multiples, so we can't convert this
['max-[theme(spacing.4)]:flex', 'max-[theme(spacing.4)]:flex'],
// This test in itself doesn't make much sense. But we need to make sure
// that this doesn't end up as the modifier in the candidate itself.
['max-[theme(spacing.4/50)]:flex', 'max-[theme(spacing.4/50)]:flex'],
// `theme(…)` calls in another CSS function is replaced correctly.
// Additionally we remove unnecessary whitespace.
['grid-cols-[min(50%_,_theme(spacing.80))_auto]', 'grid-cols-[min(50%,--spacing(80))_auto]'],
// `calc(var(--spacing)*…)` to `--spacing(…)`
['pt-[min(20%,calc(var(--spacing)*8))]', 'pt-[min(20%,--spacing(8))]'],
['pt-[min(20%,calc(var(--spacing)*var(--other)))]', 'pt-[min(20%,--spacing(var(--other)))]'],
['pt-[calc(var(--spacing)*8)]', 'pt-8'],
['pt-[calc(var(--spacing)*var(--other))]', 'pt-[--spacing(var(--other))]'],
// Renamed theme keys
['max-w-[theme(screens.md)]', 'max-w-(--breakpoint-md)'],
['w-[theme(maxWidth.md)]', 'w-md'],
// Arbitrary property to static utility
// Map number to keyword-like value
['leading-[1]', 'leading-none'],
// Arbitrary value to bare value
['border-[2px]', 'border-2'],
['border-[1234px]', 'border-1234'],
// Arbitrary value with data type, to more specific arbitrary value
['bg-[position:123px]', 'bg-position-[123px]'],
['bg-[size:123px]', 'bg-size-[123px]'],
// Arbitrary value with inferred data type, to more specific arbitrary value
['bg-[123px]', 'bg-position-[123px]'],
// Arbitrary value with spacing mul
['w-[64rem]', 'w-256'],
// Arbitrary value to bare value with percentage
['from-[25%]', 'from-25%'],
// Arbitrary percentage value must be a whole number. Should not migrate to
// a bare value.
['from-[2.5%]', 'from-[2.5%]'],
// Negative arbitrary values can be simplified
// 1. Try to move the sign _inside_ the arbitrary value
// 2. Try to move the sign _out_ of the arbitrary value
['-mt-[12rem]', '-mt-48'], // Arbitrary value → bare value
['-mt-[-12rem]', 'mt-48'], // Double negation
['-mt-[12.34rem]', 'mt-[-12.34rem]'], // Move `-` inside
['-mt-[-12.34rem]', 'mt-[12.34rem]'], // Move `-` inside, double negation
['-mt-[12.34px]', 'mt-[-12.34px]'],
['-mt-[-12.34px]', 'mt-[12.34px]'],
['-mt-[492px]', '-mt-123'], // Moving inside, allows us to migrate to a bare value
['-mt-[calc(-1*492px)]', 'mt-123'], // Double negation and constant folding into bare value
['-mt-[-492px]', 'mt-123'],
['-mt-[calc(-1*-492px)]', '-mt-123'], // Constant folding with calc expressions
['-mt-(--my-var)', '-mt-(--my-var)'], // Keep as-is
['-mt-[var(--my-var)]', '-mt-(--my-var)'], // Keep as-is, but convert to shorthand
['mt-[calc(var(--my-var)*-1)]', '-mt-(--my-var)'], // Move `-` out
['mt-[calc(-1*var(--my-var))]', '-mt-(--my-var)'], // Move `-` out
['-mt-[calc(var(--my-var)*-1)]', 'mt-(--my-var)'], // Move `-` out
['-mt-[calc(-1*var(--my-var))]', 'mt-(--my-var)'], // Move `-` out
['mt-[calc(-1*calc(-1*var(--my-var)))]', 'mt-(--my-var)'], // Move `-` out
['-mt-[calc(-1*calc(-1*var(--my-var)))]', '-mt-(--my-var)'], // Move `-` out
])(testName, { timeout }, async (candidate, expected) => {
let input = css`
@import 'tailwindcss';
`
await expectCanonicalization(input, candidate, expected)
})
})
describe('arbitrary utilities', () => {
test('migrate with custom static utility `@utility custom {…}`', { timeout }, async () => {
let candidate = '[--key:value]'
let expected = 'custom'
@ -610,71 +695,6 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
})
})
describe('deprecated utilities', () => {
test('`order-none` → `order-0`', { timeout }, async () => {
let candidate = 'order-none'
let expected = 'order-0'
let input = css`
@import 'tailwindcss';
`
await expectCanonicalization(input, candidate, expected)
})
test('`order-none` → `order-none` with custom implementation', { timeout }, async () => {
let candidate = 'order-none'
let expected = 'order-none'
let input = css`
@import 'tailwindcss';
@utility order-none {
order: none; /* imagine this exists */
}
`
await expectCanonicalization(input, candidate, expected)
})
test('`break-words` → `wrap-break-word`', { timeout }, async () => {
let candidate = 'break-words'
let expected = 'wrap-break-word'
let input = css`
@import 'tailwindcss';
`
await expectCanonicalization(input, candidate, expected)
})
test('`[overflow-wrap:break-word]` → `wrap-break-word`', { timeout }, async () => {
let candidate = '[overflow-wrap:break-word]'
let expected = 'wrap-break-word'
let input = css`
@import 'tailwindcss';
`
await expectCanonicalization(input, candidate, expected)
})
test('`break-words` → `break-words` with custom implementation', { timeout }, async () => {
let candidate = 'break-words'
let expected = 'break-words'
let input = css`
@import 'tailwindcss';
@utility break-words {
break: words; /* imagine this exists */
}
`
await expectCanonicalization(input, candidate, expected)
})
})
describe('arbitrary variants', () => {
let input = css`
@import 'tailwindcss';
@ -1044,9 +1064,21 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
test.each([
// 4 to 1
['mt-1 mr-1 mb-1 ml-1', 'm-1'],
['border-t-123 border-r-123 border-b-123 border-l-123', 'border-123'],
['border-t-1 border-r-1 border-b-1 border-l-1', 'border'], // `border` is shorter than `border-1`
['border-t-red-500 border-r-red-500 border-b-red-500 border-l-red-500', 'border-red-500'],
['scroll-mt-1 scroll-mr-1 scroll-mb-1 scroll-ml-1', 'scroll-m-1'],
['scroll-pt-1 scroll-pr-1 scroll-pb-1 scroll-pl-1', 'scroll-p-1'],
// 2 to 1
['mt-1 mb-1', 'my-1'],
['border-t-123 border-b-123', 'border-y-123'],
['border-t-1 border-b-1', 'border-y'], // `border-y` is shorter than `border-y-1`
['border-t-red-500 border-b-red-500', 'border-y-red-500'],
['scroll-mt-1 scroll-mb-1', 'scroll-my-1'],
['scroll-pt-1 scroll-pb-1', 'scroll-py-1'],
['overflow-x-hidden overflow-y-hidden', 'overflow-hidden'],
['overscroll-x-contain overscroll-y-contain', 'overscroll-contain'],
// Different order as above
['mb-1 mt-1', 'my-1'],
@ -1083,7 +1115,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
let input = css`
@import 'tailwindcss';
`
await expectCombinedCanonicalization(input, candidates, expected)
await expectCanonicalization(input, candidates, expected)
})
})
@ -1117,9 +1149,43 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
let input = css`
@import 'tailwindcss';
`
await expectCombinedCanonicalization(input, candidates.trim(), expected)
await expectCanonicalization(input, candidates.trim(), expected)
})
})
// https://github.com/tailwindlabs/tailwindcss-intellisense/issues/1558
test.each([
['tracking-[-0.05em]', 'tracking-tighter'],
['tracking-[-0.025em]', 'tracking-tight'],
['tracking-[0em]', 'tracking-normal'],
['tracking-[0.025em]', 'tracking-wide'],
['tracking-[0.05em]', 'tracking-wider'],
['tracking-[0.1em]', 'tracking-widest'],
// Negative values that don't make sense
// See: https://tailwindcss.com/docs/letter-spacing#using-negative-values
['-tracking-tighter', 'tracking-wider'],
['-tracking-tight', 'tracking-wide'],
['-tracking-normal', 'tracking-normal'],
['-tracking-wide', 'tracking-tight'],
['-tracking-wider', 'tracking-tighter'],
])(testName, { timeout }, async (candidate, expected) => {
await expectCanonicalization(
css`
@import 'tailwindcss';
@theme {
--tracking-tighter: -0.05em;
--tracking-tight: -0.025em;
--tracking-normal: 0em;
--tracking-wide: 0.025em;
--tracking-wider: 0.05em;
--tracking-widest: 0.1em;
}
`,
candidate,
expected,
)
})
})
describe('theme to var', () => {
@ -1195,84 +1261,120 @@ describe('options', () => {
})
})
// https://github.com/schoero/eslint-plugin-better-tailwindcss/issues/321
test('a subset of classes should be canonicalizable', { timeout }, async () => {
let designSystem = await designSystems.get(__dirname).get(css`
@import 'tailwindcss';
`)
describe('regressions', () => {
// https://github.com/schoero/eslint-plugin-better-tailwindcss/issues/321
{
test('a subset of classes should be canonicalizable', { timeout }, async () => {
let designSystem = await designSystems.get(__dirname).get(css`
@import 'tailwindcss';
`)
let options: CanonicalizeOptions = {
collapse: true,
logicalToPhysical: true,
rem: 16,
let options: CanonicalizeOptions = {
collapse: true,
logicalToPhysical: true,
rem: 16,
}
expect(
designSystem.canonicalizeCandidates(['underline', 'h-4', 'w-4', 'text-sm'], options),
).toEqual(['underline', 'text-sm', 'size-4'])
})
test('collapse canonicalization is not affected by previous calls', { timeout }, async () => {
let designSystem = await designSystems.get(__dirname).get(css`
@import 'tailwindcss';
`)
let options: CanonicalizeOptions = {
collapse: true,
logicalToPhysical: true,
rem: 16,
}
let target = ['underline', 'h-4', 'w-4']
expect(designSystem.canonicalizeCandidates(target, options)).toEqual(['underline', 'size-4'])
designSystem.canonicalizeCandidates(['mb-4', 'text-sm'], options)
designSystem.canonicalizeCandidates(['underline', 'mb-4'], options)
expect(designSystem.canonicalizeCandidates(target, options)).toEqual(['underline', 'size-4'])
expect(designSystem.canonicalizeCandidates(target.concat('text-sm'), options)).toEqual([
'underline',
'text-sm',
'size-4',
])
})
}
expect(
designSystem.canonicalizeCandidates(['underline', 'h-4', 'w-4', 'text-sm'], options),
).toEqual(['underline', 'text-sm', 'size-4'])
// https://github.com/tailwindlabs/tailwindcss/pull/19727
test(
'collapse does not crash when utilities with no standard properties are present',
{ timeout },
async () => {
let designSystem = await designSystems.get(__dirname).get(css`
@import 'tailwindcss';
`)
let options: CanonicalizeOptions = {
collapse: true,
logicalToPhysical: true,
rem: 16,
}
// Shadow utilities use CSS custom properties and @property rules but may
// produce empty property maps in the collapse algorithm. This should not
// crash with "Cannot read properties of null" or "X is not iterable".
expect(() =>
designSystem.canonicalizeCandidates(['shadow-sm', 'border'], options),
).not.toThrow()
expect(() => designSystem.canonicalizeCandidates(['shadow-md', 'p-4'], options)).not.toThrow()
expect(() =>
designSystem.canonicalizeCandidates(['shadow-sm', 'shadow-md'], options),
).not.toThrow()
// Verify the candidates are returned (not collapsed, since shadows can't
// meaningfully collapse with unrelated utilities)
expect(designSystem.canonicalizeCandidates(['shadow-sm', 'border'], options)).toEqual(
expect.arrayContaining(['shadow-sm', 'border']),
)
expect(designSystem.canonicalizeCandidates(['shadow-sm', 'shadow-md'], options)).toEqual(
expect.arrayContaining(['shadow-sm', 'shadow-md']),
)
},
)
// https://github.com/tailwindlabs/tailwindcss/issues/19835
test.each([
// Arbitrary values should be collapsed to another arbitrary value
[
['px-[1.2rem]', 'py-[1.2rem]', 'text-left'],
['text-left', 'p-[1.2rem]'],
],
// Arbitrary values could also be collapsed into a bare value
[
['px-[30.75rem]', 'py-[30.75rem]', 'text-left'],
['text-left', 'p-123'],
],
])(
'collapse canonicalization works for arbitrary values',
{ timeout },
async (candidates, expected) => {
let designSystem = await designSystems.get(__dirname).get(css`
@import 'tailwindcss';
`)
let options: CanonicalizeOptions = {
collapse: true,
logicalToPhysical: true,
rem: 16,
}
expect(designSystem.canonicalizeCandidates(candidates, options)).toEqual(expected)
},
)
})
test('collapse canonicalization is not affected by previous calls', { timeout }, async () => {
let designSystem = await designSystems.get(__dirname).get(css`
@import 'tailwindcss';
`)
let options: CanonicalizeOptions = {
collapse: true,
logicalToPhysical: true,
rem: 16,
}
let target = ['underline', 'h-4', 'w-4']
expect(designSystem.canonicalizeCandidates(target, options)).toEqual(['underline', 'size-4'])
designSystem.canonicalizeCandidates(['mb-4', 'text-sm'], options)
designSystem.canonicalizeCandidates(['underline', 'mb-4'], options)
expect(designSystem.canonicalizeCandidates(target, options)).toEqual(['underline', 'size-4'])
expect(designSystem.canonicalizeCandidates(target.concat('text-sm'), options)).toEqual([
'underline',
'text-sm',
'size-4',
])
})
test(
'collapse does not crash when utilities with no standard properties are present',
{ timeout },
async () => {
let designSystem = await designSystems.get(__dirname).get(css`
@import 'tailwindcss';
`)
let options: CanonicalizeOptions = {
collapse: true,
logicalToPhysical: true,
rem: 16,
}
// Shadow utilities use CSS custom properties and @property rules but may
// produce empty property maps in the collapse algorithm. This should not
// crash with "Cannot read properties of null" or "X is not iterable".
expect(() =>
designSystem.canonicalizeCandidates(['shadow-sm', 'border'], options),
).not.toThrow()
expect(() => designSystem.canonicalizeCandidates(['shadow-md', 'p-4'], options)).not.toThrow()
expect(() =>
designSystem.canonicalizeCandidates(['shadow-sm', 'shadow-md'], options),
).not.toThrow()
// Verify the candidates are returned (not collapsed, since shadows can't
// meaningfully collapse with unrelated utilities)
expect(designSystem.canonicalizeCandidates(['shadow-sm', 'border'], options)).toEqual(
expect.arrayContaining(['shadow-sm', 'border']),
)
expect(designSystem.canonicalizeCandidates(['shadow-sm', 'shadow-md'], options)).toEqual(
expect.arrayContaining(['shadow-sm', 'shadow-md']),
)
},
)

View file

@ -11,8 +11,9 @@ import {
type NamedUtilityValue,
type Variant,
} from './candidate'
import { canonicalizeCalcExpressionsAst } from './canonicalize-calc-expressions'
import { keyPathToCssProperty } from './compat/apply-config-to-theme'
import { constantFoldDeclaration } from './constant-fold-declaration'
import { constantFoldDeclaration, constantFoldDeclarationAst } from './constant-fold-declaration'
import type { DesignSystem as BaseDesignSystem } from './design-system'
import { CompileAstFlags } from './design-system'
import { expandDeclaration } from './expand-declaration'
@ -77,6 +78,10 @@ interface DesignSystem extends BaseDesignSystem {
number | null, // Rem value
DefaultMap<SignatureFeatures, SignatureOptions>
>
[COMPARE_CANDIDATES_KEY]: DefaultMap<
SignatureOptions,
(a: Candidate | string, b: Candidate | string) => boolean
>
[INTERNAL_OPTIONS_KEY]: DefaultMap<
SignatureOptions,
DefaultMap<Features, InternalCanonicalizeOptions>
@ -126,6 +131,7 @@ export function prepareDesignSystemStorage(
designSystem.storage[PRE_COMPUTED_UTILITIES_KEY] ??= createPreComputedUtilitiesCache(designSystem)
designSystem.storage[VARIANT_SIGNATURE_KEY] ??= createVariantSignatureCache(designSystem)
designSystem.storage[PRE_COMPUTED_VARIANTS_KEY] ??= createPreComputedVariantsCache(designSystem)
designSystem.storage[COMPARE_CANDIDATES_KEY] ??= createSignatureComparison(designSystem)
return designSystem
}
@ -139,6 +145,25 @@ function createSignatureOptionsCache(): DesignSystem['storage'][typeof SIGNATURE
})
}
const COMPARE_CANDIDATES_KEY = Symbol()
function createSignatureComparison(designSystem: DesignSystem) {
return new DefaultMap((options: SignatureOptions) => {
let signatures = designSystem.storage[UTILITY_SIGNATURE_KEY].get(options)
return function hasSameSignature(a: Candidate | string, b: Candidate | string): boolean {
let aCandidateString = typeof a === 'string' ? a : designSystem.printCandidate(a)
let aSignature = signatures.get(aCandidateString)
if (typeof aSignature !== 'string') return false
let bCandidateString = typeof b === 'string' ? b : designSystem.printCandidate(b)
let bSignature = signatures.get(bCandidateString)
if (typeof bSignature !== 'string') return false
return aSignature === bSignature
}
})
}
export function createSignatureOptions(
baseDesignSystem: BaseDesignSystem,
options?: CanonicalizeOptions,
@ -198,7 +223,6 @@ export function canonicalizeCandidates(
}
function collapseCandidates(options: InternalCanonicalizeOptions, candidates: string[]): string[] {
if (candidates.length <= 1) return candidates
let designSystem = options.designSystem
// To keep things simple, we group candidates such that we only collapse
@ -321,10 +345,7 @@ function collapseCandidates(options: InternalCanonicalizeOptions, candidates: st
if (relevantProperties.size === 0) return result
for (let parsedCandidate of parseCandidate(designSystem, candidate)) {
if (
parsedCandidate.kind !== 'functional' ||
parsedCandidate.value?.kind !== 'named' // Necessary for bare values
) {
if (parsedCandidate.kind !== 'functional' || parsedCandidate.value === null) {
continue
}
@ -578,6 +599,7 @@ const UTILITY_CANONICALIZATIONS: UtilityCanonicalizationFunction[] = [
bgGradientToLinear,
themeToVarUtility,
calcToSpacingFunction,
optimizeArbitraryValueExpressions,
arbitraryUtilities,
bareValueUtilities,
deprecatedUtilities,
@ -1096,6 +1118,7 @@ function arbitraryUtilities(candidate: Candidate, options: InternalCanonicalizeO
let designSystem = options.designSystem
let utilities = designSystem.storage[PRE_COMPUTED_UTILITIES_KEY].get(options.signatureOptions)
let signatures = designSystem.storage[UTILITY_SIGNATURE_KEY].get(options.signatureOptions)
let hasSameSignature = designSystem.storage[COMPARE_CANDIDATES_KEY].get(options.signatureOptions)
let targetCandidateString = designSystem.printCandidate(candidate)
@ -1105,9 +1128,7 @@ function arbitraryUtilities(candidate: Candidate, options: InternalCanonicalizeO
// Try a few options to find a suitable replacement utility
for (let replacementCandidate of tryReplacements(targetSignature, candidate)) {
let replacementString = designSystem.printCandidate(replacementCandidate)
let replacementSignature = signatures.get(replacementString)
if (replacementSignature !== targetSignature) {
if (!hasSameSignature(candidate, replacementCandidate)) {
continue
}
@ -1128,11 +1149,29 @@ function arbitraryUtilities(candidate: Candidate, options: InternalCanonicalizeO
// Find a corresponding utility for the same signature
let replacements = utilities.get(targetSignature)
// Multiple utilities can map to the same signature. Not sure how to migrate
// this one so let's just skip it for now.
//
// TODO: Do we just migrate to the first one?
if (replacements.length > 1) return
// Multiple utilities can map to the same signature.
if (replacements.length > 1) {
// Prefer positive values over negative values
let maybeReplacement: string | undefined = undefined
for (let replacement of replacements) {
if (replacement[0] === '-') continue // Skip negative values
// If multiple non-negative replacements exists then we are unsure
// what to do, so let's bail.
if (maybeReplacement) return
// Consider this replacement
maybeReplacement = replacement
}
if (maybeReplacement) {
for (let replacementCandidate of parseCandidate(designSystem, maybeReplacement)) {
yield replacementCandidate
}
}
return
}
// If we didn't find any replacement utilities, let's try to strip the
// modifier and find a replacement then. If we do, we can try to re-add the
@ -1326,6 +1365,7 @@ function bareValueUtilities(candidate: Candidate, options: InternalCanonicalizeO
let designSystem = options.designSystem
let utilities = designSystem.storage[PRE_COMPUTED_UTILITIES_KEY].get(options.signatureOptions)
let signatures = designSystem.storage[UTILITY_SIGNATURE_KEY].get(options.signatureOptions)
let hasSameSignature = designSystem.storage[COMPARE_CANDIDATES_KEY].get(options.signatureOptions)
let targetCandidateString = designSystem.printCandidate(candidate)
@ -1335,9 +1375,7 @@ function bareValueUtilities(candidate: Candidate, options: InternalCanonicalizeO
// Try a few options to find a suitable replacement utility
for (let replacementCandidate of tryReplacements(targetSignature, candidate)) {
let replacementString = designSystem.printCandidate(replacementCandidate)
let replacementSignature = signatures.get(replacementString)
if (replacementSignature !== targetSignature) {
if (!hasSameSignature(candidate, replacementCandidate)) {
continue
}
@ -1353,11 +1391,29 @@ function bareValueUtilities(candidate: Candidate, options: InternalCanonicalizeO
// Find a corresponding utility for the same signature
let replacements = utilities.get(targetSignature)
// Multiple utilities can map to the same signature. Not sure how to migrate
// this one so let's just skip it for now.
//
// TODO: Do we just migrate to the first one?
if (replacements.length > 1) return
// Multiple utilities can map to the same signature.
if (replacements.length > 1) {
// Prefer positive values over negative values
let maybeReplacement: string | undefined = undefined
for (let replacement of replacements) {
if (replacement[0] === '-') continue // Skip negative values
// If multiple non-negative replacements exists then we are unsure
// what to do, so let's bail.
if (maybeReplacement) return
// Consider this replacement
maybeReplacement = replacement
}
if (maybeReplacement) {
for (let replacementCandidate of parseCandidate(designSystem, maybeReplacement)) {
yield replacementCandidate
}
}
return
}
// If we didn't find any replacement utilities, let's try to strip the
// modifier and find a replacement then. If we do, we can try to re-add the
@ -1395,31 +1451,47 @@ function bareValueUtilities(candidate: Candidate, options: InternalCanonicalizeO
const DEPRECATION_MAP = new Map([
['order-none', 'order-0'],
['break-words', 'wrap-break-word'],
['overflow-ellipsis', 'text-ellipsis'],
])
const DEPRECATION_TRANSFORMATION_MAP = new Map([
[/^(-)?start-(.*?)$/, '$1inset-s-$2'],
[/^(-)?end-(.*?)$/, '$1inset-e-$2'],
])
function* tryDeprecatedUtilities(candidate: string) {
// Try static replacements
let replacement = DEPRECATION_MAP.get(candidate)
if (replacement) yield replacement
// Try dynamic replacements
for (let [searchValue, replaceValue] of DEPRECATION_TRANSFORMATION_MAP) {
let replacement = candidate.replace(searchValue, replaceValue)
if (replacement === candidate) continue
yield replacement
}
}
function deprecatedUtilities(
candidate: Candidate,
options: InternalCanonicalizeOptions,
): Candidate {
let designSystem = options.designSystem
let signatures = designSystem.storage[UTILITY_SIGNATURE_KEY].get(options.signatureOptions)
let hasSameSignature = designSystem.storage[COMPARE_CANDIDATES_KEY].get(options.signatureOptions)
let targetCandidateString = printUnprefixedCandidate(designSystem, candidate)
let replacementString = DEPRECATION_MAP.get(targetCandidateString) ?? null
if (replacementString === null) return candidate
for (let replacementString of tryDeprecatedUtilities(targetCandidateString)) {
if (!hasSameSignature(candidate, replacementString)) {
continue
}
let legacySignature = signatures.get(targetCandidateString)
if (typeof legacySignature !== 'string') return candidate
let [replacement] = parseCandidate(designSystem, replacementString)
return replacement
}
let replacementSignature = signatures.get(replacementString)
if (typeof replacementSignature !== 'string') return candidate
// Not the same signature, not safe to migrate
if (legacySignature !== replacementSignature) return candidate
let [replacement] = parseCandidate(designSystem, replacementString)
return replacement
return candidate
}
// ----
@ -2028,6 +2100,95 @@ function modernizeArbitraryValuesVariant(
return result
}
// ---
function optimizeArbitraryValueExpressions(
candidate: Candidate,
options: InternalCanonicalizeOptions,
): Candidate {
if (candidate.kind !== 'functional' || candidate.value?.kind !== 'arbitrary') {
return candidate
}
let designSystem = options.designSystem
let hasSameSignature = designSystem.storage[COMPARE_CANDIDATES_KEY].get(options.signatureOptions)
let valueAst = ValueParser.parse(candidate.value.value)
// Start by constant folding the value expression, when dealing with `calc(…)`
if (valueAst.length === 1 && valueAst[0].kind === 'function' && valueAst[0].value === 'calc') {
let [folded, foldedValueAst] = constantFoldDeclarationAst(valueAst)
if (folded) {
let replacement = cloneCandidate(candidate)
replacement.value!.value = ValueParser.toCss(foldedValueAst)
if (hasSameSignature(candidate, replacement)) {
candidate = replacement
valueAst = foldedValueAst
}
}
}
// Move `-` sign into the arbitrary value itself
if (candidate.root[0] === '-') {
// We're dealing with a `var(…)`, keep as-is
if (valueAst.length === 1 && valueAst[0].kind === 'function' && valueAst[0].value === 'var') {
return candidate
}
// Move `* -1` inside, and try to constant fold to see if it's even worth
// updating the candidate or not.
let expressionAst = ValueParser.parse(`calc(${candidate.value!.value} * -1)`)
let [folded, foldedExpressionAst] = constantFoldDeclarationAst(expressionAst)
if (folded) {
let replacement = cloneCandidate(candidate)
replacement.root = replacement.root.slice(1) // Drop the leading `-`
replacement.value!.value = ValueParser.toCss(foldedExpressionAst)
if (hasSameSignature(candidate, replacement)) {
candidate = replacement
valueAst = foldedExpressionAst
}
}
}
// Move `-` sign out of the arbitrary value
if (valueAst.length === 1 && valueAst[0].kind === 'function' && valueAst[0].value === 'calc') {
let calcArgs = valueAst[0].nodes
// `calc(arg * -1)` or `calc(-1 * arg)`
if (
calcArgs.length === 5 &&
calcArgs[1].kind === 'separator' &&
calcArgs[1].value === ' ' &&
calcArgs[2].kind === 'word' &&
calcArgs[2].value === '*' &&
calcArgs[3].kind === 'separator' &&
calcArgs[3].value === ' '
) {
let arg =
calcArgs[4].kind === 'word' && calcArgs[4].value === '-1'
? calcArgs[0]
: calcArgs[0].kind === 'word' && calcArgs[0].value === '-1'
? calcArgs[4]
: null
if (arg) {
let replacement = cloneCandidate(candidate)
replacement.root = `-${candidate.root}`
replacement.value!.value = ValueParser.toCss([arg])
if (hasSameSignature(candidate, replacement)) {
candidate = replacement
}
}
}
}
return candidate
}
// ----
// Optimize the modifier
@ -2221,6 +2382,8 @@ function canonicalizeAst(designSystem: DesignSystem, ast: AstNode[], options: Si
node.value = resolveVariablesInValue(node.value, designSystem)
}
let valueAst = ValueParser.parse(node.value)
// Very basic `calc(…)` constant folding to handle the spacing scale
// multiplier:
//
@ -2229,7 +2392,17 @@ function canonicalizeAst(designSystem: DesignSystem, ast: AstNode[], options: Si
// → `calc(0.25rem * 4)` ← this is the case we will see
// after inlining the variable
// → `1rem`
node.value = constantFoldDeclaration(node.value, rem)
let [folded, foldedValueAst] = constantFoldDeclarationAst(valueAst, rem)
// Normalize `calc(…)` expressions such that arguments that are
// associatively equivalent are rendered in the same way.
//
// `calc(var(--foo) * -1)` === `calc(-1 * var(--foo))`
let [normalized, canonicalizedValueAst] = canonicalizeCalcExpressionsAst(foldedValueAst)
if (folded || normalized) {
node.value = ValueParser.toCss(canonicalizedValueAst)
}
// We will normalize the `node.value`, this is the same kind of logic
// we use when printing arbitrary values. It will remove unnecessary

View file

@ -24,9 +24,30 @@ it.each([
['calc(3rem * 6)', '18rem'],
['calc(5rem / 2)', '2.5rem'],
// Negating with units
['calc(2rem * -1)', '-2rem'],
['calc(-1 * 2rem)', '-2rem'],
// Nested partial evaluation
['calc(calc(1 + 2) + 2rem)', 'calc(3 + 2rem)'],
// Nested multiplication with unknown values
['calc(2 * calc(3 * var(--foo)))', 'calc(6 * var(--foo))'],
['calc(calc(3 * var(--foo)) * 2)', 'calc(6 * var(--foo))'],
['calc(2rem * calc(3 * var(--foo)))', 'calc(6rem * var(--foo))'],
// Nested addition with unknown values
['calc(1rem + calc(2rem + var(--foo)))', 'calc(3rem + var(--foo))'],
['calc(calc(2rem + var(--foo)) + 1rem)', 'calc(3rem + var(--foo))'],
// Nested multiplication can collapse away entirely
['calc(-1 * calc(-1 * var(--foo)))', 'var(--foo)'],
['calc(calc(-1 * var(--foo)) * -1)', 'var(--foo)'],
['calc(-1 * calc(var(--foo) * -1))', 'var(--foo)'],
['calc(-1 * (-1 * var(--foo)))', 'var(--foo)'],
['calc(1 * var(--foo))', 'var(--foo)'],
['calc(var(--foo) * 1)', 'var(--foo)'],
// Evaluation only handles two operands right now, this can change in the future
['calc(1 + 2 + 3)', 'calc(1 + 2 + 3)'],
])('should constant fold `%s` into `%s`', (input, expected) => {
@ -40,6 +61,7 @@ it.each([
['calc(3rem * 3dvw)'],
['calc(3rem * 2dvh)'],
['calc(5rem / 17px)'],
['calc(2rem * calc(3px * var(--foo)))'],
])('should not constant fold different units `%s`', (input) => {
expect(constantFoldDeclaration(input)).toBe(input)
})

View file

@ -6,10 +6,18 @@ import { walk, WalkAction } from './walk'
// Assumption: We already assume that we receive somewhat valid `calc()`
// expressions. So we will see `calc(1 + 1)` and not `calc(1+1)`
export function constantFoldDeclaration(input: string, rem: number | null = null): string {
let folded = false
let valueAst = ValueParser.parse(input)
let [folded, valueAst] = constantFoldDeclarationAst(ValueParser.parse(input), rem)
walk(valueAst, {
return folded ? ValueParser.toCss(valueAst) : input
}
export function constantFoldDeclarationAst(
ast: ValueParser.ValueAstNode[],
rem: number | null = null,
): [folded: boolean, ast: ValueParser.ValueAstNode[]] {
let folded = false
walk(ast, {
exit(valueNode) {
// Canonicalize dimensions to their simplest form. This includes:
// - Convert `-0`, `+0`, `0.0`, … to `0`
@ -40,10 +48,14 @@ export function constantFoldDeclaration(input: string, rem: number | null = null
// { kind: 'word', value: '256' } 4
// ]
if (valueNode.nodes.length !== 5) return
if (valueNode.nodes[2].kind !== 'word') return
let lhs = dimensions.get(valueNode.nodes[0].value)
let lhsNode = valueNode.nodes[0]
let operator = valueNode.nodes[2].value
let rhs = dimensions.get(valueNode.nodes[4].value)
let rhsNode = valueNode.nodes[4]
let lhs = lhsNode.kind === 'word' ? dimensions.get(lhsNode.value) : null
let rhs = rhsNode.kind === 'word' ? dimensions.get(rhsNode.value) : null
// Nullify entire expression when multiplying by `0`, e.g.: `calc(0 * 100vw)` -> `0`
//
@ -57,6 +69,134 @@ export function constantFoldDeclaration(input: string, rem: number | null = null
return WalkAction.ReplaceSkip(ValueParser.word('0'))
}
if (operator === '*') {
// Multiplying by `1` can always unwrap the other side, even when that
// side is an expression like `var(--foo)` that we can't fully fold.
if (lhs?.[0] === 1 && lhs?.[1] === null) {
folded = true
return WalkAction.ReplaceSkip(rhsNode)
}
if (rhs?.[0] === 1 && rhs?.[1] === null) {
folded = true
return WalkAction.ReplaceSkip(lhsNode)
}
}
if (operator === '*' || operator === '+') {
// If only one side is known, and the unknown side is itself another
// binary expression with the same operator, try to combine the two
// known parts and keep the unknown part in place.
//
// E.g.:
//
// - `calc(2rem * calc(3 * var(--foo)))` → `calc(6rem * var(--foo))`
// - `calc(2rem + calc(3rem + var(--foo)))` → `calc(5rem + var(--foo))`
//
// At this point `lhs` and `rhs` are dimensions ([value, unit] |
// null). The `null` one is the unknown node.
let constant = lhs ?? rhs
let nestedNode = lhs === null ? lhsNode : rhs === null ? rhsNode : null
if (
constant !== null &&
nestedNode !== null &&
nestedNode.kind === 'function' &&
(nestedNode.value === 'calc' || nestedNode.value === '') &&
nestedNode.nodes.length === 5 &&
nestedNode.nodes[2].kind === 'word' &&
nestedNode.nodes[2].value === operator
) {
let nestedLhsNode = nestedNode.nodes[0]
let nestedRhsNode = nestedNode.nodes[4]
let nestedLhs =
nestedLhsNode.kind === 'word' ? dimensions.get(nestedLhsNode.value) : null
let nestedRhs =
nestedRhsNode.kind === 'word' ? dimensions.get(nestedRhsNode.value) : null
let known = nestedLhs ?? nestedRhs
let unknown =
nestedLhs === null ? nestedLhsNode : nestedRhs === null ? nestedRhsNode : null
if (known !== null && unknown !== null) {
// `*` requires both values being unitless, or one of them. Both
// values having a unit is invalid.
//
// - `2 * 3` → valid
// - `4rem * 5` → valid
// - `6 * 7rem` → valid
// - `8rem * 9rem` → invalid
if (
operator === '*' &&
!(
(constant[1] === null && known[1] === null) || // Both can be unitless
(constant[1] === null && known[1] !== null) || // One of them can be unitless, but the other can't
(constant[1] !== null && known[1] === null) // One of them can be unitless, but the other can't
)
) {
return
}
// `+` requires that the units are the same. Adding a unitless
// value to a value with a unit is not allowed.
//
// - `2 + 3` → valid
// - `4rem + 5` → invalid
// - `6 + 7rem` → invalid
// - `8rem + 9rem` → valid
if (
operator === '+' &&
!(
(constant[1] === known[1]) // Only same unit is allowed
)
) {
return
}
// Re-associate nested expressions so we can still fold the known
// part of `x op (y op z)` when `z` is unknown.
//
// Examples:
// - `calc(2 * calc(3 * var(--foo)))` -> `calc(6 * var(--foo))`
// - `calc(1rem + calc(2rem + var(--foo)))` -> `calc(3rem + var(--foo))`
let combined: string
switch (operator) {
case '*': {
combined = `${constant[0] * known[0]}${constant[1] ?? known[1] ?? ''}`
break
}
case '+': {
combined = `${constant[0] + known[0]}${constant[1] ?? known[1] ?? ''}`
break
}
default:
return
}
folded = true
if (operator === '*' && combined === '1') {
return WalkAction.ReplaceSkip(unknown)
}
let replacement: ValueParser.ValueFunctionNode = {
kind: 'function',
value: valueNode.value,
nodes: [
ValueParser.word(combined),
valueNode.nodes[1],
valueNode.nodes[2],
valueNode.nodes[3],
unknown,
],
}
return WalkAction.ReplaceSkip(replacement)
}
}
}
// We're not dealing with dimensions, so we can't fold this
if (lhs === null || rhs === null) {
return
@ -70,7 +210,9 @@ export function constantFoldDeclaration(input: string, rem: number | null = null
(lhs[1] !== null && rhs[1] === null) // Unit * Unitless, e.g.: `1rem * 2`
) {
folded = true
return WalkAction.ReplaceSkip(ValueParser.word(`${lhs[0] * rhs[0]}${lhs[1] ?? ''}`))
return WalkAction.ReplaceSkip(
ValueParser.word(`${lhs[0] * rhs[0]}${lhs[1] ?? rhs[1] ?? ''}`),
)
}
break
}
@ -111,7 +253,7 @@ export function constantFoldDeclaration(input: string, rem: number | null = null
},
})
return folded ? ValueParser.toCss(valueAst) : input
return [folded, ast]
}
function canonicalizeDimension(input: string, rem: number | null = null): string | null {

View file

@ -23,7 +23,7 @@ function expand(input: string, options: SignatureFeatures): string {
describe('expand declarations', () => {
let options = SignatureFeatures.ExpandProperties
test('expand to 4 properties', () => {
test('inset', () => {
let input = css`
.one {
inset: 10px;
@ -71,7 +71,7 @@ describe('expand declarations', () => {
`)
})
test('expand to 2 properties', () => {
test('gap', () => {
let input = css`
.one {
gap: 10px;
@ -95,6 +95,294 @@ describe('expand declarations', () => {
`)
})
test('border-width', () => {
let input = css`
.one {
border-width: 1px;
}
.two {
border-width: 1px 2px;
}
.three {
border-width: 1px 2px 3px;
}
.four {
border-width: 1px 2px 3px 4px;
}
`
expect(expand(input, options)).toMatchInlineSnapshot(`
".one {
border-top-width: 1px;
border-right-width: 1px;
border-bottom-width: 1px;
border-left-width: 1px;
}
.two {
border-top-width: 1px;
border-right-width: 2px;
border-bottom-width: 1px;
border-left-width: 2px;
}
.three {
border-top-width: 1px;
border-right-width: 2px;
border-bottom-width: 3px;
border-left-width: 2px;
}
.four {
border-top-width: 1px;
border-right-width: 2px;
border-bottom-width: 3px;
border-left-width: 4px;
}
"
`)
})
test('border-style', () => {
let input = css`
.one {
border-style: solid;
}
.two {
border-style: solid dashed;
}
.three {
border-style: solid dashed dotted;
}
.four {
border-style: solid dashed dotted double;
}
`
expect(expand(input, options)).toMatchInlineSnapshot(`
".one {
border-top-style: solid;
border-right-style: solid;
border-bottom-style: solid;
border-left-style: solid;
}
.two {
border-top-style: solid;
border-right-style: dashed;
border-bottom-style: solid;
border-left-style: dashed;
}
.three {
border-top-style: solid;
border-right-style: dashed;
border-bottom-style: dotted;
border-left-style: dashed;
}
.four {
border-top-style: solid;
border-right-style: dashed;
border-bottom-style: dotted;
border-left-style: double;
}
"
`)
})
test('border-color', () => {
let input = css`
.one {
border-color: red;
}
.two {
border-color: red green;
}
.three {
border-color: red green blue;
}
.four {
border-color: red green blue black;
}
`
expect(expand(input, options)).toMatchInlineSnapshot(`
".one {
border-top-color: red;
border-right-color: red;
border-bottom-color: red;
border-left-color: red;
}
.two {
border-top-color: red;
border-right-color: green;
border-bottom-color: red;
border-left-color: green;
}
.three {
border-top-color: red;
border-right-color: green;
border-bottom-color: blue;
border-left-color: green;
}
.four {
border-top-color: red;
border-right-color: green;
border-bottom-color: blue;
border-left-color: black;
}
"
`)
})
test('scroll-margin', () => {
let input = css`
.one {
scroll-margin: 1px;
}
.two {
scroll-margin: 1px 2px;
}
.three {
scroll-margin: 1px 2px 3px;
}
.four {
scroll-margin: 1px 2px 3px 4px;
}
`
expect(expand(input, options)).toMatchInlineSnapshot(`
".one {
scroll-margin-top: 1px;
scroll-margin-right: 1px;
scroll-margin-bottom: 1px;
scroll-margin-left: 1px;
}
.two {
scroll-margin-top: 1px;
scroll-margin-right: 2px;
scroll-margin-bottom: 1px;
scroll-margin-left: 2px;
}
.three {
scroll-margin-top: 1px;
scroll-margin-right: 2px;
scroll-margin-bottom: 3px;
scroll-margin-left: 2px;
}
.four {
scroll-margin-top: 1px;
scroll-margin-right: 2px;
scroll-margin-bottom: 3px;
scroll-margin-left: 4px;
}
"
`)
})
test('scroll-padding', () => {
let input = css`
.one {
scroll-padding: 1px;
}
.two {
scroll-padding: 1px 2px;
}
.three {
scroll-padding: 1px 2px 3px;
}
.four {
scroll-padding: 1px 2px 3px 4px;
}
`
expect(expand(input, options)).toMatchInlineSnapshot(`
".one {
scroll-padding-top: 1px;
scroll-padding-right: 1px;
scroll-padding-bottom: 1px;
scroll-padding-left: 1px;
}
.two {
scroll-padding-top: 1px;
scroll-padding-right: 2px;
scroll-padding-bottom: 1px;
scroll-padding-left: 2px;
}
.three {
scroll-padding-top: 1px;
scroll-padding-right: 2px;
scroll-padding-bottom: 3px;
scroll-padding-left: 2px;
}
.four {
scroll-padding-top: 1px;
scroll-padding-right: 2px;
scroll-padding-bottom: 3px;
scroll-padding-left: 4px;
}
"
`)
})
test('overflow', () => {
let input = css`
.one {
overflow: clip;
}
.two {
overflow: hidden visible;
}
`
expect(expand(input, options)).toMatchInlineSnapshot(`
".one {
overflow-x: clip;
overflow-y: clip;
}
.two {
overflow-x: hidden;
overflow-y: visible;
}
"
`)
})
test('overscroll-behavior', () => {
let input = css`
.one {
overscroll-behavior: none;
}
.two {
overscroll-behavior: auto contain;
}
`
expect(expand(input, options)).toMatchInlineSnapshot(`
".one {
overscroll-behavior-x: none;
overscroll-behavior-y: none;
}
.two {
overscroll-behavior-x: auto;
overscroll-behavior-y: contain;
}
"
`)
})
test('expansion with `!important`', () => {
let input = css`
.one {
@ -147,6 +435,22 @@ describe('expand declarations', () => {
describe('expand logical properties', () => {
let options = SignatureFeatures.ExpandProperties | SignatureFeatures.LogicalToPhysical
test('margin-inline', () => {
let input = css`
.example {
margin-inline: 10px 20px;
}
`
expect(expand(input, options)).toMatchInlineSnapshot(`
".example {
margin-left: 10px;
margin-right: 20px;
}
"
`)
})
test('margin-block', () => {
let input = css`
.example {
@ -162,4 +466,164 @@ describe('expand logical properties', () => {
"
`)
})
test('padding-inline', () => {
let input = css`
.example {
padding-inline: 10px 20px;
}
`
expect(expand(input, options)).toMatchInlineSnapshot(`
".example {
padding-left: 10px;
padding-right: 20px;
}
"
`)
})
test('padding-block', () => {
let input = css`
.example {
padding-block: 10px 20px;
}
`
expect(expand(input, options)).toMatchInlineSnapshot(`
".example {
padding-top: 10px;
padding-bottom: 20px;
}
"
`)
})
test('scroll-margin-inline', () => {
let input = css`
.example {
scroll-margin-inline: 10px 20px;
}
`
expect(expand(input, options)).toMatchInlineSnapshot(`
".example {
scroll-margin-left: 10px;
scroll-margin-right: 20px;
}
"
`)
})
test('scroll-margin-block', () => {
let input = css`
.example {
scroll-margin-block: 10px 20px;
}
`
expect(expand(input, options)).toMatchInlineSnapshot(`
".example {
scroll-margin-top: 10px;
scroll-margin-bottom: 20px;
}
"
`)
})
test('scroll-padding-inline', () => {
let input = css`
.example {
scroll-padding-inline: 10px 20px;
}
`
expect(expand(input, options)).toMatchInlineSnapshot(`
".example {
scroll-padding-left: 10px;
scroll-padding-right: 20px;
}
"
`)
})
test('scroll-padding-block', () => {
let input = css`
.example {
scroll-padding-block: 10px 20px;
}
`
expect(expand(input, options)).toMatchInlineSnapshot(`
".example {
scroll-padding-top: 10px;
scroll-padding-bottom: 20px;
}
"
`)
})
test('border-inline-width', () => {
let input = css`
.example {
border-inline-width: 1px;
}
`
expect(expand(input, options)).toMatchInlineSnapshot(`
".example {
border-left-width: 1px;
border-right-width: 1px;
}
"
`)
})
test('border-block-width', () => {
let input = css`
.example {
border-block-width: 1px;
}
`
expect(expand(input, options)).toMatchInlineSnapshot(`
".example {
border-bottom-width: 1px;
border-top-width: 1px;
}
"
`)
})
test('border-inline-style', () => {
let input = css`
.example {
border-inline-style: 1px;
}
`
expect(expand(input, options)).toMatchInlineSnapshot(`
".example {
border-left-style: 1px;
border-right-style: 1px;
}
"
`)
})
test('border-block-style', () => {
let input = css`
.example {
border-block-style: 1px;
}
`
expect(expand(input, options)).toMatchInlineSnapshot(`
".example {
border-bottom-style: 1px;
border-top-style: 1px;
}
"
`)
})
})

View file

@ -35,7 +35,32 @@ let VARIADIC_EXPANSION_MAP: Record<string, Record<number, [prop: string, index:
inset: createBareQuad(),
margin: createPrefixedQuad('margin'),
padding: createPrefixedQuad('padding'),
'scroll-margin': createPrefixedQuad('scroll-margin'),
'scroll-padding': createPrefixedQuad('scroll-padding'),
'border-width': createPrefixedQuad(
'border',
'top-width',
'right-width',
'bottom-width',
'left-width',
),
'border-style': createPrefixedQuad(
'border',
'top-style',
'right-style',
'bottom-style',
'left-style',
),
'border-color': createPrefixedQuad(
'border',
'top-color',
'right-color',
'bottom-color',
'left-color',
),
gap: createPair('row-gap', 'column-gap'),
overflow: createPair('overflow-x', 'overflow-y'),
'overscroll-behavior': createPair('overscroll-behavior-x', 'overscroll-behavior-y'),
}
// Depending on the length of the value, map to different properties
@ -49,6 +74,10 @@ let VARIADIC_LOGICAL_EXPANSION_MAP: Record<
'margin-inline': createPair('margin-left', 'margin-right'),
'padding-block': createPair('padding-top', 'padding-bottom'),
'padding-inline': createPair('padding-left', 'padding-right'),
'scroll-margin-block': createPair('scroll-margin-top', 'scroll-margin-bottom'),
'scroll-margin-inline': createPair('scroll-margin-left', 'scroll-margin-right'),
'scroll-padding-block': createPair('scroll-padding-top', 'scroll-padding-bottom'),
'scroll-padding-inline': createPair('scroll-padding-left', 'scroll-padding-right'),
}
// The entire value is mapped to each property

View file

@ -1501,6 +1501,14 @@ describe('Parsing theme values from CSS', () => {
`)
})
// https://github.com/tailwindlabs/tailwindcss/issues/19786
test('out-of-range escaped CSS variable candidates do not crash the build', async () => {
// Shouldn't crash
await run([
String.raw`--Coding-Projects-CharacterMapper-Master-Workspace\d8819554-4725-4235-9d22-2d0ed572e924`,
])
})
test('`@keyframes` in `@theme` are hoisted', async () => {
expect(
await compileCss(

View file

@ -5776,27 +5776,27 @@ test('rotate-x', async () => {
.-rotate-x-\\(--var\\) {
--tw-rotate-x: rotateX(calc(var(--var) * -1));
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
}
.-rotate-x-45 {
--tw-rotate-x: rotateX(calc(45deg * -1));
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
}
.rotate-x-\\(--var\\) {
--tw-rotate-x: rotateX(var(--var));
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
}
.rotate-x-45 {
--tw-rotate-x: rotateX(45deg);
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
}
.rotate-x-\\[123deg\\] {
--tw-rotate-x: rotateX(123deg);
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
}
@property --tw-rotate-x {
@ -5862,32 +5862,32 @@ test('rotate-y', async () => {
.-rotate-y-\\(--var\\) {
--tw-rotate-y: rotateY(calc(var(--var) * -1));
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
}
.-rotate-y-45 {
--tw-rotate-y: rotateY(calc(45deg * -1));
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
}
.-rotate-y-\\[123deg\\] {
--tw-rotate-y: rotateY(calc(123deg * -1));
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
}
.rotate-y-\\(--var\\) {
--tw-rotate-y: rotateY(var(--var));
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
}
.rotate-y-45 {
--tw-rotate-y: rotateY(45deg);
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
}
.rotate-y-\\[123deg\\] {
--tw-rotate-y: rotateY(123deg);
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
}
@property --tw-rotate-x {
@ -5953,32 +5953,32 @@ test('rotate-z', async () => {
.-rotate-z-\\(--var\\) {
--tw-rotate-z: rotateZ(calc(var(--var) * -1));
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
}
.-rotate-z-45 {
--tw-rotate-z: rotateZ(calc(45deg * -1));
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
}
.-rotate-z-\\[123deg\\] {
--tw-rotate-z: rotateZ(calc(123deg * -1));
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
}
.rotate-z-\\(--var\\) {
--tw-rotate-z: rotateZ(var(--var));
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
}
.rotate-z-45 {
--tw-rotate-z: rotateZ(45deg);
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
}
.rotate-z-\\[123deg\\] {
--tw-rotate-z: rotateZ(123deg);
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
}
@property --tw-rotate-x {
@ -6036,19 +6036,19 @@ test('skew', async () => {
.-skew-6 {
--tw-skew-x: skewX(calc(6deg * -1));
--tw-skew-y: skewY(calc(6deg * -1));
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
}
.skew-6 {
--tw-skew-x: skewX(6deg);
--tw-skew-y: skewY(6deg);
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
}
.skew-\\[123deg\\] {
--tw-skew-x: skewX(123deg);
--tw-skew-y: skewY(123deg);
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
}
@property --tw-rotate-x {
@ -6104,17 +6104,17 @@ test('skew-x', async () => {
.-skew-x-6 {
--tw-skew-x: skewX(calc(6deg * -1));
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
}
.skew-x-6 {
--tw-skew-x: skewX(6deg);
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
}
.skew-x-\\[123deg\\] {
--tw-skew-x: skewX(123deg);
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
}
@property --tw-rotate-x {
@ -6170,17 +6170,17 @@ test('skew-y', async () => {
.-skew-y-6 {
--tw-skew-y: skewY(calc(6deg * -1));
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
}
.skew-y-6 {
--tw-skew-y: skewY(6deg);
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
}
.skew-y-\\[123deg\\] {
--tw-skew-y: skewY(123deg);
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
}
@property --tw-rotate-x {
@ -6563,19 +6563,19 @@ test('transform', async () => {
}
.transform {
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
}
.transform-\\[scaleZ\\(2\\)_rotateY\\(45deg\\)\\] {
transform: scaleZ(2)rotateY(45deg);
transform: scaleZ(2) rotateY(45deg);
}
.transform-cpu {
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
}
.transform-gpu {
transform: translateZ(0) var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
transform: translateZ(0) var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
}
.transform-none {
@ -7087,32 +7087,32 @@ test('touch-pan', async () => {
.touch-pan-left {
--tw-pan-x: pan-left;
touch-action: var(--tw-pan-x, ) var(--tw-pan-y, ) var(--tw-pinch-zoom, );
touch-action: var(--tw-pan-x, ) var(--tw-pan-y, ) var(--tw-pinch-zoom, );
}
.touch-pan-right {
--tw-pan-x: pan-right;
touch-action: var(--tw-pan-x, ) var(--tw-pan-y, ) var(--tw-pinch-zoom, );
touch-action: var(--tw-pan-x, ) var(--tw-pan-y, ) var(--tw-pinch-zoom, );
}
.touch-pan-x {
--tw-pan-x: pan-x;
touch-action: var(--tw-pan-x, ) var(--tw-pan-y, ) var(--tw-pinch-zoom, );
touch-action: var(--tw-pan-x, ) var(--tw-pan-y, ) var(--tw-pinch-zoom, );
}
.touch-pan-down {
--tw-pan-y: pan-down;
touch-action: var(--tw-pan-x, ) var(--tw-pan-y, ) var(--tw-pinch-zoom, );
touch-action: var(--tw-pan-x, ) var(--tw-pan-y, ) var(--tw-pinch-zoom, );
}
.touch-pan-up {
--tw-pan-y: pan-up;
touch-action: var(--tw-pan-x, ) var(--tw-pan-y, ) var(--tw-pinch-zoom, );
touch-action: var(--tw-pan-x, ) var(--tw-pan-y, ) var(--tw-pinch-zoom, );
}
.touch-pan-y {
--tw-pan-y: pan-y;
touch-action: var(--tw-pan-x, ) var(--tw-pan-y, ) var(--tw-pinch-zoom, );
touch-action: var(--tw-pan-x, ) var(--tw-pan-y, ) var(--tw-pinch-zoom, );
}
@property --tw-pan-x {
@ -7162,7 +7162,7 @@ test('touch-pinch-zoom', async () => {
.touch-pinch-zoom {
--tw-pinch-zoom: pinch-zoom;
touch-action: var(--tw-pan-x, ) var(--tw-pan-y, ) var(--tw-pinch-zoom, );
touch-action: var(--tw-pan-x, ) var(--tw-pan-y, ) var(--tw-pinch-zoom, );
}
@property --tw-pan-x {
@ -23050,73 +23050,73 @@ test('filter', async () => {
.blur-\\[4px\\] {
--tw-blur: blur(4px);
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
.blur-none {
--tw-blur: ;
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
.blur-xl {
--tw-blur: blur(var(--blur-xl));
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
.brightness-50 {
--tw-brightness: brightness(50%);
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
.brightness-\\[1\\.23\\] {
--tw-brightness: brightness(1.23);
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
.contrast-50 {
--tw-contrast: contrast(50%);
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
.contrast-\\[1\\.23\\] {
--tw-contrast: contrast(1.23);
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
.drop-shadow\\/25 {
--tw-drop-shadow-alpha: 25%;
--tw-drop-shadow-size: drop-shadow(0 1px 1px var(--tw-drop-shadow-color, oklab(0% 0 0 / .25)));
--tw-drop-shadow: drop-shadow(var(--drop-shadow));
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
.drop-shadow {
--tw-drop-shadow-size: drop-shadow(0 1px 1px var(--tw-drop-shadow-color, #0000000d));
--tw-drop-shadow: drop-shadow(var(--drop-shadow));
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
.drop-shadow-\\[0_0_red\\] {
--tw-drop-shadow-size: drop-shadow(0 0 var(--tw-drop-shadow-color, red));
--tw-drop-shadow: var(--tw-drop-shadow-size);
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
.drop-shadow-multi {
--tw-drop-shadow-size: drop-shadow(0 1px 1px var(--tw-drop-shadow-color, #0000000d)) drop-shadow(0 9px 7px var(--tw-drop-shadow-color, #0000001a));
--tw-drop-shadow: drop-shadow(0 1px 1px #0000000d) drop-shadow(0 9px 7px #0000001a);
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
.drop-shadow-xl {
--tw-drop-shadow-size: drop-shadow(0 9px 7px var(--tw-drop-shadow-color, #0000001a));
--tw-drop-shadow: drop-shadow(var(--drop-shadow-xl));
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
.drop-shadow-none {
--tw-drop-shadow: ;
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
.drop-shadow-inherit {
@ -23154,91 +23154,91 @@ test('filter', async () => {
.grayscale {
--tw-grayscale: grayscale(100%);
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
.grayscale-0 {
--tw-grayscale: grayscale(0%);
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
.grayscale-\\[var\\(--value\\)\\] {
--tw-grayscale: grayscale(var(--value));
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
.-hue-rotate-15 {
--tw-hue-rotate: hue-rotate(calc(15deg * -1));
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
.-hue-rotate-\\[45deg\\] {
--tw-hue-rotate: hue-rotate(calc(45deg * -1));
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
.hue-rotate-15 {
--tw-hue-rotate: hue-rotate(15deg);
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
.hue-rotate-\\[45deg\\] {
--tw-hue-rotate: hue-rotate(45deg);
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
.invert {
--tw-invert: invert(100%);
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
.invert-0 {
--tw-invert: invert(0%);
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
.invert-\\[var\\(--value\\)\\] {
--tw-invert: invert(var(--value));
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
.saturate-0 {
--tw-saturate: saturate(0%);
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
.saturate-\\[1\\.75\\] {
--tw-saturate: saturate(1.75);
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
.saturate-\\[var\\(--value\\)\\] {
--tw-saturate: saturate(var(--value));
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
.sepia {
--tw-sepia: sepia(100%);
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
.sepia-0 {
--tw-sepia: sepia(0%);
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
.sepia-\\[50\\%\\] {
--tw-sepia: sepia(50%);
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
.sepia-\\[var\\(--value\\)\\] {
--tw-sepia: sepia(var(--value));
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
.filter {
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
.filter-\\[var\\(--value\\)\\] {
@ -23431,7 +23431,7 @@ test('filter', async () => {
.blur-none {
--tw-blur: blur(var(--blur-none));
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
@property --tw-blur {
@ -23570,187 +23570,187 @@ test('backdrop-filter', async () => {
.backdrop-blur-\\[4px\\] {
--tw-backdrop-blur: blur(4px);
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
}
.backdrop-blur-none {
--tw-backdrop-blur: ;
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
}
.backdrop-blur-xl {
--tw-backdrop-blur: blur(var(--blur-xl));
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
}
.backdrop-brightness-50 {
--tw-backdrop-brightness: brightness(50%);
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
}
.backdrop-brightness-\\[1\\.23\\] {
--tw-backdrop-brightness: brightness(1.23);
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
}
.backdrop-contrast-50 {
--tw-backdrop-contrast: contrast(50%);
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
}
.backdrop-contrast-\\[1\\.23\\] {
--tw-backdrop-contrast: contrast(1.23);
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
}
.backdrop-grayscale {
--tw-backdrop-grayscale: grayscale(100%);
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
}
.backdrop-grayscale-0 {
--tw-backdrop-grayscale: grayscale(0%);
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
}
.backdrop-grayscale-\\[var\\(--value\\)\\] {
--tw-backdrop-grayscale: grayscale(var(--value));
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
}
.-backdrop-hue-rotate-15 {
--tw-backdrop-hue-rotate: hue-rotate(calc(15deg * -1));
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
}
.-backdrop-hue-rotate-\\[45deg\\] {
--tw-backdrop-hue-rotate: hue-rotate(calc(45deg * -1));
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
}
.backdrop-hue-rotate-15 {
--tw-backdrop-hue-rotate: hue-rotate(15deg);
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
}
.backdrop-hue-rotate-\\[45deg\\] {
--tw-backdrop-hue-rotate: hue-rotate(45deg);
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
}
.backdrop-invert {
--tw-backdrop-invert: invert(100%);
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
}
.backdrop-invert-0 {
--tw-backdrop-invert: invert(0%);
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
}
.backdrop-invert-\\[var\\(--value\\)\\] {
--tw-backdrop-invert: invert(var(--value));
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
}
.backdrop-opacity-1\\.25 {
--tw-backdrop-opacity: opacity(1.25%);
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
}
.backdrop-opacity-2\\.5 {
--tw-backdrop-opacity: opacity(2.5%);
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
}
.backdrop-opacity-3\\.75 {
--tw-backdrop-opacity: opacity(3.75%);
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
}
.backdrop-opacity-50 {
--tw-backdrop-opacity: opacity(50%);
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
}
.backdrop-opacity-71 {
--tw-backdrop-opacity: opacity(71%);
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
}
.backdrop-opacity-\\[0\\.5\\] {
--tw-backdrop-opacity: opacity(.5);
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
}
.backdrop-saturate-0 {
--tw-backdrop-saturate: saturate(0%);
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
}
.backdrop-saturate-\\[1\\.75\\] {
--tw-backdrop-saturate: saturate(1.75);
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
}
.backdrop-saturate-\\[var\\(--value\\)\\] {
--tw-backdrop-saturate: saturate(var(--value));
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
}
.backdrop-sepia {
--tw-backdrop-sepia: sepia(100%);
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
}
.backdrop-sepia-0 {
--tw-backdrop-sepia: sepia(0%);
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
}
.backdrop-sepia-\\[50\\%\\] {
--tw-backdrop-sepia: sepia(50%);
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
}
.backdrop-sepia-\\[var\\(--value\\)\\] {
--tw-backdrop-sepia: sepia(var(--value));
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
}
.backdrop-filter {
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
}
.backdrop-filter-\\[var\\(--value\\)\\] {
@ -23914,8 +23914,8 @@ test('backdrop-filter', async () => {
.backdrop-blur-none {
--tw-backdrop-blur: blur(var(--backdrop-blur-none));
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
}
@property --tw-backdrop-blur {
@ -24382,27 +24382,27 @@ test('contain', async () => {
.contain-inline-size {
--tw-contain-size: inline-size;
contain: var(--tw-contain-size, ) var(--tw-contain-layout, ) var(--tw-contain-paint, ) var(--tw-contain-style, );
contain: var(--tw-contain-size, ) var(--tw-contain-layout, ) var(--tw-contain-paint, ) var(--tw-contain-style, );
}
.contain-layout {
--tw-contain-layout: layout;
contain: var(--tw-contain-size, ) var(--tw-contain-layout, ) var(--tw-contain-paint, ) var(--tw-contain-style, );
contain: var(--tw-contain-size, ) var(--tw-contain-layout, ) var(--tw-contain-paint, ) var(--tw-contain-style, );
}
.contain-paint {
--tw-contain-paint: paint;
contain: var(--tw-contain-size, ) var(--tw-contain-layout, ) var(--tw-contain-paint, ) var(--tw-contain-style, );
contain: var(--tw-contain-size, ) var(--tw-contain-layout, ) var(--tw-contain-paint, ) var(--tw-contain-style, );
}
.contain-size {
--tw-contain-size: size;
contain: var(--tw-contain-size, ) var(--tw-contain-layout, ) var(--tw-contain-paint, ) var(--tw-contain-style, );
contain: var(--tw-contain-size, ) var(--tw-contain-layout, ) var(--tw-contain-paint, ) var(--tw-contain-style, );
}
.contain-style {
--tw-contain-style: style;
contain: var(--tw-contain-size, ) var(--tw-contain-layout, ) var(--tw-contain-paint, ) var(--tw-contain-style, );
contain: var(--tw-contain-size, ) var(--tw-contain-layout, ) var(--tw-contain-paint, ) var(--tw-contain-style, );
}
.contain-\\[unset\\] {
@ -24730,42 +24730,42 @@ test('font-variant-numeric', async () => {
.diagonal-fractions {
--tw-numeric-fraction: diagonal-fractions;
font-variant-numeric: var(--tw-ordinal, ) var(--tw-slashed-zero, ) var(--tw-numeric-figure, ) var(--tw-numeric-spacing, ) var(--tw-numeric-fraction, );
font-variant-numeric: var(--tw-ordinal, ) var(--tw-slashed-zero, ) var(--tw-numeric-figure, ) var(--tw-numeric-spacing, ) var(--tw-numeric-fraction, );
}
.lining-nums {
--tw-numeric-figure: lining-nums;
font-variant-numeric: var(--tw-ordinal, ) var(--tw-slashed-zero, ) var(--tw-numeric-figure, ) var(--tw-numeric-spacing, ) var(--tw-numeric-fraction, );
font-variant-numeric: var(--tw-ordinal, ) var(--tw-slashed-zero, ) var(--tw-numeric-figure, ) var(--tw-numeric-spacing, ) var(--tw-numeric-fraction, );
}
.oldstyle-nums {
--tw-numeric-figure: oldstyle-nums;
font-variant-numeric: var(--tw-ordinal, ) var(--tw-slashed-zero, ) var(--tw-numeric-figure, ) var(--tw-numeric-spacing, ) var(--tw-numeric-fraction, );
font-variant-numeric: var(--tw-ordinal, ) var(--tw-slashed-zero, ) var(--tw-numeric-figure, ) var(--tw-numeric-spacing, ) var(--tw-numeric-fraction, );
}
.ordinal {
--tw-ordinal: ordinal;
font-variant-numeric: var(--tw-ordinal, ) var(--tw-slashed-zero, ) var(--tw-numeric-figure, ) var(--tw-numeric-spacing, ) var(--tw-numeric-fraction, );
font-variant-numeric: var(--tw-ordinal, ) var(--tw-slashed-zero, ) var(--tw-numeric-figure, ) var(--tw-numeric-spacing, ) var(--tw-numeric-fraction, );
}
.proportional-nums {
--tw-numeric-spacing: proportional-nums;
font-variant-numeric: var(--tw-ordinal, ) var(--tw-slashed-zero, ) var(--tw-numeric-figure, ) var(--tw-numeric-spacing, ) var(--tw-numeric-fraction, );
font-variant-numeric: var(--tw-ordinal, ) var(--tw-slashed-zero, ) var(--tw-numeric-figure, ) var(--tw-numeric-spacing, ) var(--tw-numeric-fraction, );
}
.slashed-zero {
--tw-slashed-zero: slashed-zero;
font-variant-numeric: var(--tw-ordinal, ) var(--tw-slashed-zero, ) var(--tw-numeric-figure, ) var(--tw-numeric-spacing, ) var(--tw-numeric-fraction, );
font-variant-numeric: var(--tw-ordinal, ) var(--tw-slashed-zero, ) var(--tw-numeric-figure, ) var(--tw-numeric-spacing, ) var(--tw-numeric-fraction, );
}
.stacked-fractions {
--tw-numeric-fraction: stacked-fractions;
font-variant-numeric: var(--tw-ordinal, ) var(--tw-slashed-zero, ) var(--tw-numeric-figure, ) var(--tw-numeric-spacing, ) var(--tw-numeric-fraction, );
font-variant-numeric: var(--tw-ordinal, ) var(--tw-slashed-zero, ) var(--tw-numeric-figure, ) var(--tw-numeric-spacing, ) var(--tw-numeric-fraction, );
}
.tabular-nums {
--tw-numeric-spacing: tabular-nums;
font-variant-numeric: var(--tw-ordinal, ) var(--tw-slashed-zero, ) var(--tw-numeric-figure, ) var(--tw-numeric-spacing, ) var(--tw-numeric-fraction, );
font-variant-numeric: var(--tw-ordinal, ) var(--tw-slashed-zero, ) var(--tw-numeric-figure, ) var(--tw-numeric-spacing, ) var(--tw-numeric-fraction, );
}
.normal-nums {
@ -27137,37 +27137,37 @@ test('ring', async () => {
}
.ring {
--tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
--tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.ring-0 {
--tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
--tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.ring-1 {
--tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
--tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.ring-2 {
--tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
--tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.ring-4 {
--tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
--tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.ring-\\[12px\\] {
--tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(12px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
--tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(12px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.ring-\\[length\\:var\\(--my-width\\)\\] {
--tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(var(--my-width) + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
--tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(var(--my-width) + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
@ -27461,7 +27461,7 @@ test('ring', async () => {
}
.ring {
--tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
--tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
@ -28031,32 +28031,32 @@ test('ring-offset', async () => {
.ring-offset-0 {
--tw-ring-offset-width: 0px;
--tw-ring-offset-shadow: var(--tw-ring-inset, ) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-offset-shadow: var(--tw-ring-inset, ) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
}
.ring-offset-1 {
--tw-ring-offset-width: 1px;
--tw-ring-offset-shadow: var(--tw-ring-inset, ) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-offset-shadow: var(--tw-ring-inset, ) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
}
.ring-offset-2 {
--tw-ring-offset-width: 2px;
--tw-ring-offset-shadow: var(--tw-ring-inset, ) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-offset-shadow: var(--tw-ring-inset, ) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
}
.ring-offset-4 {
--tw-ring-offset-width: 4px;
--tw-ring-offset-shadow: var(--tw-ring-inset, ) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-offset-shadow: var(--tw-ring-inset, ) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
}
.ring-offset-\\[12px\\] {
--tw-ring-offset-width: 12px;
--tw-ring-offset-shadow: var(--tw-ring-inset, ) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-offset-shadow: var(--tw-ring-inset, ) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
}
.ring-offset-\\[length\\:var\\(--my-width\\)\\] {
--tw-ring-offset-width: var(--my-width);
--tw-ring-offset-shadow: var(--tw-ring-inset, ) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-offset-shadow: var(--tw-ring-inset, ) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
}
.ring-offset-\\[\\#0088cc\\] {

View file

@ -3960,7 +3960,7 @@ export function createUtilities(theme: Theme) {
])
colorUtility('placeholder', {
themeKeys: ['--background-color', '--color'],
themeKeys: ['--placeholder-color', '--color'],
handle: (value) => [
styleRule('&::placeholder', [decl('--tw-sort', 'placeholder-color'), decl('color', value)]),
],

View file

@ -11,4 +11,14 @@ describe('unescape', () => {
test('removes backslashes', () => {
expect(unescape(String.raw`red-1\/2`)).toMatchInlineSnapshot(`"red-1/2"`)
})
test('replaces out-of-range escaped code points', () => {
expect(
unescape(
String.raw`--Coding-Projects-CharacterMapper-Master-Workspace\d8819554-4725-4235-9d22-2d0ed572e924`,
),
).toMatchInlineSnapshot(
`"--Coding-Projects-CharacterMapper-Master-Workspace<63>54-4725-4235-9d22-2d0ed572e924"`,
)
})
})

View file

@ -74,8 +74,24 @@ export function escape(value: string) {
export function unescape(escaped: string) {
return escaped.replace(/\\([\dA-Fa-f]{1,6}[\t\n\f\r ]?|[\S\s])/g, (match) => {
return match.length > 2
? String.fromCodePoint(Number.parseInt(match.slice(1).trim(), 16))
: match[1]
if (match.length <= 2) {
return match[1]
}
let codePoint = Number.parseInt(match.slice(1).trim(), 16)
if (
// Invalid codepoint: https://infra.spec.whatwg.org/#code-point
codePoint === 0x0000 ||
codePoint > 0x10ffff ||
// Is surrogate: https://infra.spec.whatwg.org/#leading-surrogate
// - A leading surrogate is a code point that is in the range U+D800 to U+DBFF, inclusive.
// - A trailing surrogate is a code point that is in the range U+DC00 to U+DFFF, inclusive.
(codePoint >= 0xd800 && codePoint <= 0xdfff)
) {
return '\uFFFD'
}
return String.fromCodePoint(codePoint)
})
}

View file

@ -313,7 +313,11 @@ test('placeholder-shown', async () => {
test('autofill', async () => {
expect(await run(['autofill:flex', 'group-autofill:flex', 'peer-autofill:flex']))
.toMatchInlineSnapshot(`
".group-autofill\\:flex:is(:where(.group):autofill *), .peer-autofill\\:flex:is(:where(.peer):autofill ~ *), .autofill\\:flex:autofill {
".group-autofill\\:flex:is(:where(.group):autofill *), .peer-autofill\\:flex:is(:where(.peer):autofill ~ *) {
display: flex;
}
.autofill\\:flex:autofill {
display: flex;
}"
`)
@ -1475,7 +1479,15 @@ test('not', async () => {
],
),
).toMatchInlineSnapshot(`
".not-first\\:flex:not(:first-child), .not-last\\:flex:not(:last-child), .not-only\\:flex:not(:only-child), .not-odd\\:flex:not(:nth-child(odd)), .not-even\\:flex:not(:nth-child(2n)), .not-first-of-type\\:flex:not(:first-of-type), .not-last-of-type\\:flex:not(:last-of-type), .not-only-of-type\\:flex:not(:only-of-type), .not-visited\\:flex:not(:visited), .not-target\\:flex:not(:target), .not-open\\:flex:not(:is([open], :popover-open, :open)), .not-default\\:flex:not(:default), .not-checked\\:flex:not(:checked), .not-indeterminate\\:flex:not(:indeterminate), .not-placeholder-shown\\:flex:not(:placeholder-shown), .not-autofill\\:flex:not(:autofill), .not-optional\\:flex:not(:optional), .not-required\\:flex:not(:required), .not-valid\\:flex:not(:valid), .not-invalid\\:flex:not(:invalid), .not-in-range\\:flex:not(:in-range), .not-out-of-range\\:flex:not(:out-of-range), .not-read-only\\:flex:not(:read-only), .not-empty\\:flex:not(:empty), .not-focus-within\\:flex:not(:focus-within), .not-hover\\:flex:not(:hover) {
".not-first\\:flex:not(:first-child), .not-last\\:flex:not(:last-child), .not-only\\:flex:not(:only-child), .not-odd\\:flex:not(:nth-child(odd)), .not-even\\:flex:not(:nth-child(2n)), .not-first-of-type\\:flex:not(:first-of-type), .not-last-of-type\\:flex:not(:last-of-type), .not-only-of-type\\:flex:not(:only-of-type), .not-visited\\:flex:not(:visited), .not-target\\:flex:not(:target), .not-open\\:flex:not(:is([open], :popover-open, :open)), .not-default\\:flex:not(:default), .not-checked\\:flex:not(:checked), .not-indeterminate\\:flex:not(:indeterminate), .not-placeholder-shown\\:flex:not(:placeholder-shown) {
display: flex;
}
.not-autofill\\:flex:not(:autofill) {
display: flex;
}
.not-optional\\:flex:not(:optional), .not-required\\:flex:not(:required), .not-valid\\:flex:not(:valid), .not-invalid\\:flex:not(:invalid), .not-in-range\\:flex:not(:in-range), .not-out-of-range\\:flex:not(:out-of-range), .not-read-only\\:flex:not(:read-only), .not-empty\\:flex:not(:empty), .not-focus-within\\:flex:not(:focus-within), .not-hover\\:flex:not(:hover) {
display: flex;
}
@ -2383,7 +2395,15 @@ test('variant order', async () => {
display: flex;
}
.first\\:flex:first-child, .last\\:flex:last-child, .only\\:flex:only-child, .odd\\:flex:nth-child(odd), .even\\:flex:nth-child(2n), .first-of-type\\:flex:first-of-type, .last-of-type\\:flex:last-of-type, .only-of-type\\:flex:only-of-type, .visited\\:flex:visited, .target\\:flex:target, .open\\:flex:is([open], :popover-open, :open), .default\\:flex:default, .checked\\:flex:checked, .indeterminate\\:flex:indeterminate, .placeholder-shown\\:flex:placeholder-shown, .autofill\\:flex:autofill, .optional\\:flex:optional, .required\\:flex:required, .valid\\:flex:valid, .invalid\\:flex:invalid, .in-range\\:flex:in-range, .out-of-range\\:flex:out-of-range, .read-only\\:flex:read-only, .empty\\:flex:empty, .focus-within\\:flex:focus-within {
.first\\:flex:first-child, .last\\:flex:last-child, .only\\:flex:only-child, .odd\\:flex:nth-child(odd), .even\\:flex:nth-child(2n), .first-of-type\\:flex:first-of-type, .last-of-type\\:flex:last-of-type, .only-of-type\\:flex:only-of-type, .visited\\:flex:visited, .target\\:flex:target, .open\\:flex:is([open], :popover-open, :open), .default\\:flex:default, .checked\\:flex:checked, .indeterminate\\:flex:indeterminate, .placeholder-shown\\:flex:placeholder-shown {
display: flex;
}
.autofill\\:flex:autofill {
display: flex;
}
.optional\\:flex:optional, .required\\:flex:required, .valid\\:flex:valid, .invalid\\:flex:invalid, .in-range\\:flex:in-range, .out-of-range\\:flex:out-of-range, .read-only\\:flex:read-only, .empty\\:flex:empty, .focus-within\\:flex:focus-within {
display: flex;
}

View file

@ -1,10 +1,9 @@
diff --git a/index.js b/index.js
index 8afb2b1126dcc687b7ff9b631589da252c1f9c22..398af238a439912d150b3573367873d2a9a311e3 100644
index 8afb2b1126dcc687b7ff9b631589da252c1f9c22..f28fd092644ccfb8aff2618b966cb5219a181a02 100644
--- a/index.js
+++ b/index.js
@@ -1,41 +1,34 @@
-const {createWrapper} = require('./wrapper');
+const { createWrapper } = require('./wrapper')
@@ -1,40 +1,32 @@
const {createWrapper} = require('./wrapper');
-let name = `@parcel/watcher-${process.platform}-${process.arch}`;
-if (process.platform === 'linux') {
@ -15,6 +14,8 @@ index 8afb2b1126dcc687b7ff9b631589da252c1f9c22..398af238a439912d150b3573367873d2
- name += '-glibc';
- }
-}
+// We need to take the `process.env.PLATFORM_LIBC` into account, and we also
+// need static analysis, so we can't make use of the `parts.push(…)` approach.
+function loadPackage() {
+ if (process.platform === 'linux') {
+ if (process.env.PLATFORM_LIBC === "musl") {
@ -22,7 +23,8 @@ index 8afb2b1126dcc687b7ff9b631589da252c1f9c22..398af238a439912d150b3573367873d2
+ } else if (process.env.PLATFORM_LIBC === "glibc") {
+ return require(`@parcel/watcher-${process.platform}-${process.arch}-glibc`)
+ } else {
+ let { MUSL, GLIBC, family, familySync } = require('detect-libc')
+ let { MUSL, GLIBC, familySync } = require('detect-libc')
+ let family = familySync()
-let binding;
-try {
@ -41,10 +43,6 @@ index 8afb2b1126dcc687b7ff9b631589da252c1f9c22..398af238a439912d150b3573367873d2
- }
- }
-}
+ // Bun polyfills `detect-libc` in compiled binaries. We rely on
+ // detect-libc@1.0.3 but the polyfilled version is 2.x. In detect-libc@2x
+ // there is a `familySync` function that we can use instead.
+ if (typeof familySync === 'function') family = familySync()
-function handleError(err) {
- if (err?.code !== 'MODULE_NOT_FOUND') {
@ -63,12 +61,7 @@ index 8afb2b1126dcc687b7ff9b631589da252c1f9c22..398af238a439912d150b3573367873d2
}
-const wrapper = createWrapper(binding);
-exports.writeSnapshot = wrapper.writeSnapshot;
-exports.getEventsSince = wrapper.getEventsSince;
-exports.subscribe = wrapper.subscribe;
-exports.unsubscribe = wrapper.unsubscribe;
+const wrapper = createWrapper(loadPackage())
+exports.writeSnapshot = wrapper.writeSnapshot
+exports.getEventsSince = wrapper.getEventsSince
+exports.subscribe = wrapper.subscribe
+exports.unsubscribe = wrapper.unsubscribe
+const wrapper = createWrapper(loadPackage());
exports.writeSnapshot = wrapper.writeSnapshot;
exports.getEventsSince = wrapper.getEventsSince;
exports.subscribe = wrapper.subscribe;

View file

@ -1,8 +1,8 @@
diff --git a/node/index.js b/node/index.js
index 011d04b450687083e7a0ec9023c41ee79104831c..ef26db49d98c60b5b31f3601f6ad045f0b0e8f4f 100644
index 6fe25aef457aa5448f9ee6204da1e253f5534800..742be0229b94450b631b6e069ffc372d2b35df47 100644
--- a/node/index.js
+++ b/node/index.js
@@ -1,28 +1,41 @@
@@ -1,25 +1,40 @@
-let parts = [process.platform, process.arch];
-if (process.platform === 'linux') {
- const { MUSL, familySync } = require('detect-libc');
@ -11,12 +11,8 @@ index 011d04b450687083e7a0ec9023c41ee79104831c..ef26db49d98c60b5b31f3601f6ad045f
- parts.push('musl');
- } else if (process.arch === 'arm') {
- parts.push('gnueabihf');
- } else {
- parts.push('gnu');
- }
-} else if (process.platform === 'win32') {
- parts.push('msvc');
-}
+// We need to take the `process.env.PLATFORM_LIBC` into account, and we also
+// need static analysis, so we can't make use of the `parts.push(…)` approach.
+function loadPackage() {
+ if (process.platform === 'linux') {
+ if (process.env.PLATFORM_LIBC === 'musl') {
@ -28,19 +24,8 @@ index 011d04b450687083e7a0ec9023c41ee79104831c..ef26db49d98c60b5b31f3601f6ad045f
+ return require(`lightningcss-${process.platform}-${process.arch}-gnu`)
+ }
+ } else {
+ let { MUSL, GLIBC, family, familySync } = require('detect-libc')
-if (process.env.CSS_TRANSFORMER_WASM) {
- module.exports = require(`../pkg`);
-} else {
- try {
- module.exports = require(`lightningcss-${parts.join('-')}`);
- } catch (err) {
- module.exports = require(`../lightningcss.${parts.join('-')}.node`);
+ // Bun polyfills `detect-libc` in compiled binaries. We rely on
+ // detect-libc@1.0.3 but the polyfilled version is 2.x. In detect-libc@2x
+ // there is a `familySync` function that we can use instead.
+ if (typeof familySync === 'function') family = familySync()
+ let { MUSL, GLIBC, familySync } = require('detect-libc')
+ let family = familySync()
+
+ if (family === MUSL) {
+ return require(`lightningcss-${process.platform}-${process.arch}-musl`)
@ -56,15 +41,22 @@ index 011d04b450687083e7a0ec9023c41ee79104831c..ef26db49d98c60b5b31f3601f6ad045f
+ }
+ } else if (process.platform === 'win32') {
+ return require(`lightningcss-${process.platform}-${process.arch}-msvc`)
+ } else {
} else {
- parts.push('gnu');
+ return require(`lightningcss-${process.platform}-${process.arch}`)
}
-} else if (process.platform === 'win32') {
- parts.push('msvc');
}
-module.exports.browserslistToTargets = require('./browserslistToTargets');
-module.exports.composeVisitors = require('./composeVisitors');
-module.exports.Features = require('./flags').Features;
+module.exports = loadPackage()
+module.exports.browserslistToTargets = require('./browserslistToTargets')
+module.exports.composeVisitors = require('./composeVisitors')
+module.exports.Features = require('./flags').Features
-let native;
-try {
- native = require(`lightningcss-${parts.join('-')}`);
-} catch (err) {
- native = require(`../lightningcss.${parts.join('-')}.node`);
-}
+let native = loadPackage();
module.exports.transform = wrap(native.transform);
module.exports.transformStyleAttribute = wrap(native.transformStyleAttribute);
module.exports.bundle = wrap(native.bundle);

215
pnpm-lock.yaml generated
View file

@ -10,29 +10,29 @@ catalogs:
specifier: ^20.19.0
version: 20.19.1
lightningcss:
specifier: 1.31.1
version: 1.31.1
specifier: 1.32.0
version: 1.32.0
lightningcss-darwin-arm64:
specifier: 1.31.1
version: 1.31.1
specifier: 1.32.0
version: 1.32.0
lightningcss-darwin-x64:
specifier: 1.31.1
version: 1.31.1
specifier: 1.32.0
version: 1.32.0
lightningcss-linux-arm64-gnu:
specifier: 1.31.1
version: 1.31.1
specifier: 1.32.0
version: 1.32.0
lightningcss-linux-arm64-musl:
specifier: 1.31.1
version: 1.31.1
specifier: 1.32.0
version: 1.32.0
lightningcss-linux-x64-gnu:
specifier: 1.31.1
version: 1.31.1
specifier: 1.32.0
version: 1.32.0
lightningcss-linux-x64-musl:
specifier: 1.31.1
version: 1.31.1
specifier: 1.32.0
version: 1.32.0
lightningcss-win32-x64-msvc:
specifier: 1.31.1
version: 1.31.1
specifier: 1.32.0
version: 1.32.0
prettier:
specifier: 3.8.1
version: 3.8.1
@ -45,11 +45,11 @@ catalogs:
patchedDependencies:
'@parcel/watcher@2.5.1':
hash: zs2vvlrje3h42xp5ed2v44fep4
hash: p6xahr7zs4c5qsorj3exeppxhm
path: patches/@parcel__watcher@2.5.1.patch
lightningcss@1.31.1:
hash: tzyxy3asfxcqc7ihrooumyi5fm
path: patches/lightningcss@1.31.1.patch
lightningcss@1.32.0:
hash: mz3chiqe2jbihxa25xumd4ogum
path: patches/lightningcss@1.32.0.patch
importers:
@ -87,7 +87,7 @@ importers:
version: 5.5.4
vitest:
specifier: ^4.0.18
version: 4.0.18(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.31.6)(tsx@4.19.1)(yaml@2.6.0)
version: 4.0.18(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.32.0(patch_hash=mz3chiqe2jbihxa25xumd4ogum))(terser@5.31.6)(tsx@4.19.1)(yaml@2.6.0)
crates/node:
optionalDependencies:
@ -211,7 +211,7 @@ importers:
dependencies:
'@parcel/watcher':
specifier: ^2.5.1
version: 2.5.1(patch_hash=zs2vvlrje3h42xp5ed2v44fep4)
version: 2.5.1(patch_hash=p6xahr7zs4c5qsorj3exeppxhm)
'@tailwindcss/node':
specifier: workspace:*
version: link:../@tailwindcss-node
@ -244,7 +244,7 @@ importers:
version: 2.6.1
lightningcss:
specifier: 'catalog:'
version: 1.31.1(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm)
version: 1.32.0(patch_hash=mz3chiqe2jbihxa25xumd4ogum)
magic-string:
specifier: ^0.30.21
version: 0.30.21
@ -342,25 +342,25 @@ importers:
version: 1.3.9
lightningcss-darwin-arm64:
specifier: 'catalog:'
version: 1.31.1
version: 1.32.0
lightningcss-darwin-x64:
specifier: 'catalog:'
version: 1.31.1
version: 1.32.0
lightningcss-linux-arm64-gnu:
specifier: 'catalog:'
version: 1.31.1
version: 1.32.0
lightningcss-linux-arm64-musl:
specifier: 'catalog:'
version: 1.31.1
version: 1.32.0
lightningcss-linux-x64-gnu:
specifier: 'catalog:'
version: 1.31.1
version: 1.32.0
lightningcss-linux-x64-musl:
specifier: 'catalog:'
version: 1.31.1
version: 1.32.0
lightningcss-win32-x64-msvc:
specifier: 'catalog:'
version: 1.31.1
version: 1.32.0
packages/@tailwindcss-upgrade:
dependencies:
@ -482,7 +482,7 @@ importers:
version: 1.7.1
lightningcss:
specifier: 'catalog:'
version: 1.31.1(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm)
version: 1.32.0(patch_hash=mz3chiqe2jbihxa25xumd4ogum)
magic-string:
specifier: ^0.30.21
version: 0.30.21
@ -3813,135 +3813,65 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
lightningcss-android-arm64@1.31.1:
resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [android]
lightningcss-android-arm64@1.32.0:
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [android]
lightningcss-darwin-arm64@1.31.1:
resolution: {integrity: sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==}
engines: {node: '>= 12.0.0'}
os: [darwin]
lightningcss-darwin-arm64@1.32.0:
resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [darwin]
lightningcss-darwin-x64@1.31.1:
resolution: {integrity: sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==}
engines: {node: '>= 12.0.0'}
os: [darwin]
lightningcss-darwin-x64@1.32.0:
resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [darwin]
lightningcss-freebsd-x64@1.31.1:
resolution: {integrity: sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [freebsd]
lightningcss-freebsd-x64@1.32.0:
resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [freebsd]
lightningcss-linux-arm-gnueabihf@1.31.1:
resolution: {integrity: sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==}
engines: {node: '>= 12.0.0'}
cpu: [arm]
os: [linux]
lightningcss-linux-arm-gnueabihf@1.32.0:
resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
engines: {node: '>= 12.0.0'}
cpu: [arm]
os: [linux]
lightningcss-linux-arm64-gnu@1.31.1:
resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==}
engines: {node: '>= 12.0.0'}
os: [linux]
lightningcss-linux-arm64-gnu@1.32.0:
resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
lightningcss-linux-arm64-musl@1.31.1:
resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==}
engines: {node: '>= 12.0.0'}
os: [linux]
lightningcss-linux-arm64-musl@1.32.0:
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
lightningcss-linux-x64-gnu@1.31.1:
resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==}
engines: {node: '>= 12.0.0'}
os: [linux]
lightningcss-linux-x64-gnu@1.32.0:
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
lightningcss-linux-x64-musl@1.31.1:
resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==}
engines: {node: '>= 12.0.0'}
os: [linux]
lightningcss-linux-x64-musl@1.32.0:
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
lightningcss-win32-arm64-msvc@1.31.1:
resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [win32]
lightningcss-win32-arm64-msvc@1.32.0:
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [win32]
lightningcss-win32-x64-msvc@1.31.1:
resolution: {integrity: sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==}
engines: {node: '>= 12.0.0'}
os: [win32]
lightningcss-win32-x64-msvc@1.32.0:
resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [win32]
lightningcss@1.31.1:
resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==}
engines: {node: '>= 12.0.0'}
lightningcss@1.32.0:
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
engines: {node: '>= 12.0.0'}
@ -6419,7 +6349,7 @@ snapshots:
'@parcel/watcher-win32-ia32': 2.5.0
'@parcel/watcher-win32-x64': 2.5.0
'@parcel/watcher@2.5.1(patch_hash=zs2vvlrje3h42xp5ed2v44fep4)':
'@parcel/watcher@2.5.1(patch_hash=p6xahr7zs4c5qsorj3exeppxhm)':
dependencies:
detect-libc: 1.0.3
is-glob: 4.0.3
@ -6766,13 +6696,13 @@ snapshots:
chai: 6.2.1
tinyrainbow: 3.0.3
'@vitest/mocker@4.0.18(vite@7.0.0(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.31.6)(tsx@4.19.1)(yaml@2.6.0))':
'@vitest/mocker@4.0.18(vite@7.0.0(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.32.0(patch_hash=mz3chiqe2jbihxa25xumd4ogum))(terser@5.31.6)(tsx@4.19.1)(yaml@2.6.0))':
dependencies:
'@vitest/spy': 4.0.18
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 7.0.0(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.31.6)(tsx@4.19.1)(yaml@2.6.0)
vite: 7.0.0(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.32.0(patch_hash=mz3chiqe2jbihxa25xumd4ogum))(terser@5.31.6)(tsx@4.19.1)(yaml@2.6.0)
'@vitest/pretty-format@4.0.18':
dependencies:
@ -8242,82 +8172,33 @@ snapshots:
prelude-ls: 1.2.1
type-check: 0.4.0
lightningcss-android-arm64@1.31.1:
optional: true
lightningcss-android-arm64@1.32.0:
optional: true
lightningcss-darwin-arm64@1.31.1: {}
lightningcss-darwin-arm64@1.32.0: {}
lightningcss-darwin-arm64@1.32.0:
optional: true
lightningcss-darwin-x64@1.31.1: {}
lightningcss-darwin-x64@1.32.0:
optional: true
lightningcss-freebsd-x64@1.31.1:
optional: true
lightningcss-darwin-x64@1.32.0: {}
lightningcss-freebsd-x64@1.32.0:
optional: true
lightningcss-linux-arm-gnueabihf@1.31.1:
optional: true
lightningcss-linux-arm-gnueabihf@1.32.0:
optional: true
lightningcss-linux-arm64-gnu@1.31.1: {}
lightningcss-linux-arm64-gnu@1.32.0: {}
lightningcss-linux-arm64-gnu@1.32.0:
optional: true
lightningcss-linux-arm64-musl@1.32.0: {}
lightningcss-linux-arm64-musl@1.31.1: {}
lightningcss-linux-x64-gnu@1.32.0: {}
lightningcss-linux-arm64-musl@1.32.0:
optional: true
lightningcss-linux-x64-gnu@1.31.1: {}
lightningcss-linux-x64-gnu@1.32.0:
optional: true
lightningcss-linux-x64-musl@1.31.1: {}
lightningcss-linux-x64-musl@1.32.0:
optional: true
lightningcss-win32-arm64-msvc@1.31.1:
optional: true
lightningcss-linux-x64-musl@1.32.0: {}
lightningcss-win32-arm64-msvc@1.32.0:
optional: true
lightningcss-win32-x64-msvc@1.31.1: {}
lightningcss-win32-x64-msvc@1.32.0: {}
lightningcss-win32-x64-msvc@1.32.0:
optional: true
lightningcss@1.31.1(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm):
dependencies:
detect-libc: 2.1.2
optionalDependencies:
lightningcss-android-arm64: 1.31.1
lightningcss-darwin-arm64: 1.31.1
lightningcss-darwin-x64: 1.31.1
lightningcss-freebsd-x64: 1.31.1
lightningcss-linux-arm-gnueabihf: 1.31.1
lightningcss-linux-arm64-gnu: 1.31.1
lightningcss-linux-arm64-musl: 1.31.1
lightningcss-linux-x64-gnu: 1.31.1
lightningcss-linux-x64-musl: 1.31.1
lightningcss-win32-arm64-msvc: 1.31.1
lightningcss-win32-x64-msvc: 1.31.1
lightningcss@1.32.0:
lightningcss@1.32.0(patch_hash=mz3chiqe2jbihxa25xumd4ogum):
dependencies:
detect-libc: 2.1.2
optionalDependencies:
@ -9408,7 +9289,7 @@ snapshots:
util-deprecate@1.0.2: {}
vite@7.0.0(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.31.6)(tsx@4.19.1)(yaml@2.6.0):
vite@7.0.0(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.32.0(patch_hash=mz3chiqe2jbihxa25xumd4ogum))(terser@5.31.6)(tsx@4.19.1)(yaml@2.6.0):
dependencies:
esbuild: 0.25.0
fdir: 6.5.0(picomatch@4.0.3)
@ -9420,7 +9301,7 @@ snapshots:
'@types/node': 20.19.1
fsevents: 2.3.3
jiti: 2.6.1
lightningcss: 1.32.0
lightningcss: 1.32.0(patch_hash=mz3chiqe2jbihxa25xumd4ogum)
terser: 5.31.6
tsx: 4.19.1
yaml: 2.6.0
@ -9428,7 +9309,7 @@ snapshots:
vite@8.0.0(@types/node@20.19.1)(esbuild@0.27.0)(jiti@2.6.1)(terser@5.31.6)(tsx@4.19.1)(yaml@2.6.0):
dependencies:
'@oxc-project/runtime': 0.115.0
lightningcss: 1.32.0
lightningcss: 1.32.0(patch_hash=mz3chiqe2jbihxa25xumd4ogum)
picomatch: 4.0.3
postcss: 8.5.8
rolldown: 1.0.0-rc.9
@ -9442,10 +9323,10 @@ snapshots:
tsx: 4.19.1
yaml: 2.6.0
vitest@4.0.18(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.31.6)(tsx@4.19.1)(yaml@2.6.0):
vitest@4.0.18(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.32.0(patch_hash=mz3chiqe2jbihxa25xumd4ogum))(terser@5.31.6)(tsx@4.19.1)(yaml@2.6.0):
dependencies:
'@vitest/expect': 4.0.18
'@vitest/mocker': 4.0.18(vite@7.0.0(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.31.6)(tsx@4.19.1)(yaml@2.6.0))
'@vitest/mocker': 4.0.18(vite@7.0.0(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.32.0(patch_hash=mz3chiqe2jbihxa25xumd4ogum))(terser@5.31.6)(tsx@4.19.1)(yaml@2.6.0))
'@vitest/pretty-format': 4.0.18
'@vitest/runner': 4.0.18
'@vitest/snapshot': 4.0.18
@ -9462,7 +9343,7 @@ snapshots:
tinyexec: 1.0.2
tinyglobby: 0.2.15
tinyrainbow: 3.0.3
vite: 7.0.0(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.31.6)(tsx@4.19.1)(yaml@2.6.0)
vite: 7.0.0(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.32.0(patch_hash=mz3chiqe2jbihxa25xumd4ogum))(terser@5.31.6)(tsx@4.19.1)(yaml@2.6.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 20.19.1

View file

@ -10,11 +10,11 @@ catalog:
prettier: 3.8.1
vite: ^8.0.0
webpack: ^5
lightningcss: 1.31.1
lightningcss-darwin-arm64: 1.31.1
lightningcss-darwin-x64: 1.31.1
lightningcss-linux-arm64-gnu: 1.31.1
lightningcss-linux-arm64-musl: 1.31.1
lightningcss-linux-x64-gnu: 1.31.1
lightningcss-linux-x64-musl: 1.31.1
lightningcss-win32-x64-msvc: 1.31.1
lightningcss: 1.32.0
lightningcss-darwin-arm64: 1.32.0
lightningcss-darwin-x64: 1.32.0
lightningcss-linux-arm64-gnu: 1.32.0
lightningcss-linux-arm64-musl: 1.32.0
lightningcss-linux-x64-gnu: 1.32.0
lightningcss-linux-x64-musl: 1.32.0
lightningcss-win32-x64-msvc: 1.32.0