Compare commits
6 commits
main
...
feat/v4-ut
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81d2a7562e | ||
|
|
fcee7e1eac | ||
|
|
f463878ecb | ||
|
|
0b69c4c915 | ||
|
|
542a47401f | ||
|
|
1a1d0dffb2 |
3 changed files with 888 additions and 2 deletions
|
|
@ -11,16 +11,49 @@ import {
|
|||
type CssInJs,
|
||||
type Rule,
|
||||
} from './ast'
|
||||
import type { Candidate } from './candidate'
|
||||
import { compileCandidates } from './compile'
|
||||
import * as CSS from './css-parser'
|
||||
import { buildDesignSystem, type DesignSystem } from './design-system'
|
||||
import { Theme } from './theme'
|
||||
import { withAlpha, withNegative } from './utilities'
|
||||
import { inferDataType } from './utils/infer-data-type'
|
||||
import { segment } from './utils/segment'
|
||||
|
||||
const IS_VALID_UTILITY_NAME = /^[a-z][a-zA-Z0-9/%._-]*$/
|
||||
const IS_VALID_UTILITY_SELECTOR = /^\.[a-z][a-zA-Z0-9/%._-]*$/
|
||||
|
||||
type PluginAPI = {
|
||||
addVariant(name: string, variant: string | string[] | CssInJs): void
|
||||
addUtilities(
|
||||
utilities: Record<string, CssInJs>,
|
||||
options?: Partial<{
|
||||
// todo: maybe not necessary any more
|
||||
// respectPrefix: boolean
|
||||
|
||||
// todo: needed??
|
||||
respectImportant: boolean
|
||||
}>,
|
||||
): void
|
||||
matchUtilities(
|
||||
utilities: Record<string, (value: string, extra: { modifier: string | null }) => CssInJs>,
|
||||
options?: Partial<{
|
||||
type: string | string[]
|
||||
|
||||
// todo: maybe not necessary any more
|
||||
// respectPrefix: boolean
|
||||
|
||||
// todo: needed??
|
||||
respectImportant: boolean
|
||||
|
||||
supportsNegativeValues: boolean
|
||||
|
||||
values: Record<string, string>
|
||||
modifiers: 'any' | Record<string, string>
|
||||
}>,
|
||||
): void
|
||||
|
||||
theme(path: string, fallback?: any): any
|
||||
}
|
||||
|
||||
type Plugin = (api: PluginAPI) => void
|
||||
|
|
@ -280,6 +313,202 @@ export function compile(
|
|||
designSystem.variants.fromAst(name, objectToAst(variant))
|
||||
}
|
||||
},
|
||||
|
||||
addUtilities(utilities) {
|
||||
for (let [name, css] of Object.entries(utilities)) {
|
||||
if (!IS_VALID_UTILITY_SELECTOR.test(name)) {
|
||||
throw new Error(
|
||||
`\`addUtilities({ '${name}' : … })\` defines an invalid utility selector. Utilities are a single class that is alphanumeric and starts with a lowercase letter.`,
|
||||
)
|
||||
}
|
||||
|
||||
designSystem.utilities.static(name.slice(1), (candidate) => {
|
||||
if (candidate.negative) return
|
||||
|
||||
return objectToAst(css)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
matchUtilities(utilities, options) {
|
||||
type Resolvable =
|
||||
| Extract<Candidate, { kind: 'functional' }>['value']
|
||||
| Extract<Candidate, { kind: 'functional' }>['modifier']
|
||||
|
||||
let invalid = Symbol('invalid')
|
||||
|
||||
let types = options?.type
|
||||
? Array.isArray(options?.type)
|
||||
? options.type
|
||||
: [options.type]
|
||||
: []
|
||||
|
||||
function resolve(
|
||||
item: Resolvable,
|
||||
list: 'any' | Record<string, any> | null,
|
||||
resolveBare: ((value: string) => string | null) | null,
|
||||
) {
|
||||
if (!item) {
|
||||
if (list && typeof list === 'object' && list.DEFAULT) {
|
||||
return list.DEFAULT
|
||||
}
|
||||
|
||||
// Falsy values are invalid
|
||||
return null
|
||||
}
|
||||
|
||||
// Arbitrary values and modifiers are also used as-is
|
||||
if (item.kind === 'arbitrary') return item.value
|
||||
|
||||
// In the case of modifiers: 'any' the value we're passed can be used as-is
|
||||
if (list === 'any') return item.value
|
||||
|
||||
// There's no list of valid, named values so this is invalid
|
||||
if (!list) return null
|
||||
|
||||
// If the value isn't in the list:
|
||||
if (!(item.value in list)) {
|
||||
// And bare "values" (modifiers?) are supported then try to use that
|
||||
if (resolveBare) {
|
||||
if (Number.isNaN(Number(item.value))) {
|
||||
return invalid
|
||||
}
|
||||
|
||||
return resolveBare(item.value) ?? invalid
|
||||
}
|
||||
|
||||
// Otherwise it is invalid
|
||||
return invalid
|
||||
}
|
||||
|
||||
// Otherwise we'll return the value supplied by list
|
||||
// - `options.values`
|
||||
// - `options.modifiers`
|
||||
return list[item.value]
|
||||
}
|
||||
|
||||
for (let [name, fn] of Object.entries(utilities)) {
|
||||
if (!IS_VALID_UTILITY_NAME.test(name)) {
|
||||
throw new Error(
|
||||
`\`matchUtilities({ '${name}' : … })\` defines an invalid utility name. Utilities should be alphanumeric and start with a lowercase letter.`,
|
||||
)
|
||||
}
|
||||
|
||||
designSystem.utilities.functional(name, (candidate) => {
|
||||
// Any negative candiate without support is invalid
|
||||
if (!options?.supportsNegativeValues && candidate.negative) return
|
||||
|
||||
// If this utility supports color values — try resolving as a color
|
||||
let modifiers = options?.modifiers ?? null
|
||||
|
||||
if (types.includes('color')) {
|
||||
// Colors implicitly support modifiers when no modifiers are provided
|
||||
// They're read from the opacity scale'
|
||||
if (!modifiers) {
|
||||
modifiers = Object.fromEntries(theme.namespace('--opacity').entries())
|
||||
}
|
||||
}
|
||||
|
||||
if (candidate.modifier && !modifiers) return
|
||||
|
||||
let modifier = resolve(candidate.modifier, modifiers, (value) => {
|
||||
if (!types.includes('color')) return null
|
||||
return `${value}%`
|
||||
})
|
||||
|
||||
if (modifier === invalid) return
|
||||
|
||||
let value = resolve(
|
||||
candidate.value,
|
||||
{
|
||||
inherit: 'inherit',
|
||||
transparent: 'transparent',
|
||||
current: 'currentColor',
|
||||
...(options?.values ?? null),
|
||||
},
|
||||
null,
|
||||
)
|
||||
|
||||
if (!value) return
|
||||
if (value === invalid) return
|
||||
|
||||
if (types.includes('color') && modifier) {
|
||||
value = withAlpha(value, modifier)
|
||||
}
|
||||
|
||||
// Throw out any candidate whose value isn't not of a support type
|
||||
if (candidate.value?.kind === 'arbitrary' && types.length > 0 && !types.includes('any')) {
|
||||
// Bail when the candidate has an explicit data type but it's not in
|
||||
// the list of supported types by this utility For example, given a
|
||||
// `scrollbar` utility that is used to change its color:
|
||||
// scrollbar-[length:var(--whatever)]
|
||||
if (candidate.value.dataType && !types.includes(candidate.value.dataType)) {
|
||||
return
|
||||
}
|
||||
|
||||
// We also need to bail when the candidate does not have an explicit
|
||||
// type and we're not able to infer it as one of the supported types.
|
||||
if (
|
||||
!candidate.value.dataType &&
|
||||
!inferDataType(candidate.value.value, types as any[])
|
||||
) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (candidate.negative) {
|
||||
value = withNegative(value, candidate)
|
||||
}
|
||||
|
||||
return objectToAst(fn(value, { modifier }))
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
theme(path: string, fallback?: any) {
|
||||
if (path.startsWith('--')) {
|
||||
if (path.endsWith('-*')) {
|
||||
return Object.fromEntries(theme.namespace(path.slice(0, -2) as any).entries())
|
||||
}
|
||||
|
||||
return theme.resolveValue(null, [path] as any) ?? fallback ?? null
|
||||
}
|
||||
|
||||
path = path
|
||||
// Escape dots used inside square brackets
|
||||
.replace(/\[(.*?)\]/g, (_, value) => `-${value.replace('.', '_')}`)
|
||||
// Replace dots with dashes
|
||||
.replace(/\./g, '-')
|
||||
// Replace camelCase with dashes
|
||||
.replace(/([a-z])([A-Z])/g, (_, a, b) => `${a}-${b.toLowerCase()}`)
|
||||
|
||||
// Prepend with `--` to match CSS variables
|
||||
path = `--${path}`
|
||||
|
||||
let map = theme.namespace(path as any)
|
||||
|
||||
// Does the requested value exist in the theme
|
||||
if (map.has(null)) {
|
||||
// Yes, and there are multiple values in the requested theme namespace
|
||||
if (map.size > 1) {
|
||||
return {
|
||||
DEFAULT: map.get(null),
|
||||
...Object.fromEntries(Array.from(map.entries()).filter(([key]) => key !== null)),
|
||||
}
|
||||
}
|
||||
|
||||
// Nope, just the one
|
||||
return map.get(null)
|
||||
}
|
||||
|
||||
// There is at least one value in the requested theme namespace
|
||||
// but no default value
|
||||
if (map.size > 0) {
|
||||
return Object.fromEntries(map.entries())
|
||||
}
|
||||
|
||||
return fallback ?? null
|
||||
},
|
||||
}
|
||||
|
||||
for (let plugin of plugins) {
|
||||
|
|
|
|||
|
|
@ -15150,3 +15150,660 @@ describe('custom utilities', () => {
|
|||
).toThrowError(/should be alphanumeric/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('legacy: addUtilities', () => {
|
||||
test('custom static utility', () => {
|
||||
let compiled = compile(
|
||||
css`
|
||||
@plugin "my-plugin";
|
||||
@layer utilities {
|
||||
@tailwind utilities;
|
||||
}
|
||||
|
||||
@theme reference {
|
||||
--breakpoint-lg: 1024px;
|
||||
},
|
||||
`,
|
||||
{
|
||||
loadPlugin() {
|
||||
return ({ addUtilities }) => {
|
||||
addUtilities({
|
||||
'.text-trim': {
|
||||
'text-box-trim': 'both',
|
||||
'text-box-edge': 'cap alphabetic',
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
).build(['text-trim', 'lg:text-trim'])
|
||||
|
||||
expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(`
|
||||
"@layer utilities {
|
||||
.text-trim {
|
||||
text-box-trim: both;
|
||||
text-box-edge: cap alphabetic;
|
||||
}
|
||||
|
||||
@media (width >= 1024px) {
|
||||
.lg\\:text-trim {
|
||||
text-box-trim: both;
|
||||
text-box-edge: cap alphabetic;
|
||||
}
|
||||
}
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('throws on custom static utilities with an invalid name', () => {
|
||||
expect(() => {
|
||||
return compile(
|
||||
css`
|
||||
@plugin "my-plugin";
|
||||
@layer utilities {
|
||||
@tailwind utilities;
|
||||
}
|
||||
|
||||
@theme reference {
|
||||
--breakpoint-lg: 1024px;
|
||||
},
|
||||
`,
|
||||
{
|
||||
loadPlugin() {
|
||||
return ({ addUtilities }) => {
|
||||
addUtilities({
|
||||
'.text-trim > *': {
|
||||
'text-box-trim': 'both',
|
||||
'text-box-edge': 'cap alphabetic',
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
}).toThrowError(/invalid utility selector/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('legacy: matchUtilities', () => {
|
||||
test('custom functional utility', () => {
|
||||
function run(candidates: string[]) {
|
||||
return compile(
|
||||
css`
|
||||
@plugin "my-plugin";
|
||||
|
||||
@tailwind utilities;
|
||||
|
||||
@theme reference {
|
||||
--breakpoint-lg: 1024px;
|
||||
},
|
||||
`,
|
||||
{
|
||||
loadPlugin() {
|
||||
return ({ matchUtilities }) => {
|
||||
matchUtilities(
|
||||
{
|
||||
'border-block': (value) => {
|
||||
return {
|
||||
'border-block-width': value,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
values: {
|
||||
DEFAULT: '1px',
|
||||
'2': '2px',
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
).build(candidates)
|
||||
}
|
||||
|
||||
expect(
|
||||
optimizeCss(
|
||||
run([
|
||||
'border-block',
|
||||
'border-block-2',
|
||||
'border-block-[35px]',
|
||||
'border-block-[var(--foo)]',
|
||||
'lg:border-block-2',
|
||||
]),
|
||||
).trim(),
|
||||
).toMatchInlineSnapshot(`
|
||||
".border-block {
|
||||
border-block-width: 1px;
|
||||
}
|
||||
|
||||
.border-block-2 {
|
||||
border-block-width: 2px;
|
||||
}
|
||||
|
||||
.border-block-\\[35px\\] {
|
||||
border-block-width: 35px;
|
||||
}
|
||||
|
||||
.border-block-\\[var\\(--foo\\)\\] {
|
||||
border-block-width: var(--foo);
|
||||
}
|
||||
|
||||
@media (width >= 1024px) {
|
||||
.lg\\:border-block-2 {
|
||||
border-block-width: 2px;
|
||||
}
|
||||
}"
|
||||
`)
|
||||
|
||||
expect(
|
||||
optimizeCss(
|
||||
run([
|
||||
'-border-block',
|
||||
'-border-block-2',
|
||||
'lg:-border-block-2',
|
||||
'border-block-unknown',
|
||||
'border-block/1',
|
||||
]),
|
||||
).trim(),
|
||||
).toEqual('')
|
||||
})
|
||||
|
||||
test('custom functional utility with any modifier', () => {
|
||||
function run(candidates: string[]) {
|
||||
return compile(
|
||||
css`
|
||||
@plugin "my-plugin";
|
||||
|
||||
@tailwind utilities;
|
||||
|
||||
@theme reference {
|
||||
--breakpoint-lg: 1024px;
|
||||
},
|
||||
`,
|
||||
{
|
||||
loadPlugin() {
|
||||
return ({ matchUtilities }) => {
|
||||
matchUtilities(
|
||||
{
|
||||
'border-block': (value, { modifier }) => {
|
||||
return {
|
||||
'--my-modifier': modifier ?? 'none',
|
||||
'border-block-width': value,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
values: {
|
||||
DEFAULT: '1px',
|
||||
'2': '2px',
|
||||
},
|
||||
|
||||
modifiers: 'any',
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
).build(candidates)
|
||||
}
|
||||
|
||||
expect(
|
||||
optimizeCss(
|
||||
run(['border-block', 'border-block-2', 'border-block/foo', 'border-block-2/foo']),
|
||||
).trim(),
|
||||
).toMatchInlineSnapshot(`
|
||||
".border-block {
|
||||
--my-modifier: none;
|
||||
border-block-width: 1px;
|
||||
}
|
||||
|
||||
.border-block-2 {
|
||||
--my-modifier: none;
|
||||
border-block-width: 2px;
|
||||
}
|
||||
|
||||
.border-block-2\\/foo {
|
||||
--my-modifier: foo;
|
||||
border-block-width: 2px;
|
||||
}
|
||||
|
||||
.border-block\\/foo {
|
||||
--my-modifier: foo;
|
||||
border-block-width: 1px;
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('custom functional utility with known modifier', () => {
|
||||
function run(candidates: string[]) {
|
||||
return compile(
|
||||
css`
|
||||
@plugin "my-plugin";
|
||||
|
||||
@tailwind utilities;
|
||||
|
||||
@theme reference {
|
||||
--breakpoint-lg: 1024px;
|
||||
},
|
||||
`,
|
||||
{
|
||||
loadPlugin() {
|
||||
return ({ matchUtilities }) => {
|
||||
matchUtilities(
|
||||
{
|
||||
'border-block': (value, { modifier }) => {
|
||||
return {
|
||||
'--my-modifier': modifier ?? 'none',
|
||||
'border-block-width': value,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
values: {
|
||||
DEFAULT: '1px',
|
||||
'2': '2px',
|
||||
},
|
||||
|
||||
modifiers: {
|
||||
foo: 'foo',
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
).build(candidates)
|
||||
}
|
||||
|
||||
expect(
|
||||
optimizeCss(
|
||||
run([
|
||||
'border-block',
|
||||
'border-block-2',
|
||||
'border-block/foo',
|
||||
'border-block-2/foo',
|
||||
'border-block/unknown',
|
||||
'border-block-2/unknown',
|
||||
]),
|
||||
).trim(),
|
||||
).toMatchInlineSnapshot(`
|
||||
".border-block {
|
||||
--my-modifier: none;
|
||||
border-block-width: 1px;
|
||||
}
|
||||
|
||||
.border-block-2 {
|
||||
--my-modifier: none;
|
||||
border-block-width: 2px;
|
||||
}
|
||||
|
||||
.border-block-2\\/foo {
|
||||
--my-modifier: foo;
|
||||
border-block-width: 2px;
|
||||
}
|
||||
|
||||
.border-block\\/foo {
|
||||
--my-modifier: foo;
|
||||
border-block-width: 1px;
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('throws on custom utilities with an invalid name', () => {
|
||||
expect(() => {
|
||||
return compile(
|
||||
css`
|
||||
@plugin "my-plugin";
|
||||
@layer utilities {
|
||||
@tailwind utilities;
|
||||
}
|
||||
|
||||
@theme reference {
|
||||
--breakpoint-lg: 1024px;
|
||||
},
|
||||
`,
|
||||
{
|
||||
loadPlugin() {
|
||||
return ({ matchUtilities }) => {
|
||||
matchUtilities({
|
||||
'.text-trim > *': () => ({
|
||||
'text-box-trim': 'both',
|
||||
'text-box-edge': 'cap alphabetic',
|
||||
}),
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
}).toThrowError(/invalid utility name/)
|
||||
})
|
||||
|
||||
test('custom functional utilities with different types', () => {
|
||||
function run(candidates: string[]) {
|
||||
return compile(
|
||||
css`
|
||||
@plugin "my-plugin";
|
||||
|
||||
@tailwind utilities;
|
||||
|
||||
@theme reference {
|
||||
--breakpoint-lg: 1024px;
|
||||
},
|
||||
`,
|
||||
{
|
||||
loadPlugin() {
|
||||
return ({ matchUtilities }) => {
|
||||
matchUtilities(
|
||||
{
|
||||
scrollbar: (value) => {
|
||||
return {
|
||||
'scrollbar-color': value,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
type: ['color', 'any'],
|
||||
values: {
|
||||
black: 'black',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
matchUtilities(
|
||||
{
|
||||
scrollbar: (value) => {
|
||||
return {
|
||||
'scrollbar-width': value,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
type: ['length'],
|
||||
values: {
|
||||
2: '2px',
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
).build(candidates)
|
||||
}
|
||||
|
||||
expect(
|
||||
optimizeCss(
|
||||
run([
|
||||
'scrollbar-black',
|
||||
'scrollbar-2',
|
||||
'scrollbar-[#fff]',
|
||||
'scrollbar-[2px]',
|
||||
'scrollbar-[var(--my-color)]',
|
||||
'scrollbar-[color:var(--my-color)]',
|
||||
'scrollbar-[length:var(--my-width)]',
|
||||
]),
|
||||
).trim(),
|
||||
).toMatchInlineSnapshot(`
|
||||
".scrollbar-2 {
|
||||
scrollbar-width: 2px;
|
||||
}
|
||||
|
||||
.scrollbar-\\[\\#fff\\] {
|
||||
scrollbar-color: #fff;
|
||||
}
|
||||
|
||||
.scrollbar-\\[2px\\] {
|
||||
scrollbar-width: 2px;
|
||||
}
|
||||
|
||||
.scrollbar-\\[color\\:var\\(--my-color\\)\\] {
|
||||
scrollbar-color: var(--my-color);
|
||||
}
|
||||
|
||||
.scrollbar-\\[length\\:var\\(--my-width\\)\\] {
|
||||
scrollbar-width: var(--my-width);
|
||||
}
|
||||
|
||||
.scrollbar-\\[var\\(--my-color\\)\\] {
|
||||
scrollbar-color: var(--my-color);
|
||||
}
|
||||
|
||||
.scrollbar-black {
|
||||
scrollbar-color: black;
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('custom utility that reads from the theme', () => {
|
||||
function run(candidates: string[]) {
|
||||
return compile(
|
||||
css`
|
||||
@plugin "my-plugin";
|
||||
|
||||
@tailwind utilities;
|
||||
|
||||
@theme reference {
|
||||
--scrollbar-big: 20px;
|
||||
},
|
||||
`,
|
||||
{
|
||||
loadPlugin() {
|
||||
return ({ matchUtilities, theme }) => {
|
||||
matchUtilities(
|
||||
{
|
||||
scrollbar: (value, { modifier }) => {
|
||||
return {
|
||||
'--my-modifier': modifier ?? 'none',
|
||||
'border-block-width': value,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
values: theme('scrollbar'),
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
).build(candidates)
|
||||
}
|
||||
|
||||
expect(optimizeCss(run(['scrollbar-big'])).trim()).toMatchInlineSnapshot(`
|
||||
".scrollbar-big {
|
||||
--my-modifier: none;
|
||||
border-block-width: 20px;
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('functional utilities with type: color automatically support opacity', () => {
|
||||
function run(candidates: string[]) {
|
||||
return compile(
|
||||
css`
|
||||
@plugin "my-plugin";
|
||||
|
||||
@tailwind utilities;
|
||||
|
||||
@theme reference {
|
||||
--breakpoint-lg: 1024px;
|
||||
--opacity-my-opacity: 0.5;
|
||||
},
|
||||
`,
|
||||
{
|
||||
loadPlugin() {
|
||||
return ({ matchUtilities }) => {
|
||||
matchUtilities(
|
||||
{
|
||||
scrollbar: (value) => {
|
||||
return {
|
||||
'scrollbar-color': value,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
type: ['color', 'any'],
|
||||
values: {
|
||||
black: 'black',
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
).build(candidates)
|
||||
}
|
||||
|
||||
expect(
|
||||
optimizeCss(
|
||||
run([
|
||||
'scrollbar-current',
|
||||
'scrollbar-current/45',
|
||||
'scrollbar-black',
|
||||
'scrollbar-black/my-opacity',
|
||||
'scrollbar-black/33',
|
||||
'scrollbar-black/[50%]',
|
||||
'scrollbar-[var(--my-color)]/[25%]',
|
||||
]),
|
||||
).trim(),
|
||||
).toMatchInlineSnapshot(`
|
||||
".scrollbar-\\[var\\(--my-color\\)\\]\\/\\[25\\%\\] {
|
||||
scrollbar-color: color-mix(in srgb, var(--my-color) 25%, transparent);
|
||||
}
|
||||
|
||||
.scrollbar-black {
|
||||
scrollbar-color: black;
|
||||
}
|
||||
|
||||
.scrollbar-black\\/33 {
|
||||
scrollbar-color: #00000054;
|
||||
}
|
||||
|
||||
.scrollbar-black\\/\\[50\\%\\], .scrollbar-black\\/my-opacity {
|
||||
scrollbar-color: #00000080;
|
||||
}
|
||||
|
||||
.scrollbar-current {
|
||||
scrollbar-color: currentColor;
|
||||
}
|
||||
|
||||
.scrollbar-current\\/45 {
|
||||
scrollbar-color: color-mix(in srgb, currentColor 45%, transparent);
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('functional utilities with type: color and explicit modifiers', () => {
|
||||
function run(candidates: string[]) {
|
||||
return compile(
|
||||
css`
|
||||
@plugin "my-plugin";
|
||||
|
||||
@tailwind utilities;
|
||||
|
||||
@theme reference {
|
||||
--breakpoint-lg: 1024px;
|
||||
--opacity-my-opacity: 0.5;
|
||||
},
|
||||
`,
|
||||
{
|
||||
loadPlugin() {
|
||||
return ({ matchUtilities }) => {
|
||||
matchUtilities(
|
||||
{
|
||||
scrollbar: (value, { modifier }) => {
|
||||
return {
|
||||
'--modifier': modifier ?? 'none',
|
||||
'scrollbar-width': value,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
type: ['any'],
|
||||
values: {},
|
||||
modifiers: {
|
||||
foo: 'foo',
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
).build(candidates)
|
||||
}
|
||||
|
||||
expect(
|
||||
optimizeCss(run(['scrollbar-[12px]', 'scrollbar-[12px]/foo', 'scrollbar-[12px]/bar'])).trim(),
|
||||
).toMatchInlineSnapshot(`
|
||||
".scrollbar-\\[12px\\] {
|
||||
--modifier: none;
|
||||
scrollbar-width: 12px;
|
||||
}
|
||||
|
||||
.scrollbar-\\[12px\\]\\/foo {
|
||||
--modifier: foo;
|
||||
scrollbar-width: 12px;
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('reading from the theme', () => {
|
||||
expect.hasAssertions()
|
||||
|
||||
compile(
|
||||
css`
|
||||
@plugin "my-plugin";
|
||||
@theme reference {
|
||||
--size-2_5: 2.5rem;
|
||||
|
||||
--scrollbar-big: 20px;
|
||||
--scrollbar-big-properties: auto-hidden;
|
||||
|
||||
--scrollbar-color-light: white;
|
||||
--scrollbar-color-dark: black;
|
||||
},
|
||||
`,
|
||||
{
|
||||
loadPlugin() {
|
||||
return ({ theme }) => {
|
||||
// Accessing w/ CSS property syntax
|
||||
expect(theme('--scrollbar')).toEqual(null)
|
||||
expect(theme('--scrollbar-*')).toEqual({
|
||||
big: '20px',
|
||||
'big-properties': 'auto-hidden',
|
||||
'color-dark': 'black',
|
||||
'color-light': 'white',
|
||||
})
|
||||
|
||||
expect(theme('--scrollbar-big')).toEqual('20px')
|
||||
|
||||
// Accessing via legacy dot notation
|
||||
expect(theme('size.2_5')).toEqual('2.5rem')
|
||||
expect(theme('scrollbar')).toEqual({
|
||||
big: '20px',
|
||||
'big-properties': 'auto-hidden',
|
||||
'color-dark': 'black',
|
||||
'color-light': 'white',
|
||||
})
|
||||
|
||||
expect(theme('scrollbar.big')).toEqual({
|
||||
DEFAULT: '20px',
|
||||
properties: 'auto-hidden',
|
||||
})
|
||||
expect(theme('scrollbar.big.properties')).toEqual('auto-hidden')
|
||||
|
||||
expect(theme('scrollbar.color')).toEqual({
|
||||
light: 'white',
|
||||
dark: 'black',
|
||||
})
|
||||
|
||||
expect(theme('scrollbar.foo', 'nope')).toEqual('nope')
|
||||
expect(theme('somekey', 'nope')).toEqual('nope')
|
||||
|
||||
// Square bracket syntax
|
||||
expect(theme('size[2.5]')).toEqual('2.5rem')
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ function property(ident: string, initialValue?: string, syntax?: string) {
|
|||
/**
|
||||
* Apply opacity to a color using `color-mix`.
|
||||
*/
|
||||
function withAlpha(value: string, alpha: string): string {
|
||||
export function withAlpha(value: string, alpha: string): string {
|
||||
if (alpha === null) return value
|
||||
|
||||
// Convert numeric values (like `0.5`) to percentages (like `50%`) so they
|
||||
|
|
@ -159,7 +159,7 @@ function asColor(value: string, modifier: CandidateModifier | null, theme: Theme
|
|||
/**
|
||||
* Negate a numeric value — literals get simplified by Lightning CSS.
|
||||
*/
|
||||
function withNegative(
|
||||
export function withNegative(
|
||||
value: string,
|
||||
candidate: Extract<Candidate, { kind: 'static' | 'functional' }>,
|
||||
) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue