Tweaks to clarify tooltips from tool dropdown menus (#5123)

* Separate content from ToolbarItemTooltip, make simple and "rich" versions

* Add support for dropdown-arrow-only tooltip

* Add toolbar-wide hover timeouts and clears to switch between simple and rich tooltips

* Fix the dropdown arrow button hover styling now that they're separate

* Add missing doc links to rich toolbar tooltips

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Re-run CI after snapshots

* fix codespell

* fmt

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Frank Noirot
2025-01-21 18:32:56 -05:00
committed by GitHub
parent 2692f2b73a
commit 67cc4f5835
5 changed files with 195 additions and 56 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View File

@ -1,4 +1,4 @@
import { useRef, useMemo, memo } from 'react' import { useRef, useMemo, memo, useCallback, useState } from 'react'
import { isCursorInSketchCommandRange } from 'lang/util' import { isCursorInSketchCommandRange } from 'lang/util'
import { engineCommandManager, kclManager } from 'lib/singletons' import { engineCommandManager, kclManager } from 'lib/singletons'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
@ -34,8 +34,7 @@ export function Toolbar({
const bgClassName = '!bg-transparent' const bgClassName = '!bg-transparent'
const buttonBgClassName = 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' '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 = const buttonBorderClassName = '!border-transparent'
'!border-transparent hover:!border-chalkboard-20 dark:enabled:hover:!border-primary pressed:!border-primary ui-open:!border-primary'
const sketchPathId = useMemo(() => { const sketchPathId = useMemo(() => {
if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast)) if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast))
@ -50,6 +49,7 @@ export function Toolbar({
const { overallState } = useNetworkContext() const { overallState } = useNetworkContext()
const { isExecuting } = useKclContext() const { isExecuting } = useKclContext()
const { isStreamReady } = useAppState() const { isStreamReady } = useAppState()
const [showRichContent, setShowRichContent] = useState(false)
const disableAllButtons = const disableAllButtons =
(overallState !== NetworkHealthState.Ok && (overallState !== NetworkHealthState.Ok &&
@ -77,6 +77,40 @@ export function Toolbar({
[state, send, commandBarSend, sketchPathId] [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<number | null>(null)
const richContentClearTimeout = useRef<number | null>(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, * Resolve all the callbacks and values for the current mode,
* so we don't need to worry about the other modes * so we don't need to worry about the other modes
@ -173,6 +207,12 @@ export function Toolbar({
itemConfig.disabled === true, itemConfig.disabled === true,
status: itemConfig.status, status: itemConfig.status,
}))} }))}
>
<div
className="contents"
// Mouse events do not fire on disabled buttons
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
> >
<ActionButton <ActionButton
Element="button" Element="button"
@ -206,12 +246,26 @@ export function Toolbar({
> >
{maybeIconConfig[0].title} {maybeIconConfig[0].title}
</span> </span>
</ActionButton>
<ToolbarItemTooltip <ToolbarItemTooltip
itemConfig={maybeIconConfig[0]} itemConfig={maybeIconConfig[0]}
configCallbackProps={configCallbackProps} configCallbackProps={configCallbackProps}
className="ui-open:!hidden" wrapperClassName="ui-open:!hidden"
contentClassName={tooltipContentClassName}
>
{showRichContent ? (
<ToolbarItemTooltipRichContent
itemConfig={maybeIconConfig[0]}
/> />
) : (
<ToolbarItemTooltipShortContent
status={maybeIconConfig[0].status}
title={maybeIconConfig[0].title}
hotkey={maybeIconConfig[0].hotkey}
/>
)}
</ToolbarItemTooltip>
</ActionButton>
</div>
</ActionButtonDropdown> </ActionButtonDropdown>
) )
} }
@ -219,7 +273,13 @@ export function Toolbar({
// A single button // A single button
return ( return (
<div className="relative" key={itemConfig.id}> <div
className="relative"
key={itemConfig.id}
// Mouse events do not fire on disabled buttons
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<ActionButton <ActionButton
Element="button" Element="button"
key={itemConfig.id} key={itemConfig.id}
@ -256,7 +316,18 @@ export function Toolbar({
<ToolbarItemTooltip <ToolbarItemTooltip
itemConfig={itemConfig} itemConfig={itemConfig}
configCallbackProps={configCallbackProps} configCallbackProps={configCallbackProps}
contentClassName={tooltipContentClassName}
>
{showRichContent ? (
<ToolbarItemTooltipRichContent itemConfig={itemConfig} />
) : (
<ToolbarItemTooltipShortContent
status={itemConfig.status}
title={itemConfig.title}
hotkey={itemConfig.hotkey}
/> />
)}
</ToolbarItemTooltip>
</div> </div>
) )
})} })}
@ -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 * The single button and dropdown button share content, so we extract it here
* It contains a tooltip with the title, description, and links * It contains a tooltip with the title, description, and links
@ -278,14 +355,10 @@ export function Toolbar({
const ToolbarItemTooltip = memo(function ToolbarItemContents({ const ToolbarItemTooltip = memo(function ToolbarItemContents({
itemConfig, itemConfig,
configCallbackProps, configCallbackProps,
className, wrapperClassName = '',
}: { contentClassName = '',
itemConfig: ToolbarItemResolved children,
configCallbackProps: ToolbarItemCallbackProps }: ToolbarItemContentsProps) {
className?: string
}) {
const { state } = useModelingContext()
useHotkeys( useHotkeys(
itemConfig.hotkey || '', itemConfig.hotkey || '',
() => { () => {
@ -310,10 +383,48 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
} }
hoverOnly hoverOnly
position="bottom" position="bottom"
wrapperClassName={'!p-4 !pointer-events-auto ' + className} wrapperClassName={'!p-4 !pointer-events-auto ' + wrapperClassName}
contentClassName="!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch" contentClassName={contentClassName}
delay={0}
> >
{children}
</Tooltip>
)
})
const ToolbarItemTooltipShortContent = ({
status,
title,
hotkey,
}: {
status: string
title: string
hotkey?: string | string[]
}) => (
<span
className={`text-sm ${
status !== 'available' ? 'text-chalkboard-70 dark:text-chalkboard-40' : ''
}`}
>
{title}
{hotkey && (
<kbd className="inline-block ml-2 flex-none hotkey">{hotkey}</kbd>
)}
</span>
)
const ToolbarItemTooltipRichContent = ({
itemConfig,
}: {
itemConfig: ToolbarItemResolved
}) => {
const { state } = useModelingContext()
return (
<>
<div className="rounded-top flex items-center gap-2 pt-3 pb-2 px-2 bg-chalkboard-20/50 dark:bg-chalkboard-80/50"> <div className="rounded-top flex items-center gap-2 pt-3 pb-2 px-2 bg-chalkboard-20/50 dark:bg-chalkboard-80/50">
{itemConfig.icon && (
<CustomIcon className="w-5 h-5" name={itemConfig.icon} />
)}
<span <span
className={`text-sm flex-1 ${ className={`text-sm flex-1 ${
itemConfig.status !== 'available' itemConfig.status !== 'available'
@ -382,6 +493,6 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
</ul> </ul>
</> </>
)} )}
</Tooltip> </>
) )
}) }

View File

@ -1,9 +1,11 @@
import { Popover } from '@headlessui/react' import { Popover } from '@headlessui/react'
import { ActionButtonProps } from './ActionButton' import { ActionButtonProps } from './ActionButton'
import { CustomIcon } from './CustomIcon' import { CustomIcon } from './CustomIcon'
import Tooltip from './Tooltip'
type ActionButtonSplitProps = ActionButtonProps & { Element: 'button' } & { type ActionButtonSplitProps = ActionButtonProps & { Element: 'button' } & {
name?: string name?: string
dropdownTooltipText?: string
splitMenuItems: { splitMenuItems: {
id: string id: string
label: string label: string
@ -17,6 +19,7 @@ type ActionButtonSplitProps = ActionButtonProps & { Element: 'button' } & {
export function ActionButtonDropdown({ export function ActionButtonDropdown({
splitMenuItems, splitMenuItems,
className, className,
dropdownTooltipText = 'More tools',
children, children,
...props ...props
}: ActionButtonSplitProps) { }: ActionButtonSplitProps) {
@ -26,7 +29,14 @@ export function ActionButtonDropdown({
{({ close }) => ( {({ close }) => (
<> <>
{children} {children}
<Popover.Button className="border-transparent dark:border-transparent p-0 m-0 rounded-none !outline-none ui-open:border-primary ui-open:bg-primary"> <Popover.Button
className={
'!border-transparent dark:!border-transparent ' +
'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 p-0 m-0 rounded-none !outline-none ui-open:border-primary ui-open:bg-primary'
}
>
<CustomIcon <CustomIcon
name="caretDown" name="caretDown"
className={ className={
@ -37,6 +47,14 @@ export function ActionButtonDropdown({
<span className="sr-only"> <span className="sr-only">
{props.name ? props.name + ': ' : ''}open menu {props.name ? props.name + ': ' : ''}open menu
</span> </span>
<Tooltip
delay={0}
position="bottom"
hoverOnly
wrapperClassName="ui-open:!hidden"
>
{dropdownTooltipText}
</Tooltip>
</Popover.Button> </Popover.Button>
<Popover.Panel <Popover.Panel
as="ul" as="ul"

View File

@ -280,7 +280,12 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
status: 'available', status: 'available',
title: 'Offset plane', title: 'Offset plane',
description: 'Create a plane parallel to an existing 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', id: 'plane-points',
@ -305,7 +310,12 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
status: 'available', status: 'available',
title: 'Text-to-CAD', title: 'Text-to-CAD',
description: 'Generate geometry from a text prompt.', 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', id: 'prompt-to-edit',