diff --git a/apps/builder/app/shared/tailwind/tailwind.test.tsx b/apps/builder/app/shared/tailwind/tailwind.test.tsx
index 5d2b0c7adb3b..f552414de1b7 100644
--- a/apps/builder/app/shared/tailwind/tailwind.test.tsx
+++ b/apps/builder/app/shared/tailwind/tailwind.test.tsx
@@ -114,8 +114,8 @@ test("override border opacity", async () => {
ws:tag="div"
ws:style={css`
border-style: solid;
- border-width: 1px;
border-color: rgb(229 231 235 / var(--tw-border-opacity));
+ border-width: 1px;
--tw-border-opacity: 0.6;
`}
>
@@ -315,31 +315,6 @@ describe("extract breakpoints", () => {
);
});
- test("base is first breakpoint", async () => {
- expect(
- await generateFragmentFromTailwind(
- renderTemplate(
-
- )
- )
- ).toEqual(
- renderTemplate(
-
- )
- );
- });
-
test("base is last breakpoint", async () => {
expect(
await generateFragmentFromTailwind(
@@ -393,27 +368,6 @@ describe("extract breakpoints", () => {
);
});
- test("preserve breakpoint when no base breakpoint", async () => {
- expect(
- await generateFragmentFromTailwind(
- renderTemplate(
-
- )
- )
- ).toEqual(
- renderTemplate(
-
- )
- );
- });
-
test("extract container class", async () => {
expect(
await generateFragmentFromTailwind(
@@ -427,7 +381,6 @@ describe("extract breakpoints", () => {
@media (max-width: 479px) {
max-width: none;
}
- width: 100%;
@media (max-width: 767px) {
max-width: 640px;
}
@@ -441,6 +394,7 @@ describe("extract breakpoints", () => {
@media (min-width: 1440px) {
max-width: 1536px;
}
+ width: 100%;
`}
>
)
@@ -501,11 +455,14 @@ describe("extract breakpoints", () => {
ws:tag="div"
ws:style={css`
opacity: 0.1;
- @media (min-width: 480px) {
+ @media (max-width: 479px) {
&:hover {
- opacity: 0.2;
+ opacity: unset;
}
}
+ &:hover {
+ opacity: 0.2;
+ }
`}
>
)
@@ -527,21 +484,21 @@ describe("extract breakpoints", () => {
@@ -570,6 +527,56 @@ describe("extract breakpoints", () => {
)
);
});
+
+ test("use unset for missing base breakpoint 1", async () => {
+ expect(
+ await generateFragmentFromTailwind(
+ renderTemplate(
+
+ )
+ )
+ ).toEqual(
+ renderTemplate(
+
+ )
+ );
+ });
+
+ test("use unset for missing base breakpoint 2", async () => {
+ expect(
+ await generateFragmentFromTailwind(
+ renderTemplate(
+
+ )
+ )
+ ).toEqual(
+ renderTemplate(
+
+ )
+ );
+ });
});
test("generate space without display property", async () => {
@@ -635,8 +642,8 @@ test("generate space with display property", async () => {
@media (max-width: 767px) {
display: none;
}
- row-gap: 1rem;
display: flex;
+ row-gap: 1rem;
`}
>
>
diff --git a/apps/builder/app/shared/tailwind/tailwind.ts b/apps/builder/app/shared/tailwind/tailwind.ts
index 834446472262..ded7d3ff3554 100644
--- a/apps/builder/app/shared/tailwind/tailwind.ts
+++ b/apps/builder/app/shared/tailwind/tailwind.ts
@@ -37,23 +37,25 @@ const tailwindToWebstudioMappings: Record = {
1536: 1440,
};
+type StyleDecl = Omit;
+
type Breakpoint = {
- key: string;
+ styleDecl: StyleDecl;
minWidth?: number;
maxWidth?: number;
};
type Range = {
- key: string;
+ styleDecl: StyleDecl;
start: number;
end: number;
};
const serializeBreakpoint = (breakpoint: Breakpoint) => {
- if (breakpoint?.minWidth) {
+ if (breakpoint?.minWidth !== undefined) {
return `(min-width: ${breakpoint.minWidth}px)`;
}
- if (breakpoint?.maxWidth) {
+ if (breakpoint?.maxWidth !== undefined) {
return `(max-width: ${breakpoint.maxWidth}px)`;
}
};
@@ -63,17 +65,17 @@ const UPPER_BOUND = Number.MAX_SAFE_INTEGER;
const breakpointsToRanges = (breakpoints: Breakpoint[]) => {
// collect lower bounds and ids
const values = new Set([0]);
- const keys = new Map();
+ const styles = new Map();
for (const breakpoint of breakpoints) {
if (breakpoint.minWidth !== undefined) {
values.add(breakpoint.minWidth);
- keys.set(breakpoint.minWidth, breakpoint.key);
+ styles.set(breakpoint.minWidth, breakpoint.styleDecl);
} else if (breakpoint.maxWidth !== undefined) {
values.add(breakpoint.maxWidth + 1);
- keys.set(breakpoint.maxWidth, breakpoint.key);
+ styles.set(breakpoint.maxWidth, breakpoint.styleDecl);
} else {
// base breakpoint
- keys.set(undefined, breakpoint.key);
+ styles.set(undefined, breakpoint.styleDecl);
}
}
const sortedValues = Array.from(values).sort((left, right) => left - right);
@@ -86,9 +88,24 @@ const breakpointsToRanges = (breakpoints: Breakpoint[]) => {
} else {
end = sortedValues[index + 1] - 1;
}
- const key = keys.get(start) ?? keys.get(end) ?? keys.get(undefined);
- if (key) {
- ranges.push({ key, start, end });
+ const styleDecl =
+ styles.get(start) ?? styles.get(end) ?? styles.get(undefined);
+ if (styleDecl) {
+ ranges.push({ styleDecl, start, end });
+ continue;
+ }
+ // when declaration is missing add new one with unset value
+ // to fill the hole in breakpoints
+ // for example
+ // "sm:opacity-20" has a hole at the start
+ // "max-sm:opacity-10 md:opacity-20" has a whole in the middle
+ const example = Array.from(styles.values())[0];
+ if (example) {
+ const newStyleDecl: StyleDecl = {
+ ...example,
+ value: { type: "keyword", value: "unset" },
+ };
+ ranges.push({ styleDecl: newStyleDecl, start, end });
}
}
return ranges;
@@ -96,36 +113,33 @@ const breakpointsToRanges = (breakpoints: Breakpoint[]) => {
const rangesToBreakpoints = (ranges: Range[]) => {
const breakpoints: Breakpoint[] = [];
- for (const range of ranges) {
+ for (const { styleDecl, start, end } of ranges) {
let matchedBreakpoint;
for (const breakpoint of availableBreakpoints) {
- if (breakpoint.minWidth === range.start) {
- matchedBreakpoint = { key: range.key, minWidth: range.start };
+ if (breakpoint.minWidth === start) {
+ matchedBreakpoint = { styleDecl, minWidth: start };
}
- if (breakpoint.maxWidth === range.end) {
- matchedBreakpoint = { key: range.key, maxWidth: range.end };
+ if (breakpoint.maxWidth === end) {
+ matchedBreakpoint = { styleDecl, maxWidth: end };
}
if (
breakpoint.minWidth === undefined &&
breakpoint.maxWidth === undefined
) {
- matchedBreakpoint ??= { key: range.key };
+ matchedBreakpoint ??= { styleDecl };
}
}
if (matchedBreakpoint) {
+ styleDecl.breakpoint = serializeBreakpoint(matchedBreakpoint);
breakpoints.push(matchedBreakpoint);
}
}
return breakpoints;
};
-const adaptBreakpoints = (
- parsedStyles: Omit[]
-) => {
- const newStyles: typeof parsedStyles = [];
+const adaptBreakpoints = (parsedStyles: StyleDecl[]) => {
const breakpointGroups = new Map();
for (const styleDecl of parsedStyles) {
- newStyles.push(styleDecl);
const mediaQuery = styleDecl.breakpoint
? parseMediaQuery(styleDecl.breakpoint)
: undefined;
@@ -143,27 +157,14 @@ const adaptBreakpoints = (
group = [];
breakpointGroups.set(groupKey, group);
}
- const styleDeclKey = `${styleDecl.breakpoint ?? ""}:${styleDecl.property}:${styleDecl.state ?? ""}`;
- group.push({ key: styleDeclKey, ...mediaQuery });
+ group.push({ styleDecl, ...mediaQuery });
}
- const breakpointsByKey = new Map();
- for (let group of breakpointGroups.values()) {
+ const newStyles: typeof parsedStyles = [];
+ for (const group of breakpointGroups.values()) {
const ranges = breakpointsToRanges(group);
- // adapt breakpoints only when first range is defined
- // for example opacity-10 sm:opacity-20 will work
- // but sm:opacity-20 alone does not have the base to switch to
- if (ranges[0].start === 0) {
- group = rangesToBreakpoints(ranges);
- }
- for (const breakpoint of group) {
- breakpointsByKey.set(breakpoint.key, breakpoint);
- }
- }
- for (const styleDecl of newStyles) {
- const styleDeclKey = `${styleDecl.breakpoint ?? ""}:${styleDecl.property}:${styleDecl.state ?? ""}`;
- const breakpoint = breakpointsByKey.get(styleDeclKey);
- if (breakpoint) {
- styleDecl.breakpoint = serializeBreakpoint(breakpoint);
+ const newGroup = rangesToBreakpoints(ranges);
+ for (const { styleDecl } of newGroup) {
+ newStyles.push(styleDecl);
}
}
return newStyles;
@@ -215,7 +216,7 @@ const parseTailwindClasses = async (classes: string) => {
const generated = await generator.generate(classes);
// use tailwind prefix instead of unocss one
const css = generated.css.replaceAll("--un-", "--tw-");
- let parsedStyles: Omit[] = [];
+ let parsedStyles: StyleDecl[] = [];
// @todo probably builtin in v4
if (css.includes("border")) {
// Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
@@ -264,7 +265,7 @@ const parseTailwindClasses = async (classes: string) => {
}
);
}
- adaptBreakpoints(parsedStyles);
+ parsedStyles = adaptBreakpoints(parsedStyles);
const newClasses = classes
.split(" ")
.filter((item) => !generated.matched.has(item))
@@ -353,7 +354,7 @@ export const generateFragmentFromTailwind = async (
};
const createOrMergeLocalStyles = (
instanceId: Instance["id"],
- newStyles: Omit[]
+ newStyles: StyleDecl[]
) => {
const localStyleSource =
getLocalStyleSource(instanceId) ?? createLocalStyleSource(instanceId);