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)) diff --git a/packages/tailwindcss/src/compat/apply-compat-hooks.ts b/packages/tailwindcss/src/compat/apply-compat-hooks.ts index b21d524f1456..686b37dc8f6b 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({ @@ -174,6 +175,7 @@ export async function applyCompatibilityHooks({ ...userConfig, { config: { plugins: [darkModePlugin] } }, ]) + let resolvedUserConfig = resolveConfig(designSystem, userConfig) let pluginApi = buildPluginApi(designSystem, ast, resolvedConfig) @@ -184,9 +186,10 @@ export async function applyCompatibilityHooks({ // Merge the user-configured theme keys into the design system. The compat // config would otherwise expand into namespaces like `background-color` which // core utilities already read from. - applyConfigToTheme(designSystem, userConfig) + applyConfigToTheme(designSystem, resolvedUserConfig) - registerThemeVariantOverrides(resolvedConfig, designSystem) + registerThemeVariantOverrides(resolvedUserConfig, designSystem) + registerScreensConfig(resolvedUserConfig, designSystem) // Replace `resolveThemeValue` with a version that is backwards compatible // with dot-notation but also aware of any JS theme configurations registered 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 3ba4ddb53acd..c80e8259cf65 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,10 +19,11 @@ function resolveThemeValue(value: unknown, subValue: string | null = null): stri return null } -export function applyConfigToTheme(designSystem: DesignSystem, configs: ConfigFile[]) { - let theme = resolveConfig(designSystem, configs).theme - +export function applyConfigToTheme(designSystem: DesignSystem, { theme }: ResolvedConfig) { for (let [path, value] of themeableValues(theme)) { + if (typeof value !== 'string' && typeof value !== 'number') { + continue + } let name = keyPathToCssProperty(path) designSystem.theme.add( `--${name}`, @@ -111,9 +111,8 @@ function themeableValues(config: ResolvedConfig['theme']): [string[], unknown][] } function keyPathToCssProperty(path: string[]) { - if (path[0] === 'colors') { - path[0] = 'color' - } + if (path[0] === 'colors') path[0] = 'color' + if (path[0] === 'screens') path[0] = 'breakpoint' return ( path 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'), 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..5c2fed09de1e --- /dev/null +++ b/packages/tailwindcss/src/compat/screens-config.test.ts @@ -0,0 +1,592 @@ +import { describe, expect, test } from 'vitest' +import { compile } from '..' + +const css = String.raw + +test('CSS `--breakpoint-*` merge with JS config `screens`', async () => { + let input = css` + @theme default { + --breakpoint-sm: 40rem; + --breakpoint-md: 48rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; + } + @theme { + --breakpoint-md: 50rem; + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadConfig: async () => ({ + theme: { + extend: { + screens: { + sm: '44rem', + }, + }, + }, + }), + }) + + expect( + compiler.build([ + 'sm:flex', + 'md:flex', + 'lg:flex', + 'min-sm:max-md:underline', + 'min-md:max-lg:underline', + // Ensure other core variants appear at the end + 'print:items-end', + ]), + ).toMatchInlineSnapshot(` + ":root { + --breakpoint-md: 50rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; + } + .sm\\:flex { + @media (width >= 44rem) { + display: flex; + } + } + .min-sm\\:max-md\\:underline { + @media (width >= 44rem) { + @media (width < 50rem) { + text-decoration-line: underline; + } + } + } + .md\\:flex { + @media (width >= 50rem) { + display: flex; + } + } + .min-md\\:max-lg\\:underline { + @media (width >= 50rem) { + @media (width < 64rem) { + text-decoration-line: underline; + } + } + } + .lg\\:flex { + @media (width >= 64rem) { + display: flex; + } + } + .print\\:items-end { + @media print { + align-items: flex-end; + } + } + " + `) +}) + +test('JS config `screens` extend CSS `--breakpoint-*`', async () => { + let input = css` + @theme default { + --breakpoint-xs: 39rem; + --breakpoint-md: 49rem; + } + @theme { + --breakpoint-md: 50rem; + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadConfig: async () => ({ + theme: { + extend: { + screens: { + xs: '30rem', + sm: '40rem', + md: '48rem', + lg: '60rem', + }, + }, + }, + }), + }) + + expect( + compiler.build([ + // Order is messed up on purpose + 'md:flex', + 'sm:flex', + 'lg:flex', + 'xs:flex', + 'min-md:max-lg:underline', + 'min-sm:max-md:underline', + 'min-xs:flex', + 'min-xs:max-md:underline', + + // Ensure other core variants appear at the end + 'print:items-end', + ]), + ).toMatchInlineSnapshot(` + ":root { + --breakpoint-md: 50rem; + } + .min-xs\\:flex { + @media (width >= 30rem) { + display: flex; + } + } + .xs\\:flex { + @media (width >= 30rem) { + display: flex; + } + } + .min-xs\\:max-md\\:underline { + @media (width >= 30rem) { + @media (width < 50rem) { + text-decoration-line: underline; + } + } + } + .sm\\:flex { + @media (width >= 40rem) { + display: flex; + } + } + .min-sm\\:max-md\\:underline { + @media (width >= 40rem) { + @media (width < 50rem) { + text-decoration-line: underline; + } + } + } + .md\\:flex { + @media (width >= 50rem) { + display: flex; + } + } + .min-md\\:max-lg\\:underline { + @media (width >= 50rem) { + @media (width < 60rem) { + text-decoration-line: underline; + } + } + } + .lg\\:flex { + @media (width >= 60rem) { + display: flex; + } + } + .print\\:items-end { + @media print { + align-items: flex-end; + } + } + " + `) +}) + +test('JS config `screens` only setup, even if those match the default-theme export', async () => { + let input = css` + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadConfig: async () => ({ + theme: { + screens: { + sm: '40rem', + md: '48rem', + lg: '64rem', + }, + }, + }), + }) + + expect( + compiler.build([ + // Order is messed up on purpose + 'md:flex', + 'sm:flex', + 'lg:flex', + 'min-md:max-lg:underline', + 'min-sm:max-md:underline', + + // Ensure other core variants appear at the end + 'print:items-end', + ]), + ).toMatchInlineSnapshot(` + ".sm\\:flex { + @media (width >= 40rem) { + display: flex; + } + } + .min-sm\\:max-md\\:underline { + @media (width >= 40rem) { + @media (width < 48rem) { + text-decoration-line: underline; + } + } + } + .md\\:flex { + @media (width >= 48rem) { + display: flex; + } + } + .min-md\\:max-lg\\:underline { + @media (width >= 48rem) { + @media (width < 64rem) { + text-decoration-line: underline; + } + } + } + .lg\\:flex { + @media (width >= 64rem) { + display: flex; + } + } + .print\\:items-end { + @media print { + align-items: flex-end; + } + } + " + `) +}) + +test('JS config `screens` overwrite CSS `--breakpoint-*`', async () => { + let input = css` + @theme default { + --breakpoint-sm: 40rem; + --breakpoint-md: 48rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadConfig: async () => ({ + theme: { + screens: { + mini: '40rem', + midi: '48rem', + maxi: '64rem', + }, + }, + }), + }) + + expect( + compiler.build([ + 'sm:flex', + 'md:flex', + 'mini:flex', + 'midi:flex', + 'maxi:flex', + 'min-md:max-lg:underline', + 'min-sm:max-md:underline', + 'min-midi:max-maxi:underline', + 'min-mini:max-midi:underline', + + // Ensure other core variants appear at the end + 'print:items-end', + ]), + ).toMatchInlineSnapshot(` + ":root { + --breakpoint-sm: 40rem; + --breakpoint-md: 48rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; + } + .mini\\:flex { + @media (width >= 40rem) { + display: flex; + } + } + .sm\\:flex { + @media (width >= 40rem) { + display: flex; + } + } + .min-mini\\:max-midi\\:underline { + @media (width >= 40rem) { + @media (width < 48rem) { + text-decoration-line: underline; + } + } + } + .min-sm\\:max-md\\:underline { + @media (width >= 40rem) { + @media (width < 48rem) { + text-decoration-line: underline; + } + } + } + .md\\:flex { + @media (width >= 48rem) { + display: flex; + } + } + .midi\\:flex { + @media (width >= 48rem) { + display: flex; + } + } + .min-md\\:max-lg\\:underline { + @media (width >= 48rem) { + @media (width < 64rem) { + text-decoration-line: underline; + } + } + } + .min-midi\\:max-maxi\\:underline { + @media (width >= 48rem) { + @media (width < 64rem) { + text-decoration-line: underline; + } + } + } + .maxi\\:flex { + @media (width >= 64rem) { + display: flex; + } + } + .print\\:items-end { + @media print { + align-items: flex-end; + } + } + " + `) +}) + +test('JS config with `theme: { extends }` should not include the `default-config` values', async () => { + let input = css` + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadConfig: async () => ({ + theme: { + extend: { + screens: { + mini: '40rem', + midi: '48rem', + maxi: '64rem', + }, + }, + }, + }), + }) + + expect( + compiler.build(['sm:flex', 'md:flex', 'min-md:max-lg:underline', 'min-sm:max-md:underline']), + ).toBe('') + + expect( + compiler.build([ + 'mini:flex', + 'midi:flex', + 'maxi:flex', + 'min-midi:max-maxi:underline', + 'min-mini:max-midi:underline', + + // Ensure other core variants appear at the end + 'print:items-end', + ]), + ).toMatchInlineSnapshot(` + ".mini\\:flex { + @media (width >= 40rem) { + display: flex; + } + } + .min-mini\\:max-midi\\:underline { + @media (width >= 40rem) { + @media (width < 48rem) { + text-decoration-line: underline; + } + } + } + .midi\\:flex { + @media (width >= 48rem) { + display: flex; + } + } + .min-midi\\:max-maxi\\:underline { + @media (width >= 48rem) { + @media (width < 64rem) { + text-decoration-line: underline; + } + } + } + .maxi\\:flex { + @media (width >= 64rem) { + display: flex; + } + } + .print\\:items-end { + @media print { + align-items: flex-end; + } + } + " + `) +}) + +describe('complex screen configs', () => { + test('generates utilities', async () => { + let input = css` + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadConfig: async () => ({ + theme: { + extend: { + screens: { + sm: { max: '639px' }, + md: [ + // + { min: '668px', max: '767px' }, + { min: '868px' }, + ], + lg: { min: '868px' }, + xl: { min: '1024px', max: '1279px' }, + tall: { raw: '(min-height: 800px)' }, + }, + }, + }, + }), + }) + + expect( + compiler.build(['min-sm:flex', 'min-md:flex', 'min-lg:flex', 'min-xl:flex', 'min-tall:flex']), + ).toBe('') + + expect( + compiler.build([ + 'sm:flex', + 'md:flex', + 'lg:flex', + 'xl:flex', + 'tall:flex', + + // Ensure other core variants appear at the end + 'print:items-end', + ]), + ).toMatchInlineSnapshot(` + ".lg\\:flex { + @media (min-width: 868px) { + display: flex; + } + } + .sm\\:flex { + @media (max-width: 639px) { + display: flex; + } + } + .md\\:flex { + @media (min-width: 668px and max-width: 767px), (min-width: 868px) { + display: flex; + } + } + .xl\\:flex { + @media (min-width: 1024px and max-width: 1279px) { + display: flex; + } + } + .tall\\:flex { + @media (min-height: 800px) { + display: flex; + } + } + .print\\:items-end { + @media print { + align-items: flex-end; + } + } + " + `) + }) + + test("don't interfere with `min-*` and `max-*` variants of non-complex screen configs", async () => { + let input = css` + @theme default { + --breakpoint-sm: 39rem; + --breakpoint-md: 48rem; + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadConfig: async () => ({ + theme: { + extend: { + screens: { + sm: '40rem', + portrait: { raw: 'screen and (orientation: portrait)' }, + }, + }, + }, + }), + }) + + expect( + compiler.build([ + 'sm:flex', + 'md:flex', + 'portrait:flex', + 'min-sm:flex', + 'min-md:flex', + 'min-portrait:flex', + // Ensure other core variants appear at the end + 'print:items-end', + ]), + ).toMatchInlineSnapshot(` + ":root { + --breakpoint-md: 48rem; + } + .min-sm\\:flex { + @media (width >= 40rem) { + display: flex; + } + } + .sm\\:flex { + @media (width >= 40rem) { + display: flex; + } + } + .md\\:flex { + @media (width >= 48rem) { + display: flex; + } + } + .min-md\\:flex { + @media (width >= 48rem) { + display: flex; + } + } + .portrait\\:flex { + @media screen and (orientation: portrait) { + display: flex; + } + } + .print\\:items-end { + @media print { + align-items: flex-end; + } + } + " + `) + }) +}) diff --git a/packages/tailwindcss/src/compat/screens-config.ts b/packages/tailwindcss/src/compat/screens-config.ts new file mode 100644 index 000000000000..23bda1746292 --- /dev/null +++ b/packages/tailwindcss/src/compat/screens-config.ts @@ -0,0 +1,105 @@ +import { rule } from '../ast' +import type { DesignSystem } from '../design-system' +import type { ResolvedConfig } from './config/types' + +export function registerScreensConfig(userConfig: ResolvedConfig, designSystem: DesignSystem) { + let screens = userConfig.theme.screens || {} + + // We want to insert the breakpoints in the right order as best we can. In the + // core utility, all static breakpoint variants and the `min-*` functional + // variant are registered inside a group. Since all the variants within a + // group share the same order, we can use the always-defined `min-*` variant + // as the order. + let coreOrder = designSystem.variants.get('min')?.order ?? 0 + + let additionalVariants: ((order: number) => void)[] = [] + + // Register static breakpoint variants for everything that comes from the user + // theme config. + for (let [name, value] of Object.entries(screens)) { + let coreVariant = designSystem.variants.get(name) + + // Ignore it if there's a CSS value that takes precedence over the JS config + // and the static utilities are already registered. + // + // This happens when a `@theme { }` block is used that overwrites all JS + // config options. We rely on the resolution order of the Theme for + // resolving this. If Theme has a different value, we know that this is not + // coming from the JS plugin and thus we don't need to handle it explicitly. + let cssValue = designSystem.theme.resolveValue(name, ['--breakpoint']) + if (coreVariant && cssValue && !designSystem.theme.hasDefault(`--breakpoint-${name}`)) { + continue + } + + let query: string | undefined + let deferInsert = true + if (typeof value === 'string') { + query = `(width >= ${value})` + deferInsert = false + } else if (typeof value === 'object' && value !== null) { + if (Array.isArray(value)) { + query = value.map(ruleForComplexScreenValue).join(', ') + } else { + query = ruleForComplexScreenValue(value) ?? '' + if ('min' in value && !('max' in value)) { + deferInsert = false + } + } + } else { + continue + } + + function insert(order: number) { + // `min-*` and `max-*` rules do not need to be reconfigured, as they are + // reading the latest values from the theme. + designSystem.variants.static( + name, + (ruleNode) => { + ruleNode.nodes = [rule(`@media ${query}`, ruleNode.nodes)] + }, + { order }, + ) + } + + if (deferInsert) { + additionalVariants.push(insert) + } else { + insert(coreOrder) + } + } + + // Reserve and insert slots for the additional variants + if (additionalVariants.length === 0) return + + for (let [, variant] of designSystem.variants.variants) { + if (variant.order > coreOrder) variant.order += additionalVariants.length + } + + designSystem.variants.compareFns = new Map( + Array.from(designSystem.variants.compareFns).map(([key, value]) => { + if (key > coreOrder) key += additionalVariants.length + return [key, value] + }), + ) + + for (let [index, callback] of additionalVariants.entries()) { + callback(coreOrder + index + 1) + } +} + +function ruleForComplexScreenValue(value: object): string | null { + let query = null + if ('raw' in value && typeof value.raw === 'string') { + query = value.raw + } else { + let rules: string[] = [] + + if ('min' in value) rules.push(`min-width: ${value.min}`) + if ('max' in value) rules.push(`max-width: ${value.max}`) + + if (rules.length !== 0) { + query = `(${rules.join(' and ')})` + } + } + return query +} diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 2bb9def3dece..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'] @@ -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, }) } @@ -697,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': {