Skip to content

Commit f395bbe

Browse files
authored
feat: Make empty style panel sections easier to recognize as empty (#4497)
## Description 1. Its easier to scan the panel when titles are dimmed if there is nothing in the content of that section 2. when section has nothing it shouldn't have a chevron on hover <img width="252" alt="image" src="https://github.com/user-attachments/assets/75bfb06e-ceec-4659-a836-02c8dacf8217"> <img width="250" alt="image" src="https://github.com/user-attachments/assets/39ba04c8-7b2e-4099-83df-94567c963038"> ## Steps for reproduction 1. click button 3. 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 1a54822 commit f395bbe

File tree

8 files changed

+89
-28
lines changed

8 files changed

+89
-28
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export const Section = () => {
3636
onAdd={() => {
3737
addRepeatedStyleItem(styles, parseCssFragment("none", ["background"]));
3838
}}
39+
collapsible
3940
>
4041
<Flex gap={1} direction="column">
4142
<RepeatedStyle

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ export const Section = () => {
147147
styles,
148148
})
149149
);
150+
const dots = getDots(styles);
150151

151152
return (
152153
<CollapsibleSectionRoot
@@ -156,7 +157,9 @@ export const Section = () => {
156157
onOpenChange={setIsOpen}
157158
trigger={
158159
<SectionTitle
159-
dots={getDots(styles)}
160+
inactive={dots.length === 0}
161+
collapsible={dots.length !== 0}
162+
dots={dots}
160163
suffix={
161164
<Flex gap="1" align="center">
162165
<TransformAdvancedPopover />

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ export const Section = () => {
9393
selectedOrLastStyleSourceSelector &&
9494
selectedOrLastStyleSourceSelector.state === undefined;
9595
const styles = useComputedStyles(transitionLongHandProperties);
96+
const dots = getDots(styles);
9697

9798
return (
9899
<CollapsibleSectionRoot
@@ -102,7 +103,9 @@ export const Section = () => {
102103
onOpenChange={setIsOpen}
103104
trigger={
104105
<SectionTitle
105-
dots={getDots(styles)}
106+
inactive={dots.length === 0}
107+
collapsible={dots.length !== 0}
108+
dots={dots}
106109
suffix={
107110
<Tooltip
108111
content={

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const getDots = (styles: ComputedStyleDecl[]) => {
2323
styleDecl.usedValue.type === "unparsed" ||
2424
styleDecl.usedValue.type === "guaranteedInvalid"
2525
) {
26-
return;
26+
return [];
2727
}
2828

2929
const source = styleDecl.source.name;
@@ -66,12 +66,16 @@ export const RepeatedStyleSection = (props: {
6666
label: string;
6767
description: string;
6868
properties: [StyleProperty, ...StyleProperty[]];
69+
collapsible?: boolean;
6970
onAdd: () => void;
7071
children: ReactNode;
7172
}) => {
72-
const { label, description, children, properties, onAdd } = props;
73+
const { label, description, children, properties, onAdd, collapsible } =
74+
props;
7375
const [isOpen, setIsOpen] = useOpenState(props);
7476
const styles = useComputedStyles(properties);
77+
const dots = getDots(styles);
78+
7579
return (
7680
<CollapsibleSectionRoot
7781
fullWidth
@@ -80,6 +84,8 @@ export const RepeatedStyleSection = (props: {
8084
onOpenChange={setIsOpen}
8185
trigger={
8286
<SectionTitle
87+
inactive={dots.length === 0}
88+
collapsible={collapsible}
8389
dots={getDots(styles)}
8490
suffix={
8591
<SectionTitleButton

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const labelColors = [
1616
"overwritten",
1717
"remote",
1818
"code",
19+
"inactive",
1920
] as const;
2021

2122
const StyledLabel = styled(RadixLabel, {
@@ -96,6 +97,13 @@ const StyledLabel = styled(RadixLabel, {
9697
backgroundColor: theme.colors.backgroundHover,
9798
},
9899
},
100+
// Example is collapsible section title label when section has no content.
101+
inactive: {
102+
color: theme.colors.foregroundTextSubtle,
103+
"&:hover": {
104+
color: theme.colors.foregroundMain,
105+
},
106+
},
99107
},
100108
truncate: {
101109
true: {

packages/design-system/src/components/nested-icon-label.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ const colors = {
4444
backgroundHover: theme.colors.backgroundHover,
4545
icon: theme.colors.foregroundIconMain,
4646
},
47+
inactive: {
48+
border: "transparent",
49+
background: "transparent",
50+
backgroundHover: "transparent",
51+
icon: theme.colors.foregroundSubtle,
52+
},
4753
} as const;
4854

4955
const perColorStyle = (color: (typeof labelColors)[number]) => ({
@@ -77,6 +83,7 @@ const style = css({
7783
overwritten: perColorStyle("overwritten"),
7884
remote: perColorStyle("remote"),
7985
code: perColorStyle("code"),
86+
inactive: perColorStyle("inactive"),
8087
},
8188
},
8289
defaultVariants: { color: "default" },

packages/design-system/src/components/section-title.stories.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,22 @@ const Wrap = ({ children }: { children: React.ReactNode }) => (
1717

1818
const Variants = ({
1919
state,
20+
inactive,
2021
}: {
2122
state: ComponentProps<typeof SectionTitle>["data-state"];
23+
inactive?: ComponentProps<typeof SectionTitle>["inactive"];
2224
}) => (
2325
<>
2426
<Wrap>
25-
<SectionTitle data-state={state}>
27+
<SectionTitle data-state={state} inactive={inactive}>
2628
<SectionTitleLabel>Simplest</SectionTitleLabel>
2729
</SectionTitle>
2830
</Wrap>
2931
<Wrap>
3032
<SectionTitle
3133
suffix={<SectionTitleButton prefix={<PlusIcon />} />}
3234
data-state={state}
35+
inactive={inactive}
3336
>
3437
<SectionTitleLabel>With button</SectionTitleLabel>
3538
</SectionTitle>
@@ -39,6 +42,7 @@ const Variants = ({
3942
dots={["local", "remote"]}
4043
suffix={<SectionTitleButton prefix={<PlusIcon />} />}
4144
data-state={state}
45+
inactive={inactive}
4246
>
4347
<SectionTitleLabel>With dots</SectionTitleLabel>
4448
</SectionTitle>
@@ -48,6 +52,7 @@ const Variants = ({
4852
dots={["local"]}
4953
suffix={<SectionTitleButton prefix={<PlusIcon />} />}
5054
data-state={state}
55+
inactive={inactive}
5156
>
5257
<SectionTitleLabel color="local">With label</SectionTitleLabel>
5358
</SectionTitle>
@@ -57,14 +62,15 @@ const Variants = ({
5762
dots={["local", "remote"]}
5863
suffix={<SectionTitleButton prefix={<PlusIcon />} />}
5964
data-state={state}
65+
inactive={inactive}
6066
>
6167
<SectionTitleLabel>
6268
Some title so long that it cannot possibly fit
6369
</SectionTitleLabel>
6470
</SectionTitle>
6571
</Wrap>
6672
<Wrap>
67-
<SectionTitle data-state={state}>
73+
<SectionTitle data-state={state} inactive={inactive}>
6874
<SectionTitleLabel>
6975
Some title so long that it cannot possibly fit
7076
</SectionTitleLabel>
@@ -100,6 +106,12 @@ export const Demo = () => (
100106
<Variants state="open" />
101107
</StoryGrid>
102108
</StorySection>
109+
110+
<StorySection title="Inactive">
111+
<StoryGrid>
112+
<Variants inactive state="closed" />
113+
</StoryGrid>
114+
</StorySection>
103115
</>
104116
);
105117

packages/design-system/src/components/section-title.tsx

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,12 @@ const chevronStyle = css({
7575
transition: "transform 150ms, opacity 200ms",
7676
color: theme.colors.backgroundIconSubtle,
7777
variants: {
78-
state: {
78+
openState: {
7979
open: {
8080
transform: "rotate(90deg)",
8181
},
8282
closed: {},
83+
inactive: {},
8384
},
8485
},
8586
});
@@ -100,8 +101,12 @@ const dotStyle = css({
100101
},
101102
});
102103

103-
const context = createContext<{ state: "open" | "closed" }>({
104-
state: "closed",
104+
const context = createContext<{
105+
openState: "open" | "closed";
106+
inactive: boolean;
107+
}>({
108+
openState: "closed",
109+
inactive: false,
105110
});
106111

107112
export const SectionTitle = forwardRef(
@@ -112,8 +117,12 @@ export const SectionTitle = forwardRef(
112117
css,
113118
children,
114119
suffix,
120+
inactive = false,
121+
collapsible = true,
115122
...props
116123
}: ComponentProps<"button"> & {
124+
inactive?: boolean;
125+
collapsible?: boolean;
117126
/** https://www.radix-ui.com/docs/primitives/components/collapsible#trigger */
118127
"data-state"?: "open" | "closed";
119128
dots?: Array<"local" | "overwritten" | "remote">;
@@ -123,34 +132,39 @@ export const SectionTitle = forwardRef(
123132
},
124133
ref: Ref<HTMLButtonElement>
125134
) => {
126-
const state = props["data-state"] ?? "closed";
127-
const finalDots = state === "open" ? [] : (dots ?? []);
135+
const openState = props["data-state"] ?? "closed";
136+
const finalDots = openState === "open" ? [] : (dots ?? []);
128137

129138
return (
130-
<context.Provider value={{ state }}>
139+
<context.Provider value={{ openState, inactive }}>
131140
<ArrowFocus
132141
render={({ handleKeyDown }) => (
133142
<Flex
134143
align="center"
135-
className={containerStyle({ className, css })}
136-
data-state={state}
144+
className={containerStyle({
145+
className,
146+
css,
147+
color: inactive ? "disabled" : "default",
148+
})}
149+
data-state={openState}
137150
onKeyDown={handleKeyDown}
138151
>
139-
<button
140-
className={titleButtonStyle()}
141-
data-state={state}
142-
ref={ref}
143-
{...props}
144-
>
145-
<ChevronRightIcon className={chevronStyle({ state })} />
146-
</button>
147-
152+
{collapsible && (
153+
<button
154+
className={titleButtonStyle()}
155+
data-state={openState}
156+
ref={ref}
157+
{...props}
158+
>
159+
<ChevronRightIcon className={chevronStyle({ openState })} />
160+
</button>
161+
)}
148162
{/*
149163
If the label is itself a button, we don't want to nest a button inside another button.
150164
Therefore, we render the label in a layer above the SectionTitle button
151165
*/}
152166
<div className={labelContainerStyle()}>
153-
<div className={titleButtonLayoutStyle()}>
167+
<div className={titleButtonLayoutStyle({ openState })}>
154168
{children}
155169

156170
{finalDots.length > 0 && (
@@ -187,11 +201,14 @@ export const SectionTitleLabel = forwardRef(
187201
}: Omit<ComponentProps<typeof Label>, "truncate" | "text">,
188202
ref: Ref<HTMLLabelElement>
189203
) => {
190-
const { state } = useContext(context);
204+
const { openState, inactive } = useContext(context);
191205

192206
const commonCss = { flex: "0 1 auto" };
193-
194-
const color = state === "closed" ? undefined : props.color;
207+
const color = inactive
208+
? "inactive"
209+
: openState === "closed"
210+
? "default"
211+
: props.color;
195212

196213
const isButton = isLabelButton(color);
197214

@@ -202,9 +219,13 @@ export const SectionTitleLabel = forwardRef(
202219
{...props}
203220
color={color}
204221
css={{
205-
color: state === "closed" ? `var(${labelTextColor})` : undefined,
222+
color:
223+
openState === "closed" && inactive === false
224+
? `var(${labelTextColor})`
225+
: undefined,
206226
...commonCss,
207227
...css,
228+
208229
// When we use a SectionTitle button, we can't directly render a label inside it.
209230
// Instead, we need to render the label using a div that has position:absolute and pointer-events:none
210231
// However, if the label itself is a button, we need to make sure that it remains clickable.

0 commit comments

Comments
 (0)