From 8f159f4b06b5e352130c9a2c00ed877dafff7588 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Wed, 13 Nov 2024 17:40:14 +0100 Subject: [PATCH 1/7] Add `container` utility that mimics the v3 `container` component --- .../__snapshots__/intellisense.test.ts.snap | 1 + packages/tailwindcss/src/property-order.ts | 2 + packages/tailwindcss/src/utilities.test.ts | 164 +++++++++++++++++- packages/tailwindcss/src/utilities.ts | 12 ++ 4 files changed, 178 insertions(+), 1 deletion(-) 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..2c7ba3d6ef16 100644 --- a/packages/tailwindcss/src/property-order.ts +++ b/packages/tailwindcss/src/property-order.ts @@ -28,6 +28,8 @@ export default [ 'float', 'clear', + '--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..d90f09bef6bd 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -3148,6 +3148,168 @@ 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('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-left: auto; + margin-right: auto; + padding-right: 2rem; + padding-left: 2rem; + + @media (min-width: 126rem) { + max-width: 126rem; + } + } + `, + ['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-left: auto; + margin-right: auto; + padding-left: 2rem; + padding-right: 2rem; + } + + @media (width >= 126rem) { + .container { + max-width: 126rem; + } + } + + .w-1\\/2 { + width: 50%; + } + + .max-w-\\[var\\(--breakpoint-sm\\)\\] { + max-width: var(--breakpoint-sm); + }" + `) + }) +}) + test('flex', async () => { expect( await run([ @@ -16680,7 +16842,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..f352ec57f229 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -897,6 +897,18 @@ export function createUtilities(theme: Theme) { }) } + utilities.static('container', () => { + let breakpoints = [...theme.namespace('--breakpoint').values()] + breakpoints.sort((a, b) => parseInt(a) - parseInt(b)) + + 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` */ From f263b8f3a8192c77ffb1ea7994637b9ab52e8cd7 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Wed, 13 Nov 2024 17:50:26 +0100 Subject: [PATCH 2/7] Add change log --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd3b69192809..d7d8b58efb5a 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)) +- Add `container` utility that mimics the v3 `container` component ([#14993](https://github.com/tailwindlabs/tailwindcss/pull/14993)) ### Fixed From 96ce8564a7328b7ad9c165ea5d9b4e43084f5bb5 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Wed, 13 Nov 2024 17:56:07 +0100 Subject: [PATCH 3/7] Update comment --- packages/tailwindcss/src/property-order.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/tailwindcss/src/property-order.ts b/packages/tailwindcss/src/property-order.ts index 2c7ba3d6ef16..d8bdb0265270 100644 --- a/packages/tailwindcss/src/property-order.ts +++ b/packages/tailwindcss/src/property-order.ts @@ -28,6 +28,8 @@ 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`? From df84f6ca555464d44b529798b5c95956c4b690e0 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Wed, 13 Nov 2024 17:57:25 +0100 Subject: [PATCH 4/7] Update CHANGELOG.md Co-authored-by: Adam Wathan --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7d8b58efb5a..f0addeb7945b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +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)) -- Add `container` utility that mimics the v3 `container` component ([#14993](https://github.com/tailwindlabs/tailwindcss/pull/14993)) +- Reintroduce `container` component as a utility ([#14993](https://github.com/tailwindlabs/tailwindcss/pull/14993)) ### Fixed From 6fdb27a5f6bcf22be733c148c606ff9bd7d1089e Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Wed, 13 Nov 2024 18:12:25 +0100 Subject: [PATCH 5/7] Reuse the same sorting as the breakpoint variants --- packages/tailwindcss/src/utilities.test.ts | 66 +++++++++++++++++++ packages/tailwindcss/src/utilities.ts | 3 +- .../src/utils/compare-breakpoints.ts | 46 +++++++++++++ packages/tailwindcss/src/variants.ts | 58 ++-------------- 4 files changed, 121 insertions(+), 52 deletions(-) create mode 100644 packages/tailwindcss/src/utils/compare-breakpoints.ts diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index d90f09bef6bd..f517b8f87ffc 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -3217,6 +3217,72 @@ describe('container', () => { `) }) + 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( diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index f352ec57f229..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, @@ -899,7 +900,7 @@ export function createUtilities(theme: Theme) { utilities.static('container', () => { let breakpoints = [...theme.namespace('--breakpoint').values()] - breakpoints.sort((a, b) => parseInt(a) - parseInt(b)) + 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) { diff --git a/packages/tailwindcss/src/utils/compare-breakpoints.ts b/packages/tailwindcss/src/utils/compare-breakpoints.ts new file mode 100644 index 000000000000..6c25220687cf --- /dev/null +++ b/packages/tailwindcss/src/utils/compare-breakpoints.ts @@ -0,0 +1,46 @@ +export function compareBreakpoints(a: string, z: string, direction: 'asc' | 'desc') { + // 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..be511ecf6467 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', @@ -884,52 +885,7 @@ export function createVariants(theme: Theme): Variants { 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 +934,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 +969,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 +1028,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 +1075,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( From 0c6481c332df7df8cc724f49562d72d9b43ecbba Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Wed, 13 Nov 2024 18:15:23 +0100 Subject: [PATCH 6/7] Move exact comparison into the new utility --- packages/tailwindcss/src/utils/compare-breakpoints.ts | 2 ++ packages/tailwindcss/src/variants.ts | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/tailwindcss/src/utils/compare-breakpoints.ts b/packages/tailwindcss/src/utils/compare-breakpoints.ts index 6c25220687cf..08e97998ea54 100644 --- a/packages/tailwindcss/src/utils/compare-breakpoints.ts +++ b/packages/tailwindcss/src/utils/compare-breakpoints.ts @@ -1,4 +1,6 @@ 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)` diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index be511ecf6467..2a636f20831c 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -883,8 +883,6 @@ export function createVariants(theme: Theme): Variants { let zValue = lookup.get(z) if (zValue === null) return direction === 'asc' ? 1 : -1 - if (aValue === zValue) return 0 - return compareBreakpoints(aValue, zValue, direction) } From e2623dae720107fe315894cde54458e646582129 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Wed, 13 Nov 2024 18:25:04 +0100 Subject: [PATCH 7/7] Update test example --- packages/tailwindcss/src/utilities.test.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index f517b8f87ffc..167f7feb9659 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -3297,13 +3297,11 @@ describe('container', () => { @tailwind utilities; @utility container { - margin-left: auto; - margin-right: auto; - padding-right: 2rem; - padding-left: 2rem; + margin-inline: auto; + padding-inline: 1rem; - @media (min-width: 126rem) { - max-width: 126rem; + @media (width >= theme(--breakpoint-sm)) { + padding-inline: 2rem; } } `, @@ -3353,15 +3351,13 @@ describe('container', () => { } .container { - margin-left: auto; - margin-right: auto; - padding-left: 2rem; - padding-right: 2rem; + margin-inline: auto; + padding-inline: 1rem; } - @media (width >= 126rem) { + @media (width >= 40rem) { .container { - max-width: 126rem; + padding-inline: 2rem; } }