2025-04-01 23:54:26 -07:00
|
|
|
import { memo, useCallback, useMemo, useRef, useState } from 'react'
|
2025-04-01 15:31:19 -07:00
|
|
|
import { useHotkeys } from 'react-hotkeys-hook'
|
2025-04-01 23:54:26 -07:00
|
|
|
|
|
|
|
import { useAppState } from '@src/AppState'
|
|
|
|
import { ActionButton } from '@src/components/ActionButton'
|
|
|
|
import { ActionButtonDropdown } from '@src/components/ActionButtonDropdown'
|
|
|
|
import { CustomIcon } from '@src/components/CustomIcon'
|
|
|
|
import Tooltip from '@src/components/Tooltip'
|
|
|
|
import { useModelingContext } from '@src/hooks/useModelingContext'
|
|
|
|
import { useNetworkContext } from '@src/hooks/useNetworkContext'
|
|
|
|
import { NetworkHealthState } from '@src/hooks/useNetworkStatus'
|
|
|
|
import { useKclContext } from '@src/lang/KclProvider'
|
|
|
|
import { isCursorInFunctionDefinition } from '@src/lang/queryAst'
|
|
|
|
import { isCursorInSketchCommandRange } from '@src/lang/util'
|
|
|
|
import { isDesktop } from '@src/lib/isDesktop'
|
|
|
|
import { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
|
|
|
|
import { editorManager, kclManager } from '@src/lib/singletons'
|
|
|
|
import type {
|
2024-07-24 23:33:31 -04:00
|
|
|
ToolbarItem,
|
|
|
|
ToolbarItemCallbackProps,
|
|
|
|
ToolbarItemResolved,
|
|
|
|
ToolbarModeName,
|
2025-04-01 23:54:26 -07:00
|
|
|
} from '@src/lib/toolbar'
|
|
|
|
import { toolbarConfig } from '@src/lib/toolbar'
|
|
|
|
import { isArray } from '@src/lib/utils'
|
|
|
|
import { commandBarActor } from '@src/machines/commandBarMachine'
|
2023-09-16 01:23:11 -04:00
|
|
|
|
2024-05-24 20:54:42 +10:00
|
|
|
export function Toolbar({
|
|
|
|
className = '',
|
|
|
|
...props
|
|
|
|
}: React.HTMLAttributes<HTMLElement>) {
|
2023-10-11 13:36:54 +11:00
|
|
|
const { state, send, context } = useModelingContext()
|
2024-04-05 00:59:02 -04:00
|
|
|
const iconClassName =
|
2024-07-24 23:33:31 -04:00
|
|
|
'group-disabled:text-chalkboard-50 !text-inherit dark:group-enabled:group-hover:!text-inherit'
|
|
|
|
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'
|
2025-01-21 18:32:56 -05:00
|
|
|
const buttonBorderClassName = '!border-transparent'
|
2024-07-24 23:33:31 -04:00
|
|
|
|
|
|
|
const sketchPathId = useMemo(() => {
|
2025-02-15 00:57:04 +11:00
|
|
|
if (
|
|
|
|
isCursorInFunctionDefinition(
|
|
|
|
kclManager.ast,
|
|
|
|
context.selectionRanges.graphSelections[0]
|
|
|
|
)
|
|
|
|
)
|
2024-02-19 17:23:03 +11:00
|
|
|
return false
|
|
|
|
return isCursorInSketchCommandRange(
|
2025-03-29 17:25:26 -07:00
|
|
|
kclManager.artifactGraph,
|
2024-02-19 17:23:03 +11:00
|
|
|
context.selectionRanges
|
|
|
|
)
|
2025-03-29 17:25:26 -07:00
|
|
|
}, [kclManager.artifactGraph, context.selectionRanges])
|
2024-05-24 20:54:42 +10:00
|
|
|
|
|
|
|
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
2024-06-04 08:32:24 -04:00
|
|
|
const { overallState } = useNetworkContext()
|
2024-02-26 21:02:33 +11:00
|
|
|
const { isExecuting } = useKclContext()
|
2024-07-03 09:22:46 +10:00
|
|
|
const { isStreamReady } = useAppState()
|
2025-01-21 18:32:56 -05:00
|
|
|
const [showRichContent, setShowRichContent] = useState(false)
|
2024-07-03 09:22:46 +10:00
|
|
|
|
2024-02-26 21:02:33 +11:00
|
|
|
const disableAllButtons =
|
2024-06-04 08:32:24 -04:00
|
|
|
(overallState !== NetworkHealthState.Ok &&
|
|
|
|
overallState !== NetworkHealthState.Weak) ||
|
|
|
|
isExecuting ||
|
|
|
|
!isStreamReady
|
2023-10-04 12:35:50 -04:00
|
|
|
|
2024-07-24 23:33:31 -04:00
|
|
|
const currentMode =
|
|
|
|
(Object.entries(toolbarConfig).find(([_, mode]) =>
|
|
|
|
mode.check(state)
|
|
|
|
)?.[0] as ToolbarModeName) || 'modeling'
|
|
|
|
|
|
|
|
/** These are the props that will be passed to the callbacks in the toolbar config
|
|
|
|
* They are memoized to prevent unnecessary re-renders,
|
|
|
|
* but they still get a lot of churn from the state machine
|
|
|
|
* so I think there's a lot of room for improvement here
|
|
|
|
*/
|
|
|
|
const configCallbackProps: ToolbarItemCallbackProps = useMemo(
|
|
|
|
() => ({
|
2024-09-09 19:59:36 +03:00
|
|
|
modelingState: state,
|
2024-07-24 23:33:31 -04:00
|
|
|
modelingSend: send,
|
|
|
|
sketchPathId,
|
2025-03-20 13:10:28 -04:00
|
|
|
editorHasFocus: editorManager.editorView?.hasFocus,
|
2024-07-24 23:33:31 -04:00
|
|
|
}),
|
2025-03-20 13:10:28 -04:00
|
|
|
[
|
|
|
|
state,
|
|
|
|
send,
|
|
|
|
commandBarActor.send,
|
|
|
|
sketchPathId,
|
|
|
|
editorManager.editorView?.hasFocus,
|
|
|
|
]
|
2024-07-15 19:20:32 +10:00
|
|
|
)
|
2024-05-22 11:07:02 -04:00
|
|
|
|
2025-01-21 18:32:56 -05:00
|
|
|
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])
|
|
|
|
|
2024-07-24 23:33:31 -04:00
|
|
|
/**
|
|
|
|
* Resolve all the callbacks and values for the current mode,
|
|
|
|
* so we don't need to worry about the other modes
|
|
|
|
*/
|
|
|
|
const currentModeItems: (
|
|
|
|
| ToolbarItemResolved
|
|
|
|
| ToolbarItemResolved[]
|
|
|
|
| 'break'
|
|
|
|
)[] = useMemo(() => {
|
|
|
|
return toolbarConfig[currentMode].items.map((maybeIconConfig) => {
|
|
|
|
if (maybeIconConfig === 'break') {
|
|
|
|
return 'break'
|
2025-02-05 09:01:45 -05:00
|
|
|
} else if (isArray(maybeIconConfig)) {
|
2024-07-24 23:33:31 -04:00
|
|
|
return maybeIconConfig.map(resolveItemConfig)
|
|
|
|
} else {
|
|
|
|
return resolveItemConfig(maybeIconConfig)
|
|
|
|
}
|
|
|
|
})
|
2023-10-04 12:35:50 -04:00
|
|
|
|
2024-07-24 23:33:31 -04:00
|
|
|
function resolveItemConfig(
|
|
|
|
maybeIconConfig: ToolbarItem
|
|
|
|
): ToolbarItemResolved {
|
2024-10-28 20:52:51 -04:00
|
|
|
const isDisabled =
|
|
|
|
disableAllButtons ||
|
|
|
|
maybeIconConfig.status !== 'available' ||
|
|
|
|
maybeIconConfig.disabled?.(state) === true
|
|
|
|
|
2024-07-24 23:33:31 -04:00
|
|
|
return {
|
|
|
|
...maybeIconConfig,
|
|
|
|
title:
|
|
|
|
typeof maybeIconConfig.title === 'string'
|
|
|
|
? maybeIconConfig.title
|
|
|
|
: maybeIconConfig.title(configCallbackProps),
|
|
|
|
description: maybeIconConfig.description,
|
|
|
|
links: maybeIconConfig.links || [],
|
|
|
|
isActive: maybeIconConfig.isActive?.(state),
|
|
|
|
hotkey:
|
|
|
|
typeof maybeIconConfig.hotkey === 'string'
|
|
|
|
? maybeIconConfig.hotkey
|
|
|
|
: maybeIconConfig.hotkey?.(state),
|
2024-10-28 20:52:51 -04:00
|
|
|
disabled: isDisabled,
|
|
|
|
disabledReason:
|
|
|
|
typeof maybeIconConfig.disabledReason === 'function'
|
|
|
|
? maybeIconConfig.disabledReason(state)
|
|
|
|
: maybeIconConfig.disabledReason,
|
2024-07-24 23:33:31 -04:00
|
|
|
disableHotkey: maybeIconConfig.disableHotkey?.(state),
|
|
|
|
status: maybeIconConfig.status,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, [currentMode, disableAllButtons, configCallbackProps])
|
2023-02-12 10:56:45 +11:00
|
|
|
|
2024-05-24 20:54:42 +10:00
|
|
|
return (
|
2025-02-28 15:50:01 +11:00
|
|
|
<menu
|
2025-03-18 11:14:12 +11:00
|
|
|
data-current-mode={currentMode}
|
2025-02-28 15:50:01 +11:00
|
|
|
className="max-w-full whitespace-nowrap rounded-b px-2 py-1 bg-chalkboard-10 dark:bg-chalkboard-90 relative border border-chalkboard-30 dark:border-chalkboard-80 border-t-0 shadow-sm"
|
|
|
|
>
|
2023-12-06 14:44:13 -05:00
|
|
|
<ul
|
|
|
|
{...props}
|
2023-10-04 12:35:50 -04:00
|
|
|
ref={toolbarButtonsRef}
|
2024-07-24 23:33:31 -04:00
|
|
|
className={
|
|
|
|
'has-[[aria-expanded=true]]:!pointer-events-none m-0 py-1 rounded-l-sm flex gap-1.5 items-center ' +
|
|
|
|
className
|
|
|
|
}
|
2023-10-04 12:35:50 -04:00
|
|
|
>
|
2024-07-24 23:33:31 -04:00
|
|
|
{/* A menu item will either be a vertical line break, a button with a dropdown, or a single button */}
|
|
|
|
{currentModeItems.map((maybeIconConfig, i) => {
|
2024-11-18 10:04:09 -05:00
|
|
|
// Vertical Line Break
|
2024-07-24 23:33:31 -04:00
|
|
|
if (maybeIconConfig === 'break') {
|
|
|
|
return (
|
|
|
|
<div
|
|
|
|
key={'break-' + i}
|
|
|
|
className="h-5 w-[1px] block bg-chalkboard-30 dark:bg-chalkboard-80"
|
|
|
|
/>
|
|
|
|
)
|
2025-02-05 09:01:45 -05:00
|
|
|
} else if (isArray(maybeIconConfig)) {
|
2024-11-18 10:04:09 -05:00
|
|
|
// A button with a dropdown
|
2024-07-24 23:33:31 -04:00
|
|
|
return (
|
|
|
|
<ActionButtonDropdown
|
2024-02-11 12:59:00 +11:00
|
|
|
Element="button"
|
2024-07-24 23:33:31 -04:00
|
|
|
key={maybeIconConfig[0].id}
|
|
|
|
data-testid={maybeIconConfig[0].id + '-dropdown'}
|
|
|
|
id={maybeIconConfig[0].id + '-dropdown'}
|
|
|
|
name={maybeIconConfig[0].title}
|
|
|
|
className={
|
2025-03-21 11:07:23 -04:00
|
|
|
(maybeIconConfig[0].alwaysDark
|
|
|
|
? 'dark bg-chalkboard-90 '
|
|
|
|
: '!bg-transparent ') +
|
2024-07-24 23:33:31 -04:00
|
|
|
'group/wrapper ' +
|
|
|
|
buttonBorderClassName +
|
2025-03-21 11:07:23 -04:00
|
|
|
' relative group !gap-0'
|
2024-02-11 12:59:00 +11:00
|
|
|
}
|
2024-07-24 23:33:31 -04:00
|
|
|
splitMenuItems={maybeIconConfig.map((itemConfig) => ({
|
|
|
|
id: itemConfig.id,
|
|
|
|
label: itemConfig.title,
|
|
|
|
hotkey: itemConfig.hotkey,
|
|
|
|
onClick: () => itemConfig.onClick(configCallbackProps),
|
|
|
|
disabled:
|
|
|
|
disableAllButtons ||
|
|
|
|
itemConfig.status !== 'available' ||
|
|
|
|
itemConfig.disabled === true,
|
|
|
|
status: itemConfig.status,
|
|
|
|
}))}
|
2024-02-11 12:59:00 +11:00
|
|
|
>
|
2025-01-21 18:32:56 -05:00
|
|
|
<div
|
|
|
|
className="contents"
|
|
|
|
// Mouse events do not fire on disabled buttons
|
|
|
|
onMouseEnter={handleMouseEnter}
|
|
|
|
onMouseLeave={handleMouseLeave}
|
2024-05-22 11:07:02 -04:00
|
|
|
>
|
2025-01-21 18:32:56 -05:00
|
|
|
<ActionButton
|
|
|
|
Element="button"
|
|
|
|
id={maybeIconConfig[0].id}
|
|
|
|
data-testid={maybeIconConfig[0].id}
|
|
|
|
iconStart={{
|
|
|
|
icon: maybeIconConfig[0].icon,
|
2025-03-21 11:07:23 -04:00
|
|
|
iconColor: maybeIconConfig[0].iconColor,
|
2025-01-21 18:32:56 -05:00
|
|
|
className: iconClassName,
|
|
|
|
bgClassName: bgClassName,
|
|
|
|
}}
|
|
|
|
className={
|
|
|
|
'!border-transparent !px-0 pressed:!text-chalkboard-10 pressed:enabled:hovered:!text-chalkboard-10 ' +
|
|
|
|
buttonBgClassName
|
|
|
|
}
|
|
|
|
aria-pressed={maybeIconConfig[0].isActive}
|
|
|
|
disabled={
|
|
|
|
disableAllButtons ||
|
|
|
|
maybeIconConfig[0].status !== 'available' ||
|
|
|
|
maybeIconConfig[0].disabled
|
|
|
|
}
|
|
|
|
name={maybeIconConfig[0].title}
|
|
|
|
// aria-description is still in ARIA 1.3 draft.
|
|
|
|
// eslint-disable-next-line jsx-a11y/aria-props
|
|
|
|
aria-description={maybeIconConfig[0].description}
|
|
|
|
onClick={() =>
|
|
|
|
maybeIconConfig[0].onClick(configCallbackProps)
|
|
|
|
}
|
2024-08-02 12:25:57 -04:00
|
|
|
>
|
2025-01-21 18:32:56 -05:00
|
|
|
<span
|
|
|
|
className={!maybeIconConfig[0].showTitle ? 'sr-only' : ''}
|
|
|
|
>
|
|
|
|
{maybeIconConfig[0].title}
|
|
|
|
</span>
|
|
|
|
<ToolbarItemTooltip
|
|
|
|
itemConfig={maybeIconConfig[0]}
|
|
|
|
configCallbackProps={configCallbackProps}
|
|
|
|
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>
|
2024-07-24 23:33:31 -04:00
|
|
|
</ActionButtonDropdown>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
const itemConfig = maybeIconConfig
|
|
|
|
|
2024-11-18 10:04:09 -05:00
|
|
|
// A single button
|
2024-07-24 23:33:31 -04:00
|
|
|
return (
|
2025-01-21 18:32:56 -05:00
|
|
|
<div
|
|
|
|
className="relative"
|
|
|
|
key={itemConfig.id}
|
|
|
|
// Mouse events do not fire on disabled buttons
|
|
|
|
onMouseEnter={handleMouseEnter}
|
|
|
|
onMouseLeave={handleMouseLeave}
|
|
|
|
>
|
2024-08-02 12:25:57 -04:00
|
|
|
<ActionButton
|
|
|
|
Element="button"
|
|
|
|
key={itemConfig.id}
|
|
|
|
id={itemConfig.id}
|
|
|
|
data-testid={itemConfig.id}
|
|
|
|
iconStart={{
|
|
|
|
icon: itemConfig.icon,
|
2025-03-21 11:07:23 -04:00
|
|
|
iconColor: itemConfig.iconColor,
|
2024-08-02 12:25:57 -04:00
|
|
|
className: iconClassName,
|
|
|
|
bgClassName: bgClassName,
|
|
|
|
}}
|
|
|
|
className={
|
|
|
|
'pressed:!text-chalkboard-10 pressed:enabled:hovered:!text-chalkboard-10 ' +
|
|
|
|
buttonBorderClassName +
|
|
|
|
' ' +
|
|
|
|
buttonBgClassName +
|
|
|
|
(!itemConfig.showTitle ? ' !px-0' : '')
|
|
|
|
}
|
|
|
|
name={itemConfig.title}
|
2024-08-19 15:36:18 -04:00
|
|
|
// aria-description is still in ARIA 1.3 draft.
|
|
|
|
// eslint-disable-next-line jsx-a11y/aria-props
|
2024-08-02 12:25:57 -04:00
|
|
|
aria-description={itemConfig.description}
|
|
|
|
aria-pressed={itemConfig.isActive}
|
|
|
|
disabled={
|
|
|
|
disableAllButtons ||
|
|
|
|
itemConfig.status !== 'available' ||
|
|
|
|
itemConfig.disabled
|
|
|
|
}
|
|
|
|
onClick={() => itemConfig.onClick(configCallbackProps)}
|
|
|
|
>
|
|
|
|
<span className={!itemConfig.showTitle ? 'sr-only' : ''}>
|
|
|
|
{itemConfig.title}
|
|
|
|
</span>
|
|
|
|
</ActionButton>
|
|
|
|
<ToolbarItemTooltip
|
2024-07-24 23:33:31 -04:00
|
|
|
itemConfig={itemConfig}
|
|
|
|
configCallbackProps={configCallbackProps}
|
2025-01-21 18:32:56 -05:00
|
|
|
contentClassName={tooltipContentClassName}
|
|
|
|
>
|
|
|
|
{showRichContent ? (
|
|
|
|
<ToolbarItemTooltipRichContent itemConfig={itemConfig} />
|
|
|
|
) : (
|
|
|
|
<ToolbarItemTooltipShortContent
|
|
|
|
status={itemConfig.status}
|
|
|
|
title={itemConfig.title}
|
|
|
|
hotkey={itemConfig.hotkey}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</ToolbarItemTooltip>
|
2024-08-02 12:25:57 -04:00
|
|
|
</div>
|
2024-07-24 23:33:31 -04:00
|
|
|
)
|
|
|
|
})}
|
2023-12-06 14:44:13 -05:00
|
|
|
</ul>
|
2024-07-24 23:33:31 -04:00
|
|
|
{state.matches('Sketch no face') && (
|
|
|
|
<div className="absolute top-full left-1/2 -translate-x-1/2 mt-2 py-1 px-2 bg-chalkboard-10 dark:bg-chalkboard-90 border border-chalkboard-20 dark:border-chalkboard-80 rounded shadow-lg">
|
|
|
|
<p className="text-xs">Select a plane or face to start sketching</p>
|
|
|
|
</div>
|
|
|
|
)}
|
2024-05-08 09:57:16 -04:00
|
|
|
</menu>
|
2022-11-27 14:06:33 +11:00
|
|
|
)
|
|
|
|
}
|
2024-07-24 23:33:31 -04:00
|
|
|
|
2025-01-21 18:32:56 -05:00
|
|
|
interface ToolbarItemContentsProps extends React.PropsWithChildren {
|
|
|
|
itemConfig: ToolbarItemResolved
|
|
|
|
configCallbackProps: ToolbarItemCallbackProps
|
|
|
|
wrapperClassName?: string
|
|
|
|
contentClassName?: string
|
|
|
|
}
|
2024-07-24 23:33:31 -04:00
|
|
|
/**
|
|
|
|
* The single button and dropdown button share content, so we extract it here
|
|
|
|
* It contains a tooltip with the title, description, and links
|
|
|
|
* and a hotkey listener
|
|
|
|
*/
|
2024-08-02 12:25:57 -04:00
|
|
|
const ToolbarItemTooltip = memo(function ToolbarItemContents({
|
2024-07-24 23:33:31 -04:00
|
|
|
itemConfig,
|
|
|
|
configCallbackProps,
|
2025-01-21 18:32:56 -05:00
|
|
|
wrapperClassName = '',
|
|
|
|
contentClassName = '',
|
|
|
|
children,
|
|
|
|
}: ToolbarItemContentsProps) {
|
2024-07-24 23:33:31 -04:00
|
|
|
useHotkeys(
|
|
|
|
itemConfig.hotkey || '',
|
|
|
|
() => {
|
|
|
|
itemConfig.onClick(configCallbackProps)
|
|
|
|
},
|
|
|
|
{
|
|
|
|
enabled:
|
|
|
|
itemConfig.status === 'available' &&
|
|
|
|
!!itemConfig.hotkey &&
|
|
|
|
!itemConfig.disabled &&
|
|
|
|
!itemConfig.disableHotkey,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
return (
|
2024-08-02 12:25:57 -04:00
|
|
|
<Tooltip
|
|
|
|
inert={false}
|
2024-09-05 15:20:51 -04:00
|
|
|
wrapperStyle={
|
|
|
|
isDesktop()
|
2025-03-21 01:55:20 +01:00
|
|
|
? // Without this, the tooltip disappears before being able to click on anything in it
|
|
|
|
({ WebkitAppRegion: 'no-drag' } as React.CSSProperties)
|
2024-09-05 15:20:51 -04:00
|
|
|
: {}
|
|
|
|
}
|
2025-01-18 05:22:22 -05:00
|
|
|
hoverOnly
|
2024-08-02 12:25:57 -04:00
|
|
|
position="bottom"
|
2025-01-21 18:32:56 -05:00
|
|
|
wrapperClassName={'!p-4 !pointer-events-auto ' + wrapperClassName}
|
|
|
|
contentClassName={contentClassName}
|
2024-08-02 12:25:57 -04:00
|
|
|
>
|
2025-01-21 18:32:56 -05:00
|
|
|
{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 (
|
|
|
|
<>
|
2024-08-02 12:25:57 -04:00
|
|
|
<div className="rounded-top flex items-center gap-2 pt-3 pb-2 px-2 bg-chalkboard-20/50 dark:bg-chalkboard-80/50">
|
2025-01-21 18:32:56 -05:00
|
|
|
{itemConfig.icon && (
|
2025-03-21 11:07:23 -04:00
|
|
|
<CustomIcon
|
|
|
|
className="w-5 h-5"
|
|
|
|
style={{ color: itemConfig.iconColor }}
|
|
|
|
name={itemConfig.icon}
|
|
|
|
/>
|
2025-01-21 18:32:56 -05:00
|
|
|
)}
|
2024-08-02 12:25:57 -04:00
|
|
|
<span
|
|
|
|
className={`text-sm flex-1 ${
|
|
|
|
itemConfig.status !== 'available'
|
|
|
|
? 'text-chalkboard-70 dark:text-chalkboard-40'
|
|
|
|
: ''
|
|
|
|
}`}
|
|
|
|
>
|
|
|
|
{itemConfig.title}
|
|
|
|
</span>
|
|
|
|
{itemConfig.status === 'available' && itemConfig.hotkey ? (
|
|
|
|
<kbd className="flex-none hotkey">{itemConfig.hotkey}</kbd>
|
|
|
|
) : itemConfig.status === 'kcl-only' ? (
|
|
|
|
<>
|
|
|
|
<span className="text-wrap font-sans flex-0 text-chalkboard-70 dark:text-chalkboard-40">
|
|
|
|
KCL code only
|
|
|
|
</span>
|
|
|
|
<CustomIcon
|
|
|
|
name="code"
|
|
|
|
className="w-5 h-5 text-chalkboard-70 dark:text-chalkboard-40"
|
|
|
|
/>
|
|
|
|
</>
|
|
|
|
) : (
|
|
|
|
itemConfig.status === 'unavailable' && (
|
2024-07-24 23:33:31 -04:00
|
|
|
<>
|
|
|
|
<span className="text-wrap font-sans flex-0 text-chalkboard-70 dark:text-chalkboard-40">
|
2024-08-02 12:25:57 -04:00
|
|
|
In development
|
2024-07-24 23:33:31 -04:00
|
|
|
</span>
|
|
|
|
<CustomIcon
|
2024-08-02 12:25:57 -04:00
|
|
|
name="lockClosed"
|
2024-07-24 23:33:31 -04:00
|
|
|
className="w-5 h-5 text-chalkboard-70 dark:text-chalkboard-40"
|
|
|
|
/>
|
|
|
|
</>
|
2024-08-02 12:25:57 -04:00
|
|
|
)
|
2024-07-24 23:33:31 -04:00
|
|
|
)}
|
2024-08-02 12:25:57 -04:00
|
|
|
</div>
|
|
|
|
<p className="px-2 text-ch font-sans">{itemConfig.description}</p>
|
2024-10-28 20:52:51 -04:00
|
|
|
{/* Add disabled reason if item is disabled */}
|
|
|
|
{itemConfig.disabled && itemConfig.disabledReason && (
|
|
|
|
<>
|
|
|
|
<hr className="border-chalkboard-20 dark:border-chalkboard-80" />
|
|
|
|
<p className="px-2 text-ch font-sans text-chalkboard-70 dark:text-chalkboard-40">
|
|
|
|
{typeof itemConfig.disabledReason === 'function'
|
|
|
|
? itemConfig.disabledReason(state)
|
|
|
|
: itemConfig.disabledReason}
|
|
|
|
</p>
|
|
|
|
</>
|
|
|
|
)}
|
2024-08-02 12:25:57 -04:00
|
|
|
{itemConfig.links.length > 0 && (
|
|
|
|
<>
|
|
|
|
<hr className="border-chalkboard-20 dark:border-chalkboard-80" />
|
|
|
|
<ul className="p-0 px-1 m-0 flex flex-col">
|
|
|
|
{itemConfig.links.map((link) => (
|
|
|
|
<li key={link.label} className="contents">
|
|
|
|
<a
|
|
|
|
href={link.url}
|
2024-09-05 15:20:51 -04:00
|
|
|
onClick={openExternalBrowserIfDesktop(link.url)}
|
2024-08-02 12:25:57 -04:00
|
|
|
target="_blank"
|
|
|
|
rel="noreferrer"
|
|
|
|
className="flex items-center rounded-sm p-1 no-underline text-inherit hover:bg-primary/10 hover:text-primary dark:hover:bg-chalkboard-70 dark:hover:text-inherit"
|
|
|
|
>
|
|
|
|
<span className="flex-1">Open {link.label}</span>
|
|
|
|
<CustomIcon name="link" className="w-4 h-4" />
|
|
|
|
</a>
|
|
|
|
</li>
|
|
|
|
))}
|
|
|
|
</ul>
|
|
|
|
</>
|
|
|
|
)}
|
2025-01-21 18:32:56 -05:00
|
|
|
</>
|
2024-07-24 23:33:31 -04:00
|
|
|
)
|
2025-01-21 18:32:56 -05:00
|
|
|
}
|