Skip to content

Commit

Permalink
feat(preset-mini): add container query syntax (#1821)
Browse files Browse the repository at this point in the history
  • Loading branch information
chu121su12 committed Nov 7, 2022
1 parent 1388275 commit ce59126
Show file tree
Hide file tree
Showing 20 changed files with 130 additions and 24 deletions.
13 changes: 13 additions & 0 deletions packages/preset-mini/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 packages/preset-mini/src/_rules/default.ts
Expand Up @@ -20,6 +20,7 @@ import { textAligns, verticalAligns } from './align'
import { appearance, outline, willChange } from './behaviors'
import { textDecorations } from './decoration'
import { svgUtilities } from './svg'
import { containerParent } from './container'

export const rules: Rule[] = [
cssVariables,
Expand Down Expand Up @@ -75,6 +76,7 @@ export const rules: Rule[] = [
transitions,
transforms,
willChange,
containerParent,

// should be the last
questionMark,
Expand Down
1 change: 1 addition & 0 deletions packages/preset-mini/src/_rules/index.ts
Expand Up @@ -3,6 +3,7 @@ export * from './align'
export * from './behaviors'
export * from './border'
export * from './color'
export * from './container'
export * from './default'
export * from './flex'
export * from './gap'
Expand Down
3 changes: 2 additions & 1 deletion packages/preset-mini/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'

Expand Down Expand Up @@ -40,4 +40,5 @@ export const theme: Theme = {
duration,
ringWidth,
preflightBase,
containers,
}
2 changes: 2 additions & 0 deletions packages/preset-mini/src/_theme/size.ts
Expand Up @@ -36,3 +36,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 packages/preset-mini/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 packages/preset-mini/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 packages/preset-mini/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 packages/preset-mini/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 packages/preset-mini/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 packages/preset-mini/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 packages/preset-mini/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: PresetMiniOptions): Variant<Theme>[] => [
variantAria,
Expand All @@ -38,5 +39,6 @@ export const variants = (options: PresetMiniOptions): Variant<Theme>[] => [
...variantLanguageDirections,
variantScope,

variantContainerQuery,
variantVariables,
]
1 change: 1 addition & 0 deletions packages/preset-mini/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 packages/preset-mini/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 packages/preset-mini/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 packages/preset-mini/src/_variants/pseudo.ts
Expand Up @@ -88,7 +88,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 @@ -137,7 +137,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 packages/preset-mini/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
2 changes: 2 additions & 0 deletions packages/preset-wind/src/rules/default.ts
Expand Up @@ -8,6 +8,7 @@ import {
borders,
boxShadows,
boxSizing,
containerParent,
contentVisibility,
contents,
cssProperty,
Expand Down Expand Up @@ -155,6 +156,7 @@ export const rules: Rule[] = [
contentVisibility,
contents,
placeholders,
containerParent,

// should be the last
questionMark,
Expand Down
19 changes: 19 additions & 0 deletions test/__snapshots__/preset-mini.test.ts.snap
Expand Up @@ -914,6 +914,25 @@ 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;}
@container (min-width: 10.5rem){
.\\\\@\\\\[10\\\\.5rem\\\\]-text-red{--un-text-opacity:1;color:rgba(248,113,113,var(--un-text-opacity));}
}
@container (min-width: 24rem){
.\\\\@sm\\\\:text-red{--un-text-opacity:1;color:rgba(248,113,113,var(--un-text-opacity));}
}
@container (min-width: 32rem){
.\\\\@lg-text-red{--un-text-opacity:1;color:rgba(248,113,113,var(--un-text-opacity));}
}
@container label (min-width: 100px){
.\\\\@\\\\[100px\\\\]\\\\/label\\\\:text-green{--un-text-opacity:1;color:rgba(74,222,128,var(--un-text-opacity));}
}
@container label (min-width: 20rem){
.\\\\@xs\\\\/label\\\\:text-green{--un-text-opacity:1;color:rgba(74,222,128,var(--un-text-opacity));}
}
@layer base{
.layer-base\\\\:translate-0{--un-translate-x:0rem;--un-translate-y:0rem;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 @@ -1007,6 +1007,19 @@ export const presetMiniTargets: string[] = [
// variants - data
'data-[inline]:inline',
'data-[invalid~=grammar]:underline-green-600',

// 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',
]

export const presetMiniNonTargets = [
Expand Down

0 comments on commit ce59126

Please sign in to comment.