diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-linux.png index fa6944cb7..00ad5661d 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-linux.png index 036ca2ab2..a3ac15813 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-linux.png differ diff --git a/src/Toolbar.tsx b/src/Toolbar.tsx index 6bc8a2536..ea8a775d8 100644 --- a/src/Toolbar.tsx +++ b/src/Toolbar.tsx @@ -1,4 +1,4 @@ -import { useRef, useMemo, memo } from 'react' +import { useRef, useMemo, memo, useCallback, useState } from 'react' import { isCursorInSketchCommandRange } from 'lang/util' import { engineCommandManager, kclManager } from 'lib/singletons' import { useModelingContext } from 'hooks/useModelingContext' @@ -34,8 +34,7 @@ export function Toolbar({ const bgClassName = '!bg-transparent' const buttonBgClassName = 'bg-chalkboard-transparent dark:bg-transparent disabled:bg-transparent dark:disabled:bg-transparent enabled:hover:bg-chalkboard-10 dark:enabled:hover:bg-chalkboard-100 pressed:!bg-primary pressed:enabled:hover:!text-chalkboard-10' - const buttonBorderClassName = - '!border-transparent hover:!border-chalkboard-20 dark:enabled:hover:!border-primary pressed:!border-primary ui-open:!border-primary' + const buttonBorderClassName = '!border-transparent' const sketchPathId = useMemo(() => { if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast)) @@ -50,6 +49,7 @@ export function Toolbar({ const { overallState } = useNetworkContext() const { isExecuting } = useKclContext() const { isStreamReady } = useAppState() + const [showRichContent, setShowRichContent] = useState(false) const disableAllButtons = (overallState !== NetworkHealthState.Ok && @@ -77,6 +77,40 @@ export function Toolbar({ [state, send, commandBarSend, sketchPathId] ) + const tooltipContentClassName = !showRichContent + ? '' + : '!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch' + const richContentTimeout = useRef(null) + const richContentClearTimeout = useRef(null) + // On mouse enter, show rich content after a 1s delay + const handleMouseEnter = useCallback(() => { + // Cancel the clear timeout if it's already set + if (richContentClearTimeout.current) { + clearTimeout(richContentClearTimeout.current) + } + // Start our own timeout to show the rich content + richContentTimeout.current = window.setTimeout(() => { + setShowRichContent(true) + if (richContentClearTimeout.current) { + clearTimeout(richContentClearTimeout.current) + } + }, 1000) + }, [setShowRichContent]) + // On mouse leave, clear the timeout and hide rich content + const handleMouseLeave = useCallback(() => { + // Clear the timeout to show rich content + if (richContentTimeout.current) { + clearTimeout(richContentTimeout.current) + } + // Start a timeout to hide the rich content + richContentClearTimeout.current = window.setTimeout(() => { + setShowRichContent(false) + if (richContentClearTimeout.current) { + clearTimeout(richContentClearTimeout.current) + } + }, 500) + }, [setShowRichContent]) + /** * Resolve all the callbacks and values for the current mode, * so we don't need to worry about the other modes @@ -174,44 +208,64 @@ export function Toolbar({ status: itemConfig.status, }))} > - - maybeIconConfig[0].onClick(configCallbackProps) - } +
- + maybeIconConfig[0].onClick(configCallbackProps) + } > - {maybeIconConfig[0].title} - - - + + {maybeIconConfig[0].title} + + + {showRichContent ? ( + + ) : ( + + )} + + +
) } @@ -219,7 +273,13 @@ export function Toolbar({ // A single button return ( -
+
+ contentClassName={tooltipContentClassName} + > + {showRichContent ? ( + + ) : ( + + )} +
) })} @@ -270,6 +341,12 @@ export function Toolbar({ ) } +interface ToolbarItemContentsProps extends React.PropsWithChildren { + itemConfig: ToolbarItemResolved + configCallbackProps: ToolbarItemCallbackProps + wrapperClassName?: string + contentClassName?: string +} /** * The single button and dropdown button share content, so we extract it here * It contains a tooltip with the title, description, and links @@ -278,14 +355,10 @@ export function Toolbar({ const ToolbarItemTooltip = memo(function ToolbarItemContents({ itemConfig, configCallbackProps, - className, -}: { - itemConfig: ToolbarItemResolved - configCallbackProps: ToolbarItemCallbackProps - className?: string -}) { - const { state } = useModelingContext() - + wrapperClassName = '', + contentClassName = '', + children, +}: ToolbarItemContentsProps) { useHotkeys( itemConfig.hotkey || '', () => { @@ -310,10 +383,48 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({ } hoverOnly position="bottom" - wrapperClassName={'!p-4 !pointer-events-auto ' + className} - contentClassName="!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch" + wrapperClassName={'!p-4 !pointer-events-auto ' + wrapperClassName} + contentClassName={contentClassName} + delay={0} > + {children} + + ) +}) + +const ToolbarItemTooltipShortContent = ({ + status, + title, + hotkey, +}: { + status: string + title: string + hotkey?: string | string[] +}) => ( + + {title} + {hotkey && ( + {hotkey} + )} + +) + +const ToolbarItemTooltipRichContent = ({ + itemConfig, +}: { + itemConfig: ToolbarItemResolved +}) => { + const { state } = useModelingContext() + return ( + <>
+ {itemConfig.icon && ( + + )} )} - + ) -}) +} diff --git a/src/components/ActionButtonDropdown.tsx b/src/components/ActionButtonDropdown.tsx index 21ee82f91..18973fb51 100644 --- a/src/components/ActionButtonDropdown.tsx +++ b/src/components/ActionButtonDropdown.tsx @@ -1,9 +1,11 @@ import { Popover } from '@headlessui/react' import { ActionButtonProps } from './ActionButton' import { CustomIcon } from './CustomIcon' +import Tooltip from './Tooltip' type ActionButtonSplitProps = ActionButtonProps & { Element: 'button' } & { name?: string + dropdownTooltipText?: string splitMenuItems: { id: string label: string @@ -17,6 +19,7 @@ type ActionButtonSplitProps = ActionButtonProps & { Element: 'button' } & { export function ActionButtonDropdown({ splitMenuItems, className, + dropdownTooltipText = 'More tools', children, ...props }: ActionButtonSplitProps) { @@ -26,7 +29,14 @@ export function ActionButtonDropdown({ {({ close }) => ( <> {children} - + {props.name ? props.name + ': ' : ''}open menu + + {dropdownTooltipText} + = { status: 'available', title: 'Offset plane', description: 'Create a plane parallel to an existing plane.', - links: [], + links: [ + { + label: 'KCL docs', + url: 'https://zoo.dev/docs/kcl/offsetPlane', + }, + ], }, { id: 'plane-points', @@ -305,7 +310,12 @@ export const toolbarConfig: Record = { status: 'available', title: 'Text-to-CAD', description: 'Generate geometry from a text prompt.', - links: [], + links: [ + { + label: 'API docs', + url: 'https://zoo.dev/docs/api/ml/generate-a-cad-model-from-text', + }, + ], }, { id: 'prompt-to-edit',