Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI Sanding #878

Merged
merged 14 commits into from
Dec 13, 2024
Merged
Changes from all 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
1 change: 1 addition & 0 deletions apps/studio/src/index.css
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@ html body #root {

body {
font-family: 'Inter Variable', sans-serif;
user-select: none;
}

*:focus-visible {
4 changes: 2 additions & 2 deletions apps/studio/src/routes/editor/EditPanel/StylesTab/index.tsx
Original file line number Diff line number Diff line change
@@ -130,10 +130,10 @@ const ManualTab = observer(() => {
function renderStyleSections() {
return Object.entries(STYLE_GROUP_MAPPING).map(([groupKey, baseElementStyles]) => (
<AccordionItem key={groupKey} value={groupKey}>
<AccordionTrigger className="mx-0">
<AccordionTrigger className=" mb-[-4px] mt-[-2px]">
{renderAccordianHeader(groupKey)}
</AccordionTrigger>
<AccordionContent>
<AccordionContent className="mt-2px">
{groupKey === StyleGroupKey.Text && <TagDetails />}
{renderGroupValues(baseElementStyles)}
</AccordionContent>
Original file line number Diff line number Diff line change
@@ -130,7 +130,7 @@ const NumberUnitInput = observer(

const renderUnitInput = () => {
return (
<div className="relative w-full">
<div className="relative w-full group">
<select
value={unitValue}
className="p-[6px] w-full px-2 rounded border-none text-foreground-active bg-background-onlook/75 text-start appearance-none focus:outline-none focus:ring-0"
@@ -142,7 +142,7 @@ const NumberUnitInput = observer(
</option>
))}
</select>
<div className="text-foreground-onlook absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<div className="text-foreground-onlook group-hover:text-foreground-hover absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<Icons.ChevronDown />
</div>
</div>
Original file line number Diff line number Diff line change
@@ -113,7 +113,9 @@ const SelectInput = observer(
if (elementStyle.params.options.length <= 3 || ICON_SELECTION.includes(elementStyle.key)) {
return (
<ToggleGroup
className="w-32 overflow-hidden"
className={`w-32 overflow-hidden ${
ICON_SELECTION.includes(elementStyle.key) ? 'gap-0.75' : ''
}`}
size="sm"
type="single"
value={value}
91 changes: 69 additions & 22 deletions apps/studio/src/routes/editor/LayersPanel/Tree/TreeNode.tsx
Original file line number Diff line number Diff line change
@@ -30,6 +30,12 @@ const TreeNode = observer(
const selected = editorEngine.elements.selected.some((el) => 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<HTMLDivElement>) {
if (hovered) {
@@ -138,6 +144,16 @@ const TreeNode = observer(
node.data.isVisible = !node.data.isVisible;
}

function hasComponentAncestor(node: NodeApi<LayerNode>): boolean {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a very expensive operation. With a large enough tree, the app will grind to a halt.

Copy link
Contributor

Choose a reason for hiding this comment

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

Fixed! Good thing too because I added some extra performance fix thanks to this.

if (!node) {
return false;
}
if (node.data.instanceId) {
return true;
}
return node.parent ? hasComponentAncestor(node.parent) : false;
}

return (
<Tooltip>
<TooltipTrigger asChild>
@@ -149,43 +165,54 @@ 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':
instanceId && !selected && hovered,
'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,
}),
)}
>
<span className="w-4 h-4 flex-none">
<span className="w-4 h-4 flex-none relative">
{!node.isLeaf && (
<div
className="w-4 h-4 flex items-center justify-center"
onClick={() => node.toggle()}
className="w-4 h-4 flex items-center justify-center absolute z-50"
onMouseDown={(e) => {
Kitenite marked this conversation as resolved.
Show resolved Hide resolved
node.select();
sendMouseEvent(e, node.data, MouseAction.MOUSE_DOWN);
node.toggle();
}}
>
{treeHovered && (
{hovered && (
<motion.div
initial={false}
animate={{ rotate: node.isOpen ? 90 : 0 }}
@@ -203,14 +230,34 @@ const TreeNode = observer(
hovered && !selected
? 'text-purple-600 dark:text-purple-200 '
: selected
? 'text-purple-100 dark:text-purple-100'
: 'text-purple-500 dark:text-purple-300',
? 'text-purple-100 dark:text-purple-100'
: 'text-purple-500 dark:text-purple-300',
)}
/>
) : (
<NodeIcon
iconClass={cn('w-3 h-3 ml-1 mr-2 flex-none', {
'fill-white dark:fill-primary': !instanceId && selected,
'[&_path]:!fill-purple-400 [&_path]:!dark:fill-purple-300':
isComponentAncestor &&
!instanceId &&
!selected &&
!hovered &&
!isText,
'[&_path]:!fill-purple-300 [&_path]:!dark:fill-purple-200':
isComponentAncestor &&
!instanceId &&
!selected &&
hovered &&
!isText,
'[&_path]:!fill-white [&_path]:!dark:fill-primary':
isComponentAncestor && !instanceId && selected,
'[&_.letter]:!fill-foreground/50 [&_.level]:!fill-foreground dark:[&_.letter]:!fill-foreground/50 dark:[&_.level]:!fill-foreground':
!isComponentAncestor && !selected && isText,
'[&_.letter]:!fill-purple-400/50 [&_.level]:!fill-purple-400 dark:[&_.letter]:!fill-purple-300/50 dark:[&_.level]:!fill-purple-300':
isComponentAncestor && !selected && !hovered && isText,
'[&_.letter]:!fill-purple-300/50 [&_.level]:!fill-purple-300 dark:[&_.letter]:!fill-purple-200/50 dark:[&_.level]:!fill-purple-200':
isComponentAncestor && !selected && hovered && isText,
})}
node={node.data}
/>
@@ -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}
</span>
{selected && (
123 changes: 84 additions & 39 deletions apps/studio/src/routes/editor/Toolbar/Terminal/RunButton.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Button
variant="ghost"
className={cn(
'h-11 -my-2 border-transparent rounded-none w-20 px-3 gap-x-1.5 transition-colors duration-300 z-8 relative',
getExtraButtonClasses(),
)}
disabled={
isLoading ||
runner?.state === RunState.SETTING_UP ||
runner?.state === RunState.STOPPING
}
onClick={handleButtonClick}
<motion.div
layout="preserve-aspect"
animate={{ width: buttonWidth }}
transition={{
type: 'spring',
bounce: 0.2,
duration: 0.6,
stiffness: 150,
damping: 20,
}}
>
<div className="z-10">{renderIcon()}</div>
<span className="text-mini z-10 relative">
<AnimatePresence mode="popLayout">
{buttonCharacters.map((character) => (
<motion.span
key={character.id}
layoutId={character.id}
layout="position"
className="inline-block"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{
type: 'spring',
bounce: 0.1,
duration: 0.4,
}}
>
{character.label}
</motion.span>
))}
</AnimatePresence>
</span>
</Button>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
className={cn(
'h-11 -my-2 border-transparent rounded-none px-3 gap-x-1.5 transition-colors duration-300 z-8 relative whitespace-nowrap overflow-hidden',
getExtraButtonClasses(),
)}
disabled={
isLoading ||
runner?.state === RunState.SETTING_UP ||
runner?.state === RunState.STOPPING
}
onClick={handleButtonClick}
>
<div className="z-10">{renderIcon()}</div>
<span className="text-mini z-10 relative">
<AnimatePresence mode="popLayout">
{buttonCharacters.map((character) => (
<motion.span
key={character.id}
layoutId={character.id}
layout="position"
className="inline-block"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{
type: 'spring',
bounce: 0.1,
duration: 0.4,
}}
>
{character.label}
</motion.span>
))}
</AnimatePresence>
</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{getTooltipText()}</p>
</TooltipContent>
</Tooltip>
</motion.div>
);
});

Loading