Add support for setting screens in JS config (#14415)
This PR adds support for the _simple_ case of the `screens` option
inside JS config paths. This allows JS configs to extend the responsive
theme by adding custom breakpoints. Here's an example from our v3 docs:
```js
{
theme: {
screens: {
'sm': '640px',
// => @media (min-width: 640px) { ... }
'md': '768px',
// => @media (min-width: 768px) { ... }
'lg': '1024px',
// => @media (min-width: 1024px) { ... }
'xl': '1280px',
// => @media (min-width: 1280px) { ... }
'2xl': '1536px',
// => @media (min-width: 1536px) { ... }
}
}
}
```
For simple breakpoints, this will extend the core breakpoints and will
work with the `min-*` and `max-*` utilities. However, we also support
complex ways of setting up custom screens like this:
```js
{
theme: {
extend: {
screens: {
sm: { max: '639px' },
md: [
{ min: '668px', max: '767px' },
{ min: '868px' },
],
lg: { min: '868px' },
xl: { min: '1024px', max: '1279px' },
tall: { raw: '(min-height: 800px)' },
},
},
},
}
```
For these complex setups, we _only_ generate the shorthand variant (e.g.
`tall`) but those won't integrate within `min-*` and `max-*`. In v3,
adding any of these complex configurations would omit any `min-*` and
`max-*` variants.
This commit is contained in:
parent
6ca8cc6f02
commit
2ddb715abd
9 changed files with 806 additions and 27 deletions
|
|
@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Add support for `aria`, `supports`, and `data` variants defined in JS config files ([#14407](https://github.com/tailwindlabs/tailwindcss/pull/14407))
|
||||
- Add `@tailwindcss/upgrade` tooling ([#14434](https://github.com/tailwindlabs/tailwindcss/pull/14434))
|
||||
|
||||
### Added
|
||||
|
||||
- Support `screens` in JS config files ([#14415](https://github.com/tailwindlabs/tailwindcss/pull/14415))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Support `borderRadius.*` as an alias for `--radius-*` when using dot notation inside the `theme()` function ([#14436](https://github.com/tailwindlabs/tailwindcss/pull/14436))
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { resolveConfig } from './config/resolve-config'
|
|||
import type { UserConfig } from './config/types'
|
||||
import { darkModePlugin } from './dark-mode'
|
||||
import { buildPluginApi, type CssPluginOptions, type Plugin } from './plugin-api'
|
||||
import { registerScreensConfig } from './screens-config'
|
||||
import { registerThemeVariantOverrides } from './theme-variants'
|
||||
|
||||
export async function applyCompatibilityHooks({
|
||||
|
|
@ -174,6 +175,7 @@ export async function applyCompatibilityHooks({
|
|||
...userConfig,
|
||||
{ config: { plugins: [darkModePlugin] } },
|
||||
])
|
||||
let resolvedUserConfig = resolveConfig(designSystem, userConfig)
|
||||
|
||||
let pluginApi = buildPluginApi(designSystem, ast, resolvedConfig)
|
||||
|
||||
|
|
@ -184,9 +186,10 @@ export async function applyCompatibilityHooks({
|
|||
// Merge the user-configured theme keys into the design system. The compat
|
||||
// config would otherwise expand into namespaces like `background-color` which
|
||||
// core utilities already read from.
|
||||
applyConfigToTheme(designSystem, userConfig)
|
||||
applyConfigToTheme(designSystem, resolvedUserConfig)
|
||||
|
||||
registerThemeVariantOverrides(resolvedConfig, designSystem)
|
||||
registerThemeVariantOverrides(resolvedUserConfig, designSystem)
|
||||
registerScreensConfig(resolvedUserConfig, designSystem)
|
||||
|
||||
// Replace `resolveThemeValue` with a version that is backwards compatible
|
||||
// with dot-notation but also aware of any JS theme configurations registered
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@ import { expect, test } from 'vitest'
|
|||
import { buildDesignSystem } from '../design-system'
|
||||
import { Theme } from '../theme'
|
||||
import { applyConfigToTheme } from './apply-config-to-theme'
|
||||
import { resolveConfig } from './config/resolve-config'
|
||||
|
||||
test('Config values can be merged into the theme', () => {
|
||||
let theme = new Theme()
|
||||
let design = buildDesignSystem(theme)
|
||||
|
||||
applyConfigToTheme(design, [
|
||||
let resolvedUserConfig = resolveConfig(design, [
|
||||
{
|
||||
config: {
|
||||
theme: {
|
||||
|
|
@ -36,6 +37,7 @@ test('Config values can be merged into the theme', () => {
|
|||
},
|
||||
},
|
||||
])
|
||||
applyConfigToTheme(design, resolvedUserConfig)
|
||||
|
||||
expect(theme.resolve('primary', ['--color'])).toEqual('#c0ffee')
|
||||
expect(theme.resolve('red-500', ['--color'])).toEqual('red')
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import type { DesignSystem } from '../design-system'
|
||||
import { ThemeOptions } from '../theme'
|
||||
import { resolveConfig, type ConfigFile } from './config/resolve-config'
|
||||
import type { ResolvedConfig } from './config/types'
|
||||
|
||||
function resolveThemeValue(value: unknown, subValue: string | null = null): string | null {
|
||||
|
|
@ -20,10 +19,11 @@ function resolveThemeValue(value: unknown, subValue: string | null = null): stri
|
|||
return null
|
||||
}
|
||||
|
||||
export function applyConfigToTheme(designSystem: DesignSystem, configs: ConfigFile[]) {
|
||||
let theme = resolveConfig(designSystem, configs).theme
|
||||
|
||||
export function applyConfigToTheme(designSystem: DesignSystem, { theme }: ResolvedConfig) {
|
||||
for (let [path, value] of themeableValues(theme)) {
|
||||
if (typeof value !== 'string' && typeof value !== 'number') {
|
||||
continue
|
||||
}
|
||||
let name = keyPathToCssProperty(path)
|
||||
designSystem.theme.add(
|
||||
`--${name}`,
|
||||
|
|
@ -111,9 +111,8 @@ function themeableValues(config: ResolvedConfig['theme']): [string[], unknown][]
|
|||
}
|
||||
|
||||
function keyPathToCssProperty(path: string[]) {
|
||||
if (path[0] === 'colors') {
|
||||
path[0] = 'color'
|
||||
}
|
||||
if (path[0] === 'colors') path[0] = 'color'
|
||||
if (path[0] === 'screens') path[0] = 'breakpoint'
|
||||
|
||||
return (
|
||||
path
|
||||
|
|
|
|||
|
|
@ -1078,3 +1078,65 @@ test('creates variants for `data`, `supports`, and `aria` theme options at the s
|
|||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('merges css breakpoints with js config screens', async () => {
|
||||
let input = css`
|
||||
@theme default {
|
||||
--breakpoint-sm: 40rem;
|
||||
--breakpoint-md: 48rem;
|
||||
--breakpoint-lg: 64rem;
|
||||
--breakpoint-xl: 80rem;
|
||||
--breakpoint-2xl: 96rem;
|
||||
}
|
||||
@theme {
|
||||
--breakpoint-md: 50rem;
|
||||
}
|
||||
@config "./config.js";
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
let compiler = await compile(input, {
|
||||
loadConfig: async () => ({
|
||||
theme: {
|
||||
extend: {
|
||||
screens: {
|
||||
sm: '44rem',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
expect(compiler.build(['sm:flex', 'md:flex', 'lg:flex', 'min-sm:max-md:underline']))
|
||||
.toMatchInlineSnapshot(`
|
||||
":root {
|
||||
--breakpoint-md: 50rem;
|
||||
--breakpoint-lg: 64rem;
|
||||
--breakpoint-xl: 80rem;
|
||||
--breakpoint-2xl: 96rem;
|
||||
}
|
||||
.sm\\:flex {
|
||||
@media (width >= 44rem) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.min-sm\\:max-md\\:underline {
|
||||
@media (width >= 44rem) {
|
||||
@media (width < 50rem) {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
.md\\:flex {
|
||||
@media (width >= 50rem) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.lg\\:flex {
|
||||
@media (width >= 64rem) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -884,11 +884,11 @@ export default {
|
|||
...barePercentages,
|
||||
},
|
||||
screens: {
|
||||
sm: '640px',
|
||||
md: '768px',
|
||||
lg: '1024px',
|
||||
xl: '1280px',
|
||||
'2xl': '1536px',
|
||||
sm: '40rem',
|
||||
md: '48rem',
|
||||
lg: '64rem',
|
||||
xl: '80rem',
|
||||
'2xl': '96rem',
|
||||
},
|
||||
scrollMargin: ({ theme }) => theme('spacing'),
|
||||
scrollPadding: ({ theme }) => theme('spacing'),
|
||||
|
|
|
|||
592
packages/tailwindcss/src/compat/screens-config.test.ts
Normal file
592
packages/tailwindcss/src/compat/screens-config.test.ts
Normal file
|
|
@ -0,0 +1,592 @@
|
|||
import { describe, expect, test } from 'vitest'
|
||||
import { compile } from '..'
|
||||
|
||||
const css = String.raw
|
||||
|
||||
test('CSS `--breakpoint-*` merge with JS config `screens`', async () => {
|
||||
let input = css`
|
||||
@theme default {
|
||||
--breakpoint-sm: 40rem;
|
||||
--breakpoint-md: 48rem;
|
||||
--breakpoint-lg: 64rem;
|
||||
--breakpoint-xl: 80rem;
|
||||
--breakpoint-2xl: 96rem;
|
||||
}
|
||||
@theme {
|
||||
--breakpoint-md: 50rem;
|
||||
}
|
||||
@config "./config.js";
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
let compiler = await compile(input, {
|
||||
loadConfig: async () => ({
|
||||
theme: {
|
||||
extend: {
|
||||
screens: {
|
||||
sm: '44rem',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
expect(
|
||||
compiler.build([
|
||||
'sm:flex',
|
||||
'md:flex',
|
||||
'lg:flex',
|
||||
'min-sm:max-md:underline',
|
||||
'min-md:max-lg:underline',
|
||||
// Ensure other core variants appear at the end
|
||||
'print:items-end',
|
||||
]),
|
||||
).toMatchInlineSnapshot(`
|
||||
":root {
|
||||
--breakpoint-md: 50rem;
|
||||
--breakpoint-lg: 64rem;
|
||||
--breakpoint-xl: 80rem;
|
||||
--breakpoint-2xl: 96rem;
|
||||
}
|
||||
.sm\\:flex {
|
||||
@media (width >= 44rem) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.min-sm\\:max-md\\:underline {
|
||||
@media (width >= 44rem) {
|
||||
@media (width < 50rem) {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
.md\\:flex {
|
||||
@media (width >= 50rem) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.min-md\\:max-lg\\:underline {
|
||||
@media (width >= 50rem) {
|
||||
@media (width < 64rem) {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
.lg\\:flex {
|
||||
@media (width >= 64rem) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.print\\:items-end {
|
||||
@media print {
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('JS config `screens` extend CSS `--breakpoint-*`', async () => {
|
||||
let input = css`
|
||||
@theme default {
|
||||
--breakpoint-xs: 39rem;
|
||||
--breakpoint-md: 49rem;
|
||||
}
|
||||
@theme {
|
||||
--breakpoint-md: 50rem;
|
||||
}
|
||||
@config "./config.js";
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
let compiler = await compile(input, {
|
||||
loadConfig: async () => ({
|
||||
theme: {
|
||||
extend: {
|
||||
screens: {
|
||||
xs: '30rem',
|
||||
sm: '40rem',
|
||||
md: '48rem',
|
||||
lg: '60rem',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
expect(
|
||||
compiler.build([
|
||||
// Order is messed up on purpose
|
||||
'md:flex',
|
||||
'sm:flex',
|
||||
'lg:flex',
|
||||
'xs:flex',
|
||||
'min-md:max-lg:underline',
|
||||
'min-sm:max-md:underline',
|
||||
'min-xs:flex',
|
||||
'min-xs:max-md:underline',
|
||||
|
||||
// Ensure other core variants appear at the end
|
||||
'print:items-end',
|
||||
]),
|
||||
).toMatchInlineSnapshot(`
|
||||
":root {
|
||||
--breakpoint-md: 50rem;
|
||||
}
|
||||
.min-xs\\:flex {
|
||||
@media (width >= 30rem) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.xs\\:flex {
|
||||
@media (width >= 30rem) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.min-xs\\:max-md\\:underline {
|
||||
@media (width >= 30rem) {
|
||||
@media (width < 50rem) {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
.sm\\:flex {
|
||||
@media (width >= 40rem) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.min-sm\\:max-md\\:underline {
|
||||
@media (width >= 40rem) {
|
||||
@media (width < 50rem) {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
.md\\:flex {
|
||||
@media (width >= 50rem) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.min-md\\:max-lg\\:underline {
|
||||
@media (width >= 50rem) {
|
||||
@media (width < 60rem) {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
.lg\\:flex {
|
||||
@media (width >= 60rem) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.print\\:items-end {
|
||||
@media print {
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('JS config `screens` only setup, even if those match the default-theme export', async () => {
|
||||
let input = css`
|
||||
@config "./config.js";
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
let compiler = await compile(input, {
|
||||
loadConfig: async () => ({
|
||||
theme: {
|
||||
screens: {
|
||||
sm: '40rem',
|
||||
md: '48rem',
|
||||
lg: '64rem',
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
expect(
|
||||
compiler.build([
|
||||
// Order is messed up on purpose
|
||||
'md:flex',
|
||||
'sm:flex',
|
||||
'lg:flex',
|
||||
'min-md:max-lg:underline',
|
||||
'min-sm:max-md:underline',
|
||||
|
||||
// Ensure other core variants appear at the end
|
||||
'print:items-end',
|
||||
]),
|
||||
).toMatchInlineSnapshot(`
|
||||
".sm\\:flex {
|
||||
@media (width >= 40rem) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.min-sm\\:max-md\\:underline {
|
||||
@media (width >= 40rem) {
|
||||
@media (width < 48rem) {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
.md\\:flex {
|
||||
@media (width >= 48rem) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.min-md\\:max-lg\\:underline {
|
||||
@media (width >= 48rem) {
|
||||
@media (width < 64rem) {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
.lg\\:flex {
|
||||
@media (width >= 64rem) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.print\\:items-end {
|
||||
@media print {
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('JS config `screens` overwrite CSS `--breakpoint-*`', async () => {
|
||||
let input = css`
|
||||
@theme default {
|
||||
--breakpoint-sm: 40rem;
|
||||
--breakpoint-md: 48rem;
|
||||
--breakpoint-lg: 64rem;
|
||||
--breakpoint-xl: 80rem;
|
||||
--breakpoint-2xl: 96rem;
|
||||
}
|
||||
@config "./config.js";
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
let compiler = await compile(input, {
|
||||
loadConfig: async () => ({
|
||||
theme: {
|
||||
screens: {
|
||||
mini: '40rem',
|
||||
midi: '48rem',
|
||||
maxi: '64rem',
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
expect(
|
||||
compiler.build([
|
||||
'sm:flex',
|
||||
'md:flex',
|
||||
'mini:flex',
|
||||
'midi:flex',
|
||||
'maxi:flex',
|
||||
'min-md:max-lg:underline',
|
||||
'min-sm:max-md:underline',
|
||||
'min-midi:max-maxi:underline',
|
||||
'min-mini:max-midi:underline',
|
||||
|
||||
// Ensure other core variants appear at the end
|
||||
'print:items-end',
|
||||
]),
|
||||
).toMatchInlineSnapshot(`
|
||||
":root {
|
||||
--breakpoint-sm: 40rem;
|
||||
--breakpoint-md: 48rem;
|
||||
--breakpoint-lg: 64rem;
|
||||
--breakpoint-xl: 80rem;
|
||||
--breakpoint-2xl: 96rem;
|
||||
}
|
||||
.mini\\:flex {
|
||||
@media (width >= 40rem) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.sm\\:flex {
|
||||
@media (width >= 40rem) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.min-mini\\:max-midi\\:underline {
|
||||
@media (width >= 40rem) {
|
||||
@media (width < 48rem) {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
.min-sm\\:max-md\\:underline {
|
||||
@media (width >= 40rem) {
|
||||
@media (width < 48rem) {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
.md\\:flex {
|
||||
@media (width >= 48rem) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.midi\\:flex {
|
||||
@media (width >= 48rem) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.min-md\\:max-lg\\:underline {
|
||||
@media (width >= 48rem) {
|
||||
@media (width < 64rem) {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
.min-midi\\:max-maxi\\:underline {
|
||||
@media (width >= 48rem) {
|
||||
@media (width < 64rem) {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
.maxi\\:flex {
|
||||
@media (width >= 64rem) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.print\\:items-end {
|
||||
@media print {
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('JS config with `theme: { extends }` should not include the `default-config` values', async () => {
|
||||
let input = css`
|
||||
@config "./config.js";
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
let compiler = await compile(input, {
|
||||
loadConfig: async () => ({
|
||||
theme: {
|
||||
extend: {
|
||||
screens: {
|
||||
mini: '40rem',
|
||||
midi: '48rem',
|
||||
maxi: '64rem',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
expect(
|
||||
compiler.build(['sm:flex', 'md:flex', 'min-md:max-lg:underline', 'min-sm:max-md:underline']),
|
||||
).toBe('')
|
||||
|
||||
expect(
|
||||
compiler.build([
|
||||
'mini:flex',
|
||||
'midi:flex',
|
||||
'maxi:flex',
|
||||
'min-midi:max-maxi:underline',
|
||||
'min-mini:max-midi:underline',
|
||||
|
||||
// Ensure other core variants appear at the end
|
||||
'print:items-end',
|
||||
]),
|
||||
).toMatchInlineSnapshot(`
|
||||
".mini\\:flex {
|
||||
@media (width >= 40rem) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.min-mini\\:max-midi\\:underline {
|
||||
@media (width >= 40rem) {
|
||||
@media (width < 48rem) {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
.midi\\:flex {
|
||||
@media (width >= 48rem) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.min-midi\\:max-maxi\\:underline {
|
||||
@media (width >= 48rem) {
|
||||
@media (width < 64rem) {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
.maxi\\:flex {
|
||||
@media (width >= 64rem) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.print\\:items-end {
|
||||
@media print {
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
describe('complex screen configs', () => {
|
||||
test('generates utilities', async () => {
|
||||
let input = css`
|
||||
@config "./config.js";
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
let compiler = await compile(input, {
|
||||
loadConfig: async () => ({
|
||||
theme: {
|
||||
extend: {
|
||||
screens: {
|
||||
sm: { max: '639px' },
|
||||
md: [
|
||||
//
|
||||
{ min: '668px', max: '767px' },
|
||||
{ min: '868px' },
|
||||
],
|
||||
lg: { min: '868px' },
|
||||
xl: { min: '1024px', max: '1279px' },
|
||||
tall: { raw: '(min-height: 800px)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
expect(
|
||||
compiler.build(['min-sm:flex', 'min-md:flex', 'min-lg:flex', 'min-xl:flex', 'min-tall:flex']),
|
||||
).toBe('')
|
||||
|
||||
expect(
|
||||
compiler.build([
|
||||
'sm:flex',
|
||||
'md:flex',
|
||||
'lg:flex',
|
||||
'xl:flex',
|
||||
'tall:flex',
|
||||
|
||||
// Ensure other core variants appear at the end
|
||||
'print:items-end',
|
||||
]),
|
||||
).toMatchInlineSnapshot(`
|
||||
".lg\\:flex {
|
||||
@media (min-width: 868px) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.sm\\:flex {
|
||||
@media (max-width: 639px) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.md\\:flex {
|
||||
@media (min-width: 668px and max-width: 767px), (min-width: 868px) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.xl\\:flex {
|
||||
@media (min-width: 1024px and max-width: 1279px) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.tall\\:flex {
|
||||
@media (min-height: 800px) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.print\\:items-end {
|
||||
@media print {
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test("don't interfere with `min-*` and `max-*` variants of non-complex screen configs", async () => {
|
||||
let input = css`
|
||||
@theme default {
|
||||
--breakpoint-sm: 39rem;
|
||||
--breakpoint-md: 48rem;
|
||||
}
|
||||
@config "./config.js";
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
let compiler = await compile(input, {
|
||||
loadConfig: async () => ({
|
||||
theme: {
|
||||
extend: {
|
||||
screens: {
|
||||
sm: '40rem',
|
||||
portrait: { raw: 'screen and (orientation: portrait)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
expect(
|
||||
compiler.build([
|
||||
'sm:flex',
|
||||
'md:flex',
|
||||
'portrait:flex',
|
||||
'min-sm:flex',
|
||||
'min-md:flex',
|
||||
'min-portrait:flex',
|
||||
// Ensure other core variants appear at the end
|
||||
'print:items-end',
|
||||
]),
|
||||
).toMatchInlineSnapshot(`
|
||||
":root {
|
||||
--breakpoint-md: 48rem;
|
||||
}
|
||||
.min-sm\\:flex {
|
||||
@media (width >= 40rem) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.sm\\:flex {
|
||||
@media (width >= 40rem) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.md\\:flex {
|
||||
@media (width >= 48rem) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.min-md\\:flex {
|
||||
@media (width >= 48rem) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.portrait\\:flex {
|
||||
@media screen and (orientation: portrait) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.print\\:items-end {
|
||||
@media print {
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
})
|
||||
105
packages/tailwindcss/src/compat/screens-config.ts
Normal file
105
packages/tailwindcss/src/compat/screens-config.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import { rule } from '../ast'
|
||||
import type { DesignSystem } from '../design-system'
|
||||
import type { ResolvedConfig } from './config/types'
|
||||
|
||||
export function registerScreensConfig(userConfig: ResolvedConfig, designSystem: DesignSystem) {
|
||||
let screens = userConfig.theme.screens || {}
|
||||
|
||||
// We want to insert the breakpoints in the right order as best we can. In the
|
||||
// core utility, all static breakpoint variants and the `min-*` functional
|
||||
// variant are registered inside a group. Since all the variants within a
|
||||
// group share the same order, we can use the always-defined `min-*` variant
|
||||
// as the order.
|
||||
let coreOrder = designSystem.variants.get('min')?.order ?? 0
|
||||
|
||||
let additionalVariants: ((order: number) => void)[] = []
|
||||
|
||||
// Register static breakpoint variants for everything that comes from the user
|
||||
// theme config.
|
||||
for (let [name, value] of Object.entries(screens)) {
|
||||
let coreVariant = designSystem.variants.get(name)
|
||||
|
||||
// Ignore it if there's a CSS value that takes precedence over the JS config
|
||||
// and the static utilities are already registered.
|
||||
//
|
||||
// This happens when a `@theme { }` block is used that overwrites all JS
|
||||
// config options. We rely on the resolution order of the Theme for
|
||||
// resolving this. If Theme has a different value, we know that this is not
|
||||
// coming from the JS plugin and thus we don't need to handle it explicitly.
|
||||
let cssValue = designSystem.theme.resolveValue(name, ['--breakpoint'])
|
||||
if (coreVariant && cssValue && !designSystem.theme.hasDefault(`--breakpoint-${name}`)) {
|
||||
continue
|
||||
}
|
||||
|
||||
let query: string | undefined
|
||||
let deferInsert = true
|
||||
if (typeof value === 'string') {
|
||||
query = `(width >= ${value})`
|
||||
deferInsert = false
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
if (Array.isArray(value)) {
|
||||
query = value.map(ruleForComplexScreenValue).join(', ')
|
||||
} else {
|
||||
query = ruleForComplexScreenValue(value) ?? ''
|
||||
if ('min' in value && !('max' in value)) {
|
||||
deferInsert = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
function insert(order: number) {
|
||||
// `min-*` and `max-*` rules do not need to be reconfigured, as they are
|
||||
// reading the latest values from the theme.
|
||||
designSystem.variants.static(
|
||||
name,
|
||||
(ruleNode) => {
|
||||
ruleNode.nodes = [rule(`@media ${query}`, ruleNode.nodes)]
|
||||
},
|
||||
{ order },
|
||||
)
|
||||
}
|
||||
|
||||
if (deferInsert) {
|
||||
additionalVariants.push(insert)
|
||||
} else {
|
||||
insert(coreOrder)
|
||||
}
|
||||
}
|
||||
|
||||
// Reserve and insert slots for the additional variants
|
||||
if (additionalVariants.length === 0) return
|
||||
|
||||
for (let [, variant] of designSystem.variants.variants) {
|
||||
if (variant.order > coreOrder) variant.order += additionalVariants.length
|
||||
}
|
||||
|
||||
designSystem.variants.compareFns = new Map(
|
||||
Array.from(designSystem.variants.compareFns).map(([key, value]) => {
|
||||
if (key > coreOrder) key += additionalVariants.length
|
||||
return [key, value]
|
||||
}),
|
||||
)
|
||||
|
||||
for (let [index, callback] of additionalVariants.entries()) {
|
||||
callback(coreOrder + index + 1)
|
||||
}
|
||||
}
|
||||
|
||||
function ruleForComplexScreenValue(value: object): string | null {
|
||||
let query = null
|
||||
if ('raw' in value && typeof value.raw === 'string') {
|
||||
query = value.raw
|
||||
} else {
|
||||
let rules: string[] = []
|
||||
|
||||
if ('min' in value) rules.push(`min-width: ${value.min}`)
|
||||
if ('max' in value) rules.push(`max-width: ${value.max}`)
|
||||
|
||||
if (rules.length !== 0) {
|
||||
query = `(${rules.join(' and ')})`
|
||||
}
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
|
@ -13,8 +13,8 @@ type VariantFn<T extends Variant['kind']> = (
|
|||
type CompareFn = (a: Variant, z: Variant) => number
|
||||
|
||||
export class Variants {
|
||||
private compareFns = new Map<number, CompareFn>()
|
||||
private variants = new Map<
|
||||
public compareFns = new Map<number, CompareFn>()
|
||||
public variants = new Map<
|
||||
string,
|
||||
{
|
||||
kind: Variant['kind']
|
||||
|
|
@ -39,8 +39,12 @@ export class Variants {
|
|||
*/
|
||||
private lastOrder = 0
|
||||
|
||||
static(name: string, applyFn: VariantFn<'static'>, { compounds }: { compounds?: boolean } = {}) {
|
||||
this.set(name, { kind: 'static', applyFn, compounds: compounds ?? true })
|
||||
static(
|
||||
name: string,
|
||||
applyFn: VariantFn<'static'>,
|
||||
{ compounds, order }: { compounds?: boolean; order?: number } = {},
|
||||
) {
|
||||
this.set(name, { kind: 'static', applyFn, compounds: compounds ?? true, order })
|
||||
}
|
||||
|
||||
fromAst(name: string, ast: AstNode[]) {
|
||||
|
|
@ -54,17 +58,17 @@ export class Variants {
|
|||
functional(
|
||||
name: string,
|
||||
applyFn: VariantFn<'functional'>,
|
||||
{ compounds }: { compounds?: boolean } = {},
|
||||
{ compounds, order }: { compounds?: boolean; order?: number } = {},
|
||||
) {
|
||||
this.set(name, { kind: 'functional', applyFn, compounds: compounds ?? true })
|
||||
this.set(name, { kind: 'functional', applyFn, compounds: compounds ?? true, order })
|
||||
}
|
||||
|
||||
compound(
|
||||
name: string,
|
||||
applyFn: VariantFn<'compound'>,
|
||||
{ compounds }: { compounds?: boolean } = {},
|
||||
{ compounds, order }: { compounds?: boolean; order?: number } = {},
|
||||
) {
|
||||
this.set(name, { kind: 'compound', applyFn, compounds: compounds ?? true })
|
||||
this.set(name, { kind: 'compound', applyFn, compounds: compounds ?? true, order })
|
||||
}
|
||||
|
||||
group(fn: () => void, compareFn?: CompareFn) {
|
||||
|
|
@ -146,17 +150,25 @@ export class Variants {
|
|||
|
||||
private set<T extends Variant['kind']>(
|
||||
name: string,
|
||||
{ kind, applyFn, compounds }: { kind: T; applyFn: VariantFn<T>; compounds: boolean },
|
||||
{
|
||||
kind,
|
||||
applyFn,
|
||||
compounds,
|
||||
order,
|
||||
}: { kind: T; applyFn: VariantFn<T>; compounds: boolean; order?: number },
|
||||
) {
|
||||
let existing = this.variants.get(name)
|
||||
if (existing) {
|
||||
Object.assign(existing, { kind, applyFn, compounds })
|
||||
} else {
|
||||
this.lastOrder = this.nextOrder()
|
||||
if (order === undefined) {
|
||||
this.lastOrder = this.nextOrder()
|
||||
order = this.lastOrder
|
||||
}
|
||||
this.variants.set(name, {
|
||||
kind,
|
||||
applyFn,
|
||||
order: this.lastOrder,
|
||||
order,
|
||||
compounds,
|
||||
})
|
||||
}
|
||||
|
|
@ -698,7 +710,7 @@ export function createVariants(theme: Theme): Variants {
|
|||
let resolvedBreakpoints = new DefaultMap((variant: Variant) => {
|
||||
switch (variant.kind) {
|
||||
case 'static': {
|
||||
return breakpoints.get(variant.root) ?? null
|
||||
return theme.resolveValue(variant.root, ['--breakpoint']) ?? null
|
||||
}
|
||||
|
||||
case 'functional': {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue