Add tuple syntax to guarantee screens order (#6104)

* add normalizeScreens function

This will allow us to normalize the various kinds of inputs to a stable
version that is consistent regardless of the input.

* use normalized screens

* add dedicated test for new tuple syntax

* make test consistent with other tests

While working on the normalizeScreens feature, some tests started
failing (the one with multiple screens), while looking at them I made
them consistent with the rest of the codebase.

* add test to ensure consistent order in screens output

* update changelog

* Update CHANGELOG.md

* Update CHANGELOG.md

Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
This commit is contained in:
Robin Malfait 2021-11-16 18:01:06 +01:00 committed by GitHub
parent 6c4b86d438
commit ef325ea35b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 527 additions and 190 deletions

View File

@ -7,8 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
Nothing yet!
### Fixed
- Enforce the order of some variants (like `before` and `after`) ([#6018](https://github.com/tailwindlabs/tailwindcss/pull/6018))
@ -16,6 +14,7 @@ Nothing yet!
### Added
- Add `placeholder` variant ([#6106](https://github.com/tailwindlabs/tailwindcss/pull/6106))
- Add tuple syntax for configuring screens while guaranteeing order ([#5956](https://github.com/tailwindlabs/tailwindcss/pull/5956))
## [3.0.0-alpha.2] - 2021-11-08

View File

@ -11,6 +11,7 @@ import isPlainObject from './util/isPlainObject'
import transformThemeValue from './util/transformThemeValue'
import { version as tailwindVersion } from '../package.json'
import log from './util/log'
import { normalizeScreens } from './util/normalizeScreens'
import { formatBoxShadowValue, parseBoxShadowValue } from './util/parseBoxShadowValue'
export let variantPlugins = {
@ -158,11 +159,10 @@ export let variantPlugins = {
},
screenVariants: ({ theme, addVariant }) => {
for (let screen in theme('screens')) {
let size = theme('screens')[screen]
let query = buildMediaQuery(size)
for (let screen of normalizeScreens(theme('screens'))) {
let query = buildMediaQuery(screen)
addVariant(screen, `@media ${query}`)
addVariant(screen.name, `@media ${query}`)
}
},
}
@ -182,24 +182,10 @@ export let corePlugins = {
},
container: (() => {
function extractMinWidths(breakpoints) {
return Object.values(breakpoints ?? {}).flatMap((breakpoints) => {
if (typeof breakpoints === 'string') {
breakpoints = { min: breakpoints }
}
if (!Array.isArray(breakpoints)) {
breakpoints = [breakpoints]
}
return breakpoints
.filter((breakpoint) => {
return breakpoint?.hasOwnProperty?.('min') || breakpoint?.hasOwnProperty('min-width')
})
.map((breakpoint) => {
return breakpoint['min-width'] ?? breakpoint.min
})
})
function extractMinWidths(breakpoints = []) {
return breakpoints
.flatMap((breakpoint) => breakpoint.values.map((breakpoint) => breakpoint.min))
.filter((v) => v !== undefined)
}
function mapMinWidthsToPadding(minWidths, screens, paddings) {
@ -228,16 +214,11 @@ export let corePlugins = {
}
for (let minWidth of minWidths) {
for (let [screen, value] of Object.entries(screens)) {
let screenMinWidth =
typeof value === 'object' && value !== null ? value.min || value['min-width'] : value
if (`${screenMinWidth}` === `${minWidth}`) {
mapping.push({
screen,
minWidth,
padding: paddings[screen],
})
for (let screen of screens) {
for (let { min } of screen.values) {
if (min === minWidth) {
mapping.push({ minWidth, padding: paddings[screen.name] })
}
}
}
}
@ -246,12 +227,12 @@ export let corePlugins = {
}
return function ({ addComponents, theme }) {
let screens = theme('container.screens', theme('screens'))
let screens = normalizeScreens(theme('container.screens', theme('screens')))
let minWidths = extractMinWidths(screens)
let paddings = mapMinWidthsToPadding(minWidths, screens, theme('container.padding'))
let generatePaddingFor = (minWidth) => {
let paddingConfig = paddings.find((padding) => `${padding.minWidth}` === `${minWidth}`)
let paddingConfig = paddings.find((padding) => padding.minWidth === minWidth)
if (!paddingConfig) {
return {}

View File

@ -2,6 +2,7 @@ import dlv from 'dlv'
import didYouMean from 'didyoumean'
import transformThemeValue from '../util/transformThemeValue'
import parseValue from 'postcss-value-parser'
import { normalizeScreens } from '../util/normalizeScreens'
import buildMediaQuery from '../util/buildMediaQuery'
import { toPath } from '../util/toPath'
@ -173,12 +174,14 @@ export default function ({ tailwindConfig: config }) {
},
screen: (node, screen) => {
screen = screen.replace(/^['"]+/g, '').replace(/['"]+$/g, '')
let screens = normalizeScreens(config.theme.screens)
let screenDefinition = screens.find(({ name }) => name === screen)
if (config.theme.screens[screen] === undefined) {
if (!screenDefinition) {
throw node.error(`The '${screen}' screen does not exist in your theme.`)
}
return buildMediaQuery(config.theme.screens[screen])
return buildMediaQuery(screenDefinition)
},
}
return (root) => {

View File

@ -1,16 +1,19 @@
import { normalizeScreens } from '../util/normalizeScreens'
import buildMediaQuery from '../util/buildMediaQuery'
export default function ({ tailwindConfig: { theme } }) {
return function (css) {
css.walkAtRules('screen', (atRule) => {
const screen = atRule.params
let screen = atRule.params
let screens = normalizeScreens(theme.screens)
let screenDefinition = screens.find(({ name }) => name === screen)
if (!theme.screens?.hasOwnProperty?.(screen)) {
if (!screenDefinition) {
throw atRule.error(`No \`${screen}\` screen found.`)
}
atRule.name = 'media'
atRule.params = buildMediaQuery(theme.screens[screen])
atRule.params = buildMediaQuery(screenDefinition)
})
}
}

View File

@ -1,24 +1,20 @@
export default function buildMediaQuery(screens) {
if (typeof screens === 'string') {
screens = { min: screens }
}
if (!Array.isArray(screens)) {
screens = [screens]
}
screens = Array.isArray(screens) ? screens : [screens]
return screens
.map((screen) => {
if (screen?.hasOwnProperty?.('raw')) {
return screen.raw
}
.map((screen) =>
screen.values.map((screen) => {
if (screen.raw !== undefined) {
return screen.raw
}
return Object.entries(screen)
.map(([feature, value]) => {
feature = { min: 'min-width', max: 'max-width' }[feature] ?? feature
return `(${feature}: ${value})`
})
.join(' and ')
})
return [
screen.min && `(min-width: ${screen.min})`,
screen.max && `(max-width: ${screen.max})`,
]
.filter(Boolean)
.join(' and ')
})
)
.join(', ')
}

View File

@ -0,0 +1,42 @@
/**
* A function that normalizes the various forms that the screens object can be
* provided in.
*
* Input(s):
* - ['100px', '200px'] // Raw strings
* - { sm: '100px', md: '200px' } // Object with string values
* - { sm: { min: '100px' }, md: { max: '100px' } } // Object with object values
* - { sm: [{ min: '100px' }, { max: '200px' }] } // Object with object array (multiple values)
* - [['sm', '100px'], ['md', '200px']] // Tuple object
*
* Output(s):
* - [{ name: 'sm', values: [{ min: '100px', max: '200px' }] }] // List of objects, that contains multiple values
*/
export function normalizeScreens(screens) {
if (Array.isArray(screens)) {
return screens.map((screen) => {
if (typeof screen === 'string') {
return { name: screen.toString(), values: [{ min: screen, max: undefined }] }
}
let [name, options] = screen
name = name.toString()
if (typeof options === 'string') {
return { name, values: [{ min: options, max: undefined }] }
}
if (Array.isArray(options)) {
return { name, values: options.map((option) => resolveValue(option)) }
}
return { name, values: [resolveValue(options)] }
})
}
return normalizeScreens(Object.entries(screens ?? {}))
}
function resolveValue({ 'min-width': _minWidth, min = _minWidth, max, raw } = {}) {
return { min, max, raw }
}

View File

@ -343,3 +343,39 @@ test('container can use variants', () => {
`)
})
})
test('container can use screens with tuple syntax', () => {
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
screens: [
[1800, { min: '1800px' }],
[1200, { min: '1200px' }],
[768, { min: '768px' }],
],
},
}
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 1200px) {
.container {
max-width: 1200px;
}
}
@media (min-width: 1800px) {
.container {
max-width: 1800px;
}
}
`)
})
})

View File

@ -1,17 +1,22 @@
import postcss from 'postcss'
import plugin from '../src/lib/evaluateTailwindFunctions'
import { css } from './util/run'
function run(input, opts = {}) {
return postcss([plugin({ tailwindConfig: opts })]).process(input, { from: undefined })
}
test('it looks up values in the theme using dot notation', () => {
const input = `
.banana { color: theme('colors.yellow'); }
let input = css`
.banana {
color: theme('colors.yellow');
}
`
const output = `
.banana { color: #f7cc50; }
let output = css`
.banana {
color: #f7cc50;
}
`
return run(input, {
@ -27,37 +32,85 @@ test('it looks up values in the theme using dot notation', () => {
})
test('color can be a function', () => {
const input = `
.backgroundColor { color: theme('backgroundColor.fn') }
.borderColor { color: theme('borderColor.fn') }
.caretColor { color: theme('caretColor.fn') }
.colors { color: theme('colors.fn') }
.divideColor { color: theme('divideColor.fn') }
.fill { color: theme('fill.fn') }
.gradientColorStops { color: theme('gradientColorStops.fn') }
.placeholderColor { color: theme('placeholderColor.fn') }
.ringColor { color: theme('ringColor.fn') }
.ringOffsetColor { color: theme('ringOffsetColor.fn') }
.stroke { color: theme('stroke.fn') }
.textColor { color: theme('textColor.fn') }
let input = css`
.backgroundColor {
color: theme('backgroundColor.fn');
}
.borderColor {
color: theme('borderColor.fn');
}
.caretColor {
color: theme('caretColor.fn');
}
.colors {
color: theme('colors.fn');
}
.divideColor {
color: theme('divideColor.fn');
}
.fill {
color: theme('fill.fn');
}
.gradientColorStops {
color: theme('gradientColorStops.fn');
}
.placeholderColor {
color: theme('placeholderColor.fn');
}
.ringColor {
color: theme('ringColor.fn');
}
.ringOffsetColor {
color: theme('ringOffsetColor.fn');
}
.stroke {
color: theme('stroke.fn');
}
.textColor {
color: theme('textColor.fn');
}
`
const output = `
.backgroundColor { color: #f00 }
.borderColor { color: #f00 }
.caretColor { color: #f00 }
.colors { color: #f00 }
.divideColor { color: #f00 }
.fill { color: #f00 }
.gradientColorStops { color: #f00 }
.placeholderColor { color: #f00 }
.ringColor { color: #f00 }
.ringOffsetColor { color: #f00 }
.stroke { color: #f00 }
.textColor { color: #f00 }
let output = css`
.backgroundColor {
color: #f00;
}
.borderColor {
color: #f00;
}
.caretColor {
color: #f00;
}
.colors {
color: #f00;
}
.divideColor {
color: #f00;
}
.fill {
color: #f00;
}
.gradientColorStops {
color: #f00;
}
.placeholderColor {
color: #f00;
}
.ringColor {
color: #f00;
}
.ringOffsetColor {
color: #f00;
}
.stroke {
color: #f00;
}
.textColor {
color: #f00;
}
`
const fn = () => `#f00`
let fn = () => `#f00`
return run(input, {
theme: {
@ -81,12 +134,16 @@ test('color can be a function', () => {
})
test('quotes are optional around the lookup path', () => {
const input = `
.banana { color: theme(colors.yellow); }
let input = css`
.banana {
color: theme(colors.yellow);
}
`
const output = `
.banana { color: #f7cc50; }
let output = css`
.banana {
color: #f7cc50;
}
`
return run(input, {
@ -102,12 +159,16 @@ test('quotes are optional around the lookup path', () => {
})
test('a default value can be provided', () => {
const input = `
.cookieMonster { color: theme('colors.blue', #0000ff); }
let input = css`
.cookieMonster {
color: theme('colors.blue', #0000ff);
}
`
const output = `
.cookieMonster { color: #0000ff; }
let output = css`
.cookieMonster {
color: #0000ff;
}
`
return run(input, {
@ -123,12 +184,16 @@ test('a default value can be provided', () => {
})
test('the default value can use the theme function', () => {
const input = `
.cookieMonster { color: theme('colors.blue', theme('colors.yellow')); }
let input = css`
.cookieMonster {
color: theme('colors.blue', theme('colors.yellow'));
}
`
const output = `
.cookieMonster { color: #f7cc50; }
let output = css`
.cookieMonster {
color: #f7cc50;
}
`
return run(input, {
@ -144,12 +209,16 @@ test('the default value can use the theme function', () => {
})
test('quotes are preserved around default values', () => {
const input = `
.heading { font-family: theme('fontFamily.sans', "Helvetica Neue"); }
let input = css`
.heading {
font-family: theme('fontFamily.sans', 'Helvetica Neue');
}
`
const output = `
.heading { font-family: "Helvetica Neue"; }
let output = css`
.heading {
font-family: 'Helvetica Neue';
}
`
return run(input, {
@ -165,12 +234,16 @@ test('quotes are preserved around default values', () => {
})
test('an unquoted list is valid as a default value', () => {
const input = `
.heading { font-family: theme('fontFamily.sans', Helvetica, Arial, sans-serif); }
let input = css`
.heading {
font-family: theme('fontFamily.sans', Helvetica, Arial, sans-serif);
}
`
const output = `
.heading { font-family: Helvetica, Arial, sans-serif; }
let output = css`
.heading {
font-family: Helvetica, Arial, sans-serif;
}
`
return run(input, {
@ -186,8 +259,10 @@ test('an unquoted list is valid as a default value', () => {
})
test('a missing root theme value throws', () => {
const input = `
.heading { color: theme('colours.gray.100'); }
let input = css`
.heading {
color: theme('colours.gray.100');
}
`
return expect(
@ -204,8 +279,10 @@ test('a missing root theme value throws', () => {
})
test('a missing nested theme property throws', () => {
const input = `
.heading { color: theme('colors.red'); }
let input = css`
.heading {
color: theme('colors.red');
}
`
return expect(
@ -223,8 +300,10 @@ test('a missing nested theme property throws', () => {
})
test('a missing nested theme property with a close alternative throws with a suggestion', () => {
const input = `
.heading { color: theme('colors.yellw'); }
let input = css`
.heading {
color: theme('colors.yellw');
}
`
return expect(
@ -241,8 +320,10 @@ test('a missing nested theme property with a close alternative throws with a sug
})
test('a path through a non-object throws', () => {
const input = `
.heading { color: theme('colors.yellow.100'); }
let input = css`
.heading {
color: theme('colors.yellow.100');
}
`
return expect(
@ -259,8 +340,10 @@ test('a path through a non-object throws', () => {
})
test('a path which exists but is not a string throws', () => {
const input = `
.heading { color: theme('colors.yellow'); }
let input = css`
.heading {
color: theme('colors.yellow');
}
`
return expect(
@ -275,8 +358,10 @@ test('a path which exists but is not a string throws', () => {
})
test('a path which exists but is invalid throws', () => {
const input = `
.heading { color: theme('colors'); }
let input = css`
.heading {
color: theme('colors');
}
`
return expect(
@ -289,8 +374,10 @@ test('a path which exists but is invalid throws', () => {
})
test('a path which is an object throws with a suggested key', () => {
const input = `
.heading { color: theme('colors'); }
let input = css`
.heading {
color: theme('colors');
}
`
return expect(
@ -307,12 +394,16 @@ test('a path which is an object throws with a suggested key', () => {
})
test('array values are joined by default', () => {
const input = `
.heading { font-family: theme('fontFamily.sans'); }
let input = css`
.heading {
font-family: theme('fontFamily.sans');
}
`
const output = `
.heading { font-family: Inter, Helvetica, sans-serif; }
let output = css`
.heading {
font-family: Inter, Helvetica, sans-serif;
}
`
return run(input, {
@ -328,14 +419,22 @@ test('array values are joined by default', () => {
})
test('font sizes are retrieved without default line-heights or letter-spacing', () => {
const input = `
.heading-1 { font-size: theme('fontSize.lg'); }
.heading-2 { font-size: theme('fontSize.xl'); }
let input = css`
.heading-1 {
font-size: theme('fontSize.lg');
}
.heading-2 {
font-size: theme('fontSize.xl');
}
`
const output = `
.heading-1 { font-size: 20px; }
.heading-2 { font-size: 24px; }
let output = css`
.heading-1 {
font-size: 20px;
}
.heading-2 {
font-size: 24px;
}
`
return run(input, {
@ -352,12 +451,16 @@ test('font sizes are retrieved without default line-heights or letter-spacing',
})
test('outlines are retrieved without default outline-offset', () => {
const input = `
.element { outline: theme('outline.black'); }
let input = css`
.element {
outline: theme('outline.black');
}
`
const output = `
.element { outline: 2px dotted black; }
let output = css`
.element {
outline: 2px dotted black;
}
`
return run(input, {
@ -373,12 +476,16 @@ test('outlines are retrieved without default outline-offset', () => {
})
test('font-family values are joined when an array', () => {
const input = `
.element { font-family: theme('fontFamily.sans'); }
let input = css`
.element {
font-family: theme('fontFamily.sans');
}
`
const output = `
.element { font-family: Helvetica, Arial, sans-serif; }
let output = css`
.element {
font-family: Helvetica, Arial, sans-serif;
}
`
return run(input, {
@ -394,12 +501,16 @@ test('font-family values are joined when an array', () => {
})
test('box-shadow values are joined when an array', () => {
const input = `
.element { box-shadow: theme('boxShadow.wtf'); }
let input = css`
.element {
box-shadow: theme('boxShadow.wtf');
}
`
const output = `
.element { box-shadow: 0 0 2px black, 1px 2px 3px white; }
let output = css`
.element {
box-shadow: 0 0 2px black, 1px 2px 3px white;
}
`
return run(input, {
@ -415,12 +526,16 @@ test('box-shadow values are joined when an array', () => {
})
test('transition-property values are joined when an array', () => {
const input = `
.element { transition-property: theme('transitionProperty.colors'); }
let input = css`
.element {
transition-property: theme('transitionProperty.colors');
}
`
const output = `
.element { transition-property: color, fill; }
let output = css`
.element {
transition-property: color, fill;
}
`
return run(input, {
@ -436,12 +551,16 @@ test('transition-property values are joined when an array', () => {
})
test('transition-duration values are joined when an array', () => {
const input = `
.element { transition-duration: theme('transitionDuration.lol'); }
let input = css`
.element {
transition-duration: theme('transitionDuration.lol');
}
`
const output = `
.element { transition-duration: 1s, 2s; }
let output = css`
.element {
transition-duration: 1s, 2s;
}
`
return run(input, {
@ -457,16 +576,18 @@ test('transition-duration values are joined when an array', () => {
})
test('basic screen function calls are expanded', () => {
const input = `
let input = css`
@media screen(sm) {
.foo {}
.foo {
}
}
`
const output = `
@media (min-width: 600px) {
.foo {}
}
let output = css`
@media (min-width: 600px) {
.foo {
}
}
`
return run(input, {
@ -478,16 +599,18 @@ test('basic screen function calls are expanded', () => {
})
test('screen function supports max-width screens', () => {
const input = `
let input = css`
@media screen(sm) {
.foo {}
.foo {
}
}
`
const output = `
@media (max-width: 600px) {
.foo {}
}
let output = css`
@media (max-width: 600px) {
.foo {
}
}
`
return run(input, {
@ -499,16 +622,18 @@ test('screen function supports max-width screens', () => {
})
test('screen function supports min-width screens', () => {
const input = `
let input = css`
@media screen(sm) {
.foo {}
.foo {
}
}
`
const output = `
@media (min-width: 600px) {
.foo {}
}
let output = css`
@media (min-width: 600px) {
.foo {
}
}
`
return run(input, {
@ -520,16 +645,18 @@ test('screen function supports min-width screens', () => {
})
test('screen function supports min-width and max-width screens', () => {
const input = `
let input = css`
@media screen(sm) {
.foo {}
.foo {
}
}
`
const output = `
@media (min-width: 600px) and (max-width: 700px) {
.foo {}
}
let output = css`
@media (min-width: 600px) and (max-width: 700px) {
.foo {
}
}
`
return run(input, {
@ -541,16 +668,18 @@ test('screen function supports min-width and max-width screens', () => {
})
test('screen function supports raw screens', () => {
const input = `
let input = css`
@media screen(mono) {
.foo {}
.foo {
}
}
`
const output = `
@media monochrome {
.foo {}
}
let output = css`
@media monochrome {
.foo {
}
}
`
return run(input, {
@ -562,16 +691,18 @@ test('screen function supports raw screens', () => {
})
test('screen arguments can be quoted', () => {
const input = `
let input = css`
@media screen('sm') {
.foo {}
.foo {
}
}
`
const output = `
@media (min-width: 600px) {
.foo {}
}
let output = css`
@media (min-width: 600px) {
.foo {
}
}
`
return run(input, {

View File

@ -0,0 +1,98 @@
import { normalizeScreens } from '../src/util/normalizeScreens'
it('should normalize an array of string values', () => {
let screens = ['768px', '1200px']
expect(normalizeScreens(screens)).toEqual([
{ name: '768px', values: [{ min: '768px', max: undefined }] },
{ name: '1200px', values: [{ min: '1200px', max: undefined }] },
])
})
it('should normalize an object with string values', () => {
let screens = {
a: '768px',
b: '1200px',
}
expect(normalizeScreens(screens)).toEqual([
{ name: 'a', values: [{ min: '768px', max: undefined }] },
{ name: 'b', values: [{ min: '1200px', max: undefined }] },
])
})
it('should normalize an object with object values', () => {
let screens = {
a: { min: '768px' },
b: { max: '1200px' },
}
expect(normalizeScreens(screens)).toEqual([
{ name: 'a', values: [{ min: '768px', max: undefined }] },
{ name: 'b', values: [{ min: undefined, max: '1200px' }] },
])
})
it('should normalize an object with multiple object values', () => {
let screens = {
a: [{ min: '768px' }, { max: '1200px' }],
}
expect(normalizeScreens(screens)).toEqual([
{
name: 'a',
values: [
{ max: undefined, min: '768px', raw: undefined },
{ max: '1200px', min: undefined, raw: undefined },
],
},
])
})
it('should normalize an object with object values (min-width normalized to width)', () => {
let screens = {
a: { 'min-width': '768px' },
b: { max: '1200px' },
}
expect(normalizeScreens(screens)).toEqual([
{ name: 'a', values: [{ min: '768px', max: undefined }] },
{ name: 'b', values: [{ min: undefined, max: '1200px' }] },
])
})
it('should normalize a tuple with string values', () => {
let screens = [
['a', '768px'],
['b', '1200px'],
]
expect(normalizeScreens(screens)).toEqual([
{ name: 'a', values: [{ min: '768px', max: undefined }] },
{ name: 'b', values: [{ min: '1200px', max: undefined }] },
])
})
it('should normalize a tuple with object values', () => {
let screens = [
['a', { min: '768px' }],
['b', { max: '1200px' }],
]
expect(normalizeScreens(screens)).toEqual([
{ name: 'a', values: [{ min: '768px', max: undefined }] },
{ name: 'b', values: [{ min: undefined, max: '1200px' }] },
])
})
it('should normalize a tuple with object values (min-width normalized to width)', () => {
let screens = [
['a', { 'min-width': '768px' }],
['b', { max: '1200px' }],
]
expect(normalizeScreens(screens)).toEqual([
{ name: 'a', values: [{ min: '768px', max: undefined }] },
{ name: 'b', values: [{ min: undefined, max: '1200px' }] },
])
})

View File

@ -65,3 +65,51 @@ test('`@tailwind screens` works as an alias for `@tailwind variants`', async ()
`)
})
})
test('screen names are in the correct order', () => {
// Using custom css function here, because otherwise with String.raw, we run
// into an issue with `\31 ` escapes. If we use `\31 ` then JS complains
// about strict mode. But `\\31 ` is not what it expected.
function css(templates) {
return templates.join('')
}
let config = {
content: [
{
raw: html`<div
class="768:font-light 1200:font-normal 1800:font-bold max-w-768 container"
></div>`,
},
],
theme: {
screens: [
[1800, { max: '1800px' }],
[1200, { max: '1200px' }],
[768, { max: '768px' }],
],
},
}
return run('@tailwind utilities;', config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
@media (max-width: 1800px) {
.\\31 800\\:font-bold {
font-weight: 700;
}
}
@media (max-width: 1200px) {
.\\31 200\\:font-normal {
font-weight: 400;
}
}
@media (max-width: 768px) {
.\\37 68\\:font-light {
font-weight: 300;
}
}
`)
})
})