deven broke tailwind

This commit is contained in:
2026-07-15 15:30:08 -04:00
parent 342de9e18a
commit bf42426c0b

View File

@@ -1304,6 +1304,7 @@ func twCompile(input, baseDir string, candidates []string) (string, int, error)
var keyframes []*AstNode var keyframes []*AstNode
var passthrough []*AstNode var passthrough []*AstNode
var customUtilities []*AstNode var customUtilities []*AstNode
var customVariants []*AstNode
var safelistAdd []string var safelistAdd []string
var safelistRemove []string var safelistRemove []string
hasPreflight := false hasPreflight := false
@@ -1361,6 +1362,8 @@ func twCompile(input, baseDir string, candidates []string) (string, int, error)
processTheme(node) processTheme(node)
case node.Kind == nAtRule && node.Name == "@utility": case node.Kind == nAtRule && node.Name == "@utility":
customUtilities = append(customUtilities, node) customUtilities = append(customUtilities, node)
case node.Kind == nAtRule && node.Name == "@custom-variant":
customVariants = append(customVariants, node)
case node.Kind == nAtRule && node.Name == "@source": case node.Kind == nAtRule && node.Name == "@source":
literals, negate := parseSourceDirective(node.Params) literals, negate := parseSourceDirective(node.Params)
if negate { if negate {
@@ -1399,6 +1402,20 @@ func twCompile(input, baseDir string, candidates []string) (string, int, error)
ds := buildDesignSystem(theme) ds := buildDesignSystem(theme)
// Register @custom-variant blocks. This is how a project defines `dark:` as a CLASS
// toggle rather than a media query — the built-in dark variant follows the OS, which
// a site with a theme switch cannot use:
//
// @custom-variant dark (&:where(.dark, .dark *));
//
// Both of Tailwind's forms are accepted: the shorthand above, and the block form
// with an explicit @slot.
for _, cv := range customVariants {
if name, body, ok := parseCustomVariant(cv); ok {
ds.variants.fromAst(name, body, ds)
}
}
// Register @utility blocks as static utilities. // Register @utility blocks as static utilities.
for _, u := range customUtilities { for _, u := range customUtilities {
name := strings.TrimSpace(u.Params) name := strings.TrimSpace(u.Params)
@@ -8813,6 +8830,95 @@ func (v *Variants) kind(name string) variantKind {
func (v *Variants) keys() []string { return v.order } func (v *Variants) keys() []string { return v.order }
// parseCustomVariant reads an @custom-variant at-rule into a name and the AST body that
// fromAst expects (a body whose rules contain an @slot where the utility goes).
//
// Two forms, both from Tailwind:
//
// @custom-variant dark (&:where(.dark, .dark *)); // shorthand
//
// @custom-variant dark { // block, explicit slot
// &:where(.dark, .dark *) { @slot; }
// }
//
// In the shorthand, a parenthesised selector starting with '@' is an at-rule
// (`@custom-variant any-hover (@media (any-hover: hover))`), and anything else is a
// selector. Several may be given, comma-separated at the top level.
func parseCustomVariant(node *AstNode) (name string, body []*AstNode, ok bool) {
params := strings.TrimSpace(node.Params)
if params == "" {
return "", nil, false
}
// The name is the first token; whatever follows is the shorthand's parenthesised part.
i := strings.IndexAny(params, " \t(")
if i < 0 {
// No shorthand: it must be the block form, which carries its own @slot.
if len(node.Nodes) == 0 {
return "", nil, false
}
return params, node.Nodes, true
}
name = strings.TrimSpace(params[:i])
rest := strings.TrimSpace(params[i:])
if rest == "" {
if len(node.Nodes) == 0 {
return "", nil, false
}
return name, node.Nodes, true
}
if !strings.HasPrefix(rest, "(") || !strings.HasSuffix(rest, ")") {
return "", nil, false
}
inner := strings.TrimSpace(rest[1 : len(rest)-1])
if inner == "" {
return "", nil, false
}
for _, sel := range splitTopLevel(inner, ',') {
sel = strings.TrimSpace(sel)
if sel == "" {
continue
}
slot := atRule("@slot", "")
if strings.HasPrefix(sel, "@") {
// "@media (any-hover: hover)" -> name "@media", params "(any-hover: hover)"
at, params, _ := strings.Cut(sel, " ")
body = append(body, atRule(at, strings.TrimSpace(params), slot))
continue
}
body = append(body, styleRule(sel, slot))
}
if len(body) == 0 {
return "", nil, false
}
return name, body, true
}
// splitTopLevel splits on sep, ignoring separators nested inside brackets — a selector
// list like `&:where(.dark, .dark *)` is ONE selector, and splitting it on its inner
// comma would produce two broken halves.
func splitTopLevel(s string, sep byte) []string {
var parts []string
depth := 0
start := 0
for i := 0; i < len(s); i++ {
switch s[i] {
case '(', '[':
depth++
case ')', ']':
depth--
case sep:
if depth == 0 {
parts = append(parts, s[start:i])
start = i + 1
}
}
}
return append(parts, s[start:])
}
func (v *Variants) compoundsWith(parent string, child *Variant) bool { func (v *Variants) compoundsWith(parent string, child *Variant) bool {
parentInfo, ok := v.variants[parent] parentInfo, ok := v.variants[parent]
if !ok { if !ok {