Skip to content

Commit 8bdbee9

Browse files
Expose a way to allocate an order after a known utility
1 parent 4ff479e commit 8bdbee9

File tree

4 files changed

+217
-19
lines changed

4 files changed

+217
-19
lines changed

packages/tailwindcss/src/compat/apply-config-to-theme.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ function resolveThemeValue(value: unknown, subValue: string | null = null): stri
2121

2222
export function applyConfigToTheme(designSystem: DesignSystem, { theme }: ResolvedConfig) {
2323
for (let [path, value] of themeableValues(theme)) {
24+
if (typeof value !== 'string' && typeof value !== 'number') {
25+
continue
26+
}
2427
let name = keyPathToCssProperty(path)
2528
designSystem.theme.add(
2629
`--${name}`,

packages/tailwindcss/src/compat/screens-config.test.ts

Lines changed: 151 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expect, test } from 'vitest'
1+
import { describe, expect, test } from 'vitest'
22
import { compile } from '..'
33

44
const css = String.raw
@@ -440,3 +440,153 @@ test('JS config with `theme: { extends }` should not include the `default-config
440440
"
441441
`)
442442
})
443+
444+
describe('complex screen configs', () => {
445+
test('generates utilities', async () => {
446+
let input = css`
447+
@config "./config.js";
448+
@tailwind utilities;
449+
`
450+
451+
let compiler = await compile(input, {
452+
loadConfig: async () => ({
453+
theme: {
454+
extend: {
455+
screens: {
456+
sm: { max: '639px' },
457+
md: [
458+
//
459+
{ min: '668px', max: '767px' },
460+
{ min: '868px' },
461+
],
462+
lg: { min: '868px' },
463+
xl: { min: '1024px', max: '1279px' },
464+
tall: { raw: '(min-height: 800px)' },
465+
},
466+
},
467+
},
468+
}),
469+
})
470+
471+
expect(
472+
compiler.build([
473+
'sm:flex',
474+
'md:flex',
475+
'lg:flex',
476+
'xl:flex',
477+
'tall:flex',
478+
'min-sm:flex',
479+
'min-md:flex',
480+
'min-lg:flex',
481+
'min-xl:flex',
482+
'min-tall:flex',
483+
// Ensure other core variants appear at the end
484+
'print:items-end',
485+
]),
486+
).toMatchInlineSnapshot(`
487+
".lg\\:flex {
488+
@media (min-width: 868px) {
489+
display: flex;
490+
}
491+
}
492+
.tall\\:flex {
493+
@media (min-height: 800px) {
494+
display: flex;
495+
}
496+
}
497+
.xl\\:flex {
498+
@media (min-width: 1024px and max-width: 1279px) {
499+
display: flex;
500+
}
501+
}
502+
.md\\:flex {
503+
@media (min-width: 668px and max-width: 767px), (min-width: 868px) {
504+
display: flex;
505+
}
506+
}
507+
.sm\\:flex {
508+
@media (max-width: 639px) {
509+
display: flex;
510+
}
511+
}
512+
.print\\:items-end {
513+
@media print {
514+
align-items: flex-end;
515+
}
516+
}
517+
"
518+
`)
519+
})
520+
521+
test("don't interfere with `min-*` and `max-*` variants of non-complex screen configs", async () => {
522+
let input = css`
523+
@theme default {
524+
--breakpoint-sm: 39rem;
525+
--breakpoint-md: 48rem;
526+
}
527+
@config "./config.js";
528+
@tailwind utilities;
529+
`
530+
531+
let compiler = await compile(input, {
532+
loadConfig: async () => ({
533+
theme: {
534+
extend: {
535+
screens: {
536+
sm: '40rem',
537+
portrait: { raw: 'screen and (orientation: portrait)' },
538+
},
539+
},
540+
},
541+
}),
542+
})
543+
544+
expect(
545+
compiler.build([
546+
'sm:flex',
547+
'md:flex',
548+
'portrait:flex',
549+
'min-sm:flex',
550+
'min-md:flex',
551+
'min-portrait:flex',
552+
// Ensure other core variants appear at the end
553+
'print:items-end',
554+
]),
555+
).toMatchInlineSnapshot(`
556+
":root {
557+
--breakpoint-md: 48rem;
558+
}
559+
.min-sm\\:flex {
560+
@media (width >= 40rem) {
561+
display: flex;
562+
}
563+
}
564+
.sm\\:flex {
565+
@media (width >= 40rem) {
566+
display: flex;
567+
}
568+
}
569+
.md\\:flex {
570+
@media (width >= 48rem) {
571+
display: flex;
572+
}
573+
}
574+
.min-md\\:flex {
575+
@media (width >= 48rem) {
576+
display: flex;
577+
}
578+
}
579+
.portrait\\:flex {
580+
@media screen and (orientation: portrait) {
581+
display: flex;
582+
}
583+
}
584+
.print\\:items-end {
585+
@media print {
586+
align-items: flex-end;
587+
}
588+
}
589+
"
590+
`)
591+
})
592+
})
Lines changed: 61 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { rule } from '../ast'
22
import type { DesignSystem } from '../design-system'
33
import type { ResolvedConfig } from './config/types'
4-
import DefaultTheme from './default-theme'
54

6-
export function registerScreensConfig(config: ResolvedConfig, designSystem: DesignSystem) {
7-
let screens = config.theme.screens || {}
5+
export function registerScreensConfig(userConfig: ResolvedConfig, designSystem: DesignSystem) {
6+
let screens = userConfig.theme.screens || {}
87

98
// We want to insert the breakpoints in the right order as best we can. In the
109
// core utility, all static breakpoint variants and the `min-*` functional
@@ -18,16 +17,6 @@ export function registerScreensConfig(config: ResolvedConfig, designSystem: Desi
1817
for (let [name, value] of Object.entries(screens)) {
1918
let coreVariant = designSystem.variants.get(name)
2019

21-
// Ignore defaults if they are already registered
22-
//
23-
// Note: We can't rely on the `designSystem.theme` for this, as it has the
24-
// JS config values applied already. However, the DefaultTheme might not
25-
// match what is actually already set in the designSystem since the @theme
26-
// is set at runtime.
27-
if (coreVariant && DefaultTheme.screens[name as 'sm'] === screens[name]) {
28-
continue
29-
}
30-
3120
// Ignore it if there's a CSS value that takes precedence over the JS config
3221
// and the static utilities are already registered.
3322
//
@@ -36,19 +25,75 @@ export function registerScreensConfig(config: ResolvedConfig, designSystem: Desi
3625
// resolving this. If Theme has a different value, we know that this is not
3726
// coming from the JS plugin and thus we don't need to handle it explicitly.
3827
let cssValue = designSystem.theme.resolveValue(name, ['--breakpoint'])
39-
if (coreVariant && cssValue && cssValue !== value) {
28+
if (coreVariant && cssValue && !designSystem.theme.hasDefault(`--breakpoint-${name}`)) {
4029
continue
4130
}
4231

4332
// min-${breakpoint} and max-${breakpoint} rules do not need to be
4433
// reconfigured, as they are using functional utilities and will not eagerly
4534
// capture the breakpoints before the compat layer runs.
35+
let query: string | undefined
36+
let insertOrder: number | undefined
37+
if (typeof value === 'string') {
38+
query = `(width >= ${value})`
39+
insertOrder = order
40+
} else if (typeof value === 'object' && value !== null) {
41+
if (Array.isArray(value)) {
42+
query = value.map(ruleForComplexScreenValue).join(', ')
43+
} else {
44+
query = ruleForComplexScreenValue(value) ?? ''
45+
if ('min' in value && !('max' in value)) {
46+
insertOrder = order
47+
}
48+
}
49+
} else {
50+
continue
51+
}
52+
53+
if (order && insertOrder === undefined) {
54+
insertOrder = allocateOrderAfter(designSystem, order)
55+
}
56+
4657
designSystem.variants.static(
4758
name,
4859
(ruleNode) => {
49-
ruleNode.nodes = [rule(`@media (width >= ${value})`, ruleNode.nodes)]
60+
ruleNode.nodes = [rule(`@media ${query}`, ruleNode.nodes)]
5061
},
51-
{ order },
62+
{ order: insertOrder },
5263
)
5364
}
5465
}
66+
67+
function allocateOrderAfter(designSystem: DesignSystem, order: number): number {
68+
for (let [, variant] of designSystem.variants.variants) {
69+
if (variant.order > order) variant.order++
70+
}
71+
designSystem.variants.compareFns = new Map(
72+
Array.from(designSystem.variants.compareFns).map(([key, value]) => {
73+
if (key > order) key++
74+
return [key, value]
75+
}),
76+
)
77+
return order + 1
78+
}
79+
80+
function ruleForComplexScreenValue(value: object): string | null {
81+
let query = null
82+
if ('raw' in value && typeof value.raw === 'string') {
83+
query = value.raw
84+
} else {
85+
let rules: string[] = []
86+
87+
if ('min' in value && typeof value.min === 'string') {
88+
rules.push(`min-width: ${value.min}`)
89+
}
90+
if ('max' in value && typeof value.max === 'string') {
91+
rules.push(`max-width: ${value.max}`)
92+
}
93+
94+
if (rules.length !== 0) {
95+
query = `(${rules.join(' and ')})`
96+
}
97+
}
98+
return query
99+
}

packages/tailwindcss/src/variants.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ type VariantFn<T extends Variant['kind']> = (
1212
type CompareFn = (a: Variant, z: Variant) => number
1313

1414
export class Variants {
15-
private compareFns = new Map<number, CompareFn>()
16-
private variants = new Map<
15+
public compareFns = new Map<number, CompareFn>()
16+
public variants = new Map<
1717
string,
1818
{
1919
kind: Variant['kind']

0 commit comments

Comments
 (0)