Compare commits
3 commits
main
...
fix/issue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec06099222 | ||
|
|
027097d94e | ||
|
|
dd23e0f029 |
4 changed files with 159 additions and 31 deletions
|
|
@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Substitute `@variant` inside legacy JS APIs ([#19263](https://github.com/tailwindlabs/tailwindcss/pull/19263))
|
- Substitute `@variant` inside legacy JS APIs ([#19263](https://github.com/tailwindlabs/tailwindcss/pull/19263))
|
||||||
|
- Handle utilities with `1` keys in a JS config ([#19254](https://github.com/tailwindlabs/tailwindcss/pull/19254))
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
import { describe, expect, test } from 'vitest'
|
import { describe, expect, test } from 'vitest'
|
||||||
import { buildDesignSystem } from '../design-system'
|
import { buildDesignSystem } from '../design-system'
|
||||||
import { Theme, ThemeOptions } from '../theme'
|
import { Theme, ThemeOptions } from '../theme'
|
||||||
import { applyConfigToTheme, keyPathToCssProperty } from './apply-config-to-theme'
|
import {
|
||||||
|
applyConfigToTheme,
|
||||||
|
keyPathToCssProperty,
|
||||||
|
keyPathsToCssProperty,
|
||||||
|
} from './apply-config-to-theme'
|
||||||
import { resolveConfig } from './config/resolve-config'
|
import { resolveConfig } from './config/resolve-config'
|
||||||
|
|
||||||
test('config values can be merged into the theme', () => {
|
test('config values can be merged into the theme', () => {
|
||||||
|
|
@ -213,6 +217,48 @@ describe('keyPathToCssProperty', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('keyPathsToCssProperty', () => {
|
||||||
|
test.each([
|
||||||
|
// No "1" entries - should return single result
|
||||||
|
[['width', '40', '2/5'], ['width-40-2/5']],
|
||||||
|
[['spacing', '0.5'], ['spacing-0_5']],
|
||||||
|
|
||||||
|
// Single "1" entry before the end - should return two variants
|
||||||
|
[
|
||||||
|
['fontSize', 'xs', '1', 'lineHeight'],
|
||||||
|
['text-xs--line-height', 'text-xs-1-line-height'],
|
||||||
|
],
|
||||||
|
|
||||||
|
// Multiple "1" entries before the end - should only split the last "1"
|
||||||
|
[
|
||||||
|
['test', '1', 'middle', '1', 'end'],
|
||||||
|
['test-1-middle--end', 'test-1-middle-1-end'],
|
||||||
|
],
|
||||||
|
|
||||||
|
// A "1" at the end means everything should be kept as-is
|
||||||
|
[['spacing', '1'], ['spacing-1']],
|
||||||
|
|
||||||
|
// Even when there are other 1s in the path
|
||||||
|
[['test', '1', 'middle', '1'], ['test-1-middle-1']],
|
||||||
|
|
||||||
|
[['colors', 'a', '1', 'DEFAULT'], ['color-a-1']],
|
||||||
|
[
|
||||||
|
['colors', 'a', '1', 'foo'],
|
||||||
|
['color-a--foo', 'color-a-1-foo'],
|
||||||
|
],
|
||||||
|
])('converts %s to %s', (keyPath, expected) => {
|
||||||
|
expect(keyPathsToCssProperty(keyPath)).toEqual(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns empty array for container path', () => {
|
||||||
|
expect(keyPathsToCssProperty(['container', 'sm'])).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns empty array for invalid keys', () => {
|
||||||
|
expect(keyPathsToCssProperty(['test', 'invalid@key'])).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test('converts opacity modifiers from decimal to percentage values', () => {
|
test('converts opacity modifiers from decimal to percentage values', () => {
|
||||||
let theme = new Theme()
|
let theme = new Theme()
|
||||||
let design = buildDesignSystem(theme)
|
let design = buildDesignSystem(theme)
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,9 @@ export function applyConfigToTheme(
|
||||||
replacedThemeKeys: Set<string>,
|
replacedThemeKeys: Set<string>,
|
||||||
) {
|
) {
|
||||||
for (let replacedThemeKey of replacedThemeKeys) {
|
for (let replacedThemeKey of replacedThemeKeys) {
|
||||||
let name = keyPathToCssProperty([replacedThemeKey])
|
for (let name of keyPathsToCssProperty([replacedThemeKey])) {
|
||||||
if (!name) continue
|
designSystem.theme.clearNamespace(`--${name}`, ThemeOptions.DEFAULT)
|
||||||
|
}
|
||||||
designSystem.theme.clearNamespace(`--${name}`, ThemeOptions.DEFAULT)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let [path, value] of themeableValues(theme)) {
|
for (let [path, value] of themeableValues(theme)) {
|
||||||
|
|
@ -50,14 +49,13 @@ export function applyConfigToTheme(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = keyPathToCssProperty(path)
|
for (let name of keyPathsToCssProperty(path)) {
|
||||||
if (!name) continue
|
designSystem.theme.add(
|
||||||
|
`--${name}`,
|
||||||
designSystem.theme.add(
|
'' + value,
|
||||||
`--${name}`,
|
ThemeOptions.INLINE | ThemeOptions.REFERENCE | ThemeOptions.DEFAULT,
|
||||||
'' + value,
|
)
|
||||||
ThemeOptions.INLINE | ThemeOptions.REFERENCE | ThemeOptions.DEFAULT,
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If someone has updated `fontFamily.sans` or `fontFamily.mono` in a JS
|
// If someone has updated `fontFamily.sans` or `fontFamily.mono` in a JS
|
||||||
|
|
@ -150,8 +148,12 @@ export function themeableValues(config: ResolvedConfig['theme']): [string[], unk
|
||||||
const IS_VALID_KEY = /^[a-zA-Z0-9-_%/\.]+$/
|
const IS_VALID_KEY = /^[a-zA-Z0-9-_%/\.]+$/
|
||||||
|
|
||||||
export function keyPathToCssProperty(path: string[]) {
|
export function keyPathToCssProperty(path: string[]) {
|
||||||
|
return keyPathsToCssProperty(path)[0] ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function keyPathsToCssProperty(path: string[]): string[] {
|
||||||
// The legacy container component config should not be included in the Theme
|
// The legacy container component config should not be included in the Theme
|
||||||
if (path[0] === 'container') return null
|
if (path[0] === 'container') return []
|
||||||
|
|
||||||
path = path.slice()
|
path = path.slice()
|
||||||
|
|
||||||
|
|
@ -170,32 +172,43 @@ export function keyPathToCssProperty(path: string[]) {
|
||||||
if (path[0] === 'transitionTimingFunction') path[0] = 'ease'
|
if (path[0] === 'transitionTimingFunction') path[0] = 'ease'
|
||||||
|
|
||||||
for (let part of path) {
|
for (let part of path) {
|
||||||
if (!IS_VALID_KEY.test(part)) return null
|
if (!IS_VALID_KEY.test(part)) return []
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
// Find the position of the last `1` as long as it's not at the end
|
||||||
path
|
let lastOnePosition = path.lastIndexOf('1')
|
||||||
// [1] should move into the nested object tuple. To create the CSS variable
|
if (lastOnePosition === path.length - 1) lastOnePosition = -1
|
||||||
// name for this, we replace it with an empty string that will result in two
|
|
||||||
// subsequent dashes when joined.
|
|
||||||
//
|
|
||||||
// E.g.:
|
|
||||||
// - `fontSize.xs.1.lineHeight` -> `font-size-xs--line-height`
|
|
||||||
// - `spacing.1` -> `--spacing-1`
|
|
||||||
.map((path, idx, all) => (path === '1' && idx !== all.length - 1 ? '' : path))
|
|
||||||
|
|
||||||
// Resolve the key path to a CSS variable segment
|
// Generate two combinations based on tuple access:
|
||||||
|
let paths: string[][] = []
|
||||||
|
|
||||||
|
// Option 1: Replace the last "1" with empty string if it exists
|
||||||
|
//
|
||||||
|
// We place this first as we "prefer" treating this as a tuple access. The exception to this is if
|
||||||
|
// the keypath ends in `DEFAULT` otherwise we'd see a key that ends in a dash like `--color-a-`
|
||||||
|
if (lastOnePosition !== -1 && path.at(-1) !== 'DEFAULT') {
|
||||||
|
let modified = path.slice()
|
||||||
|
modified[lastOnePosition] = ''
|
||||||
|
paths.push(modified)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option 2: The path as is
|
||||||
|
paths.push(path)
|
||||||
|
|
||||||
|
return paths.map((path) => {
|
||||||
|
// Remove the `DEFAULT` key at the end of a path
|
||||||
|
// We're reading from CSS anyway so it'll be a string
|
||||||
|
if (path.at(-1) === 'DEFAULT') path = path.slice(0, -1)
|
||||||
|
|
||||||
|
// Resolve the key path to a CSS variable segment
|
||||||
|
return path
|
||||||
.map((part) =>
|
.map((part) =>
|
||||||
part
|
part
|
||||||
.replaceAll('.', '_')
|
.replaceAll('.', '_')
|
||||||
.replace(/([a-z])([A-Z])/g, (_, a, b) => `${a}-${b.toLowerCase()}`),
|
.replace(/([a-z])([A-Z])/g, (_, a, b) => `${a}-${b.toLowerCase()}`),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Remove the `DEFAULT` key at the end of a path
|
|
||||||
// We're reading from CSS anyway so it'll be a string
|
|
||||||
.filter((part, index) => part !== 'DEFAULT' || index !== path.length - 1)
|
|
||||||
.join('-')
|
.join('-')
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidThemePrimitive(value: unknown) {
|
function isValidThemePrimitive(value: unknown) {
|
||||||
|
|
|
||||||
|
|
@ -297,6 +297,74 @@ test('Variants in CSS overwrite variants from plugins', async () => {
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('A `1` key is treated like a nested theme key *and* a normal theme key', async () => {
|
||||||
|
let input = css`
|
||||||
|
@tailwind utilities;
|
||||||
|
@config "./config.js";
|
||||||
|
`
|
||||||
|
|
||||||
|
let compiler = await compile(input, {
|
||||||
|
loadModule: async () => ({
|
||||||
|
module: {
|
||||||
|
theme: {
|
||||||
|
fontSize: {
|
||||||
|
xs: ['0.5rem', { lineHeight: '0.25rem' }],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
a: {
|
||||||
|
1: { DEFAULT: '#ffffff', hovered: '#000000' },
|
||||||
|
2: { DEFAULT: '#000000', hovered: '#c0ffee' },
|
||||||
|
},
|
||||||
|
b: {
|
||||||
|
1: '#ffffff',
|
||||||
|
2: '#000000',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
base: '/root',
|
||||||
|
path: '',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(
|
||||||
|
compiler.build([
|
||||||
|
'text-xs',
|
||||||
|
|
||||||
|
'text-a-1',
|
||||||
|
'text-a-2',
|
||||||
|
'text-a-1-hovered',
|
||||||
|
'text-a-2-hovered',
|
||||||
|
'text-b-1',
|
||||||
|
'text-b-2',
|
||||||
|
]),
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
|
".text-xs {
|
||||||
|
font-size: 0.5rem;
|
||||||
|
line-height: var(--tw-leading, 0.25rem);
|
||||||
|
}
|
||||||
|
.text-a-1 {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
.text-a-1-hovered {
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
.text-a-2 {
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
.text-a-2-hovered {
|
||||||
|
color: #c0ffee;
|
||||||
|
}
|
||||||
|
.text-b-1 {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
.text-b-2 {
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
describe('theme callbacks', () => {
|
describe('theme callbacks', () => {
|
||||||
test('tuple values from the config overwrite `@theme default` tuple-ish values from the CSS theme', async ({
|
test('tuple values from the config overwrite `@theme default` tuple-ish values from the CSS theme', async ({
|
||||||
expect,
|
expect,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue