From 511fc89179831c1ef5f7890faee4c9a4f8ac6cbb Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Wed, 14 May 2025 11:20:18 +0200 Subject: [PATCH 1/2] Fix `-rotate-*` with arbitary values --- CHANGELOG.md | 1 + packages/tailwindcss/src/utilities.test.ts | 299 +++++++++++++-------- packages/tailwindcss/src/utilities.ts | 2 +- 3 files changed, 189 insertions(+), 113 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40c85b5c38d9..640bfaef66c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ensure that media queries within `::before` and `::after` pseudo selectors create valid CSS rules when building a production build ([#17979](https://github.com/tailwindlabs/tailwindcss/pull/17979)) - `lightningcss` now statically links Visual Studio redistributables ([#17979](https://github.com/tailwindlabs/tailwindcss/pull/17979)) - Ensure that running the Standalone build does not leave temporary files behind ([#17981](https://github.com/tailwindlabs/tailwindcss/pull/17981)) +- Fix `-rotate-*` utilities with arbitrary values ([#18014](https://github.com/tailwindlabs/tailwindcss/pull/18014)) ## [4.1.6] - 2025-05-09 diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index 4c2a7799b3e7..79934c8c044d 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -4453,24 +4453,45 @@ test('translate-3d', async () => { }) test('rotate', async () => { - expect(await run(['rotate-45', '-rotate-45', 'rotate-[123deg]', 'rotate-[0.3_0.7_1_45deg]'])) - .toMatchInlineSnapshot(` - ".-rotate-45 { - rotate: -45deg; - } + expect( + await run([ + 'rotate-45', + '-rotate-45', + 'rotate-[123deg]', + 'rotate-[0.3_0.7_1_45deg]', + 'rotate-(--var)', + '-rotate-[123deg]', + '-rotate-(--var)', + ]), + ).toMatchInlineSnapshot(` + ".-rotate-\\(--var\\) { + rotate: calc(var(--var) * -1); + } - .rotate-45 { - rotate: 45deg; - } + .-rotate-45 { + rotate: -45deg; + } - .rotate-\\[0\\.3_0\\.7_1_45deg\\] { - rotate: .3 .7 1 45deg; - } + .-rotate-\\[123deg\\] { + rotate: -123deg; + } - .rotate-\\[123deg\\] { - rotate: 123deg; - }" - `) + .rotate-\\(--var\\) { + rotate: var(--var); + } + + .rotate-45 { + rotate: 45deg; + } + + .rotate-\\[0\\.3_0\\.7_1_45deg\\] { + rotate: .3 .7 1 45deg; + } + + .rotate-\\[123deg\\] { + rotate: 123deg; + }" + `) expect( await run([ 'rotate', @@ -4486,7 +4507,15 @@ test('rotate', async () => { }) test('rotate-x', async () => { - expect(await run(['rotate-x-45', '-rotate-x-45', 'rotate-x-[123deg]'])).toMatchInlineSnapshot(` + expect( + await run([ + 'rotate-x-45', + '-rotate-x-45', + 'rotate-x-[123deg]', + 'rotate-x-(--var)', + '-rotate-x-(--var)', + ]), + ).toMatchInlineSnapshot(` "@layer properties { @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) { *, :before, :after, ::backdrop { @@ -4499,11 +4528,21 @@ 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, ); + } + .-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, ); } + .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, ); + } + .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, ); @@ -4553,65 +4592,83 @@ test('rotate-x', async () => { }) test('rotate-y', async () => { - expect(await run(['rotate-y-45', '-rotate-y-45', 'rotate-y-[123deg]', '-rotate-y-[123deg]'])) - .toMatchInlineSnapshot(` - "@layer properties { - @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) { - *, :before, :after, ::backdrop { - --tw-rotate-x: initial; - --tw-rotate-y: initial; - --tw-rotate-z: initial; - --tw-skew-x: initial; - --tw-skew-y: initial; - } + expect( + await run([ + 'rotate-y-45', + 'rotate-y-[123deg]', + 'rotate-y-(--var)', + '-rotate-y-45', + '-rotate-y-[123deg]', + '-rotate-y-(--var)', + ]), + ).toMatchInlineSnapshot(` + "@layer properties { + @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) { + *, :before, :after, ::backdrop { + --tw-rotate-x: initial; + --tw-rotate-y: initial; + --tw-rotate-z: initial; + --tw-skew-x: initial; + --tw-skew-y: initial; } } + } - .-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, ); - } + .-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, ); + } - .-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, ); - } + .-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, ); + } - .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, ); - } + .-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, ); + } - .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, ); - } + .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, ); + } - @property --tw-rotate-x { - syntax: "*"; - inherits: false - } + .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, ); + } - @property --tw-rotate-y { - syntax: "*"; - inherits: false - } + .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, ); + } - @property --tw-rotate-z { - syntax: "*"; - inherits: false - } + @property --tw-rotate-x { + syntax: "*"; + inherits: false + } - @property --tw-skew-x { - syntax: "*"; - inherits: false - } + @property --tw-rotate-y { + syntax: "*"; + inherits: false + } - @property --tw-skew-y { - syntax: "*"; - inherits: false - }" - `) + @property --tw-rotate-z { + syntax: "*"; + inherits: false + } + + @property --tw-skew-x { + syntax: "*"; + inherits: false + } + + @property --tw-skew-y { + syntax: "*"; + inherits: false + }" + `) expect( await run([ 'rotate-y', @@ -4626,65 +4683,83 @@ test('rotate-y', async () => { }) test('rotate-z', async () => { - expect(await run(['rotate-z-45', '-rotate-z-45', 'rotate-z-[123deg]', '-rotate-z-[123deg]'])) - .toMatchInlineSnapshot(` - "@layer properties { - @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) { - *, :before, :after, ::backdrop { - --tw-rotate-x: initial; - --tw-rotate-y: initial; - --tw-rotate-z: initial; - --tw-skew-x: initial; - --tw-skew-y: initial; - } + expect( + await run([ + 'rotate-z-45', + 'rotate-z-[123deg]', + 'rotate-z-(--var)', + '-rotate-z-45', + '-rotate-z-[123deg]', + '-rotate-z-(--var)', + ]), + ).toMatchInlineSnapshot(` + "@layer properties { + @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) { + *, :before, :after, ::backdrop { + --tw-rotate-x: initial; + --tw-rotate-y: initial; + --tw-rotate-z: initial; + --tw-skew-x: initial; + --tw-skew-y: initial; } } + } - .-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, ); - } + .-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, ); + } - .-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, ); - } + .-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, ); + } - .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, ); - } + .-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, ); + } - .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, ); - } + .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, ); + } - @property --tw-rotate-x { - syntax: "*"; - inherits: false - } + .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, ); + } - @property --tw-rotate-y { - syntax: "*"; - inherits: false - } + .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, ); + } - @property --tw-rotate-z { - syntax: "*"; - inherits: false - } + @property --tw-rotate-x { + syntax: "*"; + inherits: false + } - @property --tw-skew-x { - syntax: "*"; - inherits: false - } + @property --tw-rotate-y { + syntax: "*"; + inherits: false + } - @property --tw-skew-y { - syntax: "*"; - inherits: false - }" - `) + @property --tw-rotate-z { + syntax: "*"; + inherits: false + } + + @property --tw-skew-x { + syntax: "*"; + inherits: false + } + + @property --tw-skew-y { + syntax: "*"; + inherits: false + }" + `) expect( await run([ 'rotate-z', diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index d28a368a43f5..9a671a41d106 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -1388,7 +1388,7 @@ export function createUtilities(theme: Theme) { if (type === 'vector') { return [decl('rotate', `${value} var(--tw-rotate)`)] } else if (type !== 'angle') { - return [decl('rotate', value)] + return [decl('rotate', negative ? `calc(${value} * -1)` : value)] } } else { value = theme.resolve(candidate.value.value, ['--rotate']) From e1b30c7436935e0f3087fdd9033bef61376220e8 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Wed, 14 May 2025 13:25:15 +0200 Subject: [PATCH 2/2] Change casing of utilities with named values to kebab-case to match updated theme variables --- CHANGELOG.md | 1 + integrations/upgrade/js-config.test.ts | 10 ++-- .../migrate-camelcase-in-named-value.test.ts | 21 ++++++++ .../migrate-camelcase-in-named-value.ts | 53 +++++++++++++++++++ .../src/codemods/template/migrate.ts | 2 + 5 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 packages/@tailwindcss-upgrade/src/codemods/template/migrate-camelcase-in-named-value.test.ts create mode 100644 packages/@tailwindcss-upgrade/src/codemods/template/migrate-camelcase-in-named-value.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 640bfaef66c8..9242e6097168 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `lightningcss` now statically links Visual Studio redistributables ([#17979](https://github.com/tailwindlabs/tailwindcss/pull/17979)) - Ensure that running the Standalone build does not leave temporary files behind ([#17981](https://github.com/tailwindlabs/tailwindcss/pull/17981)) - Fix `-rotate-*` utilities with arbitrary values ([#18014](https://github.com/tailwindlabs/tailwindcss/pull/18014)) +- Upgrade: Change casing of utilities with named values to kebab-case to match updated theme variables ## [4.1.6] - 2025-05-09 diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts index d2240fbf03b6..08005bb45340 100644 --- a/integrations/upgrade/js-config.test.ts +++ b/integrations/upgrade/js-config.test.ts @@ -30,6 +30,7 @@ test( 400: '#f87171', 500: 'red', }, + superRed: '#ff0000', steel: 'rgb(70 130 180 / )', smoke: 'rgba(245, 245, 245, var(--smoke-alpha, ))', }, @@ -144,9 +145,10 @@ test( } `, 'src/index.html': html` -
+
`, 'node_modules/my-external-lib/src/template.html': html`
@@ -162,8 +164,9 @@ test( " --- src/index.html ---
+ class="[letter-spacing:var(--tracking-super-wide)] [line-height:var(--leading-super-loose)]" + >
+
--- src/input.css --- @import 'tailwindcss'; @@ -181,6 +184,7 @@ test( --color-red-500: #ef4444; --color-red-600: #dc2626; + --color-super-red: #ff0000; --color-steel: rgb(70 130 180); --color-smoke: rgba(245, 245, 245, var(--smoke-alpha, 1)); diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-camelcase-in-named-value.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-camelcase-in-named-value.test.ts new file mode 100644 index 000000000000..ca31ae249ab4 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-camelcase-in-named-value.test.ts @@ -0,0 +1,21 @@ +import { __unstable__loadDesignSystem } from '@tailwindcss/node' +import { expect, test, vi } from 'vitest' +import * as versions from '../../utils/version' +import { migrateCamelcaseInNamedValue } from './migrate-camelcase-in-named-value' +vi.spyOn(versions, 'isMajor').mockReturnValue(true) + +test.only.each([ + ['text-superRed', 'text-super-red'], + ['text-red/superOpaque', 'text-red/super-opaque'], + ['text-superRed/superOpaque', 'text-super-red/super-opaque'], + + ['hover:text-superRed', 'hover:text-super-red'], + ['hover:text-red/superOpaque', 'hover:text-red/super-opaque'], + ['hover:text-superRed/superOpaque', 'hover:text-super-red/super-opaque'], +])('%s => %s', async (candidate, result) => { + let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', { + base: __dirname, + }) + + expect(migrateCamelcaseInNamedValue(designSystem, {}, candidate)).toEqual(result) +}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-camelcase-in-named-value.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-camelcase-in-named-value.ts new file mode 100644 index 000000000000..fea5da42951d --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-camelcase-in-named-value.ts @@ -0,0 +1,53 @@ +import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' +import type { DesignSystem } from '../../../../tailwindcss/src/design-system' +import * as version from '../../utils/version' + +// Converts named values to use kebab-case. This is necessary because the +// upgrade tool also renames the theme values to kebab-case, so `text-superRed` +// will have it's theme value renamed to `--color-super-red` and thus the +// utility will be renamed to `text-super-red`. +export function migrateCamelcaseInNamedValue( + designSystem: DesignSystem, + _userConfig: Config | null, + rawCandidate: string, +): string { + if (!version.isMajor(3)) return rawCandidate + + for (let candidate of designSystem.parseCandidate(rawCandidate)) { + if (candidate.kind !== 'functional') continue + let clone = structuredClone(candidate) + let didChange = false + + if ( + candidate.value && + clone.value && + candidate.value.kind === 'named' && + clone.value.kind === 'named' && + candidate.value.value.match(/[A-Z]/) + ) { + clone.value.value = camelToKebab(candidate.value.value) + didChange = true + } + + if ( + candidate.modifier && + clone.modifier && + candidate.modifier.kind === 'named' && + clone.modifier.kind === 'named' && + candidate.modifier.value.match(/[A-Z]/) + ) { + clone.modifier.value = camelToKebab(candidate.modifier.value) + didChange = true + } + + if (didChange) { + return designSystem.printCandidate(clone) + } + } + + return rawCandidate +} + +function camelToKebab(str: string): string { + return str.replace(/([A-Z])/g, '-$1').toLowerCase() +} diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts index 029a2a97bd98..db92e0e37ebe 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts @@ -10,6 +10,7 @@ import { migrateArbitraryValueToBareValue } from './migrate-arbitrary-value-to-b import { migrateArbitraryVariants } from './migrate-arbitrary-variants' import { migrateAutomaticVarInjection } from './migrate-automatic-var-injection' import { migrateBgGradient } from './migrate-bg-gradient' +import { migrateCamelcaseInNamedValue } from './migrate-camelcase-in-named-value' import { migrateDropUnnecessaryDataTypes } from './migrate-drop-unnecessary-data-types' import { migrateEmptyArbitraryValues } from './migrate-handle-empty-arbitrary-values' import { migrateImportant } from './migrate-important' @@ -40,6 +41,7 @@ export const DEFAULT_MIGRATIONS: Migration[] = [ migrateImportant, migrateBgGradient, migrateSimpleLegacyClasses, + migrateCamelcaseInNamedValue, migrateLegacyClasses, migrateMaxWidthScreen, migrateThemeToVar,