Add support for prefixes (#14501)
This PR adds support for requiring a custom prefix on utility classes.
Prefixes work a bit differently in v4 than they did in v3:
- They look like a custom variant: `tw:bg-white`
- It is always first in a utility — even before other variants:
`tw:hover:bg-white`
- It is required on **all** utility classes — even arbitrary properties:
`tw:[color:red]`
- Prefixes also apply to generated CSS variables which will be separated
by a dash: `--tw-color-white: #fff;`
- Only alpha (a-z) characters are allowed in a prefix — so no `#tw#` or
`__` or similar prefixes are allowed
To configure a prefix you can use add `prefix(tw)` to your theme or when
importing Tailwind CSS like so:
```css
/* when importing `tailwindcss` */
@import 'tailwindcss' prefix(tw);
/* when importing the theme separately */
@import 'tailwindcss/theme' prefix(tw);
/* or when using an entirely custom theme */
@theme prefix(tw) {
--color-white: #fff;
--breakpoint-sm: 640px;
/* … */
}
```
This will configure Tailwind CSS to require a prefix on all utility
classes when used in HTML:
```html
<div class="tw:bg-white tw:p-4">
This will have a white background and 4 units of padding.
</div>
<div class="bg-white p-4">
This will not because the prefix is missing.
</div>
```
and when used in CSS via `@apply`:
```css
.my-class {
@apply tw:bg-white tw:p-4;
}
```
Additionally, the prefix will be added to the generated CSS variables.
You **do not** need to prefix the variables in the `@theme` block
yourself — Tailwind CSS handles this automatically.
```css
:root {
--tw-color-white: #fff;
--tw-breakpoint-sm: 640px;
}
```
A prefix is not necessary when using the `theme(…)` function in your CSS
or JS given that plugins will not know what the current prefix is and
must work with or without a prefix:
```css
.my-class {
color: theme(--color-white);
}
```
However, because the variables themselves are prefixed when outputting
the CSS, you **do** need to prefix the variables when using `var(…)` in
your CSS:
```css
.my-class {
color: var(--tw-color-white);
}
```
If you want to customize the prefix itself change `tw` to something
else:
```css
/* my:underline, my:hover:bg-red-500, etc… */
@import 'tailwindcss' prefix(my);
```
---------
Co-authored-by: Philipp Spiess <hello@philippspiess.com>
This commit is contained in:
parent
ceae1dbdfb
commit
a144360217
10 changed files with 597 additions and 6 deletions
|
|
@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Add support for prefixes ([#14501](https://github.com/tailwindlabs/tailwindcss/pull/14501))
|
||||
|
||||
### Fixed
|
||||
|
||||
- _Experimental_: Improve codemod output, keep CSS after last Tailwind directive unlayered ([#14512](https://github.com/tailwindlabs/tailwindcss/pull/14512))
|
||||
|
|
|
|||
|
|
@ -6,12 +6,17 @@ import { Variants } from './variants'
|
|||
|
||||
function run(
|
||||
candidate: string,
|
||||
{ utilities, variants }: { utilities?: Utilities; variants?: Variants } = {},
|
||||
{
|
||||
utilities,
|
||||
variants,
|
||||
prefix,
|
||||
}: { utilities?: Utilities; variants?: Variants; prefix?: string } = {},
|
||||
) {
|
||||
utilities ??= new Utilities()
|
||||
variants ??= new Variants()
|
||||
|
||||
let designSystem = buildDesignSystem(new Theme())
|
||||
designSystem.theme.prefix = prefix ?? null
|
||||
|
||||
designSystem.utilities = utilities
|
||||
designSystem.variants = variants
|
||||
|
|
@ -1259,3 +1264,46 @@ it('should parse a variant containing an arbitrary string with unbalanced parens
|
|||
]
|
||||
`)
|
||||
})
|
||||
|
||||
it('should parse candidates with a prefix', () => {
|
||||
let utilities = new Utilities()
|
||||
utilities.static('flex', () => [])
|
||||
|
||||
let variants = new Variants()
|
||||
variants.static('hover', () => {})
|
||||
|
||||
// A prefix is required
|
||||
expect(run(`flex`, { utilities, variants, prefix: 'tw' })).toEqual([])
|
||||
|
||||
// The prefix always comes first — even before variants
|
||||
expect(run(`tw:flex`, { utilities, variants, prefix: 'tw' })).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"important": false,
|
||||
"kind": "static",
|
||||
"negative": false,
|
||||
"raw": "tw:flex",
|
||||
"root": "flex",
|
||||
"variants": [],
|
||||
},
|
||||
]
|
||||
`)
|
||||
expect(run(`tw:hover:flex`, { utilities, variants, prefix: 'tw' })).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"important": false,
|
||||
"kind": "static",
|
||||
"negative": false,
|
||||
"raw": "tw:hover:flex",
|
||||
"root": "flex",
|
||||
"variants": [
|
||||
{
|
||||
"compounds": true,
|
||||
"kind": "static",
|
||||
"root": "hover",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
`)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -226,6 +226,15 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter
|
|||
// ^^^^^^^^^ -> Base
|
||||
let rawVariants = segment(input, ':')
|
||||
|
||||
// A prefix is a special variant used to prefix all utilities. When present,
|
||||
// all utilities must start with that variant which we will then remove from
|
||||
// the variant list so no other part of the codebase has to know about it.
|
||||
if (designSystem.theme.prefix) {
|
||||
if (rawVariants[0] !== designSystem.theme.prefix) return null
|
||||
|
||||
rawVariants.shift()
|
||||
}
|
||||
|
||||
// Safety: At this point it is safe to use TypeScript's non-null assertion
|
||||
// operator because even if the `input` was an empty string, splitting an
|
||||
// empty string by `:` will always result in an array with at least one
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import { buildPluginApi, type CssPluginOptions, type Plugin } from './plugin-api
|
|||
import { registerScreensConfig } from './screens-config'
|
||||
import { registerThemeVariantOverrides } from './theme-variants'
|
||||
|
||||
const IS_VALID_PREFIX = /^[a-z]+$/
|
||||
|
||||
export async function applyCompatibilityHooks({
|
||||
designSystem,
|
||||
base,
|
||||
|
|
@ -208,6 +210,25 @@ export async function applyCompatibilityHooks({
|
|||
registerThemeVariantOverrides(resolvedUserConfig, designSystem)
|
||||
registerScreensConfig(resolvedUserConfig, designSystem)
|
||||
|
||||
// If a prefix has already been set in CSS don't override it
|
||||
if (!designSystem.theme.prefix && resolvedConfig.prefix) {
|
||||
if (resolvedConfig.prefix.endsWith('-')) {
|
||||
resolvedConfig.prefix = resolvedConfig.prefix.slice(0, -1)
|
||||
|
||||
console.warn(
|
||||
`The prefix "${resolvedConfig.prefix}" is invalid. Prefixes must be lowercase ASCII letters (a-z) only and is written as a variant before all utilities. We have fixed up the prefix for you. Remove the trailing \`-\` to silence this warning.`,
|
||||
)
|
||||
}
|
||||
|
||||
if (!IS_VALID_PREFIX.test(resolvedConfig.prefix)) {
|
||||
throw new Error(
|
||||
`The prefix "${resolvedConfig.prefix}" is invalid. Prefixes must be lowercase ASCII letters (a-z) only.`,
|
||||
)
|
||||
}
|
||||
|
||||
designSystem.theme.prefix = resolvedConfig.prefix
|
||||
}
|
||||
|
||||
// Replace `resolveThemeValue` with a version that is backwards compatible
|
||||
// with dot-notation but also aware of any JS theme configurations registered
|
||||
// by plugins or JS config files. This is significantly slower than just
|
||||
|
|
|
|||
|
|
@ -1217,3 +1217,157 @@ test('merges css breakpoints with js config screens', async () => {
|
|||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('utilities must be prefixed', async () => {
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
@config "./config.js";
|
||||
|
||||
@utility custom {
|
||||
color: red;
|
||||
}
|
||||
`
|
||||
|
||||
let compiler = await compile(input, {
|
||||
loadModule: async (id, base) => ({
|
||||
base,
|
||||
module: { prefix: 'tw' },
|
||||
}),
|
||||
})
|
||||
|
||||
// Prefixed utilities are generated
|
||||
expect(compiler.build(['tw:underline', 'tw:hover:line-through', 'tw:custom']))
|
||||
.toMatchInlineSnapshot(`
|
||||
".tw\\:custom {
|
||||
color: red;
|
||||
}
|
||||
.tw\\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.tw\\:hover\\:line-through {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
text-decoration-line: line-through;
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
|
||||
// Non-prefixed utilities are ignored
|
||||
compiler = await compile(input, {
|
||||
loadModule: async (id, base) => ({
|
||||
base,
|
||||
module: { prefix: 'tw' },
|
||||
}),
|
||||
})
|
||||
|
||||
expect(compiler.build(['underline', 'hover:line-through', 'custom'])).toEqual('')
|
||||
})
|
||||
|
||||
test('utilities used in @apply must be prefixed', async () => {
|
||||
let compiler = await compile(
|
||||
css`
|
||||
@config "./config.js";
|
||||
|
||||
.my-underline {
|
||||
@apply tw:underline;
|
||||
}
|
||||
`,
|
||||
{
|
||||
loadModule: async (id, base) => ({
|
||||
base,
|
||||
module: { prefix: 'tw' },
|
||||
}),
|
||||
},
|
||||
)
|
||||
|
||||
// Prefixed utilities are generated
|
||||
expect(compiler.build([])).toMatchInlineSnapshot(`
|
||||
".my-underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
"
|
||||
`)
|
||||
|
||||
// Non-prefixed utilities cause an error
|
||||
expect(() =>
|
||||
compile(
|
||||
css`
|
||||
@config "./config.js";
|
||||
|
||||
.my-underline {
|
||||
@apply underline;
|
||||
}
|
||||
`,
|
||||
{
|
||||
loadModule: async (id, base) => ({
|
||||
base,
|
||||
module: { prefix: 'tw' },
|
||||
}),
|
||||
},
|
||||
),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`[Error: Cannot apply unknown utility class: underline]`,
|
||||
)
|
||||
})
|
||||
|
||||
test('Prefixes configured in CSS take precedence over those defined in JS configs', async () => {
|
||||
let compiler = await compile(
|
||||
css`
|
||||
@theme prefix(wat) {
|
||||
--color-red: #f00;
|
||||
--color-green: #0f0;
|
||||
--breakpoint-sm: 640px;
|
||||
}
|
||||
|
||||
@config "./plugin.js";
|
||||
|
||||
@tailwind utilities;
|
||||
|
||||
@utility custom {
|
||||
color: red;
|
||||
}
|
||||
`,
|
||||
{
|
||||
async loadModule(id, base) {
|
||||
return {
|
||||
base,
|
||||
module: { prefix: 'tw' },
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
expect(compiler.build(['wat:custom'])).toMatchInlineSnapshot(`
|
||||
":root {
|
||||
--wat-color-red: #f00;
|
||||
--wat-color-green: #0f0;
|
||||
--wat-breakpoint-sm: 640px;
|
||||
}
|
||||
.wat\\:custom {
|
||||
color: red;
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('a prefix must be letters only', async () => {
|
||||
await expect(() =>
|
||||
compile(
|
||||
css`
|
||||
@config "./plugin.js";
|
||||
`,
|
||||
{
|
||||
async loadModule(id, base) {
|
||||
return {
|
||||
base,
|
||||
module: { prefix: '__' },
|
||||
}
|
||||
},
|
||||
},
|
||||
),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`[Error: The prefix "__" is invalid. Prefixes must be lowercase ASCII letters (a-z) only.]`,
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ interface ResolutionContext {
|
|||
}
|
||||
|
||||
let minimal: ResolvedConfig = {
|
||||
prefix: '',
|
||||
darkMode: null,
|
||||
theme: {},
|
||||
plugins: [],
|
||||
|
|
@ -54,11 +55,15 @@ export function resolveConfig(design: DesignSystem, files: ConfigFile[]): Resolv
|
|||
extractConfigs(ctx, file)
|
||||
}
|
||||
|
||||
// Merge dark mode
|
||||
// Merge top level keys
|
||||
for (let config of ctx.configs) {
|
||||
if ('darkMode' in config && config.darkMode !== undefined) {
|
||||
ctx.result.darkMode = config.darkMode ?? null
|
||||
}
|
||||
|
||||
if ('prefix' in config && config.prefix !== undefined) {
|
||||
ctx.result.prefix = config.prefix ?? ''
|
||||
}
|
||||
}
|
||||
|
||||
// Merge themes
|
||||
|
|
|
|||
|
|
@ -69,3 +69,12 @@ export interface UserConfig {
|
|||
export interface ResolvedConfig {
|
||||
darkMode: DarkModeStrategy | null
|
||||
}
|
||||
|
||||
// `prefix` support
|
||||
export interface UserConfig {
|
||||
prefix?: string
|
||||
}
|
||||
|
||||
export interface ResolvedConfig {
|
||||
prefix: string
|
||||
}
|
||||
|
|
|
|||
297
packages/tailwindcss/src/compat/prefix.test.ts
Normal file
297
packages/tailwindcss/src/compat/prefix.test.ts
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
import { expect, test } from 'vitest'
|
||||
import { compile } from '..'
|
||||
import plugin from '../plugin'
|
||||
|
||||
const css = String.raw
|
||||
|
||||
test('utilities must be prefixed', async () => {
|
||||
let input = css`
|
||||
@theme reference prefix(tw);
|
||||
@tailwind utilities;
|
||||
|
||||
@utility custom {
|
||||
color: red;
|
||||
}
|
||||
`
|
||||
|
||||
let compiler = await compile(input)
|
||||
|
||||
// Prefixed utilities are generated
|
||||
expect(compiler.build(['tw:underline', 'tw:hover:line-through', 'tw:custom']))
|
||||
.toMatchInlineSnapshot(`
|
||||
".tw\\:custom {
|
||||
color: red;
|
||||
}
|
||||
.tw\\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.tw\\:hover\\:line-through {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
text-decoration-line: line-through;
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
|
||||
// Non-prefixed utilities are ignored
|
||||
compiler = await compile(input)
|
||||
|
||||
expect(compiler.build(['underline', 'hover:line-through', 'custom'])).toEqual('')
|
||||
})
|
||||
|
||||
test('utilities used in @apply must be prefixed', async () => {
|
||||
let compiler = await compile(css`
|
||||
@theme reference prefix(tw);
|
||||
|
||||
.my-underline {
|
||||
@apply tw:underline;
|
||||
}
|
||||
`)
|
||||
|
||||
// Prefixed utilities are generated
|
||||
expect(compiler.build([])).toMatchInlineSnapshot(`
|
||||
".my-underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
"
|
||||
`)
|
||||
|
||||
// Non-prefixed utilities cause an error
|
||||
expect(() =>
|
||||
compile(css`
|
||||
@theme reference prefix(tw);
|
||||
|
||||
.my-underline {
|
||||
@apply underline;
|
||||
}
|
||||
`),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`[Error: Cannot apply unknown utility class: underline]`,
|
||||
)
|
||||
})
|
||||
|
||||
test('CSS variables output by the theme are prefixed', async () => {
|
||||
let compiler = await compile(css`
|
||||
@theme prefix(tw) {
|
||||
--color-red: #f00;
|
||||
--color-green: #0f0;
|
||||
--breakpoint-sm: 640px;
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
`)
|
||||
|
||||
// Prefixed utilities are generated
|
||||
expect(compiler.build(['tw:text-red'])).toMatchInlineSnapshot(`
|
||||
":root {
|
||||
--tw-color-red: #f00;
|
||||
--tw-color-green: #0f0;
|
||||
--tw-breakpoint-sm: 640px;
|
||||
}
|
||||
.tw\\:text-red {
|
||||
color: var(--tw-color-red, #f00);
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('CSS theme functions do not use the prefix', async () => {
|
||||
let compiler = await compile(css`
|
||||
@theme prefix(tw) {
|
||||
--color-red: #f00;
|
||||
--color-green: #0f0;
|
||||
--breakpoint-sm: 640px;
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
`)
|
||||
|
||||
expect(compiler.build(['tw:[color:theme(--color-red)]', 'tw:text-[theme(--color-red)]']))
|
||||
.toMatchInlineSnapshot(`
|
||||
":root {
|
||||
--tw-color-red: #f00;
|
||||
--tw-color-green: #0f0;
|
||||
--tw-breakpoint-sm: 640px;
|
||||
}
|
||||
.tw\\:\\[color\\:theme\\(--color-red\\)\\] {
|
||||
color: #f00;
|
||||
}
|
||||
.tw\\:text-\\[theme\\(--color-red\\)\\] {
|
||||
color: #f00;
|
||||
}
|
||||
"
|
||||
`)
|
||||
|
||||
compiler = await compile(css`
|
||||
@theme reference prefix(tw) {
|
||||
--color-red: #f00;
|
||||
--color-green: #0f0;
|
||||
--breakpoint-sm: 640px;
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
`)
|
||||
|
||||
expect(
|
||||
compiler.build(['tw:[color:theme(--tw-color-red)]', 'tw:text-[theme(--tw-color-red)]']),
|
||||
).toEqual('')
|
||||
})
|
||||
|
||||
test('JS theme functions do not use the prefix', async () => {
|
||||
let compiler = await compile(
|
||||
css`
|
||||
@theme prefix(tw) {
|
||||
--color-red: #f00;
|
||||
--color-green: #0f0;
|
||||
--breakpoint-sm: 640px;
|
||||
}
|
||||
|
||||
@plugin "./plugin.js";
|
||||
|
||||
@tailwind utilities;
|
||||
`,
|
||||
{
|
||||
async loadModule(id, base) {
|
||||
return {
|
||||
base,
|
||||
module: plugin(({ addUtilities, theme }) => {
|
||||
addUtilities({
|
||||
'.my-custom': {
|
||||
color: theme('--color-red'),
|
||||
},
|
||||
})
|
||||
|
||||
// The theme function does not use the prefix
|
||||
expect(theme('--tw-color-red')).toEqual(undefined)
|
||||
}),
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
expect(compiler.build(['tw:my-custom'])).toMatchInlineSnapshot(`
|
||||
":root {
|
||||
--tw-color-red: #f00;
|
||||
--tw-color-green: #0f0;
|
||||
--tw-breakpoint-sm: 640px;
|
||||
}
|
||||
.tw\\:my-custom {
|
||||
color: #f00;
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('a prefix can be configured via @import theme(…)', async () => {
|
||||
let input = css`
|
||||
@import 'tailwindcss/theme' theme(reference prefix(tw));
|
||||
@tailwind utilities;
|
||||
|
||||
@utility custom {
|
||||
color: red;
|
||||
}
|
||||
`
|
||||
|
||||
let compiler = await compile(input, {
|
||||
async loadStylesheet(id, base) {
|
||||
return {
|
||||
base,
|
||||
content: '@theme {}',
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Prefixed utilities are generated
|
||||
expect(compiler.build(['tw:underline', 'tw:hover:line-through', 'tw:custom']))
|
||||
.toMatchInlineSnapshot(`
|
||||
".tw\\:custom {
|
||||
color: red;
|
||||
}
|
||||
.tw\\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.tw\\:hover\\:line-through {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
text-decoration-line: line-through;
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
|
||||
// Non-prefixed utilities are ignored
|
||||
compiler = await compile(input, {
|
||||
async loadStylesheet(id, base) {
|
||||
return {
|
||||
base,
|
||||
content: '@theme {}',
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
expect(compiler.build(['underline', 'hover:line-through', 'custom'])).toEqual('')
|
||||
})
|
||||
|
||||
test('a prefix can be configured via @import prefix(…)', async () => {
|
||||
let input = css`
|
||||
@import 'tailwindcss/theme' prefix(tw);
|
||||
@tailwind utilities;
|
||||
|
||||
@utility custom {
|
||||
color: red;
|
||||
}
|
||||
`
|
||||
|
||||
let compiler = await compile(input, {
|
||||
async loadStylesheet(id, base) {
|
||||
return {
|
||||
base,
|
||||
content: '@theme reference {}',
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Prefixed utilities are generated
|
||||
expect(compiler.build(['tw:underline', 'tw:hover:line-through', 'tw:custom']))
|
||||
.toMatchInlineSnapshot(`
|
||||
".tw\\:custom {
|
||||
color: red;
|
||||
}
|
||||
.tw\\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.tw\\:hover\\:line-through {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
text-decoration-line: line-through;
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
|
||||
// Non-prefixed utilities are ignored
|
||||
compiler = await compile(input, {
|
||||
async loadStylesheet(id, base) {
|
||||
return {
|
||||
base,
|
||||
content: '@theme reference {}',
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
expect(compiler.build(['underline', 'hover:line-through', 'custom'])).toEqual('')
|
||||
})
|
||||
|
||||
test('a prefix must be letters only', async () => {
|
||||
let input = css`
|
||||
@theme reference prefix(__);
|
||||
`
|
||||
|
||||
await expect(() => compile(input)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`[Error: The prefix "__" is invalid. Prefixes must be lowercase ASCII letters (a-z) only.]`,
|
||||
)
|
||||
})
|
||||
|
|
@ -23,6 +23,7 @@ import { Theme, ThemeOptions } from './theme'
|
|||
import { segment } from './utils/segment'
|
||||
export type Config = UserConfig
|
||||
|
||||
const IS_VALID_PREFIX = /^[a-z]+$/
|
||||
const IS_VALID_UTILITY_NAME = /^[a-z][a-zA-Z0-9/%._-]*$/
|
||||
|
||||
type CompileOptions = {
|
||||
|
|
@ -45,6 +46,7 @@ function throwOnLoadStylesheet(): never {
|
|||
|
||||
function parseThemeOptions(selector: string) {
|
||||
let options = ThemeOptions.NONE
|
||||
let prefix = null
|
||||
|
||||
for (let option of segment(selector.slice(6) /* '@theme'.length */, ' ')) {
|
||||
if (option === 'reference') {
|
||||
|
|
@ -53,10 +55,12 @@ function parseThemeOptions(selector: string) {
|
|||
options |= ThemeOptions.INLINE
|
||||
} else if (option === 'default') {
|
||||
options |= ThemeOptions.DEFAULT
|
||||
} else if (option.startsWith('prefix(') && option.endsWith(')')) {
|
||||
prefix = option.slice(7, -1)
|
||||
}
|
||||
}
|
||||
|
||||
return options
|
||||
return [options, prefix] as const
|
||||
}
|
||||
|
||||
async function parseCss(
|
||||
|
|
@ -210,9 +214,37 @@ async function parseCss(
|
|||
return WalkAction.Skip
|
||||
}
|
||||
|
||||
// Drop instances of `@media prefix(…)`
|
||||
//
|
||||
// We support `@import "tailwindcss" prefix(ident)` as a way to
|
||||
// configure a theme prefix for variables and utilities.
|
||||
if (node.selector.startsWith('@media prefix(')) {
|
||||
let themeParams = node.selector.slice(7)
|
||||
|
||||
walk(node.nodes, (child) => {
|
||||
if (child.kind !== 'rule') return
|
||||
if (child.selector === '@theme' || child.selector.startsWith('@theme ')) {
|
||||
child.selector += ' ' + themeParams
|
||||
return WalkAction.Skip
|
||||
}
|
||||
})
|
||||
replaceWith(node.nodes)
|
||||
return WalkAction.Skip
|
||||
}
|
||||
|
||||
if (node.selector !== '@theme' && !node.selector.startsWith('@theme ')) return
|
||||
|
||||
let themeOptions = parseThemeOptions(node.selector)
|
||||
let [themeOptions, themePrefix] = parseThemeOptions(node.selector)
|
||||
|
||||
if (themePrefix) {
|
||||
if (!IS_VALID_PREFIX.test(themePrefix)) {
|
||||
throw new Error(
|
||||
`The prefix "${themePrefix}" is invalid. Prefixes must be lowercase ASCII letters (a-z) only.`,
|
||||
)
|
||||
}
|
||||
|
||||
theme.prefix = themePrefix
|
||||
}
|
||||
|
||||
// Record all custom properties in the `@theme` declaration
|
||||
walk(node.nodes, (child, { replaceWith }) => {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ export const enum ThemeOptions {
|
|||
}
|
||||
|
||||
export class Theme {
|
||||
public prefix: string | null = null
|
||||
|
||||
constructor(private values = new Map<string, { value: string; options: number }>()) {}
|
||||
|
||||
add(key: string, value: string, options = ThemeOptions.NONE): void {
|
||||
|
|
@ -74,7 +76,17 @@ export class Theme {
|
|||
}
|
||||
|
||||
entries() {
|
||||
return this.values.entries()
|
||||
if (!this.prefix) return this.values.entries()
|
||||
|
||||
return Array.from(this.values, (entry) => {
|
||||
entry[0] = this.#prefixKey(entry[0])
|
||||
return entry
|
||||
})
|
||||
}
|
||||
|
||||
#prefixKey(key: string) {
|
||||
if (!this.prefix) return key
|
||||
return `--${this.prefix}-${key.slice(2)}`
|
||||
}
|
||||
|
||||
#clearNamespace(namespace: string) {
|
||||
|
|
@ -103,7 +115,7 @@ export class Theme {
|
|||
return null
|
||||
}
|
||||
|
||||
return `var(${themeKey}, ${this.values.get(themeKey)?.value})`
|
||||
return `var(${this.#prefixKey(themeKey)}, ${this.values.get(themeKey)?.value})`
|
||||
}
|
||||
|
||||
resolve(candidateValue: string | null, themeKeys: ThemeKey[]): string | null {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue