diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index e60b19d0620b..e9a7ee35f0f4 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -85,7 +85,7 @@ jobs:
const results = [
await assertSize('./fixtures/ssg/dist/client', 288),
await assertSize('./fixtures/webstudio-remix-netlify-functions/build/client', 376),
- await assertSize('./fixtures/webstudio-remix-vercel/build/client', 904),
+ await assertSize('./fixtures/webstudio-remix-vercel/build/client', 908),
]
for (const result of results) {
if (result.passed) {
diff --git a/apps/builder/app/builder/builder.tsx b/apps/builder/app/builder/builder.tsx
index a446ae002902..47435bb6ea73 100644
--- a/apps/builder/app/builder/builder.tsx
+++ b/apps/builder/app/builder/builder.tsx
@@ -64,11 +64,13 @@ import { migrateWebstudioDataMutable } from "~/shared/webstudio-data-migrator";
import { Loading, LoadingBackground } from "./shared/loading";
import { mergeRefs } from "@react-aria/utils";
import { CommandPanel } from "./features/command-panel";
+
import {
initCopyPaste,
initCopyPasteForContentEditMode,
} from "~/shared/copy-paste/init-copy-paste";
import { useInertHandlers } from "./shared/inert-handlers";
+import { TextToolbar } from "./features/workspace/canvas-tools/text-toolbar";
registerContainers();
@@ -122,6 +124,7 @@ const Main = ({ children, css }: { children: ReactNode; css?: CSS }) => (
css={{
gridArea: "main",
position: "relative",
+ isolation: "isolate",
...css,
}}
>
@@ -416,6 +419,9 @@ export const Builder = ({
/>
}
/>
+
+
+
{isPreviewMode === false && }
{
as="footer"
align="center"
css={{
+ isolation: "isolate",
gridArea: "footer",
height: theme.spacing[11],
background: theme.colors.backgroundTopbar,
diff --git a/apps/builder/app/builder/features/workspace/canvas-tools/canvas-tools.tsx b/apps/builder/app/builder/features/workspace/canvas-tools/canvas-tools.tsx
index 9009d21827e5..d4455df224f8 100644
--- a/apps/builder/app/builder/features/workspace/canvas-tools/canvas-tools.tsx
+++ b/apps/builder/app/builder/features/workspace/canvas-tools/canvas-tools.tsx
@@ -12,12 +12,10 @@ import {
HoveredInstanceOutline,
SelectedInstanceOutline,
} from "./outline";
-import { TextToolbar } from "./text-toolbar";
+
import { Label } from "./outline/label";
import { Outline } from "./outline/outline";
import { useSubscribeDragAndDropState } from "./use-subscribe-drag-drop-state";
-import { ResizeHandles } from "./resize-handles";
-import { MediaBadge } from "./media-badge";
import { applyScale } from "./outline";
import { $scale } from "~/builder/shared/nano-states";
import { BlockChildHoveredInstanceOutline } from "./outline/block-instance-outline";
@@ -73,15 +71,11 @@ export const CanvasTools = () => {
return (
<>
-
-
{isPreviewMode === false && (
<>
-
-
>
)}
diff --git a/apps/builder/app/builder/features/workspace/workspace.tsx b/apps/builder/app/builder/features/workspace/workspace.tsx
index 66a94cbaec5a..958c5e12664b 100644
--- a/apps/builder/app/builder/features/workspace/workspace.tsx
+++ b/apps/builder/app/builder/features/workspace/workspace.tsx
@@ -10,6 +10,8 @@ import { $textEditingInstanceSelector } from "~/shared/nano-states";
import { CanvasTools } from "./canvas-tools";
import { useSetCanvasWidth } from "../breakpoints";
import { selectInstance } from "~/shared/awareness";
+import { ResizeHandles } from "./canvas-tools/resize-handles";
+import { MediaBadge } from "./canvas-tools/media-badge";
const workspaceStyle = css({
flexGrow: 1,
@@ -101,6 +103,7 @@ export const Workspace = ({ children, onTransitionEnd }: WorkspaceProps) => {
selectInstance(undefined);
$textEditingInstanceSelector.set(undefined);
};
+ const outlineStyle = useOutlineStyle();
return (
<>
@@ -116,6 +119,14 @@ export const Workspace = ({ children, onTransitionEnd }: WorkspaceProps) => {
>
{children}
+
+
+
+
>
);
@@ -123,7 +134,6 @@ export const Workspace = ({ children, onTransitionEnd }: WorkspaceProps) => {
export const CanvasToolsContainer = () => {
const outlineStyle = useOutlineStyle();
- useSetCanvasWidth();
return (
<>
diff --git a/fixtures/webstudio-remix-vercel/.webstudio/data.json b/fixtures/webstudio-remix-vercel/.webstudio/data.json
index 76686e2b5664..9239d458ba4b 100644
--- a/fixtures/webstudio-remix-vercel/.webstudio/data.json
+++ b/fixtures/webstudio-remix-vercel/.webstudio/data.json
@@ -1,10 +1,10 @@
{
"build": {
- "id": "5646b5e6-a161-4fbc-8312-6e2852e1d4b3",
+ "id": "00bc4703-0722-4929-9647-fbe0ae091b94",
"projectId": "cddc1d44-af37-4cb6-a430-d300cf6f932d",
- "version": 345,
- "createdAt": "2024-11-04T04:15:27.144+00:00",
- "updatedAt": "2024-11-04T04:15:27.144+00:00",
+ "version": 392,
+ "createdAt": "2024-12-02T14:50:20.265+00:00",
+ "updatedAt": "2024-12-02T14:50:20.265+00:00",
"pages": {
"meta": {
"siteName": "KittyGuardedZone",
@@ -181,6 +181,27 @@
"include": false
},
"path": "/sitemap.xml"
+ },
+ {
+ "id": "Q1D-6G1cl0SfXyM9Xj4_O",
+ "name": "content-block",
+ "title": "\"Untitled\"",
+ "rootInstanceId": "-BW4QOi3PJTZ1sDCY8LW6",
+ "systemDataSourceId": "F3zbkztYW_mJNC5EOkopM",
+ "meta": {
+ "description": "\"\"",
+ "excludePageFromSearch": "true",
+ "language": "\"\"",
+ "socialImageUrl": "\"\"",
+ "status": "200",
+ "redirect": "\"\"",
+ "documentType": "html",
+ "custom": []
+ },
+ "marketplace": {
+ "include": false
+ },
+ "path": "/content-block"
}
],
"folders": [
@@ -198,7 +219,8 @@
"42cWhASQ3tTtKDnsvzhUF",
"9xlwLSHxuk8HmS3-EEGcf",
"lTS5DKrDEC_mXSAc5ZDDA",
- "FsnS9ui6btzM4W3YELE3Q"
+ "FsnS9ui6btzM4W3YELE3Q",
+ "Q1D-6G1cl0SfXyM9Xj4_O"
]
},
{
@@ -1606,6 +1628,118 @@
"value": 0
}
}
+ ],
+ [
+ "MX5X3_9QBk2KNCj9_oKWA:UoTkWyaFuTYJihS3MFYK5:fontSize:",
+ {
+ "breakpointId": "UoTkWyaFuTYJihS3MFYK5",
+ "styleSourceId": "MX5X3_9QBk2KNCj9_oKWA",
+ "property": "fontSize",
+ "value": {
+ "type": "unit",
+ "unit": "em",
+ "value": 1
+ }
+ }
+ ],
+ [
+ "9manQUfGxXPqfUvJR7nx_:UoTkWyaFuTYJihS3MFYK5:backgroundColor:",
+ {
+ "breakpointId": "UoTkWyaFuTYJihS3MFYK5",
+ "styleSourceId": "9manQUfGxXPqfUvJR7nx_",
+ "property": "backgroundColor",
+ "value": {
+ "type": "rgb",
+ "r": 251,
+ "g": 247,
+ "b": 3,
+ "alpha": 1
+ }
+ }
+ ],
+ [
+ "-S9zvxalqR_PGnY3STOlK:UoTkWyaFuTYJihS3MFYK5:fontSize:",
+ {
+ "styleSourceId": "-S9zvxalqR_PGnY3STOlK",
+ "breakpointId": "UoTkWyaFuTYJihS3MFYK5",
+ "property": "fontSize",
+ "value": {
+ "type": "unit",
+ "unit": "em",
+ "value": 1
+ }
+ }
+ ],
+ [
+ "FdQDnQuBGdJjsbJJkurgl:UoTkWyaFuTYJihS3MFYK5:backgroundColor:",
+ {
+ "breakpointId": "UoTkWyaFuTYJihS3MFYK5",
+ "styleSourceId": "FdQDnQuBGdJjsbJJkurgl",
+ "property": "backgroundColor",
+ "value": {
+ "type": "rgb",
+ "r": 89,
+ "g": 250,
+ "b": 2,
+ "alpha": 1
+ }
+ }
+ ],
+ [
+ "w3UQay-q-ZxidA7tiKjlT:UoTkWyaFuTYJihS3MFYK5:fontSize:",
+ {
+ "styleSourceId": "w3UQay-q-ZxidA7tiKjlT",
+ "breakpointId": "UoTkWyaFuTYJihS3MFYK5",
+ "property": "fontSize",
+ "value": {
+ "type": "unit",
+ "unit": "em",
+ "value": 1
+ }
+ }
+ ],
+ [
+ "2fMK05sbKQLCp_N7ES5cH:UoTkWyaFuTYJihS3MFYK5:backgroundColor:",
+ {
+ "breakpointId": "UoTkWyaFuTYJihS3MFYK5",
+ "styleSourceId": "2fMK05sbKQLCp_N7ES5cH",
+ "property": "backgroundColor",
+ "value": {
+ "type": "rgb",
+ "r": 2,
+ "g": 250,
+ "b": 168,
+ "alpha": 1
+ }
+ }
+ ],
+ [
+ "z9mqrlxshNacx8dvtUKFI:UoTkWyaFuTYJihS3MFYK5:fontSize:",
+ {
+ "styleSourceId": "z9mqrlxshNacx8dvtUKFI",
+ "breakpointId": "UoTkWyaFuTYJihS3MFYK5",
+ "property": "fontSize",
+ "value": {
+ "type": "unit",
+ "unit": "em",
+ "value": 1
+ }
+ }
+ ],
+ [
+ "nlUHVVdPJqX4ucX3QvPHQ:UoTkWyaFuTYJihS3MFYK5:backgroundColor:",
+ {
+ "breakpointId": "UoTkWyaFuTYJihS3MFYK5",
+ "styleSourceId": "nlUHVVdPJqX4ucX3QvPHQ",
+ "property": "backgroundColor",
+ "value": {
+ "type": "rgb",
+ "r": 2,
+ "g": 139,
+ "b": 250,
+ "alpha": 1
+ }
+ }
]
],
"styleSources": [
@@ -1797,6 +1931,62 @@
"type": "local",
"id": "LnOCG1kkSViZaAVoz4GXw"
}
+ ],
+ [
+ "MX5X3_9QBk2KNCj9_oKWA",
+ {
+ "type": "local",
+ "id": "MX5X3_9QBk2KNCj9_oKWA"
+ }
+ ],
+ [
+ "9manQUfGxXPqfUvJR7nx_",
+ {
+ "type": "local",
+ "id": "9manQUfGxXPqfUvJR7nx_"
+ }
+ ],
+ [
+ "FdQDnQuBGdJjsbJJkurgl",
+ {
+ "type": "local",
+ "id": "FdQDnQuBGdJjsbJJkurgl"
+ }
+ ],
+ [
+ "-S9zvxalqR_PGnY3STOlK",
+ {
+ "type": "local",
+ "id": "-S9zvxalqR_PGnY3STOlK"
+ }
+ ],
+ [
+ "2fMK05sbKQLCp_N7ES5cH",
+ {
+ "type": "local",
+ "id": "2fMK05sbKQLCp_N7ES5cH"
+ }
+ ],
+ [
+ "w3UQay-q-ZxidA7tiKjlT",
+ {
+ "type": "local",
+ "id": "w3UQay-q-ZxidA7tiKjlT"
+ }
+ ],
+ [
+ "nlUHVVdPJqX4ucX3QvPHQ",
+ {
+ "type": "local",
+ "id": "nlUHVVdPJqX4ucX3QvPHQ"
+ }
+ ],
+ [
+ "z9mqrlxshNacx8dvtUKFI",
+ {
+ "type": "local",
+ "id": "z9mqrlxshNacx8dvtUKFI"
+ }
]
],
"styleSourceSelections": [
@@ -1988,6 +2178,62 @@
"instanceId": "LYBzzBvHLrfSOmPLs52dP",
"values": ["LnOCG1kkSViZaAVoz4GXw"]
}
+ ],
+ [
+ "3rtiNx74sdhz1rNTQBmRA",
+ {
+ "instanceId": "3rtiNx74sdhz1rNTQBmRA",
+ "values": ["MX5X3_9QBk2KNCj9_oKWA"]
+ }
+ ],
+ [
+ "Dwv02N8aJoxXacL58cmf-",
+ {
+ "instanceId": "Dwv02N8aJoxXacL58cmf-",
+ "values": ["9manQUfGxXPqfUvJR7nx_"]
+ }
+ ],
+ [
+ "TNyJ2G7r6h267foLukveQ",
+ {
+ "instanceId": "TNyJ2G7r6h267foLukveQ",
+ "values": ["FdQDnQuBGdJjsbJJkurgl"]
+ }
+ ],
+ [
+ "YCTo59XfqTGNPH7ow1rvU",
+ {
+ "instanceId": "YCTo59XfqTGNPH7ow1rvU",
+ "values": ["-S9zvxalqR_PGnY3STOlK"]
+ }
+ ],
+ [
+ "VJAhRG8CDslSQStNf0C_A",
+ {
+ "instanceId": "VJAhRG8CDslSQStNf0C_A",
+ "values": ["2fMK05sbKQLCp_N7ES5cH"]
+ }
+ ],
+ [
+ "67Te7ahXi4mbqb0CMC6Xh",
+ {
+ "instanceId": "67Te7ahXi4mbqb0CMC6Xh",
+ "values": ["w3UQay-q-ZxidA7tiKjlT"]
+ }
+ ],
+ [
+ "RHGykFKK2R4PyjNtIuSEk",
+ {
+ "instanceId": "RHGykFKK2R4PyjNtIuSEk",
+ "values": ["nlUHVVdPJqX4ucX3QvPHQ"]
+ }
+ ],
+ [
+ "ccyTkEM40ar6Z2UTXhFvY",
+ {
+ "instanceId": "ccyTkEM40ar6Z2UTXhFvY",
+ "values": ["z9mqrlxshNacx8dvtUKFI"]
+ }
]
],
"props": [
@@ -2808,6 +3054,15 @@
"scopeInstanceId": "zJkrskDJhaEk-nOSRWrGg",
"name": "url"
}
+ ],
+ [
+ "F3zbkztYW_mJNC5EOkopM",
+ {
+ "type": "parameter",
+ "id": "F3zbkztYW_mJNC5EOkopM",
+ "scopeInstanceId": "-BW4QOi3PJTZ1sDCY8LW6",
+ "name": "system"
+ }
]
],
"resources": [
@@ -4152,6 +4407,422 @@
}
]
}
+ ],
+ [
+ "-BW4QOi3PJTZ1sDCY8LW6",
+ {
+ "type": "instance",
+ "id": "-BW4QOi3PJTZ1sDCY8LW6",
+ "component": "Body",
+ "children": [
+ {
+ "type": "id",
+ "value": "Dwv02N8aJoxXacL58cmf-"
+ },
+ {
+ "type": "id",
+ "value": "TNyJ2G7r6h267foLukveQ"
+ },
+ {
+ "type": "id",
+ "value": "VJAhRG8CDslSQStNf0C_A"
+ },
+ {
+ "type": "id",
+ "value": "RHGykFKK2R4PyjNtIuSEk"
+ }
+ ]
+ }
+ ],
+ [
+ "Dwv02N8aJoxXacL58cmf-",
+ {
+ "type": "instance",
+ "id": "Dwv02N8aJoxXacL58cmf-",
+ "component": "Box",
+ "label": "With Templates And Content",
+ "children": [
+ {
+ "type": "id",
+ "value": "3rtiNx74sdhz1rNTQBmRA"
+ },
+ {
+ "type": "id",
+ "value": "mNIGsDuFN_jhG8g1socRY"
+ }
+ ]
+ }
+ ],
+ [
+ "3rtiNx74sdhz1rNTQBmRA",
+ {
+ "type": "instance",
+ "id": "3rtiNx74sdhz1rNTQBmRA",
+ "component": "Heading",
+ "children": [
+ {
+ "type": "text",
+ "value": "Content Block With Templates And Content"
+ }
+ ]
+ }
+ ],
+ [
+ "mNIGsDuFN_jhG8g1socRY",
+ {
+ "type": "instance",
+ "id": "mNIGsDuFN_jhG8g1socRY",
+ "component": "ws:block",
+ "label": "With Templates And Content",
+ "children": [
+ {
+ "type": "id",
+ "value": "B1Gn2WHnTDrPgYCvarFx4"
+ },
+ {
+ "type": "id",
+ "value": "GAKL3iFwdWc85Yp9vsGBI"
+ },
+ {
+ "type": "id",
+ "value": "KIJoY0O_HgjVtc8cskt5U"
+ }
+ ]
+ }
+ ],
+ [
+ "B1Gn2WHnTDrPgYCvarFx4",
+ {
+ "type": "instance",
+ "id": "B1Gn2WHnTDrPgYCvarFx4",
+ "component": "ws:block-template",
+ "label": "Templates",
+ "children": [
+ {
+ "type": "id",
+ "value": "4KmEPyxWtLOVyK7EqhJyT"
+ },
+ {
+ "type": "id",
+ "value": "nCJ6DV5oGv4DOdzQvlav8"
+ }
+ ]
+ }
+ ],
+ [
+ "4KmEPyxWtLOVyK7EqhJyT",
+ {
+ "type": "instance",
+ "id": "4KmEPyxWtLOVyK7EqhJyT",
+ "component": "Heading",
+ "label": "T-Heading",
+ "children": [
+ {
+ "type": "text",
+ "value": "Heading text you can edit",
+ "placeholder": true
+ }
+ ]
+ }
+ ],
+ [
+ "nCJ6DV5oGv4DOdzQvlav8",
+ {
+ "type": "instance",
+ "id": "nCJ6DV5oGv4DOdzQvlav8",
+ "component": "Paragraph",
+ "label": "T-Paragraph",
+ "children": [
+ {
+ "type": "text",
+ "value": "Paragraph text you can edit",
+ "placeholder": true
+ }
+ ]
+ }
+ ],
+ [
+ "GAKL3iFwdWc85Yp9vsGBI",
+ {
+ "type": "instance",
+ "id": "GAKL3iFwdWc85Yp9vsGBI",
+ "component": "Heading",
+ "label": "T-Heading",
+ "children": [
+ {
+ "type": "text",
+ "value": "H1"
+ }
+ ]
+ }
+ ],
+ [
+ "KIJoY0O_HgjVtc8cskt5U",
+ {
+ "type": "instance",
+ "id": "KIJoY0O_HgjVtc8cskt5U",
+ "component": "Paragraph",
+ "label": "T-Paragraph",
+ "children": [
+ {
+ "type": "text",
+ "value": "Paragraph"
+ }
+ ]
+ }
+ ],
+ [
+ "TNyJ2G7r6h267foLukveQ",
+ {
+ "type": "instance",
+ "id": "TNyJ2G7r6h267foLukveQ",
+ "component": "Box",
+ "label": "With Templates Only",
+ "children": [
+ {
+ "type": "id",
+ "value": "YCTo59XfqTGNPH7ow1rvU"
+ },
+ {
+ "type": "id",
+ "value": "_HZlreqcBl-3vSqpdwIZj"
+ }
+ ]
+ }
+ ],
+ [
+ "YCTo59XfqTGNPH7ow1rvU",
+ {
+ "type": "instance",
+ "id": "YCTo59XfqTGNPH7ow1rvU",
+ "component": "Heading",
+ "children": [
+ {
+ "type": "text",
+ "value": "Content Block With Templates Only"
+ }
+ ]
+ }
+ ],
+ [
+ "_HZlreqcBl-3vSqpdwIZj",
+ {
+ "type": "instance",
+ "id": "_HZlreqcBl-3vSqpdwIZj",
+ "component": "ws:block",
+ "label": "With Templates Only",
+ "children": [
+ {
+ "type": "id",
+ "value": "uWkdb-95WncG3WVqWe1gl"
+ }
+ ]
+ }
+ ],
+ [
+ "uWkdb-95WncG3WVqWe1gl",
+ {
+ "type": "instance",
+ "id": "uWkdb-95WncG3WVqWe1gl",
+ "component": "ws:block-template",
+ "label": "Templates",
+ "children": [
+ {
+ "type": "id",
+ "value": "XnNmz3xvg5uC5ZHfSMbKQ"
+ },
+ {
+ "type": "id",
+ "value": "u4sJ-bYw1KAAtN2XybZFD"
+ }
+ ]
+ }
+ ],
+ [
+ "XnNmz3xvg5uC5ZHfSMbKQ",
+ {
+ "type": "instance",
+ "id": "XnNmz3xvg5uC5ZHfSMbKQ",
+ "component": "Heading",
+ "label": "T-Heading",
+ "children": [
+ {
+ "type": "text",
+ "value": "Heading text you can edit",
+ "placeholder": true
+ }
+ ]
+ }
+ ],
+ [
+ "u4sJ-bYw1KAAtN2XybZFD",
+ {
+ "type": "instance",
+ "id": "u4sJ-bYw1KAAtN2XybZFD",
+ "component": "Paragraph",
+ "label": "T-Paragraph",
+ "children": [
+ {
+ "type": "text",
+ "value": "Paragraph text you can edit",
+ "placeholder": true
+ }
+ ]
+ }
+ ],
+ [
+ "VJAhRG8CDslSQStNf0C_A",
+ {
+ "type": "instance",
+ "id": "VJAhRG8CDslSQStNf0C_A",
+ "component": "Box",
+ "label": "With Content Only",
+ "children": [
+ {
+ "type": "id",
+ "value": "67Te7ahXi4mbqb0CMC6Xh"
+ },
+ {
+ "type": "id",
+ "value": "Wtt1RQs-cGQVxwGoCBTrQ"
+ }
+ ]
+ }
+ ],
+ [
+ "67Te7ahXi4mbqb0CMC6Xh",
+ {
+ "type": "instance",
+ "id": "67Te7ahXi4mbqb0CMC6Xh",
+ "component": "Heading",
+ "children": [
+ {
+ "type": "text",
+ "value": "With Content Only"
+ }
+ ]
+ }
+ ],
+ [
+ "Wtt1RQs-cGQVxwGoCBTrQ",
+ {
+ "type": "instance",
+ "id": "Wtt1RQs-cGQVxwGoCBTrQ",
+ "component": "ws:block",
+ "label": "With Content Only",
+ "children": [
+ {
+ "type": "id",
+ "value": "pKWGbMqgT95dnExR0JI93"
+ },
+ {
+ "type": "id",
+ "value": "5dI7VvESR58C2ZXcszIRr"
+ },
+ {
+ "type": "id",
+ "value": "lI28xjfL4rGE5Wn1kcatW"
+ }
+ ]
+ }
+ ],
+ [
+ "pKWGbMqgT95dnExR0JI93",
+ {
+ "type": "instance",
+ "id": "pKWGbMqgT95dnExR0JI93",
+ "component": "ws:block-template",
+ "label": "Templates",
+ "children": []
+ }
+ ],
+ [
+ "5dI7VvESR58C2ZXcszIRr",
+ {
+ "type": "instance",
+ "id": "5dI7VvESR58C2ZXcszIRr",
+ "component": "Heading",
+ "label": "T-Heading",
+ "children": [
+ {
+ "type": "text",
+ "value": "H1"
+ }
+ ]
+ }
+ ],
+ [
+ "lI28xjfL4rGE5Wn1kcatW",
+ {
+ "type": "instance",
+ "id": "lI28xjfL4rGE5Wn1kcatW",
+ "component": "Paragraph",
+ "label": "T-Paragraph",
+ "children": [
+ {
+ "type": "text",
+ "value": "Paragraph"
+ }
+ ]
+ }
+ ],
+ [
+ "RHGykFKK2R4PyjNtIuSEk",
+ {
+ "type": "instance",
+ "id": "RHGykFKK2R4PyjNtIuSEk",
+ "component": "Box",
+ "label": "Empty",
+ "children": [
+ {
+ "type": "id",
+ "value": "ccyTkEM40ar6Z2UTXhFvY"
+ },
+ {
+ "type": "id",
+ "value": "qwlUNFg1FL8rTLvu614Fe"
+ }
+ ]
+ }
+ ],
+ [
+ "ccyTkEM40ar6Z2UTXhFvY",
+ {
+ "type": "instance",
+ "id": "ccyTkEM40ar6Z2UTXhFvY",
+ "component": "Heading",
+ "children": [
+ {
+ "type": "text",
+ "value": "Empty"
+ }
+ ]
+ }
+ ],
+ [
+ "qwlUNFg1FL8rTLvu614Fe",
+ {
+ "type": "instance",
+ "id": "qwlUNFg1FL8rTLvu614Fe",
+ "component": "ws:block",
+ "label": "With Content Only",
+ "children": [
+ {
+ "type": "id",
+ "value": "ciTnbTkbURP61HSjy8TX3"
+ }
+ ]
+ }
+ ],
+ [
+ "ciTnbTkbURP61HSjy8TX3",
+ {
+ "type": "instance",
+ "id": "ciTnbTkbURP61HSjy8TX3",
+ "component": "ws:block-template",
+ "label": "Templates",
+ "children": []
+ }
]
],
"deployment": {
@@ -4349,6 +5020,27 @@
"include": false
},
"path": "/sitemap.xml"
+ },
+ {
+ "id": "Q1D-6G1cl0SfXyM9Xj4_O",
+ "name": "content-block",
+ "title": "\"Untitled\"",
+ "rootInstanceId": "-BW4QOi3PJTZ1sDCY8LW6",
+ "systemDataSourceId": "F3zbkztYW_mJNC5EOkopM",
+ "meta": {
+ "description": "\"\"",
+ "excludePageFromSearch": "true",
+ "language": "\"\"",
+ "socialImageUrl": "\"\"",
+ "status": "200",
+ "redirect": "\"\"",
+ "documentType": "html",
+ "custom": []
+ },
+ "marketplace": {
+ "include": false
+ },
+ "path": "/content-block"
}
],
"assets": [
diff --git a/fixtures/webstudio-remix-vercel/app/__generated__/$resources.sitemap.xml.ts b/fixtures/webstudio-remix-vercel/app/__generated__/$resources.sitemap.xml.ts
index d771f93ec29d..46b231d82ce3 100644
--- a/fixtures/webstudio-remix-vercel/app/__generated__/$resources.sitemap.xml.ts
+++ b/fixtures/webstudio-remix-vercel/app/__generated__/$resources.sitemap.xml.ts
@@ -1,26 +1,26 @@
export const sitemap = [
{
path: "/",
- lastModified: "2024-11-04",
+ lastModified: "2024-12-02",
},
{
path: "/_route_with_symbols_",
- lastModified: "2024-11-04",
+ lastModified: "2024-12-02",
},
{
path: "/form",
- lastModified: "2024-11-04",
+ lastModified: "2024-12-02",
},
{
path: "/heading-with-id",
- lastModified: "2024-11-04",
+ lastModified: "2024-12-02",
},
{
path: "/resources",
- lastModified: "2024-11-04",
+ lastModified: "2024-12-02",
},
{
path: "/nested/nested-page",
- lastModified: "2024-11-04",
+ lastModified: "2024-12-02",
},
];
diff --git a/fixtures/webstudio-remix-vercel/app/__generated__/[content-block]._index.server.tsx b/fixtures/webstudio-remix-vercel/app/__generated__/[content-block]._index.server.tsx
new file mode 100644
index 000000000000..6c968b385baa
--- /dev/null
+++ b/fixtures/webstudio-remix-vercel/app/__generated__/[content-block]._index.server.tsx
@@ -0,0 +1,39 @@
+/* eslint-disable */
+/* This is a auto generated file for building the project */
+
+import type { PageMeta } from "@webstudio-is/sdk";
+import type { System, ResourceRequest } from "@webstudio-is/sdk";
+export const getResources = (_props: { system: System }) => {
+ const _data = new Map([]);
+ const _action = new Map([]);
+ return { data: _data, action: _action };
+};
+
+export const getPageMeta = ({
+ system,
+ resources,
+}: {
+ system: System;
+ resources: Record;
+}): PageMeta => {
+ return {
+ title: "Untitled",
+ description: "",
+ excludePageFromSearch: true,
+ language: "",
+ socialImageAssetName: undefined,
+ socialImageUrl: "",
+ status: 200,
+ redirect: "",
+ custom: [],
+ };
+};
+
+type Params = Record;
+export const getRemixParams = ({ ...params }: Params): Params => {
+ return params;
+};
+
+export const projectId = "cddc1d44-af37-4cb6-a430-d300cf6f932d";
+
+export const contactEmail = "hello@webstudio.is";
diff --git a/fixtures/webstudio-remix-vercel/app/__generated__/[content-block]._index.tsx b/fixtures/webstudio-remix-vercel/app/__generated__/[content-block]._index.tsx
new file mode 100644
index 000000000000..6d51d1518abb
--- /dev/null
+++ b/fixtures/webstudio-remix-vercel/app/__generated__/[content-block]._index.tsx
@@ -0,0 +1,60 @@
+/* eslint-disable */
+/* This is a auto generated file for building the project */
+
+import { Fragment, useState } from "react";
+import type { FontAsset, ImageAsset } from "@webstudio-is/sdk";
+import { useResource, useVariableState } from "@webstudio-is/react-sdk/runtime";
+import { Body as Body } from "@webstudio-is/sdk-components-react-remix";
+import {
+ Box as Box,
+ Heading as Heading,
+ Paragraph as Paragraph,
+} from "@webstudio-is/sdk-components-react";
+
+export const siteName = "KittyGuardedZone";
+
+export const favIconAsset: ImageAsset | undefined = {
+ id: "88d5e2ff-b8f2-4899-aaf8-dde4ade6da10",
+ name: "DALL_E_2023-10-30_12.39.46_-_Photo_logo_with_a_bold_cat_silhouette_centered_on_a_contrasting_background_designed_for_clarity_at_small_32x32_favicon_resolution_00h6cEA8u2pJRvVJv7hRe.png",
+ description: null,
+ projectId: "cddc1d44-af37-4cb6-a430-d300cf6f932d",
+ size: 268326,
+ type: "image",
+ format: "png",
+ createdAt: "2023-10-30T13:51:08.416+00:00",
+ meta: { width: 790, height: 786 },
+};
+
+// Font assets on current page (can be preloaded)
+export const pageFontAssets: FontAsset[] = [];
+
+export const pageBackgroundImageAssets: ImageAsset[] = [];
+
+const Page = ({}: { system: any }) => {
+ return (
+
+
+
+ {"Content Block With Templates And Content"}
+
+ {"H1"}
+ {"Paragraph"}
+
+
+
+ {"Content Block With Templates Only"}
+
+
+
+ {"With Content Only"}
+ {"H1"}
+ {"Paragraph"}
+
+
+ {"Empty"}
+
+
+ );
+};
+
+export { Page };
diff --git a/fixtures/webstudio-remix-vercel/app/__generated__/index.css b/fixtures/webstudio-remix-vercel/app/__generated__/index.css
index 82f1e61b4cc1..51a36c2e556a 100644
--- a/fixtures/webstudio-remix-vercel/app/__generated__/index.css
+++ b/fixtures/webstudio-remix-vercel/app/__generated__/index.css
@@ -398,4 +398,19 @@
.cvdtpev {
margin-bottom: 0em;
}
+ .cc5h0no {
+ font-size: 1em;
+ }
+ .coklr1z {
+ background-color: rgba(251, 247, 3, 1);
+ }
+ .c1cqkpk9 {
+ background-color: rgba(89, 250, 2, 1);
+ }
+ .ccxj65f {
+ background-color: rgba(2, 250, 168, 1);
+ }
+ .c1a2hnxl {
+ background-color: rgba(2, 139, 250, 1);
+ }
}
diff --git a/fixtures/webstudio-remix-vercel/app/routes/[content-block]._index.tsx b/fixtures/webstudio-remix-vercel/app/routes/[content-block]._index.tsx
new file mode 100644
index 000000000000..35c66fec95d1
--- /dev/null
+++ b/fixtures/webstudio-remix-vercel/app/routes/[content-block]._index.tsx
@@ -0,0 +1,342 @@
+import {
+ type ServerRuntimeMetaFunction as MetaFunction,
+ type LinksFunction,
+ type LinkDescriptor,
+ type ActionFunctionArgs,
+ type LoaderFunctionArgs,
+ type HeadersFunction,
+ json,
+ redirect,
+} from "@remix-run/server-runtime";
+import { useLoaderData } from "@remix-run/react";
+import {
+ isLocalResource,
+ loadResource,
+ loadResources,
+ formIdFieldName,
+ formBotFieldName,
+} from "@webstudio-is/sdk/runtime";
+import { ReactSdkContext } from "@webstudio-is/react-sdk/runtime";
+import {
+ Page,
+ siteName,
+ favIconAsset,
+ pageFontAssets,
+ pageBackgroundImageAssets,
+} from "../__generated__/[content-block]._index";
+import {
+ getResources,
+ getPageMeta,
+ getRemixParams,
+ projectId,
+ contactEmail,
+} from "../__generated__/[content-block]._index.server";
+import { assetBaseUrl, imageBaseUrl, imageLoader } from "../constants.mjs";
+import css from "../__generated__/index.css?url";
+import { sitemap } from "../__generated__/$resources.sitemap.xml";
+
+const customFetch: typeof fetch = (input, init) => {
+ if (typeof input !== "string") {
+ return fetch(input, init);
+ }
+
+ if (isLocalResource(input, "sitemap.xml")) {
+ // @todo: dynamic import sitemap ???
+ const response = new Response(JSON.stringify(sitemap));
+ response.headers.set("content-type", "application/json; charset=utf-8");
+ return Promise.resolve(response);
+ }
+
+ return fetch(input, init);
+};
+
+export const loader = async (arg: LoaderFunctionArgs) => {
+ const url = new URL(arg.request.url);
+ const host =
+ arg.request.headers.get("x-forwarded-host") ||
+ arg.request.headers.get("host") ||
+ "";
+ url.host = host;
+ url.protocol = "https";
+
+ const params = getRemixParams(arg.params);
+ const system = {
+ params,
+ search: Object.fromEntries(url.searchParams),
+ origin: url.origin,
+ };
+
+ const resources = await loadResources(
+ customFetch,
+ getResources({ system }).data
+ );
+ const pageMeta = getPageMeta({ system, resources });
+
+ if (pageMeta.redirect) {
+ const status =
+ pageMeta.status === 301 || pageMeta.status === 302
+ ? pageMeta.status
+ : 302;
+ return redirect(pageMeta.redirect, status);
+ }
+
+ // typecheck
+ arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;
+
+ if (arg.context.EXCLUDE_FROM_SEARCH) {
+ pageMeta.excludePageFromSearch = arg.context.EXCLUDE_FROM_SEARCH;
+ }
+
+ return json(
+ {
+ host,
+ url: url.href,
+ system,
+ resources,
+ pageMeta,
+ },
+ // No way for current information to change, so add cache for 10 minutes
+ // In case of CRM Data, this should be set to 0
+ {
+ status: pageMeta.status,
+ headers: {
+ "Cache-Control": "public, max-age=600",
+ },
+ }
+ );
+};
+
+export const headers: HeadersFunction = () => {
+ return {
+ "Cache-Control": "public, max-age=0, must-revalidate",
+ };
+};
+
+export const meta: MetaFunction = ({ data }) => {
+ const metas: ReturnType = [];
+ if (data === undefined) {
+ return metas;
+ }
+ const { pageMeta } = data;
+
+ if (data.url) {
+ metas.push({
+ property: "og:url",
+ content: data.url,
+ });
+ }
+
+ if (pageMeta.title) {
+ metas.push({ title: pageMeta.title });
+
+ metas.push({
+ property: "og:title",
+ content: pageMeta.title,
+ });
+ }
+
+ metas.push({ property: "og:type", content: "website" });
+
+ const origin = `https://${data.host}`;
+
+ if (siteName) {
+ metas.push({
+ property: "og:site_name",
+ content: siteName,
+ });
+ metas.push({
+ "script:ld+json": {
+ "@context": "https://schema.org",
+ "@type": "WebSite",
+ name: siteName,
+ url: origin,
+ },
+ });
+ }
+
+ if (pageMeta.excludePageFromSearch) {
+ metas.push({
+ name: "robots",
+ content: "noindex, nofollow",
+ });
+ }
+
+ if (pageMeta.description) {
+ metas.push({
+ name: "description",
+ content: pageMeta.description,
+ });
+ metas.push({
+ property: "og:description",
+ content: pageMeta.description,
+ });
+ }
+
+ if (pageMeta.socialImageAssetName) {
+ metas.push({
+ property: "og:image",
+ content: `https://${data.host}${imageLoader({
+ src: pageMeta.socialImageAssetName,
+ // Do not transform social image (not enough information do we need to do this)
+ format: "raw",
+ })}`,
+ });
+ } else if (pageMeta.socialImageUrl) {
+ metas.push({
+ property: "og:image",
+ content: pageMeta.socialImageUrl,
+ });
+ }
+
+ metas.push(...pageMeta.custom);
+
+ return metas;
+};
+
+export const links: LinksFunction = () => {
+ const result: LinkDescriptor[] = [];
+
+ result.push({
+ rel: "stylesheet",
+ href: css,
+ });
+
+ if (favIconAsset) {
+ result.push({
+ rel: "icon",
+ href: imageLoader({
+ src: favIconAsset.name,
+ // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search
+ width: 144,
+ height: 144,
+ fit: "pad",
+ quality: 100,
+ format: "auto",
+ }),
+ type: undefined,
+ });
+ }
+
+ for (const asset of pageFontAssets) {
+ result.push({
+ rel: "preload",
+ href: `${assetBaseUrl}${asset.name}`,
+ as: "font",
+ crossOrigin: "anonymous",
+ });
+ }
+
+ for (const backgroundImageAsset of pageBackgroundImageAssets) {
+ result.push({
+ rel: "preload",
+ href: `${assetBaseUrl}${backgroundImageAsset.name}`,
+ as: "image",
+ });
+ }
+
+ return result;
+};
+
+const getRequestHost = (request: Request): string =>
+ request.headers.get("x-forwarded-host") || request.headers.get("host") || "";
+
+export const action = async ({
+ request,
+ context,
+}: ActionFunctionArgs): Promise<
+ { success: true } | { success: false; errors: string[] }
+> => {
+ try {
+ const url = new URL(request.url);
+ url.host = getRequestHost(request);
+
+ const formData = await request.formData();
+
+ const system = {
+ params: {},
+ search: {},
+ origin: url.origin,
+ };
+
+ const resourceName = formData.get(formIdFieldName);
+ let resource =
+ typeof resourceName === "string"
+ ? getResources({ system }).action.get(resourceName)
+ : undefined;
+
+ const formBotValue = formData.get(formBotFieldName);
+
+ if (formBotValue == null || typeof formBotValue !== "string") {
+ throw new Error("Form bot field not found");
+ }
+
+ const submitTime = parseInt(formBotValue, 16);
+ // Assumes that the difference between the server time and the form submission time,
+ // including any client-server time drift, is within a 5-minute range.
+ // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.
+ // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`
+ if (
+ Number.isNaN(submitTime) ||
+ Math.abs(Date.now() - submitTime) > 1000 * 60 * 5
+ ) {
+ throw new Error(`Form bot value invalid ${formBotValue}`);
+ }
+
+ formData.delete(formIdFieldName);
+ formData.delete(formBotFieldName);
+
+ if (resource) {
+ resource.headers.push({
+ name: "Content-Type",
+ value: "application/json",
+ });
+ resource.body = Object.fromEntries(formData);
+ } else {
+ if (contactEmail === undefined) {
+ throw new Error("Contact email not found");
+ }
+
+ resource = context.getDefaultActionResource?.({
+ url,
+ projectId,
+ contactEmail,
+ formData,
+ });
+ }
+
+ if (resource === undefined) {
+ throw Error("Resource not found");
+ }
+ const { ok, statusText } = await loadResource(fetch, resource);
+ if (ok) {
+ return { success: true };
+ }
+ return { success: false, errors: [statusText] };
+ } catch (error) {
+ console.error(error);
+
+ return {
+ success: false,
+ errors: [error instanceof Error ? error.message : "Unknown error"],
+ };
+ }
+};
+
+const Outlet = () => {
+ const { system, resources, url } = useLoaderData();
+ return (
+
+ {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}
+
+
+ );
+};
+
+export default Outlet;
diff --git a/fixtures/webstudio-remix-vercel/package.json b/fixtures/webstudio-remix-vercel/package.json
index 1bb4e18c703b..fc8170ad8af1 100644
--- a/fixtures/webstudio-remix-vercel/package.json
+++ b/fixtures/webstudio-remix-vercel/package.json
@@ -6,7 +6,7 @@
"typecheck": "tsc",
"cli": "NODE_OPTIONS='--conditions=webstudio --import=tsx' webstudio",
"fixtures:link": "pnpm cli link --link https://p-cddc1d44-af37-4cb6-a430-d300cf6f932d-dot-${BUILDER_HOST:-main.development.webstudio.is}'?authToken=1cdc6026-dd5b-4624-b89b-9bd45e9bcc3d'",
- "fixtures:sync": "pnpm cli sync --buildId 5646b5e6-a161-4fbc-8312-6e2852e1d4b3 && pnpm prettier --write ./.webstudio/",
+ "fixtures:sync": "pnpm cli sync --buildId 00bc4703-0722-4929-9647-fbe0ae091b94 && pnpm prettier --write ./.webstudio/",
"fixtures:build": "pnpm cli build --template vercel --template internal --preview && pnpm prettier --write ./app/ ./package.json ./tsconfig.json"
},
"private": true,
diff --git a/packages/react-sdk/src/component-generator.test.tsx b/packages/react-sdk/src/component-generator.test.tsx
index 6c5774eb2a2c..2500f1406848 100644
--- a/packages/react-sdk/src/component-generator.test.tsx
+++ b/packages/react-sdk/src/component-generator.test.tsx
@@ -1133,3 +1133,75 @@ test("variable names can be js identifiers", () => {
)
);
});
+
+test("Renders nothing if only templates are present in block", () => {
+ const Bt = ws["block-template"];
+
+ expect(
+ generateWebstudioComponent({
+ classesMap: new Map(),
+ scope: createScope(),
+ name: "Page",
+ rootInstanceId: "body",
+ parameters: [],
+ dataSources: new Map(),
+ indexesWithinAncestors: new Map(),
+ ...renderJsx(
+ <$.Body ws:id="body">
+
+
+ <$.Box>Test$.Box>
+
+
+ $.Body>
+ ),
+ })
+ ).toEqual(
+ validateJSX(
+ clear(`
+ const Page = () => {
+ return
+
+ }
+ `)
+ )
+ );
+});
+
+test("Renders only block children", () => {
+ const Bt = ws["block-template"];
+
+ expect(
+ generateWebstudioComponent({
+ classesMap: new Map(),
+ scope: createScope(),
+ name: "Page",
+ rootInstanceId: "body",
+ parameters: [],
+ dataSources: new Map(),
+ indexesWithinAncestors: new Map(),
+ ...renderJsx(
+ <$.Body ws:id="body">
+
+
+ <$.Box>Test$.Box>
+
+ <$.Box>Child0$.Box>
+
+ $.Body>
+ ),
+ })
+ ).toEqual(
+ validateJSX(
+ clear(`
+ const Page = () => {
+ return
+
+ {"Child0"}
+
+
+ }
+ `)
+ )
+ );
+});
diff --git a/packages/react-sdk/src/component-generator.ts b/packages/react-sdk/src/component-generator.ts
index 50f5fb9c2d7c..eadbf192f6b1 100644
--- a/packages/react-sdk/src/component-generator.ts
+++ b/packages/react-sdk/src/component-generator.ts
@@ -14,7 +14,12 @@ import {
transpileExpression,
} from "@webstudio-is/sdk";
import { indexAttribute, isAttributeNameSafe, showAttribute } from "./props";
-import { collectionComponent, descendantComponent } from "./core-components";
+import {
+ blockComponent,
+ blockTemplateComponent,
+ collectionComponent,
+ descendantComponent,
+} from "./core-components";
import type { IndexesWithinAncestors } from "./instance-utils";
/**
@@ -231,6 +236,10 @@ export const generateJsxElement = ({
}
let generatedElement = "";
+ if (instance.component === blockTemplateComponent) {
+ return "";
+ }
+
if (instance.component === collectionComponent) {
// prevent generating invalid collection
if (
@@ -246,6 +255,8 @@ export const generateJsxElement = ({
generatedElement += children;
generatedElement += `\n`;
generatedElement += `)}\n`;
+ } else if (instance.component === blockComponent) {
+ generatedElement += children;
} else {
const [_namespace, shortName] = parseComponentName(instance.component);
const componentVariable = scope.getName(instance.component, shortName);