Compare commits
4 commits
main
...
graphite-b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15ec670306 | ||
|
|
56608a5ddf | ||
|
|
db4256f47e | ||
|
|
f99717b1cc |
10 changed files with 325 additions and 13 deletions
|
|
@ -20,12 +20,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Ensure color opacity modifiers work with OKLCH colors ([#14741](https://github.com/tailwindlabs/tailwindcss/pull/14741))
|
||||
- Ensure changes to the input CSS file result in a full rebuild ([#14744](https://github.com/tailwindlabs/tailwindcss/pull/14744))
|
||||
- Add `postcss` as a dependency of `@tailwindcss/postcss` ([#14750](https://github.com/tailwindlabs/tailwindcss/pull/14750))
|
||||
- Ensure the JS `theme()` function can reference CSS theme variables that contain special characters without escaping them (e.g. referencing `--width-1\/2` as `theme('width.1/2')`) ([#14739](https://github.com/tailwindlabs/tailwindcss/pull/14739))
|
||||
- Ensure JS theme keys containing special characters correctly produce utility classes (e.g. `'1/2': 50%` to `w-1/2`) ([#14739](https://github.com/tailwindlabs/tailwindcss/pull/14739))
|
||||
- Ensure loading stylesheets via the `?raw` and `?url` static asset query works when using the Vite plugin ([#14716](https://github.com/tailwindlabs/tailwindcss/pull/14716))
|
||||
- _Upgrade (experimental)_: Migrate `flex-grow` to `grow` and `flex-shrink` to `shrink` ([#14721](https://github.com/tailwindlabs/tailwindcss/pull/14721))
|
||||
- _Upgrade (experimental)_: Minify arbitrary values when printing candidates ([#14720](https://github.com/tailwindlabs/tailwindcss/pull/14720))
|
||||
- _Upgrade (experimental)_: Ensure legacy theme values ending in `1` (like `theme(spacing.1)`) are correctly migrated to custom properties ([#14724](https://github.com/tailwindlabs/tailwindcss/pull/14724))
|
||||
- _Upgrade (experimental)_: Migrate arbitrary values to bare values for the `from-*`, `via-*`, and `to-*` utilities ([#14725](https://github.com/tailwindlabs/tailwindcss/pull/14725))
|
||||
- _Upgrade (experimental)_: Ensure `layer(utilities)` is removed from `@import` to keep `@utility` top-level ([#14738](https://github.com/tailwindlabs/tailwindcss/pull/14738))
|
||||
- _Upgrade (experimental)_: Ensure JS theme keys with special characters are escaped when migrated to CSS variables ([#14736](https://github.com/tailwindlabs/tailwindcss/pull/14736))
|
||||
- _Upgrade (experimental)_: Don't migrate important modifiers that are actually logical negations (e.g. `let foo = !border` to `let foo = border!`) ([#14737](https://github.com/tailwindlabs/tailwindcss/pull/14737))
|
||||
|
||||
### Changed
|
||||
|
|
|
|||
|
|
@ -34,6 +34,57 @@ test(
|
|||
sm: ['0.875rem', { lineHeight: '1.5rem' }],
|
||||
base: ['1rem', { lineHeight: '2rem' }],
|
||||
},
|
||||
width: {
|
||||
px: '1px',
|
||||
auto: 'auto',
|
||||
1: '0.25rem',
|
||||
1.5: '0.375rem',
|
||||
2: '0.5rem',
|
||||
2.5: '0.625rem',
|
||||
3: '0.75rem',
|
||||
3.5: '0.875rem',
|
||||
4: '1rem',
|
||||
5: '1.25rem',
|
||||
6: '1.5rem',
|
||||
8: '2rem',
|
||||
10: '2.5rem',
|
||||
11: '2.75rem',
|
||||
12: '3rem',
|
||||
16: '4rem',
|
||||
24: '6rem',
|
||||
32: '8rem',
|
||||
40: '10rem',
|
||||
48: '12rem',
|
||||
64: '16rem',
|
||||
80: '20rem',
|
||||
96: '24rem',
|
||||
128: '32rem',
|
||||
|
||||
full: '100%',
|
||||
0: '0%',
|
||||
'1/2': '50%',
|
||||
'1/3': 'calc(100% / 3)',
|
||||
'2/3': 'calc(100% / 3 * 2)',
|
||||
'1/4': '25%',
|
||||
'3/4': '75%',
|
||||
'1/5': '20%',
|
||||
'2/5': '40%',
|
||||
'3/5': '60%',
|
||||
'4/5': '80%',
|
||||
'1/6': 'calc(100% / 6)',
|
||||
'5/6': 'calc(100% / 6 * 5)',
|
||||
'1/7': 'calc(100% / 7)',
|
||||
'1/10': 'calc(100% / 10)',
|
||||
'3/10': 'calc(100% / 10 * 3)',
|
||||
'7/10': 'calc(100% / 10 * 7)',
|
||||
'9/10': 'calc(100% / 10 * 9)',
|
||||
screen: '100vw',
|
||||
|
||||
'full-minus-80': 'calc(100% - 20rem)',
|
||||
'full-minus-96': 'calc(100% - 24rem)',
|
||||
|
||||
'225px': '225px',
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
red: {
|
||||
|
|
@ -113,6 +164,54 @@ test(
|
|||
--font-size-base: 1rem;
|
||||
--font-size-base--line-height: 2rem;
|
||||
|
||||
--width-*: initial;
|
||||
--width-0: 0%;
|
||||
--width-1: 0.25rem;
|
||||
--width-2: 0.5rem;
|
||||
--width-3: 0.75rem;
|
||||
--width-4: 1rem;
|
||||
--width-5: 1.25rem;
|
||||
--width-6: 1.5rem;
|
||||
--width-8: 2rem;
|
||||
--width-10: 2.5rem;
|
||||
--width-11: 2.75rem;
|
||||
--width-12: 3rem;
|
||||
--width-16: 4rem;
|
||||
--width-24: 6rem;
|
||||
--width-32: 8rem;
|
||||
--width-40: 10rem;
|
||||
--width-48: 12rem;
|
||||
--width-64: 16rem;
|
||||
--width-80: 20rem;
|
||||
--width-96: 24rem;
|
||||
--width-128: 32rem;
|
||||
--width-px: 1px;
|
||||
--width-auto: auto;
|
||||
--width-1_5: 0.375rem;
|
||||
--width-2_5: 0.625rem;
|
||||
--width-3_5: 0.875rem;
|
||||
--width-full: 100%;
|
||||
--width-1\\/2: 50%;
|
||||
--width-1\\/3: calc(100% / 3);
|
||||
--width-2\\/3: calc(100% / 3 * 2);
|
||||
--width-1\\/4: 25%;
|
||||
--width-3\\/4: 75%;
|
||||
--width-1\\/5: 20%;
|
||||
--width-2\\/5: 40%;
|
||||
--width-3\\/5: 60%;
|
||||
--width-4\\/5: 80%;
|
||||
--width-1\\/6: calc(100% / 6);
|
||||
--width-5\\/6: calc(100% / 6 * 5);
|
||||
--width-1\\/7: calc(100% / 7);
|
||||
--width-1\\/10: calc(100% / 10);
|
||||
--width-3\\/10: calc(100% / 10 * 3);
|
||||
--width-7\\/10: calc(100% / 10 * 7);
|
||||
--width-9\\/10: calc(100% / 10 * 9);
|
||||
--width-screen: 100vw;
|
||||
--width-full-minus-80: calc(100% - 20rem);
|
||||
--width-full-minus-96: calc(100% - 24rem);
|
||||
--width-225px: 225px;
|
||||
|
||||
--font-family-sans: Inter, system-ui, sans-serif;
|
||||
--font-family-display: Cabinet Grotesk, ui-sans-serif, system-ui, sans-serif,
|
||||
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { resolveConfig, type ConfigFile } from '../../tailwindcss/src/compat/con
|
|||
import type { ThemeConfig } from '../../tailwindcss/src/compat/config/types'
|
||||
import { darkModePlugin } from '../../tailwindcss/src/compat/dark-mode'
|
||||
import type { DesignSystem } from '../../tailwindcss/src/design-system'
|
||||
import { escape } from '../../tailwindcss/src/utils/escape'
|
||||
import { findStaticPlugins, type StaticPluginOptions } from './utils/extract-static-plugins'
|
||||
import { info } from './utils/renderer'
|
||||
|
||||
|
|
@ -121,10 +122,16 @@ async function migrateTheme(
|
|||
|
||||
if (resetNamespaces.has(key[0]) && resetNamespaces.get(key[0]) === false) {
|
||||
resetNamespaces.set(key[0], true)
|
||||
css += ` --${keyPathToCssProperty([key[0]])}-*: initial;\n`
|
||||
let property = keyPathToCssProperty([key[0]])
|
||||
if (property !== null) {
|
||||
css += ` ${escape(`--${property}`)}-*: initial;\n`
|
||||
}
|
||||
}
|
||||
|
||||
css += ` --${keyPathToCssProperty(key)}: ${value};\n`
|
||||
let property = keyPathToCssProperty(key)
|
||||
if (property !== null) {
|
||||
css += ` ${escape(`--${property}`)}: ${value};\n`
|
||||
}
|
||||
}
|
||||
|
||||
if ('keyframes' in resolvedConfig.theme) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { expect, test } from 'vitest'
|
||||
import { describe, expect, test } from 'vitest'
|
||||
import { buildDesignSystem } from '../design-system'
|
||||
import { Theme, ThemeOptions } from '../theme'
|
||||
import { applyConfigToTheme } from './apply-config-to-theme'
|
||||
import { applyConfigToTheme, keyPathToCssProperty } from './apply-config-to-theme'
|
||||
import { resolveConfig } from './config/resolve-config'
|
||||
|
||||
test('config values can be merged into the theme', () => {
|
||||
|
|
@ -49,6 +49,13 @@ test('config values can be merged into the theme', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
|
||||
width: {
|
||||
// Purposely setting to something different from the default
|
||||
'1/2': '60%',
|
||||
'0.5': '60%',
|
||||
'100%': '100%',
|
||||
},
|
||||
},
|
||||
},
|
||||
base: '/root',
|
||||
|
|
@ -73,6 +80,9 @@ test('config values can be merged into the theme', () => {
|
|||
'1rem',
|
||||
{ '--line-height': '1.5' },
|
||||
])
|
||||
expect(theme.resolve('1/2', ['--width'])).toEqual('60%')
|
||||
expect(theme.resolve('0.5', ['--width'])).toEqual('60%')
|
||||
expect(theme.resolve('100%', ['--width'])).toEqual('100%')
|
||||
})
|
||||
|
||||
test('will reset default theme values with overwriting theme values', () => {
|
||||
|
|
@ -147,3 +157,12 @@ test('invalid keys are not merged into the theme', () => {
|
|||
|
||||
expect(entries.length).toEqual(0)
|
||||
})
|
||||
|
||||
describe('keyPathToCssProperty', () => {
|
||||
test.each([
|
||||
[['width', '40', '2/5'], '--width-40-2/5'],
|
||||
[['spacing', '0.5'], '--spacing-0_5'],
|
||||
])('converts %s to %s', (keyPath, expected) => {
|
||||
expect(`--${keyPathToCssProperty(keyPath)}`).toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
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 {
|
||||
|
|
@ -40,8 +41,8 @@ export function applyConfigToTheme(
|
|||
if (!name) continue
|
||||
|
||||
designSystem.theme.add(
|
||||
`--${name}`,
|
||||
value as any,
|
||||
`--${escape(name)}`,
|
||||
'' + value,
|
||||
ThemeOptions.INLINE | ThemeOptions.REFERENCE | ThemeOptions.DEFAULT,
|
||||
)
|
||||
}
|
||||
|
|
@ -124,7 +125,7 @@ export function themeableValues(config: ResolvedConfig['theme']): [string[], unk
|
|||
return toAdd
|
||||
}
|
||||
|
||||
const IS_VALID_KEY = /^[a-zA-Z0-9-_]+$/
|
||||
const IS_VALID_KEY = /^[a-zA-Z0-9-_%/\.]+$/
|
||||
|
||||
export function keyPathToCssProperty(path: string[]) {
|
||||
if (path[0] === 'colors') path[0] = 'color'
|
||||
|
|
|
|||
|
|
@ -1209,6 +1209,165 @@ describe('theme', async () => {
|
|||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('can use escaped JS variables in theme values', async () => {
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
@plugin "my-plugin";
|
||||
`
|
||||
|
||||
let compiler = await compile(input, {
|
||||
loadModule: async (id, base) => {
|
||||
return {
|
||||
base,
|
||||
module: plugin(
|
||||
function ({ matchUtilities, theme }) {
|
||||
matchUtilities(
|
||||
{ 'my-width': (value) => ({ width: value }) },
|
||||
{ values: theme('width') },
|
||||
)
|
||||
},
|
||||
{
|
||||
theme: {
|
||||
extend: {
|
||||
width: {
|
||||
'1': '0.25rem',
|
||||
// Purposely setting to something different from the v3 default
|
||||
'1/2': '60%',
|
||||
'1.5': '0.375rem',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
expect(compiler.build(['my-width-1', 'my-width-1/2', 'my-width-1.5'])).toMatchInlineSnapshot(
|
||||
`
|
||||
".my-width-1 {
|
||||
width: 0.25rem;
|
||||
}
|
||||
.my-width-1\\.5 {
|
||||
width: 0.375rem;
|
||||
}
|
||||
.my-width-1\\/2 {
|
||||
width: 60%;
|
||||
}
|
||||
"
|
||||
`,
|
||||
)
|
||||
})
|
||||
|
||||
test('can use escaped CSS variables in theme values', async () => {
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
@plugin "my-plugin";
|
||||
|
||||
@theme {
|
||||
--width-1: 0.25rem;
|
||||
/* Purposely setting to something different from the v3 default */
|
||||
--width-1\/2: 60%;
|
||||
--width-1\.5: 0.375rem;
|
||||
--width-2_5: 0.625rem;
|
||||
}
|
||||
`
|
||||
|
||||
let compiler = await compile(input, {
|
||||
loadModule: async (id, base) => {
|
||||
return {
|
||||
base,
|
||||
module: plugin(function ({ matchUtilities, theme }) {
|
||||
matchUtilities(
|
||||
{ 'my-width': (value) => ({ width: value }) },
|
||||
{ values: theme('width') },
|
||||
)
|
||||
}),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
expect(compiler.build(['my-width-1', 'my-width-1.5', 'my-width-1/2', 'my-width-2.5']))
|
||||
.toMatchInlineSnapshot(`
|
||||
".my-width-1 {
|
||||
width: 0.25rem;
|
||||
}
|
||||
.my-width-1\\.5 {
|
||||
width: 0.375rem;
|
||||
}
|
||||
.my-width-1\\/2 {
|
||||
width: 60%;
|
||||
}
|
||||
.my-width-2\\.5 {
|
||||
width: 0.625rem;
|
||||
}
|
||||
:root {
|
||||
--width-1: 0.25rem;
|
||||
--width-1\\/2: 60%;
|
||||
--width-1\\.5: 0.375rem;
|
||||
--width-2_5: 0.625rem;
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('can use escaped CSS variables in referenced theme namespace', async () => {
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
@plugin "my-plugin";
|
||||
|
||||
@theme {
|
||||
--width-1: 0.25rem;
|
||||
/* Purposely setting to something different from the v3 default */
|
||||
--width-1\/2: 60%;
|
||||
--width-1\.5: 0.375rem;
|
||||
--width-2_5: 0.625rem;
|
||||
}
|
||||
`
|
||||
|
||||
let compiler = await compile(input, {
|
||||
loadModule: async (id, base) => {
|
||||
return {
|
||||
base,
|
||||
module: plugin(
|
||||
function ({ matchUtilities, theme }) {
|
||||
matchUtilities(
|
||||
{ 'my-width': (value) => ({ width: value }) },
|
||||
{ values: theme('myWidth') },
|
||||
)
|
||||
},
|
||||
{
|
||||
theme: { myWidth: ({ theme }) => theme('width') },
|
||||
},
|
||||
),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
expect(compiler.build(['my-width-1', 'my-width-1.5', 'my-width-1/2', 'my-width-2.5']))
|
||||
.toMatchInlineSnapshot(`
|
||||
".my-width-1 {
|
||||
width: 0.25rem;
|
||||
}
|
||||
.my-width-1\\.5 {
|
||||
width: 0.375rem;
|
||||
}
|
||||
.my-width-1\\/2 {
|
||||
width: 60%;
|
||||
}
|
||||
.my-width-2\\.5 {
|
||||
width: 0.625rem;
|
||||
}
|
||||
:root {
|
||||
--width-1: 0.25rem;
|
||||
--width-1\\/2: 60%;
|
||||
--width-1\\.5: 0.375rem;
|
||||
--width-2_5: 0.625rem;
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('addVariant', () => {
|
||||
|
|
|
|||
|
|
@ -267,7 +267,7 @@ export function buildPluginApi(
|
|||
|
||||
// Resolve the candidate value
|
||||
let value: string | null = null
|
||||
let isFraction = false
|
||||
let ignoreModifier = false
|
||||
|
||||
{
|
||||
let values = options?.values ?? {}
|
||||
|
|
@ -289,12 +289,14 @@ export function buildPluginApi(
|
|||
value = values.DEFAULT ?? null
|
||||
} else if (candidate.value.kind === 'arbitrary') {
|
||||
value = candidate.value.value
|
||||
} else if (candidate.value.fraction && values[candidate.value.fraction]) {
|
||||
value = values[candidate.value.fraction]
|
||||
ignoreModifier = true
|
||||
} else if (values[candidate.value.value]) {
|
||||
value = values[candidate.value.value]
|
||||
} else if (values.__BARE_VALUE__) {
|
||||
value = values.__BARE_VALUE__(candidate.value) ?? null
|
||||
|
||||
isFraction = (candidate.value.fraction !== null && value?.includes('/')) ?? false
|
||||
ignoreModifier = (candidate.value.fraction !== null && value?.includes('/')) ?? false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -320,7 +322,7 @@ export function buildPluginApi(
|
|||
}
|
||||
|
||||
// A modifier was provided but is invalid
|
||||
if (candidate.modifier && modifier === null && !isFraction) {
|
||||
if (candidate.modifier && modifier === null && !ignoreModifier) {
|
||||
// For arbitrary values, return `null` to avoid falling through to the next utility
|
||||
return candidate.value?.kind === 'arbitrary' ? null : undefined
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { DesignSystem } from '../design-system'
|
|||
import { ThemeOptions, type Theme, type ThemeKey } from '../theme'
|
||||
import { withAlpha } from '../utilities'
|
||||
import { DefaultMap } from '../utils/default-map'
|
||||
import { unescape } from '../utils/escape'
|
||||
import { toKeyPath } from '../utils/to-key-path'
|
||||
import { deepMerge } from './config/deep-merge'
|
||||
import type { UserConfig } from './config/types'
|
||||
|
|
@ -37,7 +38,6 @@ export function createThemeFn(
|
|||
return cssValue
|
||||
}
|
||||
|
||||
//
|
||||
if (configValue !== null && typeof configValue === 'object' && !Array.isArray(configValue)) {
|
||||
let configValueCopy: Record<string, unknown> & { __CSS_VALUES__?: Record<string, number> } =
|
||||
// We want to make sure that we don't mutate the original config
|
||||
|
|
@ -70,7 +70,7 @@ export function createThemeFn(
|
|||
}
|
||||
|
||||
// CSS values from `@theme` win over values from the config
|
||||
configValueCopy[key] = cssValue[key]
|
||||
configValueCopy[unescape(key)] = cssValue[key]
|
||||
}
|
||||
|
||||
return configValueCopy
|
||||
|
|
|
|||
14
packages/tailwindcss/src/utils/escape.test.ts
Normal file
14
packages/tailwindcss/src/utils/escape.test.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { describe, expect, test } from 'vitest'
|
||||
import { escape, unescape } from './escape'
|
||||
|
||||
describe('escape', () => {
|
||||
test('adds backslashes', () => {
|
||||
expect(escape(String.raw`red-1/2`)).toMatchInlineSnapshot(`"red-1\\/2"`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('unescape', () => {
|
||||
test('removes backslashes', () => {
|
||||
expect(unescape(String.raw`red-1\/2`)).toMatchInlineSnapshot(`"red-1/2"`)
|
||||
})
|
||||
})
|
||||
|
|
@ -71,3 +71,11 @@ export function escape(value: string) {
|
|||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function unescape(escaped: string) {
|
||||
return escaped.replace(/\\([\dA-Fa-f]{1,6}[\t\n\f\r ]?|[\S\s])/g, (match) => {
|
||||
return match.length > 2
|
||||
? String.fromCodePoint(Number.parseInt(match.slice(1).trim(), 16))
|
||||
: match[1]
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue