Skip to content

Commit

Permalink
feat: add container query syntax (unocss/unocss#1821)
Browse files Browse the repository at this point in the history
  • Loading branch information
MellowCo committed Nov 12, 2022
1 parent 6cc25c2 commit 3228ea5
Show file tree
Hide file tree
Showing 21 changed files with 166 additions and 25 deletions.
13 changes: 13 additions & 0 deletions src/rules/container.ts
@@ -0,0 +1,13 @@
import type { Rule } from '@unocss/core'
import { warnOnce } from '@unocss/core'

export const containerParent: Rule[] = [
[/^@container(?:\/(\w+))?(?:-(normal))?$/, ([, l, v]) => {
warnOnce('The container query rule is experimental and may not follow semver.')

return {
'container-type': v ?? 'inline-size',
'container-name': l,
}
}],
]
2 changes: 2 additions & 0 deletions src/rules/default.ts
Expand Up @@ -23,6 +23,7 @@ import { svgUtilities } from './svg'
import { safeArea } from './safe-area'
import { animations } from './animation'
import { backgroundStyles } from './background'
import { containerParent } from './container'

export const rules: Rule[] = [
animations,
Expand Down Expand Up @@ -80,6 +81,7 @@ export const rules: Rule[] = [
transitions,
transforms,
willChange,
containerParent,
safeArea,

// should be the last
Expand Down
2 changes: 1 addition & 1 deletion src/rules/index.ts
Expand Up @@ -24,4 +24,4 @@ export * from './typography'
export * from './variables'
export * from './decoration'
export * from './safe-area'

export * from './container'
3 changes: 2 additions & 1 deletion src/theme/default.ts
Expand Up @@ -2,7 +2,7 @@ import { colors } from './colors'
import { fontFamily, fontSize, letterSpacing, lineHeight, textIndent, textShadow, textStrokeWidth, wordSpacing } from './font'
import { borderRadius, boxShadow, breakpoints, duration, easing, lineWidth, ringWidth, spacing, verticalBreakpoints } from './misc'
import { blur, dropShadow } from './filters'
import { height, maxHeight, maxWidth, width } from './size'
import { containers, height, maxHeight, maxWidth, width } from './size'
import type { Theme } from './types'
import { preflightBase } from './preflight'
import { animation } from './animation'
Expand Down Expand Up @@ -42,6 +42,7 @@ export const theme: Theme = {
duration,
ringWidth,
preflightBase,
containers,
animation,
aria,
}
2 changes: 2 additions & 0 deletions src/theme/size.ts
Expand Up @@ -39,3 +39,5 @@ export const maxHeight = {
...baseSize,
screen: '100vh',
}

export const containers = Object.fromEntries(Object.entries(baseSize).map(([k, v]) => [k, `(min-width: ${v})`]))
2 changes: 2 additions & 0 deletions src/theme/types.ts
Expand Up @@ -49,6 +49,8 @@ export interface Theme {
media?: Record<string, string>
// supports queries
supports?: Record<string, string>
// container queries
containers?: Record<string, string>
// animation
animation?: ThemeAnimation
// grids
Expand Down
33 changes: 21 additions & 12 deletions src/utils/variants.ts
Expand Up @@ -42,9 +42,9 @@ export const variantParentMatcher = (name: string, parent: string): VariantObjec
}
}

export const variantGetBracket = (name: string, matcher: string, separators: string[]): string[] | undefined => {
if (matcher.startsWith(`${name}-[`)) {
const [match, rest] = getBracket(matcher.slice(name.length + 1), '[', ']') ?? []
export const variantGetBracket = (prefix: string, matcher: string, separators: string[]): string[] | undefined => {
if (matcher.startsWith(`${prefix}[`)) {
const [match, rest] = getBracket(matcher.slice(prefix.length), '[', ']') ?? []
if (match && rest) {
for (const separator of separators) {
if (rest.startsWith(separator))
Expand All @@ -55,15 +55,24 @@ export const variantGetBracket = (name: string, matcher: string, separators: str
}
}

export const variantGetParameter = (name: string, matcher: string, separators: string[]): string[] | undefined => {
if (matcher.startsWith(`${name}-`)) {
const body = variantGetBracket(name, matcher, separators)
if (body)
return body
for (const separator of separators) {
const pos = matcher.indexOf(separator, name.length + 1)
if (pos !== -1)
return [matcher.slice(name.length + 1, pos), matcher.slice(pos + separator.length)]
export const variantGetParameter = (prefix: string, matcher: string, separators: string[]): string[] | undefined => {
if (matcher.startsWith(prefix)) {
const body = variantGetBracket(prefix, matcher, separators)
if (body) {
const [label = '', rest = body[1]] = variantGetParameter('/', body[1], separators) ?? []
return [body[0], rest, label]
}
for (const separator of separators.filter(x => x !== '/')) {
const pos = matcher.indexOf(separator, prefix.length)
if (pos !== -1) {
const labelPos = matcher.indexOf('/', prefix.length)
const unlabelled = labelPos === -1 || pos <= labelPos
return [
matcher.slice(prefix.length, unlabelled ? pos : labelPos),
matcher.slice(pos + separator.length),
unlabelled ? '' : matcher.slice(labelPos + 1, pos),
]
}
}
}
}
2 changes: 1 addition & 1 deletion src/variants/aria.ts
Expand Up @@ -5,7 +5,7 @@ import { handler as h, variantGetParameter } from '../utils'
export const variantAria: VariantObject = {
name: 'aria',
match(matcher, { theme }: VariantContext<Theme>) {
const variant = variantGetParameter('aria', matcher, [':', '-'])
const variant = variantGetParameter('aria-', matcher, [':', '-'])
if (variant) {
const [match, rest] = variant
const aria = h.bracket(match) ?? theme.aria?.[match] ?? ''
Expand Down
2 changes: 1 addition & 1 deletion src/variants/combinators.ts
Expand Up @@ -7,7 +7,7 @@ const scopeMatcher = (name: string, combinator: string): VariantObject => ({
if (!matcher.startsWith(name))
return

let body = variantGetBracket(name, matcher, [':', '-'])
let body = variantGetBracket(`${name}-`, matcher, [':', '-'])
if (!body) {
for (const separator of [':', '-']) {
if (matcher.startsWith(`${name}${separator}`)) {
Expand Down
39 changes: 39 additions & 0 deletions src/variants/container.ts
@@ -0,0 +1,39 @@
import type { VariantContext, VariantObject } from '@unocss/core'
import { warnOnce } from '@unocss/core'
import type { Theme } from '../theme'
import { handler as h, variantGetParameter } from '../utils'

export const variantContainerQuery: VariantObject = {
name: '@',
match(matcher, { theme }: VariantContext<Theme>) {
if (matcher.startsWith('@container'))
return

const variant = variantGetParameter('@', matcher, [':', '-'])
if (variant) {
const [match, rest, label] = variant
const unbracket = h.bracket(match)
let container: string | undefined
if (unbracket) {
const minWidth = h.numberWithUnit(unbracket)
if (minWidth)
container = `(min-width: ${minWidth})`
}
else {
container = theme.containers?.[match] ?? ''
}

if (container) {
warnOnce('The container query variant is experimental and may not follow semver.')
return {
matcher: rest,
handle: (input, next) => next({
...input,
parent: `${input.parent ? `${input.parent} $$ ` : ''}@container${label ? ` ${label} ` : ' '}${container}`,
}),
}
}
}
},
multiPass: true,
}
2 changes: 1 addition & 1 deletion src/variants/data.ts
Expand Up @@ -5,7 +5,7 @@ import { handler as h, variantGetParameter } from '../utils'
export const variantDataAttribute: VariantObject = {
name: 'data',
match(matcher, { theme }: VariantContext<Theme>) {
const variant = variantGetParameter('data', matcher, [':', '-'])
const variant = variantGetParameter('data-', matcher, [':', '-'])
if (variant) {
const [match, rest] = variant
const dataAttribute = h.bracket(match) ?? theme.data?.[match] ?? ''
Expand Down
2 changes: 2 additions & 0 deletions src/variants/default.ts
Expand Up @@ -13,6 +13,7 @@ import { variantSupports } from './supports'
import { partClasses, variantPseudoClassFunctions, variantPseudoClassesAndElements, variantTaggedPseudoClasses } from './pseudo'
import { variantAria } from './aria'
import { variantDataAttribute } from './data'
import { variantContainerQuery } from './container'

export const variants = (options: PresetWeappOptions): Variant<Theme>[] => [
variantAria,
Expand All @@ -38,5 +39,6 @@ export const variants = (options: PresetWeappOptions): Variant<Theme>[] => [
...variantLanguageDirections,
variantScope,

variantContainerQuery,
variantVariables,
]
1 change: 1 addition & 0 deletions src/variants/index.ts
Expand Up @@ -2,6 +2,7 @@
export * from './aria'
export * from './breakpoints'
export * from './combinators'
export * from './container'
export * from './data'
export * from './media'
export * from './supports'
Expand Down
2 changes: 1 addition & 1 deletion src/variants/media.ts
Expand Up @@ -7,7 +7,7 @@ export const variantPrint: Variant = variantParentMatcher('print', '@media print
export const variantCustomMedia: VariantObject = {
name: 'media',
match(matcher, { theme }: VariantContext<Theme>) {
const variant = variantGetParameter('media', matcher, [':', '-'])
const variant = variantGetParameter('media-', matcher, [':', '-'])
if (variant) {
const [match, rest] = variant

Expand Down
8 changes: 4 additions & 4 deletions src/variants/misc.ts
Expand Up @@ -4,7 +4,7 @@ import { getBracket, handler as h, variantGetBracket, variantGetParameter } from
export const variantSelector: Variant = {
name: 'selector',
match(matcher) {
const variant = variantGetBracket('selector', matcher, [':', '-'])
const variant = variantGetBracket('selector-', matcher, [':', '-'])
if (variant) {
const [match, rest] = variant
const selector = h.bracket(match)
Expand All @@ -21,7 +21,7 @@ export const variantSelector: Variant = {
export const variantCssLayer: Variant = {
name: 'layer',
match(matcher) {
const variant = variantGetParameter('layer', matcher, [':', '-'])
const variant = variantGetParameter('layer-', matcher, [':', '-'])
if (variant) {
const [match, rest] = variant
const layer = h.bracket(match) ?? match
Expand All @@ -41,7 +41,7 @@ export const variantCssLayer: Variant = {
export const variantInternalLayer: Variant = {
name: 'uno-layer',
match(matcher) {
const variant = variantGetParameter('uno-layer', matcher, [':', '-'])
const variant = variantGetParameter('uno-layer-', matcher, [':', '-'])
if (variant) {
const [match, rest] = variant
const layer = h.bracket(match) ?? match
Expand All @@ -58,7 +58,7 @@ export const variantInternalLayer: Variant = {
export const variantScope: Variant = {
name: 'scope',
match(matcher) {
const variant = variantGetBracket('scope', matcher, [':', '-'])
const variant = variantGetBracket('scope-', matcher, [':', '-'])
if (variant) {
const [match, rest] = variant
const scope = h.bracket(match)
Expand Down
4 changes: 2 additions & 2 deletions src/variants/pseudo.ts
Expand Up @@ -91,7 +91,7 @@ const taggedPseudoClassMatcher = (tag: string, parent: string, combinator: strin
const pseudoColonRE = new RegExp(`^${tag}-(?:(?:(${PseudoClassFunctionsStr})-)?(${PseudoClassesColonStr}))(?:(/\\w+))?[:]`)

const matchBracket = (input: string) => {
const body = variantGetBracket(tag, input, [])
const body = variantGetBracket(`${tag}-`, input, [])
if (!body)
return

Expand Down Expand Up @@ -140,7 +140,7 @@ const taggedPseudoClassMatcher = (tag: string, parent: string, combinator: strin

const [label, matcher, prefix, sort] = result as [string, string, string, number | undefined]
if (label !== '')
warnOnce('The labeled pseudo is experimental and may be changed in breaking ways at any time.')
warnOnce('The labeled variant is experimental and may not follow semver.')

return {
matcher,
Expand Down
2 changes: 1 addition & 1 deletion src/variants/supports.ts
Expand Up @@ -5,7 +5,7 @@ import { handler as h, variantGetParameter } from '../utils'
export const variantSupports: VariantObject = {
name: 'supports',
match(matcher, { theme }: VariantContext<Theme>) {
const variant = variantGetParameter('supports', matcher, [':', '-'])
const variant = variantGetParameter('supports-', matcher, [':', '-'])
if (variant) {
const [match, rest] = variant

Expand Down
19 changes: 19 additions & 0 deletions test/__snapshots__/preset-mini.test.ts.snap
Expand Up @@ -1037,6 +1037,10 @@ unocss .scope-\\\\[unocss\\\\]\\\\:block{display:block;}
.will-change-scroll{will-change:scroll-position;}
.will-change-transform{will-change:transform;}
.will-change-unset{will-change:unset;}
.\\\\@container{container-type:inline-size;}
.\\\\@container-normal{container-type:normal;}
.\\\\@container\\\\/label{container-type:inline-size;container-name:label;}
.\\\\@container\\\\/label-normal{container-type:normal;container-name:label;}
.p-safe{padding:env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);}
.pt-safe{padding-top:env(safe-area-inset-top);}
.pb-safe{padding-bottom:env(safe-area-inset-bottom);}
Expand All @@ -1046,6 +1050,21 @@ unocss .scope-\\\\[unocss\\\\]\\\\:block{display:block;}
.bg-blend-luminosity{background-blend-mode:luminosity;}
.bg-blend-normal{background-blend-mode:normal;}
.bg-blend-unset{background-blend:unset;}
@container (min-width: 10.5rem){
.\\\\@\\\\[10\\\\.5rem\\\\]-text-red{--licl-text-opacity:1;color:rgba(248,113,113,var(--licl-text-opacity));}
}
@container (min-width: 220rpx){
.\\\\@sm\\\\:text-red{--licl-text-opacity:1;color:rgba(248,113,113,var(--licl-text-opacity));}
}
@container (min-width: 300rpx){
.\\\\@lg-text-red{--licl-text-opacity:1;color:rgba(248,113,113,var(--licl-text-opacity));}
}
@container label (min-width: 100px){
.\\\\@\\\\[100px\\\\]\\\\/label\\\\:text-green{--licl-text-opacity:1;color:rgba(74,222,128,var(--licl-text-opacity));}
}
@container label (min-width: 180rpx){
.\\\\@xs\\\\/label\\\\:text-green{--licl-text-opacity:1;color:rgba(74,222,128,var(--licl-text-opacity));}
}
@layer base{
.layer-base\\\\:translate-0{--licl-translate-x:0rpx;--licl-translate-y:0rpx;transform:translateX(var(--licl-translate-x)) translateY(var(--licl-translate-y)) translateZ(var(--licl-translate-z)) rotate(var(--licl-rotate)) rotateX(var(--licl-rotate-x)) rotateY(var(--licl-rotate-y)) rotateZ(var(--licl-rotate-z)) skewX(var(--licl-skew-x)) skewY(var(--licl-skew-y)) scaleX(var(--licl-scale-x)) scaleY(var(--licl-scale-y)) scaleZ(var(--licl-scale-z));}
}
Expand Down
19 changes: 19 additions & 0 deletions test/__snapshots__/preset-weapp-rules.test.ts.snap
Expand Up @@ -1000,6 +1000,10 @@ unocss .scope--fl1r-unocss-fr1r--cr1-block{display:block;}
.will-change-scroll{will-change:scroll-position;}
.will-change-transform{will-change:transform;}
.will-change-unset{will-change:unset;}
.\\\\@container{container-type:inline-size;}
.\\\\@container-normal{container-type:normal;}
.\\\\@container-sr1-label{container-type:inline-size;container-name:label;}
.\\\\@container-sr1-label-normal{container-type:normal;container-name:label;}
.p-safe{padding:env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);}
.pt-safe{padding-top:env(safe-area-inset-top);}
.pb-safe{padding-bottom:env(safe-area-inset-bottom);}
Expand All @@ -1009,6 +1013,21 @@ unocss .scope--fl1r-unocss-fr1r--cr1-block{display:block;}
.bg-blend-luminosity{background-blend-mode:luminosity;}
.bg-blend-normal{background-blend-mode:normal;}
.bg-blend-unset{background-blend:unset;}
@container (min-width: 10.5rem){
.\\\\@-fl1r-10-dr1-5rem-fr1r--text-red{--un-text-opacity:1;color:rgba(248,113,113,var(--un-text-opacity));}
}
@container (min-width: 220rpx){
.\\\\@sm-cr1-text-red{--un-text-opacity:1;color:rgba(248,113,113,var(--un-text-opacity));}
}
@container (min-width: 300rpx){
.\\\\@lg-text-red{--un-text-opacity:1;color:rgba(248,113,113,var(--un-text-opacity));}
}
@container label (min-width: 100px){
.\\\\@-fl1r-100px-fr1r--sr1-label-cr1-text-green{--un-text-opacity:1;color:rgba(74,222,128,var(--un-text-opacity));}
}
@container label (min-width: 180rpx){
.\\\\@xs-sr1-label-cr1-text-green{--un-text-opacity:1;color:rgba(74,222,128,var(--un-text-opacity));}
}
@layer base{
.layer-base-cr1-translate-0{--un-translate-x:0rpx;--un-translate-y:0rpx;transform:translateX(var(--un-translate-x)) translateY(var(--un-translate-y)) translateZ(var(--un-translate-z)) rotate(var(--un-rotate)) rotateX(var(--un-rotate-x)) rotateY(var(--un-rotate-y)) rotateZ(var(--un-rotate-z)) skewX(var(--un-skew-x)) skewY(var(--un-skew-y)) scaleX(var(--un-scale-x)) scaleY(var(--un-scale-y)) scaleZ(var(--un-scale-z));}
}
Expand Down
19 changes: 19 additions & 0 deletions test/__snapshots__/preset-weapp.test.ts.snap
Expand Up @@ -1000,6 +1000,10 @@ unocss .scope--lfl-unocss-lfr--cl-block{display:block;}
.will-change-scroll{will-change:scroll-position;}
.will-change-transform{will-change:transform;}
.will-change-unset{will-change:unset;}
.\\\\@container{container-type:inline-size;}
.\\\\@container-normal{container-type:normal;}
.\\\\@container-sl-label{container-type:inline-size;container-name:label;}
.\\\\@container-sl-label-normal{container-type:normal;container-name:label;}
.p-safe{padding:env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);}
.pt-safe{padding-top:env(safe-area-inset-top);}
.pb-safe{padding-bottom:env(safe-area-inset-bottom);}
Expand All @@ -1009,6 +1013,21 @@ unocss .scope--lfl-unocss-lfr--cl-block{display:block;}
.bg-blend-luminosity{background-blend-mode:luminosity;}
.bg-blend-normal{background-blend-mode:normal;}
.bg-blend-unset{background-blend:unset;}
@container (min-width: 10.5rem){
.\\\\@-lfl-10-dl-5rem-lfr--text-red{--un-text-opacity:1;color:rgba(248,113,113,var(--un-text-opacity));}
}
@container (min-width: 220rpx){
.\\\\@sm-cl-text-red{--un-text-opacity:1;color:rgba(248,113,113,var(--un-text-opacity));}
}
@container (min-width: 300rpx){
.\\\\@lg-text-red{--un-text-opacity:1;color:rgba(248,113,113,var(--un-text-opacity));}
}
@container label (min-width: 100px){
.\\\\@-lfl-100px-lfr--sl-label-cl-text-green{--un-text-opacity:1;color:rgba(74,222,128,var(--un-text-opacity));}
}
@container label (min-width: 180rpx){
.\\\\@xs-sl-label-cl-text-green{--un-text-opacity:1;color:rgba(74,222,128,var(--un-text-opacity));}
}
@layer base{
.layer-base-cl-translate-0{--un-translate-x:0rpx;--un-translate-y:0rpx;transform:translateX(var(--un-translate-x)) translateY(var(--un-translate-y)) translateZ(var(--un-translate-z)) rotate(var(--un-rotate)) rotateX(var(--un-rotate-x)) rotateY(var(--un-rotate-y)) rotateZ(var(--un-rotate-z)) skewX(var(--un-skew-x)) skewY(var(--un-skew-y)) scaleX(var(--un-scale-x)) scaleY(var(--un-scale-y)) scaleZ(var(--un-scale-z));}
}
Expand Down
13 changes: 13 additions & 0 deletions test/assets/preset-mini-targets.ts
Expand Up @@ -1141,6 +1141,19 @@ export const presetMiniTargets: string[] = [
// variants - aria (theme)
'aria-hidden:hidden',

// variants - container parent
'@container',
'@container/label',
'@container-normal',
'@container/label-normal',

// variants - container query (@)
'@sm:text-red',
'@lg-text-red',
'@[10.5rem]-text-red',
'@xs/label:text-green',
'@[100px]/label:text-green',

// safe-area
'p-safe',
'pt-safe',
Expand Down

0 comments on commit 3228ea5

Please sign in to comment.