diff --git a/packages/preset-mini/src/_rules/container.ts b/packages/preset-mini/src/_rules/container.ts new file mode 100644 index 0000000000..51eaeff87d --- /dev/null +++ b/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, + } + }], +] diff --git a/packages/preset-mini/src/_rules/default.ts b/packages/preset-mini/src/_rules/default.ts index 9bfc460269..5204a785fa 100644 --- a/packages/preset-mini/src/_rules/default.ts +++ b/packages/preset-mini/src/_rules/default.ts @@ -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, @@ -75,6 +76,7 @@ export const rules: Rule[] = [ transitions, transforms, willChange, + containerParent, // should be the last questionMark, diff --git a/packages/preset-mini/src/_rules/index.ts b/packages/preset-mini/src/_rules/index.ts index 4e140f55fc..df1c8d94f5 100644 --- a/packages/preset-mini/src/_rules/index.ts +++ b/packages/preset-mini/src/_rules/index.ts @@ -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' diff --git a/packages/preset-mini/src/_theme/default.ts b/packages/preset-mini/src/_theme/default.ts index ccb4bd83be..42ae84d623 100644 --- a/packages/preset-mini/src/_theme/default.ts +++ b/packages/preset-mini/src/_theme/default.ts @@ -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' @@ -40,4 +40,5 @@ export const theme: Theme = { duration, ringWidth, preflightBase, + containers, } diff --git a/packages/preset-mini/src/_theme/size.ts b/packages/preset-mini/src/_theme/size.ts index 5a85bddf17..a3d4a968ee 100644 --- a/packages/preset-mini/src/_theme/size.ts +++ b/packages/preset-mini/src/_theme/size.ts @@ -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})`])) diff --git a/packages/preset-mini/src/_theme/types.ts b/packages/preset-mini/src/_theme/types.ts index 5a4677bb21..8d476c85d1 100644 --- a/packages/preset-mini/src/_theme/types.ts +++ b/packages/preset-mini/src/_theme/types.ts @@ -49,6 +49,8 @@ export interface Theme { media?: Record // supports queries supports?: Record + // container queries + containers?: Record // animation animation?: ThemeAnimation // grids diff --git a/packages/preset-mini/src/_utils/variants.ts b/packages/preset-mini/src/_utils/variants.ts index d485ec70b6..ea51ea15b9 100644 --- a/packages/preset-mini/src/_utils/variants.ts +++ b/packages/preset-mini/src/_utils/variants.ts @@ -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)) @@ -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), + ] + } } } } diff --git a/packages/preset-mini/src/_variants/aria.ts b/packages/preset-mini/src/_variants/aria.ts index 999453bec1..26caf67468 100644 --- a/packages/preset-mini/src/_variants/aria.ts +++ b/packages/preset-mini/src/_variants/aria.ts @@ -5,7 +5,7 @@ import { handler as h, variantGetParameter } from '../utils' export const variantAria: VariantObject = { name: 'aria', match(matcher, { theme }: VariantContext) { - const variant = variantGetParameter('aria', matcher, [':', '-']) + const variant = variantGetParameter('aria-', matcher, [':', '-']) if (variant) { const [match, rest] = variant const aria = h.bracket(match) ?? theme.aria?.[match] ?? '' diff --git a/packages/preset-mini/src/_variants/combinators.ts b/packages/preset-mini/src/_variants/combinators.ts index 40254d5b83..bea85d797c 100644 --- a/packages/preset-mini/src/_variants/combinators.ts +++ b/packages/preset-mini/src/_variants/combinators.ts @@ -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}`)) { diff --git a/packages/preset-mini/src/_variants/container.ts b/packages/preset-mini/src/_variants/container.ts new file mode 100644 index 0000000000..8d7062810f --- /dev/null +++ b/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) { + 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, +} diff --git a/packages/preset-mini/src/_variants/data.ts b/packages/preset-mini/src/_variants/data.ts index 9cbc08783c..8bd9315920 100644 --- a/packages/preset-mini/src/_variants/data.ts +++ b/packages/preset-mini/src/_variants/data.ts @@ -5,7 +5,7 @@ import { handler as h, variantGetParameter } from '../utils' export const variantDataAttribute: VariantObject = { name: 'data', match(matcher, { theme }: VariantContext) { - const variant = variantGetParameter('data', matcher, [':', '-']) + const variant = variantGetParameter('data-', matcher, [':', '-']) if (variant) { const [match, rest] = variant const dataAttribute = h.bracket(match) ?? theme.data?.[match] ?? '' diff --git a/packages/preset-mini/src/_variants/default.ts b/packages/preset-mini/src/_variants/default.ts index 70f0502e28..d9426d92b2 100644 --- a/packages/preset-mini/src/_variants/default.ts +++ b/packages/preset-mini/src/_variants/default.ts @@ -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[] => [ variantAria, @@ -38,5 +39,6 @@ export const variants = (options: PresetMiniOptions): Variant[] => [ ...variantLanguageDirections, variantScope, + variantContainerQuery, variantVariables, ] diff --git a/packages/preset-mini/src/_variants/index.ts b/packages/preset-mini/src/_variants/index.ts index e7b9c93dbf..1dafdb3709 100644 --- a/packages/preset-mini/src/_variants/index.ts +++ b/packages/preset-mini/src/_variants/index.ts @@ -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' diff --git a/packages/preset-mini/src/_variants/media.ts b/packages/preset-mini/src/_variants/media.ts index 1f682ca499..90fcd1259a 100644 --- a/packages/preset-mini/src/_variants/media.ts +++ b/packages/preset-mini/src/_variants/media.ts @@ -7,7 +7,7 @@ export const variantPrint: Variant = variantParentMatcher('print', '@media print export const variantCustomMedia: VariantObject = { name: 'media', match(matcher, { theme }: VariantContext) { - const variant = variantGetParameter('media', matcher, [':', '-']) + const variant = variantGetParameter('media-', matcher, [':', '-']) if (variant) { const [match, rest] = variant diff --git a/packages/preset-mini/src/_variants/misc.ts b/packages/preset-mini/src/_variants/misc.ts index 594c102dfd..3392bbbece 100644 --- a/packages/preset-mini/src/_variants/misc.ts +++ b/packages/preset-mini/src/_variants/misc.ts @@ -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) @@ -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 @@ -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 @@ -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) diff --git a/packages/preset-mini/src/_variants/pseudo.ts b/packages/preset-mini/src/_variants/pseudo.ts index bd15b95a93..e018cb0b17 100644 --- a/packages/preset-mini/src/_variants/pseudo.ts +++ b/packages/preset-mini/src/_variants/pseudo.ts @@ -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 @@ -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, diff --git a/packages/preset-mini/src/_variants/supports.ts b/packages/preset-mini/src/_variants/supports.ts index 8acdce0427..14e7d5ff73 100644 --- a/packages/preset-mini/src/_variants/supports.ts +++ b/packages/preset-mini/src/_variants/supports.ts @@ -5,7 +5,7 @@ import { handler as h, variantGetParameter } from '../utils' export const variantSupports: VariantObject = { name: 'supports', match(matcher, { theme }: VariantContext) { - const variant = variantGetParameter('supports', matcher, [':', '-']) + const variant = variantGetParameter('supports-', matcher, [':', '-']) if (variant) { const [match, rest] = variant diff --git a/packages/preset-wind/src/rules/default.ts b/packages/preset-wind/src/rules/default.ts index 823c09732c..0b23d02c44 100644 --- a/packages/preset-wind/src/rules/default.ts +++ b/packages/preset-wind/src/rules/default.ts @@ -8,6 +8,7 @@ import { borders, boxShadows, boxSizing, + containerParent, contentVisibility, contents, cssProperty, @@ -155,6 +156,7 @@ export const rules: Rule[] = [ contentVisibility, contents, placeholders, + containerParent, // should be the last questionMark, diff --git a/test/__snapshots__/preset-mini.test.ts.snap b/test/__snapshots__/preset-mini.test.ts.snap index 22a8c65e08..344fc77e31 100644 --- a/test/__snapshots__/preset-mini.test.ts.snap +++ b/test/__snapshots__/preset-mini.test.ts.snap @@ -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));} } diff --git a/test/assets/preset-mini-targets.ts b/test/assets/preset-mini-targets.ts index 5b9eeb4725..5f8e73e08b 100644 --- a/test/assets/preset-mini-targets.ts +++ b/test/assets/preset-mini-targets.ts @@ -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 = [