diff --git a/apps/builder/app/builder/features/inspector/inspector.tsx b/apps/builder/app/builder/features/inspector/inspector.tsx
index 45e8dc8c59..e823a04456 100644
--- a/apps/builder/app/builder/features/inspector/inspector.tsx
+++ b/apps/builder/app/builder/features/inspector/inspector.tsx
@@ -19,7 +19,7 @@ import {
Kbd,
FloatingPanelProvider,
} from "@webstudio-is/design-system";
-import { StylePanel } from "~/builder/features/style-panel";
+import { ModeMenu, StylePanel } from "~/builder/features/style-panel";
import { SettingsPanelContainer } from "~/builder/features/settings-panel";
import {
$registeredComponentMetas,
@@ -33,6 +33,7 @@ import { getInstanceLabel } from "~/shared/instance-utils";
import { BindingPopoverProvider } from "~/builder/shared/binding-popover";
import { $activeInspectorPanel } from "~/builder/shared/nano-states";
import { $selectedInstance, $selectedPage } from "~/shared/awareness";
+import { isFeatureEnabled } from "@webstudio-is/feature-flags";
const InstanceInfo = ({ instance }: { instance: Instance }) => {
const metas = useStore($registeredComponentMetas);
@@ -42,16 +43,7 @@ const InstanceInfo = ({ instance }: { instance: Instance }) => {
}
const label = getInstanceLabel(instance, componentMeta);
return (
-
+
{label}
@@ -175,7 +167,18 @@ export const Inspector = ({ navigatorLayout }: InspectorProps) => {
-
+
+
+ {isFeatureEnabled("stylePanelModes") && }
+
{
tabIndex={-1}
>
-
+
+
+
{
);
};
+const label = "Data Variables";
+
export const VariablesSection = () => {
const containerRef = useRef(null);
- const [isOpen, setIsOpen] = useOpenState({ label: "variables" });
+ const [isOpen, setIsOpen] = useOpenState(label);
return (
}
- suffix={}
+ suffix={}
css={{ gridTemplateColumns: "1fr max-content 1fr" }}
onClick={onReset}
>
diff --git a/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx b/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx
index fe38c2277c..3eb13d1b07 100644
--- a/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx
+++ b/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx
@@ -83,7 +83,7 @@ const AdvancedStyleSection = (props: {
children: ReactNode;
}) => {
const { label, children, properties, onAdd } = props;
- const [isOpen, setIsOpen] = useOpenState(props);
+ const [isOpen, setIsOpen] = useOpenState(label);
const styles = useComputedStyles(properties);
return (
{
};
export const Section = () => {
- const [isOpen, setIsOpen] = useState(true);
+ const [isOpen, setIsOpen] = useOpenState(label);
const styles = useComputedStyles(properties);
const isAnyTransformPropertyAdded = transformPanels.some((panel) =>
diff --git a/apps/builder/app/builder/features/style-panel/sections/transitions/transitions.tsx b/apps/builder/app/builder/features/style-panel/sections/transitions/transitions.tsx
index 216cd9cf63..45516aacf6 100644
--- a/apps/builder/app/builder/features/style-panel/sections/transitions/transitions.tsx
+++ b/apps/builder/app/builder/features/style-panel/sections/transitions/transitions.tsx
@@ -1,4 +1,3 @@
-import { useState } from "react";
import { useStore } from "@nanostores/react";
import { PlusIcon } from "@webstudio-is/icons";
import {
@@ -12,7 +11,10 @@ import {
type LayerValueItem,
type StyleProperty,
} from "@webstudio-is/css-engine";
-import { CollapsibleSectionRoot } from "~/builder/shared/collapsible-section";
+import {
+ CollapsibleSectionRoot,
+ useOpenState,
+} from "~/builder/shared/collapsible-section";
import { $selectedOrLastStyleSourceSelector } from "~/shared/nano-states";
import { humanizeString } from "~/shared/string-utils";
import { repeatUntil } from "~/shared/array-utils";
@@ -83,7 +85,7 @@ const getLayerLabel = ({
};
export const Section = () => {
- const [isOpen, setIsOpen] = useState(true);
+ const [isOpen, setIsOpen] = useOpenState(label);
const selectedOrLastStyleSourceSelector = useStore(
$selectedOrLastStyleSourceSelector
diff --git a/apps/builder/app/builder/features/style-panel/shared/style-section.tsx b/apps/builder/app/builder/features/style-panel/shared/style-section.tsx
index 314d759aaa..7b161661fb 100644
--- a/apps/builder/app/builder/features/style-panel/shared/style-section.tsx
+++ b/apps/builder/app/builder/features/style-panel/shared/style-section.tsx
@@ -41,11 +41,9 @@ export const StyleSection = (props: {
// @todo remove to keep sections consistent
fullWidth?: boolean;
children: ReactNode;
- accordion?: string;
- initialOpen?: string;
}) => {
const { label, children, properties, fullWidth } = props;
- const [isOpen, setIsOpen] = useOpenState(props);
+ const [isOpen, setIsOpen] = useOpenState(label);
const styles = useComputedStyles(properties);
return (
{
const { label, description, children, properties, onAdd, collapsible } =
props;
- const [isOpen, setIsOpen] = useOpenState(props);
+ const [isOpen, setIsOpen] = useOpenState(label);
const styles = useComputedStyles(properties);
const dots = getDots(styles);
diff --git a/apps/builder/app/builder/features/style-panel/style-panel.tsx b/apps/builder/app/builder/features/style-panel/style-panel.tsx
index be36c6d753..f4fd0aeae3 100644
--- a/apps/builder/app/builder/features/style-panel/style-panel.tsx
+++ b/apps/builder/app/builder/features/style-panel/style-panel.tsx
@@ -5,6 +5,18 @@ import {
Text,
Separator,
ScrollArea,
+ DropdownMenu,
+ DropdownMenuTrigger,
+ IconButton,
+ DropdownMenuContent,
+ DropdownMenuRadioItem,
+ MenuCheckedIcon,
+ DropdownMenuRadioGroup,
+ rawTheme,
+ Kbd,
+ Flex,
+ DropdownMenuSeparator,
+ DropdownMenuItem,
} from "@webstudio-is/design-system";
import { useStore } from "@nanostores/react";
import { computed } from "nanostores";
@@ -14,8 +26,15 @@ import { sections } from "./sections";
import { toValue } from "@webstudio-is/css-engine";
import { $instanceTags, useParentComputedStyleDecl } from "./shared/model";
import { $selectedInstance } from "~/shared/awareness";
-import { CollapsibleSectionContext } from "~/builder/shared/collapsible-section";
-import { isFeatureEnabled } from "@webstudio-is/feature-flags";
+import { CollapsibleProvider } from "~/builder/shared/collapsible-section";
+import { EllipsesIcon } from "@webstudio-is/icons";
+import {
+ $settings,
+ getSetting,
+ setSetting,
+ type Settings,
+} from "~/builder/shared/client-settings";
+import { useState } from "react";
const $selectedInstanceTag = computed(
[$selectedInstance, $instanceTags],
@@ -27,7 +46,67 @@ const $selectedInstanceTag = computed(
}
);
+export const ModeMenu = () => {
+ const value = getSetting("stylePanelMode");
+ const [focusedValue, setFocusedValue] = useState(value);
+
+ return (
+
+
+
+
+
+
+
+ {
+ setSetting("stylePanelMode", value as Settings["stylePanelMode"]);
+ }}
+ >
+ }
+ onFocus={() => setFocusedValue("default")}
+ >
+ Default
+
+ }
+ onFocus={() => setFocusedValue("focus")}
+ >
+
+ Focus mode
+
+
+ {/*
+ }>
+ Advanced mode
+ */}
+
+
+
+ {focusedValue === "default" && (
+
+ All sections are open by default.
+
+ )}
+ {focusedValue === "focus" && (
+
+ Only one section is open at a time.
+
+ )}
+
+
+ );
+};
+
export const StylePanel = () => {
+ const { stylePanelMode } = useStore($settings);
const selectedInstanceRenderState = useStore($selectedInstanceRenderState);
const tag = useStore($selectedInstanceTag);
const display = useParentComputedStyleDecl("display");
@@ -75,14 +154,12 @@ export const StylePanel = () => {
-
{all}
-
+
>
);
diff --git a/apps/builder/app/builder/features/topbar/topbar.tsx b/apps/builder/app/builder/features/topbar/topbar.tsx
index 2015df68f4..b19fbaf033 100644
--- a/apps/builder/app/builder/features/topbar/topbar.tsx
+++ b/apps/builder/app/builder/features/topbar/topbar.tsx
@@ -41,7 +41,7 @@ const PagesButton = () => {
content={
{"Pages or page settings "}
-
+
}
>
diff --git a/apps/builder/app/builder/features/workspace/canvas-tools/outline/block-instance-outline.tsx b/apps/builder/app/builder/features/workspace/canvas-tools/outline/block-instance-outline.tsx
index 241c96b400..66f797bf78 100644
--- a/apps/builder/app/builder/features/workspace/canvas-tools/outline/block-instance-outline.tsx
+++ b/apps/builder/app/builder/features/workspace/canvas-tools/outline/block-instance-outline.tsx
@@ -203,7 +203,7 @@ export const TemplatesMenu = ({
display: hasChildren ? undefined : "none",
}}
>
- to add before
+ to add before
@@ -303,7 +303,7 @@ export const BlockChildHoveredInstanceOutline = () => {
gap={1}
css={{ order: 1, display: !hasChildren ? "none" : undefined }}
>
- {" "}
+ {" "}
to delete
diff --git a/apps/builder/app/builder/shared/client-settings/settings.ts b/apps/builder/app/builder/shared/client-settings/settings.ts
index 73938528e6..54c8befc1f 100644
--- a/apps/builder/app/builder/shared/client-settings/settings.ts
+++ b/apps/builder/app/builder/shared/client-settings/settings.ts
@@ -5,6 +5,7 @@ const Settings = z.object({
navigatorLayout: z.enum(["docked", "undocked"]).default("undocked"),
isAiMenuOpen: z.boolean().default(true),
isAiCommandBarVisible: z.boolean().default(false),
+ stylePanelMode: z.enum(["default", "focus", "advanced"]).default("default"),
});
export type Settings = z.infer;
diff --git a/apps/builder/app/builder/shared/collapsible-section.tsx b/apps/builder/app/builder/shared/collapsible-section.tsx
index 5cff9df844..7cbd4c263d 100644
--- a/apps/builder/app/builder/shared/collapsible-section.tsx
+++ b/apps/builder/app/builder/shared/collapsible-section.tsx
@@ -11,7 +11,10 @@ import {
} from "@webstudio-is/design-system";
import {
createContext,
+ useCallback,
useContext,
+ useEffect,
+ useState,
type ComponentProps,
type ReactNode,
} from "react";
@@ -20,46 +23,94 @@ import type { Simplify } from "type-fest";
type Label = string;
-type UseOpenStateProps = {
- label: Label;
- isOpen?: boolean;
+type State = {
+ [label: string]: boolean;
};
-export const CollapsibleSectionContext = createContext<{
+// Preserves the open/close state even when component gets unmounted
+const $state = atom({});
+
+type HandleOpenState = (
+ label: Label,
+ isOpenForced?: boolean
+) => [boolean, (value: boolean) => void];
+
+const CollapsibleSectionContext = createContext<
+ | undefined
+ | {
+ handleOpenState: HandleOpenState;
+ }
+>(undefined);
+
+export const CollapsibleProvider = ({
+ children,
+ accordion,
+ initialOpen,
+}: {
+ children: ReactNode;
accordion?: boolean;
initialOpen?: Label;
-}>({});
+}) => {
+ const state = useStore($state);
+ useEffect(() => {
+ const nextState = { ...$state.get() };
-const $stateContainer = atom<{
- [label: string]: boolean;
-}>({});
+ if (initialOpen === "*") {
+ for (const key in nextState) {
+ nextState[key] = true;
+ }
+ $state.set(nextState);
+ return;
+ }
-// Preserves the open/close state even when component gets unmounted
-export const useOpenState = ({
- label,
- isOpen: isOpenForced,
-}: UseOpenStateProps): [boolean, (value: boolean) => void] => {
- const state = useStore($stateContainer);
- const { accordion, initialOpen } = useContext(CollapsibleSectionContext);
- const setIsOpen = (isOpen: boolean) => {
- const update = { ...state };
- if (isOpen && accordion) {
- // In accordion mode we close everything else within that accordion.
- for (const key in update) {
- update[key] = false;
+ if (accordion) {
+ for (const key in nextState) {
+ nextState[key] = false;
}
}
- update[label] = isOpen;
- $stateContainer.set(update);
- };
- // Set initial value for accordion mode.
- if (accordion && state[label] === undefined) {
- state[label] = initialOpen === label;
+ if (initialOpen) {
+ nextState[initialOpen] = true;
+ }
+ $state.set(nextState);
+ }, [accordion, initialOpen]);
+
+ const handleOpenState: HandleOpenState = useCallback(
+ (label, isOpenForced) => {
+ const nextState = { ...state };
+ if (nextState[label] === undefined) {
+ nextState[label] = accordion ? false : true;
+ }
+
+ const setIsOpen = (isOpen: boolean) => {
+ if (accordion) {
+ for (const key in nextState) {
+ nextState[key] = false;
+ }
+ }
+ nextState[label] = isOpen;
+ $state.set(nextState);
+ };
+
+ return [isOpenForced ?? nextState[label], setIsOpen];
+ },
+ [state, accordion]
+ );
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useOpenState: HandleOpenState = (label, isOpen) => {
+ const context = useContext(CollapsibleSectionContext);
+ const localState = useState(isOpen ?? true);
+ if (context === undefined) {
+ return localState;
}
-
- const isOpenCurrent = state[label];
- return [isOpenForced ?? isOpenCurrent ?? true, setIsOpen];
+ return context.handleOpenState(label, isOpen);
};
type CollapsibleSectionBaseProps = {
@@ -67,8 +118,8 @@ type CollapsibleSectionBaseProps = {
children: ReactNode;
fullWidth?: boolean;
label: string;
- isOpen: boolean;
- onOpenChange: (value: boolean) => void;
+ isOpen?: boolean;
+ onOpenChange?: (value: boolean) => void;
};
export const CollapsibleSectionRoot = ({
@@ -78,44 +129,47 @@ export const CollapsibleSectionRoot = ({
fullWidth = false,
isOpen,
onOpenChange,
-}: CollapsibleSectionBaseProps) => (
-
- <>
-
- {trigger ?? (
-
- {label}
-
- )}
-
+}: CollapsibleSectionBaseProps) => {
+ return (
+
+ <>
+
+ {trigger ?? (
+
+ {label}
+
+ )}
+
-
-
- {children}
-
-
-
- >
-
-);
+
+
+ {children}
+
+
+
+ >
+
+ );
+};
type CollapsibleSectionProps = Simplify<
- Omit &
- UseOpenStateProps
+ Omit & {
+ label: Label;
+ }
>;
export const CollapsibleSection = (props: CollapsibleSectionProps) => {
const { label, trigger, children, fullWidth } = props;
- const [isOpen, setIsOpen] = useOpenState(props);
+ const [isOpen, setIsOpen] = useOpenState(label, props.isOpen);
return (
["dots"];
}) => {
const { label, children } = props;
- const [isOpen, setIsOpen] = useOpenState(props);
+ const [isOpen, setIsOpen] = useOpenState(label, props.isOpen);
const isEmpty =
hasItems === false || (Array.isArray(hasItems) && hasItems.length === 0);
diff --git a/apps/builder/app/builder/shared/commands.ts b/apps/builder/app/builder/shared/commands.ts
index 185d170d54..829489ebfa 100644
--- a/apps/builder/app/builder/shared/commands.ts
+++ b/apps/builder/app/builder/shared/commands.ts
@@ -43,6 +43,7 @@ import {
isInstanceDetachable,
isTreeMatching,
} from "~/shared/matcher";
+import { getSetting, setSetting } from "./client-settings";
const makeBreakpointCommand = (
name: CommandName,
@@ -350,6 +351,17 @@ export const { emitCommand, subscribeCommands } = createCommandsEmitter({
},
disableOnInputLikeControls: true,
},
+ {
+ name: "toggleStylePanelFocusMode",
+ defaultHotkeys: ["alt+shift+s"],
+ handler: () => {
+ setSetting(
+ "stylePanelMode",
+ getSetting("stylePanelMode") === "focus" ? "default" : "focus"
+ );
+ },
+ disableOnInputLikeControls: true,
+ },
{
name: "openSettingsPanel",
defaultHotkeys: ["d"],
diff --git a/packages/design-system/src/components/kbd.tsx b/packages/design-system/src/components/kbd.tsx
index 5c9abe76c2..5715d887b7 100644
--- a/packages/design-system/src/components/kbd.tsx
+++ b/packages/design-system/src/components/kbd.tsx
@@ -6,7 +6,7 @@ const isMac =
const shortcutSymbolMap: Record = {
cmd: isMac ? "⌘" : "Ctrl",
shift: "⇧",
- option: isMac ? "⌥" : "Alt",
+ alt: isMac ? "⌥" : "Alt",
backspace: "⌫",
enter: "↵",
tab: isMac ? "tab" : "Tab",
diff --git a/packages/design-system/src/components/menu.tsx b/packages/design-system/src/components/menu.tsx
index e27046596c..496b8aec42 100644
--- a/packages/design-system/src/components/menu.tsx
+++ b/packages/design-system/src/components/menu.tsx
@@ -61,7 +61,7 @@ export const menuItemCss = css({
borderRadius: theme.borderRadius[3],
// override button default styles
backgroundColor: "transparent",
- "&:focus, &[data-found], &[aria-selected=true], &[data-state=open], &[data-state=checked]":
+ "&:focus, &[data-found], &[aria-selected=true], &[data-state=open], &[data-state=checked]:is(:hover,:focus)":
{
backgroundColor: theme.colors.backgroundItemMenuItemHover,
},