diff --git a/CHANGELOG.md b/CHANGELOG.md index dd3b69192809..f0addeb7945b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ 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)) ### Fixed diff --git a/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap b/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap index 2d4cdf5c5bdb..0ccdfa90546f 100644 --- a/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap +++ b/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap @@ -3375,6 +3375,7 @@ exports[`getClassList 1`] = ` "contain-size", "contain-strict", "contain-style", + "container", "content-around", "content-baseline", "content-between", diff --git a/packages/tailwindcss/src/property-order.ts b/packages/tailwindcss/src/property-order.ts index 6667ec16b062..d8bdb0265270 100644 --- a/packages/tailwindcss/src/property-order.ts +++ b/packages/tailwindcss/src/property-order.ts @@ -28,6 +28,10 @@ export default [ 'float', 'clear', + // Ensure that the included `container` class is always sorted before any + // custom container extensions + '--tw-container-component', + // How do we make `mx-0` come before `mt-0`? // Idea: `margin-x` property that we compile away with a Visitor plugin? 'margin', diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index dfe1535408dc..167f7feb9659 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -3148,6 +3148,230 @@ test('max-height', async () => { ).toEqual('') }) +describe('container', () => { + test('creates the right media queries and sorts it before width', async () => { + expect( + await compileCss( + css` + @theme { + --breakpoint-sm: 40rem; + --breakpoint-md: 48rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; + } + @tailwind utilities; + `, + ['w-1/2', 'container', 'max-w-[var(--breakpoint-sm)]'], + ), + ).toMatchInlineSnapshot(` + ":root { + --breakpoint-sm: 40rem; + --breakpoint-md: 48rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; + } + + .container { + width: 100%; + } + + @media (width >= 40rem) { + .container { + max-width: 40rem; + } + } + + @media (width >= 48rem) { + .container { + max-width: 48rem; + } + } + + @media (width >= 64rem) { + .container { + max-width: 64rem; + } + } + + @media (width >= 80rem) { + .container { + max-width: 80rem; + } + } + + @media (width >= 96rem) { + .container { + max-width: 96rem; + } + } + + .w-1\\/2 { + width: 50%; + } + + .max-w-\\[var\\(--breakpoint-sm\\)\\] { + max-width: var(--breakpoint-sm); + }" + `) + }) + + test('sorts breakpoints based on unit and then in ascending aOrder', async () => { + expect( + await compileCss( + css` + @theme reference { + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-3xl: 1600px; + --breakpoint-sm: 40em; + --breakpoint-2xl: 96rem; + --breakpoint-xs: 30px; + --breakpoint-md: 48em; + } + @tailwind utilities; + `, + ['container'], + ), + ).toMatchInlineSnapshot(` + ".container { + width: 100%; + } + + @media (width >= 40em) { + .container { + max-width: 40em; + } + } + + @media (width >= 48em) { + .container { + max-width: 48em; + } + } + + @media (width >= 30px) { + .container { + max-width: 30px; + } + } + + @media (width >= 1600px) { + .container { + max-width: 1600px; + } + } + + @media (width >= 64rem) { + .container { + max-width: 64rem; + } + } + + @media (width >= 80rem) { + .container { + max-width: 80rem; + } + } + + @media (width >= 96rem) { + .container { + max-width: 96rem; + } + }" + `) + }) + + test('custom `@utility container` always follow the core utility ', async () => { + expect( + await compileCss( + css` + @theme { + --breakpoint-sm: 40rem; + --breakpoint-md: 48rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; + } + @tailwind utilities; + + @utility container { + margin-inline: auto; + padding-inline: 1rem; + + @media (width >= theme(--breakpoint-sm)) { + padding-inline: 2rem; + } + } + `, + ['w-1/2', 'container', 'max-w-[var(--breakpoint-sm)]'], + ), + ).toMatchInlineSnapshot(` + ":root { + --breakpoint-sm: 40rem; + --breakpoint-md: 48rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; + } + + .container { + width: 100%; + } + + @media (width >= 40rem) { + .container { + max-width: 40rem; + } + } + + @media (width >= 48rem) { + .container { + max-width: 48rem; + } + } + + @media (width >= 64rem) { + .container { + max-width: 64rem; + } + } + + @media (width >= 80rem) { + .container { + max-width: 80rem; + } + } + + @media (width >= 96rem) { + .container { + max-width: 96rem; + } + } + + .container { + margin-inline: auto; + padding-inline: 1rem; + } + + @media (width >= 40rem) { + .container { + padding-inline: 2rem; + } + } + + .w-1\\/2 { + width: 50%; + } + + .max-w-\\[var\\(--breakpoint-sm\\)\\] { + max-width: var(--breakpoint-sm); + }" + `) + }) +}) + test('flex', async () => { expect( await run([ @@ -16680,7 +16904,7 @@ describe('spacing utilities', () => { `) }) - test('only multiples of 0.25 with no trailing zeroes are supported with the spacing multipler', async () => { + test('only multiples of 0.25 with no trailing zeroes are supported with the spacing multiplier', async () => { let { build } = await compile(css` @theme { --spacing: 4px; diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index 785b5ff6c119..f979b4a583d6 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -1,6 +1,7 @@ import { atRoot, atRule, decl, styleRule, type AstNode } from './ast' import type { Candidate, CandidateModifier, NamedUtilityValue } from './candidate' import type { Theme, ThemeKey } from './theme' +import { compareBreakpoints } from './utils/compare-breakpoints' import { DefaultMap } from './utils/default-map' import { inferDataType, @@ -897,6 +898,18 @@ export function createUtilities(theme: Theme) { }) } + utilities.static('container', () => { + let breakpoints = [...theme.namespace('--breakpoint').values()] + breakpoints.sort((a, z) => compareBreakpoints(a, z, 'asc')) + + 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)])) + } + + return decls + }) + /** * @css `flex` */ diff --git a/packages/tailwindcss/src/utils/compare-breakpoints.ts b/packages/tailwindcss/src/utils/compare-breakpoints.ts new file mode 100644 index 000000000000..08e97998ea54 --- /dev/null +++ b/packages/tailwindcss/src/utils/compare-breakpoints.ts @@ -0,0 +1,48 @@ +export function compareBreakpoints(a: string, z: string, direction: 'asc' | 'desc') { + if (a === z) return 0 + + // Assumption: when a `(` exists, we are dealing with a CSS function. + // + // E.g.: `calc(100% - 1rem)` + let aIsCssFunction = a.indexOf('(') + let zIsCssFunction = z.indexOf('(') + + let aBucket = + aIsCssFunction === -1 + ? // No CSS function found, bucket by unit instead + a.replace(/[\d.]+/g, '') + : // CSS function found, bucket by function name + a.slice(0, aIsCssFunction) + + let zBucket = + zIsCssFunction === -1 + ? // No CSS function found, bucket by unit + z.replace(/[\d.]+/g, '') + : // CSS function found, bucket by function name + z.slice(0, zIsCssFunction) + + let order = + // Compare by bucket name + (aBucket === zBucket ? 0 : aBucket < zBucket ? -1 : 1) || + // If bucket names are the same, compare by value + (direction === 'asc' ? parseInt(a) - parseInt(z) : parseInt(z) - parseInt(a)) + + // If the groups are the same, and the contents are not numbers, the + // `order` will result in `NaN`. In this case, we want to make sorting + // stable by falling back to a string comparison. + // + // This can happen when using CSS functions such as `calc`. + // + // E.g.: + // + // - `min-[calc(100%-1rem)]` and `min-[calc(100%-2rem)]` + // - `@[calc(100%-1rem)]` and `@[calc(100%-2rem)]` + // + // In this scenario, we want to alphabetically sort `calc(100%-1rem)` and + // `calc(100%-2rem)` to make it deterministic. + if (Number.isNaN(order)) { + return a < z ? -1 : 1 + } + + return order +} diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 7174323eb92a..2a636f20831c 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -13,6 +13,7 @@ import { } from './ast' import { type Variant } from './candidate' import type { Theme } from './theme' +import { compareBreakpoints } from './utils/compare-breakpoints' import { DefaultMap } from './utils/default-map' import { isPositiveInteger } from './utils/infer-data-type' import { segment } from './utils/segment' @@ -869,7 +870,7 @@ export function createVariants(theme: Theme): Variants { // Helper to compare variants by their resolved values, this is used by the // responsive variants (`sm`, `md`, ...), `min-*`, `max-*` and container // queries (`@`). - function compareBreakpoints( + function compareBreakpointVariants( a: Variant, z: Variant, direction: 'asc' | 'desc', @@ -882,54 +883,7 @@ export function createVariants(theme: Theme): Variants { let zValue = lookup.get(z) if (zValue === null) return direction === 'asc' ? 1 : -1 - if (aValue === zValue) return 0 - - // Assumption: when a `(` exists, we are dealing with a CSS function. - // - // E.g.: `calc(100% - 1rem)` - let aIsCssFunction = aValue.indexOf('(') - let zIsCssFunction = zValue.indexOf('(') - - let aBucket = - aIsCssFunction === -1 - ? // No CSS function found, bucket by unit instead - aValue.replace(/[\d.]+/g, '') - : // CSS function found, bucket by function name - aValue.slice(0, aIsCssFunction) - - let zBucket = - zIsCssFunction === -1 - ? // No CSS function found, bucket by unit - zValue.replace(/[\d.]+/g, '') - : // CSS function found, bucket by function name - zValue.slice(0, zIsCssFunction) - - let order = - // Compare by bucket name - (aBucket === zBucket ? 0 : aBucket < zBucket ? -1 : 1) || - // If bucket names are the same, compare by value - (direction === 'asc' - ? parseInt(aValue) - parseInt(zValue) - : parseInt(zValue) - parseInt(aValue)) - - // If the groups are the same, and the contents are not numbers, the - // `order` will result in `NaN`. In this case, we want to make sorting - // stable by falling back to a string comparison. - // - // This can happen when using CSS functions such as `calc`. - // - // E.g.: - // - // - `min-[calc(100%-1rem)]` and `min-[calc(100%-2rem)]` - // - `@[calc(100%-1rem)]` and `@[calc(100%-2rem)]` - // - // In this scenario, we want to alphabetically sort `calc(100%-1rem)` and - // `calc(100%-2rem)` to make it deterministic. - if (Number.isNaN(order)) { - return aValue < zValue ? -1 : 1 - } - - return order + return compareBreakpoints(aValue, zValue, direction) } // Breakpoints @@ -978,7 +932,7 @@ export function createVariants(theme: Theme): Variants { { compounds: Compounds.AtRules }, ) }, - (a, z) => compareBreakpoints(a, z, 'desc', resolvedBreakpoints), + (a, z) => compareBreakpointVariants(a, z, 'desc', resolvedBreakpoints), ) variants.suggest( @@ -1013,7 +967,7 @@ export function createVariants(theme: Theme): Variants { { compounds: Compounds.AtRules }, ) }, - (a, z) => compareBreakpoints(a, z, 'asc', resolvedBreakpoints), + (a, z) => compareBreakpointVariants(a, z, 'asc', resolvedBreakpoints), ) variants.suggest( @@ -1072,7 +1026,7 @@ export function createVariants(theme: Theme): Variants { { compounds: Compounds.AtRules }, ) }, - (a, z) => compareBreakpoints(a, z, 'desc', resolvedWidths), + (a, z) => compareBreakpointVariants(a, z, 'desc', resolvedWidths), ) variants.suggest( @@ -1119,7 +1073,7 @@ export function createVariants(theme: Theme): Variants { { compounds: Compounds.AtRules }, ) }, - (a, z) => compareBreakpoints(a, z, 'asc', resolvedWidths), + (a, z) => compareBreakpointVariants(a, z, 'asc', resolvedWidths), ) variants.suggest(