Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { type Frame } from '@onlook/models';
import { Icons } from '@onlook/ui/icons';
import { colors } from '@onlook/ui/tokens';
import { observer } from 'mobx-react-lite';
import { useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { RightClickMenu } from '../../right-click-menu';
import { GestureScreen } from './gesture';
import { ResizeHandles } from './resize-handles';
Expand All @@ -13,10 +13,37 @@ import { useFrameReload } from './use-frame-reload';
import { useSandboxTimeout } from './use-sandbox-timeout';
import { FrameComponent, type IFrameView } from './view';

const LOADING_MESSAGES = [
'Starting up your project...',
'This may take a minute or two...',
'Initializing development environment...',
'Tip: Use SHIFT+Click to add multiple elements on the canvas to your prompt',
'If you have a large project, it may take a while...',
'Tip: Click the "Branch" icon to create a new version of your project on the canvas',
'Preparing the visual editor...',
'Tip: Double-click on an element to open it up in the code editor',
'Hang in there... seems like a large project...',
'Thanks for your patience... standby...',
'Loading your components and assets...',
'Tip: Select multiple windows by clicking and dragging on the canvas',
'Getting everything ready for you...',
'Give it another minute...',
'Hmmmmm...',
'You may want to try refreshing your tab...',
'Still not loading? Try refreshing your browser...',
'If you\'re seeing this message, it\'s probably because your project is large...',
'Onlook is still working on it...',
'If you\'re seeing this message, it\'s probably because your project is large...',
'If it\'s still not loading, contact support with the ? button in the bottom left corner',
'If it\'s still not loading, contact support with the ? button in the bottom left corner',
'If it\'s still not loading, contact support with the ? button in the bottom left corner',
];

export const FrameView = observer(({ frame, isInDragSelection = false }: { frame: Frame; isInDragSelection?: boolean }) => {
const editorEngine = useEditorEngine();
const iFrameRef = useRef<IFrameView>(null);
const [isResizing, setIsResizing] = useState(false);
const [messageIndex, setMessageIndex] = useState(0);

const {
reloadKey,
Expand All @@ -33,6 +60,14 @@ export const FrameView = observer(({ frame, isInDragSelection = false }: { frame
const preloadScriptReady = branchData?.sandbox?.preloadScriptState === PreloadScriptState.INJECTED;
const isFrameReady = preloadScriptReady && !(isConnecting && !hasTimedOut);

useEffect(() => {
const interval = setInterval(() => {
setMessageIndex((prev) => (prev + 1) % LOADING_MESSAGES.length);
}, 12000);

return () => clearInterval(interval);
}, []);

return (
<div
className="flex flex-col fixed"
Expand Down Expand Up @@ -74,10 +109,18 @@ export const FrameView = observer(({ frame, isInDragSelection = false }: { frame
}}
>
<div
className="flex items-center gap-3 text-foreground"
style={{ transform: `scale(${1 / editorEngine.canvas.scale})` }}
className="flex flex-col items-center gap-3 text-foreground"
style={{
transform: `scale(${1 / editorEngine.canvas.scale})`,
width: `${frame.dimension.width * editorEngine.canvas.scale}px`,
maxWidth: `${frame.dimension.width * editorEngine.canvas.scale}px`,
padding: '0 16px'
}}
>
<Icons.LoadingSpinner className="animate-spin h-8 w-8" />
<p className="text-sm text-center bg-gradient-to-l from-white/20 via-white/90 to-white/20 bg-[length:200%_100%] bg-clip-text text-transparent animate-shimmer filter drop-shadow-[0_0_10px_rgba(255,255,255,0.4)]">
{LOADING_MESSAGES[messageIndex]}
</p>
</div>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const BranchDisplay = observer(({ frame, tooltipSide = "top", buttonSize
</DropdownMenuTrigger>
</HoverOnlyTooltip>
<DropdownMenuSeparator />
<DropdownMenuContent align="start" className="w-[320px] p-0">
<DropdownMenuContent align="start" className="w-[200px] p-0">
<BranchControls
branch={frameBranch}
onClose={() => setIsOpen(false)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export const TopBar = observer(
<Icons.ArrowRight />
</Button>
</HoverOnlyTooltip>
<HoverOnlyTooltip content="Refresh Page" side="top" className="mb-1" hideArrow>
<HoverOnlyTooltip content="Refresh Page" side="top" className="mb-2" hideArrow>
<Button
variant="ghost"
size="sm"
Expand All @@ -201,7 +201,7 @@ export const TopBar = observer(
<span className={cn("ml-1.25 mb-0.5", isSelected ? "text-teal-700" : "text-foreground-secondary/50")}>·</span>
<PageSelector frame={frame} />
</div>
<HoverOnlyTooltip content="Preview in new tab" side="top" hideArrow className="mb-1">
<HoverOnlyTooltip content="Preview in new tab" side="top" hideArrow className="mb-0">
<Link
className={cn(
'absolute right-1 top-1/2 -translate-y-1/2 transition-opacity duration-300',
Expand Down
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the start of the multi-side / all side dropdown adjustment

Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,31 @@ export const Border = observer(() => {

const [activeTab, setActiveTab] = useState<BorderTab>(areAllBordersEqual ? BorderTab.ALL : BorderTab.INDIVIDUAL);

// Track if user is actively interacting with the input
const [isUserInteracting, setIsUserInteracting] = useState(false);

// Determine if we should show "Mixed" in the input when on "All sides" tab
const shouldShowMixed = activeTab === BorderTab.ALL && !areAllBordersEqual && !isUserInteracting;

// Custom onChange handler that tracks user interaction
const handleBorderChange = (value: number) => {
setIsUserInteracting(true);
handleBoxChange('borderWidth', value.toString());
};

// Reset interaction state when switching tabs or closing dropdown
const handleTabChange = (tab: BorderTab) => {
setActiveTab(tab);
setIsUserInteracting(false);
};

const handleOpenChange = (open: boolean) => {
onOpenChange(open);
if (!open) {
setIsUserInteracting(false);
}
};

const getBorderDisplay = () => {
const top = boxState.borderTopWidth.num ?? 0;
const right = boxState.borderRightWidth.num ?? 0;
Expand All @@ -64,7 +89,7 @@ export const Border = observer(() => {
const borderValue = getBorderDisplay()

return (
<DropdownMenu open={isOpen} onOpenChange={onOpenChange} modal={false}>
<DropdownMenu open={isOpen} onOpenChange={handleOpenChange} modal={false}>
<HoverOnlyTooltip
content="Border"
side="bottom"
Expand Down Expand Up @@ -93,7 +118,7 @@ export const Border = observer(() => {
>
<div className="flex items-center gap-2 mb-3">
<button
onClick={() => setActiveTab(BorderTab.ALL)}
onClick={() => handleTabChange(BorderTab.ALL)}
className={`flex-1 text-sm px-4 py-1.5 rounded-md transition-colors cursor-pointer ${activeTab === BorderTab.ALL
? 'text-foreground-primary bg-background-active/50'
: 'text-muted-foreground hover:bg-background-tertiary/20 hover:text-foreground-hover'
Expand All @@ -102,7 +127,7 @@ export const Border = observer(() => {
All sides
</button>
<button
onClick={() => setActiveTab(BorderTab.INDIVIDUAL)}
onClick={() => handleTabChange(BorderTab.INDIVIDUAL)}
className={`flex-1 text-sm px-4 py-1.5 rounded-md transition-colors cursor-pointer ${activeTab === BorderTab.INDIVIDUAL
? 'text-foreground-primary bg-background-active/50'
: 'text-muted-foreground hover:bg-background-tertiary/20 hover:text-foreground-hover'
Expand All @@ -113,10 +138,11 @@ export const Border = observer(() => {
</div>
{activeTab === BorderTab.ALL ? (
<InputRange
value={boxState.borderWidth.num ?? 0}
onChange={(value) => handleBoxChange('borderWidth', value.toString())}
value={shouldShowMixed ? 0 : (boxState.borderWidth.num ?? 0)}
onChange={handleBorderChange}
unit={boxState.borderWidth.unit}
onUnitChange={(unit) => handleUnitChange('borderWidth', unit)}
displayValue={shouldShowMixed ? 'Mixed' : undefined}
/>
) : (
<SpacingInputs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,31 @@ export const Margin = observer(() => {

const [activeTab, setActiveTab] = useState<MarginTab>(areAllMarginsEqual ? MarginTab.ALL : MarginTab.INDIVIDUAL);

// Track if user is actively interacting with the input
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the border component, the margin component duplicates isUserInteracting logic. Consider refactoring this into a shared hook.

const [isUserInteracting, setIsUserInteracting] = useState(false);

// Determine if we should show "Mixed" in the input when on "All sides" tab
const shouldShowMixed = activeTab === MarginTab.ALL && !areAllMarginsEqual && !isUserInteracting;

// Custom onChange handler that tracks user interaction
const handleMarginChange = (value: number) => {
setIsUserInteracting(true);
handleBoxChange('margin', value.toString());
};

// Reset interaction state when switching tabs or closing dropdown
const handleTabChange = (tab: MarginTab) => {
setActiveTab(tab);
setIsUserInteracting(false);
};

const handleOpenChange = (open: boolean) => {
onOpenChange(open);
if (!open) {
setIsUserInteracting(false);
}
};

const getMarginIcon = () => {
const margins = {
top: boxState.marginTop.num ?? 0,
Expand Down Expand Up @@ -148,7 +173,7 @@ export const Margin = observer(() => {
const marginValue = getMarginDisplay();

return (
<DropdownMenu open={isOpen} onOpenChange={onOpenChange} modal={false}>
<DropdownMenu open={isOpen} onOpenChange={handleOpenChange} modal={false}>
<HoverOnlyTooltip content="Margin" side="bottom" className="mt-1" hideArrow disabled={isOpen}>
<DropdownMenuTrigger asChild>
<ToolbarButton
Expand All @@ -168,16 +193,16 @@ export const Margin = observer(() => {
>
<div className="mb-3 flex items-center gap-2">
<button
onClick={() => setActiveTab(MarginTab.ALL)}
onClick={() => handleTabChange(MarginTab.ALL)}
className={`flex-1 cursor-pointer rounded-md px-4 py-1.5 text-sm transition-colors ${activeTab === MarginTab.ALL
? "bg-background-active/50 text-foreground-primary"
: "text-muted-foreground hover:bg-background-tertiary/20 hover:text-foreground-hover"
}`}
>
{areAllMarginsEqual ? "All sides" : "Mixed"}
All sides
</button>
<button
onClick={() => setActiveTab(MarginTab.INDIVIDUAL)}
onClick={() => handleTabChange(MarginTab.INDIVIDUAL)}
className={`flex-1 cursor-pointer rounded-md px-4 py-1.5 text-sm transition-colors ${activeTab === MarginTab.INDIVIDUAL
? "bg-background-active/50 text-foreground-primary"
: "text-muted-foreground hover:bg-background-tertiary/20 hover:text-foreground-hover"
Expand All @@ -188,10 +213,11 @@ export const Margin = observer(() => {
</div>
{activeTab === MarginTab.ALL ? (
<InputRange
value={boxState.margin.num ?? 0}
onChange={(value) => handleBoxChange('margin', value.toString())}
value={shouldShowMixed ? 0 : (boxState.margin.num ?? 0)}
onChange={handleMarginChange}
unit={boxState.margin.unit}
onUnitChange={(unit) => handleUnitChange('margin', unit)}
displayValue={shouldShowMixed ? 'Mixed' : undefined}
/>
) : (
<SpacingInputs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,31 @@ export const Padding = observer(() => {

const [activeTab, setActiveTab] = useState<PaddingTab>(areAllPaddingsEqual ? PaddingTab.ALL : PaddingTab.INDIVIDUAL);

// Track if user is actively interacting with the input
const [isUserInteracting, setIsUserInteracting] = useState(false);

// Determine if we should show "Mixed" in the input when on "All sides" tab
const shouldShowMixed = activeTab === PaddingTab.ALL && !areAllPaddingsEqual && !isUserInteracting;

// Custom onChange handler that tracks user interaction
const handlePaddingChange = (value: number) => {
setIsUserInteracting(true);
handleBoxChange('padding', value.toString());
};

// Reset interaction state when switching tabs or closing dropdown
const handleTabChange = (tab: PaddingTab) => {
setActiveTab(tab);
setIsUserInteracting(false);
};

const handleOpenChange = (open: boolean) => {
onOpenChange(open);
if (!open) {
setIsUserInteracting(false);
}
};

const getPaddingIcon = () => {
const paddings = {
top: boxState.paddingTop.num ?? 0,
Expand Down Expand Up @@ -123,7 +148,7 @@ export const Padding = observer(() => {
const paddingValue = getPaddingDisplay();

return (
<DropdownMenu open={isOpen} onOpenChange={onOpenChange} modal={false}>
<DropdownMenu open={isOpen} onOpenChange={handleOpenChange} modal={false}>
<HoverOnlyTooltip content="Padding" side="bottom">
<DropdownMenuTrigger asChild>
<ToolbarButton
Expand All @@ -140,16 +165,16 @@ export const Padding = observer(() => {
<DropdownMenuContent align="start" className="w-[280px] mt-1 p-3 rounded-lg">
<div className="flex items-center gap-2 mb-3">
<button
onClick={() => setActiveTab(PaddingTab.ALL)}
onClick={() => handleTabChange(PaddingTab.ALL)}
className={`flex-1 text-sm px-4 py-1.5 rounded-md transition-colors cursor-pointer ${activeTab === PaddingTab.ALL
? 'text-foreground-primary bg-background-active/50'
: 'text-muted-foreground hover:bg-background-tertiary/20 hover:text-foreground-hover'
}`}
>
{areAllPaddingsEqual ? "All sides" : "Mixed"}
All sides
</button>
<button
onClick={() => setActiveTab(PaddingTab.INDIVIDUAL)}
onClick={() => handleTabChange(PaddingTab.INDIVIDUAL)}
className={`flex-1 text-sm px-4 py-1.5 rounded-md transition-colors cursor-pointer ${activeTab === PaddingTab.INDIVIDUAL
? 'text-foreground-primary bg-background-active/50'
: 'text-muted-foreground hover:bg-background-tertiary/20 hover:text-foreground-hover'
Expand All @@ -160,10 +185,11 @@ export const Padding = observer(() => {
</div>
{activeTab === PaddingTab.ALL ? (
<InputRange
value={boxState.padding.num ?? 0}
onChange={(value) => handleBoxChange('padding', value.toString())}
value={shouldShowMixed ? 0 : (boxState.padding.num ?? 0)}
onChange={handlePaddingChange}
unit={boxState.padding.unit}
onUnitChange={(unit) => handleUnitChange('padding', unit)}
displayValue={shouldShowMixed ? 'Mixed' : undefined}
/>
) : (
<SpacingInputs
Expand Down
Loading
Loading