Skip to content

Commit a8bfa4d

Browse files
authored
experimental: Style panel focus mode (#4835)
#4816 ## Description - implements accordion pattern - adds a dropdown switch ## 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
1 parent 609fdb4 commit a8bfa4d

File tree

15 files changed

+267
-110
lines changed

15 files changed

+267
-110
lines changed

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

+26-13
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
Kbd,
2020
FloatingPanelProvider,
2121
} from "@webstudio-is/design-system";
22-
import { StylePanel } from "~/builder/features/style-panel";
22+
import { ModeMenu, StylePanel } from "~/builder/features/style-panel";
2323
import { SettingsPanelContainer } from "~/builder/features/settings-panel";
2424
import {
2525
$registeredComponentMetas,
@@ -33,6 +33,7 @@ import { getInstanceLabel } from "~/shared/instance-utils";
3333
import { BindingPopoverProvider } from "~/builder/shared/binding-popover";
3434
import { $activeInspectorPanel } from "~/builder/shared/nano-states";
3535
import { $selectedInstance, $selectedPage } from "~/shared/awareness";
36+
import { isFeatureEnabled } from "@webstudio-is/feature-flags";
3637

3738
const InstanceInfo = ({ instance }: { instance: Instance }) => {
3839
const metas = useStore($registeredComponentMetas);
@@ -42,16 +43,7 @@ const InstanceInfo = ({ instance }: { instance: Instance }) => {
4243
}
4344
const label = getInstanceLabel(instance, componentMeta);
4445
return (
45-
<Flex
46-
shrink="false"
47-
gap="1"
48-
align="center"
49-
css={{
50-
p: theme.panel.padding,
51-
pb: 0,
52-
color: theme.colors.foregroundSubtle,
53-
}}
54-
>
46+
<Flex shrink={false} gap="1" align="center">
5547
<MetaIcon icon={componentMeta.icon} />
5648
<Text truncate variant="labelsSentenceCase">
5749
{label}
@@ -175,7 +167,18 @@ export const Inspector = ({ navigatorLayout }: InspectorProps) => {
175167
</PanelTabsList>
176168
<Separator />
177169
<PanelTabsContent value="style" css={contentStyle} tabIndex={-1}>
178-
<InstanceInfo instance={selectedInstance} />
170+
<Flex
171+
justify="between"
172+
align="center"
173+
shrink={false}
174+
css={{
175+
paddingInline: theme.panel.paddingInline,
176+
height: theme.spacing[13],
177+
}}
178+
>
179+
<InstanceInfo instance={selectedInstance} />
180+
{isFeatureEnabled("stylePanelModes") && <ModeMenu />}
181+
</Flex>
179182
<StylePanel />
180183
</PanelTabsContent>
181184
<PanelTabsContent
@@ -184,7 +187,17 @@ export const Inspector = ({ navigatorLayout }: InspectorProps) => {
184187
tabIndex={-1}
185188
>
186189
<ScrollArea>
187-
<InstanceInfo instance={selectedInstance} />
190+
<Flex
191+
justify="between"
192+
align="center"
193+
shrink={false}
194+
css={{
195+
paddingInline: theme.panel.paddingInline,
196+
height: theme.spacing[13],
197+
}}
198+
>
199+
<InstanceInfo instance={selectedInstance} />
200+
</Flex>
188201
<SettingsPanelContainer
189202
// Re-render when instance changes
190203
key={selectedInstance.id}

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -310,13 +310,15 @@ const VariablesList = () => {
310310
);
311311
};
312312

313+
const label = "Data Variables";
314+
313315
export const VariablesSection = () => {
314316
const containerRef = useRef<HTMLDivElement>(null);
315-
const [isOpen, setIsOpen] = useOpenState({ label: "variables" });
317+
const [isOpen, setIsOpen] = useOpenState(label);
316318
return (
317319
<VariablePopoverProvider value={{ containerRef }}>
318320
<CollapsibleSectionRoot
319-
label="Data Variables"
321+
label={label}
320322
fullWidth={true}
321323
isOpen={isOpen}
322324
onOpenChange={setIsOpen}

apps/builder/app/builder/features/style-panel/property-label.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ export const PropertyInfo = ({
199199
<ResetIcon />
200200
</Flex>
201201
}
202-
suffix={<Kbd value={["option", "click"]} color="moreSubtle" />}
202+
suffix={<Kbd value={["alt", "click"]} color="moreSubtle" />}
203203
css={{ gridTemplateColumns: "1fr max-content 1fr" }}
204204
onClick={onReset}
205205
>

apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ const AdvancedStyleSection = (props: {
8383
children: ReactNode;
8484
}) => {
8585
const { label, children, properties, onAdd } = props;
86-
const [isOpen, setIsOpen] = useOpenState(props);
86+
const [isOpen, setIsOpen] = useOpenState(label);
8787
const styles = useComputedStyles(properties);
8888
return (
8989
<CollapsibleSectionRoot

apps/builder/app/builder/features/style-panel/sections/transforms/transforms.tsx

+6-8
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
import {
2-
forwardRef,
3-
useState,
4-
type ElementRef,
5-
type ComponentProps,
6-
} from "react";
1+
import { forwardRef, type ElementRef, type ComponentProps } from "react";
72
import type { StyleProperty } from "@webstudio-is/css-engine";
83
import { propertyDescriptions } from "@webstudio-is/css-data";
94
import {
@@ -32,7 +27,10 @@ import {
3227
EyeOpenIcon,
3328
EllipsesIcon,
3429
} from "@webstudio-is/icons";
35-
import { CollapsibleSectionRoot } from "~/builder/shared/collapsible-section";
30+
import {
31+
CollapsibleSectionRoot,
32+
useOpenState,
33+
} from "~/builder/shared/collapsible-section";
3634
import {
3735
addDefaultsForTransormSection,
3836
isTransformPanelPropertyUsed,
@@ -138,7 +136,7 @@ const TransformAdvancedPopover = () => {
138136
};
139137

140138
export const Section = () => {
141-
const [isOpen, setIsOpen] = useState(true);
139+
const [isOpen, setIsOpen] = useOpenState(label);
142140

143141
const styles = useComputedStyles(properties);
144142
const isAnyTransformPropertyAdded = transformPanels.some((panel) =>

apps/builder/app/builder/features/style-panel/sections/transitions/transitions.tsx

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { useState } from "react";
21
import { useStore } from "@nanostores/react";
32
import { PlusIcon } from "@webstudio-is/icons";
43
import {
@@ -12,7 +11,10 @@ import {
1211
type LayerValueItem,
1312
type StyleProperty,
1413
} from "@webstudio-is/css-engine";
15-
import { CollapsibleSectionRoot } from "~/builder/shared/collapsible-section";
14+
import {
15+
CollapsibleSectionRoot,
16+
useOpenState,
17+
} from "~/builder/shared/collapsible-section";
1618
import { $selectedOrLastStyleSourceSelector } from "~/shared/nano-states";
1719
import { humanizeString } from "~/shared/string-utils";
1820
import { repeatUntil } from "~/shared/array-utils";
@@ -83,7 +85,7 @@ const getLayerLabel = ({
8385
};
8486

8587
export const Section = () => {
86-
const [isOpen, setIsOpen] = useState(true);
88+
const [isOpen, setIsOpen] = useOpenState(label);
8789

8890
const selectedOrLastStyleSourceSelector = useStore(
8991
$selectedOrLastStyleSourceSelector

apps/builder/app/builder/features/style-panel/shared/style-section.tsx

+2-4
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,9 @@ export const StyleSection = (props: {
4141
// @todo remove to keep sections consistent
4242
fullWidth?: boolean;
4343
children: ReactNode;
44-
accordion?: string;
45-
initialOpen?: string;
4644
}) => {
4745
const { label, children, properties, fullWidth } = props;
48-
const [isOpen, setIsOpen] = useOpenState(props);
46+
const [isOpen, setIsOpen] = useOpenState(label);
4947
const styles = useComputedStyles(properties);
5048
return (
5149
<CollapsibleSectionRoot
@@ -74,7 +72,7 @@ export const RepeatedStyleSection = (props: {
7472
}) => {
7573
const { label, description, children, properties, onAdd, collapsible } =
7674
props;
77-
const [isOpen, setIsOpen] = useOpenState(props);
75+
const [isOpen, setIsOpen] = useOpenState(label);
7876
const styles = useComputedStyles(properties);
7977
const dots = getDots(styles);
8078

apps/builder/app/builder/features/style-panel/style-panel.tsx

+85-8
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@ import {
55
Text,
66
Separator,
77
ScrollArea,
8+
DropdownMenu,
9+
DropdownMenuTrigger,
10+
IconButton,
11+
DropdownMenuContent,
12+
DropdownMenuRadioItem,
13+
MenuCheckedIcon,
14+
DropdownMenuRadioGroup,
15+
rawTheme,
16+
Kbd,
17+
Flex,
18+
DropdownMenuSeparator,
19+
DropdownMenuItem,
820
} from "@webstudio-is/design-system";
921
import { useStore } from "@nanostores/react";
1022
import { computed } from "nanostores";
@@ -14,8 +26,15 @@ import { sections } from "./sections";
1426
import { toValue } from "@webstudio-is/css-engine";
1527
import { $instanceTags, useParentComputedStyleDecl } from "./shared/model";
1628
import { $selectedInstance } from "~/shared/awareness";
17-
import { CollapsibleSectionContext } from "~/builder/shared/collapsible-section";
18-
import { isFeatureEnabled } from "@webstudio-is/feature-flags";
29+
import { CollapsibleProvider } from "~/builder/shared/collapsible-section";
30+
import { EllipsesIcon } from "@webstudio-is/icons";
31+
import {
32+
$settings,
33+
getSetting,
34+
setSetting,
35+
type Settings,
36+
} from "~/builder/shared/client-settings";
37+
import { useState } from "react";
1938

2039
const $selectedInstanceTag = computed(
2140
[$selectedInstance, $instanceTags],
@@ -27,7 +46,67 @@ const $selectedInstanceTag = computed(
2746
}
2847
);
2948

49+
export const ModeMenu = () => {
50+
const value = getSetting("stylePanelMode");
51+
const [focusedValue, setFocusedValue] = useState<string>(value);
52+
53+
return (
54+
<DropdownMenu>
55+
<DropdownMenuTrigger asChild>
56+
<IconButton>
57+
<EllipsesIcon />
58+
</IconButton>
59+
</DropdownMenuTrigger>
60+
<DropdownMenuContent
61+
sideOffset={Number.parseFloat(rawTheme.spacing[5])}
62+
css={{ width: theme.spacing[25] }}
63+
>
64+
<DropdownMenuRadioGroup
65+
value={value}
66+
onValueChange={(value) => {
67+
setSetting("stylePanelMode", value as Settings["stylePanelMode"]);
68+
}}
69+
>
70+
<DropdownMenuRadioItem
71+
value="default"
72+
icon={<MenuCheckedIcon />}
73+
onFocus={() => setFocusedValue("default")}
74+
>
75+
Default
76+
</DropdownMenuRadioItem>
77+
<DropdownMenuRadioItem
78+
value="focus"
79+
icon={<MenuCheckedIcon />}
80+
onFocus={() => setFocusedValue("focus")}
81+
>
82+
<Flex justify="between" grow>
83+
<Text>Focus mode</Text> <Kbd value={["alt", "shift", "s"]} />
84+
</Flex>
85+
</DropdownMenuRadioItem>
86+
{/*
87+
<DropdownMenuRadioItem value="advanced" icon={<MenuCheckedIcon />}>
88+
Advanced mode
89+
</DropdownMenuRadioItem> */}
90+
</DropdownMenuRadioGroup>
91+
<DropdownMenuSeparator />
92+
93+
{focusedValue === "default" && (
94+
<DropdownMenuItem hint>
95+
All sections are open by default.
96+
</DropdownMenuItem>
97+
)}
98+
{focusedValue === "focus" && (
99+
<DropdownMenuItem hint>
100+
Only one section is open at a time.
101+
</DropdownMenuItem>
102+
)}
103+
</DropdownMenuContent>
104+
</DropdownMenu>
105+
);
106+
};
107+
30108
export const StylePanel = () => {
109+
const { stylePanelMode } = useStore($settings);
31110
const selectedInstanceRenderState = useStore($selectedInstanceRenderState);
32111
const tag = useStore($selectedInstanceTag);
33112
const display = useParentComputedStyleDecl("display");
@@ -75,14 +154,12 @@ export const StylePanel = () => {
75154
</Box>
76155
<Separator />
77156
<ScrollArea>
78-
<CollapsibleSectionContext.Provider
79-
value={{
80-
accordion: isFeatureEnabled("stylePanelModes"),
81-
initialOpen: "Layout",
82-
}}
157+
<CollapsibleProvider
158+
accordion={stylePanelMode === "focus"}
159+
initialOpen={stylePanelMode === "focus" ? "Layout" : "*"}
83160
>
84161
{all}
85-
</CollapsibleSectionContext.Provider>
162+
</CollapsibleProvider>
86163
</ScrollArea>
87164
</>
88165
);

apps/builder/app/builder/features/topbar/topbar.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const PagesButton = () => {
4141
content={
4242
<Text>
4343
{"Pages or page settings "}
44-
<Kbd value={["option", "click"]} color="moreSubtle" />
44+
<Kbd value={["alt", "click"]} color="moreSubtle" />
4545
</Text>
4646
}
4747
>

apps/builder/app/builder/features/workspace/canvas-tools/outline/block-instance-outline.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ export const TemplatesMenu = ({
203203
display: hasChildren ? undefined : "none",
204204
}}
205205
>
206-
<Kbd value={["option", "click"]} /> <Text>to add before</Text>
206+
<Kbd value={["alt", "click"]} /> <Text>to add before</Text>
207207
</Flex>
208208
</Grid>
209209
</div>
@@ -303,7 +303,7 @@ export const BlockChildHoveredInstanceOutline = () => {
303303
gap={1}
304304
css={{ order: 1, display: !hasChildren ? "none" : undefined }}
305305
>
306-
<Kbd value={["option", "click"]} color="contrast" />{" "}
306+
<Kbd value={["alt", "click"]} color="contrast" />{" "}
307307
<Text color="subtle">to delete</Text>
308308
</Flex>
309309
</Grid>

apps/builder/app/builder/shared/client-settings/settings.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const Settings = z.object({
55
navigatorLayout: z.enum(["docked", "undocked"]).default("undocked"),
66
isAiMenuOpen: z.boolean().default(true),
77
isAiCommandBarVisible: z.boolean().default(false),
8+
stylePanelMode: z.enum(["default", "focus", "advanced"]).default("default"),
89
});
910

1011
export type Settings = z.infer<typeof Settings>;

0 commit comments

Comments
 (0)