From dadb096da21c63946ccb694d649835438cf16614 Mon Sep 17 00:00:00 2001 From: Kris Braun Date: Tue, 19 Mar 2024 12:52:32 -0400 Subject: [PATCH] 3D transform utilities (#13248) * 3D rotation utilities * Validate rotate values * Replace forEach with for loop * transform-style, transform-box, and backface-visibility utilities * Tests for transform utilities * 'perspective' utility * Fix tests * Remove unnecessary suggestion; move function comments * scale-z * Fix Intellisense test * perspective-origin * scale-3d * Only include the z component of scale if it's defined We want to avoid triggerring unnecessary 3D transformations. * Comment the reason for setting --tw-rotate * Test full bare rotate * Fix merge * Comment on rotate arbitrary value * perspective bare values Support `perspective-123` (but not `perspective-potato`) * scale-3d as a static modifier to scale Instead of scale-3d taking a separate scale, it modifies scale to apply in three dimensions. * Test that scale-x overrides scale * scale arbitrary values Support arbitrary value for scale (e.g. `scale-[1_2_3.5]`). * Specify rotation axis using a modifier Support single rotation angles in line with the [CSS `rotate` property](https://developer.mozilla.org/en-US/docs/Web/CSS/rotate). Using modifiers (e.g. `rotate-45/x`) makes it clearer that the axis of rotation is modified. Thanks @adamwathan for this suggestion. Composing angles is only supported in CSS via a pipeline of `transform` functions. I'll add arbitrary value support to `transform` next as an escape hatch for those cases that need more complex transformations. * Use property defaults for scale-3d * `transform` arbitrary values Support arbitrary values for `transform`. The `skew-x` and `skew-y` transforms are applied before any arbitrary transformations. * Add translate-z and translate-3d Both work the same way as scale-z and scale-3d. * Add translate-[xyz]-px * Comment on how skewX and skewY are applied * Remove unnecessary suggest * Simplify translate * Fix up comment on rotate syntax * Back to rotate-x and rotate-y rather than rotate modifiers * 3D transform test fixes --- .../src/__snapshots__/index.test.ts.snap | 5 + .../src/__snapshots__/index.test.ts.snap | 5 + .../__snapshots__/intellisense.test.ts.snap | 93 +++ packages/tailwindcss/src/index.test.ts | 6 + packages/tailwindcss/src/intellisense.test.ts | 2 + packages/tailwindcss/src/property-order.ts | 5 +- packages/tailwindcss/src/theme.ts | 2 + packages/tailwindcss/src/utilities.test.ts | 557 ++++++++++++++++-- packages/tailwindcss/src/utilities.ts | 308 ++++++---- .../tailwindcss/src/utils/infer-data-type.ts | 37 ++ packages/tailwindcss/theme.css | 7 + 11 files changed, 857 insertions(+), 170 deletions(-) diff --git a/packages/@tailwindcss-postcss/src/__snapshots__/index.test.ts.snap b/packages/@tailwindcss-postcss/src/__snapshots__/index.test.ts.snap index de2280a4e174..9d469389a437 100644 --- a/packages/@tailwindcss-postcss/src/__snapshots__/index.test.ts.snap +++ b/packages/@tailwindcss-postcss/src/__snapshots__/index.test.ts.snap @@ -393,6 +393,11 @@ exports[`\`@import 'tailwindcss'\` is replaced with the generated CSS 1`] = ` --line-height-8: 2rem; --line-height-9: 2.25rem; --line-height-10: 2.5rem; + --perspective-dramatic: 100px; + --perspective-near: 300px; + --perspective-normal: 500px; + --perspective-midrange: 800px; + --perspective-distant: 1200px; --transition-timing-function-linear: linear; --transition-timing-function-in: cubic-bezier(.4, 0, 1, 1); --transition-timing-function-out: cubic-bezier(0, 0, .2, 1); diff --git a/packages/tailwindcss/src/__snapshots__/index.test.ts.snap b/packages/tailwindcss/src/__snapshots__/index.test.ts.snap index d776eee9efe8..6a43dfeb0838 100644 --- a/packages/tailwindcss/src/__snapshots__/index.test.ts.snap +++ b/packages/tailwindcss/src/__snapshots__/index.test.ts.snap @@ -392,6 +392,11 @@ exports[`compiling CSS > \`@tailwind utilities\` is replaced by utilities using --line-height-8: 2rem; --line-height-9: 2.25rem; --line-height-10: 2.5rem; + --perspective-dramatic: 100px; + --perspective-near: 300px; + --perspective-normal: 500px; + --perspective-midrange: 800px; + --perspective-distant: 1200px; --transition-timing-function-linear: linear; --transition-timing-function-in: cubic-bezier(.4, 0, 1, 1); --transition-timing-function-out: cubic-bezier(0, 0, .2, 1); diff --git a/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap b/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap index a9f669162f88..40a795aec459 100644 --- a/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap +++ b/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap @@ -91,6 +91,24 @@ exports[`getClassList 1`] = ` "-rotate-45", "-rotate-6", "-rotate-90", + "-rotate-x-0", + "-rotate-x-1", + "-rotate-x-12", + "-rotate-x-180", + "-rotate-x-2", + "-rotate-x-3", + "-rotate-x-45", + "-rotate-x-6", + "-rotate-x-90", + "-rotate-y-0", + "-rotate-y-1", + "-rotate-y-12", + "-rotate-y-180", + "-rotate-y-2", + "-rotate-y-3", + "-rotate-y-45", + "-rotate-y-6", + "-rotate-y-90", "-scale-0", "-scale-100", "-scale-105", @@ -124,6 +142,17 @@ exports[`getClassList 1`] = ` "-scale-y-75", "-scale-y-90", "-scale-y-95", + "-scale-z-0", + "-scale-z-100", + "-scale-z-105", + "-scale-z-110", + "-scale-z-125", + "-scale-z-150", + "-scale-z-200", + "-scale-z-50", + "-scale-z-75", + "-scale-z-90", + "-scale-z-95", "-scroll-m-0.5", "-scroll-m-1", "-scroll-m-3", @@ -242,6 +271,10 @@ exports[`getClassList 1`] = ` "-translate-y-1", "-translate-y-3", "-translate-y-4", + "-translate-z-0.5", + "-translate-z-1", + "-translate-z-3", + "-translate-z-4", "-underline-offset-0", "-underline-offset-1", "-underline-offset-2", @@ -347,6 +380,8 @@ exports[`getClassList 1`] = ` "backdrop-sepia-0", "backdrop-sepia-100", "backdrop-sepia-50", + "backface-hidden", + "backface-visible", "basis-0.5", "basis-1", "basis-3", @@ -1180,6 +1215,18 @@ exports[`getClassList 1`] = ` "pe-1", "pe-3", "pe-4", + "perspective-dramatic", + "perspective-none", + "perspective-normal", + "perspective-origin-bottom", + "perspective-origin-bottom-left", + "perspective-origin-bottom-right", + "perspective-origin-center", + "perspective-origin-left", + "perspective-origin-right", + "perspective-origin-top", + "perspective-origin-top-left", + "perspective-origin-top-right", "pl-0.5", "pl-1", "pl-3", @@ -1266,6 +1313,24 @@ exports[`getClassList 1`] = ` "rotate-45", "rotate-6", "rotate-90", + "rotate-x-0", + "rotate-x-1", + "rotate-x-12", + "rotate-x-180", + "rotate-x-2", + "rotate-x-3", + "rotate-x-45", + "rotate-x-6", + "rotate-x-90", + "rotate-y-0", + "rotate-y-1", + "rotate-y-12", + "rotate-y-180", + "rotate-y-2", + "rotate-y-3", + "rotate-y-45", + "rotate-y-6", + "rotate-y-90", "rounded-b-full", "rounded-b-none", "rounded-bl-full", @@ -1350,6 +1415,7 @@ exports[`getClassList 1`] = ` "scale-125", "scale-150", "scale-200", + "scale-3d", "scale-50", "scale-75", "scale-90", @@ -1376,6 +1442,17 @@ exports[`getClassList 1`] = ` "scale-y-75", "scale-y-90", "scale-y-95", + "scale-z-0", + "scale-z-100", + "scale-z-105", + "scale-z-110", + "scale-z-125", + "scale-z-150", + "scale-z-200", + "scale-z-50", + "scale-z-75", + "scale-z-90", + "scale-z-95", "scroll-auto", "scroll-m-0.5", "scroll-m-1", @@ -1608,9 +1685,16 @@ exports[`getClassList 1`] = ` "touch-pan-y", "touch-pinch-zoom", "transform", + "transform-3d", + "transform-border", + "transform-content", "transform-cpu", + "transform-fill", + "transform-flat", "transform-gpu", "transform-none", + "transform-stroke", + "transform-view", "transition", "transition-all", "transition-colors", @@ -1621,6 +1705,7 @@ exports[`getClassList 1`] = ` "translate-0.5", "translate-1", "translate-3", + "translate-3d", "translate-4", "translate-full", "translate-x-0.5", @@ -1628,11 +1713,19 @@ exports[`getClassList 1`] = ` "translate-x-3", "translate-x-4", "translate-x-full", + "translate-x-px", "translate-y-0.5", "translate-y-1", "translate-y-3", "translate-y-4", "translate-y-full", + "translate-y-px", + "translate-z-0.5", + "translate-z-1", + "translate-z-3", + "translate-z-4", + "translate-z-full", + "translate-z-px", "truncate", "underline", "underline-offset-0", diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index 21fab8074946..eee2e4dcaeca 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -227,6 +227,12 @@ describe('@apply', () => { syntax: ""; inherits: false; initial-value: 0; + } + + @property --tw-translate-z { + syntax: ""; + inherits: false; + initial-value: 0; }" `) }) diff --git a/packages/tailwindcss/src/intellisense.test.ts b/packages/tailwindcss/src/intellisense.test.ts index 4128e336b880..d32d8e2f46d7 100644 --- a/packages/tailwindcss/src/intellisense.test.ts +++ b/packages/tailwindcss/src/intellisense.test.ts @@ -14,6 +14,8 @@ function loadDesignSystem() { theme.add('--breakpoint-sm', '640px') theme.add('--font-size-xs', '0.75rem') theme.add('--font-size-xs--line-height', '1rem') + theme.add('--perspective-dramatic', '100px') + theme.add('--perspective-normal', '500px') return buildDesignSystem(theme) } diff --git a/packages/tailwindcss/src/property-order.ts b/packages/tailwindcss/src/property-order.ts index b28fbbfa1ae1..e059cc07325a 100644 --- a/packages/tailwindcss/src/property-order.ts +++ b/packages/tailwindcss/src/property-order.ts @@ -74,9 +74,10 @@ export default [ // '--tw-rotate', '--tw-skew-x', '--tw-skew-y', + '--tw-scale-x', + '--tw-scale-y', + '--tw-scale-z', 'scale', - // '--tw-scale-x', - // '--tw-scale-y', 'transform', 'animation', diff --git a/packages/tailwindcss/src/theme.ts b/packages/tailwindcss/src/theme.ts index f7f491f303fe..33b04ca0d732 100644 --- a/packages/tailwindcss/src/theme.ts +++ b/packages/tailwindcss/src/theme.ts @@ -197,6 +197,8 @@ export type ThemeKey = | '--outline-offset' | '--padding' | '--placeholder-color' + | '--perspective' + | '--perspective-origin' | '--radius' | '--ring-color' | '--ring-offset-color' diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index 383da727dd60..2690761dda84 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -2364,6 +2364,69 @@ test('origin', () => { expect(run(['-origin-center', '-origin-[--value]'])).toEqual('') }) +test('perspective-origin', () => { + expect( + run([ + 'perspective-origin-center', + 'perspective-origin-top', + 'perspective-origin-top-right', + 'perspective-origin-right', + 'perspective-origin-bottom-right', + 'perspective-origin-bottom', + 'perspective-origin-bottom-left', + 'perspective-origin-left', + 'perspective-origin-top-left', + 'perspective-origin-[50px_100px]', + 'perspective-origin-[--value]', + ]), + ).toMatchInlineSnapshot(` + ".perspective-origin-\\[--value\\] { + perspective-origin: var(--value); + } + + .perspective-origin-\\[50px_100px\\] { + perspective-origin: 50px 100px; + } + + .perspective-origin-bottom { + perspective-origin: bottom; + } + + .perspective-origin-bottom-left { + perspective-origin: 0 100%; + } + + .perspective-origin-bottom-right { + perspective-origin: 100% 100%; + } + + .perspective-origin-center { + perspective-origin: center; + } + + .perspective-origin-left { + perspective-origin: 0; + } + + .perspective-origin-right { + perspective-origin: 100%; + } + + .perspective-origin-top { + perspective-origin: top; + } + + .perspective-origin-top-left { + perspective-origin: 0 0; + } + + .perspective-origin-top-right { + perspective-origin: 100% 0; + }" + `) + expect(run(['-perspective-origin-center', '-perspective-origin-[--value]'])).toEqual('') +}) + test('translate', () => { expect( run([ @@ -2377,30 +2440,35 @@ test('translate', () => { ".-translate-\\[--value\\] { --tw-translate-x: calc(var(--value) * -1); --tw-translate-y: calc(var(--value) * -1); + --tw-translate-z: calc(var(--value) * -1); translate: var(--tw-translate-x) var(--tw-translate-y); } .-translate-full { --tw-translate-x: -100%; --tw-translate-y: -100%; + --tw-translate-z: -100%; translate: var(--tw-translate-x) var(--tw-translate-y); } .translate-1\\/2 { --tw-translate-x: calc(1 / 2 * 100%); --tw-translate-y: calc(1 / 2 * 100%); + --tw-translate-z: calc(1 / 2 * 100%); translate: var(--tw-translate-x) var(--tw-translate-y); } .translate-\\[123px\\] { --tw-translate-x: 123px; --tw-translate-y: 123px; + --tw-translate-z: 123px; translate: var(--tw-translate-x) var(--tw-translate-y); } .translate-full { --tw-translate-x: 100%; --tw-translate-y: 100%; + --tw-translate-z: 100%; translate: var(--tw-translate-x) var(--tw-translate-y); } @@ -2414,79 +2482,179 @@ test('translate', () => { syntax: ""; inherits: false; initial-value: 0; + } + + @property --tw-translate-z { + syntax: ""; + inherits: false; + initial-value: 0; }" `) expect(run(['translate'])).toEqual('') }) test('translate-x', () => { - expect(run(['translate-x-full', '-translate-x-full', '-translate-x-[--value]'])) + expect(run(['translate-x-full', '-translate-x-full', 'translate-x-px', '-translate-x-[--value]'])) .toMatchInlineSnapshot(` - ".-translate-x-\\[--value\\] { - --tw-translate-x: calc(var(--value) * -1); - translate: var(--tw-translate-x) var(--tw-translate-y); - } + ".-translate-x-\\[--value\\] { + --tw-translate-x: calc(var(--value) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } - .-translate-x-full { - --tw-translate-x: -100%; - translate: var(--tw-translate-x) var(--tw-translate-y); - } + .-translate-x-full { + --tw-translate-x: -100%; + translate: var(--tw-translate-x) var(--tw-translate-y); + } - .translate-x-full { - --tw-translate-x: 100%; - translate: var(--tw-translate-x) var(--tw-translate-y); - } + .translate-x-full { + --tw-translate-x: 100%; + translate: var(--tw-translate-x) var(--tw-translate-y); + } - @property --tw-translate-x { - syntax: ""; - inherits: false; - initial-value: 0; - } + .translate-x-px { + --tw-translate-x: 1px; + translate: var(--tw-translate-x) var(--tw-translate-y); + } - @property --tw-translate-y { - syntax: ""; - inherits: false; - initial-value: 0; - }" - `) + @property --tw-translate-x { + syntax: ""; + inherits: false; + initial-value: 0; + } + + @property --tw-translate-y { + syntax: ""; + inherits: false; + initial-value: 0; + } + + @property --tw-translate-z { + syntax: ""; + inherits: false; + initial-value: 0; + }" + `) expect(run(['translate-x'])).toEqual('') }) test('translate-y', () => { - expect(run(['translate-y-full', '-translate-y-full', '-translate-y-[--value]'])) + expect(run(['translate-y-full', '-translate-y-full', 'translate-y-px', '-translate-y-[--value]'])) .toMatchInlineSnapshot(` - ".-translate-y-\\[--value\\] { - --tw-translate-y: calc(var(--value) * -1); - translate: var(--tw-translate-x) var(--tw-translate-y); - } + ".-translate-y-\\[--value\\] { + --tw-translate-y: calc(var(--value) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } - .-translate-y-full { - --tw-translate-y: -100%; - translate: var(--tw-translate-x) var(--tw-translate-y); - } + .-translate-y-full { + --tw-translate-y: -100%; + translate: var(--tw-translate-x) var(--tw-translate-y); + } - .translate-y-full { - --tw-translate-y: 100%; - translate: var(--tw-translate-x) var(--tw-translate-y); - } + .translate-y-full { + --tw-translate-y: 100%; + translate: var(--tw-translate-x) var(--tw-translate-y); + } - @property --tw-translate-x { - syntax: ""; - inherits: false; - initial-value: 0; - } + .translate-y-px { + --tw-translate-y: 1px; + translate: var(--tw-translate-x) var(--tw-translate-y); + } - @property --tw-translate-y { - syntax: ""; - inherits: false; - initial-value: 0; - }" - `) + @property --tw-translate-x { + syntax: ""; + inherits: false; + initial-value: 0; + } + + @property --tw-translate-y { + syntax: ""; + inherits: false; + initial-value: 0; + } + + @property --tw-translate-z { + syntax: ""; + inherits: false; + initial-value: 0; + }" + `) expect(run(['translate-y'])).toEqual('') }) +test('translate-z', () => { + expect(run(['translate-z-full', '-translate-z-full', 'translate-y-px', '-translate-z-[--value]'])) + .toMatchInlineSnapshot(` + ".-translate-z-\\[--value\\] { + --tw-translate-z: calc(var(--value) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y) var(--tw-translate-z); + } + + .-translate-z-full { + --tw-translate-z: -100%; + translate: var(--tw-translate-x) var(--tw-translate-y) var(--tw-translate-z); + } + + .translate-y-px { + --tw-translate-y: 1px; + translate: var(--tw-translate-x) var(--tw-translate-y); + } + + .translate-z-full { + --tw-translate-z: 100%; + translate: var(--tw-translate-x) var(--tw-translate-y) var(--tw-translate-z); + } + + @property --tw-translate-x { + syntax: ""; + inherits: false; + initial-value: 0; + } + + @property --tw-translate-y { + syntax: ""; + inherits: false; + initial-value: 0; + } + + @property --tw-translate-z { + syntax: ""; + inherits: false; + initial-value: 0; + }" + `) + expect(run(['translate-z'])).toEqual('') +}) + +test('translate-3d', () => { + expect(run(['translate-3d'])).toMatchInlineSnapshot(` + ".translate-3d { + translate: var(--tw-translate-x) var(--tw-translate-y) var(--tw-translate-z); + } + + @property --tw-translate-x { + syntax: ""; + inherits: false; + initial-value: 0; + } + + @property --tw-translate-y { + syntax: ""; + inherits: false; + initial-value: 0; + } + + @property --tw-translate-z { + syntax: ""; + inherits: false; + initial-value: 0; + }" + `) + expect(run(['-translate-3d'])).toEqual('') +}) + test('rotate', () => { - expect(run(['rotate-45', '-rotate-45', 'rotate-[123deg]'])).toMatchInlineSnapshot(` + expect(run(['rotate-45', '-rotate-45', 'rotate-[123deg]', 'rotate-[0.3_0.7_1_45deg]'])) + .toMatchInlineSnapshot(` ".-rotate-45 { rotate: -45deg; } @@ -2495,11 +2663,49 @@ test('rotate', () => { rotate: 45deg; } + .rotate-\\[0\\.3_0\\.7_1_45deg\\] { + rotate: .3 .7 1 45deg; + } + .rotate-\\[123deg\\] { rotate: 123deg; }" `) - expect(run(['rotate', 'rotate-unknown'])).toEqual('') + expect(run(['rotate', 'rotate-z', 'rotate-unknown'])).toEqual('') +}) + +test('rotate-x', () => { + expect(run(['rotate-x-45', '-rotate-x-45', 'rotate-x-[123deg]'])).toMatchInlineSnapshot(` + ".-rotate-x-45 { + rotate: x -45deg; + } + + .rotate-x-45 { + rotate: x 45deg; + } + + .rotate-x-\\[123deg\\] { + rotate: x 123deg; + }" + `) + expect(run(['rotate-x', '-rotate-x', 'rotate-x-potato'])).toEqual('') +}) + +test('rotate-y', () => { + expect(run(['rotate-y-45', '-rotate-y-45', 'rotate-y-[123deg]'])).toMatchInlineSnapshot(` + ".-rotate-y-45 { + rotate: y -45deg; + } + + .rotate-y-45 { + rotate: y 45deg; + } + + .rotate-y-\\[123deg\\] { + rotate: y 123deg; + }" + `) + expect(run(['rotate-y', '-rotate-y', 'rotate-y-potato'])).toEqual('') }) test('skew', () => { @@ -2602,23 +2808,27 @@ test('skew-y', () => { }) test('scale', () => { - expect(run(['scale-50', '-scale-50', 'scale-[123deg]'])).toMatchInlineSnapshot(` + expect(run(['scale-50', '-scale-50', 'scale-[2]', 'scale-[2_1.5_3]'])).toMatchInlineSnapshot(` ".-scale-50 { --tw-scale-x: calc(50% * -1); --tw-scale-y: calc(50% * -1); + --tw-scale-z: calc(50% * -1); scale: var(--tw-scale-x) var(--tw-scale-y); } .scale-50 { --tw-scale-x: 50%; --tw-scale-y: 50%; + --tw-scale-z: 50%; scale: var(--tw-scale-x) var(--tw-scale-y); } - .scale-\\[123deg\\] { - --tw-scale-x: 123deg; - --tw-scale-y: 123deg; - scale: var(--tw-scale-x) var(--tw-scale-y); + .scale-\\[2\\] { + scale: 2; + } + + .scale-\\[2_1\\.5_3\\] { + scale: 2 1.5 3; } @property --tw-scale-x { @@ -2631,13 +2841,46 @@ test('scale', () => { syntax: " | "; inherits: false; initial-value: 1; + } + + @property --tw-scale-z { + syntax: " | "; + inherits: false; + initial-value: 1; }" `) expect(run(['scale', 'scale-unknown'])).toEqual('') }) +test('scale-3d', () => { + expect(run(['scale-3d'])).toMatchInlineSnapshot(` + ".scale-3d { + scale: var(--tw-scale-x) var(--tw-scale-y) var(--tw-scale-z); + } + + @property --tw-scale-x { + syntax: " | "; + inherits: false; + initial-value: 1; + } + + @property --tw-scale-y { + syntax: " | "; + inherits: false; + initial-value: 1; + } + + @property --tw-scale-z { + syntax: " | "; + inherits: false; + initial-value: 1; + }" + `) + expect(run(['-scale-3d'])).toEqual('') +}) + test('scale-x', () => { - expect(run(['scale-x-50', '-scale-x-50', 'scale-x-[123deg]'])).toMatchInlineSnapshot(` + expect(run(['scale-x-50', '-scale-x-50', 'scale-x-[2]'])).toMatchInlineSnapshot(` ".-scale-x-50 { --tw-scale-x: calc(50% * -1); scale: var(--tw-scale-x) var(--tw-scale-y); @@ -2648,8 +2891,8 @@ test('scale-x', () => { scale: var(--tw-scale-x) var(--tw-scale-y); } - .scale-x-\\[123deg\\] { - --tw-scale-x: 123deg; + .scale-x-\\[2\\] { + --tw-scale-x: 2; scale: var(--tw-scale-x) var(--tw-scale-y); } @@ -2663,13 +2906,50 @@ test('scale-x', () => { syntax: " | "; inherits: false; initial-value: 1; + } + + @property --tw-scale-z { + syntax: " | "; + inherits: false; + initial-value: 1; + }" + `) + expect(run(['scale-200', 'scale-x-400'])).toMatchInlineSnapshot(` + ".scale-200 { + --tw-scale-x: 200%; + --tw-scale-y: 200%; + --tw-scale-z: 200%; + scale: var(--tw-scale-x) var(--tw-scale-y); + } + + .scale-x-400 { + --tw-scale-x: 400%; + scale: var(--tw-scale-x) var(--tw-scale-y); + } + + @property --tw-scale-x { + syntax: " | "; + inherits: false; + initial-value: 1; + } + + @property --tw-scale-y { + syntax: " | "; + inherits: false; + initial-value: 1; + } + + @property --tw-scale-z { + syntax: " | "; + inherits: false; + initial-value: 1; }" `) expect(run(['scale-x', 'scale-x-unknown'])).toEqual('') }) test('scale-y', () => { - expect(run(['scale-y-50', '-scale-y-50', 'scale-y-[123deg]'])).toMatchInlineSnapshot(` + expect(run(['scale-y-50', '-scale-y-50', 'scale-y-[2]'])).toMatchInlineSnapshot(` ".-scale-y-50 { --tw-scale-y: calc(50% * -1); scale: var(--tw-scale-x) var(--tw-scale-y); @@ -2680,8 +2960,8 @@ test('scale-y', () => { scale: var(--tw-scale-x) var(--tw-scale-y); } - .scale-y-\\[123deg\\] { - --tw-scale-y: 123deg; + .scale-y-\\[2\\] { + --tw-scale-y: 2; scale: var(--tw-scale-x) var(--tw-scale-y); } @@ -2695,14 +2975,65 @@ test('scale-y', () => { syntax: " | "; inherits: false; initial-value: 1; + } + + @property --tw-scale-z { + syntax: " | "; + inherits: false; + initial-value: 1; }" `) expect(run(['scale-y', 'scale-y-unknown'])).toEqual('') }) +test('scale-z', () => { + expect(run(['scale-z-50', '-scale-z-50', 'scale-z-[123deg]'])).toMatchInlineSnapshot(` + ".-scale-z-50 { + --tw-scale-z: calc(50% * -1); + scale: var(--tw-scale-x) var(--tw-scale-y) var(--tw-scale-z); + } + + .scale-z-50 { + --tw-scale-z: 50%; + scale: var(--tw-scale-x) var(--tw-scale-y) var(--tw-scale-z); + } + + .scale-z-\\[123deg\\] { + --tw-scale-z: 123deg; + scale: var(--tw-scale-x) var(--tw-scale-y) var(--tw-scale-z); + } + + @property --tw-scale-x { + syntax: " | "; + inherits: false; + initial-value: 1; + } + + @property --tw-scale-y { + syntax: " | "; + inherits: false; + initial-value: 1; + } + + @property --tw-scale-z { + syntax: " | "; + inherits: false; + initial-value: 1; + }" + `) + expect(run(['scale-z'])).toEqual('') +}) + test('transform', () => { - expect(run(['transform', 'transform-cpu', 'transform-gpu', 'transform-none'])) - .toMatchInlineSnapshot(` + expect( + run([ + 'transform', + 'transform-cpu', + 'transform-gpu', + 'transform-none', + 'transform-[scaleZ(2)_rotateY(45deg)]', + ]), + ).toMatchInlineSnapshot(` ".transform-none { transform: none; } @@ -2711,6 +3042,10 @@ test('transform', () => { transform: skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)); } + .transform-\\[scaleZ\\(2\\)_rotateY\\(45deg\\)\\] { + transform: scaleZ(2) rotateY(45deg) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)); + } + .transform-cpu { transform: translate(0); } @@ -2731,9 +3066,105 @@ test('transform', () => { initial-value: 0deg; }" `) + expect( + run([ + 'transform-flat', + 'transform-3d', + 'transform-content', + 'transform-border', + 'transform-fill', + 'transform-stroke', + 'transform-view', + 'backface-visible', + 'backface-hidden', + ]), + ).toMatchInlineSnapshot(` + ".backface-hidden { + backface-visibility: hidden; + } + + .backface-visible { + backface-visibility: visible; + } + + .transform-3d { + transform-style: preserve-3d; + } + + .transform-border { + transform-box: border-box; + } + + .transform-content { + transform-box: content-box; + } + + .transform-fill { + transform-box: fill-box; + } + + .transform-flat { + transform-style: flat; + } + + .transform-stroke { + transform-box: stroke-box; + } + + .transform-view { + transform-box: view-box; + }" + `) expect(run(['-transform', '-transform-cpu', '-transform-gpu', '-transform-none'])).toEqual('') }) +test('perspective', () => { + expect( + compileCss( + css` + @theme { + --perspective-dramatic: 100px; + --perspective-normal: 500px; + } + @tailwind utilities; + `, + [ + 'perspective-normal', + 'perspective-dramatic', + 'perspective-none', + 'perspective-123', + 'perspective-[456px]', + ], + ), + ).toMatchInlineSnapshot(` + ":root { + --perspective-dramatic: 100px; + --perspective-normal: 500px; + } + + .perspective-123 { + perspective: 123px; + } + + .perspective-\\[456px\\] { + perspective: 456px; + } + + .perspective-dramatic { + perspective: 100px; + } + + .perspective-none { + perspective: none; + } + + .perspective-normal { + perspective: 500px; + }" + `) + expect(run(['perspective', '-perspective', 'perspective-potato'])).toEqual('') +}) + test('cursor', () => { expect( compileCss( diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index fb952c1f5199..b3051597716d 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -1189,10 +1189,25 @@ export function createUtilities(theme: Theme) { handle: (value) => [decl('transform-origin', value)], }) + staticUtility('perspective-origin-center', [['perspective-origin', 'center']]) + staticUtility('perspective-origin-top', [['perspective-origin', 'top']]) + staticUtility('perspective-origin-top-right', [['perspective-origin', 'top right']]) + staticUtility('perspective-origin-right', [['perspective-origin', 'right']]) + staticUtility('perspective-origin-bottom-right', [['perspective-origin', 'bottom right']]) + staticUtility('perspective-origin-bottom', [['perspective-origin', 'bottom']]) + staticUtility('perspective-origin-bottom-left', [['perspective-origin', 'bottom left']]) + staticUtility('perspective-origin-left', [['perspective-origin', 'left']]) + staticUtility('perspective-origin-top-left', [['perspective-origin', 'top left']]) + functionalUtility('perspective-origin', { + themeKeys: ['--perspective-origin'], + handle: (value) => [decl('perspective-origin', value)], + }) + let translateProperties = () => atRoot([ property('--tw-translate-x', '0', ''), property('--tw-translate-y', '0', ''), + property('--tw-translate-z', '0', ''), ]) /** @@ -1205,9 +1220,11 @@ export function createUtilities(theme: Theme) { translateProperties(), decl('--tw-translate-x', value), decl('--tw-translate-y', value), + decl('--tw-translate-z', value), decl('translate', 'var(--tw-translate-x) var(--tw-translate-y)'), ] }) + functionalUtility('translate', { supportsNegative: true, supportsFractions: true, @@ -1216,67 +1233,77 @@ export function createUtilities(theme: Theme) { translateProperties(), decl('--tw-translate-x', value), decl('--tw-translate-y', value), + decl('--tw-translate-z', value), decl('translate', 'var(--tw-translate-x) var(--tw-translate-y)'), ], }) - /** - * @css `translate` - */ - utilities.static('translate-x-full', (candidate) => { - let value = candidate.negative ? '-100%' : '100%' - - return [ + for (let axis of ['x', 'y', 'z']) { + let handle = (value: string) => [ translateProperties(), - decl('--tw-translate-x', value), - decl('translate', 'var(--tw-translate-x) var(--tw-translate-y)'), + decl(`--tw-translate-${axis}`, value), + decl( + 'translate', + `var(--tw-translate-x) var(--tw-translate-y)${axis === 'z' ? ' var(--tw-translate-z)' : ''}`, + ), ] - }) - functionalUtility('translate-x', { - supportsNegative: true, - supportsFractions: true, - themeKeys: ['--translate', '--spacing'], - handle: (value) => [ - translateProperties(), - decl('--tw-translate-x', value), - decl('translate', 'var(--tw-translate-x) var(--tw-translate-y)'), - ], - }) + + /** + * @css `translate` + */ + functionalUtility(`translate-${axis}`, { + supportsNegative: true, + supportsFractions: true, + themeKeys: ['--translate', '--spacing'], + handleBareValue: ({ value }) => { + if (Number.isNaN(Number(value))) return null + return `${value}%` + }, + handle, + }) + utilities.static(`translate-${axis}-px`, (candidate) => { + return handle(candidate.negative ? '-1px' : '1px') + }) + utilities.static(`translate-${axis}-full`, (candidate) => { + return handle(candidate.negative ? '-100%' : '100%') + }) + } /** * @css `translate` */ - utilities.static('translate-y-full', (candidate) => { - let value = candidate.negative ? '-100%' : '100%' - - return [ - translateProperties(), - decl('--tw-translate-y', value), - decl('translate', 'var(--tw-translate-x) var(--tw-translate-y)'), - ] - }) - functionalUtility('translate-y', { - supportsNegative: true, - supportsFractions: true, - themeKeys: ['--translate', '--spacing'], - handle: (value) => [ - translateProperties(), - decl('--tw-translate-y', value), - decl('translate', 'var(--tw-translate-x) var(--tw-translate-y)'), - ], - }) + staticUtility('translate-3d', [ + translateProperties, + ['translate', 'var(--tw-translate-x) var(--tw-translate-y) var(--tw-translate-z)'], + ]) /** * @css `rotate` + * + * `rotate-45` => `rotate: 45deg` + * `rotate-[x_45deg]` => `rotate: x 45deg` + * `rotate-[1_2_3_45deg]` => `rotate: 1 2 3 45deg` */ - functionalUtility('rotate', { - supportsNegative: true, - themeKeys: ['--rotate'], - handleBareValue: ({ value }) => { - if (Number.isNaN(Number(value))) return null - return `${value}deg` - }, - handle: (value) => [decl('rotate', value)], + utilities.functional('rotate', (candidate) => { + if (!candidate.value) return + let value + if (candidate.value.kind === 'arbitrary') { + value = candidate.value.value + let type = candidate.value.dataType ?? inferDataType(value, ['angle', 'vector']) + if (type === 'vector') { + return [decl('rotate', `${value} var(--tw-rotate)`)] + } else if (type !== 'angle') { + return [decl('rotate', value)] + } + } else { + value = theme.resolve(candidate.value.value, ['--rotate']) + if (!value && !Number.isNaN(Number(candidate.value.value))) { + value = `${candidate.value.value}deg` + } + if (!value) return + } + value = withNegative(value, candidate) + return [decl('rotate', value)] }) suggest('rotate', () => [ @@ -1287,6 +1314,26 @@ export function createUtilities(theme: Theme) { }, ]) + for (let axis of ['x', 'y']) { + functionalUtility(`rotate-${axis}`, { + supportsNegative: true, + themeKeys: ['--rotate'], + handleBareValue: ({ value }) => { + if (Number.isNaN(Number(value))) return null + return `${value}deg` + }, + handle: (value) => [decl('rotate', `${axis} ${value}`)], + }) + + suggest(`rotate-${axis}`, () => [ + { + supportsNegative: true, + values: ['0', '1', '2', '3', '6', '12', '45', '90', '180'], + valueThemeKeys: ['--rotate'], + }, + ]) + } + let skewProperties = () => atRoot([property('--tw-skew-x', '0deg', ''), property('--tw-skew-y', '0deg', '')]) @@ -1373,58 +1420,33 @@ export function createUtilities(theme: Theme) { atRoot([ property('--tw-scale-x', '1', ' | '), property('--tw-scale-y', '1', ' | '), + property('--tw-scale-z', '1', ' | '), ]) /** * @css `scale` */ - functionalUtility('scale', { - supportsNegative: true, - themeKeys: ['--scale'], - handleBareValue: ({ value }) => { - if (Number.isNaN(Number(value))) return null - return `${value}%` - }, - handle: (value) => [ - scaleProperties(), - decl('--tw-scale-x', value), - decl('--tw-scale-y', value), - decl('scale', 'var(--tw-scale-x) var(--tw-scale-y)'), - ], - }) - - /** - * @css `scale` - */ - functionalUtility('scale-x', { - supportsNegative: true, - themeKeys: ['--scale'], - handleBareValue: ({ value }) => { - if (Number.isNaN(Number(value))) return null - return `${value}%` - }, - handle: (value) => [ + utilities.functional('scale', (candidate) => { + if (!candidate.value) return + let value + if (candidate.value.kind === 'arbitrary') { + value = candidate.value.value + return [decl('scale', value)] + } else { + value = theme.resolve(candidate.value.value, ['--scale']) + if (!value && !Number.isNaN(Number(candidate.value.value))) { + value = `${candidate.value.value}%` + } + if (!value) return + } + value = withNegative(value, candidate) + return [ scaleProperties(), decl('--tw-scale-x', value), - decl('scale', 'var(--tw-scale-x) var(--tw-scale-y)'), - ], - }) - - /** - * @css `scale` - */ - functionalUtility('scale-y', { - supportsNegative: true, - themeKeys: ['--scale'], - handleBareValue: ({ value }) => { - if (Number.isNaN(Number(value))) return null - return `${value}%` - }, - handle: (value) => [ - scaleProperties(), decl('--tw-scale-y', value), - decl('scale', 'var(--tw-scale-x) var(--tw-scale-y)'), - ], + decl('--tw-scale-z', value), + decl('scale', `var(--tw-scale-x) var(--tw-scale-y)`), + ] }) suggest('scale', () => [ @@ -1435,28 +1457,83 @@ export function createUtilities(theme: Theme) { }, ]) - suggest('scale-x', () => [ - { + for (let axis of ['x', 'y', 'z']) { + /** + * @css `scale` + */ + functionalUtility(`scale-${axis}`, { supportsNegative: true, - values: ['0', '50', '75', '90', '95', '100', '105', '110', '125', '150', '200'], - valueThemeKeys: ['--scale'], - }, + themeKeys: ['--scale'], + handleBareValue: ({ value }) => { + if (Number.isNaN(Number(value))) return null + return `${value}%` + }, + handle: (value) => [ + scaleProperties(), + decl(`--tw-scale-${axis}`, value), + decl( + 'scale', + `var(--tw-scale-x) var(--tw-scale-y)${axis === 'z' ? ' var(--tw-scale-z)' : ''}`, + ), + ], + }) + + suggest(`scale-${axis}`, () => [ + { + supportsNegative: true, + values: ['0', '50', '75', '90', '95', '100', '105', '110', '125', '150', '200'], + valueThemeKeys: ['--scale'], + }, + ]) + } + + /** + * @css `scale` + */ + staticUtility('scale-3d', [ + scaleProperties, + ['scale', 'var(--tw-scale-x) var(--tw-scale-y) var(--tw-scale-z)'], ]) - suggest('scale-y', () => [ - { - supportsNegative: true, - values: ['0', '50', '75', '90', '95', '100', '105', '110', '125', '150', '200'], - valueThemeKeys: ['--scale'], + /** + * @css `perspective` + */ + staticUtility('perspective-none', [['perspective', 'none']]) + functionalUtility('perspective', { + themeKeys: ['--perspective'], + handleBareValue: ({ value }) => { + if (!Number.isInteger(Number(value))) return null + return `${value}px` }, - ]) + handle: (value) => [decl('perspective', value)], + }) /** * @css `transform` */ - staticUtility('transform', [ - skewProperties, - ['transform', 'skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y))'], + utilities.functional('transform', (candidate) => { + if (candidate.negative) return + + let value = 'skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y))' + if (!candidate.value) { + // Supported as a legacy value for its stacking context + } else if (candidate.value.kind === 'arbitrary') { + // Since skew-x and skew-y are implemented using `transform`, we preserve + // them even if an arbitrary transform is provided. Transform functions + // are applied right to left, so we put them at the end so they are + // applied before other transforms, similar to other specific transform + // properties. + // + // https://developer.mozilla.org/en-US/docs/Web/CSS/transform#values + value = `${candidate.value.value} ${value}` + } + + return [skewProperties(), decl('transform', value)] + }) + suggest('transform', () => [ + { + hasDefaultValue: true, + }, ]) staticUtility('transform-cpu', [['transform', 'translate(0,0)']]) @@ -1468,6 +1545,27 @@ export function createUtilities(theme: Theme) { ['transform', 'none'], ]) + /** + * @css `transform-style` + */ + staticUtility('transform-flat', [['transform-style', 'flat']]) + staticUtility('transform-3d', [['transform-style', 'preserve-3d']]) + + /** + * @css `transform-box` + */ + staticUtility('transform-content', [['transform-box', 'content-box']]) + staticUtility('transform-border', [['transform-box', 'border-box']]) + staticUtility('transform-fill', [['transform-box', 'fill-box']]) + staticUtility('transform-stroke', [['transform-box', 'stroke-box']]) + staticUtility('transform-view', [['transform-box', 'view-box']]) + + /** + * @css `backface-visibility` + */ + staticUtility('backface-visible', [['backface-visibility', 'visible']]) + staticUtility('backface-hidden', [['backface-visibility', 'hidden']]) + /** * @css `cursor` */ diff --git a/packages/tailwindcss/src/utils/infer-data-type.ts b/packages/tailwindcss/src/utils/infer-data-type.ts index 9025eeb3feb3..380c08ac09b3 100644 --- a/packages/tailwindcss/src/utils/infer-data-type.ts +++ b/packages/tailwindcss/src/utils/infer-data-type.ts @@ -16,6 +16,8 @@ type DataType = | 'generic-name' | 'absolute-size' | 'relative-size' + | 'angle' + | 'vector' const checks: Record boolean> = { color: isColor, @@ -31,6 +33,8 @@ const checks: Record boolean> = { 'generic-name': isGenericName, 'absolute-size': isAbsoluteSize, 'relative-size': isRelativeSize, + angle: isAngle, + vector: isVector, } /** @@ -283,3 +287,36 @@ function isBackgroundSize(value: string) { return count > 0 } + +/* -------------------------------------------------------------------------- */ + +const ANGLE_UNITS = ['deg', 'rad', 'grad', 'turn'] + +const IS_ANGLE = new RegExp(`^${HAS_NUMBER.source}(${ANGLE_UNITS.join('|')})$`) + +/** + * Determine if `value` is valid angle + * + * = + * = deg | rad | grad | turn + * + * @see https://developer.mozilla.org/en-US/docs/Web/CSS/angle + */ +function isAngle(value: string) { + return IS_ANGLE.test(value) +} + +/* -------------------------------------------------------------------------- */ + +const IS_VECTOR = new RegExp(`^${HAS_NUMBER.source} +${HAS_NUMBER.source} +${HAS_NUMBER.source}$`) + +/** + * Determine if `value` is valid for the vector component of `rotate` + * + * = + * + * @see https://developer.mozilla.org/en-US/docs/Web/CSS/rotate#vector_plus_angle_value + */ +function isVector(value: string) { + return IS_VECTOR.test(value) +} diff --git a/packages/tailwindcss/theme.css b/packages/tailwindcss/theme.css index 00fe4cc5e8f0..2028f4da5cbf 100644 --- a/packages/tailwindcss/theme.css +++ b/packages/tailwindcss/theme.css @@ -421,6 +421,13 @@ --line-height-9: 2.25rem; --line-height-10: 2.5rem; + /* 3D perspectives */ + --perspective-dramatic: 100px; + --perspective-near: 300px; + --perspective-normal: 500px; + --perspective-midrange: 800px; + --perspective-distant: 1200px; + /* Transition timing functions */ --transition-timing-function-linear: linear; --transition-timing-function-in: cubic-bezier(0.4, 0, 1, 1);