Skip to content

Commit 411c0e3

Browse files
authored
layer opacity control + toolbar responsive grouping functionality (#1899)
* layer opacity input for toolbar * toolbar grouping, tons of styling, responsive-width for the toolbar
1 parent 27c7ea0 commit 411c0e3

File tree

12 files changed

+642
-128
lines changed

12 files changed

+642
-128
lines changed

apps/web/client/src/app/project/[id]/_components/editor-bar/HoverOnlyTooltip.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ interface HoverOnlyTooltipProps {
88
side?: "top" | "right" | "bottom" | "left";
99
className?: string;
1010
hideArrow?: boolean;
11+
disabled?: boolean;
1112
}
1213

1314
export function HoverOnlyTooltip({
@@ -16,14 +17,15 @@ export function HoverOnlyTooltip({
1617
side = "bottom",
1718
className,
1819
hideArrow = false,
20+
disabled = false,
1921
}: HoverOnlyTooltipProps) {
2022
const [hovered, setHovered] = useState(false);
2123

2224
return (
23-
<Tooltip open={hovered}>
25+
<Tooltip open={hovered && !disabled}>
2426
<TooltipTrigger
2527
asChild
26-
onMouseEnter={() => setHovered(true)}
28+
onMouseEnter={() => !disabled && setHovered(true)}
2729
onMouseLeave={() => setHovered(false)}
2830
onBlur={() => setHovered(false)}
2931
>
Lines changed: 175 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,192 @@
11
'use client';
22

3+
import { Button } from '@onlook/ui/button';
4+
import { Icons } from '@onlook/ui/icons';
5+
import { Popover, PopoverContent, PopoverTrigger } from '@onlook/ui/popover';
6+
import React, { useCallback, useEffect, useRef, useState } from 'react';
37
import { Border } from './dropdowns/border';
48
import { ColorBackground } from './dropdowns/color-background';
59
import { Display } from './dropdowns/display';
610
import { Height } from './dropdowns/height';
711
import { ImageBackground } from './dropdowns/img-background';
812
import { Margin } from './dropdowns/margin';
13+
import { Opacity } from './dropdowns/opacity';
914
import { Padding } from './dropdowns/padding';
1015
import { Radius } from './dropdowns/radius';
1116
import { Width } from './dropdowns/width';
1217
import { ViewButtons } from './panels/panel-bar/bar';
1318
import { InputSeparator } from './separator';
1419

15-
export const DivSelected = () => {
20+
const COMPONENT_MAP: { [key: string]: any } = {
21+
Opacity,
22+
Width,
23+
Height,
24+
Display,
25+
Padding,
26+
Margin,
27+
Radius,
28+
Border,
29+
ColorBackground,
30+
ImageBackground,
31+
ViewButtons,
32+
};
33+
34+
// Group definitions for the div-selected toolbar
35+
export const DIV_SELECTED_GROUPS = [
36+
{
37+
key: 'dimensions',
38+
label: 'Dimensions',
39+
components: ['Width', 'Height'],
40+
},
41+
{
42+
key: 'base',
43+
label: 'Base',
44+
components: ['ColorBackground', 'Border', 'Radius'],
45+
},
46+
{
47+
key: 'layout',
48+
label: 'Layout',
49+
components: ['Display', 'Padding', 'Margin'],
50+
},
51+
{
52+
key: 'typography',
53+
label: 'Typography',
54+
components: ['FontFamily', 'FontWeight', 'FontSize', 'FontColor', 'TextAlign'],
55+
},
56+
{
57+
key: 'opacity',
58+
label: 'Opacity',
59+
components: ['Opacity'],
60+
},
61+
];
62+
63+
export const DivSelected = ({ availableWidth = 0 }: { availableWidth?: number }) => {
64+
const groupRefs = useRef<(HTMLDivElement | null)[]>([]);
65+
const [groupWidths, setGroupWidths] = useState<number[]>([]);
66+
const [overflowOpen, setOverflowOpen] = useState(false);
67+
const [visibleCount, setVisibleCount] = useState(DIV_SELECTED_GROUPS.length);
68+
69+
// Calculate total width of a group including margins and padding
70+
const calculateGroupWidth = useCallback((element: HTMLElement | null): number => {
71+
if (!element) return 0;
72+
const style = window.getComputedStyle(element);
73+
const width = element.offsetWidth;
74+
const marginLeft = parseFloat(style.marginLeft);
75+
const marginRight = parseFloat(style.marginRight);
76+
const paddingLeft = parseFloat(style.paddingLeft);
77+
const paddingRight = parseFloat(style.paddingRight);
78+
return width + marginLeft + marginRight + paddingLeft + paddingRight;
79+
}, []);
80+
81+
// Measure all group widths
82+
const measureGroups = useCallback(() => {
83+
const widths = groupRefs.current.map(ref => calculateGroupWidth(ref));
84+
setGroupWidths(widths);
85+
}, [calculateGroupWidth]);
86+
87+
// Update visible count based on available width
88+
const updateVisibleCount = useCallback(() => {
89+
if (!groupWidths.length || !availableWidth) return;
90+
91+
const OVERFLOW_BUTTON_WIDTH = 32; // Reduced from 48px
92+
const MIN_GROUP_WIDTH = 80; // Reduced from 100px
93+
const SEPARATOR_WIDTH = 8; // Width of the InputSeparator
94+
let used = 0;
95+
let count = 0;
96+
97+
for (let i = 0; i < groupWidths.length; i++) {
98+
const width = groupWidths[i] ?? 0;
99+
if (width < MIN_GROUP_WIDTH) continue;
100+
101+
// Add separator width if this isn't the first group
102+
const totalWidth = width + (count > 0 ? SEPARATOR_WIDTH : 0);
103+
104+
if (used + totalWidth <= availableWidth - OVERFLOW_BUTTON_WIDTH) {
105+
used += totalWidth;
106+
count++;
107+
} else {
108+
break;
109+
}
110+
}
111+
112+
setVisibleCount(count);
113+
}, [groupWidths, availableWidth]);
114+
115+
// Measure group widths after mount and when groupRefs change
116+
useEffect(() => {
117+
measureGroups();
118+
}, [measureGroups, availableWidth]);
119+
120+
// Update visible count when measurements change
121+
useEffect(() => {
122+
updateVisibleCount();
123+
}, [updateVisibleCount]);
124+
125+
const visibleGroups = DIV_SELECTED_GROUPS.slice(0, visibleCount);
126+
const overflowGroups = DIV_SELECTED_GROUPS.slice(visibleCount);
127+
16128
return (
17-
<div className="flex items-center gap-0.5">
18-
{/* <StateDropdown /> */}
19-
<Width />
20-
<Height />
21-
<InputSeparator />
22-
<Display />
23-
<Padding />
24-
<Margin />
25-
<InputSeparator />
26-
<Radius />
27-
<Border />
28-
<InputSeparator />
29-
<ColorBackground />
30-
<InputSeparator />
31-
<ImageBackground />
32-
<ViewButtons />
33-
</div>
129+
<>
130+
{/* Hidden measurement container */}
131+
<div style={{ position: 'absolute', visibility: 'hidden', height: 0, overflow: 'hidden', pointerEvents: 'none' }}>
132+
{DIV_SELECTED_GROUPS.map((group, groupIdx) => (
133+
<div
134+
key={group.key}
135+
className="flex items-center justify-center gap-0.5"
136+
ref={el => { groupRefs.current[groupIdx] = el; }}
137+
>
138+
{group.components.map((compKey, idx) => {
139+
const Comp = COMPONENT_MAP[compKey];
140+
return Comp ? <Comp key={compKey + idx} /> : null;
141+
})}
142+
</div>
143+
))}
144+
</div>
145+
<div
146+
className="flex items-center justify-center gap-0.5 w-full overflow-hidden"
147+
>
148+
{DIV_SELECTED_GROUPS.map((group, groupIdx) => (
149+
groupIdx < visibleCount ? (
150+
<React.Fragment key={group.key}>
151+
{groupIdx > 0 && <InputSeparator />}
152+
<div className="flex items-center justify-center gap-0.5">
153+
{group.components.map((compKey, idx) => {
154+
const Comp = COMPONENT_MAP[compKey];
155+
return Comp ? <Comp key={compKey + idx} /> : null;
156+
})}
157+
</div>
158+
</React.Fragment>
159+
) : null
160+
))}
161+
{overflowGroups.length > 0 && visibleCount > 0 && <InputSeparator />}
162+
{overflowGroups.length > 0 && (
163+
<Popover open={overflowOpen} onOpenChange={setOverflowOpen}>
164+
<PopoverTrigger asChild>
165+
<Button
166+
variant="ghost"
167+
size="toolbar"
168+
className="w-8 h-8 flex items-center justify-center"
169+
aria-label="Show more toolbar controls"
170+
>
171+
<Icons.DotsHorizontal className="w-5 h-5" />
172+
</Button>
173+
</PopoverTrigger>
174+
<PopoverContent align="end" className="flex flex-row gap-1 p-1 px-1 bg-background rounded-lg shadow-xl shadow-black/20 min-w-[fit-content] items-center w-[fit-content]">
175+
{overflowGroups.map((group, groupIdx) => (
176+
<React.Fragment key={group.key}>
177+
{groupIdx > 0 && <InputSeparator />}
178+
<div className="flex items-center gap-0.5">
179+
{group.components.map((compKey, idx) => {
180+
const Comp = COMPONENT_MAP[compKey];
181+
return Comp ? <Comp key={compKey + idx} /> : null;
182+
})}
183+
</div>
184+
</React.Fragment>
185+
))}
186+
</PopoverContent>
187+
</Popover>
188+
)}
189+
</div>
190+
</>
34191
);
35192
};

apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/border.tsx

Lines changed: 43 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,30 @@ import { Icons } from "@onlook/ui/icons";
1111
import { Color } from "@onlook/utility";
1212
import { useEffect, useState } from "react";
1313
import { useBoxControl } from "../hooks/use-box-control";
14+
import { HoverOnlyTooltip } from "../HoverOnlyTooltip";
1415
import { InputColor } from "../inputs/input-color";
1516
import { InputRange } from "../inputs/input-range";
1617
import { SpacingInputs } from "../inputs/spacing-inputs";
17-
import { HoverOnlyTooltip } from "../HoverOnlyTooltip";
1818

1919
export const Border = () => {
2020
const editorEngine = useEditorEngine();
2121
const initialColor = editorEngine.style.selectedStyle?.styles.computed.borderColor;
2222
const [activeTab, setActiveTab] = useState('all');
2323
const { boxState, handleBoxChange, handleUnitChange, handleIndividualChange } =
2424
useBoxControl('border');
25-
const [borderColor, setBorderColor] = useState<string>(Color.from(initialColor ?? '#080808').toHex());
25+
const [borderColor, setBorderColor] = useState<string>('#080808');
26+
const [open, setOpen] = useState(false);
27+
28+
useEffect(() => {
29+
const color = editorEngine.style.selectedStyle?.styles.computed.borderColor;
30+
if (color) {
31+
setBorderColor(
32+
Color.from(
33+
color ?? '#080808',
34+
).toHex(),
35+
);
36+
}
37+
}, [editorEngine.style.selectedStyle?.styles.computed.borderColor]);
2638

2739
const handleColorChange = (color: string) => {
2840
setBorderColor(color);
@@ -39,35 +51,35 @@ export const Border = () => {
3951
};
4052

4153
return (
42-
<DropdownMenu>
43-
<HoverOnlyTooltip content="Border" side="bottom" className="mt-1" hideArrow>
54+
<DropdownMenu open={open} onOpenChange={setOpen}>
55+
<HoverOnlyTooltip content="Border" side="bottom" className="mt-1" hideArrow disabled={open}>
4456
<DropdownMenuTrigger asChild>
4557
<Button
46-
variant="ghost"
47-
size="toolbar"
48-
className="flex items-center gap-1 text-muted-foreground hover:text-foreground border border-border/0 cursor-pointer rounded-lg hover:bg-background-tertiary/20 hover:text-white hover:border hover:border-border data-[state=open]:bg-background-tertiary/20 data-[state=open]:text-white data-[state=open]:border data-[state=open]:border-border focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none focus-visible:outline-none active:border-0 data-[state=open]:border data-[state=open]:text-white"
49-
>
50-
<Icons.BorderEdit className="h-4 w-4 min-h-4 min-w-4" />
51-
{boxState.borderWidth.unit !== 'px' && typeof boxState.borderWidth.num === 'number' && boxState.borderWidth.num !== 0 ? (
52-
<span className="text-small">{boxState.borderWidth.num}</span>
53-
) : null}
54-
{boxState.borderWidth.unit !== 'px' && boxState.borderWidth.value ? (
55-
<span className="text-small">{boxState.borderWidth.value}</span>
56-
) : null}
57-
{(boxState.borderWidth.num ?? 0) > 0 ||
58-
(boxState.borderTopWidth?.num ?? 0) > 0 ||
59-
(boxState.borderRightWidth?.num ?? 0) > 0 ||
60-
(boxState.borderBottomWidth?.num ?? 0) > 0 ||
61-
(boxState.borderLeftWidth?.num ?? 0) > 0 ? (
62-
<div
63-
className="w-5 h-5 rounded-md"
64-
style={borderStyle}
65-
/>
66-
) : null}
67-
</Button>
58+
variant="ghost"
59+
size="toolbar"
60+
className="flex items-center gap-1 text-muted-foreground hover:text-foreground border border-border/0 cursor-pointer rounded-lg hover:bg-background-tertiary/20 hover:text-white hover:border hover:border-border data-[state=open]:bg-background-tertiary/20 data-[state=open]:text-white data-[state=open]:border data-[state=open]:border-border focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none focus-visible:outline-none active:border-0 data-[state=open]:border data-[state=open]:text-white"
61+
>
62+
<Icons.BorderEdit className="h-4 w-4 min-h-4 min-w-4" />
63+
{boxState.borderWidth.unit !== 'px' && typeof boxState.borderWidth.num === 'number' && boxState.borderWidth.num !== 0 ? (
64+
<span className="text-small">{boxState.borderWidth.num}</span>
65+
) : null}
66+
{boxState.borderWidth.unit !== 'px' && boxState.borderWidth.value ? (
67+
<span className="text-small">{boxState.borderWidth.value}</span>
68+
) : null}
69+
{(boxState.borderWidth.num ?? 0) > 0 ||
70+
(boxState.borderTopWidth?.num ?? 0) > 0 ||
71+
(boxState.borderRightWidth?.num ?? 0) > 0 ||
72+
(boxState.borderBottomWidth?.num ?? 0) > 0 ||
73+
(boxState.borderLeftWidth?.num ?? 0) > 0 ? (
74+
<div
75+
className="w-5 h-5 rounded-md"
76+
style={borderStyle}
77+
/>
78+
) : null}
79+
</Button>
6880
</DropdownMenuTrigger>
6981
</HoverOnlyTooltip>
70-
<DropdownMenuContent align="start" className="w-[280px] mt-1 p-3 rounded-lg">
82+
<DropdownMenuContent align="center" side="bottom" className="w-[280px] mt-1 p-3 rounded-lg">
7183
<div className="flex items-center gap-2 mb-3">
7284
<button
7385
onClick={() => setActiveTab('all')}
@@ -114,10 +126,10 @@ export const Border = () => {
114126
(boxState.borderBottomWidth.num ?? 0) > 0 ||
115127
(boxState.borderLeftWidth.num ?? 0) > 0)
116128
) && (
117-
<div className="mt-3">
118-
<InputColor color={borderColor} elementStyleKey="borderColor" onColorChange={handleColorChange} />
119-
</div>
120-
)}
129+
<div className="mt-3">
130+
<InputColor color={borderColor} elementStyleKey="borderColor" onColorChange={handleColorChange} />
131+
</div>
132+
)}
121133
</DropdownMenuContent>
122134
</DropdownMenu>
123135
);

apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/height.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const Height = () => {
2828
) && (
2929
<span className="text-small">
3030
{dimensionState.height.unit === 'px'
31-
? dimensionState.height.num
31+
? Math.round(dimensionState.height.num ?? 0)
3232
: dimensionState.height.value}
3333
</span>
3434
)}

0 commit comments

Comments
 (0)