mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Add codemod and interop for legacy container component configu (#14999)
This PR adds support for handling v3 [`container` customizations ](https://tailwindcss.com/docs/container#customizing). This is done by adding a custom utility to extend the core `container` utility. A concrete example can be taken from the added integration test. ### Input ```ts /** @type {import('tailwindcss').Config} */ export default { content: ['./src/**/*.html'], theme: { container: { center: true, padding: { DEFAULT: '2rem', '2xl': '4rem', }, screens: { md: '48rem', // Matches a default --breakpoint xl: '1280px', '2xl': '1536px', }, }, }, } ``` ### Output ```css @import "tailwindcss"; @utility container { margin-inline: auto; padding-inline: 2rem; @media (width >= theme(--breakpoint-sm)) { max-width: none; } @media (width >= 48rem) { max-width: 48rem; } @media (width >= 1280px) { max-width: 1280px; } @media (width >= 1536px) { max-width: 1536px; padding-inline: 4rem; } } ```` ## Test Plan This PR adds extensive tests to the compat layer as part of unit tests. Additionally it does at a test to the codemod setup that shows that the right `@utility` code is generated. Furthermore I compared the implementation against v3 on both the compat layer and the custom `@utility`: https://github.com/user-attachments/assets/44d6cbfb-4861-4225-9593-602b719f628f
This commit is contained in:
parent
4079059420
commit
890f18de93
@ -11,7 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
- Support opacity values in increments of `0.25` by default ([#14980](https://github.com/tailwindlabs/tailwindcss/pull/14980))
|
||||
- Support specifying the color interpolation method for gradients via modifier ([#14984](https://github.com/tailwindlabs/tailwindcss/pull/14984))
|
||||
- Reintroduce `container` component as a utility ([#14993](https://github.com/tailwindlabs/tailwindcss/pull/14993))
|
||||
- Reintroduce `container` component as a utility ([#14993](https://github.com/tailwindlabs/tailwindcss/pull/14993), [#14999](https://github.com/tailwindlabs/tailwindcss/pull/14999))
|
||||
- _Upgrade (experimental)_: Migrate `container` component configuration to CSS ([#14999](https://github.com/tailwindlabs/tailwindcss/pull/14999))
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
@ -1435,4 +1435,97 @@ describe('border compatibility', () => {
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'migrates `container` component configurations',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"@tailwindcss/upgrade": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'tailwind.config.ts': ts`
|
||||
import { type Config } from 'tailwindcss'
|
||||
|
||||
export default {
|
||||
content: ['./src/**/*.html'],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: {
|
||||
DEFAULT: '2rem',
|
||||
'2xl': '4rem',
|
||||
},
|
||||
screens: {
|
||||
md: '48rem', // Matches a default --breakpoint
|
||||
xl: '1280px',
|
||||
'2xl': '1536px',
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Config
|
||||
`,
|
||||
'src/input.css': css`
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
`,
|
||||
'src/index.html': html`
|
||||
<div class="container"></div>
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ exec, fs }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.{css,html}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- src/index.html ---
|
||||
<div class="container"></div>
|
||||
|
||||
--- src/input.css ---
|
||||
@import 'tailwindcss';
|
||||
|
||||
@utility container {
|
||||
margin-inline: auto;
|
||||
padding-inline: 2rem;
|
||||
@media (width >= theme(--breakpoint-sm)) {
|
||||
max-width: none;
|
||||
}
|
||||
@media (width >= 48rem) {
|
||||
max-width: 48rem;
|
||||
}
|
||||
@media (width >= 1280px) {
|
||||
max-width: 1280px;
|
||||
}
|
||||
@media (width >= 1536px) {
|
||||
max-width: 1536px;
|
||||
padding-inline: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
The default border color has changed to \`currentColor\` in Tailwind CSS v4,
|
||||
so we've added these compatibility styles to make sure everything still
|
||||
looks the same as it did with Tailwind CSS v3.
|
||||
|
||||
If we ever want to remove these styles, we need to add an explicit border
|
||||
color utility to any element that depends on these defaults.
|
||||
*/
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentColor);
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url'
|
||||
import { type Config } from 'tailwindcss'
|
||||
import defaultTheme from 'tailwindcss/defaultTheme'
|
||||
import { loadModule } from '../../@tailwindcss-node/src/compile'
|
||||
import { toCss, type AstNode } from '../../tailwindcss/src/ast'
|
||||
import { atRule, toCss, type AstNode } from '../../tailwindcss/src/ast'
|
||||
import {
|
||||
keyPathToCssProperty,
|
||||
themeableValues,
|
||||
@ -13,6 +13,7 @@ import {
|
||||
import { keyframesToRules } from '../../tailwindcss/src/compat/apply-keyframes-to-theme'
|
||||
import { resolveConfig, type ConfigFile } from '../../tailwindcss/src/compat/config/resolve-config'
|
||||
import type { ResolvedConfig, ThemeConfig } from '../../tailwindcss/src/compat/config/types'
|
||||
import { buildCustomContainerUtilityRules } from '../../tailwindcss/src/compat/container'
|
||||
import { darkModePlugin } from '../../tailwindcss/src/compat/dark-mode'
|
||||
import type { DesignSystem } from '../../tailwindcss/src/design-system'
|
||||
import { escape } from '../../tailwindcss/src/utils/escape'
|
||||
@ -148,6 +149,14 @@ async function migrateTheme(
|
||||
}
|
||||
|
||||
css += '}\n' // @theme
|
||||
|
||||
if ('container' in resolvedConfig.theme) {
|
||||
let rules = buildCustomContainerUtilityRules(resolvedConfig.theme.container, designSystem)
|
||||
if (rules.length > 0) {
|
||||
css += '\n' + toCss([atRule('@utility', 'container', rules)])
|
||||
}
|
||||
}
|
||||
|
||||
css += '}\n' // @tw-bucket
|
||||
|
||||
return css
|
||||
|
||||
@ -6,6 +6,7 @@ import { applyKeyframesToTheme } from './apply-keyframes-to-theme'
|
||||
import { createCompatConfig } from './config/create-compat-config'
|
||||
import { resolveConfig } from './config/resolve-config'
|
||||
import type { UserConfig } from './config/types'
|
||||
import { registerContainerCompat } from './container'
|
||||
import { darkModePlugin } from './dark-mode'
|
||||
import { buildPluginApi, type CssPluginOptions, type Plugin } from './plugin-api'
|
||||
import { registerScreensConfig } from './screens-config'
|
||||
@ -239,6 +240,7 @@ function upgradeToFullPluginSupport({
|
||||
|
||||
registerThemeVariantOverrides(resolvedUserConfig, designSystem)
|
||||
registerScreensConfig(resolvedUserConfig, designSystem)
|
||||
registerContainerCompat(resolvedUserConfig, designSystem)
|
||||
|
||||
// If a prefix has already been set in CSS don't override it
|
||||
if (!designSystem.theme.prefix && resolvedConfig.prefix) {
|
||||
|
||||
@ -128,6 +128,9 @@ export function themeableValues(config: ResolvedConfig['theme']): [string[], unk
|
||||
const IS_VALID_KEY = /^[a-zA-Z0-9-_%/\.]+$/
|
||||
|
||||
export function keyPathToCssProperty(path: string[]) {
|
||||
// The legacy container component config should not be included in the Theme
|
||||
if (path[0] === 'container') return null
|
||||
|
||||
path = structuredClone(path)
|
||||
|
||||
if (path[0] === 'animation') path[0] = 'animate'
|
||||
|
||||
603
packages/tailwindcss/src/compat/container-config.test.ts
Normal file
603
packages/tailwindcss/src/compat/container-config.test.ts
Normal file
@ -0,0 +1,603 @@
|
||||
import { expect, test } from 'vitest'
|
||||
import { compile } from '..'
|
||||
|
||||
const css = String.raw
|
||||
|
||||
test('creates a custom utility to extend the built-in container', 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, {
|
||||
loadModule: async () => ({
|
||||
module: {
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: '2rem',
|
||||
},
|
||||
},
|
||||
},
|
||||
base: '/root',
|
||||
}),
|
||||
})
|
||||
|
||||
expect(compiler.build(['container'])).toMatchInlineSnapshot(`
|
||||
":root {
|
||||
--breakpoint-sm: 40rem;
|
||||
--breakpoint-md: 48rem;
|
||||
--breakpoint-lg: 64rem;
|
||||
--breakpoint-xl: 80rem;
|
||||
--breakpoint-2xl: 96rem;
|
||||
}
|
||||
.container {
|
||||
width: 100%;
|
||||
@media (width >= 40rem) {
|
||||
max-width: 40rem;
|
||||
}
|
||||
@media (width >= 48rem) {
|
||||
max-width: 48rem;
|
||||
}
|
||||
@media (width >= 64rem) {
|
||||
max-width: 64rem;
|
||||
}
|
||||
@media (width >= 80rem) {
|
||||
max-width: 80rem;
|
||||
}
|
||||
@media (width >= 96rem) {
|
||||
max-width: 96rem;
|
||||
}
|
||||
}
|
||||
.container {
|
||||
margin-inline: auto;
|
||||
padding-inline: 2rem;
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('allows padding to be defined at custom breakpoints', 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, {
|
||||
loadModule: async () => ({
|
||||
module: {
|
||||
theme: {
|
||||
container: {
|
||||
padding: {
|
||||
// The order here is messed up on purpose
|
||||
'2xl': '3rem',
|
||||
DEFAULT: '1rem',
|
||||
lg: '2rem',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
base: '/root',
|
||||
}),
|
||||
})
|
||||
|
||||
expect(compiler.build(['container'])).toMatchInlineSnapshot(`
|
||||
":root {
|
||||
--breakpoint-sm: 40rem;
|
||||
--breakpoint-md: 48rem;
|
||||
--breakpoint-lg: 64rem;
|
||||
--breakpoint-xl: 80rem;
|
||||
--breakpoint-2xl: 96rem;
|
||||
}
|
||||
.container {
|
||||
width: 100%;
|
||||
@media (width >= 40rem) {
|
||||
max-width: 40rem;
|
||||
}
|
||||
@media (width >= 48rem) {
|
||||
max-width: 48rem;
|
||||
}
|
||||
@media (width >= 64rem) {
|
||||
max-width: 64rem;
|
||||
}
|
||||
@media (width >= 80rem) {
|
||||
max-width: 80rem;
|
||||
}
|
||||
@media (width >= 96rem) {
|
||||
max-width: 96rem;
|
||||
}
|
||||
}
|
||||
.container {
|
||||
padding-inline: 1rem;
|
||||
@media (width >= 64rem) {
|
||||
padding-inline: 2rem;
|
||||
}
|
||||
@media (width >= 96rem) {
|
||||
padding-inline: 3rem;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('allows breakpoints to be overwritten', 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, {
|
||||
loadModule: async () => ({
|
||||
module: {
|
||||
theme: {
|
||||
container: {
|
||||
screens: {
|
||||
xl: '1280px',
|
||||
'2xl': '1536px',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
base: '/root',
|
||||
}),
|
||||
})
|
||||
|
||||
expect(compiler.build(['container'])).toMatchInlineSnapshot(`
|
||||
":root {
|
||||
--breakpoint-sm: 40rem;
|
||||
--breakpoint-md: 48rem;
|
||||
--breakpoint-lg: 64rem;
|
||||
--breakpoint-xl: 80rem;
|
||||
--breakpoint-2xl: 96rem;
|
||||
}
|
||||
.container {
|
||||
width: 100%;
|
||||
@media (width >= 40rem) {
|
||||
max-width: 40rem;
|
||||
}
|
||||
@media (width >= 48rem) {
|
||||
max-width: 48rem;
|
||||
}
|
||||
@media (width >= 64rem) {
|
||||
max-width: 64rem;
|
||||
}
|
||||
@media (width >= 80rem) {
|
||||
max-width: 80rem;
|
||||
}
|
||||
@media (width >= 96rem) {
|
||||
max-width: 96rem;
|
||||
}
|
||||
}
|
||||
.container {
|
||||
@media (width >= 40rem) {
|
||||
max-width: none;
|
||||
}
|
||||
@media (width >= 1280px) {
|
||||
max-width: 1280px;
|
||||
}
|
||||
@media (width >= 1536px) {
|
||||
max-width: 1536px;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('padding applies to custom `container` screens', 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, {
|
||||
loadModule: async () => ({
|
||||
module: {
|
||||
theme: {
|
||||
container: {
|
||||
padding: {
|
||||
sm: '2rem',
|
||||
md: '3rem',
|
||||
},
|
||||
screens: {
|
||||
md: '48rem',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
base: '/root',
|
||||
}),
|
||||
})
|
||||
|
||||
expect(compiler.build(['container'])).toMatchInlineSnapshot(`
|
||||
":root {
|
||||
--breakpoint-sm: 40rem;
|
||||
--breakpoint-md: 48rem;
|
||||
--breakpoint-lg: 64rem;
|
||||
--breakpoint-xl: 80rem;
|
||||
--breakpoint-2xl: 96rem;
|
||||
}
|
||||
.container {
|
||||
width: 100%;
|
||||
@media (width >= 40rem) {
|
||||
max-width: 40rem;
|
||||
}
|
||||
@media (width >= 48rem) {
|
||||
max-width: 48rem;
|
||||
}
|
||||
@media (width >= 64rem) {
|
||||
max-width: 64rem;
|
||||
}
|
||||
@media (width >= 80rem) {
|
||||
max-width: 80rem;
|
||||
}
|
||||
@media (width >= 96rem) {
|
||||
max-width: 96rem;
|
||||
}
|
||||
}
|
||||
.container {
|
||||
@media (width >= 40rem) {
|
||||
max-width: none;
|
||||
}
|
||||
@media (width >= 48rem) {
|
||||
max-width: 48rem;
|
||||
padding-inline: 3rem;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test("an empty `screen` config will undo all custom media screens and won't apply any breakpoint-specific padding", 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, {
|
||||
loadModule: async () => ({
|
||||
module: {
|
||||
theme: {
|
||||
container: {
|
||||
padding: {
|
||||
DEFAULT: '1rem',
|
||||
sm: '2rem',
|
||||
md: '3rem',
|
||||
},
|
||||
screens: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
base: '/root',
|
||||
}),
|
||||
})
|
||||
|
||||
expect(compiler.build(['container'])).toMatchInlineSnapshot(`
|
||||
":root {
|
||||
--breakpoint-sm: 40rem;
|
||||
--breakpoint-md: 48rem;
|
||||
--breakpoint-lg: 64rem;
|
||||
--breakpoint-xl: 80rem;
|
||||
--breakpoint-2xl: 96rem;
|
||||
}
|
||||
.container {
|
||||
width: 100%;
|
||||
@media (width >= 40rem) {
|
||||
max-width: 40rem;
|
||||
}
|
||||
@media (width >= 48rem) {
|
||||
max-width: 48rem;
|
||||
}
|
||||
@media (width >= 64rem) {
|
||||
max-width: 64rem;
|
||||
}
|
||||
@media (width >= 80rem) {
|
||||
max-width: 80rem;
|
||||
}
|
||||
@media (width >= 96rem) {
|
||||
max-width: 96rem;
|
||||
}
|
||||
}
|
||||
.container {
|
||||
padding-inline: 1rem;
|
||||
@media (width >= 40rem) {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('legacy container component does not interfere with new --container variables', async () => {
|
||||
let input = css`
|
||||
@theme default {
|
||||
--container-3xs: 16rem;
|
||||
--container-2xs: 18rem;
|
||||
--container-xs: 20rem;
|
||||
--container-sm: 24rem;
|
||||
--container-md: 28rem;
|
||||
--container-lg: 32rem;
|
||||
--container-xl: 36rem;
|
||||
--container-2xl: 42rem;
|
||||
--container-3xl: 48rem;
|
||||
--container-4xl: 56rem;
|
||||
--container-5xl: 64rem;
|
||||
--container-6xl: 72rem;
|
||||
--container-7xl: 80rem;
|
||||
--container-prose: 65ch;
|
||||
}
|
||||
@config "./config.js";
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
let compiler = await compile(input, {
|
||||
loadModule: async () => ({
|
||||
module: {
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: '2rem',
|
||||
},
|
||||
},
|
||||
},
|
||||
base: '/root',
|
||||
}),
|
||||
})
|
||||
|
||||
expect(compiler.build(['max-w-sm'])).toMatchInlineSnapshot(`
|
||||
":root {
|
||||
--container-3xs: 16rem;
|
||||
--container-2xs: 18rem;
|
||||
--container-xs: 20rem;
|
||||
--container-sm: 24rem;
|
||||
--container-md: 28rem;
|
||||
--container-lg: 32rem;
|
||||
--container-xl: 36rem;
|
||||
--container-2xl: 42rem;
|
||||
--container-3xl: 48rem;
|
||||
--container-4xl: 56rem;
|
||||
--container-5xl: 64rem;
|
||||
--container-6xl: 72rem;
|
||||
--container-7xl: 80rem;
|
||||
--container-prose: 65ch;
|
||||
}
|
||||
.max-w-sm {
|
||||
max-width: var(--container-sm);
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('combines custom padding and screen overwrites', 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, {
|
||||
loadModule: async () => ({
|
||||
module: {
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: {
|
||||
DEFAULT: '2rem',
|
||||
'2xl': '4rem',
|
||||
},
|
||||
screens: {
|
||||
md: '48rem', // Matches a default --breakpoint
|
||||
xl: '1280px',
|
||||
'2xl': '1536px',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
base: '/root',
|
||||
}),
|
||||
})
|
||||
|
||||
expect(compiler.build(['container', '!container'])).toMatchInlineSnapshot(`
|
||||
":root {
|
||||
--breakpoint-sm: 40rem;
|
||||
--breakpoint-md: 48rem;
|
||||
--breakpoint-lg: 64rem;
|
||||
--breakpoint-xl: 80rem;
|
||||
--breakpoint-2xl: 96rem;
|
||||
}
|
||||
.\\!container {
|
||||
width: 100% !important;
|
||||
@media (width >= 40rem) {
|
||||
max-width: 40rem !important;
|
||||
}
|
||||
@media (width >= 48rem) {
|
||||
max-width: 48rem !important;
|
||||
}
|
||||
@media (width >= 64rem) {
|
||||
max-width: 64rem !important;
|
||||
}
|
||||
@media (width >= 80rem) {
|
||||
max-width: 80rem !important;
|
||||
}
|
||||
@media (width >= 96rem) {
|
||||
max-width: 96rem !important;
|
||||
}
|
||||
}
|
||||
.container {
|
||||
width: 100%;
|
||||
@media (width >= 40rem) {
|
||||
max-width: 40rem;
|
||||
}
|
||||
@media (width >= 48rem) {
|
||||
max-width: 48rem;
|
||||
}
|
||||
@media (width >= 64rem) {
|
||||
max-width: 64rem;
|
||||
}
|
||||
@media (width >= 80rem) {
|
||||
max-width: 80rem;
|
||||
}
|
||||
@media (width >= 96rem) {
|
||||
max-width: 96rem;
|
||||
}
|
||||
}
|
||||
.\\!container {
|
||||
margin-inline: auto !important;
|
||||
padding-inline: 2rem !important;
|
||||
@media (width >= 40rem) {
|
||||
max-width: none !important;
|
||||
}
|
||||
@media (width >= 48rem) {
|
||||
max-width: 48rem !important;
|
||||
}
|
||||
@media (width >= 1280px) {
|
||||
max-width: 1280px !important;
|
||||
}
|
||||
@media (width >= 1536px) {
|
||||
max-width: 1536px !important;
|
||||
padding-inline: 4rem !important;
|
||||
}
|
||||
}
|
||||
.container {
|
||||
margin-inline: auto;
|
||||
padding-inline: 2rem;
|
||||
@media (width >= 40rem) {
|
||||
max-width: none;
|
||||
}
|
||||
@media (width >= 48rem) {
|
||||
max-width: 48rem;
|
||||
}
|
||||
@media (width >= 1280px) {
|
||||
max-width: 1280px;
|
||||
}
|
||||
@media (width >= 1536px) {
|
||||
max-width: 1536px;
|
||||
padding-inline: 4rem;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('filters out complex breakpoints', 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, {
|
||||
loadModule: async () => ({
|
||||
module: {
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: {
|
||||
DEFAULT: '2rem',
|
||||
'2xl': '4rem',
|
||||
},
|
||||
screens: {
|
||||
sm: '20px',
|
||||
md: { min: '100px' },
|
||||
lg: { max: '200px' },
|
||||
xl: { min: '300px', max: '400px' },
|
||||
'2xl': { raw: 'print' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
base: '/root',
|
||||
}),
|
||||
})
|
||||
|
||||
expect(compiler.build(['container'])).toMatchInlineSnapshot(`
|
||||
":root {
|
||||
--breakpoint-sm: 40rem;
|
||||
--breakpoint-md: 48rem;
|
||||
--breakpoint-lg: 64rem;
|
||||
--breakpoint-xl: 80rem;
|
||||
--breakpoint-2xl: 96rem;
|
||||
}
|
||||
.container {
|
||||
width: 100%;
|
||||
@media (width >= 40rem) {
|
||||
max-width: 40rem;
|
||||
}
|
||||
@media (width >= 48rem) {
|
||||
max-width: 48rem;
|
||||
}
|
||||
@media (width >= 64rem) {
|
||||
max-width: 64rem;
|
||||
}
|
||||
@media (width >= 80rem) {
|
||||
max-width: 80rem;
|
||||
}
|
||||
@media (width >= 96rem) {
|
||||
max-width: 96rem;
|
||||
}
|
||||
}
|
||||
.container {
|
||||
margin-inline: auto;
|
||||
padding-inline: 2rem;
|
||||
@media (width >= 40rem) {
|
||||
max-width: none;
|
||||
}
|
||||
@media (width >= 20px) {
|
||||
max-width: 20px;
|
||||
}
|
||||
@media (width >= 100px) {
|
||||
max-width: 100px;
|
||||
}
|
||||
@media (width >= 300px) {
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
120
packages/tailwindcss/src/compat/container.ts
Normal file
120
packages/tailwindcss/src/compat/container.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import { atRule, decl, type AstNode, type AtRule } from '../ast'
|
||||
import type { DesignSystem } from '../design-system'
|
||||
import { compareBreakpoints } from '../utils/compare-breakpoints'
|
||||
import type { ResolvedConfig } from './config/types'
|
||||
|
||||
export function registerContainerCompat(userConfig: ResolvedConfig, designSystem: DesignSystem) {
|
||||
let container = userConfig.theme.container || {}
|
||||
|
||||
if (typeof container !== 'object' || container === null) {
|
||||
return
|
||||
}
|
||||
|
||||
let rules = buildCustomContainerUtilityRules(container, designSystem)
|
||||
|
||||
if (rules.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
designSystem.utilities.static('container', () => structuredClone(rules))
|
||||
}
|
||||
|
||||
export function buildCustomContainerUtilityRules(
|
||||
{
|
||||
center,
|
||||
padding,
|
||||
screens,
|
||||
}: {
|
||||
center?: boolean
|
||||
padding?: string | {}
|
||||
screens?: {}
|
||||
},
|
||||
designSystem: DesignSystem,
|
||||
): AstNode[] {
|
||||
let rules = []
|
||||
let breakpointOverwrites: null | Map<string, AtRule> = null
|
||||
|
||||
if (center) {
|
||||
rules.push(decl('margin-inline', 'auto'))
|
||||
}
|
||||
|
||||
if (
|
||||
typeof padding === 'string' ||
|
||||
(typeof padding === 'object' && padding !== null && 'DEFAULT' in padding)
|
||||
) {
|
||||
rules.push(
|
||||
decl('padding-inline', typeof padding === 'string' ? padding : (padding.DEFAULT as string)),
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof screens === 'object' && screens !== null) {
|
||||
breakpointOverwrites = new Map()
|
||||
|
||||
// When setting a the `screens` in v3, you were overwriting the default
|
||||
// screens config. To do this in v4, you have to manually unset all core
|
||||
// screens.
|
||||
|
||||
let breakpoints = Array.from(designSystem.theme.namespace('--breakpoint').entries())
|
||||
breakpoints.sort((a, z) => compareBreakpoints(a[1], z[1], 'asc'))
|
||||
if (breakpoints.length > 0) {
|
||||
let [key] = breakpoints[0]
|
||||
// Unset all default breakpoints
|
||||
rules.push(
|
||||
atRule('@media', `(width >= theme(--breakpoint-${key}))`, [decl('max-width', 'none')]),
|
||||
)
|
||||
}
|
||||
|
||||
for (let [key, value] of Object.entries(screens)) {
|
||||
if (typeof value === 'object') {
|
||||
if ('min' in value) {
|
||||
value = value.min
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// We're inlining the breakpoint values because the screens configured in
|
||||
// the `container` option do not have to match the ones defined in the
|
||||
// root `screen` setting.
|
||||
breakpointOverwrites.set(
|
||||
key,
|
||||
atRule('@media', `(width >= ${value})`, [decl('max-width', value)]),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof padding === 'object' && padding !== null) {
|
||||
let breakpoints = Object.entries(padding)
|
||||
.filter(([key]) => key !== 'DEFAULT')
|
||||
.map(([key, value]) => {
|
||||
return [key, designSystem.theme.resolveValue(key, ['--breakpoint']), value]
|
||||
})
|
||||
.filter(Boolean) as [string, string, string][]
|
||||
breakpoints.sort((a, z) => compareBreakpoints(a[1], z[1], 'asc'))
|
||||
|
||||
for (let [key, , value] of breakpoints) {
|
||||
if (breakpointOverwrites && breakpointOverwrites.has(key)) {
|
||||
let overwrite = breakpointOverwrites.get(key)!
|
||||
overwrite.nodes.push(decl('padding-inline', value))
|
||||
} else if (breakpointOverwrites) {
|
||||
// The breakpoint does not exist in the overwritten breakpoints list, so
|
||||
// we skip rendering it.
|
||||
continue
|
||||
} else {
|
||||
rules.push(
|
||||
atRule('@media', `(width >= theme(--breakpoint-${key}))`, [
|
||||
decl('padding-inline', value),
|
||||
]),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (breakpointOverwrites) {
|
||||
for (let [, rule] of breakpointOverwrites) {
|
||||
rules.push(rule)
|
||||
}
|
||||
}
|
||||
|
||||
return rules
|
||||
}
|
||||
@ -904,7 +904,7 @@ export function createUtilities(theme: Theme) {
|
||||
|
||||
let decls: AstNode[] = [decl('--tw-sort', '--tw-container-component'), decl('width', '100%')]
|
||||
for (let breakpoint of breakpoints) {
|
||||
decls.push(atRule('@media', `(min-width: ${breakpoint})`, [decl('max-width', breakpoint)]))
|
||||
decls.push(atRule('@media', `(width >= ${breakpoint})`, [decl('max-width', breakpoint)]))
|
||||
}
|
||||
|
||||
return decls
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user