diff --git a/CHANGELOG.md b/CHANGELOG.md index c60d4490700e..d61bfc1eec4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Nothing yet! +## [4.0.2] - 2025-01-31 + +### Fixed + +- Only generate positive `grid-cols-*` and `grid-rows-*` utilities ([#16020](https://github.com/tailwindlabs/tailwindcss/pull/16020)) +- Ensure escaped theme variables are handled correctly ([#16064](https://github.com/tailwindlabs/tailwindcss/pull/16064)) +- Ensure we process Tailwind CSS features when only using `@reference` or `@variant` ([#16057](https://github.com/tailwindlabs/tailwindcss/pull/16057)) +- Refactor gradient implementation to work around [prettier/prettier#17058](https://github.com/prettier/prettier/issues/17058) ([#16072](https://github.com/tailwindlabs/tailwindcss/pull/16072)) +- Vite: Ensure hot-reloading works with SolidStart setups ([#16052](https://github.com/tailwindlabs/tailwindcss/pull/16052)) +- Vite: Fix a crash when starting the development server in SolidStart setups ([#16052](https://github.com/tailwindlabs/tailwindcss/pull/16052)) +- Vite: Don't rebase URLs that appear to be aliases ([#16078](https://github.com/tailwindlabs/tailwindcss/pull/16078)) +- Vite: Transform ` + + + `, + }, + }, + async ({ fs, exec, expect }) => { + await exec('pnpm vite build') + + expect(await fs.dumpFiles('dist/*.html')).toMatchInlineSnapshot(` + " + --- dist/index.html --- + + + +
+ + + + " + `) + }, +) diff --git a/integrations/vite/solidstart.test.ts b/integrations/vite/solidstart.test.ts new file mode 100644 index 000000000000..bbe9ccc8868d --- /dev/null +++ b/integrations/vite/solidstart.test.ts @@ -0,0 +1,108 @@ +import { candidate, css, fetchStyles, js, json, retryAssertion, test, ts } from '../utils' + +const WORKSPACE = { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@solidjs/start": "^1", + "solid-js": "^1", + "vinxi": "^0", + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + } + } + `, + 'jsconfig.json': json` + { + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js" + } + } + `, + 'app.config.js': ts` + import { defineConfig } from '@solidjs/start/config' + import tailwindcss from '@tailwindcss/vite' + + export default defineConfig({ + vite: { + plugins: [tailwindcss()], + }, + }) + `, + 'src/entry-server.jsx': js` + // @refresh reload + import { createHandler, StartServer } from '@solidjs/start/server' + + export default createHandler(() => ( + ( + + {assets} + +
{children}
+ {scripts} + + + )} + /> + )) + `, + 'src/entry-client.jsx': js` + // @refresh reload + import { mount, StartClient } from '@solidjs/start/client' + + mount(() => , document.getElementById('app')) + `, + 'src/app.jsx': js` + import './app.css' + export default function App() { + return

Hello world!

+ } + `, + 'src/app.css': css`@import 'tailwindcss';`, +} + +test( + 'dev mode', + { + fs: WORKSPACE, + }, + async ({ fs, spawn, expect }) => { + let process = await spawn('pnpm vinxi dev', { + env: { + TEST: 'false', // VERY IMPORTANT OTHERWISE YOU WON'T GET OUTPUT + NODE_ENV: 'development', + }, + }) + + 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 css = await fetchStyles(url) + expect(css).toContain(candidate`underline`) + }) + + await retryAssertion(async () => { + await fs.write( + 'src/app.jsx', + js` + import './app.css' + export default function App() { + return

Hello world!

+ } + `, + ) + + let css = await fetchStyles(url) + expect(css).toContain(candidate`underline`) + expect(css).toContain(candidate`font-bold`) + }) + }, +) diff --git a/package.json b/package.json index 047f461c5200..4266843fffef 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ }, "license": "MIT", "devDependencies": { - "@playwright/test": "^1.49.1", + "@playwright/test": "^1.50.0", "@types/node": "catalog:", "postcss": "8.5.1", "postcss-import": "^16.1.0", @@ -56,7 +56,7 @@ "prettier-plugin-embed": "^0.4.15", "prettier-plugin-organize-imports": "^4.0.0", "tsup": "^8.2.4", - "turbo": "^2.3.3", + "turbo": "^2.3.4", "typescript": "^5.5.4", "vitest": "^2.0.5" }, diff --git a/packages/@tailwindcss-browser/package.json b/packages/@tailwindcss-browser/package.json index 5758c89547eb..c1d96c5584c1 100644 --- a/packages/@tailwindcss-browser/package.json +++ b/packages/@tailwindcss-browser/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/browser", - "version": "4.0.1", + "version": "4.0.2", "description": "A utility-first CSS framework for rapidly building custom user interfaces.", "license": "MIT", "main": "./dist/index.global.js", diff --git a/packages/@tailwindcss-cli/package.json b/packages/@tailwindcss-cli/package.json index 63b35d2fb3c8..a496c6b01e30 100644 --- a/packages/@tailwindcss-cli/package.json +++ b/packages/@tailwindcss-cli/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/cli", - "version": "4.0.1", + "version": "4.0.2", "description": "A utility-first CSS framework for rapidly building custom user interfaces.", "license": "MIT", "repository": { diff --git a/packages/@tailwindcss-node/package.json b/packages/@tailwindcss-node/package.json index fb5333b8e72b..a70b59c404a8 100644 --- a/packages/@tailwindcss-node/package.json +++ b/packages/@tailwindcss-node/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/node", - "version": "4.0.1", + "version": "4.0.2", "description": "A utility-first CSS framework for rapidly building custom user interfaces.", "license": "MIT", "repository": { diff --git a/packages/@tailwindcss-node/src/urls.test.ts b/packages/@tailwindcss-node/src/urls.test.ts index 3378e45edbdc..16ba352a7f66 100644 --- a/packages/@tailwindcss-node/src/urls.test.ts +++ b/packages/@tailwindcss-node/src/urls.test.ts @@ -24,6 +24,20 @@ test('URLs can be rewritten', async () => { background: url('/image.jpg'); background: url("/image.jpg"); + /* Potentially Vite-aliased URLs: ignored */ + background: url(~/image.jpg); + background: url(~/foo/image.jpg); + background: url('~/image.jpg'); + background: url("~/image.jpg"); + background: url(#/image.jpg); + background: url(#/foo/image.jpg); + background: url('#/image.jpg'); + background: url("#/image.jpg"); + background: url(@/image.jpg); + background: url(@/foo/image.jpg); + background: url('@/image.jpg'); + background: url("@/image.jpg"); + /* External URL: ignored */ background: url(http://example.com/image.jpg); background: url('http://example.com/image.jpg'); @@ -109,6 +123,18 @@ test('URLs can be rewritten', async () => { background: url(/foo/image.jpg); background: url('/image.jpg'); background: url("/image.jpg"); + background: url(~/image.jpg); + background: url(~/foo/image.jpg); + background: url('~/image.jpg'); + background: url("~/image.jpg"); + background: url(#/image.jpg); + background: url(#/foo/image.jpg); + background: url('#/image.jpg'); + background: url("#/image.jpg"); + background: url(@/image.jpg); + background: url(@/foo/image.jpg); + background: url('@/image.jpg'); + background: url("@/image.jpg"); background: url(http://example.com/image.jpg); background: url('http://example.com/image.jpg'); background: url("http://example.com/image.jpg"); diff --git a/packages/@tailwindcss-node/src/urls.ts b/packages/@tailwindcss-node/src/urls.ts index c4d56deb7e60..e35b9d280a06 100644 --- a/packages/@tailwindcss-node/src/urls.ts +++ b/packages/@tailwindcss-node/src/urls.ts @@ -149,9 +149,12 @@ async function doUrlReplace( return `${funcName}(${wrap}${newUrl}${wrap})` } -function skipUrlReplacer(rawUrl: string) { +function skipUrlReplacer(rawUrl: string, aliases?: string[]) { return ( - isExternalUrl(rawUrl) || isDataUrl(rawUrl) || rawUrl[0] === '#' || functionCallRE.test(rawUrl) + isExternalUrl(rawUrl) || + isDataUrl(rawUrl) || + !rawUrl[0].match(/[\.a-zA-Z0-9_]/) || + functionCallRE.test(rawUrl) ) } diff --git a/packages/@tailwindcss-postcss/package.json b/packages/@tailwindcss-postcss/package.json index 00b49bdf5783..cb0d24cd2ad1 100644 --- a/packages/@tailwindcss-postcss/package.json +++ b/packages/@tailwindcss-postcss/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/postcss", - "version": "4.0.1", + "version": "4.0.2", "description": "PostCSS plugin for Tailwind CSS, a utility-first CSS framework for rapidly building custom user interfaces", "license": "MIT", "repository": { diff --git a/packages/@tailwindcss-postcss/src/index.test.ts b/packages/@tailwindcss-postcss/src/index.test.ts index 97eedb95d183..97eee3c1c5f0 100644 --- a/packages/@tailwindcss-postcss/src/index.test.ts +++ b/packages/@tailwindcss-postcss/src/index.test.ts @@ -248,6 +248,56 @@ test('bail early when Tailwind is not used', async () => { `) }) +test('handle CSS when only using a `@reference` (we should not bail early)', async () => { + let processor = postcss([ + tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), + ]) + + let result = await processor.process( + css` + @reference "tailwindcss/theme.css"; + + .foo { + @variant md { + bar: baz; + } + } + `, + { from: inputCssFilePath() }, + ) + + expect(result.css.trim()).toMatchInlineSnapshot(` + "@media (width >= 48rem) { + .foo { + bar: baz; + } + }" + `) +}) + +test('handle CSS when using a `@variant` using variants that do not rely on the `@theme`', async () => { + let processor = postcss([ + tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), + ]) + + let result = await processor.process( + css` + .foo { + @variant data-is-hoverable { + bar: baz; + } + } + `, + { from: inputCssFilePath() }, + ) + + expect(result.css.trim()).toMatchInlineSnapshot(` + ".foo[data-is-hoverable] { + bar: baz; + }" + `) +}) + test('runs `Once` plugins in the right order', async () => { let before = '' let after = '' diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index 084f3671586f..d027fbd9ca67 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -77,7 +77,9 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { root.walkAtRules((node) => { if ( node.name === 'import' || + node.name === 'reference' || node.name === 'theme' || + node.name === 'variant' || node.name === 'config' || node.name === 'plugin' || node.name === 'apply' diff --git a/packages/@tailwindcss-standalone/package.json b/packages/@tailwindcss-standalone/package.json index d45ad2037f5e..6343af214a24 100644 --- a/packages/@tailwindcss-standalone/package.json +++ b/packages/@tailwindcss-standalone/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/standalone", - "version": "4.0.1", + "version": "4.0.2", "private": true, "description": "Standalone CLI for Tailwind CSS", "license": "MIT", diff --git a/packages/@tailwindcss-upgrade/package.json b/packages/@tailwindcss-upgrade/package.json index 2274dd358018..bc7ce6e8b047 100644 --- a/packages/@tailwindcss-upgrade/package.json +++ b/packages/@tailwindcss-upgrade/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/upgrade", - "version": "4.0.1", + "version": "4.0.2", "description": "A utility-first CSS framework for rapidly building custom user interfaces.", "license": "MIT", "repository": { diff --git a/packages/@tailwindcss-vite/package.json b/packages/@tailwindcss-vite/package.json index bd8682a9ddfb..810a734f6df1 100644 --- a/packages/@tailwindcss-vite/package.json +++ b/packages/@tailwindcss-vite/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/vite", - "version": "4.0.1", + "version": "4.0.2", "description": "A utility-first CSS framework for rapidly building custom user interfaces.", "license": "MIT", "repository": { diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index c8976c0276da..c7cc111fad05 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -8,6 +8,7 @@ import type { Plugin, ResolvedConfig, Rollup, Update, ViteDevServer } from 'vite const DEBUG = env.DEBUG const SPECIAL_QUERY_RE = /[?&](raw|url)\b/ +const INLINE_STYLE_ID_RE = /[?&]index\=\d+\.css$/ const IGNORED_DEPENDENCIES = ['tailwind-merge'] @@ -63,7 +64,7 @@ export default function tailwindcss(): Plugin[] { ) }) - function scanFile(id: string, content: string, extension: string, isSSR: boolean) { + function scanFile(id: string, content: string, extension: string) { for (let dependency of IGNORED_DEPENDENCIES) { // We validated that Vite IDs always use posix style path separators, even on Windows. // In dev build, Vite precompiles dependencies @@ -83,26 +84,16 @@ export default function tailwindcss(): Plugin[] { } if (updated) { - invalidateAllRoots(isSSR) + invalidateAllRoots() } } - function invalidateAllRoots(isSSR: boolean) { + function invalidateAllRoots() { for (let server of servers) { let updates: Update[] = [] - for (let [id, root] of roots.entries()) { + for (let [id] of roots.entries()) { let module = server.moduleGraph.getModuleById(id) - if (!module) { - // Note: Removing this during SSR is not safe and will produce - // inconsistent results based on the timing of the removal and - // the order / timing of transforms. - if (!isSSR) { - // It is safe to remove the item here since we're iterating on a copy - // of the keys. - roots.delete(id) - } - continue - } + if (!module) continue roots.get(id).requiresRebuild = false server.moduleGraph.invalidateModule(module) @@ -113,7 +104,6 @@ export default function tailwindcss(): Plugin[] { timestamp: Date.now(), }) } - if (updates.length > 0) { server.hot.send({ type: 'update', updates }) } @@ -210,12 +200,15 @@ export default function tailwindcss(): Plugin[] { // Scan all non-CSS files for candidates transformIndexHtml(html, { path }) { - scanFile(path, html, 'html', isSSR) + // SolidStart emits HTML chunks with an undefined path and the html content of `\`. + if (!path) return + + scanFile(path, html, 'html') }, transform(src, id, options) { let extension = getExtension(id) if (isPotentialCssRootFile(id)) return - scanFile(id, src, extension, options?.ssr ?? false) + scanFile(id, src, extension) }, }, @@ -320,7 +313,7 @@ function isPotentialCssRootFile(id: string) { if (id.includes('/.vite/')) return let extension = getExtension(id) let isCssFile = - (extension === 'css' || id.includes('&lang.css')) && + (extension === 'css' || id.includes('&lang.css') || id.match(INLINE_STYLE_ID_RE)) && // Don't intercept special static asset resources !SPECIAL_QUERY_RE.test(id) diff --git a/packages/tailwindcss/package.json b/packages/tailwindcss/package.json index 29b92bef2394..74df33960b3d 100644 --- a/packages/tailwindcss/package.json +++ b/packages/tailwindcss/package.json @@ -1,6 +1,6 @@ { "name": "tailwindcss", - "version": "4.0.1", + "version": "4.0.2", "description": "A utility-first CSS framework for rapidly building custom user interfaces.", "license": "MIT", "repository": { diff --git a/packages/tailwindcss/src/ast.test.ts b/packages/tailwindcss/src/ast.test.ts index c3d9b2f159bf..174971ea541d 100644 --- a/packages/tailwindcss/src/ast.test.ts +++ b/packages/tailwindcss/src/ast.test.ts @@ -2,6 +2,8 @@ import { expect, it } from 'vitest' import { context, decl, optimizeAst, styleRule, toCss, walk, WalkAction } from './ast' import * as CSS from './css-parser' +const css = String.raw + it('should pretty print an AST', () => { expect(toCss(optimizeAst(CSS.parse('.foo{color:red;&:hover{color:blue;}}')))) .toMatchInlineSnapshot(` @@ -95,3 +97,91 @@ it('should stop walking when returning `WalkAction.Stop`', () => { } `) }) + +it('should not emit empty rules once optimized', () => { + let ast = CSS.parse(css` + /* Empty rule */ + .foo { + } + + /* Empty rule, with nesting */ + .foo { + .bar { + } + .baz { + } + } + + /* Empty rule, with special case '&' rules */ + .foo { + & { + &:hover { + } + &:focus { + } + } + } + + /* Empty at-rule */ + @media (min-width: 768px) { + } + + /* Empty at-rule with nesting*/ + @media (min-width: 768px) { + .foo { + } + + @media (min-width: 1024px) { + .bar { + } + } + } + + /* Exceptions: */ + @charset "UTF-8"; + @layer foo, bar, baz; + @custom-media --modern (color), (hover); + @namespace 'http://www.w3.org/1999/xhtml'; + `) + + expect(toCss(ast)).toMatchInlineSnapshot(` + ".foo { + } + .foo { + .bar { + } + .baz { + } + } + .foo { + & { + &:hover { + } + &:focus { + } + } + } + @media (min-width: 768px); + @media (min-width: 768px) { + .foo { + } + @media (min-width: 1024px) { + .bar { + } + } + } + @charset "UTF-8"; + @layer foo, bar, baz; + @custom-media --modern (color), (hover); + @namespace 'http://www.w3.org/1999/xhtml'; + " + `) + + expect(toCss(optimizeAst(ast))).toMatchInlineSnapshot(` + "@charset "UTF-8"; + @layer foo, bar, baz; + @custom-media --modern (color), (hover); + @namespace 'http://www.w3.org/1999/xhtml'; + " + `) +}) diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index 6052724f33b9..7f67fdda5f57 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -261,7 +261,9 @@ export function optimizeAst(ast: AstNode[]) { for (let child of node.nodes) { let nodes: AstNode[] = [] transform(child, nodes, depth + 1) - parent.push(...nodes) + if (nodes.length > 0) { + parent.push(...nodes) + } } } @@ -271,7 +273,9 @@ export function optimizeAst(ast: AstNode[]) { for (let child of node.nodes) { transform(child, copy.nodes, depth + 1) } - parent.push(copy) + if (copy.nodes.length > 0) { + parent.push(copy) + } } } @@ -297,7 +301,15 @@ export function optimizeAst(ast: AstNode[]) { for (let child of node.nodes) { transform(child, copy.nodes, depth + 1) } - parent.push(copy) + if ( + copy.nodes.length > 0 || + copy.name === '@layer' || + copy.name === '@charset' || + copy.name === '@custom-media' || + copy.name === '@namespace' + ) { + parent.push(copy) + } } // AtRoot diff --git a/packages/tailwindcss/src/compat/apply-config-to-theme.ts b/packages/tailwindcss/src/compat/apply-config-to-theme.ts index 3f18711c7f16..c57e4308fcb7 100644 --- a/packages/tailwindcss/src/compat/apply-config-to-theme.ts +++ b/packages/tailwindcss/src/compat/apply-config-to-theme.ts @@ -1,6 +1,5 @@ import type { DesignSystem } from '../design-system' import { ThemeOptions } from '../theme' -import { escape } from '../utils/escape' import type { ResolvedConfig } from './config/types' function resolveThemeValue(value: unknown, subValue: string | null = null): string | null { @@ -55,7 +54,7 @@ export function applyConfigToTheme( if (!name) continue designSystem.theme.add( - `--${escape(name)}`, + `--${name}`, '' + value, ThemeOptions.INLINE | ThemeOptions.REFERENCE | ThemeOptions.DEFAULT, ) diff --git a/packages/tailwindcss/src/compat/legacy-utilities.test.ts b/packages/tailwindcss/src/compat/legacy-utilities.test.ts index 4c5dd99d0816..9a828d673b78 100644 --- a/packages/tailwindcss/src/compat/legacy-utilities.test.ts +++ b/packages/tailwindcss/src/compat/legacy-utilities.test.ts @@ -22,42 +22,42 @@ test('bg-gradient-*', async () => { ), ).toMatchInlineSnapshot(` ".bg-gradient-to-b { - --tw-gradient-position: to bottom in oklab, ; + --tw-gradient-position: to bottom in oklab; background-image: linear-gradient(var(--tw-gradient-stops)); } .bg-gradient-to-bl { - --tw-gradient-position: to bottom left in oklab, ; + --tw-gradient-position: to bottom left in oklab; background-image: linear-gradient(var(--tw-gradient-stops)); } .bg-gradient-to-br { - --tw-gradient-position: to bottom right in oklab, ; + --tw-gradient-position: to bottom right in oklab; background-image: linear-gradient(var(--tw-gradient-stops)); } .bg-gradient-to-l { - --tw-gradient-position: to left in oklab, ; + --tw-gradient-position: to left in oklab; background-image: linear-gradient(var(--tw-gradient-stops)); } .bg-gradient-to-r { - --tw-gradient-position: to right in oklab, ; + --tw-gradient-position: to right in oklab; background-image: linear-gradient(var(--tw-gradient-stops)); } .bg-gradient-to-t { - --tw-gradient-position: to top in oklab, ; + --tw-gradient-position: to top in oklab; background-image: linear-gradient(var(--tw-gradient-stops)); } .bg-gradient-to-tl { - --tw-gradient-position: to top left in oklab, ; + --tw-gradient-position: to top left in oklab; background-image: linear-gradient(var(--tw-gradient-stops)); } .bg-gradient-to-tr { - --tw-gradient-position: to top right in oklab, ; + --tw-gradient-position: to top right in oklab; background-image: linear-gradient(var(--tw-gradient-stops)); }" `) diff --git a/packages/tailwindcss/src/compat/legacy-utilities.ts b/packages/tailwindcss/src/compat/legacy-utilities.ts index d86b1770ab6e..5711892d7359 100644 --- a/packages/tailwindcss/src/compat/legacy-utilities.ts +++ b/packages/tailwindcss/src/compat/legacy-utilities.ts @@ -14,7 +14,7 @@ export function registerLegacyUtilities(designSystem: DesignSystem) { ['tl', 'top left'], ]) { designSystem.utilities.static(`bg-gradient-to-${value}`, () => [ - decl('--tw-gradient-position', `to ${direction} in oklab,`), + decl('--tw-gradient-position', `to ${direction} in oklab`), decl('background-image', `linear-gradient(var(--tw-gradient-stops))`), ]) } diff --git a/packages/tailwindcss/src/compat/plugin-api.test.ts b/packages/tailwindcss/src/compat/plugin-api.test.ts index 539c04a3c66c..9eb65ccb954a 100644 --- a/packages/tailwindcss/src/compat/plugin-api.test.ts +++ b/packages/tailwindcss/src/compat/plugin-api.test.ts @@ -1534,6 +1534,38 @@ describe('addBase', () => { " `) }) + + test('does not modify CSS variables', async () => { + let input = css` + @plugin "my-plugin"; + ` + + let compiler = await compile(input, { + loadModule: async () => ({ + module: plugin(function ({ addBase }) { + addBase({ + ':root': { + '--PascalCase': '1', + '--camelCase': '1', + '--UPPERCASE': '1', + }, + }) + }), + base: '/root', + }), + }) + + expect(compiler.build([])).toMatchInlineSnapshot(` + "@layer base { + :root { + --PascalCase: 1; + --camelCase: 1; + --UPPERCASE: 1; + } + } + " + `) + }) }) describe('addVariant', () => { @@ -3135,8 +3167,6 @@ describe('addUtilities()', () => { color: red; } } - } - :root, :host { }" `, ) diff --git a/packages/tailwindcss/src/compat/plugin-api.ts b/packages/tailwindcss/src/compat/plugin-api.ts index 13673280769b..8db4d68973fb 100644 --- a/packages/tailwindcss/src/compat/plugin-api.ts +++ b/packages/tailwindcss/src/compat/plugin-api.ts @@ -499,15 +499,18 @@ export function objectToAst(rules: CssInJs | CssInJs[]): AstNode[] { for (let [name, value] of entries) { if (typeof value !== 'object') { - if (!name.startsWith('--') && value === '@slot') { - ast.push(rule(name, [atRule('@slot')])) - } else { + if (!name.startsWith('--')) { + if (value === '@slot') { + ast.push(rule(name, [atRule('@slot')])) + continue + } + // Convert camelCase to kebab-case: // https://github.com/postcss/postcss-js/blob/b3db658b932b42f6ac14ca0b1d50f50c4569805b/parser.js#L30-L35 name = name.replace(/([A-Z])/g, '-$1').toLowerCase() - - ast.push(decl(name, String(value))) } + + ast.push(decl(name, String(value))) } else if (Array.isArray(value)) { for (let item of value) { if (typeof item === 'string') { diff --git a/packages/tailwindcss/src/compat/screens-config.test.ts b/packages/tailwindcss/src/compat/screens-config.test.ts index 09ab97fc9f8a..e8f96ee55fd3 100644 --- a/packages/tailwindcss/src/compat/screens-config.test.ts +++ b/packages/tailwindcss/src/compat/screens-config.test.ts @@ -655,9 +655,5 @@ test('JS config `screens` can overwrite default CSS `--breakpoint-*`', async () // currently. expect( compiler.build(['min-sm:flex', 'min-md:flex', 'min-lg:flex', 'min-xl:flex', 'min-2xl:flex']), - ).toMatchInlineSnapshot(` - ":root, :host { - } - " - `) + ).toMatchInlineSnapshot(`""`) }) diff --git a/packages/tailwindcss/src/css-parser.test.ts b/packages/tailwindcss/src/css-parser.test.ts index a4b123b28fde..25d5b3a454ca 100644 --- a/packages/tailwindcss/src/css-parser.test.ts +++ b/packages/tailwindcss/src/css-parser.test.ts @@ -329,6 +329,28 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => { ]) }) + it('should parse a custom property with an empty value', () => { + expect(parse('--foo:;')).toEqual([ + { + kind: 'declaration', + property: '--foo', + value: '', + important: false, + }, + ]) + }) + + it('should parse a custom property with a space value', () => { + expect(parse('--foo: ;')).toEqual([ + { + kind: 'declaration', + property: '--foo', + value: '', + important: false, + }, + ]) + }) + it('should parse a custom property with a block including nested "css"', () => { expect( parse(css` @@ -1097,5 +1119,39 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => { `), ).toThrowErrorMatchingInlineSnapshot(`[Error: Unterminated string: "Hello world!;"]`) }) + + it('should error when incomplete custom properties are used', () => { + expect(() => parse('--foo')).toThrowErrorMatchingInlineSnapshot( + `[Error: Invalid custom property, expected a value]`, + ) + }) + + it('should error when incomplete custom properties are used inside rules', () => { + expect(() => parse('.foo { --bar }')).toThrowErrorMatchingInlineSnapshot( + `[Error: Invalid custom property, expected a value]`, + ) + }) + + it('should error when a declaration is incomplete', () => { + expect(() => parse('.foo { bar }')).toThrowErrorMatchingInlineSnapshot( + `[Error: Invalid declaration: \`bar\`]`, + ) + }) + + it('should error when a semicolon exists after an at-rule with a body', () => { + expect(() => parse('@plugin "foo" {} ;')).toThrowErrorMatchingInlineSnapshot( + `[Error: Unexpected semicolon]`, + ) + }) + + it('should error when consecutive semicolons exist', () => { + expect(() => parse(';;;')).toThrowErrorMatchingInlineSnapshot(`[Error: Unexpected semicolon]`) + }) + + it('should error when consecutive semicolons exist after a declaration', () => { + expect(() => parse('.foo { color: red;;; }')).toThrowErrorMatchingInlineSnapshot( + `[Error: Unexpected semicolon]`, + ) + }) }) }) diff --git a/packages/tailwindcss/src/css-parser.ts b/packages/tailwindcss/src/css-parser.ts index c12c63051cee..d80bb3e79807 100644 --- a/packages/tailwindcss/src/css-parser.ts +++ b/packages/tailwindcss/src/css-parser.ts @@ -286,6 +286,8 @@ export function parse(input: string) { } let declaration = parseDeclaration(buffer, colonIdx) + if (!declaration) throw new Error(`Invalid custom property, expected a value`) + if (parent) { parent.nodes.push(declaration) } else { @@ -337,6 +339,11 @@ export function parse(input: string) { closingBracketStack[closingBracketStack.length - 1] !== ')' ) { let declaration = parseDeclaration(buffer) + if (!declaration) { + if (buffer.length === 0) throw new Error('Unexpected semicolon') + throw new Error(`Invalid declaration: \`${buffer.trim()}\``) + } + if (parent) { parent.nodes.push(declaration) } else { @@ -435,7 +442,10 @@ export function parse(input: string) { // Attach the declaration to the parent. if (parent) { - parent.nodes.push(parseDeclaration(buffer, colonIdx)) + let node = parseDeclaration(buffer, colonIdx) + if (!node) throw new Error(`Invalid declaration: \`${buffer.trim()}\``) + + parent.nodes.push(node) } } } @@ -543,7 +553,11 @@ export function parseAtRule(buffer: string, nodes: AstNode[] = []): AtRule { return atRule(buffer.trim(), '', nodes) } -function parseDeclaration(buffer: string, colonIdx: number = buffer.indexOf(':')): Declaration { +function parseDeclaration( + buffer: string, + colonIdx: number = buffer.indexOf(':'), +): Declaration | null { + if (colonIdx === -1) return null let importantIdx = buffer.indexOf('!important', colonIdx + 1) return decl( buffer.slice(0, colonIdx).trim(), diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index 8fba63cfccef..67a0a3d64dab 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -152,6 +152,49 @@ describe('compiling CSS', () => { `) }) + test('unescapes theme variables and handles dots as underscore', async () => { + expect( + await compileCss( + css` + @theme { + --spacing-*: initial; + --spacing-1\.5: 1.5px; + --spacing-2_5: 2.5px; + --spacing-3\.5: 3.5px; + --spacing-3_5: 3.5px; + --spacing-foo\/bar: 3rem; + } + @tailwind utilities; + `, + ['m-1.5', 'm-2.5', 'm-2_5', 'm-3.5', 'm-foo/bar'], + ), + ).toMatchInlineSnapshot(` + ":root, :host { + --spacing-1\\.5: 1.5px; + --spacing-2_5: 2.5px; + --spacing-3\\.5: 3.5px; + --spacing-3_5: 3.5px; + --spacing-foo\\/bar: 3rem; + } + + .m-1\\.5 { + margin: var(--spacing-1\\.5); + } + + .m-2\\.5, .m-2_5 { + margin: var(--spacing-2_5); + } + + .m-3\\.5 { + margin: var(--spacing-3\\.5); + } + + .m-foo\\/bar { + margin: var(--spacing-foo\\/bar); + }" + `) + }) + test('adds vendor prefixes', async () => { expect( await compileCss( @@ -1530,6 +1573,75 @@ describe('Parsing themes values from CSS', () => { `) }) + test('`@keyframes` added in `@theme reference` should not be emitted', async () => { + return expect( + await compileCss( + css` + @theme reference { + --animate-foo: foo 1s infinite; + + @keyframes foo { + 0%, + 100% { + color: red; + } + 50% { + color: blue; + } + } + } + @tailwind utilities; + `, + ['animate-foo'], + ), + ).toMatchInlineSnapshot(` + ".animate-foo { + animation: var(--animate-foo); + }" + `) + }) + + test('`@keyframes` added in `@theme reference` should not be emitted, even if another `@theme` block exists', async () => { + return expect( + await compileCss( + css` + @theme reference { + --animate-foo: foo 1s infinite; + + @keyframes foo { + 0%, + 100% { + color: red; + } + 50% { + color: blue; + } + } + } + + @theme { + --color-pink: pink; + } + + @tailwind utilities; + `, + ['bg-pink', 'animate-foo'], + ), + ).toMatchInlineSnapshot(` + ":root, :host { + --color-pink: pink; + } + + .animate-foo { + animation: var(--animate-foo); + } + + .bg-pink { + background-color: var(--color-pink); + }" + `) + }) + test('theme values added as reference that override existing theme value suppress the output of the original theme value as a variable', async () => { expect( await compileCss( @@ -3398,6 +3510,38 @@ describe('@variant', () => { background: white; } } + + @variant hover { + @variant landscape { + .btn2 { + color: red; + } + } + } + + @variant hover { + .foo { + color: red; + } + @variant landscape { + .bar { + color: blue; + } + } + .baz { + @variant portrait { + color: green; + } + } + } + + @media something { + @variant landscape { + @page { + color: red; + } + } + } `, [], ), @@ -3410,6 +3554,38 @@ describe('@variant', () => { .btn { background: #fff; } + } + + @media (hover: hover) { + @media (orientation: landscape) { + :scope:hover .btn2 { + color: red; + } + } + + :scope:hover .foo { + color: red; + } + + @media (orientation: landscape) { + :scope:hover .bar { + color: #00f; + } + } + + @media (orientation: portrait) { + :scope:hover .baz { + color: green; + } + } + } + + @media something { + @media (orientation: landscape) { + @page { + color: red; + } + } }" `) }) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index df6e09f90649..2baed9acf6a5 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -27,6 +27,7 @@ import * as CSS from './css-parser' import { buildDesignSystem, type DesignSystem } from './design-system' import { Theme, ThemeOptions } from './theme' import { createCssUtility } from './utilities' +import { escape, unescape } from './utils/escape' import { segment } from './utils/segment' import { compoundsForSelectors, IS_VALID_VARIANT_NAME } from './variants' export type Config = UserConfig @@ -243,6 +244,11 @@ async function parseCss( return WalkAction.Stop } }) + + // No `@slot` found, so this is still a regular `@variant` at-rule + if (node.name === '@variant') { + variantNodes.push(node) + } } } @@ -428,6 +434,13 @@ async function parseCss( replaceWith(node.nodes) } + walk(node.nodes, (node) => { + if (node.kind !== 'at-rule') return + if (node.name !== '@variant') return + + variantNodes.push(node) + }) + return WalkAction.Skip } @@ -454,6 +467,12 @@ async function parseCss( // Collect `@keyframes` rules to re-insert with theme variables later, // since the `@theme` rule itself will be removed. if (child.kind === 'at-rule' && child.name === '@keyframes') { + // Do not track/emit `@keyframes`, if they are part of a `@theme reference`. + if (themeOptions & ThemeOptions.REFERENCE) { + replaceWith([]) + return WalkAction.Skip + } + theme.addKeyframes(child) replaceWith([]) return WalkAction.Skip @@ -461,7 +480,7 @@ async function parseCss( if (child.kind === 'comment') return if (child.kind === 'declaration' && child.property.startsWith('--')) { - theme.add(child.property, child.value ?? '', themeOptions) + theme.add(unescape(child.property), child.value ?? '', themeOptions) return } @@ -520,7 +539,7 @@ async function parseCss( for (let [key, value] of theme.entries()) { if (value.options & ThemeOptions.REFERENCE) continue - nodes.push(decl(key, value.value)) + nodes.push(decl(escape(key), value.value)) } let keyframesRules = theme.getKeyframes() diff --git a/packages/tailwindcss/src/theme.ts b/packages/tailwindcss/src/theme.ts index a2d3c205fa4a..0b0ca96c8d11 100644 --- a/packages/tailwindcss/src/theme.ts +++ b/packages/tailwindcss/src/theme.ts @@ -41,10 +41,6 @@ export class Theme { ) {} add(key: string, value: string, options = ThemeOptions.NONE): void { - if (key.endsWith('\\*')) { - key = key.slice(0, -2) + '*' - } - if (key.endsWith('-*')) { if (value !== 'initial') { throw new Error(`Invalid theme value \`${value}\` for namespace \`${key}\``) @@ -149,11 +145,20 @@ export class Theme { #resolveKey(candidateValue: string | null, themeKeys: ThemeKey[]): string | null { for (let namespace of themeKeys) { let themeKey = - candidateValue !== null - ? (escape(`${namespace}-${candidateValue.replaceAll('.', '_')}`) as ThemeKey) - : namespace + candidateValue !== null ? (`${namespace}-${candidateValue}` as ThemeKey) : namespace + + if (!this.values.has(themeKey)) { + // If the exact theme key is not found, we might be trying to resolve a key containing a dot + // that was registered with an underscore instead: + if (candidateValue !== null && candidateValue.includes('.')) { + themeKey = `${namespace}-${candidateValue.replaceAll('.', '_')}` as ThemeKey + + if (!this.values.has(themeKey)) continue + } else { + continue + } + } - if (!this.values.has(themeKey)) continue if (isIgnoredThemeKey(themeKey, namespace)) continue return themeKey @@ -167,7 +172,7 @@ export class Theme { return null } - return `var(${this.#prefixKey(themeKey)})` + return `var(${escape(this.#prefixKey(themeKey))})` } resolve(candidateValue: string | null, themeKeys: ThemeKey[]): string | null { diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index 8e40d3816317..10720b167759 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -6995,6 +6995,7 @@ test('grid-cols', async () => { expect( await run([ 'grid-cols', + 'grid-cols-0', '-grid-cols-none', '-grid-cols-subgrid', 'grid-cols--12', @@ -7043,6 +7044,7 @@ test('grid-rows', async () => { expect( await run([ 'grid-rows', + 'grid-rows-0', '-grid-rows-none', '-grid-rows-subgrid', 'grid-rows--12', @@ -10158,257 +10160,257 @@ test('bg', async () => { } .-bg-conic-45\\/oklab { - --tw-gradient-position: from calc(45 * -1) in oklab, ; + --tw-gradient-position: from calc(45 * -1) in oklab; background-image: conic-gradient(var(--tw-gradient-stops)); } .-bg-linear-45, .-bg-linear-45\\/oklab { - --tw-gradient-position: calc(45deg * -1) in oklab, ; + --tw-gradient-position: calc(45deg * -1) in oklab; background-image: linear-gradient(var(--tw-gradient-stops)); } .-bg-linear-\\[1\\.3rad\\] { - --tw-gradient-position: calc(74.4845deg * -1), ; + --tw-gradient-position: calc(74.4845deg * -1); background-image: linear-gradient(var(--tw-gradient-stops, calc(74.4845deg * -1))); } .-bg-linear-\\[125deg\\] { - --tw-gradient-position: calc(125deg * -1), ; + --tw-gradient-position: calc(125deg * -1); background-image: linear-gradient(var(--tw-gradient-stops, calc(125deg * -1))); } .bg-conic-45\\/\\[in_hsl_longer_hue\\] { - --tw-gradient-position: from 45deg in hsl longer hue, ; + --tw-gradient-position: from 45deg in hsl longer hue; background-image: conic-gradient(var(--tw-gradient-stops)); } .bg-conic-45\\/oklab { - --tw-gradient-position: from 45deg in oklab, ; + --tw-gradient-position: from 45deg in oklab; background-image: conic-gradient(var(--tw-gradient-stops)); } .bg-conic-45\\/shorter { - --tw-gradient-position: from 45deg in oklch shorter hue, ; + --tw-gradient-position: from 45deg in oklch shorter hue; background-image: conic-gradient(var(--tw-gradient-stops)); } .bg-conic\\/\\[in_hsl_longer_hue\\] { - --tw-gradient-position: in hsl longer hue, ; + --tw-gradient-position: in hsl longer hue; background-image: conic-gradient(var(--tw-gradient-stops)); } .bg-conic\\/decreasing { - --tw-gradient-position: in oklch decreasing hue, ; + --tw-gradient-position: in oklch decreasing hue; background-image: conic-gradient(var(--tw-gradient-stops)); } .bg-conic\\/hsl { - --tw-gradient-position: in hsl, ; + --tw-gradient-position: in hsl; background-image: conic-gradient(var(--tw-gradient-stops)); } .bg-conic\\/increasing { - --tw-gradient-position: in oklch increasing hue, ; + --tw-gradient-position: in oklch increasing hue; background-image: conic-gradient(var(--tw-gradient-stops)); } .bg-conic\\/longer { - --tw-gradient-position: in oklch longer hue, ; + --tw-gradient-position: in oklch longer hue; background-image: conic-gradient(var(--tw-gradient-stops)); } .bg-conic\\/oklab { - --tw-gradient-position: in oklab, ; + --tw-gradient-position: in oklab; background-image: conic-gradient(var(--tw-gradient-stops)); } .bg-conic\\/oklch { - --tw-gradient-position: in oklch, ; + --tw-gradient-position: in oklch; background-image: conic-gradient(var(--tw-gradient-stops)); } .bg-conic\\/shorter { - --tw-gradient-position: in oklch shorter hue, ; + --tw-gradient-position: in oklch shorter hue; background-image: conic-gradient(var(--tw-gradient-stops)); } .bg-conic\\/srgb { - --tw-gradient-position: in srgb, ; + --tw-gradient-position: in srgb; background-image: conic-gradient(var(--tw-gradient-stops)); } .bg-linear-45 { - --tw-gradient-position: 45deg in oklab, ; + --tw-gradient-position: 45deg in oklab; background-image: linear-gradient(var(--tw-gradient-stops)); } .bg-linear-45\\/\\[in_hsl_longer_hue\\] { - --tw-gradient-position: 45deg in hsl longer hue, ; + --tw-gradient-position: 45deg in hsl longer hue; background-image: linear-gradient(var(--tw-gradient-stops)); } .bg-linear-45\\/oklab { - --tw-gradient-position: 45deg in oklab, ; + --tw-gradient-position: 45deg in oklab; background-image: linear-gradient(var(--tw-gradient-stops)); } .bg-linear-45\\/shorter { - --tw-gradient-position: 45deg in oklch shorter hue, ; + --tw-gradient-position: 45deg in oklch shorter hue; background-image: linear-gradient(var(--tw-gradient-stops)); } .bg-linear-\\[1\\.3rad\\] { - --tw-gradient-position: 74.4845deg, ; + --tw-gradient-position: 74.4845deg; background-image: linear-gradient(var(--tw-gradient-stops, 74.4845deg)); } .bg-linear-\\[125deg\\] { - --tw-gradient-position: 125deg, ; + --tw-gradient-position: 125deg; background-image: linear-gradient(var(--tw-gradient-stops, 125deg)); } .bg-linear-\\[to_bottom\\] { - --tw-gradient-position: to bottom, ; + --tw-gradient-position: to bottom; background-image: linear-gradient(var(--tw-gradient-stops, to bottom)); } .bg-linear-to-b { - --tw-gradient-position: to bottom in oklab, ; + --tw-gradient-position: to bottom in oklab; background-image: linear-gradient(var(--tw-gradient-stops)); } .bg-linear-to-bl { - --tw-gradient-position: to bottom left in oklab, ; + --tw-gradient-position: to bottom left in oklab; background-image: linear-gradient(var(--tw-gradient-stops)); } .bg-linear-to-br { - --tw-gradient-position: to bottom right in oklab, ; + --tw-gradient-position: to bottom right in oklab; background-image: linear-gradient(var(--tw-gradient-stops)); } .bg-linear-to-l { - --tw-gradient-position: to left in oklab, ; + --tw-gradient-position: to left in oklab; background-image: linear-gradient(var(--tw-gradient-stops)); } .bg-linear-to-r { - --tw-gradient-position: to right in oklab, ; + --tw-gradient-position: to right in oklab; background-image: linear-gradient(var(--tw-gradient-stops)); } .bg-linear-to-r\\/\\[in_hsl_longer_hue\\] { - --tw-gradient-position: to right in hsl longer hue, ; + --tw-gradient-position: to right in hsl longer hue; background-image: linear-gradient(var(--tw-gradient-stops)); } .bg-linear-to-r\\/\\[longer\\] { - --tw-gradient-position: to right longer, ; + --tw-gradient-position: to right longer; background-image: linear-gradient(var(--tw-gradient-stops)); } .bg-linear-to-r\\/decreasing { - --tw-gradient-position: to right in oklch decreasing hue, ; + --tw-gradient-position: to right in oklch decreasing hue; background-image: linear-gradient(var(--tw-gradient-stops)); } .bg-linear-to-r\\/hsl { - --tw-gradient-position: to right in hsl, ; + --tw-gradient-position: to right in hsl; background-image: linear-gradient(var(--tw-gradient-stops)); } .bg-linear-to-r\\/increasing { - --tw-gradient-position: to right in oklch increasing hue, ; + --tw-gradient-position: to right in oklch increasing hue; background-image: linear-gradient(var(--tw-gradient-stops)); } .bg-linear-to-r\\/longer { - --tw-gradient-position: to right in oklch longer hue, ; + --tw-gradient-position: to right in oklch longer hue; background-image: linear-gradient(var(--tw-gradient-stops)); } .bg-linear-to-r\\/oklab { - --tw-gradient-position: to right in oklab, ; + --tw-gradient-position: to right in oklab; background-image: linear-gradient(var(--tw-gradient-stops)); } .bg-linear-to-r\\/oklch { - --tw-gradient-position: to right in oklch, ; + --tw-gradient-position: to right in oklch; background-image: linear-gradient(var(--tw-gradient-stops)); } .bg-linear-to-r\\/shorter { - --tw-gradient-position: to right in oklch shorter hue, ; + --tw-gradient-position: to right in oklch shorter hue; background-image: linear-gradient(var(--tw-gradient-stops)); } .bg-linear-to-r\\/srgb { - --tw-gradient-position: to right in srgb, ; + --tw-gradient-position: to right in srgb; background-image: linear-gradient(var(--tw-gradient-stops)); } .bg-linear-to-t { - --tw-gradient-position: to top in oklab, ; + --tw-gradient-position: to top in oklab; background-image: linear-gradient(var(--tw-gradient-stops)); } .bg-linear-to-tl { - --tw-gradient-position: to top left in oklab, ; + --tw-gradient-position: to top left in oklab; background-image: linear-gradient(var(--tw-gradient-stops)); } .bg-linear-to-tr { - --tw-gradient-position: to top right in oklab, ; + --tw-gradient-position: to top right in oklab; background-image: linear-gradient(var(--tw-gradient-stops)); } .bg-radial-\\[circle_at_center\\] { - --tw-gradient-position: circle at center, ; + --tw-gradient-position: circle at center; background-image: radial-gradient(var(--tw-gradient-stops, circle at center)); } .bg-radial\\/\\[in_hsl_longer_hue\\] { - --tw-gradient-position: in hsl longer hue, ; + --tw-gradient-position: in hsl longer hue; background-image: radial-gradient(var(--tw-gradient-stops)); } .bg-radial\\/decreasing { - --tw-gradient-position: in oklch decreasing hue, ; + --tw-gradient-position: in oklch decreasing hue; background-image: radial-gradient(var(--tw-gradient-stops)); } .bg-radial\\/hsl { - --tw-gradient-position: in hsl, ; + --tw-gradient-position: in hsl; background-image: radial-gradient(var(--tw-gradient-stops)); } .bg-radial\\/increasing { - --tw-gradient-position: in oklch increasing hue, ; + --tw-gradient-position: in oklch increasing hue; background-image: radial-gradient(var(--tw-gradient-stops)); } .bg-radial\\/longer { - --tw-gradient-position: in oklch longer hue, ; + --tw-gradient-position: in oklch longer hue; background-image: radial-gradient(var(--tw-gradient-stops)); } .bg-radial\\/oklab { - --tw-gradient-position: in oklab, ; + --tw-gradient-position: in oklab; background-image: radial-gradient(var(--tw-gradient-stops)); } .bg-radial\\/oklch { - --tw-gradient-position: in oklch, ; + --tw-gradient-position: in oklch; background-image: radial-gradient(var(--tw-gradient-stops)); } .bg-radial\\/shorter { - --tw-gradient-position: in oklch shorter hue, ; + --tw-gradient-position: in oklch shorter hue; background-image: radial-gradient(var(--tw-gradient-stops)); } .bg-radial\\/srgb { - --tw-gradient-position: in srgb, ; + --tw-gradient-position: in srgb; background-image: radial-gradient(var(--tw-gradient-stops)); } @@ -10708,62 +10710,62 @@ test('from', async () => { .from-\\[\\#0088cc\\] { --tw-gradient-from: #08c; - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } .from-\\[\\#0088cc\\]\\/50, .from-\\[\\#0088cc\\]\\/\\[0\\.5\\], .from-\\[\\#0088cc\\]\\/\\[50\\%\\] { --tw-gradient-from: oklab(59.9824% -.06725 -.12414 / .5); - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } .from-\\[color\\:var\\(--my-color\\)\\] { --tw-gradient-from: var(--my-color); - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } .from-\\[color\\:var\\(--my-color\\)\\]\\/50, .from-\\[color\\:var\\(--my-color\\)\\]\\/\\[0\\.5\\], .from-\\[color\\:var\\(--my-color\\)\\]\\/\\[50\\%\\] { --tw-gradient-from: color-mix(in oklab, var(--my-color) 50%, transparent); - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } .from-\\[var\\(--my-color\\)\\] { --tw-gradient-from: var(--my-color); - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } .from-\\[var\\(--my-color\\)\\]\\/50, .from-\\[var\\(--my-color\\)\\]\\/\\[0\\.5\\], .from-\\[var\\(--my-color\\)\\]\\/\\[50\\%\\] { --tw-gradient-from: color-mix(in oklab, var(--my-color) 50%, transparent); - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } .from-current { --tw-gradient-from: currentColor; - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } .from-current\\/50, .from-current\\/\\[0\\.5\\], .from-current\\/\\[50\\%\\] { --tw-gradient-from: color-mix(in oklab, currentColor 50%, transparent); - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } .from-inherit { --tw-gradient-from: inherit; - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } .from-red-500 { --tw-gradient-from: var(--color-red-500); - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } .from-red-500\\/50, .from-red-500\\/\\[0\\.5\\], .from-red-500\\/\\[50\\%\\] { --tw-gradient-from: color-mix(in oklab, var(--color-red-500) 50%, transparent); - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } .from-transparent { --tw-gradient-from: transparent; - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } .from-0\\% { @@ -10927,73 +10929,73 @@ test('via', async () => { .via-\\[\\#0088cc\\] { --tw-gradient-via: #08c; - --tw-gradient-via-stops: var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); + --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); --tw-gradient-stops: var(--tw-gradient-via-stops); } .via-\\[\\#0088cc\\]\\/50, .via-\\[\\#0088cc\\]\\/\\[0\\.5\\], .via-\\[\\#0088cc\\]\\/\\[50\\%\\] { --tw-gradient-via: oklab(59.9824% -.06725 -.12414 / .5); - --tw-gradient-via-stops: var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); + --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); --tw-gradient-stops: var(--tw-gradient-via-stops); } .via-\\[color\\:var\\(--my-color\\)\\] { --tw-gradient-via: var(--my-color); - --tw-gradient-via-stops: var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); + --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); --tw-gradient-stops: var(--tw-gradient-via-stops); } .via-\\[color\\:var\\(--my-color\\)\\]\\/50, .via-\\[color\\:var\\(--my-color\\)\\]\\/\\[0\\.5\\], .via-\\[color\\:var\\(--my-color\\)\\]\\/\\[50\\%\\] { --tw-gradient-via: color-mix(in oklab, var(--my-color) 50%, transparent); - --tw-gradient-via-stops: var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); + --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); --tw-gradient-stops: var(--tw-gradient-via-stops); } .via-\\[var\\(--my-color\\)\\] { --tw-gradient-via: var(--my-color); - --tw-gradient-via-stops: var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); + --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); --tw-gradient-stops: var(--tw-gradient-via-stops); } .via-\\[var\\(--my-color\\)\\]\\/50, .via-\\[var\\(--my-color\\)\\]\\/\\[0\\.5\\], .via-\\[var\\(--my-color\\)\\]\\/\\[50\\%\\] { --tw-gradient-via: color-mix(in oklab, var(--my-color) 50%, transparent); - --tw-gradient-via-stops: var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); + --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); --tw-gradient-stops: var(--tw-gradient-via-stops); } .via-current { --tw-gradient-via: currentColor; - --tw-gradient-via-stops: var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); + --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); --tw-gradient-stops: var(--tw-gradient-via-stops); } .via-current\\/50, .via-current\\/\\[0\\.5\\], .via-current\\/\\[50\\%\\] { --tw-gradient-via: color-mix(in oklab, currentColor 50%, transparent); - --tw-gradient-via-stops: var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); + --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); --tw-gradient-stops: var(--tw-gradient-via-stops); } .via-inherit { --tw-gradient-via: inherit; - --tw-gradient-via-stops: var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); + --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); --tw-gradient-stops: var(--tw-gradient-via-stops); } .via-red-500 { --tw-gradient-via: var(--color-red-500); - --tw-gradient-via-stops: var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); + --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); --tw-gradient-stops: var(--tw-gradient-via-stops); } .via-red-500\\/50, .via-red-500\\/\\[0\\.5\\], .via-red-500\\/\\[50\\%\\] { --tw-gradient-via: color-mix(in oklab, var(--color-red-500) 50%, transparent); - --tw-gradient-via-stops: var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); + --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); --tw-gradient-stops: var(--tw-gradient-via-stops); } .via-transparent { --tw-gradient-via: transparent; - --tw-gradient-via-stops: var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); + --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); --tw-gradient-stops: var(--tw-gradient-via-stops); } @@ -11156,62 +11158,62 @@ test('to', async () => { .to-\\[\\#0088cc\\] { --tw-gradient-to: #08c; - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } .to-\\[\\#0088cc\\]\\/50, .to-\\[\\#0088cc\\]\\/\\[0\\.5\\], .to-\\[\\#0088cc\\]\\/\\[50\\%\\] { --tw-gradient-to: oklab(59.9824% -.06725 -.12414 / .5); - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } .to-\\[color\\:var\\(--my-color\\)\\] { --tw-gradient-to: var(--my-color); - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } .to-\\[color\\:var\\(--my-color\\)\\]\\/50, .to-\\[color\\:var\\(--my-color\\)\\]\\/\\[0\\.5\\], .to-\\[color\\:var\\(--my-color\\)\\]\\/\\[50\\%\\] { --tw-gradient-to: color-mix(in oklab, var(--my-color) 50%, transparent); - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } .to-\\[var\\(--my-color\\)\\] { --tw-gradient-to: var(--my-color); - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } .to-\\[var\\(--my-color\\)\\]\\/50, .to-\\[var\\(--my-color\\)\\]\\/\\[0\\.5\\], .to-\\[var\\(--my-color\\)\\]\\/\\[50\\%\\] { --tw-gradient-to: color-mix(in oklab, var(--my-color) 50%, transparent); - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } .to-current { --tw-gradient-to: currentColor; - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } .to-current\\/50, .to-current\\/\\[0\\.5\\], .to-current\\/\\[50\\%\\] { --tw-gradient-to: color-mix(in oklab, currentColor 50%, transparent); - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } .to-inherit { --tw-gradient-to: inherit; - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } .to-red-500 { --tw-gradient-to: var(--color-red-500); - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } .to-red-500\\/50, .to-red-500\\/\\[0\\.5\\], .to-red-500\\/\\[50\\%\\] { --tw-gradient-to: color-mix(in oklab, var(--color-red-500) 50%, transparent); - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } .to-transparent { --tw-gradient-to: transparent; - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position, ) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } .to-0\\% { diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index 2fb717fae418..9f4a1039be95 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -17,6 +17,7 @@ import { DefaultMap } from './utils/default-map' import { inferDataType, isPositiveInteger, + isStrictPositiveInteger, isValidOpacityValue, isValidSpacingMultiplier, } from './utils/infer-data-type' @@ -1752,7 +1753,7 @@ export function createUtilities(theme: Theme) { functionalUtility('grid-cols', { themeKeys: ['--grid-template-columns'], handleBareValue: ({ value }) => { - if (!isPositiveInteger(value)) return null + if (!isStrictPositiveInteger(value)) return null return `repeat(${value}, minmax(0, 1fr))` }, handle: (value) => [decl('grid-template-columns', value)], @@ -1763,7 +1764,7 @@ export function createUtilities(theme: Theme) { functionalUtility('grid-rows', { themeKeys: ['--grid-template-rows'], handleBareValue: ({ value }) => { - if (!isPositiveInteger(value)) return null + if (!isStrictPositiveInteger(value)) return null return `repeat(${value}, minmax(0, 1fr))` }, handle: (value) => [decl('grid-template-rows', value)], @@ -2369,7 +2370,7 @@ export function createUtilities(theme: Theme) { value = negative ? `calc(${value} * -1)` : `${value}` return [ - decl('--tw-gradient-position', `${value},`), + decl('--tw-gradient-position', value), decl('background-image', `linear-gradient(var(--tw-gradient-stops,${value}))`), ] } @@ -2377,7 +2378,7 @@ export function createUtilities(theme: Theme) { if (negative) return return [ - decl('--tw-gradient-position', `${value},`), + decl('--tw-gradient-position', value), decl('background-image', `linear-gradient(var(--tw-gradient-stops,${value}))`), ] } @@ -2397,7 +2398,7 @@ export function createUtilities(theme: Theme) { let interpolationMethod = resolveInterpolationModifier(candidate.modifier) return [ - decl('--tw-gradient-position', `${value} ${interpolationMethod},`), + decl('--tw-gradient-position', `${value} ${interpolationMethod}`), decl('background-image', `linear-gradient(var(--tw-gradient-stops))`), ] } @@ -2424,7 +2425,7 @@ export function createUtilities(theme: Theme) { if (candidate.modifier) return let value = candidate.value.value return [ - decl('--tw-gradient-position', `${value},`), + decl('--tw-gradient-position', value), decl('background-image', `conic-gradient(var(--tw-gradient-stops,${value}))`), ] } @@ -2433,7 +2434,7 @@ export function createUtilities(theme: Theme) { if (!candidate.value) { return [ - decl('--tw-gradient-position', `${interpolationMethod},`), + decl('--tw-gradient-position', interpolationMethod), decl('background-image', `conic-gradient(var(--tw-gradient-stops))`), ] } @@ -2445,7 +2446,7 @@ export function createUtilities(theme: Theme) { value = negative ? `calc(${value} * -1)` : `${value}deg` return [ - decl('--tw-gradient-position', `from ${value} ${interpolationMethod},`), + decl('--tw-gradient-position', `from ${value} ${interpolationMethod}`), decl('background-image', `conic-gradient(var(--tw-gradient-stops))`), ] } @@ -2470,7 +2471,7 @@ export function createUtilities(theme: Theme) { if (!candidate.value) { let interpolationMethod = resolveInterpolationModifier(candidate.modifier) return [ - decl('--tw-gradient-position', `${interpolationMethod},`), + decl('--tw-gradient-position', interpolationMethod), decl('background-image', `radial-gradient(var(--tw-gradient-stops))`), ] } @@ -2479,7 +2480,7 @@ export function createUtilities(theme: Theme) { if (candidate.modifier) return let value = candidate.value.value return [ - decl('--tw-gradient-position', `${value},`), + decl('--tw-gradient-position', value), decl('background-image', `radial-gradient(var(--tw-gradient-stops,${value}))`), ] } @@ -2654,7 +2655,7 @@ export function createUtilities(theme: Theme) { decl('--tw-gradient-from', value), decl( '--tw-gradient-stops', - 'var(--tw-gradient-via-stops, var(--tw-gradient-position,) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))', + 'var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))', ), ], position: (value) => [gradientStopProperties(), decl('--tw-gradient-from-position', value)], @@ -2667,7 +2668,7 @@ export function createUtilities(theme: Theme) { decl('--tw-gradient-via', value), decl( '--tw-gradient-via-stops', - 'var(--tw-gradient-position,) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position)', + 'var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position)', ), decl('--tw-gradient-stops', 'var(--tw-gradient-via-stops)'), ], @@ -2680,7 +2681,7 @@ export function createUtilities(theme: Theme) { decl('--tw-gradient-to', value), decl( '--tw-gradient-stops', - 'var(--tw-gradient-via-stops, var(--tw-gradient-position,) var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))', + 'var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))', ), ], position: (value) => [gradientStopProperties(), decl('--tw-gradient-to-position', value)], diff --git a/packages/tailwindcss/src/utils/infer-data-type.ts b/packages/tailwindcss/src/utils/infer-data-type.ts index 56ab9c60cdde..9496986557e5 100644 --- a/packages/tailwindcss/src/utils/infer-data-type.ts +++ b/packages/tailwindcss/src/utils/infer-data-type.ts @@ -341,6 +341,11 @@ export function isPositiveInteger(value: any) { return Number.isInteger(num) && num >= 0 && String(num) === String(value) } +export function isStrictPositiveInteger(value: any) { + let num = Number(value) + return Number.isInteger(num) && num > 0 && String(num) === String(value) +} + export function isValidSpacingMultiplier(value: any) { return isMultipleOf(value, 0.25) } diff --git a/packages/tailwindcss/tests/ui.spec.ts b/packages/tailwindcss/tests/ui.spec.ts index 2595d9982b93..53d8a71c9a48 100644 --- a/packages/tailwindcss/tests/ui.spec.ts +++ b/packages/tailwindcss/tests/ui.spec.ts @@ -157,6 +157,10 @@ for (let [classes, expected] of [ 'bg-radial-[at_0%_0%,var(--color-red),transparent]', 'radial-gradient(at 0% 0%, rgb(255, 0, 0), rgba(0, 0, 0, 0))', ], + [ + 'bg-radial-[at_center] from-red to-green', + 'radial-gradient(rgb(255, 0, 0) 0%, rgb(0, 255, 0) 100%)', + ], ]) { test(`radial gradient, "${classes}"`, async ({ page }) => { let { getPropertyValue } = await render( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3d70043b4ac5..4f4852d1daa2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,8 +29,8 @@ importers: .: devDependencies: '@playwright/test': - specifier: ^1.49.1 - version: 1.49.1 + specifier: ^1.50.0 + version: 1.50.0 '@types/node': specifier: 'catalog:' version: 20.14.13 @@ -53,8 +53,8 @@ importers: specifier: ^8.2.4 version: 8.2.4(jiti@2.4.2)(postcss@8.5.1)(tsx@4.19.1)(typescript@5.5.4)(yaml@2.6.0) turbo: - specifier: ^2.3.3 - version: 2.3.3 + specifier: ^2.3.4 + version: 2.3.4 typescript: specifier: ^5.5.4 version: 5.5.4 @@ -406,7 +406,7 @@ importers: version: 3.3.3 next: specifier: 15.1.4 - version: 15.1.4(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 15.1.4(@playwright/test@1.50.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: specifier: ^19.0.0 version: 19.0.0 @@ -440,7 +440,7 @@ importers: dependencies: next: specifier: 15.1.4 - version: 15.1.4(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 15.1.4(@playwright/test@1.50.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: specifier: ^19.0.0 version: 19.0.0 @@ -1612,8 +1612,8 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@playwright/test@1.49.1': - resolution: {integrity: sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==} + '@playwright/test@1.50.0': + resolution: {integrity: sha512-ZGNXbt+d65EGjBORQHuYKj+XhCewlwpnSd/EDuLPZGSiEWmgOJB5RmMCCYGy5aMfTs9wx61RivfDKi8H/hcMvw==} engines: {node: '>=18'} hasBin: true @@ -3301,13 +3301,13 @@ packages: pkg-types@1.3.0: resolution: {integrity: sha512-kS7yWjVFCkIw9hqdJBoMxDdzEngmkr5FXeWZZfQ6GoYacjVnsW6l2CcYW/0ThD0vF4LPJgVYnrg4d0uuhwYQbg==} - playwright-core@1.49.1: - resolution: {integrity: sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==} + playwright-core@1.50.0: + resolution: {integrity: sha512-CXkSSlr4JaZs2tZHI40DsZUN/NIwgaUPsyLuOAaIZp2CyF2sN5MM5NJsyB188lFSSozFxQ5fPT4qM+f0tH/6wQ==} engines: {node: '>=18'} hasBin: true - playwright@1.49.1: - resolution: {integrity: sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==} + playwright@1.50.0: + resolution: {integrity: sha512-+GinGfGTrd2IfX1TA4N2gNmeIksSb+IAe589ZH+FlmpV3MYTx6+buChGIuDLQwrGNCw2lWibqV50fU510N7S+w==} engines: {node: '>=18'} hasBin: true @@ -3396,10 +3396,6 @@ packages: resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.4.49: - resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.5.1: resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==} engines: {node: ^10 || ^12 || >=14} @@ -3795,38 +3791,38 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - turbo-darwin-64@2.3.3: - resolution: {integrity: sha512-bxX82xe6du/3rPmm4aCC5RdEilIN99VUld4HkFQuw+mvFg6darNBuQxyWSHZTtc25XgYjQrjsV05888w1grpaA==} + turbo-darwin-64@2.3.4: + resolution: {integrity: sha512-uOi/cUIGQI7uakZygH+cZQ5D4w+aMLlVCN2KTGot+cmefatps2ZmRRufuHrEM0Rl63opdKD8/JIu+54s25qkfg==} cpu: [x64] os: [darwin] - turbo-darwin-arm64@2.3.3: - resolution: {integrity: sha512-DYbQwa3NsAuWkCUYVzfOUBbSUBVQzH5HWUFy2Kgi3fGjIWVZOFk86ss+xsWu//rlEAfYwEmopigsPYSmW4X15A==} + turbo-darwin-arm64@2.3.4: + resolution: {integrity: sha512-IIM1Lq5R+EGMtM1YFGl4x8Xkr0MWb4HvyU8N4LNoQ1Be5aycrOE+VVfH+cDg/Q4csn+8bxCOxhRp5KmUflrVTQ==} cpu: [arm64] os: [darwin] - turbo-linux-64@2.3.3: - resolution: {integrity: sha512-eHj9OIB0dFaP6BxB88jSuaCLsOQSYWBgmhy2ErCu6D2GG6xW3b6e2UWHl/1Ho9FsTg4uVgo4DB9wGsKa5erjUA==} + turbo-linux-64@2.3.4: + resolution: {integrity: sha512-1aD2EfR7NfjFXNH3mYU5gybLJEFi2IGOoKwoPLchAFRQ6OEJQj201/oNo9CDL75IIrQo64/NpEgVyZtoPlfhfA==} cpu: [x64] os: [linux] - turbo-linux-arm64@2.3.3: - resolution: {integrity: sha512-NmDE/NjZoDj1UWBhMtOPmqFLEBKhzGS61KObfrDEbXvU3lekwHeoPvAMfcovzswzch+kN2DrtbNIlz+/rp8OCg==} + turbo-linux-arm64@2.3.4: + resolution: {integrity: sha512-MxTpdKwxCaA5IlybPxgGLu54fT2svdqTIxRd0TQmpRJIjM0s4kbM+7YiLk0mOh6dGqlWPUsxz/A0Mkn8Xr5o7Q==} cpu: [arm64] os: [linux] - turbo-windows-64@2.3.3: - resolution: {integrity: sha512-O2+BS4QqjK3dOERscXqv7N2GXNcqHr9hXumkMxDj/oGx9oCatIwnnwx34UmzodloSnJpgSqjl8iRWiY65SmYoQ==} + turbo-windows-64@2.3.4: + resolution: {integrity: sha512-yyCrWqcRGu1AOOlrYzRnizEtdkqi+qKP0MW9dbk9OsMDXaOI5jlWtTY/AtWMkLw/czVJ7yS9Ex1vi9DB6YsFvw==} cpu: [x64] os: [win32] - turbo-windows-arm64@2.3.3: - resolution: {integrity: sha512-dW4ZK1r6XLPNYLIKjC4o87HxYidtRRcBeo/hZ9Wng2XM/MqqYkAyzJXJGgRMsc0MMEN9z4+ZIfnSNBrA0b08ag==} + turbo-windows-arm64@2.3.4: + resolution: {integrity: sha512-PggC3qH+njPfn1PDVwKrQvvQby8X09ufbqZ2Ha4uSu+5TvPorHHkAbZVHKYj2Y+tvVzxRzi4Sv6NdHXBS9Be5w==} cpu: [arm64] os: [win32] - turbo@2.3.3: - resolution: {integrity: sha512-DUHWQAcC8BTiUZDRzAYGvpSpGLiaOQPfYXlCieQbwUvmml/LRGIe3raKdrOPOoiX0DYlzxs2nH6BoWJoZrj8hA==} + turbo@2.3.4: + resolution: {integrity: sha512-1kiLO5C0Okh5ay1DbHsxkPsw9Sjsbjzm6cF85CpWjR0BIyBFNDbKqtUyqGADRS1dbbZoQanJZVj4MS5kk8J42Q==} hasBin: true type-check@0.4.0: @@ -4792,9 +4788,9 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@playwright/test@1.49.1': + '@playwright/test@1.50.0': dependencies: - playwright: 1.49.1 + playwright: 1.50.0 '@rollup/rollup-android-arm-eabi@4.20.0': optional: true @@ -6632,7 +6628,7 @@ snapshots: natural-compare@1.4.0: {} - next@15.1.4(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + next@15.1.4(@playwright/test@1.50.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: '@next/env': 15.1.4 '@swc/counter': 0.1.3 @@ -6652,7 +6648,7 @@ snapshots: '@next/swc-linux-x64-musl': 15.1.4 '@next/swc-win32-arm64-msvc': 15.1.4 '@next/swc-win32-x64-msvc': 15.1.4 - '@playwright/test': 1.49.1 + '@playwright/test': 1.50.0 sharp: 0.33.5 transitivePeerDependencies: - '@babel/core' @@ -6798,11 +6794,11 @@ snapshots: mlly: 1.7.3 pathe: 1.1.2 - playwright-core@1.49.1: {} + playwright-core@1.50.0: {} - playwright@1.49.1: + playwright@1.50.0: dependencies: - playwright-core: 1.49.1 + playwright-core: 1.50.0 optionalDependencies: fsevents: 2.3.2 @@ -6890,12 +6886,6 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.4.49: - dependencies: - nanoid: 3.3.7 - picocolors: 1.1.1 - source-map-js: 1.2.1 - postcss@8.5.1: dependencies: nanoid: 3.3.8 @@ -7388,32 +7378,32 @@ snapshots: fsevents: 2.3.3 optional: true - turbo-darwin-64@2.3.3: + turbo-darwin-64@2.3.4: optional: true - turbo-darwin-arm64@2.3.3: + turbo-darwin-arm64@2.3.4: optional: true - turbo-linux-64@2.3.3: + turbo-linux-64@2.3.4: optional: true - turbo-linux-arm64@2.3.3: + turbo-linux-arm64@2.3.4: optional: true - turbo-windows-64@2.3.3: + turbo-windows-64@2.3.4: optional: true - turbo-windows-arm64@2.3.3: + turbo-windows-arm64@2.3.4: optional: true - turbo@2.3.3: + turbo@2.3.4: optionalDependencies: - turbo-darwin-64: 2.3.3 - turbo-darwin-arm64: 2.3.3 - turbo-linux-64: 2.3.3 - turbo-linux-arm64: 2.3.3 - turbo-windows-64: 2.3.3 - turbo-windows-arm64: 2.3.3 + turbo-darwin-64: 2.3.4 + turbo-darwin-arm64: 2.3.4 + turbo-linux-64: 2.3.4 + turbo-linux-arm64: 2.3.4 + turbo-windows-64: 2.3.4 + turbo-windows-arm64: 2.3.4 type-check@0.4.0: dependencies: @@ -7532,7 +7522,7 @@ snapshots: vite@6.0.0(@types/node@20.14.13)(jiti@2.4.2)(lightningcss@1.29.1(patch_hash=gkqcezdn4goium3e3s43dhy4by))(terser@5.31.6)(tsx@4.19.1)(yaml@2.6.0): dependencies: esbuild: 0.24.0 - postcss: 8.4.49 + postcss: 8.5.1 rollup: 4.27.4 optionalDependencies: '@types/node': 20.14.13