From 3dc0605df815d4baf29ea86b45032453325ee860 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 13 Sep 2024 13:03:20 +0200 Subject: [PATCH 01/18] Support JS config to overwrite breakpoints --- .../src/compat/apply-config-to-theme.ts | 5 +- .../tailwindcss/src/compat/config.test.ts | 62 +++++++++++++++++++ .../tailwindcss/src/compat/default-theme.ts | 10 +-- 3 files changed, 69 insertions(+), 8 deletions(-) diff --git a/packages/tailwindcss/src/compat/apply-config-to-theme.ts b/packages/tailwindcss/src/compat/apply-config-to-theme.ts index 3ba4ddb53acd..be84cbb8074a 100644 --- a/packages/tailwindcss/src/compat/apply-config-to-theme.ts +++ b/packages/tailwindcss/src/compat/apply-config-to-theme.ts @@ -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 diff --git a/packages/tailwindcss/src/compat/config.test.ts b/packages/tailwindcss/src/compat/config.test.ts index a4477ec1496c..ca19505c5d03 100644 --- a/packages/tailwindcss/src/compat/config.test.ts +++ b/packages/tailwindcss/src/compat/config.test.ts @@ -1064,3 +1064,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; + } + } + " + `) +}) diff --git a/packages/tailwindcss/src/compat/default-theme.ts b/packages/tailwindcss/src/compat/default-theme.ts index 5b5c3d2ab9c4..955d839af449 100644 --- a/packages/tailwindcss/src/compat/default-theme.ts +++ b/packages/tailwindcss/src/compat/default-theme.ts @@ -883,11 +883,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'), From af5c36cbf157c97b7070c34764ecc5d54bd6418d Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 13 Sep 2024 13:13:01 +0200 Subject: [PATCH 02/18] Allow variant registration with custom order --- packages/tailwindcss/src/variants.ts | 30 +++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 2bb9def3dece..a337f29368f4 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -38,8 +38,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[]) { @@ -53,17 +57,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) { @@ -145,17 +149,25 @@ export class Variants { private set( name: string, - { kind, applyFn, compounds }: { kind: T; applyFn: VariantFn; compounds: boolean }, + { + kind, + applyFn, + compounds, + order, + }: { kind: T; applyFn: VariantFn; 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, }) } From 13807cf190e1aefd8c8a8dba45afd7da0db692b9 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 13 Sep 2024 13:30:52 +0200 Subject: [PATCH 03/18] Fix merging of CSS `--breakpoint` and JS config `screens` --- .../src/compat/apply-compat-hooks.ts | 2 + .../tailwindcss/src/compat/config.test.ts | 2 +- .../src/compat/screens-config.test.ts | 66 +++++++++++++++++++ .../tailwindcss/src/compat/screens-config.ts | 31 +++++++++ 4 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 packages/tailwindcss/src/compat/screens-config.test.ts create mode 100644 packages/tailwindcss/src/compat/screens-config.ts diff --git a/packages/tailwindcss/src/compat/apply-compat-hooks.ts b/packages/tailwindcss/src/compat/apply-compat-hooks.ts index b21d524f1456..28b5ef5188b2 100644 --- a/packages/tailwindcss/src/compat/apply-compat-hooks.ts +++ b/packages/tailwindcss/src/compat/apply-compat-hooks.ts @@ -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({ @@ -187,6 +188,7 @@ export async function applyCompatibilityHooks({ applyConfigToTheme(designSystem, userConfig) registerThemeVariantOverrides(resolvedConfig, designSystem) + registerScreensConfig(resolvedConfig, designSystem) // Replace `resolveThemeValue` with a version that is backwards compatible // with dot-notation but also aware of any JS theme configurations registered diff --git a/packages/tailwindcss/src/compat/config.test.ts b/packages/tailwindcss/src/compat/config.test.ts index ca19505c5d03..ca7d78849127 100644 --- a/packages/tailwindcss/src/compat/config.test.ts +++ b/packages/tailwindcss/src/compat/config.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from 'vitest' +import { describe, test } from 'vitest' import { compile } from '..' import plugin from '../plugin' import { flattenColorPalette } from './flatten-color-palette' diff --git a/packages/tailwindcss/src/compat/screens-config.test.ts b/packages/tailwindcss/src/compat/screens-config.test.ts new file mode 100644 index 000000000000..a94f58afd56e --- /dev/null +++ b/packages/tailwindcss/src/compat/screens-config.test.ts @@ -0,0 +1,66 @@ +import { expect, test } from 'vitest' +import { compile } from '..' + +const css = String.raw + +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; + } + } + " + `) +}) diff --git a/packages/tailwindcss/src/compat/screens-config.ts b/packages/tailwindcss/src/compat/screens-config.ts new file mode 100644 index 000000000000..9d6db0fc0665 --- /dev/null +++ b/packages/tailwindcss/src/compat/screens-config.ts @@ -0,0 +1,31 @@ +import { rule } from '../ast' +import type { DesignSystem } from '../design-system' +import type { ResolvedConfig } from './config/types' +import DefaultTheme from './default-theme' + +export function registerScreensConfig(config: ResolvedConfig, designSystem: DesignSystem) { + let screens = config.theme.screens || {} + + // Case 1: All theme config are simple (non-nested-object) values. This means + // all breakpoints are used as `min` values, like we do in the core. + for (let [name, value] of Object.entries(screens)) { + // Ignore defaults + if (DefaultTheme.screens[name as 'sm'] === screens[name]) continue + + let coreVariant = designSystem.variants.get(name) + if (!coreVariant) { + throw new Error('not yet supported, needs more thought') + } + + // min-${breakpoint} and max-${breakpoint} rules do not need to be + // reconfigured, as they are using functional utilities and will not eagerly + // capture the breakpoints before the compat layer runs. + designSystem.variants.static( + name, + (ruleNode) => { + ruleNode.nodes = [rule(`@media (width >= ${value})`, ruleNode.nodes)] + }, + { order: coreVariant.order }, + ) + } +} From 3c01e8368e4bb2514871812324f14d949a2a1191 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 13 Sep 2024 13:33:50 +0200 Subject: [PATCH 04/18] Clean up test --- .../src/compat/screens-config.test.ts | 73 ++++++++++++------- 1 file changed, 47 insertions(+), 26 deletions(-) diff --git a/packages/tailwindcss/src/compat/screens-config.test.ts b/packages/tailwindcss/src/compat/screens-config.test.ts index a94f58afd56e..86b20baffe03 100644 --- a/packages/tailwindcss/src/compat/screens-config.test.ts +++ b/packages/tailwindcss/src/compat/screens-config.test.ts @@ -3,7 +3,7 @@ import { compile } from '..' const css = String.raw -test('merges css breakpoints with js config screens', async () => { +test('merges CSS `--breakpoint-*` with JS config `screens`', async () => { let input = css` @theme default { --breakpoint-sm: 40rem; @@ -31,36 +31,57 @@ test('merges css breakpoints with js config screens', async () => { }), }) - 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; + 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; } - .sm\\:flex { - @media (width >= 44rem) { - display: flex; + } + .min-sm\\:max-md\\:underline { + @media (width >= 44rem) { + @media (width < 50rem) { + text-decoration-line: underline; } } - .min-sm\\:max-md\\:underline { - @media (width >= 44rem) { - @media (width < 50rem) { - text-decoration-line: underline; - } - } + } + .md\\:flex { + @media (width >= 50rem) { + display: flex; } - .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; - } + } + .lg\\:flex { + @media (width >= 64rem) { + display: flex; } - " - `) + } + .print\\:items-end { + @media print { + align-items: flex-end; + } + } + " + `) }) From 5fbc78206aecf7124ffc2b6eec3fd411b4e8b87d Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 13 Sep 2024 13:49:53 +0200 Subject: [PATCH 05/18] Allow JS theme to extend CSS theme --- .../src/compat/screens-config.test.ts | 96 ++++++++++++++++++- .../tailwindcss/src/compat/screens-config.ts | 32 +++++-- 2 files changed, 121 insertions(+), 7 deletions(-) diff --git a/packages/tailwindcss/src/compat/screens-config.test.ts b/packages/tailwindcss/src/compat/screens-config.test.ts index 86b20baffe03..0bdc8a70e93d 100644 --- a/packages/tailwindcss/src/compat/screens-config.test.ts +++ b/packages/tailwindcss/src/compat/screens-config.test.ts @@ -3,7 +3,7 @@ import { compile } from '..' const css = String.raw -test('merges CSS `--breakpoint-*` with JS config `screens`', async () => { +test('CSS `--breakpoint-*` merge with JS config `screens`', async () => { let input = css` @theme default { --breakpoint-sm: 40rem; @@ -85,3 +85,97 @@ test('merges CSS `--breakpoint-*` with JS config `screens`', async () => { " `) }) + +test('JS config `screens` extend CSS `--breakpoint-*`', async () => { + let input = css` + @theme default { + --breakpoint-sm: 40rem; + --breakpoint-md: 48rem; + } + @theme { + --breakpoint-md: 50rem; + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadConfig: async () => ({ + theme: { + extend: { + screens: { + xs: '30rem', + sm: '44rem', + md: '49rem', + lg: '64rem', + }, + }, + }, + }), + }) + + 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:max-md:underline', + + // Ensure other core variants appear at the end + 'print:items-end', + ]), + ).toMatchInlineSnapshot(` + ":root { + --breakpoint-md: 50rem; + } + .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 >= 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; + } + } + } + .print\\:items-end { + @media print { + align-items: flex-end; + } + } + " + `) +}) + +test('JS config `screens` only setup') +test('JS config `screens` overwrite CSS `--breakpoint-*`') diff --git a/packages/tailwindcss/src/compat/screens-config.ts b/packages/tailwindcss/src/compat/screens-config.ts index 9d6db0fc0665..ef01fab1bab6 100644 --- a/packages/tailwindcss/src/compat/screens-config.ts +++ b/packages/tailwindcss/src/compat/screens-config.ts @@ -6,15 +6,35 @@ import DefaultTheme from './default-theme' export function registerScreensConfig(config: ResolvedConfig, designSystem: DesignSystem) { let screens = config.theme.screens || {} + // We want to insert the breakpoints in the right order as best we can, we + // know that the `max` functional variant is added before all min variants, so + // we use this as our lower bound. + let lastKnownOrder = designSystem.variants.get('max')?.order ?? 0 + // Case 1: All theme config are simple (non-nested-object) values. This means // all breakpoints are used as `min` values, like we do in the core. for (let [name, value] of Object.entries(screens)) { - // Ignore defaults - if (DefaultTheme.screens[name as 'sm'] === screens[name]) continue - let coreVariant = designSystem.variants.get(name) - if (!coreVariant) { - throw new Error('not yet supported, needs more thought') + + // Ignore defaults, but update the order accordingly + // + // Note: We can't rely on the `designSystem.theme` for this, as it has the + // JS config values applied already. + if (DefaultTheme.screens[name as 'sm'] === screens[name]) { + if (coreVariant) lastKnownOrder = coreVariant.order + continue + } + + // Ignore it if there's a CSS value that takes precedence over the JS config + // + // This happens when a `@theme { }` block is used that overwrites all JS + // config options. We rely on the order inside 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 (cssValue && cssValue !== value) { + if (coreVariant) lastKnownOrder = coreVariant.order + continue } // min-${breakpoint} and max-${breakpoint} rules do not need to be @@ -25,7 +45,7 @@ export function registerScreensConfig(config: ResolvedConfig, designSystem: Desi (ruleNode) => { ruleNode.nodes = [rule(`@media (width >= ${value})`, ruleNode.nodes)] }, - { order: coreVariant.order }, + { order: lastKnownOrder }, ) } } From ef26cee12c595d632e81490f636d4b687eb95c79 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 13 Sep 2024 16:35:43 +0200 Subject: [PATCH 06/18] Fix order of static utilities --- .../src/compat/screens-config.test.ts | 209 ++++++++++++++++-- .../tailwindcss/src/compat/screens-config.ts | 38 ++-- packages/tailwindcss/src/index.test.ts | 2 +- packages/tailwindcss/src/variants.ts | 2 +- 4 files changed, 220 insertions(+), 31 deletions(-) diff --git a/packages/tailwindcss/src/compat/screens-config.test.ts b/packages/tailwindcss/src/compat/screens-config.test.ts index 0bdc8a70e93d..fdedb5bdb518 100644 --- a/packages/tailwindcss/src/compat/screens-config.test.ts +++ b/packages/tailwindcss/src/compat/screens-config.test.ts @@ -48,11 +48,6 @@ test('CSS `--breakpoint-*` merge with JS config `screens`', async () => { --breakpoint-xl: 80rem; --breakpoint-2xl: 96rem; } - .sm\\:flex { - @media (width >= 44rem) { - display: flex; - } - } .min-sm\\:max-md\\:underline { @media (width >= 44rem) { @media (width < 50rem) { @@ -60,6 +55,11 @@ test('CSS `--breakpoint-*` merge with JS config `screens`', async () => { } } } + .sm\\:flex { + @media (width >= 44rem) { + display: flex; + } + } .md\\:flex { @media (width >= 50rem) { display: flex; @@ -132,11 +132,6 @@ test('JS config `screens` extend CSS `--breakpoint-*`', async () => { ":root { --breakpoint-md: 50rem; } - .xs\\:flex { - @media (width >= 30rem) { - display: flex; - } - } .min-xs\\:max-md\\:underline { @media (width >= 30rem) { @media (width < 50rem) { @@ -144,8 +139,8 @@ test('JS config `screens` extend CSS `--breakpoint-*`', async () => { } } } - .sm\\:flex { - @media (width >= 44rem) { + .xs\\:flex { + @media (width >= 30rem) { display: flex; } } @@ -156,6 +151,11 @@ test('JS config `screens` extend CSS `--breakpoint-*`', async () => { } } } + .sm\\:flex { + @media (width >= 44rem) { + display: flex; + } + } .md\\:flex { @media (width >= 50rem) { display: flex; @@ -168,6 +168,11 @@ test('JS config `screens` extend CSS `--breakpoint-*`', async () => { } } } + .lg\\:flex { + @media (width >= 64rem) { + display: flex; + } + } .print\\:items-end { @media print { align-items: flex-end; @@ -177,5 +182,181 @@ test('JS config `screens` extend CSS `--breakpoint-*`', async () => { `) }) -test('JS config `screens` only setup') -test('JS config `screens` overwrite CSS `--breakpoint-*`') +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(` + ".min-sm\\:max-md\\:underline { + @media (width >= 40rem) { + @media (width < 48rem) { + text-decoration-line: underline; + } + } + } + .sm\\:flex { + @media (width >= 40rem) { + display: flex; + } + } + .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-*` and can remove 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, { + 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; + } + .min-sm\\:max-md\\:underline { + @media (width >= 40rem) { + @media (width < 48rem) { + text-decoration-line: underline; + } + } + } + .min-mini\\:max-midi\\:underline { + @media (width >= 40rem) { + @media (width < 48rem) { + text-decoration-line: underline; + } + } + } + .mini\\:flex { + @media (width >= 40rem) { + display: flex; + } + } + .sm\\:flex { + @media (width >= 40rem) { + display: flex; + } + } + .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; + } + } + " + `) +}) diff --git a/packages/tailwindcss/src/compat/screens-config.ts b/packages/tailwindcss/src/compat/screens-config.ts index ef01fab1bab6..e8351650c8fa 100644 --- a/packages/tailwindcss/src/compat/screens-config.ts +++ b/packages/tailwindcss/src/compat/screens-config.ts @@ -6,34 +6,40 @@ import DefaultTheme from './default-theme' export function registerScreensConfig(config: ResolvedConfig, designSystem: DesignSystem) { let screens = config.theme.screens || {} - // We want to insert the breakpoints in the right order as best we can, we - // know that the `max` functional variant is added before all min variants, so - // we use this as our lower bound. - let lastKnownOrder = designSystem.variants.get('max')?.order ?? 0 + // 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 order = designSystem.variants.get('min')?.order ?? undefined - // Case 1: All theme config are simple (non-nested-object) values. This means + // Case A: All theme config are simple (non-nested-object) values. This means // all breakpoints are used as `min` values, like we do in the core. + + // Step 1: 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 defaults, but update the order accordingly + // Ignore defaults if they are already registered // // Note: We can't rely on the `designSystem.theme` for this, as it has the - // JS config values applied already. - if (DefaultTheme.screens[name as 'sm'] === screens[name]) { - if (coreVariant) lastKnownOrder = coreVariant.order + // JS config values applied already. However, the DefaultTheme might not + // match what is actually already set in the designSystem since the @theme + // is set at runtime. + if (coreVariant && DefaultTheme.screens[name as 'sm'] === screens[name]) { continue } // 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 order inside 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. + // 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 (cssValue && cssValue !== value) { - if (coreVariant) lastKnownOrder = coreVariant.order + if (coreVariant && cssValue && cssValue !== value) { continue } @@ -45,7 +51,9 @@ export function registerScreensConfig(config: ResolvedConfig, designSystem: Desi (ruleNode) => { ruleNode.nodes = [rule(`@media (width >= ${value})`, ruleNode.nodes)] }, - { order: lastKnownOrder }, + { order }, ) } + + // } diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index fb226708e689..0da4758d8c10 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -1416,7 +1416,7 @@ describe('Parsing themes values from CSS', () => { `) }) - test('`default` theme values can be overridden by plugin theme values', async () => { + test.only('`default` theme values can be overridden by plugin theme values', async () => { let { build } = await compile( css` @theme default { diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index a337f29368f4..844162da4820 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -709,7 +709,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': { From a918e8b0b4c6aa044282f8d40ff5af79a97dd6d2 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 13 Sep 2024 17:54:42 +0200 Subject: [PATCH 07/18] WIP next steps, check compile.ts,make something that returns a nested array --- .../src/compat/screens-config.test.ts | 151 +++++++++++++----- .../tailwindcss/src/compat/screens-config.ts | 3 +- packages/tailwindcss/src/compile.ts | 27 ++++ packages/tailwindcss/src/variants.test.ts | 4 +- packages/tailwindcss/src/variants.ts | 7 + 5 files changed, 151 insertions(+), 41 deletions(-) diff --git a/packages/tailwindcss/src/compat/screens-config.test.ts b/packages/tailwindcss/src/compat/screens-config.test.ts index fdedb5bdb518..7fade8ce8296 100644 --- a/packages/tailwindcss/src/compat/screens-config.test.ts +++ b/packages/tailwindcss/src/compat/screens-config.test.ts @@ -3,7 +3,7 @@ import { compile } from '..' const css = String.raw -test('CSS `--breakpoint-*` merge with JS config `screens`', async () => { +test.skip('CSS `--breakpoint-*` merge with JS config `screens`', async () => { let input = css` @theme default { --breakpoint-sm: 40rem; @@ -86,10 +86,10 @@ test('CSS `--breakpoint-*` merge with JS config `screens`', async () => { `) }) -test('JS config `screens` extend CSS `--breakpoint-*`', async () => { +test.skip('JS config `screens` extend CSS `--breakpoint-*`', async () => { let input = css` @theme default { - --breakpoint-sm: 40rem; + --breakpoint-xs: 30rem; --breakpoint-md: 48rem; } @theme { @@ -117,21 +117,27 @@ test('JS config `screens` extend CSS `--breakpoint-*`', async () => { expect( compiler.build([ // Order is messed up on purpose - 'md:flex', - 'sm:flex', - 'lg:flex', + /* 'md:flex', */ + /* 'sm:flex', */ + /* 'lg:flex', */ 'xs:flex', - 'min-md:max-lg:underline', - 'min-sm:max-md:underline', + /* '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', + /* // Ensure other core variants appear at the end + 'print:items-end', */ ]), ).toMatchInlineSnapshot(` ":root { --breakpoint-md: 50rem; } + .min-xs\\:flex { + @media (width >= 30rem) { + display: flex; + } + } .min-xs\\:max-md\\:underline { @media (width >= 30rem) { @media (width < 50rem) { @@ -144,25 +150,60 @@ test('JS config `screens` extend CSS `--breakpoint-*`', async () => { display: flex; } } - .min-sm\\:max-md\\:underline { - @media (width >= 44rem) { - @media (width < 50rem) { + " + `) +}) + +test.skip('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(` + ".min-sm\\:max-md\\:underline { + @media (width >= 40rem) { + @media (width < 48rem) { text-decoration-line: underline; } } } .sm\\:flex { - @media (width >= 44rem) { + @media (width >= 40rem) { display: flex; } } .md\\:flex { - @media (width >= 50rem) { + @media (width >= 48rem) { display: flex; } } .min-md\\:max-lg\\:underline { - @media (width >= 50rem) { + @media (width >= 48rem) { @media (width < 64rem) { text-decoration-line: underline; } @@ -182,8 +223,15 @@ test('JS config `screens` extend CSS `--breakpoint-*`', async () => { `) }) -test('JS config `screens` only setup, even if those match the default-theme export', async () => { +test.skip('JS config `screens` overwrite CSS `--breakpoint-*` and can remove 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; ` @@ -192,9 +240,9 @@ test('JS config `screens` only setup, even if those match the default-theme expo loadConfig: async () => ({ theme: { screens: { - sm: '40rem', - md: '48rem', - lg: '64rem', + mini: '40rem', + midi: '48rem', + maxi: '64rem', }, }, }), @@ -202,24 +250,46 @@ test('JS config `screens` only setup, even if those match the default-theme expo expect( compiler.build([ - // Order is messed up on purpose - 'md:flex', 'sm:flex', - 'lg: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(` - ".min-sm\\:max-md\\:underline { + ":root { + --breakpoint-sm: 40rem; + --breakpoint-md: 48rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; + } + .min-sm\\:max-md\\:underline { @media (width >= 40rem) { @media (width < 48rem) { text-decoration-line: underline; } } } + .min-mini\\:max-midi\\:underline { + @media (width >= 40rem) { + @media (width < 48rem) { + text-decoration-line: underline; + } + } + } + .mini\\:flex { + @media (width >= 40rem) { + display: flex; + } + } .sm\\:flex { @media (width >= 40rem) { display: flex; @@ -230,6 +300,11 @@ test('JS config `screens` only setup, even if those match the default-theme expo display: flex; } } + .midi\\:flex { + @media (width >= 48rem) { + display: flex; + } + } .min-md\\:max-lg\\:underline { @media (width >= 48rem) { @media (width < 64rem) { @@ -237,7 +312,14 @@ test('JS config `screens` only setup, even if those match the default-theme expo } } } - .lg\\:flex { + .min-midi\\:max-maxi\\:underline { + @media (width >= 48rem) { + @media (width < 64rem) { + text-decoration-line: underline; + } + } + } + .maxi\\:flex { @media (width >= 64rem) { display: flex; } @@ -251,15 +333,8 @@ test('JS config `screens` only setup, even if those match the default-theme expo `) }) -test('JS config `screens` overwrite CSS `--breakpoint-*` and can remove breakpoints', async () => { +test.skip('JS config with `theme: { extends }` should not include the `default-config` values', 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; ` @@ -267,10 +342,12 @@ test('JS config `screens` overwrite CSS `--breakpoint-*` and can remove breakpoi let compiler = await compile(input, { loadConfig: async () => ({ theme: { - screens: { - mini: '40rem', - midi: '48rem', - maxi: '64rem', + extend: { + screens: { + mini: '40rem', + midi: '48rem', + maxi: '64rem', + }, }, }, }), diff --git a/packages/tailwindcss/src/compat/screens-config.ts b/packages/tailwindcss/src/compat/screens-config.ts index e8351650c8fa..ca549fe384a9 100644 --- a/packages/tailwindcss/src/compat/screens-config.ts +++ b/packages/tailwindcss/src/compat/screens-config.ts @@ -4,6 +4,7 @@ import type { ResolvedConfig } from './config/types' import DefaultTheme from './default-theme' export function registerScreensConfig(config: ResolvedConfig, designSystem: DesignSystem) { + return let screens = config.theme.screens || {} // We want to insert the breakpoints in the right order as best we can. In the @@ -54,6 +55,4 @@ export function registerScreensConfig(config: ResolvedConfig, designSystem: Desi { order }, ) } - - // } diff --git a/packages/tailwindcss/src/compile.ts b/packages/tailwindcss/src/compile.ts index 931d16ecdf2f..67e498bb2ce8 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -32,6 +32,23 @@ export function compileCandidates( let variantOrderMap = designSystem.getVariantOrder() + // max-sm 0 => 1 + // min-sm 1 => 2 + // sm 2 => 4 + + // + // max-sm // [1] + // max-md // [1] + // + + // [ + // [max-sm, max-md], + // [[&:foo]], + // [[&:bar]], + // ] + + // To do this, variantOrder needs to be an array and the inner sorting needs to work + // Create the AST for (let [rawCandidate, candidates] of matches) { let found = false @@ -47,9 +64,19 @@ export function compileCandidates( // variant. This allows us to sort the rules based on the order of // variants used. let variantOrder = 0n + console.log(candidate.variants) + for (let variant of candidate.variants) { +<<<<<<< HEAD variantOrder |= 1n << BigInt(variantOrderMap.get(variant)!) +||||||| parent of 8345e050 (WIP next steps, check compile.ts,make something that returns a nested array) + variantOrder |= 1n << BigInt(variants.indexOf(variant)) +======= + console.log(designSystem.variants.getOrders(variant)) + variantOrder |= 1n << BigInt(variants.indexOf(variant)) +>>>>>>> 8345e050 (WIP next steps, check compile.ts,make something that returns a nested array) } + console.log({ variantOrder }) nodeSorting.set(node, { properties: propertySort, diff --git a/packages/tailwindcss/src/variants.test.ts b/packages/tailwindcss/src/variants.test.ts index 40042045c740..4a8f61888cbc 100644 --- a/packages/tailwindcss/src/variants.test.ts +++ b/packages/tailwindcss/src/variants.test.ts @@ -1407,13 +1407,13 @@ test('min, max and unprefixed breakpoints', async () => { } @media (width >= 1024px) { - .lg\\:flex { + .min-lg\\:flex { display: flex; } } @media (width >= 1024px) { - .min-lg\\:flex { + .lg\\:flex { display: flex; } }" diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 844162da4820..561e3f82721f 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -139,6 +139,13 @@ export class Variants { return compareFn(a, z) } + getOrders(a: Variant): number[] { + if (a.kind === 'compound') { + return [this.variants.get(a.root)!.order, ...this.getOrders(a.variant)] + } + return [this.variants.get(a.root)!.order] + } + keys() { return this.variants.keys() } From 35825e016c1667a1ebdec6b0e36c5b1c3c766b8e Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Tue, 17 Sep 2024 11:48:29 +0200 Subject: [PATCH 08/18] Revert debugging code --- .../tailwindcss/src/compat/screens-config.ts | 1 - packages/tailwindcss/src/compile.ts | 27 ------------------- packages/tailwindcss/src/index.test.ts | 2 +- packages/tailwindcss/src/variants.test.ts | 4 +-- packages/tailwindcss/src/variants.ts | 7 ----- 5 files changed, 3 insertions(+), 38 deletions(-) diff --git a/packages/tailwindcss/src/compat/screens-config.ts b/packages/tailwindcss/src/compat/screens-config.ts index ca549fe384a9..b4b0cd4a2a91 100644 --- a/packages/tailwindcss/src/compat/screens-config.ts +++ b/packages/tailwindcss/src/compat/screens-config.ts @@ -4,7 +4,6 @@ import type { ResolvedConfig } from './config/types' import DefaultTheme from './default-theme' export function registerScreensConfig(config: ResolvedConfig, designSystem: DesignSystem) { - return let screens = config.theme.screens || {} // We want to insert the breakpoints in the right order as best we can. In the diff --git a/packages/tailwindcss/src/compile.ts b/packages/tailwindcss/src/compile.ts index 67e498bb2ce8..931d16ecdf2f 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -32,23 +32,6 @@ export function compileCandidates( let variantOrderMap = designSystem.getVariantOrder() - // max-sm 0 => 1 - // min-sm 1 => 2 - // sm 2 => 4 - - // - // max-sm // [1] - // max-md // [1] - // - - // [ - // [max-sm, max-md], - // [[&:foo]], - // [[&:bar]], - // ] - - // To do this, variantOrder needs to be an array and the inner sorting needs to work - // Create the AST for (let [rawCandidate, candidates] of matches) { let found = false @@ -64,19 +47,9 @@ export function compileCandidates( // variant. This allows us to sort the rules based on the order of // variants used. let variantOrder = 0n - console.log(candidate.variants) - for (let variant of candidate.variants) { -<<<<<<< HEAD variantOrder |= 1n << BigInt(variantOrderMap.get(variant)!) -||||||| parent of 8345e050 (WIP next steps, check compile.ts,make something that returns a nested array) - variantOrder |= 1n << BigInt(variants.indexOf(variant)) -======= - console.log(designSystem.variants.getOrders(variant)) - variantOrder |= 1n << BigInt(variants.indexOf(variant)) ->>>>>>> 8345e050 (WIP next steps, check compile.ts,make something that returns a nested array) } - console.log({ variantOrder }) nodeSorting.set(node, { properties: propertySort, diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index 0da4758d8c10..fb226708e689 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -1416,7 +1416,7 @@ describe('Parsing themes values from CSS', () => { `) }) - test.only('`default` theme values can be overridden by plugin theme values', async () => { + test('`default` theme values can be overridden by plugin theme values', async () => { let { build } = await compile( css` @theme default { diff --git a/packages/tailwindcss/src/variants.test.ts b/packages/tailwindcss/src/variants.test.ts index 4a8f61888cbc..40042045c740 100644 --- a/packages/tailwindcss/src/variants.test.ts +++ b/packages/tailwindcss/src/variants.test.ts @@ -1407,13 +1407,13 @@ test('min, max and unprefixed breakpoints', async () => { } @media (width >= 1024px) { - .min-lg\\:flex { + .lg\\:flex { display: flex; } } @media (width >= 1024px) { - .lg\\:flex { + .min-lg\\:flex { display: flex; } }" diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 561e3f82721f..844162da4820 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -139,13 +139,6 @@ export class Variants { return compareFn(a, z) } - getOrders(a: Variant): number[] { - if (a.kind === 'compound') { - return [this.variants.get(a.root)!.order, ...this.getOrders(a.variant)] - } - return [this.variants.get(a.root)!.order] - } - keys() { return this.variants.keys() } From efca65db34e01a764f7f30cfde4125826bdfb953 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Tue, 17 Sep 2024 12:36:56 +0200 Subject: [PATCH 09/18] Only add new breakpoint for user-configued screens --- packages/tailwindcss/src/compat/apply-compat-hooks.ts | 5 +++-- .../tailwindcss/src/compat/apply-config-to-theme.test.ts | 4 +++- packages/tailwindcss/src/compat/apply-config-to-theme.ts | 5 +---- packages/tailwindcss/src/compat/config.test.ts | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/tailwindcss/src/compat/apply-compat-hooks.ts b/packages/tailwindcss/src/compat/apply-compat-hooks.ts index 28b5ef5188b2..7a721d1e2599 100644 --- a/packages/tailwindcss/src/compat/apply-compat-hooks.ts +++ b/packages/tailwindcss/src/compat/apply-compat-hooks.ts @@ -175,6 +175,7 @@ export async function applyCompatibilityHooks({ ...userConfig, { config: { plugins: [darkModePlugin] } }, ]) + let resolvedUserConfig = resolveConfig(designSystem, userConfig) let pluginApi = buildPluginApi(designSystem, ast, resolvedConfig) @@ -185,10 +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) - registerScreensConfig(resolvedConfig, 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 diff --git a/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts b/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts index d1be15ba6b59..af511a25e32f 100644 --- a/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts +++ b/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts @@ -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') diff --git a/packages/tailwindcss/src/compat/apply-config-to-theme.ts b/packages/tailwindcss/src/compat/apply-config-to-theme.ts index be84cbb8074a..2bb01864b717 100644 --- a/packages/tailwindcss/src/compat/apply-config-to-theme.ts +++ b/packages/tailwindcss/src/compat/apply-config-to-theme.ts @@ -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,9 +19,7 @@ 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)) { let name = keyPathToCssProperty(path) designSystem.theme.add( diff --git a/packages/tailwindcss/src/compat/config.test.ts b/packages/tailwindcss/src/compat/config.test.ts index ca7d78849127..ca19505c5d03 100644 --- a/packages/tailwindcss/src/compat/config.test.ts +++ b/packages/tailwindcss/src/compat/config.test.ts @@ -1,4 +1,4 @@ -import { describe, test } from 'vitest' +import { describe, expect, test } from 'vitest' import { compile } from '..' import plugin from '../plugin' import { flattenColorPalette } from './flatten-color-palette' From 4c865ea2049da0d73d09fd2d4d97bc59f7564a3e Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Tue, 17 Sep 2024 12:40:56 +0200 Subject: [PATCH 10/18] Update tests --- .../src/compat/screens-config.test.ts | 101 +++++++++--------- .../tailwindcss/src/compat/screens-config.ts | 7 +- 2 files changed, 54 insertions(+), 54 deletions(-) diff --git a/packages/tailwindcss/src/compat/screens-config.test.ts b/packages/tailwindcss/src/compat/screens-config.test.ts index 7fade8ce8296..a7221c1c5edc 100644 --- a/packages/tailwindcss/src/compat/screens-config.test.ts +++ b/packages/tailwindcss/src/compat/screens-config.test.ts @@ -3,7 +3,7 @@ import { compile } from '..' const css = String.raw -test.skip('CSS `--breakpoint-*` merge with JS config `screens`', async () => { +test('CSS `--breakpoint-*` merge with JS config `screens`', async () => { let input = css` @theme default { --breakpoint-sm: 40rem; @@ -86,11 +86,11 @@ test.skip('CSS `--breakpoint-*` merge with JS config `screens`', async () => { `) }) -test.skip('JS config `screens` extend CSS `--breakpoint-*`', async () => { +test('JS config `screens` extend CSS `--breakpoint-*`', async () => { let input = css` @theme default { - --breakpoint-xs: 30rem; - --breakpoint-md: 48rem; + --breakpoint-xs: 39rem; + --breakpoint-md: 49rem; } @theme { --breakpoint-md: 50rem; @@ -105,9 +105,9 @@ test.skip('JS config `screens` extend CSS `--breakpoint-*`', async () => { extend: { screens: { xs: '30rem', - sm: '44rem', - md: '49rem', - lg: '64rem', + sm: '40rem', + md: '48rem', + lg: '60rem', }, }, }, @@ -117,17 +117,17 @@ test.skip('JS config `screens` extend CSS `--breakpoint-*`', async () => { expect( compiler.build([ // Order is messed up on purpose - /* 'md:flex', */ - /* 'sm:flex', */ - /* 'lg:flex', */ + 'md:flex', + 'sm:flex', + 'lg:flex', 'xs:flex', - /* 'min-md:max-lg:underline', - 'min-sm:max-md:underline', */ + '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', */ + // Ensure other core variants appear at the end + 'print:items-end', ]), ).toMatchInlineSnapshot(` ":root { @@ -150,11 +150,45 @@ test.skip('JS config `screens` extend CSS `--breakpoint-*`', async () => { display: flex; } } + .min-sm\\:max-md\\:underline { + @media (width >= 40rem) { + @media (width < 50rem) { + text-decoration-line: underline; + } + } + } + .sm\\:flex { + @media (width >= 40rem) { + display: flex; + } + } + .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.skip('JS config `screens` only setup, even if those match the default-theme export', async () => { +test('JS config `screens` only setup, even if those match the default-theme export', async () => { let input = css` @config "./config.js"; @tailwind utilities; @@ -223,7 +257,7 @@ test.skip('JS config `screens` only setup, even if those match the default-theme `) }) -test.skip('JS config `screens` overwrite CSS `--breakpoint-*` and can remove breakpoints', async () => { +test('JS config `screens` overwrite CSS `--breakpoint-*`', async () => { let input = css` @theme default { --breakpoint-sm: 40rem; @@ -333,7 +367,7 @@ test.skip('JS config `screens` overwrite CSS `--breakpoint-*` and can remove bre `) }) -test.skip('JS config with `theme: { extends }` should not include the `default-config` values', async () => { +test('JS config with `theme: { extends }` should not include the `default-config` values', async () => { let input = css` @config "./config.js"; @tailwind utilities; @@ -369,21 +403,7 @@ test.skip('JS config with `theme: { extends }` should not include the `default-c 'print:items-end', ]), ).toMatchInlineSnapshot(` - ":root { - --breakpoint-sm: 40rem; - --breakpoint-md: 48rem; - --breakpoint-lg: 64rem; - --breakpoint-xl: 80rem; - --breakpoint-2xl: 96rem; - } - .min-sm\\:max-md\\:underline { - @media (width >= 40rem) { - @media (width < 48rem) { - text-decoration-line: underline; - } - } - } - .min-mini\\:max-midi\\:underline { + ".min-mini\\:max-midi\\:underline { @media (width >= 40rem) { @media (width < 48rem) { text-decoration-line: underline; @@ -395,28 +415,11 @@ test.skip('JS config with `theme: { extends }` should not include the `default-c display: flex; } } - .sm\\:flex { - @media (width >= 40rem) { - display: flex; - } - } - .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) { diff --git a/packages/tailwindcss/src/compat/screens-config.ts b/packages/tailwindcss/src/compat/screens-config.ts index b4b0cd4a2a91..826bc92c1d33 100644 --- a/packages/tailwindcss/src/compat/screens-config.ts +++ b/packages/tailwindcss/src/compat/screens-config.ts @@ -13,11 +13,8 @@ export function registerScreensConfig(config: ResolvedConfig, designSystem: Desi // as the order. let order = designSystem.variants.get('min')?.order ?? undefined - // Case A: All theme config are simple (non-nested-object) values. This means - // all breakpoints are used as `min` values, like we do in the core. - - // Step 1: Register static breakpoint variants for everything that comes from - // the user theme config. + // 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) From 66d4ff8e4c1bcff14c92ae1de77b074c55cec05c Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Tue, 17 Sep 2024 12:41:48 +0200 Subject: [PATCH 11/18] Add change log --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a63180fd5c18..355bb73182b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,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)) +### 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)) From 1f5dc2d00c38236fe5a6e87e7c0eeb1bd05c9c32 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Tue, 17 Sep 2024 14:59:22 +0200 Subject: [PATCH 12/18] Expose a way to allocate an order after a known utility --- .../src/compat/apply-config-to-theme.ts | 3 + .../src/compat/screens-config.test.ts | 152 +++++++++++++++++- .../tailwindcss/src/compat/screens-config.ts | 77 +++++++-- packages/tailwindcss/src/variants.ts | 4 +- 4 files changed, 217 insertions(+), 19 deletions(-) diff --git a/packages/tailwindcss/src/compat/apply-config-to-theme.ts b/packages/tailwindcss/src/compat/apply-config-to-theme.ts index 2bb01864b717..c80e8259cf65 100644 --- a/packages/tailwindcss/src/compat/apply-config-to-theme.ts +++ b/packages/tailwindcss/src/compat/apply-config-to-theme.ts @@ -21,6 +21,9 @@ function resolveThemeValue(value: unknown, subValue: string | null = null): stri 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}`, diff --git a/packages/tailwindcss/src/compat/screens-config.test.ts b/packages/tailwindcss/src/compat/screens-config.test.ts index a7221c1c5edc..fdf360a3cac4 100644 --- a/packages/tailwindcss/src/compat/screens-config.test.ts +++ b/packages/tailwindcss/src/compat/screens-config.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from 'vitest' +import { describe, expect, test } from 'vitest' import { compile } from '..' const css = String.raw @@ -440,3 +440,153 @@ test('JS config with `theme: { extends }` should not include the `default-config " `) }) + +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([ + 'sm:flex', + 'md:flex', + 'lg:flex', + 'xl:flex', + 'tall:flex', + 'min-sm:flex', + 'min-md:flex', + 'min-lg:flex', + 'min-xl:flex', + 'min-tall:flex', + // Ensure other core variants appear at the end + 'print:items-end', + ]), + ).toMatchInlineSnapshot(` + ".lg\\:flex { + @media (min-width: 868px) { + display: flex; + } + } + .tall\\:flex { + @media (min-height: 800px) { + display: flex; + } + } + .xl\\:flex { + @media (min-width: 1024px and max-width: 1279px) { + display: flex; + } + } + .md\\:flex { + @media (min-width: 668px and max-width: 767px), (min-width: 868px) { + display: flex; + } + } + .sm\\:flex { + @media (max-width: 639px) { + 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; + } + } + " + `) + }) +}) diff --git a/packages/tailwindcss/src/compat/screens-config.ts b/packages/tailwindcss/src/compat/screens-config.ts index 826bc92c1d33..2d6b7212662b 100644 --- a/packages/tailwindcss/src/compat/screens-config.ts +++ b/packages/tailwindcss/src/compat/screens-config.ts @@ -1,10 +1,9 @@ import { rule } from '../ast' import type { DesignSystem } from '../design-system' import type { ResolvedConfig } from './config/types' -import DefaultTheme from './default-theme' -export function registerScreensConfig(config: ResolvedConfig, designSystem: DesignSystem) { - let screens = config.theme.screens || {} +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 @@ -18,16 +17,6 @@ export function registerScreensConfig(config: ResolvedConfig, designSystem: Desi for (let [name, value] of Object.entries(screens)) { let coreVariant = designSystem.variants.get(name) - // Ignore defaults if they are already registered - // - // Note: We can't rely on the `designSystem.theme` for this, as it has the - // JS config values applied already. However, the DefaultTheme might not - // match what is actually already set in the designSystem since the @theme - // is set at runtime. - if (coreVariant && DefaultTheme.screens[name as 'sm'] === screens[name]) { - continue - } - // Ignore it if there's a CSS value that takes precedence over the JS config // and the static utilities are already registered. // @@ -36,19 +25,75 @@ export function registerScreensConfig(config: ResolvedConfig, designSystem: Desi // 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 && cssValue !== value) { + if (coreVariant && cssValue && !designSystem.theme.hasDefault(`--breakpoint-${name}`)) { continue } // min-${breakpoint} and max-${breakpoint} rules do not need to be // reconfigured, as they are using functional utilities and will not eagerly // capture the breakpoints before the compat layer runs. + let query: string | undefined + let insertOrder: number | undefined + if (typeof value === 'string') { + query = `(width >= ${value})` + insertOrder = order + } 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)) { + insertOrder = order + } + } + } else { + continue + } + + if (order && insertOrder === undefined) { + insertOrder = allocateOrderAfter(designSystem, order) + } + designSystem.variants.static( name, (ruleNode) => { - ruleNode.nodes = [rule(`@media (width >= ${value})`, ruleNode.nodes)] + ruleNode.nodes = [rule(`@media ${query}`, ruleNode.nodes)] }, - { order }, + { order: insertOrder }, ) } } + +function allocateOrderAfter(designSystem: DesignSystem, order: number): number { + for (let [, variant] of designSystem.variants.variants) { + if (variant.order > order) variant.order++ + } + designSystem.variants.compareFns = new Map( + Array.from(designSystem.variants.compareFns).map(([key, value]) => { + if (key > order) key++ + return [key, value] + }), + ) + return order + 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 && typeof value.min === 'string') { + rules.push(`min-width: ${value.min}`) + } + if ('max' in value && typeof value.max === 'string') { + rules.push(`max-width: ${value.max}`) + } + + if (rules.length !== 0) { + query = `(${rules.join(' and ')})` + } + } + return query +} diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 844162da4820..564d18b25e35 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -12,8 +12,8 @@ type VariantFn = ( type CompareFn = (a: Variant, z: Variant) => number export class Variants { - private compareFns = new Map() - private variants = new Map< + public compareFns = new Map() + public variants = new Map< string, { kind: Variant['kind'] From 47bb34aeb91423df0a109a062d8c6d86c6afa3a3 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Tue, 17 Sep 2024 15:05:50 +0200 Subject: [PATCH 13/18] Smaller code cleanups --- .../tailwindcss/src/compat/screens-config.ts | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/packages/tailwindcss/src/compat/screens-config.ts b/packages/tailwindcss/src/compat/screens-config.ts index 2d6b7212662b..7373ffdabc63 100644 --- a/packages/tailwindcss/src/compat/screens-config.ts +++ b/packages/tailwindcss/src/compat/screens-config.ts @@ -10,7 +10,7 @@ export function registerScreensConfig(userConfig: ResolvedConfig, designSystem: // 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 order = designSystem.variants.get('min')?.order ?? undefined + let coreOrder = designSystem.variants.get('min')?.order ?? undefined // Register static breakpoint variants for everything that comes from the user // theme config. @@ -29,31 +29,30 @@ export function registerScreensConfig(userConfig: ResolvedConfig, designSystem: continue } - // min-${breakpoint} and max-${breakpoint} rules do not need to be - // reconfigured, as they are using functional utilities and will not eagerly - // capture the breakpoints before the compat layer runs. let query: string | undefined let insertOrder: number | undefined if (typeof value === 'string') { query = `(width >= ${value})` - insertOrder = order + insertOrder = coreOrder } 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)) { - insertOrder = order + insertOrder = coreOrder } } } else { continue } - if (order && insertOrder === undefined) { - insertOrder = allocateOrderAfter(designSystem, order) + if (coreOrder && insertOrder === undefined) { + insertOrder = allocateOrderAfter(designSystem, coreOrder) } + // `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) => { @@ -84,12 +83,8 @@ function ruleForComplexScreenValue(value: object): string | null { } else { let rules: string[] = [] - if ('min' in value && typeof value.min === 'string') { - rules.push(`min-width: ${value.min}`) - } - if ('max' in value && typeof value.max === 'string') { - rules.push(`max-width: ${value.max}`) - } + 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 ')})` From fd36a58706a9f8c1f6770272fac1ea8b37802d45 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Tue, 17 Sep 2024 17:26:36 +0200 Subject: [PATCH 14/18] Update order --- .../src/compat/screens-config.test.ts | 70 +++++++++---------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/packages/tailwindcss/src/compat/screens-config.test.ts b/packages/tailwindcss/src/compat/screens-config.test.ts index fdf360a3cac4..192753468882 100644 --- a/packages/tailwindcss/src/compat/screens-config.test.ts +++ b/packages/tailwindcss/src/compat/screens-config.test.ts @@ -48,6 +48,11 @@ test('CSS `--breakpoint-*` merge with JS config `screens`', async () => { --breakpoint-xl: 80rem; --breakpoint-2xl: 96rem; } + .sm\\:flex { + @media (width >= 44rem) { + display: flex; + } + } .min-sm\\:max-md\\:underline { @media (width >= 44rem) { @media (width < 50rem) { @@ -55,11 +60,6 @@ test('CSS `--breakpoint-*` merge with JS config `screens`', async () => { } } } - .sm\\:flex { - @media (width >= 44rem) { - display: flex; - } - } .md\\:flex { @media (width >= 50rem) { display: flex; @@ -138,6 +138,11 @@ test('JS config `screens` extend CSS `--breakpoint-*`', async () => { display: flex; } } + .xs\\:flex { + @media (width >= 30rem) { + display: flex; + } + } .min-xs\\:max-md\\:underline { @media (width >= 30rem) { @media (width < 50rem) { @@ -145,8 +150,8 @@ test('JS config `screens` extend CSS `--breakpoint-*`', async () => { } } } - .xs\\:flex { - @media (width >= 30rem) { + .sm\\:flex { + @media (width >= 40rem) { display: flex; } } @@ -157,11 +162,6 @@ test('JS config `screens` extend CSS `--breakpoint-*`', async () => { } } } - .sm\\:flex { - @media (width >= 40rem) { - display: flex; - } - } .md\\:flex { @media (width >= 50rem) { display: flex; @@ -219,16 +219,16 @@ test('JS config `screens` only setup, even if those match the default-theme expo 'print:items-end', ]), ).toMatchInlineSnapshot(` - ".min-sm\\:max-md\\:underline { + ".sm\\:flex { @media (width >= 40rem) { - @media (width < 48rem) { - text-decoration-line: underline; - } + display: flex; } } - .sm\\:flex { + .min-sm\\:max-md\\:underline { @media (width >= 40rem) { - display: flex; + @media (width < 48rem) { + text-decoration-line: underline; + } } } .md\\:flex { @@ -305,11 +305,14 @@ test('JS config `screens` overwrite CSS `--breakpoint-*`', async () => { --breakpoint-xl: 80rem; --breakpoint-2xl: 96rem; } - .min-sm\\:max-md\\:underline { + .mini\\:flex { @media (width >= 40rem) { - @media (width < 48rem) { - text-decoration-line: underline; - } + display: flex; + } + } + .sm\\:flex { + @media (width >= 40rem) { + display: flex; } } .min-mini\\:max-midi\\:underline { @@ -319,14 +322,11 @@ test('JS config `screens` overwrite CSS `--breakpoint-*`', async () => { } } } - .mini\\:flex { - @media (width >= 40rem) { - display: flex; - } - } - .sm\\:flex { + .min-sm\\:max-md\\:underline { @media (width >= 40rem) { - display: flex; + @media (width < 48rem) { + text-decoration-line: underline; + } } } .md\\:flex { @@ -403,16 +403,16 @@ test('JS config with `theme: { extends }` should not include the `default-config 'print:items-end', ]), ).toMatchInlineSnapshot(` - ".min-mini\\:max-midi\\:underline { + ".mini\\:flex { @media (width >= 40rem) { - @media (width < 48rem) { - text-decoration-line: underline; - } + display: flex; } } - .mini\\:flex { + .min-mini\\:max-midi\\:underline { @media (width >= 40rem) { - display: flex; + @media (width < 48rem) { + text-decoration-line: underline; + } } } .midi\\:flex { From ce3c0cb6ff9d90017f82ed67d2cfd6c65859fb37 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Tue, 17 Sep 2024 17:35:36 +0200 Subject: [PATCH 15/18] Use resolvedUserConfig for registerThemeVariantOverrides --- packages/tailwindcss/src/compat/apply-compat-hooks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/compat/apply-compat-hooks.ts b/packages/tailwindcss/src/compat/apply-compat-hooks.ts index 7a721d1e2599..686b37dc8f6b 100644 --- a/packages/tailwindcss/src/compat/apply-compat-hooks.ts +++ b/packages/tailwindcss/src/compat/apply-compat-hooks.ts @@ -188,7 +188,7 @@ export async function applyCompatibilityHooks({ // core utilities already read from. applyConfigToTheme(designSystem, resolvedUserConfig) - registerThemeVariantOverrides(resolvedConfig, designSystem) + registerThemeVariantOverrides(resolvedUserConfig, designSystem) registerScreensConfig(resolvedUserConfig, designSystem) // Replace `resolveThemeValue` with a version that is backwards compatible From f08593133279888d322568da6ea1e6de4f3d5677 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Wed, 18 Sep 2024 11:53:37 +0200 Subject: [PATCH 16/18] Fix order for complex variants --- .../src/compat/screens-config.test.ts | 16 ++++++++-------- .../tailwindcss/src/compat/screens-config.ts | 7 ++++++- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/tailwindcss/src/compat/screens-config.test.ts b/packages/tailwindcss/src/compat/screens-config.test.ts index 192753468882..24e4ca1fec68 100644 --- a/packages/tailwindcss/src/compat/screens-config.test.ts +++ b/packages/tailwindcss/src/compat/screens-config.test.ts @@ -489,23 +489,23 @@ describe('complex screen configs', () => { display: flex; } } - .tall\\:flex { - @media (min-height: 800px) { + .sm\\:flex { + @media (max-width: 639px) { display: flex; } } - .xl\\:flex { - @media (min-width: 1024px and max-width: 1279px) { + .md\\:flex { + @media (min-width: 668px and max-width: 767px), (min-width: 868px) { display: flex; } } - .md\\:flex { - @media (min-width: 668px and max-width: 767px), (min-width: 868px) { + .xl\\:flex { + @media (min-width: 1024px and max-width: 1279px) { display: flex; } } - .sm\\:flex { - @media (max-width: 639px) { + .tall\\:flex { + @media (min-height: 800px) { display: flex; } } diff --git a/packages/tailwindcss/src/compat/screens-config.ts b/packages/tailwindcss/src/compat/screens-config.ts index 7373ffdabc63..21443427bf11 100644 --- a/packages/tailwindcss/src/compat/screens-config.ts +++ b/packages/tailwindcss/src/compat/screens-config.ts @@ -14,7 +14,12 @@ export function registerScreensConfig(userConfig: ResolvedConfig, designSystem: // Register static breakpoint variants for everything that comes from the user // theme config. - for (let [name, value] of Object.entries(screens)) { + // + // We loop over the entries in reverse order since we always add complex + // variants directly after the `min-*` group. + let entries = Object.entries(screens) + for (let i = entries.length - 1; i >= 0; i--) { + let [name, value] = entries[i] let coreVariant = designSystem.variants.get(name) // Ignore it if there's a CSS value that takes precedence over the JS config From a43e514f00dba3f101ed1389c0b8204af138ff84 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Wed, 18 Sep 2024 11:58:25 +0200 Subject: [PATCH 17/18] Make it clear which utilities don't generate anything anymore --- .../src/compat/screens-config.test.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/tailwindcss/src/compat/screens-config.test.ts b/packages/tailwindcss/src/compat/screens-config.test.ts index 24e4ca1fec68..5c2fed09de1e 100644 --- a/packages/tailwindcss/src/compat/screens-config.test.ts +++ b/packages/tailwindcss/src/compat/screens-config.test.ts @@ -387,15 +387,15 @@ test('JS config with `theme: { extends }` should not include the `default-config }), }) + expect( + compiler.build(['sm:flex', 'md:flex', 'min-md:max-lg:underline', 'min-sm:max-md:underline']), + ).toBe('') + 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', @@ -468,6 +468,10 @@ describe('complex screen configs', () => { }), }) + expect( + compiler.build(['min-sm:flex', 'min-md:flex', 'min-lg:flex', 'min-xl:flex', 'min-tall:flex']), + ).toBe('') + expect( compiler.build([ 'sm:flex', @@ -475,11 +479,7 @@ describe('complex screen configs', () => { 'lg:flex', 'xl:flex', 'tall:flex', - 'min-sm:flex', - 'min-md:flex', - 'min-lg:flex', - 'min-xl:flex', - 'min-tall:flex', + // Ensure other core variants appear at the end 'print:items-end', ]), From 55bb34a46d9695d0d5a7fb0b09379296136c8a12 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Wed, 18 Sep 2024 16:40:22 +0200 Subject: [PATCH 18/18] Only shift core variants once --- .../tailwindcss/src/compat/screens-config.ts | 58 ++++++++++--------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/packages/tailwindcss/src/compat/screens-config.ts b/packages/tailwindcss/src/compat/screens-config.ts index 21443427bf11..23bda1746292 100644 --- a/packages/tailwindcss/src/compat/screens-config.ts +++ b/packages/tailwindcss/src/compat/screens-config.ts @@ -10,16 +10,13 @@ export function registerScreensConfig(userConfig: ResolvedConfig, designSystem: // 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 ?? undefined + 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. - // - // We loop over the entries in reverse order since we always add complex - // variants directly after the `min-*` group. - let entries = Object.entries(screens) - for (let i = entries.length - 1; i >= 0; i--) { - let [name, value] = entries[i] + 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 @@ -35,50 +32,59 @@ export function registerScreensConfig(userConfig: ResolvedConfig, designSystem: } let query: string | undefined - let insertOrder: number | undefined + let deferInsert = true if (typeof value === 'string') { query = `(width >= ${value})` - insertOrder = coreOrder + 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)) { - insertOrder = coreOrder + deferInsert = false } } } else { continue } - if (coreOrder && insertOrder === undefined) { - insertOrder = allocateOrderAfter(designSystem, coreOrder) + 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 }, + ) } - // `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: insertOrder }, - ) + if (deferInsert) { + additionalVariants.push(insert) + } else { + insert(coreOrder) + } } -} -function allocateOrderAfter(designSystem: DesignSystem, order: number): number { + // Reserve and insert slots for the additional variants + if (additionalVariants.length === 0) return + for (let [, variant] of designSystem.variants.variants) { - if (variant.order > order) variant.order++ + if (variant.order > coreOrder) variant.order += additionalVariants.length } + designSystem.variants.compareFns = new Map( Array.from(designSystem.variants.compareFns).map(([key, value]) => { - if (key > order) key++ + if (key > coreOrder) key += additionalVariants.length return [key, value] }), ) - return order + 1 + + for (let [index, callback] of additionalVariants.entries()) { + callback(coreOrder + index + 1) + } } function ruleForComplexScreenValue(value: object): string | null {