diff --git a/apps/builder/app/shared/copy-paste/plugin-webflow/instances-properties.ts b/apps/builder/app/shared/copy-paste/plugin-webflow/instances-properties.ts index 6138b4afcb24..7b3e1489ff0a 100644 --- a/apps/builder/app/shared/copy-paste/plugin-webflow/instances-properties.ts +++ b/apps/builder/app/shared/copy-paste/plugin-webflow/instances-properties.ts @@ -345,7 +345,12 @@ const toFragment = ( addInstance(component); return fragment; } - + case "LightboxWrapper": { + addProp("tag", wfNode.tag); + addProp("href", wfNode.data?.attr?.href); + addInstance("Box", [], component); + return fragment; + } case "NavbarMenu": { addProp("tag", wfNode.tag); addProp("role", wfNode.data?.attr?.role); diff --git a/apps/builder/app/shared/copy-paste/plugin-webflow/plugin-webflow.test.tsx b/apps/builder/app/shared/copy-paste/plugin-webflow/plugin-webflow.test.tsx index c8b19eed5cdd..8e81dcd00437 100644 --- a/apps/builder/app/shared/copy-paste/plugin-webflow/plugin-webflow.test.tsx +++ b/apps/builder/app/shared/copy-paste/plugin-webflow/plugin-webflow.test.tsx @@ -685,6 +685,7 @@ test("QuickStack with instance styles", async () => { noPseudo: { gridTemplateColumns: "1fr 1fr", gridTemplateRows: "auto", + order: 1, }, }, }, @@ -731,7 +732,8 @@ test("QuickStack with instance styles", async () => { } Local { grid-template-columns: 1fr 1fr; - grid-template-rows: auto + grid-template-rows: auto; + order: 1 } w-layout-cell { flex-direction: column; @@ -3075,12 +3077,81 @@ describe("Styles", () => { const fragment = await toWebstudioFragment(input); expect(toCss(fragment)).toMatchInlineSnapshot(` -"@media all { - Div Block { - color: black; - background-color: red - } -}" -`); + "@media all { + Div Block { + color: black; + background-color: red + } + }" + `); + }); + + test("append transparent color when background-clip is used", async () => { + const input = WfData.parse({ + type: "@webflow/XscpData", + payload: { + nodes: [ + { + _id: "15c3fb65-b871-abe4-f9a4-3747c8a882e0", + type: "Heading", + tag: "h2", + classes: ["069649be-a33a-1a9a-3763-c0bd9d1f3a3d"], + children: ["b69a5869-f046-5a0c-151e-9b134a6852aa"], + data: { + tag: "h2", + devlink: { runtimeProps: {}, slot: "" }, + displayName: "", + attr: { id: "" }, + xattr: [], + search: { exclude: false }, + visibility: { conditions: [] }, + }, + }, + { + _id: "b69a5869-f046-5a0c-151e-9b134a6852aa", + text: true, + v: "Protect your systems securely with Prism", + }, + ], + styles: [ + { + _id: "069649be-a33a-1a9a-3763-c0bd9d1f3a3d", + fake: false, + type: "class", + name: "H2 Heading 2", + namespace: "", + comb: "", + styleLess: + "background-image: linear-gradient(350deg, hsla(256.3636363636363, 72.13%, 23.92%, 0.00), hsla(256.2162162162162, 72.55%, 80.00%, 1.00) 49%, #bba7f1); color: hsla(0, 0.00%, 100.00%, 1.00); background-clip: text;", + variants: {}, + children: [], + createdBy: "58b4b8186ceb395341fcf640", + origin: null, + selector: null, + }, + ], + assets: [], + }, + }); + + const fragment = await toWebstudioFragment(input); + + expect(toCss(fragment)).toMatchInlineSnapshot(` + "@media all { + h2 { + margin-bottom: 10px; + font-weight: bold; + margin-top: 20px; + font-size: 32px; + line-height: 36px + } + H2 Heading 2 { + background-image: linear-gradient(350deg,hsla(256.3636363636363,72.13%,23.92%,0.00),hsla(256.2162162162162,72.55%,80.00%,1.00) 49%,#bba7f1); + -webkit-background-clip: text; + background-clip: text; + color: transparent + } + }" + `); }); }); diff --git a/apps/builder/app/shared/copy-paste/plugin-webflow/schema.ts b/apps/builder/app/shared/copy-paste/plugin-webflow/schema.ts index 49fc5a57a208..b6b00f8f7e61 100644 --- a/apps/builder/app/shared/copy-paste/plugin-webflow/schema.ts +++ b/apps/builder/app/shared/copy-paste/plugin-webflow/schema.ts @@ -1,6 +1,8 @@ import { z } from "zod"; -const Attr = z.object({ id: z.string(), role: z.string() }).partial(); +const Attr = z + .object({ id: z.string(), role: z.string(), href: z.string() }) + .partial(); const styleBase = z.string(); @@ -10,7 +12,7 @@ const stylePseudo = z.string(); const styleProperty = z.string(); -const styleValue = z.string(); +const styleValue = z.unknown(); const WfNodeData = z.object({ attr: Attr.optional(), @@ -124,6 +126,7 @@ export const wfNodeTypes = [ "NavbarButton", "NavbarContainer", "Icon", + "LightboxWrapper", ] as const; const WfElementNode = z.union([ @@ -139,6 +142,7 @@ const WfElementNode = z.union([ }), }), + WfBaseNode.extend({ type: z.enum(["LightboxWrapper"]) }), WfBaseNode.extend({ type: z.enum(["NavbarMenu"]) }), WfBaseNode.extend({ type: z.enum(["NavbarContainer"]) }), @@ -202,7 +206,7 @@ const WfElementNode = z.union([ type: z.enum(["Image"]), data: WfNodeData.extend({ attr: Attr.extend({ - alt: z.string(), + alt: z.string().optional(), loading: z.enum(["lazy", "eager", "auto"]), src: z.string(), width: z.string(), diff --git a/apps/builder/app/shared/copy-paste/plugin-webflow/styles.ts b/apps/builder/app/shared/copy-paste/plugin-webflow/styles.ts index 7028f88e7281..e6fc623df8bf 100644 --- a/apps/builder/app/shared/copy-paste/plugin-webflow/styles.ts +++ b/apps/builder/app/shared/copy-paste/plugin-webflow/styles.ts @@ -102,6 +102,30 @@ const replaceAtImages = ( }); }; +const processStyles = (parsedStyles: ParsedStyleDecl[]) => { + const styles = new Map(); + for (const parsedStyleDecl of parsedStyles) { + const { breakpoint, selector, state, property } = parsedStyleDecl; + const key = `${breakpoint}:${selector}:${state}:${property}`; + styles.set(key, parsedStyleDecl); + } + for (const parsedStyleDecl of styles.values()) { + const { breakpoint, selector, state, property } = parsedStyleDecl; + const key = `${breakpoint}:${selector}:${state}:${property}`; + styles.set(key, parsedStyleDecl); + if (property === "backgroundClip") { + const colorKey = `${breakpoint}:${selector}:${state}:color`; + styles.delete(colorKey); + styles.set(colorKey, { + ...parsedStyleDecl, + property: "color", + value: { type: "keyword", value: "transparent" }, + }); + } + } + return Array.from(styles.values()); +}; + type UnparsedVariants = Map>; // Variants value can be wf styleLess string which is a styles block @@ -114,7 +138,9 @@ const toParsedVariants = (variants: UnparsedVariants) => { if (typeof styles === "string") { try { const sanitizedStyles = styles.replaceAll(/@raw<\|([^@]+)\|>/g, "$1"); - const parsed = parseCss(`.styles${state} {${sanitizedStyles}}`) ?? []; + const parsed = processStyles( + parseCss(`.styles${state} {${sanitizedStyles}}`) ?? [] + ); const allBreakpointStyles = parsedVariants.get(breakpointName) ?? []; allBreakpointStyles.push(...parsed); parsedVariants.set(breakpointName, allBreakpointStyles); diff --git a/packages/css-data/src/parse-css-value.test.ts b/packages/css-data/src/parse-css-value.test.ts index 8beac987c6fd..3342da198403 100644 --- a/packages/css-data/src/parse-css-value.test.ts +++ b/packages/css-data/src/parse-css-value.test.ts @@ -58,13 +58,6 @@ describe("Parse CSS value", () => { type: "invalid", value: "10", }); - - // This will return number unit, as number is valid for aspectRatio. - expect(parseCssValue("aspectRatio", "10")).toEqual({ - type: "unit", - unit: "number", - value: 10, - }); }); }); @@ -870,3 +863,25 @@ describe("parse filters", () => { }); }); }); + +describe("aspect-ratio", () => { + test("support single numeric value", () => { + expect(parseCssValue("aspectRatio", "10")).toEqual({ + type: "unit", + unit: "number", + value: 10, + }); + }); + test("support keyword", () => { + expect(parseCssValue("aspectRatio", "auto")).toEqual({ + type: "keyword", + value: "auto", + }); + }); + test("support two values", () => { + expect(parseCssValue("aspectRatio", "16 / 9")).toEqual({ + type: "unparsed", + value: "16 / 9", + }); + }); +}); diff --git a/packages/css-data/src/parse-css-value.ts b/packages/css-data/src/parse-css-value.ts index a137aeb7e6e1..125f34eb6b87 100644 --- a/packages/css-data/src/parse-css-value.ts +++ b/packages/css-data/src/parse-css-value.ts @@ -402,13 +402,17 @@ export const parseCssValue = ( // Probably a tuple like background-size or box-shadow if ( ast.type === "Value" && - (ast.children.size > 1 || tupleProps.has(property)) + (ast.children.size === 2 || tupleProps.has(property)) ) { const tuple: TupleValue = { type: "tuple", value: [], }; for (const node of ast.children) { + // output any values with unhandled operators like slash or comma as unparsed + if (node.type === "Operator") { + return { type: "unparsed", value: input }; + } const matchedValue = parseLiteral(node, keywordValues[property as never]); if (matchedValue) { tuple.value.push(matchedValue as never);