From 35bf48dffcc7ba2495093e6a2b326a1400372801 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 4 Jun 2024 15:57:01 -0400 Subject: [PATCH] Add mask image utilities --- packages/tailwindcss/src/utilities.test.ts | 180 ++++++++++++++++++++ packages/tailwindcss/src/utilities.ts | 185 +++++++++++++++++++++ 2 files changed, 365 insertions(+) diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index 6038ca164..892bd7f21 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -8100,6 +8100,186 @@ test('bg', () => { `) }) +test('mask', () => { + expect( + compileCss( + css` + @theme { + --color-red-500: #ef4444; + } + @tailwind utilities; + `, + [ + // mask-mode + 'mask-alpha', + 'mask-luminance', + 'mask-match-source', + + // mask-image + 'mask-none', + + 'mask-linear-to-t', + 'mask-linear-to-tr', + 'mask-linear-to-r', + 'mask-linear-to-br', + 'mask-linear-to-b', + 'mask-linear-to-bl', + 'mask-linear-to-l', + 'mask-linear-to-tl', + + 'mask-[url(/image.png)]', + 'mask-[url:--my-url]', + 'mask-[linear-gradient(to_bottom,red,blue)]', + 'mask-[image:--my-gradient]', + 'mask-linear-[125deg]', + 'mask-linear-[1.3rad]', + 'mask-linear-[to_bottom]', + '-mask-linear-[125deg]', + '-mask-linear-[1.3rad]', + ], + ), + ).toMatchInlineSnapshot(` + ":root { + --color-red-500: #ef4444; + } + + .mask-\\[image\\:--my-gradient\\] { + background-image: var(--my-gradient); + } + + .mask-\\[linear-gradient\\(to_bottom\\,red\\,blue\\)\\] { + background-image: linear-gradient(red, #00f); + } + + .mask-\\[url\\(\\/image\\.png\\)\\] { + background-image: url("/image.png"); + } + + .mask-\\[url\\:--my-url\\] { + background-image: var(--my-url); + } + + .-mask-linear-\\[1\\.3rad\\] { + mask-image: linear-gradient(calc(74.4845deg * -1), var(--tw-mask-stops, )); + } + + .-mask-linear-\\[125deg\\] { + mask-image: linear-gradient(calc(125deg * -1), var(--tw-mask-stops, )); + } + + .mask-alpha { + mask-mode: alpha; + } + + .mask-linear-\\[1\\.3rad\\] { + mask-image: linear-gradient(74.4845deg, var(--tw-mask-stops, )); + } + + .mask-linear-\\[125deg\\] { + mask-image: linear-gradient(125deg, var(--tw-mask-stops, )); + } + + .mask-linear-\\[to_bottom\\], .mask-linear-to-b { + mask-image: linear-gradient(to bottom, var(--tw-mask-stops, )); + } + + .mask-linear-to-bl { + mask-image: linear-gradient(to bottom left, var(--tw-mask-stops, )); + } + + .mask-linear-to-br { + mask-image: linear-gradient(to bottom right, var(--tw-mask-stops, )); + } + + .mask-linear-to-l { + mask-image: linear-gradient(to left, var(--tw-mask-stops, )); + } + + .mask-linear-to-r { + mask-image: linear-gradient(to right, var(--tw-mask-stops, )); + } + + .mask-linear-to-t { + mask-image: linear-gradient(to top, var(--tw-mask-stops, )); + } + + .mask-linear-to-tl { + mask-image: linear-gradient(to top left, var(--tw-mask-stops, )); + } + + .mask-linear-to-tr { + mask-image: linear-gradient(to top right, var(--tw-mask-stops, )); + } + + .mask-luminance { + mask-mode: luminance; + } + + .mask-match-source { + mask-mode: match-source; + } + + .mask-none { + mask-image: none; + }" + `) + expect( + run([ + 'mask', + 'mask-unknown', + + // mask-mode + '-mask-alpha', + '-mask-luminance', + '-mask-match-source', + + // mask-image + '-mask-none', + + '-mask-linear-to-t', + '-mask-linear-to-tr', + '-mask-linear-to-r', + '-mask-linear-to-br', + '-mask-linear-to-b', + '-mask-linear-to-bl', + '-mask-linear-to-l', + '-mask-linear-to-tl', + + '-mask-[url(/image.png)]', + '-mask-[url:--my-url]', + '-mask-[linear-gradient(to_bottom,red,blue)]', + '-mask-[image:--my-gradient]', + '-mask-linear-[to_bottom]', + ]), + ).toEqual('') + + expect( + compileCss( + css` + @theme { + --opacity-half: 0.5; + --opacity-custom: var(--custom-opacity); + } + @tailwind utilities; + `, + ['bg-current/half', 'bg-current/custom'], + ), + ).toMatchInlineSnapshot(` + ":root { + --opacity-half: .5; + --opacity-custom: var(--custom-opacity); + } + + .bg-current\\/custom { + background-color: color-mix(in srgb, currentColor var(--opacity-custom, var(--custom-opacity)), transparent); + } + + .bg-current\\/half { + background-color: color-mix(in srgb, currentColor var(--opacity-half, .5), transparent); + }" + `) +}) + test('from', () => { expect( compileCss( diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index 328eb3da3..4ad4a8945 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -2501,8 +2501,39 @@ export function createUtilities(theme: Theme) { staticUtility(`bg-linear-to-${value}`, [ ['background-image', `linear-gradient(to ${direction}, var(--tw-gradient-stops,))`], ]) + + staticUtility(`mask-linear-to-${value}`, [ + ['mask-image', `linear-gradient(to ${direction}, var(--tw-mask-stops,))`], + ]) } + staticUtility('mask-none', [['mask-image', 'none']]) + staticUtility('mask-alpha', [['mask-mode', 'alpha']]) + staticUtility('mask-luminance', [['mask-mode', 'luminance']]) + staticUtility('mask-match-source', [['mask-mode', 'match-source']]) + + utilities.functional('mask-linear', (candidate) => { + if (!candidate.value) return + + if (candidate.value.kind === 'arbitrary') { + let value: string | null = candidate.value.value + let type = candidate.value.dataType ?? inferDataType(value, ['angle']) + + switch (type) { + case 'angle': { + value = withNegative(value, candidate) + + return [decl('mask-image', `linear-gradient(${value}, var(--tw-mask-stops,))`)] + } + default: { + if (candidate.negative) return + + return [decl('mask-image', `linear-gradient(${value}, var(--tw-mask-stops,))`)] + } + } + } + }) + utilities.functional('bg-linear', (candidate) => { if (!candidate.value) return @@ -2525,6 +2556,41 @@ export function createUtilities(theme: Theme) { } }) + utilities.functional('mask', (candidate) => { + if (candidate.negative || !candidate.value) return + + // Arbitrary values + if (candidate.value.kind === 'arbitrary') { + let value: string | null = candidate.value.value + let type = candidate.value.dataType ?? inferDataType(value, ['image', 'url']) + + switch (type) { + case 'image': + case 'url': { + return [decl('background-image', value)] + } + default: { + return + } + } + } + + // `background-image` property + { + let value = theme.resolve(candidate.value.value, ['--background-image']) + if (value) { + return [decl('mask-image', value)] + } + } + }) + + suggest('mask', () => [ + { + values: [], + valueThemeKeys: ['--background-image'], + }, + ]) + utilities.functional('bg', (candidate) => { if (candidate.negative || !candidate.value) return @@ -2611,11 +2677,31 @@ export function createUtilities(theme: Theme) { ]) } + let maskStopProperties = () => { + return atRoot([ + property('--tw-mask-from', '#0000', ''), + property('--tw-mask-to', '#0000', ''), + property('--tw-mask-from', 'transparent', ''), + property('--tw-mask-via', 'transparent', ''), + property('--tw-mask-to', 'transparent', ''), + property('--tw-mask-stops'), + property('--tw-mask-via-stops'), + property('--tw-mask-from-position', '0%', ' | '), + property('--tw-mask-via-position', '50%', ' | '), + property('--tw-mask-to-position', '100%', ' | '), + ]) + } + type GradientStopDescription = { color: (value: string) => AstNode[] | undefined position: (value: string) => AstNode[] | undefined } + type MaskStopDescription = { + color: (value: string) => AstNode[] | undefined + position: (value: string) => AstNode[] | undefined + } + function gradientStopUtility(classRoot: string, desc: GradientStopDescription) { utilities.functional(classRoot, (candidate) => { if (candidate.negative || !candidate.value) return @@ -2676,6 +2762,66 @@ export function createUtilities(theme: Theme) { ]) } + function maskStopUtility(classRoot: string, desc: MaskStopDescription) { + utilities.functional(classRoot, (candidate) => { + if (candidate.negative || !candidate.value) return + + // Arbitrary values + if (candidate.value.kind === 'arbitrary') { + let value: string | null = candidate.value.value + let type = + candidate.value.dataType ?? inferDataType(value, ['color', 'length', 'percentage']) + + switch (type) { + case 'length': + case 'percentage': { + return desc.position(value) + } + default: { + value = asColor(value, candidate.modifier, theme) + if (value === null) return + + return desc.color(value) + } + } + } + + // Known values: Color Stops + { + let value = resolveThemeColor(candidate, theme, ['--background-color', '--color']) + if (value) { + return desc.color(value) + } + } + + // Known values: Positions + { + let value = theme.resolve(candidate.value.value, ['--gradient-color-stop-positions']) + if (value) { + return desc.position(value) + } else if ( + candidate.value.value[candidate.value.value.length - 1] === '%' && + !Number.isNaN(Number(candidate.value.value.slice(0, -1))) + ) { + return desc.position(candidate.value.value) + } + } + }) + + suggest(classRoot, () => [ + { + values: ['current', 'inherit', 'transparent'], + valueThemeKeys: ['--background-color', '--color'], + modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`), + modifierThemeKeys: ['--opacity'], + }, + { + values: Array.from({ length: 21 }, (_, index) => `${index * 5}%`), + valueThemeKeys: ['--gradient-color-stop-positions'], + }, + ]) + } + gradientStopUtility('from', { color: (value) => [ gradientStopProperties(), @@ -2715,6 +2861,45 @@ export function createUtilities(theme: Theme) { position: (value) => [gradientStopProperties(), decl('--tw-gradient-to-position', value)], }) + maskStopUtility('mask-from', { + color: (value) => [ + maskStopProperties(), + decl('--tw-sort', '--tw-mask-from'), + decl('--tw-mask-from', value), + decl( + '--tw-mask-stops', + 'var(--tw-mask-via-stops, var(--tw-mask-from) var(--tw-mask-from-position), var(--tw-mask-to) var(--tw-mask-to-position))', + ), + ], + position: (value) => [maskStopProperties(), decl('--tw-mask-from-position', value)], + }) + staticUtility('mask-via-none', [['--tw-mask-via-stops', 'initial']]) + maskStopUtility('mask-via', { + color: (value) => [ + maskStopProperties(), + decl('--tw-sort', '--tw-mask-via'), + decl('--tw-mask-via', value), + decl( + '--tw-mask-via-stops', + 'var(--tw-mask-from) var(--tw-mask-from-position), var(--tw-mask-via) var(--tw-mask-via-position), var(--tw-mask-to) var(--tw-mask-to-position)', + ), + decl('--tw-mask-stops', 'var(--tw-mask-via-stops)'), + ], + position: (value) => [maskStopProperties(), decl('--tw-mask-via-position', value)], + }) + maskStopUtility('mask-to', { + color: (value) => [ + maskStopProperties(), + decl('--tw-sort', '--tw-mask-to'), + decl('--tw-mask-to', value), + decl( + '--tw-mask-stops', + 'var(--tw-mask-via-stops, var(--tw-mask-from) var(--tw-mask-from-position), var(--tw-mask-to) var(--tw-mask-to-position))', + ), + ], + position: (value) => [maskStopProperties(), decl('--tw-mask-to-position', value)], + }) + /** * @css `box-decoration-break` */