diff --git a/apps/studio/src/index.css b/apps/studio/src/index.css index ada838599..309937f1d 100644 --- a/apps/studio/src/index.css +++ b/apps/studio/src/index.css @@ -10,6 +10,7 @@ html body #root { body { font-family: 'Inter Variable', sans-serif; + user-select: none; } *:focus-visible { diff --git a/apps/studio/src/routes/editor/EditPanel/StylesTab/index.tsx b/apps/studio/src/routes/editor/EditPanel/StylesTab/index.tsx index 15ad7a05b..c6854a14e 100644 --- a/apps/studio/src/routes/editor/EditPanel/StylesTab/index.tsx +++ b/apps/studio/src/routes/editor/EditPanel/StylesTab/index.tsx @@ -130,10 +130,10 @@ const ManualTab = observer(() => { function renderStyleSections() { return Object.entries(STYLE_GROUP_MAPPING).map(([groupKey, baseElementStyles]) => ( - + {renderAccordianHeader(groupKey)} - + {groupKey === StyleGroupKey.Text && } {renderGroupValues(baseElementStyles)} diff --git a/apps/studio/src/routes/editor/EditPanel/StylesTab/single/NumberUnitInput.tsx b/apps/studio/src/routes/editor/EditPanel/StylesTab/single/NumberUnitInput.tsx index dc66cd285..ffab26e1b 100644 --- a/apps/studio/src/routes/editor/EditPanel/StylesTab/single/NumberUnitInput.tsx +++ b/apps/studio/src/routes/editor/EditPanel/StylesTab/single/NumberUnitInput.tsx @@ -130,7 +130,7 @@ const NumberUnitInput = observer( const renderUnitInput = () => { return ( -
+
-
+
diff --git a/apps/studio/src/routes/editor/EditPanel/StylesTab/single/SelectInput.tsx b/apps/studio/src/routes/editor/EditPanel/StylesTab/single/SelectInput.tsx index a860da29b..3181923f2 100644 --- a/apps/studio/src/routes/editor/EditPanel/StylesTab/single/SelectInput.tsx +++ b/apps/studio/src/routes/editor/EditPanel/StylesTab/single/SelectInput.tsx @@ -113,7 +113,9 @@ const SelectInput = observer( if (elementStyle.params.options.length <= 3 || ICON_SELECTION.includes(elementStyle.key)) { return ( el.domId === node.data.domId); const instanceId = node.data.instanceId; const component = node.data.component; + const isParentSelected = parentSelected(node); + const isParentGroupEnd = parentGroupEnd(node); + const isComponentAncestor = hasComponentAncestor(node); + const isText = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes( + node.data.tagName.toLowerCase(), + ); function handleHoverNode(e: React.MouseEvent) { if (hovered) { @@ -138,6 +144,16 @@ const TreeNode = observer( node.data.isVisible = !node.data.isVisible; } + function hasComponentAncestor(node: NodeApi): boolean { + if (!node) { + return false; + } + if (node.data.instanceId) { + return true; + } + return node.parent ? hasComponentAncestor(node.parent) : false; + } + return ( @@ -149,18 +165,27 @@ const TreeNode = observer( onMouseOver={(e) => handleHoverNode(e)} className={twMerge( cn('flex flex-row items-center h-6 cursor-pointer w-full pr-1', { + 'text-purple-600 dark:text-purple-300': + isComponentAncestor && !instanceId && !hovered, + 'text-purple-500 dark:text-purple-200': + isComponentAncestor && !instanceId && hovered, + 'text-foreground-onlook': + !isComponentAncestor && + !instanceId && + !selected && + !hovered, rounded: - (hovered && !parentSelected(node) && !selected) || + (hovered && !isParentSelected && !selected) || (selected && node.isLeaf) || (selected && node.isClosed), 'rounded-t': selected && node.isInternal, - 'rounded-b': parentSelected(node) && parentGroupEnd(node), - 'rounded-none': parentSelected(node) && node.nextSibling, + 'rounded-b': isParentSelected && isParentGroupEnd, + 'rounded-none': isParentSelected && node.nextSibling, 'bg-background-onlook': hovered, 'bg-[#FA003C] dark:bg-[#FA003C]/90': selected, - 'bg-[#FA003C]/10 dark:bg-[#FA003C]/10': parentSelected(node), + 'bg-[#FA003C]/10 dark:bg-[#FA003C]/10': isParentSelected, 'bg-[#FA003C]/20 dark:bg-[#FA003C]/20': - hovered && parentSelected(node), + hovered && isParentSelected, 'text-purple-100 dark:text-purple-100': instanceId && selected, 'text-purple-500 dark:text-purple-300': instanceId && !selected, 'text-purple-800 dark:text-purple-200': @@ -168,24 +193,26 @@ const TreeNode = observer( 'bg-purple-700/70 dark:bg-purple-500/50': instanceId && selected, 'bg-purple-400/30 dark:bg-purple-900/60': - instanceId && !selected && hovered && !parentSelected(node), + instanceId && !selected && hovered && !isParentSelected, 'bg-purple-300/30 dark:bg-purple-900/30': - parentSelected(node)?.data.instanceId, + isParentSelected?.data.instanceId, 'bg-purple-300/50 dark:bg-purple-900/50': - hovered && parentSelected(node)?.data.instanceId, + hovered && isParentSelected?.data.instanceId, 'text-white dark:text-primary': !instanceId && selected, - 'text-hover': !instanceId && !selected && hovered, - 'text-foreground-onlook': !instanceId && !selected && !hovered, }), )} > - + {!node.isLeaf && (
node.toggle()} + className="w-4 h-4 flex items-center justify-center absolute z-50" + onMouseDown={(e) => { + node.select(); + sendMouseEvent(e, node.data, MouseAction.MOUSE_DOWN); + node.toggle(); + }} > - {treeHovered && ( + {hovered && ( ) : ( @@ -222,8 +269,8 @@ const TreeNode = observer( ? selected ? 'text-purple-100 dark:text-purple-100' : hovered - ? 'text-purple-600 dark:text-purple-200' - : 'text-purple-500 dark:text-purple-300' + ? 'text-purple-600 dark:text-purple-200' + : 'text-purple-500 dark:text-purple-300' : '', !node.data.isVisible && 'opacity-80', selected && 'mr-5', @@ -232,10 +279,10 @@ const TreeNode = observer( {component ? component : ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p'].includes( - node.data.tagName.toLowerCase(), - ) - ? '' - : node.data.tagName.toLowerCase()} + node.data.tagName.toLowerCase(), + ) + ? '' + : node.data.tagName.toLowerCase()} {' ' + node.data.textContent} {selected && ( diff --git a/apps/studio/src/routes/editor/Toolbar/Terminal/RunButton.tsx b/apps/studio/src/routes/editor/Toolbar/Terminal/RunButton.tsx index cabc9b8de..01a51f86f 100644 --- a/apps/studio/src/routes/editor/Toolbar/Terminal/RunButton.tsx +++ b/apps/studio/src/routes/editor/Toolbar/Terminal/RunButton.tsx @@ -6,8 +6,13 @@ import { cn } from '@onlook/ui/utils'; import { AnimatePresence, motion } from 'framer-motion'; import { observer } from 'mobx-react-lite'; import { useMemo, useState } from 'react'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@onlook/ui/tooltip'; -const RunButton = observer(() => { +interface RunButtonProps { + setTerminalHidden: (hidden: boolean) => void; +} + +const RunButton = observer(({ setTerminalHidden }: RunButtonProps) => { const projectsManager = useProjectsManager(); const runner = projectsManager.runner; const [isLoading, setIsLoading] = useState(false); @@ -37,12 +42,15 @@ const RunButton = observer(() => { } if (runner.state === RunState.STOPPED) { - runner.start(); startLoadingTimer(); + runner.start(); + setTerminalHidden(false); } else if (runner.state === RunState.RUNNING) { runner.stop(); } else if (runner.state === RunState.ERROR) { + startLoadingTimer(); runner.restart(); + setTerminalHidden(false); } else { console.error('Unexpected state:', runner.state); } @@ -88,50 +96,87 @@ const RunButton = observer(() => { const buttonCharacters = useMemo(() => { const text = getButtonTitle(); const characters = text.split('').map((ch, index) => ({ - id: `${ch}${index}`, + id: `runbutton_${ch}${index}`, label: index === 0 ? ch.toUpperCase() : ch, })); return characters; }, [runner?.state, isLoading]); + const buttonText = getButtonTitle(); + const buttonWidth = useMemo(() => { + // Base width for icon + padding + const baseWidth = 44; + return baseWidth + buttonText.length * 7; + }, [buttonText]); + + function getTooltipText() { + switch (runner?.state) { + case RunState.STOPPED: + return 'Run your app'; + case RunState.RUNNING: + return 'Stop Running your App & Clean Code'; + default: + return ''; + } + } + return ( - + + + + + +

{getTooltipText()}

+
+
+
); }); diff --git a/apps/studio/src/routes/editor/Toolbar/index.tsx b/apps/studio/src/routes/editor/Toolbar/index.tsx index 5cee89461..db78388b0 100644 --- a/apps/studio/src/routes/editor/Toolbar/index.tsx +++ b/apps/studio/src/routes/editor/Toolbar/index.tsx @@ -11,6 +11,7 @@ import { useEffect, useState } from 'react'; import Terminal from './Terminal'; import RunButton from './Terminal/RunButton'; import { Hotkey } from '/common/hotkeys'; +import { motion, AnimatePresence } from 'framer-motion'; const TOOLBAR_ITEMS: { mode: EditorMode; @@ -98,85 +99,107 @@ const Toolbar = observer(() => { }; return ( -
- {!terminalHidden ? ( - // Terminal header when expanded -
- - Terminal - -
- - - - - - Toggle Terminal - -
-
- ) : ( - // Regular toolbar when terminal is hidden -
- { - if (value) { - editorEngine.mode = value as EditorMode; - setMode(value as EditorMode); - } - }} - > - {TOOLBAR_ITEMS.map((item) => ( - + + {editorEngine.mode !== EditorMode.INTERACT && ( + + {!terminalHidden ? ( + + + Terminal + +
+ + + + + + + + Toggle Terminal + +
+
+ ) : ( + + { + if (value) { + editorEngine.mode = value as EditorMode; + setMode(value as EditorMode); + } + }} + > + {TOOLBAR_ITEMS.map((item) => ( + + +
handleDragStart(e, item.mode)} + > + + + +
+
+ + + +
+ ))} +
+ + + + -
handleDragStart(e, item.mode)} +
+ +
- - - + Toggle Terminal
- ))} -
- - - - - - Toggle Terminal - -
+ + )} +
+ ); }); diff --git a/apps/studio/src/routes/editor/TopBar/OpenCode/index.tsx b/apps/studio/src/routes/editor/TopBar/OpenCode/index.tsx index 33a4cce37..f3ecd1a2e 100644 --- a/apps/studio/src/routes/editor/TopBar/OpenCode/index.tsx +++ b/apps/studio/src/routes/editor/TopBar/OpenCode/index.tsx @@ -76,7 +76,7 @@ const OpenCode = observer(() => { const ideCharacters = useMemo(() => { const prefixChars = 'Open in '.split('').map((ch, index) => ({ - id: `prefix_${index}`, + id: `opencode_prefix_${index}`, label: ch === ' ' ? '\u00A0' : ch, })); const entities = `${ide}`.split('').map((ch) => ch); @@ -87,7 +87,7 @@ const OpenCode = observer(() => { const count = entities.slice(0, index).filter((e) => e === entity).length; characters.push({ - id: `${entity}${count + 1}`, + id: `opencode_${entity}${count + 1}`, label: characters.length === 0 ? entity.toUpperCase() : entity, }); } diff --git a/apps/studio/src/routes/editor/TopBar/ProjectSelect/index.tsx b/apps/studio/src/routes/editor/TopBar/ProjectSelect/index.tsx index c194fe8da..7f5bed338 100644 --- a/apps/studio/src/routes/editor/TopBar/ProjectSelect/index.tsx +++ b/apps/studio/src/routes/editor/TopBar/ProjectSelect/index.tsx @@ -18,7 +18,7 @@ import ProjectNameInput from './ProjectNameInput'; const ProjectBreadcrumb = observer(() => { const editorEngine = useEditorEngine(); const projectsManager = useProjectsManager(); - const [isDirectoryHovered, setIsDirectoryHovered] = useState(false); + const [isSettingsOpen, setIsSettingsOpen] = useState(false); async function handleReturn() { await saveScreenshot(); @@ -74,30 +74,29 @@ const ProjectBreadcrumb = observer(() => { - setIsDirectoryHovered(true)} - onMouseLeave={() => setIsDirectoryHovered(false)} - > -
- {isDirectoryHovered ? ( - - ) : ( - - )} + +
+ + {'Open Project Folder'}
- e.preventDefault()}> - -
- Project Settings -
-
+ setIsSettingsOpen(true)}> +
+ + Project Settings +
+ + {null} + ); }); diff --git a/apps/studio/src/routes/editor/TopBar/ZoomControls/index.tsx b/apps/studio/src/routes/editor/TopBar/ZoomControls/index.tsx index f6b3806ec..cd326215f 100644 --- a/apps/studio/src/routes/editor/TopBar/ZoomControls/index.tsx +++ b/apps/studio/src/routes/editor/TopBar/ZoomControls/index.tsx @@ -83,9 +83,9 @@ const ZoomControls = observer( return (
- + {Math.round(scale * 100)}% - + { + ({ + children, + project, + open: controlledOpen, + onOpenChange: controlledOnOpenChange, + }: { + children: React.ReactNode; + project?: Project | null; + open?: boolean; + onOpenChange?: (open: boolean) => void; + }) => { const projectsManager = useProjectsManager(); - const [isOpen, setIsOpen] = useState(false); const projectToUpdate = project || projectsManager.project; const [formValues, setFormValues] = useState({ name: projectToUpdate?.name || '', @@ -25,6 +34,12 @@ const ProjectSettingsModal = observer( runCommand: projectToUpdate?.runCommand || 'npm run dev', }); + const [uncontrolledOpen, setUncontrolledOpen] = useState(false); + + // Use controlled props if provided, otherwise use internal state + const isOpen = controlledOpen ?? uncontrolledOpen; + const onOpenChange = controlledOnOpenChange ?? setUncontrolledOpen; + const handleChange = (e: React.ChangeEvent) => { setFormValues({ ...formValues, @@ -39,19 +54,19 @@ const ProjectSettingsModal = observer( ...formValues, }); } - setIsOpen(false); + onOpenChange?.(false); }; return ( - + {children} Project Settings -
+
-
-
-
-