Skip to content

Commit

Permalink
feat: Add new button mode switch button (#4532)
Browse files Browse the repository at this point in the history
## 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]>
  • Loading branch information
istarkov and kof authored Dec 8, 2024
1 parent c3e0a6a commit 0dbdbe6
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 95 deletions.
161 changes: 90 additions & 71 deletions apps/builder/app/builder/features/topbar/builder-mode.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useStore } from "@nanostores/react";
import {
ChevronDownIcon,
NotebookAndPenIcon,
PaintBrushIcon,
PlayIcon,
Expand All @@ -12,10 +13,13 @@ import {
DropdownMenuSeparator,
Flex,
Kbd,
MenuCheckedIcon,
menuItemCss,
styled,
theme,
ToolbarToggleGroup,
ToolbarToggleItem,
Tooltip,
Text,
} from "@webstudio-is/design-system";
import {
DropdownMenu,
Expand All @@ -29,17 +33,11 @@ import {
$isDesignModeAllowed,
isBuilderMode,
setBuilderMode,
type BuilderMode,
toggleBuilderMode,
} from "~/shared/nano-states";
import { useState } from "react";
import { isFeatureEnabled } from "@webstudio-is/feature-flags";

const StyledMenuItem = styled(DropdownMenuRadioItem, {
"&:where([data-state='checked'])": {
backgroundColor: `oklch(from ${theme.colors.backgroundItemMenuItemHover} l c h / 0.3)`,
},
});

export const BuilderModeDropDown = () => {
const builderMode = useStore($builderMode);
const isContentModeAllowed = useStore($isContentModeAllowed);
Expand All @@ -60,18 +58,13 @@ export const BuilderModeDropDown = () => {
shortcut: ["cmd", "shift", "c"],
enabled: isContentModeAllowed && isFeatureEnabled("contentEditableMode"),
},
preview: {
icon: <PlayIcon />,
description: "View the page as it will appear to users",
title: "Preview",
shortcut: ["cmd", "shift", "p"],
enabled: true,
},
} as const;

const [activeMode, setActiveMode] = useState<BuilderMode | undefined>();
const [activeMode, setActiveMode] = useState<
keyof typeof menuItems | undefined
>();

const handleFocus = (mode: BuilderMode) => () => {
const handleFocus = (mode: keyof typeof menuItems) => () => {
setActiveMode(mode);
};

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

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<ToolbarToggleItem
value="preview"
aria-label="Toggle Preview"
variant="preview"
tabIndex={0}
>
{menuItems[builderMode].icon}
</ToolbarToggleItem>
</DropdownMenuTrigger>

<DropdownMenuPortal>
<DropdownMenuContent
sideOffset={4}
collisionPadding={16}
side="bottom"
loop
<Flex align="center">
<Tooltip
content={
<Flex gap="1">
<Text variant="regular">Toggle preview</Text>
<Kbd value={["cmd", "shift", "p"]} />
</Flex>
}
>
<ToolbarToggleGroup
type="single"
value={builderMode}
onValueChange={() => {
toggleBuilderMode("preview");
}}
>
<DropdownMenuRadioGroup
value={builderMode}
onValueChange={(value) => {
if (isBuilderMode(value)) {
setBuilderMode(value);
}
}}
<ToolbarToggleItem variant="preview" value="preview">
<PlayIcon />
</ToolbarToggleItem>
</ToolbarToggleGroup>
</Tooltip>
<DropdownMenu>
<Tooltip content={"Choose mode"}>
<DropdownMenuTrigger asChild>
<ToolbarToggleItem
tabIndex={0}
aria-label="Choose mode"
variant="chevron"
value="chevron"
>
<ChevronDownIcon />
</ToolbarToggleItem>
</DropdownMenuTrigger>
</Tooltip>
<DropdownMenuPortal>
<DropdownMenuContent
sideOffset={4}
collisionPadding={16}
side="bottom"
loop
>
{Object.entries(menuItems)
.filter(([_, { enabled }]) => enabled)
.map(([mode, { icon, title, shortcut }]) => (
<StyledMenuItem
key={mode}
value={mode}
onFocus={handleFocus(mode as BuilderMode)}
onBlur={handleBlur}
>
<Flex css={{ px: theme.spacing[3] }} gap={2}>
{icon}
<Box>{title}</Box>
</Flex>
<DropdownMenuItemRightSlot>
<Kbd value={shortcut} />
</DropdownMenuItemRightSlot>
&nbsp;
</StyledMenuItem>
))}
</DropdownMenuRadioGroup>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup
value={builderMode}
onValueChange={(value) => {
if (isBuilderMode(value)) {
setBuilderMode(value);
}
}}
>
{Object.entries(menuItems)
.filter(([_, { enabled }]) => enabled)
.map(([mode, { icon, title, shortcut }]) => (
<DropdownMenuRadioItem
key={mode}
value={mode}
onFocus={handleFocus(mode as keyof typeof menuItems)}
onBlur={handleBlur}
icon={<MenuCheckedIcon />}
>
<Flex css={{ px: theme.spacing[3] }} gap={2}>
{icon}
<Box>{title}</Box>
</Flex>
<DropdownMenuItemRightSlot>
<Kbd value={shortcut} />
</DropdownMenuItemRightSlot>
&nbsp;
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
<DropdownMenuSeparator />

<div className={menuItemCss({ hint: true })}>
<Box css={{ width: theme.spacing[25] }}>
{menuItems[activeMode ?? builderMode].description}
</Box>
</div>
</DropdownMenuContent>
</DropdownMenuPortal>
</DropdownMenu>
<div className={menuItemCss({ hint: true })}>
<Box css={{ width: theme.spacing[25] }}>
{activeMode
? menuItems[activeMode].description
: "Select Design or Content mode"}
</Box>
</div>
</DropdownMenuContent>
</DropdownMenuPortal>
</DropdownMenu>
</Flex>
);
};
57 changes: 36 additions & 21 deletions apps/builder/app/shared/nano-states/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,37 +353,46 @@ export const $isDesignModeAllowed = computed([$authPermit], (authPermit) => {
return authPermit !== "edit";
});

let previousBuilderMode: BuilderMode | undefined = undefined;
let lastEditableBuilderMode: Exclude<BuilderMode, "preview"> | undefined =
undefined;

const getNextEditableMode = (): "design" | "content" => {
if (lastEditableBuilderMode === undefined) {
if ($isDesignModeAllowed.get()) {
return "design";
}

return "content";
}
return lastEditableBuilderMode;
};

/**
* - preview, preview -> 'last known editable mode i.e. design or content' ?? 'default editable mode'
* - preview, design -> design
* - preview, content -> content
*
* - design, design -> preview
* - design, preview -> preview
* - design, content -> content
*
* - content, content -> preview
* - content, preview -> preview
* - content, design -> design
*/
export const toggleBuilderMode = (mode: BuilderMode) => {
const currentMode = $builderMode.get();

if (currentMode === mode) {
if (previousBuilderMode !== undefined) {
setBuilderMode(previousBuilderMode);
previousBuilderMode = currentMode;
if (mode === "preview") {
setBuilderMode(getNextEditableMode());
return;
}

// Switch back
const availableModes: BuilderMode[] = [];
if ($isDesignModeAllowed.get() && currentMode !== "design") {
availableModes.push("design");
}
if ($isContentModeAllowed.get() && currentMode !== "content") {
availableModes.push("content");
}
if (currentMode !== "preview") {
availableModes.push("preview");
}

setBuilderMode(availableModes[0] ?? "preview");

previousBuilderMode = currentMode;
setBuilderMode("preview");
return;
}

previousBuilderMode = currentMode;
setBuilderMode(mode);
};

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

$builderMode.set("content");
lastEditableBuilderMode = "content";
return;
}

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

$builderMode.set(mode ?? defaultMode);
const nextMode = mode ?? defaultMode;

$builderMode.set(nextMode);
if (nextMode !== "preview") {
lastEditableBuilderMode = nextMode;
}
};

export const $toastErrors = atom<string[]>([]);
Expand Down
5 changes: 3 additions & 2 deletions packages/design-system/src/components/focus-ring.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { theme } from "../stitches.config";
import { theme, type CSS } from "../stitches.config";

export const focusRingStyle = () => ({
export const focusRingStyle = (props?: CSS) => ({
"&::after": {
content: '""',
position: "absolute",
Expand All @@ -10,5 +10,6 @@ export const focusRingStyle = () => ({
outlineColor: theme.colors.borderFocus,
borderRadius: theme.borderRadius[3],
pointerEvents: "none",
...props,
},
});
5 changes: 4 additions & 1 deletion packages/design-system/src/components/toolbar.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PlayIcon } from "@webstudio-is/icons";
import { PlayIcon, ChevronDownIcon } from "@webstudio-is/icons";
import { theme } from "../stitches.config";
import {
Toolbar,
Expand Down Expand Up @@ -36,6 +36,9 @@ const ToolbarStory = () => {
<ToolbarToggleItem value="5" focused>
<PlayIcon size={22} />
</ToolbarToggleItem>
<ToolbarToggleItem value="5" variant="chevron">
<ChevronDownIcon />
</ToolbarToggleItem>
</ToolbarToggleGroup>
</Toolbar>
);
Expand Down
9 changes: 9 additions & 0 deletions packages/design-system/src/components/toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ const toggleItemStyle = css(textVariants.labelsTitleCase, {
color: theme.colors.foregroundSuccess,
},
},
chevron: {
minWidth: "auto",
paddingInline: 0,
color: theme.colors.foregroundContrastSubtle,
"&:hover, &:focus-visible, &[aria-expanded=true]": {
color: theme.colors.foregroundContrastMain,
},
"&:focus-visible": focusRingStyle({ left: 0, right: 0 }),
},
},
},
});
Expand Down

0 comments on commit 0dbdbe6

Please sign in to comment.