Skip to content

Commit 0dbdbe6

Browse files
istarkovkof
andauthored
feat: Add new button mode switch button (#4532)
## Description ref #3994 <img width="256" alt="image" src="https://github.com/user-attachments/assets/18e74c3a-9bc9-4de0-aae3-df943ba708a4"> <img width="223" alt="image" src="https://github.com/user-attachments/assets/82c65222-c07f-406e-bdce-a208ae898b27"> <img width="223" alt="image" src="https://github.com/user-attachments/assets/262fc4f0-0a33-49f3-aa7b-656f4ae8d849"> <img width="223" alt="image" src="https://github.com/user-attachments/assets/552620a1-1259-4ed0-9a60-03d4c24b98b1"> ## Steps for reproduction 1. click button 2. expect xyz ## 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: 0000) - [ ] 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 --------- Co-authored-by: Oleg Isonen <[email protected]>
1 parent c3e0a6a commit 0dbdbe6

File tree

5 files changed

+142
-95
lines changed

5 files changed

+142
-95
lines changed
Lines changed: 90 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useStore } from "@nanostores/react";
22
import {
3+
ChevronDownIcon,
34
NotebookAndPenIcon,
45
PaintBrushIcon,
56
PlayIcon,
@@ -12,10 +13,13 @@ import {
1213
DropdownMenuSeparator,
1314
Flex,
1415
Kbd,
16+
MenuCheckedIcon,
1517
menuItemCss,
16-
styled,
1718
theme,
19+
ToolbarToggleGroup,
1820
ToolbarToggleItem,
21+
Tooltip,
22+
Text,
1923
} from "@webstudio-is/design-system";
2024
import {
2125
DropdownMenu,
@@ -29,17 +33,11 @@ import {
2933
$isDesignModeAllowed,
3034
isBuilderMode,
3135
setBuilderMode,
32-
type BuilderMode,
36+
toggleBuilderMode,
3337
} from "~/shared/nano-states";
3438
import { useState } from "react";
3539
import { isFeatureEnabled } from "@webstudio-is/feature-flags";
3640

37-
const StyledMenuItem = styled(DropdownMenuRadioItem, {
38-
"&:where([data-state='checked'])": {
39-
backgroundColor: `oklch(from ${theme.colors.backgroundItemMenuItemHover} l c h / 0.3)`,
40-
},
41-
});
42-
4341
export const BuilderModeDropDown = () => {
4442
const builderMode = useStore($builderMode);
4543
const isContentModeAllowed = useStore($isContentModeAllowed);
@@ -60,18 +58,13 @@ export const BuilderModeDropDown = () => {
6058
shortcut: ["cmd", "shift", "c"],
6159
enabled: isContentModeAllowed && isFeatureEnabled("contentEditableMode"),
6260
},
63-
preview: {
64-
icon: <PlayIcon />,
65-
description: "View the page as it will appear to users",
66-
title: "Preview",
67-
shortcut: ["cmd", "shift", "p"],
68-
enabled: true,
69-
},
7061
} as const;
7162

72-
const [activeMode, setActiveMode] = useState<BuilderMode | undefined>();
63+
const [activeMode, setActiveMode] = useState<
64+
keyof typeof menuItems | undefined
65+
>();
7366

74-
const handleFocus = (mode: BuilderMode) => () => {
67+
const handleFocus = (mode: keyof typeof menuItems) => () => {
7568
setActiveMode(mode);
7669
};
7770

@@ -80,62 +73,88 @@ export const BuilderModeDropDown = () => {
8073
};
8174

8275
return (
83-
<DropdownMenu>
84-
<DropdownMenuTrigger asChild>
85-
<ToolbarToggleItem
86-
value="preview"
87-
aria-label="Toggle Preview"
88-
variant="preview"
89-
tabIndex={0}
90-
>
91-
{menuItems[builderMode].icon}
92-
</ToolbarToggleItem>
93-
</DropdownMenuTrigger>
94-
95-
<DropdownMenuPortal>
96-
<DropdownMenuContent
97-
sideOffset={4}
98-
collisionPadding={16}
99-
side="bottom"
100-
loop
76+
<Flex align="center">
77+
<Tooltip
78+
content={
79+
<Flex gap="1">
80+
<Text variant="regular">Toggle preview</Text>
81+
<Kbd value={["cmd", "shift", "p"]} />
82+
</Flex>
83+
}
84+
>
85+
<ToolbarToggleGroup
86+
type="single"
87+
value={builderMode}
88+
onValueChange={() => {
89+
toggleBuilderMode("preview");
90+
}}
10191
>
102-
<DropdownMenuRadioGroup
103-
value={builderMode}
104-
onValueChange={(value) => {
105-
if (isBuilderMode(value)) {
106-
setBuilderMode(value);
107-
}
108-
}}
92+
<ToolbarToggleItem variant="preview" value="preview">
93+
<PlayIcon />
94+
</ToolbarToggleItem>
95+
</ToolbarToggleGroup>
96+
</Tooltip>
97+
<DropdownMenu>
98+
<Tooltip content={"Choose mode"}>
99+
<DropdownMenuTrigger asChild>
100+
<ToolbarToggleItem
101+
tabIndex={0}
102+
aria-label="Choose mode"
103+
variant="chevron"
104+
value="chevron"
105+
>
106+
<ChevronDownIcon />
107+
</ToolbarToggleItem>
108+
</DropdownMenuTrigger>
109+
</Tooltip>
110+
<DropdownMenuPortal>
111+
<DropdownMenuContent
112+
sideOffset={4}
113+
collisionPadding={16}
114+
side="bottom"
115+
loop
109116
>
110-
{Object.entries(menuItems)
111-
.filter(([_, { enabled }]) => enabled)
112-
.map(([mode, { icon, title, shortcut }]) => (
113-
<StyledMenuItem
114-
key={mode}
115-
value={mode}
116-
onFocus={handleFocus(mode as BuilderMode)}
117-
onBlur={handleBlur}
118-
>
119-
<Flex css={{ px: theme.spacing[3] }} gap={2}>
120-
{icon}
121-
<Box>{title}</Box>
122-
</Flex>
123-
<DropdownMenuItemRightSlot>
124-
<Kbd value={shortcut} />
125-
</DropdownMenuItemRightSlot>
126-
&nbsp;
127-
</StyledMenuItem>
128-
))}
129-
</DropdownMenuRadioGroup>
130-
<DropdownMenuSeparator />
117+
<DropdownMenuRadioGroup
118+
value={builderMode}
119+
onValueChange={(value) => {
120+
if (isBuilderMode(value)) {
121+
setBuilderMode(value);
122+
}
123+
}}
124+
>
125+
{Object.entries(menuItems)
126+
.filter(([_, { enabled }]) => enabled)
127+
.map(([mode, { icon, title, shortcut }]) => (
128+
<DropdownMenuRadioItem
129+
key={mode}
130+
value={mode}
131+
onFocus={handleFocus(mode as keyof typeof menuItems)}
132+
onBlur={handleBlur}
133+
icon={<MenuCheckedIcon />}
134+
>
135+
<Flex css={{ px: theme.spacing[3] }} gap={2}>
136+
{icon}
137+
<Box>{title}</Box>
138+
</Flex>
139+
<DropdownMenuItemRightSlot>
140+
<Kbd value={shortcut} />
141+
</DropdownMenuItemRightSlot>
142+
&nbsp;
143+
</DropdownMenuRadioItem>
144+
))}
145+
</DropdownMenuRadioGroup>
146+
<DropdownMenuSeparator />
131147

132-
<div className={menuItemCss({ hint: true })}>
133-
<Box css={{ width: theme.spacing[25] }}>
134-
{menuItems[activeMode ?? builderMode].description}
135-
</Box>
136-
</div>
137-
</DropdownMenuContent>
138-
</DropdownMenuPortal>
139-
</DropdownMenu>
148+
<div className={menuItemCss({ hint: true })}>
149+
<Box css={{ width: theme.spacing[25] }}>
150+
{activeMode
151+
? menuItems[activeMode].description
152+
: "Select Design or Content mode"}
153+
</Box>
154+
</div>
155+
</DropdownMenuContent>
156+
</DropdownMenuPortal>
157+
</DropdownMenu>
158+
</Flex>
140159
);
141160
};

apps/builder/app/shared/nano-states/misc.ts

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -353,37 +353,46 @@ export const $isDesignModeAllowed = computed([$authPermit], (authPermit) => {
353353
return authPermit !== "edit";
354354
});
355355

356-
let previousBuilderMode: BuilderMode | undefined = undefined;
356+
let lastEditableBuilderMode: Exclude<BuilderMode, "preview"> | undefined =
357+
undefined;
357358

359+
const getNextEditableMode = (): "design" | "content" => {
360+
if (lastEditableBuilderMode === undefined) {
361+
if ($isDesignModeAllowed.get()) {
362+
return "design";
363+
}
364+
365+
return "content";
366+
}
367+
return lastEditableBuilderMode;
368+
};
369+
370+
/**
371+
* - preview, preview -> 'last known editable mode i.e. design or content' ?? 'default editable mode'
372+
* - preview, design -> design
373+
* - preview, content -> content
374+
*
375+
* - design, design -> preview
376+
* - design, preview -> preview
377+
* - design, content -> content
378+
*
379+
* - content, content -> preview
380+
* - content, preview -> preview
381+
* - content, design -> design
382+
*/
358383
export const toggleBuilderMode = (mode: BuilderMode) => {
359384
const currentMode = $builderMode.get();
360385

361386
if (currentMode === mode) {
362-
if (previousBuilderMode !== undefined) {
363-
setBuilderMode(previousBuilderMode);
364-
previousBuilderMode = currentMode;
387+
if (mode === "preview") {
388+
setBuilderMode(getNextEditableMode());
365389
return;
366390
}
367391

368-
// Switch back
369-
const availableModes: BuilderMode[] = [];
370-
if ($isDesignModeAllowed.get() && currentMode !== "design") {
371-
availableModes.push("design");
372-
}
373-
if ($isContentModeAllowed.get() && currentMode !== "content") {
374-
availableModes.push("content");
375-
}
376-
if (currentMode !== "preview") {
377-
availableModes.push("preview");
378-
}
379-
380-
setBuilderMode(availableModes[0] ?? "preview");
381-
382-
previousBuilderMode = currentMode;
392+
setBuilderMode("preview");
383393
return;
384394
}
385395

386-
previousBuilderMode = currentMode;
387396
setBuilderMode(mode);
388397
};
389398

@@ -402,6 +411,7 @@ export const setBuilderMode = (mode: BuilderMode | null) => {
402411
toast.info("Design mode is not available for content edit links.");
403412

404413
$builderMode.set("content");
414+
lastEditableBuilderMode = "content";
405415
return;
406416
}
407417

@@ -411,7 +421,12 @@ export const setBuilderMode = (mode: BuilderMode | null) => {
411421
? "content"
412422
: "preview";
413423

414-
$builderMode.set(mode ?? defaultMode);
424+
const nextMode = mode ?? defaultMode;
425+
426+
$builderMode.set(nextMode);
427+
if (nextMode !== "preview") {
428+
lastEditableBuilderMode = nextMode;
429+
}
415430
};
416431

417432
export const $toastErrors = atom<string[]>([]);
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { theme } from "../stitches.config";
1+
import { theme, type CSS } from "../stitches.config";
22

3-
export const focusRingStyle = () => ({
3+
export const focusRingStyle = (props?: CSS) => ({
44
"&::after": {
55
content: '""',
66
position: "absolute",
@@ -10,5 +10,6 @@ export const focusRingStyle = () => ({
1010
outlineColor: theme.colors.borderFocus,
1111
borderRadius: theme.borderRadius[3],
1212
pointerEvents: "none",
13+
...props,
1314
},
1415
});

packages/design-system/src/components/toolbar.stories.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { PlayIcon } from "@webstudio-is/icons";
1+
import { PlayIcon, ChevronDownIcon } from "@webstudio-is/icons";
22
import { theme } from "../stitches.config";
33
import {
44
Toolbar,
@@ -36,6 +36,9 @@ const ToolbarStory = () => {
3636
<ToolbarToggleItem value="5" focused>
3737
<PlayIcon size={22} />
3838
</ToolbarToggleItem>
39+
<ToolbarToggleItem value="5" variant="chevron">
40+
<ChevronDownIcon />
41+
</ToolbarToggleItem>
3942
</ToolbarToggleGroup>
4043
</Toolbar>
4144
);

packages/design-system/src/components/toolbar.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,15 @@ const toggleItemStyle = css(textVariants.labelsTitleCase, {
7070
color: theme.colors.foregroundSuccess,
7171
},
7272
},
73+
chevron: {
74+
minWidth: "auto",
75+
paddingInline: 0,
76+
color: theme.colors.foregroundContrastSubtle,
77+
"&:hover, &:focus-visible, &[aria-expanded=true]": {
78+
color: theme.colors.foregroundContrastMain,
79+
},
80+
"&:focus-visible": focusRingStyle({ left: 0, right: 0 }),
81+
},
7382
},
7483
},
7584
});

0 commit comments

Comments
 (0)