diff --git a/CHANGELOG.md b/CHANGELOG.md index f239b0a..9d43f8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -- Nothing yet! +### Added + +- Support of `max-width` container queries using `@max` or `atMax` variants. ## [0.1.1] - 2023-03-31 diff --git a/README.md b/README.md index 0538d51..7ce9230 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,26 @@ Start by marking an element as a container using the `@container` class, and the By default we provide [container sizes](#configuration) from `@xs` (`20rem`) to `@7xl` (`80rem`). +In case of `max-width` container queries: + +```html +
+
+ +
+
+``` + +or alternatively there is an `atMax` version: + +```html +
+
+ +
+
+``` + ### Named containers You can optionally name containers using a `@container/{name}` class, and then include that name in the container variants using classes like `@lg/{name}:underline`: @@ -52,6 +72,26 @@ You can optionally name containers using a `@container/{name}` class, and then i ``` +In case of `max-width` container queries: + +```html +
+
+ +
+
+``` + +or alternatively the `atMax` version: + +```html +
+
+ +
+
+``` + ### Arbitrary container sizes In addition to using one of the [container sizes](#configuration) provided by default, you can also create one-off sizes using any arbitrary value: @@ -64,6 +104,56 @@ In addition to using one of the [container sizes](#configuration) provided by de ``` +In case of `max-width` container queries: + +```html +
+
+ +
+
+``` + +or alternatively the `atMax` version: + +```html +
+
+ +
+
+``` + +### Combining named containers and arbitrary container sizes + +You can combine both [named containers](#named-containers) and +[arbitrary container sizes](#arbitrary-container-sizes) this way: + +```html +
+
+ +
+ +``` + +In case of `max-width` container queries only the `atMax` version is working +because to support extraction of `@max-[17.5rem]/main:underline` by the default +extractor of Tailwind CSS its regular expressions would need to be updated +(or a custom extractor could be used but that is really an advanced topic since it +overrides the default one which does really an excellent job to extract class name +candidates from anywhere). + +```html +
+
+ +
+
+``` + + + ### Removing a container To stop an element from acting as a container, use the `@container-normal` class. diff --git a/src/index.ts b/src/index.ts index 84b9869..37092c6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,6 +29,34 @@ export = plugin( } ) + const sort: ( + a: { value: string; modifier: string | null }, + b: { value: string; modifier: string | null } + ) => number = (aVariant, zVariant) => { + let a = parseFloat(aVariant.value) + let z = parseFloat(zVariant.value) + + if (a === null || z === null) return 0 + + // Sort values themselves regardless of unit + if (a - z !== 0) return a - z + + let aLabel = aVariant.modifier ?? '' + let zLabel = zVariant.modifier ?? '' + + // Explicitly move empty labels to the end + if (aLabel === '' && zLabel !== '') { + return 1 + } else if (aLabel !== '' && zLabel === '') { + return -1 + } + + // Sort labels alphabetically in the English locale + // We are intentionally overriding the locale because we do not want the sort to + // be affected by the machine's locale (be it a developer or CI environment) + return aLabel.localeCompare(zLabel, 'en', { numeric: true }) + } + matchVariant( '@', (value = '', { modifier }) => { @@ -38,32 +66,34 @@ export = plugin( }, { values, - sort(aVariant, zVariant) { - let a = parseFloat(aVariant.value) - let z = parseFloat(zVariant.value) - - if (a === null || z === null) return 0 + sort, + } + ) - // Sort values themselves regardless of unit - if (a - z !== 0) return a - z + const maxVariantFn: (value: string, { modifier }: { modifier: string | null }) => string | string[] = (value = '', { modifier }) => { + let parsed = parseValue(value) - let aLabel = aVariant.modifier ?? '' - let zLabel = zVariant.modifier ?? '' + return parsed !== null ? `@container ${modifier ?? ''} (max-width: ${value})` : [] + } - // Explicitly move empty labels to the end - if (aLabel === '' && zLabel !== '') { - return 1 - } else if (aLabel !== '' && zLabel === '') { - return -1 - } + matchVariant( + '@max', + maxVariantFn, + { + values, + sort, + } + ) - // Sort labels alphabetically in the English locale - // We are intentionally overriding the locale because we do not want the sort to - // be affected by the machine's locale (be it a developer or CI environment) - return aLabel.localeCompare(zLabel, 'en', { numeric: true }) - }, + matchVariant( + 'atMax', + maxVariantFn, + { + values, + sort, } ) + }, { theme: { diff --git a/tests/index.test.ts b/tests/index.test.ts index 077b628..d63db2a 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,5 +1,5 @@ import { expect } from '@jest/globals' -import { html, css, run } from './run' +import { css, html, run } from './run' it('container queries', () => { let config = { @@ -245,3 +245,493 @@ it('should be possible to use default container queries', () => { `) }) }) + +it('max-width container queries', () => { + let config = { + content: [ + { + raw: html` +
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+ + +
+
+
+
+ `, + }, + ], + theme: { + containers: { + sm: '320px', + md: '768px', + lg: '1024px', + }, + }, + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .\@container { + container-type: inline-size; + } + + .\@container-normal { + container-type: normal; + } + + .\@container\/sidebar { + container-type: inline-size; + container-name: sidebar; + } + + .\@container-normal\/sidebar { + container-type: normal; + container-name: sidebar; + } + + @container (max-width: 123px) { + .\@max-\[123px\]\:underline { + text-decoration-line: underline; + } + } + + @container (max-width: 200rem) { + .\@max-\[200rem\]\:underline { + text-decoration-line: underline; + } + } + + @container (max-width: 312px) { + .\@max-\[312px\]\:underline { + text-decoration-line: underline; + } + } + + @container container1 (max-width: 320px) { + .\@max-sm\/container1\:underline { + text-decoration-line: underline; + } + } + + @container container2 (max-width: 320px) { + .\@max-sm\/container2\:underline { + text-decoration-line: underline; + } + } + + @container container10 (max-width: 320px) { + .\@max-sm\/container10\:underline { + text-decoration-line: underline; + } + } + + @container (max-width: 320px) { + .\@max-sm\:underline { + text-decoration-line: underline; + } + } + + @container container1 (max-width: 768px) { + .\@max-md\/container1\:underline { + text-decoration-line: underline; + } + } + + @container container2 (max-width: 768px) { + .\@max-md\/container2\:underline { + text-decoration-line: underline; + } + } + + @container container10 (max-width: 768px) { + .\@max-md\/container10\:underline { + text-decoration-line: underline; + } + } + + @container (max-width: 768px) { + .\@max-md\:underline { + text-decoration-line: underline; + } + } + + @container container1 (max-width: 1024px) { + .\@max-lg\/container1\:underline { + text-decoration-line: underline; + } + } + + @container container2 (max-width: 1024px) { + .\@max-lg\/container2\:underline { + text-decoration-line: underline; + } + } + + @container container10 (max-width: 1024px) { + .\@max-lg\/container10\:underline { + text-decoration-line: underline; + } + } + + @container (max-width: 1024px) { + .\@max-lg\:underline { + text-decoration-line: underline; + } + .\@max-\[1024px\]\:underline { + text-decoration-line: underline; + } + } + `) + }) +}) + +it('should be possible to use default max-width container queries', () => { + let config = { + content: [ + { + raw: html` +
+
+
+
+
+
+
+
+
+
+ `, + }, + ], + theme: {}, + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + @container (max-width: 20rem) { + .\@max-xs\:underline { + text-decoration-line: underline; + } + } + + @container (max-width: 24rem) { + .\@max-sm\:underline { + text-decoration-line: underline; + } + } + + @container (max-width: 28rem) { + .\@max-md\:underline { + text-decoration-line: underline; + } + } + + @container (max-width: 32rem) { + .\@max-lg\:underline { + text-decoration-line: underline; + } + } + + @container (max-width: 48rem) { + .\@max-3xl\:underline { + text-decoration-line: underline; + } + } + + @container (max-width: 64rem) { + .\@max-5xl\:underline { + text-decoration-line: underline; + } + } + + @container (max-width: 72rem) { + .\@max-6xl\:underline { + text-decoration-line: underline; + } + } + + @container (max-width: 80rem) { + .\@max-7xl\:underline { + text-decoration-line: underline; + } + } + `) + }) +}) + +it('atMax max-width container queries', () => { + let config = { + content: [ + { + raw: html` +
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+ `, + }, + ], + theme: { + containers: { + sm: '320px', + md: '768px', + lg: '1024px', + }, + }, + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .\@container { + container-type: inline-size; + } + + .\@container-normal { + container-type: normal; + } + + .\@container\/sidebar { + container-type: inline-size; + container-name: sidebar; + } + + .\@container-normal\/sidebar { + container-type: normal; + container-name: sidebar; + } + + @container (max-width: 123px) { + .atMax-\[123px\]\:underline { + text-decoration-line: underline; + } + } + + @container (max-width: 200rem) { + .atMax-\[200rem\]\:underline { + text-decoration-line: underline; + } + } + + @container (max-width: 312px) { + .atMax-\[312px\]\:underline { + text-decoration-line: underline; + } + } + + @container container1 (max-width: 320px) { + .atMax-sm\/container1\:underline { + text-decoration-line: underline; + } + } + + @container container2 (max-width: 320px) { + .atMax-sm\/container2\:underline { + text-decoration-line: underline; + } + } + + @container container10 (max-width: 320px) { + .atMax-sm\/container10\:underline { + text-decoration-line: underline; + } + } + + @container (max-width: 320px) { + .atMax-sm\:underline { + text-decoration-line: underline; + } + } + + @container container1 (max-width: 768px) { + .atMax-md\/container1\:underline { + text-decoration-line: underline; + } + } + + @container container2 (max-width: 768px) { + .atMax-md\/container2\:underline { + text-decoration-line: underline; + } + } + + @container container10 (max-width: 768px) { + .atMax-md\/container10\:underline { + text-decoration-line: underline; + } + } + + @container (max-width: 768px) { + .atMax-md\:underline { + text-decoration-line: underline; + } + } + + @container container1 (max-width: 1024px) { + .atMax-lg\/container1\:underline { + text-decoration-line: underline; + } + + .atMax-\[1024px\]\/container1\:underline { + text-decoration-line: underline; + } + } + + @container container2 (max-width: 1024px) { + .atMax-lg\/container2\:underline { + text-decoration-line: underline; + } + } + + @container container10 (max-width: 1024px) { + .atMax-lg\/container10\:underline { + text-decoration-line: underline; + } + } + + @container (max-width: 1024px) { + .atMax-lg\:underline { + text-decoration-line: underline; + } + .atMax-\[1024px\]\:underline { + text-decoration-line: underline; + } + } + `) + }) +}) + +it('should be possible to use default atMax max-width container queries', () => { + let config = { + content: [ + { + raw: html` +
+
+
+
+
+
+
+
+
+
+ `, + }, + ], + theme: {}, + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + @container (max-width: 20rem) { + .atMax-xs\:underline { + text-decoration-line: underline; + } + } + + @container (max-width: 24rem) { + .atMax-sm\:underline { + text-decoration-line: underline; + } + } + + @container (max-width: 28rem) { + .atMax-md\:underline { + text-decoration-line: underline; + } + } + + @container (max-width: 32rem) { + .atMax-lg\:underline { + text-decoration-line: underline; + } + } + + @container (max-width: 48rem) { + .atMax-3xl\:underline { + text-decoration-line: underline; + } + } + + @container (max-width: 64rem) { + .atMax-5xl\:underline { + text-decoration-line: underline; + } + } + + @container (max-width: 72rem) { + .atMax-6xl\:underline { + text-decoration-line: underline; + } + } + + @container (max-width: 80rem) { + .atMax-7xl\:underline { + text-decoration-line: underline; + } + } + `) + }) +})