Skip to content

Commit 36ba02d

Browse files
authored
feat: Add builderMode, add mode "edit" (#4376)
## Description Part 1 Of #3994 This PR introduces a feature flag, `contentEditableMode`, that hides the `EditorButton` in the top bar. This button is now intended for **testing purposes only** and will **never be used in production**. <img width="154" alt="image" src="https://github.com/user-attachments/assets/f4a37334-71b9-43d8-b4a8-afd518fc6833"> ## Most Notable Things In The Content Edit Mode 1. **Notion-like Editing**: Elements are editable with a single click. 2. **Restricted Element Editing**: Binded elements, such as expressions, are non-editable. 3. **Selective Property Editing**: Only properties for `img` and `link` elements are editable. Additional changes: - **Copy/Paste and Drag/Drop**: Disabled to maintain a controlled testing environment. Refer to the issue `Part 1` for further details on upcoming changes and planned enhancements. ## Steps for reproduction Play play ## Code Review - [ ] hi @kof, I need you to do - conceptual review (architecture, feature-correctness) - detailed review (read every line) - test it on preview ## Before requesting a review - [ ] made a self-review - [ ] added inline comments where things may be not obvious (the "why", not "what") ## Before merging - [ ] tested locally and on preview environment (preview dev login: 5de6) - [ ] updated [test cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md) document - [ ] added tests - [ ] if any new env variables are added, added them to `.env` file
1 parent f75a837 commit 36ba02d

File tree

30 files changed

+663
-198
lines changed

30 files changed

+663
-198
lines changed

apps/builder/app/builder/builder.tsx

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import {
2929
$publisherHost,
3030
$imageLoader,
3131
$textEditingInstanceSelector,
32+
$isDesignMode,
33+
$isContentMode,
3234
} from "~/shared/nano-states";
3335
import { $settings, type Settings } from "./shared/client-settings";
3436
import { builderUrl, getCanvasUrl } from "~/shared/router-utils";
@@ -57,8 +59,11 @@ import { updateWebstudioData } from "~/shared/instance-utils";
5759
import { migrateWebstudioDataMutable } from "~/shared/webstudio-data-migrator";
5860
import { Loading, LoadingBackground } from "./shared/loading";
5961
import { mergeRefs } from "@react-aria/utils";
60-
import { initCopyPaste } from "~/shared/copy-paste";
6162
import { CommandPanel } from "./features/command-panel";
63+
import {
64+
initCopyPaste,
65+
initCopyPasteForContentEditMode,
66+
} from "~/shared/copy-paste/init-copy-paste";
6267

6368
registerContainers();
6469

@@ -281,6 +286,9 @@ export const Builder = ({
281286
});
282287
const isCloneDialogOpen = useStore($isCloneDialogOpen);
283288
const isPreviewMode = useStore($isPreviewMode);
289+
const isDesignMode = useStore($isDesignMode);
290+
const isContentMode = useStore($isContentMode);
291+
284292
const { onRef: onRefReadCanvas, onTransitionEnd } = useReadCanvasRect();
285293

286294
useSetWindowTitle();
@@ -295,23 +303,33 @@ export const Builder = ({
295303

296304
useEffect(() => {
297305
const abortController = new AbortController();
298-
// We need to initialize this in both canvas and builder,
299-
// because the events will fire in either one, depending on where the focus is
300-
// @todo we need to forward the events from canvas to builder and avoid importing this
301-
// in both places
302-
initCopyPaste(abortController);
303306

307+
if (isDesignMode) {
308+
// We need to initialize this in both canvas and builder,
309+
// because the events will fire in either one, depending on where the focus is
310+
// @todo we need to forward the events from canvas to builder and avoid importing this
311+
// in both places
312+
initCopyPaste(abortController);
313+
}
314+
315+
if (isContentMode) {
316+
initCopyPasteForContentEditMode(abortController);
317+
}
318+
319+
return () => {
320+
abortController.abort();
321+
};
322+
}, [isContentMode, isDesignMode]);
323+
324+
useEffect(() => {
304325
const unsubscribe = $loadingState.subscribe((loadingState) => {
305326
setLoadingState(loadingState);
306327
// We need to stop updating it once it's ready in case in the future it changes again.
307328
if (loadingState.state === "ready") {
308329
unsubscribe();
309330
}
310331
});
311-
return () => {
312-
unsubscribe();
313-
abortController.abort();
314-
};
332+
return unsubscribe;
315333
}, []);
316334

317335
const canvasUrl = getCanvasUrl();
@@ -399,7 +417,8 @@ export const Builder = ({
399417
/>
400418
)}
401419
</Workspace>
402-
<AiCommandBar isPreviewMode={isPreviewMode} />
420+
421+
{isDesignMode && <AiCommandBar />}
403422
</Main>
404423
<SidePanel gridArea="sidebar">
405424
<SidebarLeft publish={publish} />

apps/builder/app/builder/features/ai/ai-command-bar.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ const initialPrompts = [
6161
"Create a testimonials section on 2 rows. The first row has a heading and subheading, the second row has 3 testimonial cards with an image, headline, description and link.",
6262
];
6363

64-
export const AiCommandBar = ({ isPreviewMode }: { isPreviewMode: boolean }) => {
64+
export const AiCommandBar = () => {
6565
const [value, setValue] = useState("");
6666
const [prompts, setPrompts] = useState<string[]>(initialPrompts);
6767
const isMenuOpen = getSetting("isAiMenuOpen");
@@ -167,10 +167,6 @@ export const AiCommandBar = ({ isPreviewMode }: { isPreviewMode: boolean }) => {
167167
},
168168
});
169169

170-
if (isPreviewMode) {
171-
return;
172-
}
173-
174170
const handleAiRequest = async (prompt: string) => {
175171
if (abortController.current) {
176172
if (abortController.current.signal.aborted === false) {

apps/builder/app/builder/features/breakpoints/breakpoints-popover.tsx

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
$styles,
2929
$selectedBreakpointId,
3030
$selectedBreakpoint,
31+
$isContentMode,
3132
} from "~/shared/nano-states";
3233
import {
3334
$breakpointsMenuView,
@@ -47,6 +48,7 @@ export const BreakpointsPopover = () => {
4748
const breakpoints = useStore($breakpoints);
4849
const selectedBreakpoint = useStore($selectedBreakpoint);
4950
const scale = useStore($scale);
51+
const isContentMode = useStore($isContentMode);
5052

5153
if (selectedBreakpoint === undefined) {
5254
return null;
@@ -183,18 +185,27 @@ export const BreakpointsPopover = () => {
183185
padding: theme.spacing[5],
184186
}}
185187
>
186-
<Button
187-
color="neutral"
188-
css={{ flexGrow: 1 }}
189-
onClick={(event) => {
190-
event.preventDefault();
191-
$breakpointsMenuView.set(
192-
view === "initial" ? "editor" : "initial"
193-
);
194-
}}
188+
<Tooltip
189+
content={
190+
isContentMode
191+
? "Editing is not allowed in content mode"
192+
: undefined
193+
}
195194
>
196-
{view === "editor" ? "Done" : "Edit breakpoints"}
197-
</Button>
195+
<Button
196+
color="neutral"
197+
css={{ flexGrow: 1 }}
198+
disabled={isContentMode}
199+
onClick={(event) => {
200+
event.preventDefault();
201+
$breakpointsMenuView.set(
202+
view === "initial" ? "editor" : "initial"
203+
);
204+
}}
205+
>
206+
{view === "editor" ? "Done" : "Edit breakpoints"}
207+
</Button>
208+
</Tooltip>
198209
</Flex>
199210
</>
200211
)}

apps/builder/app/builder/features/inspector/inspector.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { FloatingPanelProvider } from "~/builder/shared/floating-panel";
2424
import {
2525
$registeredComponentMetas,
2626
$dragAndDropState,
27+
$isDesignMode,
2728
} from "~/shared/nano-states";
2829
import { NavigatorTree } from "~/builder/features/navigator";
2930
import type { Settings } from "~/builder/shared/client-settings";
@@ -78,6 +79,7 @@ export const Inspector = ({ navigatorLayout }: InspectorProps) => {
7879
const metas = useStore($registeredComponentMetas);
7980
const selectedPage = useStore($selectedPage);
8081
const activeInspectorPanel = useStore($activeInspectorPanel);
82+
const isDesignMode = useStore($isDesignMode);
8183

8284
if (navigatorLayout === "docked" && isDragging) {
8385
return <NavigatorTree />;
@@ -100,7 +102,7 @@ export const Inspector = ({ navigatorLayout }: InspectorProps) => {
100102
type PanelName = "style" | "settings";
101103

102104
const availablePanels = new Set<PanelName>();
103-
if (documentType === "html" && (meta?.stylable ?? true)) {
105+
if (documentType === "html" && (meta?.stylable ?? true) && isDesignMode) {
104106
availablePanels.add("style");
105107
}
106108
// @todo hide root component settings until

apps/builder/app/builder/features/navigator/navigator-tree.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
$editingItemSelector,
3737
$hoveredInstanceSelector,
3838
$instances,
39+
$isContentMode,
3940
$props,
4041
$propsIndex,
4142
$propValuesByInstanceSelector,
@@ -404,6 +405,10 @@ const getBuilderDropTarget = (
404405
};
405406

406407
const canDrag = (instance: Instance) => {
408+
if ($isContentMode.get()) {
409+
return false;
410+
}
411+
407412
const meta = $registeredComponentMetas.get().get(instance.component);
408413
if (meta === undefined) {
409414
return true;

apps/builder/app/builder/features/navigator/navigator.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ import {
88
import { CrossIcon } from "@webstudio-is/icons";
99
import { CssPreview } from "./css-preview";
1010
import { NavigatorTree } from "./navigator-tree";
11+
import { $isDesignMode } from "~/shared/nano-states";
12+
import { useStore } from "@nanostores/react";
1113

1214
export const NavigatorPanel = ({ onClose }: { onClose: () => void }) => {
15+
const isDesignMode = useStore($isDesignMode);
1316
return (
1417
<>
1518
<PanelTitle
@@ -30,7 +33,7 @@ export const NavigatorPanel = ({ onClose }: { onClose: () => void }) => {
3033
<Flex grow direction="column" justify="end">
3134
<NavigatorTree />
3235
<Separator />
33-
<CssPreview />
36+
{isDesignMode && <CssPreview />}
3437
</Flex>
3538
</>
3639
);

apps/builder/app/builder/features/pages/pages.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
} from "@webstudio-is/icons";
2828
import { ExtendedPanel } from "../../shared/extended-sidebar-panel";
2929
import { NewPageSettings, PageSettings } from "./page-settings";
30-
import { $editingPageId, $pages } from "~/shared/nano-states";
30+
import { $editingPageId, $isContentMode, $pages } from "~/shared/nano-states";
3131
import {
3232
getAllChildrenAndSelf,
3333
reparentOrphansMutable,
@@ -334,6 +334,10 @@ const PagesTree = ({
334334
isLastChild={item.isLastChild}
335335
data={item}
336336
canDrag={() => {
337+
if ($isContentMode.get()) {
338+
return false;
339+
}
340+
337341
// forbid dragging home page
338342
if (item.id === pages.homePage.id) {
339343
toast.error("Home page cannot be moved");

apps/builder/app/builder/features/settings-panel/controls/text.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export const TextControl = ({
6464
onBlur={localValue.save}
6565
onSubmit={localValue.save}
6666
/>
67+
6768
<BindingPopover
6869
scope={scope}
6970
aliases={aliases}

apps/builder/app/builder/features/settings-panel/props-section/props-section.tsx

Lines changed: 45 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
Separator,
1010
Flex,
1111
Box,
12+
Grid,
1213
} from "@webstudio-is/design-system";
1314
import {
1415
descendantComponent,
@@ -18,11 +19,12 @@ import {
1819
$propValuesByInstanceSelector,
1920
$propsIndex,
2021
$props,
22+
$isDesignMode,
23+
$isContentMode,
2124
} from "~/shared/nano-states";
2225
import { CollapsibleSectionWithAddButton } from "~/builder/shared/collapsible-section";
2326
import { renderControl } from "../controls/combined";
2427
import { usePropsLogic, type PropAndMeta } from "./use-props-logic";
25-
import { Row } from "../shared";
2628
import { serverSyncStore } from "~/shared/sync";
2729
import { $selectedInstanceKey } from "~/shared/awareness";
2830

@@ -155,41 +157,57 @@ type PropsSectionProps = {
155157
// A UI componet with minimum logic that can be demoed in Storybook etc.
156158
export const PropsSection = (props: PropsSectionProps) => {
157159
const { propsLogic: logic } = props;
158-
159160
const [addingProp, setAddingProp] = useState(false);
161+
const isDesignMode = useStore($isDesignMode);
162+
const isContentMode = useStore($isContentMode);
160163

161164
const hasItems =
162165
logic.addedProps.length > 0 || addingProp || logic.initialProps.length > 0;
163166

167+
const showPropertiesSection =
168+
isDesignMode || (isContentMode && logic.initialProps.length > 0);
169+
164170
return (
165171
<>
166-
<Row css={{ py: theme.panel.paddingBlock }}>
167-
{logic.systemProps.map((item) => renderProperty(props, item))}
168-
</Row>
172+
<Grid
173+
css={{
174+
paddingBottom: theme.panel.paddingBlock,
175+
}}
176+
>
177+
{logic.systemProps.map((item) => (
178+
<Box
179+
key={item.propName}
180+
css={{ paddingInline: theme.panel.paddingInline }}
181+
>
182+
{renderProperty(props, item)}
183+
</Box>
184+
))}
185+
</Grid>
169186

170187
<Separator />
171-
172-
<CollapsibleSectionWithAddButton
173-
label="Properties & Attributes"
174-
onAdd={() => setAddingProp(true)}
175-
hasItems={hasItems}
176-
>
177-
<Flex gap="1" direction="column">
178-
{addingProp && (
179-
<AddPropertyOrAttribute
180-
availableProps={logic.availableProps}
181-
onPropSelected={(propName) => {
182-
setAddingProp(false);
183-
logic.handleAdd(propName);
184-
}}
185-
/>
186-
)}
187-
{logic.addedProps.map((item) =>
188-
renderProperty(props, item, { deletable: true })
189-
)}
190-
{logic.initialProps.map((item) => renderProperty(props, item))}
191-
</Flex>
192-
</CollapsibleSectionWithAddButton>
188+
{showPropertiesSection && (
189+
<CollapsibleSectionWithAddButton
190+
label="Properties & Attributes"
191+
onAdd={isDesignMode ? () => setAddingProp(true) : undefined}
192+
hasItems={hasItems}
193+
>
194+
<Flex gap="1" direction="column">
195+
{addingProp && (
196+
<AddPropertyOrAttribute
197+
availableProps={logic.availableProps}
198+
onPropSelected={(propName) => {
199+
setAddingProp(false);
200+
logic.handleAdd(propName);
201+
}}
202+
/>
203+
)}
204+
{logic.addedProps.map((item) =>
205+
renderProperty(props, item, { deletable: true })
206+
)}
207+
{logic.initialProps.map((item) => renderProperty(props, item))}
208+
</Flex>
209+
</CollapsibleSectionWithAddButton>
210+
)}
193211
</>
194212
);
195213
};

0 commit comments

Comments
 (0)