Skip to content

Commit b4941aa

Browse files
authored
fix: Tailwind border color arbitrary var (#5717)
1 parent 8f73885 commit b4941aa

6 files changed

Lines changed: 3428 additions & 2739 deletions

File tree

apps/builder/app/shared/tailwind/tailwind.test.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,21 @@ test("keep typed literal border color utility", async () => {
215215
);
216216
});
217217

218+
test("keep border side typed arbitrary var color utility", async () => {
219+
const fragment = await generateFragmentFromTailwind(
220+
renderTemplate(
221+
<ws.element
222+
ws:tag="div"
223+
class="p-8 border-t border-[color:var(--border-color)]"
224+
></ws.element>
225+
)
226+
);
227+
228+
expect(getBaseStyleValue(fragment, "borderTopColor")).toEqual(
229+
expect.objectContaining({ type: "var", value: "border-color" })
230+
);
231+
});
232+
218233
test("keep inline border shorthand var color override", async () => {
219234
const fragment = await generateFragmentFromTailwind(
220235
renderTemplate(
@@ -985,3 +1000,17 @@ test("generate space with display property", async () => {
9851000
)
9861001
).toBe(true);
9871002
});
1003+
1004+
test("keep typed arbitrary var border color utility", async () => {
1005+
const fragment = await generateFragmentFromTailwind(
1006+
renderTemplate(
1007+
<ws.element
1008+
ws:tag="div"
1009+
class="p-8 border border-[color:var(--border-color)]"
1010+
></ws.element>
1011+
)
1012+
);
1013+
expect(getBaseStyleValue(fragment, "borderTopColor")).toEqual(
1014+
expect.objectContaining({ type: "var", value: "border-color" })
1015+
);
1016+
});

apps/builder/app/shared/tailwind/tailwind.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,10 +377,19 @@ const parseTailwindClasses = async (
377377
// Tailwind v4 css variable shorthand: border-(--x)
378378
// UnoCSS doesn't parse this alias directly, so normalize it to
379379
// an explicit arbitrary property utility it understands.
380+
// TODO: remove workaround once fixed https://github.com/unocss/unocss/issues/5188
380381
if (/^border-\(--[\w-]+\)$/.test(item)) {
381382
const varName = item.slice("border-(".length, -1);
382383
return `[border-color:var(${varName})]`;
383384
}
385+
// Tailwind v4 typed arbitrary border color: border-[color:value]
386+
// UnoCSS wind4 incorrectly maps this to border-width. Rewrite to the
387+
// explicit arbitrary property form so it resolves to border-color.
388+
// TODO: remove workaround once fixed https://github.com/unocss/unocss/issues/5188
389+
const borderColorMatch = item.match(/^border-\[color:(.+)\]$/);
390+
if (borderColorMatch) {
391+
return `[border-color:${borderColorMatch[1]}]`;
392+
}
384393
// styles data cannot express space-x and space-y selectors
385394
// with lobotomized owl so replace with gaps
386395
if (item.includes("space-x-")) {

packages/css-data/bin/property-var-test-fixtures.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,28 @@ const buildPatterns = ({
639639
return current;
640640
};
641641

642+
/**
643+
* Derive a semantic CSS variable name from a slot's grammar position label.
644+
* The label looks like "border:term:2:type:color:branch:0:type:hex-color".
645+
* We extract the first `type:XXX` segment to build `--slot-XXX`.
646+
* When multiple slots in the same pattern share the same type we append
647+
* a counter suffix: `--slot-color`, `--slot-color-2`, etc.
648+
* Falling back to `--slot-N` when no type can be extracted.
649+
*/
650+
const slotVarName = (
651+
label: string | undefined,
652+
fallbackIndex: number,
653+
used: Map<string, number>
654+
): string => {
655+
const typeMatch = label?.match(/(?:^|:)type:([^:]+)/);
656+
const base = typeMatch
657+
? `--slot-${typeMatch[1]}`
658+
: `--slot-${fallbackIndex + 1}`;
659+
const count = (used.get(base) ?? 0) + 1;
660+
used.set(base, count);
661+
return count === 1 ? base : `${base}-${count}`;
662+
};
663+
642664
const buildCases = ({
643665
patterns,
644666
property,
@@ -658,7 +680,12 @@ const buildCases = ({
658680
}
659681

660682
for (const [slotOrder, slot] of slots.entries()) {
661-
const variableName = `--slot-${slotOrder + 1}`;
683+
const used = new Map<string, number>();
684+
// pre-register names for earlier slots so this slot gets the right suffix
685+
for (let i = 0; i < slotOrder; i++) {
686+
slotVarName(slots[i].part.label, i, used);
687+
}
688+
const variableName = slotVarName(slot.part.label, slotOrder, used);
662689
const parts = pattern.parts.map((part, index) => {
663690
if (index !== slot.index || part.kind !== "slot") {
664691
return part;
@@ -685,12 +712,13 @@ const buildCases = ({
685712
}
686713

687714
const variables: Record<string, string> = {};
715+
const allUsed = new Map<string, number>();
688716
const parts = pattern.parts.map((part, index) => {
689717
const slotOrder = slots.findIndex((slot) => slot.index === index);
690718
if (slotOrder === -1 || part.kind !== "slot") {
691719
return part;
692720
}
693-
const variableName = `--slot-${slotOrder + 1}`;
721+
const variableName = slotVarName(part.label, slotOrder, allUsed);
694722
variables[variableName] = part.text;
695723
return {
696724
kind: "text" as const,

0 commit comments

Comments
 (0)