mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-31 23:47:51 +00:00
scrolling model list update (#942)
This commit is contained in:
1 parent
19258c257c
commit
68ff2b484b
3 files changed
+316
-140
No files matched your search
@@ -23,6 +23,7 @@ interface ClassyModelSelectorProps {
|
||||
triggerLabel?: string;
|
||||
providersByModel?: Partial<Record<string, Provider[]>>;
|
||||
providerTooltipDelayMs?: number;
|
||||
maxListHeight?: number | string;
|
||||
}
|
||||
|
||||
const DEFAULT_PROVIDER_OPTIONS: Provider[] = [Provider.ArtCraft];
|
||||
@@ -100,6 +101,7 @@ export function ClassyModelSelector({
|
||||
page,
|
||||
providersByModel,
|
||||
providerTooltipDelayMs = 300,
|
||||
maxListHeight = "60vh",
|
||||
...popoverProps
|
||||
}: ClassyModelSelectorProps) {
|
||||
const { selectedModels, setSelectedModel, setSelectedProvider } =
|
||||
@@ -107,7 +109,7 @@ export function ClassyModelSelector({
|
||||
const selectedModel = selectedModels[page] || items[0]?.model;
|
||||
const selectedProvider = useSelectedProviderForModel(page, selectedModel?.id);
|
||||
const selectedProvidersByModel = useClassyModelSelectorStore(
|
||||
(s) => s.selectedProviders[page] ?? {}
|
||||
(s) => s.selectedProviders[page] ?? {},
|
||||
);
|
||||
|
||||
// For the first mount, make sure the selected model is set for other components to listen
|
||||
@@ -145,9 +147,9 @@ export function ClassyModelSelector({
|
||||
items.map((item) => {
|
||||
const modelId = item.model?.id;
|
||||
const allowedProviders: Provider[] = modelId
|
||||
? providersByModel?.[modelId] ??
|
||||
? (providersByModel?.[modelId] ??
|
||||
PROVIDER_LOOKUP_BY_PAGE[page]?.[modelId] ??
|
||||
DEFAULT_PROVIDER_OPTIONS
|
||||
DEFAULT_PROVIDER_OPTIONS)
|
||||
: DEFAULT_PROVIDER_OPTIONS;
|
||||
|
||||
return {
|
||||
@@ -198,7 +200,7 @@ export function ClassyModelSelector({
|
||||
page,
|
||||
providersByModel,
|
||||
providerTooltipDelayMs,
|
||||
]
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -208,6 +210,7 @@ export function ClassyModelSelector({
|
||||
items={modelList}
|
||||
onSelect={handleModelSelect}
|
||||
mode="hoverSelect"
|
||||
maxListHeight={maxListHeight}
|
||||
{...popoverProps}
|
||||
buttonClassName="rounded-xl bg-ui-controls/90 hover:bg-ui-controls text-left shadow-sm px-3 py-1 gap-3 border border-ui-controls-border"
|
||||
renderTrigger={(selectedItem) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ReactNode, useRef, useEffect, useState } from "react";
|
||||
import { ReactNode, useRef, useEffect, useState, useCallback } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import {
|
||||
Popover,
|
||||
Transition,
|
||||
@@ -11,11 +12,127 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faCheck,
|
||||
faChevronUp,
|
||||
faChevronDown,
|
||||
faCircleCheck,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { Model, ModelInfo } from "@storyteller/model-list";
|
||||
import { Tooltip } from "@storyteller/ui-tooltip";
|
||||
|
||||
interface PortalTooltipProps {
|
||||
children: React.ReactElement;
|
||||
content: ReactNode;
|
||||
delay?: number;
|
||||
className?: string;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
function PortalTooltip({
|
||||
children,
|
||||
content,
|
||||
delay = 300,
|
||||
className,
|
||||
onOpenChange,
|
||||
}: PortalTooltipProps) {
|
||||
const [isShowing, setIsShowing] = useState(false);
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
const [position, setPosition] = useState({ top: 0, left: 0 });
|
||||
const triggerRef = useRef<HTMLDivElement>(null);
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
const [isHoveringTrigger, setIsHoveringTrigger] = useState(false);
|
||||
const [isHoveringTooltip, setIsHoveringTooltip] = useState(false);
|
||||
const showTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const closeTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const checkVisibilityAndUpdatePosition = useCallback(() => {
|
||||
if (!triggerRef.current) return;
|
||||
const rect = triggerRef.current.getBoundingClientRect();
|
||||
|
||||
const scrollParent = triggerRef.current.closest("[data-scroll-container]");
|
||||
if (scrollParent) {
|
||||
const parentRect = scrollParent.getBoundingClientRect();
|
||||
const isInView =
|
||||
rect.top >= parentRect.top - 10 &&
|
||||
rect.bottom <= parentRect.bottom + 10;
|
||||
setIsVisible(isInView);
|
||||
if (!isInView) return;
|
||||
}
|
||||
|
||||
setPosition({
|
||||
top: rect.top + rect.height / 2,
|
||||
left: rect.right + 10,
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const shouldShow = isHoveringTrigger || isHoveringTooltip;
|
||||
if (shouldShow) {
|
||||
if (closeTimeoutRef.current) clearTimeout(closeTimeoutRef.current);
|
||||
checkVisibilityAndUpdatePosition();
|
||||
if (showTimeoutRef.current) clearTimeout(showTimeoutRef.current);
|
||||
showTimeoutRef.current = setTimeout(() => setIsShowing(true), delay);
|
||||
} else {
|
||||
if (showTimeoutRef.current) clearTimeout(showTimeoutRef.current);
|
||||
if (closeTimeoutRef.current) clearTimeout(closeTimeoutRef.current);
|
||||
closeTimeoutRef.current = setTimeout(() => setIsShowing(false), 150);
|
||||
}
|
||||
return () => {
|
||||
if (showTimeoutRef.current) clearTimeout(showTimeoutRef.current);
|
||||
if (closeTimeoutRef.current) clearTimeout(closeTimeoutRef.current);
|
||||
};
|
||||
}, [
|
||||
isHoveringTrigger,
|
||||
isHoveringTooltip,
|
||||
delay,
|
||||
checkVisibilityAndUpdatePosition,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
onOpenChange?.(isShowing && isVisible);
|
||||
}, [isShowing, isVisible, onOpenChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isShowing) return;
|
||||
const handleScroll = () => checkVisibilityAndUpdatePosition();
|
||||
window.addEventListener("scroll", handleScroll, true);
|
||||
return () => window.removeEventListener("scroll", handleScroll, true);
|
||||
}, [isShowing, checkVisibilityAndUpdatePosition]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={triggerRef}
|
||||
onMouseEnter={() => setIsHoveringTrigger(true)}
|
||||
onMouseLeave={() => setIsHoveringTrigger(false)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{isShowing &&
|
||||
isVisible &&
|
||||
createPortal(
|
||||
<div
|
||||
ref={tooltipRef}
|
||||
onMouseEnter={() => setIsHoveringTooltip(true)}
|
||||
onMouseLeave={() => setIsHoveringTooltip(false)}
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: position.top,
|
||||
left: position.left,
|
||||
transform: "translateY(-50%)",
|
||||
zIndex: 9999,
|
||||
}}
|
||||
className={twMerge(
|
||||
"pointer-events-auto rounded-lg bg-ui-panel p-3 shadow-xl border border-ui-panel-border text-base-fg",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Global hover manager to debounce close across adjacent hover popovers
|
||||
let globalCloseTimer: NodeJS.Timeout | null = null;
|
||||
const cancelGlobalClose = () => {
|
||||
@@ -83,6 +200,7 @@ interface PopoverMenuProps {
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
closeOnUnhover?: boolean;
|
||||
renderTrigger?: (selected?: PopoverItem, open?: boolean) => ReactNode;
|
||||
maxListHeight?: number | string;
|
||||
}
|
||||
|
||||
export const PopoverMenu = ({
|
||||
@@ -106,10 +224,41 @@ export const PopoverMenu = ({
|
||||
onOpenChange,
|
||||
closeOnUnhover = false,
|
||||
renderTrigger,
|
||||
maxListHeight,
|
||||
}: PopoverMenuProps) => {
|
||||
const selectedItem = items.find((item) => item.selected);
|
||||
const [openTooltipIdx, setOpenTooltipIdx] = useState<number | null>(null);
|
||||
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [canScrollUp, setCanScrollUp] = useState(false);
|
||||
const [canScrollDown, setCanScrollDown] = useState(false);
|
||||
const [scrollReady, setScrollReady] = useState(false);
|
||||
|
||||
const updateScrollIndicators = useCallback(() => {
|
||||
const container = scrollContainerRef.current;
|
||||
if (!container) return;
|
||||
const { scrollTop, scrollHeight, clientHeight } = container;
|
||||
setCanScrollUp(scrollTop > 1);
|
||||
setCanScrollDown(scrollTop + clientHeight < scrollHeight - 1);
|
||||
}, []);
|
||||
|
||||
const scrollToSelectedItem = useCallback(() => {
|
||||
const container = scrollContainerRef.current;
|
||||
if (!container) return;
|
||||
const selectedElement = container.querySelector("[data-selected='true']");
|
||||
if (selectedElement) {
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const elementRect = selectedElement.getBoundingClientRect();
|
||||
const relativeTop =
|
||||
elementRect.top - containerRect.top + container.scrollTop;
|
||||
const scrollTo =
|
||||
relativeTop - containerRect.height / 2 + elementRect.height / 2;
|
||||
container.scrollTop = Math.max(0, scrollTo);
|
||||
}
|
||||
updateScrollIndicators();
|
||||
setScrollReady(true);
|
||||
}, [updateScrollIndicators]);
|
||||
|
||||
const handleItemClick = (item: PopoverItem, close: () => void) => {
|
||||
if (mode === "button" && item.action && onPanelAction) {
|
||||
onPanelAction(item.action);
|
||||
@@ -126,7 +275,7 @@ export const PopoverMenu = ({
|
||||
"transition-all duration-150",
|
||||
"bg-ui-controls px-3 text-base-fg hover:bg-ui-controls/80 border border-ui-controls-border",
|
||||
"active:scale-95 transform",
|
||||
buttonClassName
|
||||
buttonClassName,
|
||||
);
|
||||
|
||||
const positionClasses = {
|
||||
@@ -192,7 +341,7 @@ export const PopoverMenu = ({
|
||||
() => {
|
||||
closeFn();
|
||||
},
|
||||
mode === "hoverSelect" ? 200 : 120
|
||||
mode === "hoverSelect" ? 200 : 120,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -205,14 +354,35 @@ export const PopoverMenu = ({
|
||||
const thisId = ++popoverIdCounter;
|
||||
useEffect(() => {
|
||||
onOpenChange?.(open);
|
||||
let scrollTimeoutId: NodeJS.Timeout | undefined;
|
||||
if (open && (mode === "hoverSelect" || closeOnUnhover)) {
|
||||
// Broadcast that this popover opened; others should close
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(ST_POPOVER_OPEN_EVENT, {
|
||||
detail: { id: thisId },
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
if (open && maxListHeight) {
|
||||
setScrollReady(false);
|
||||
scrollTimeoutId = setTimeout(() => {
|
||||
scrollToSelectedItem();
|
||||
const container = scrollContainerRef.current;
|
||||
if (container) {
|
||||
container.addEventListener(
|
||||
"scroll",
|
||||
updateScrollIndicators,
|
||||
);
|
||||
resizeObserver = new ResizeObserver(
|
||||
updateScrollIndicators,
|
||||
);
|
||||
resizeObserver.observe(container);
|
||||
}
|
||||
}, 20);
|
||||
} else if (!open) {
|
||||
setScrollReady(false);
|
||||
}
|
||||
const handler = (e: Event) => {
|
||||
const detail = (e as CustomEvent).detail as { id: number };
|
||||
if (detail?.id !== thisId && open) {
|
||||
@@ -222,13 +392,25 @@ export const PopoverMenu = ({
|
||||
};
|
||||
window.addEventListener(
|
||||
ST_POPOVER_OPEN_EVENT,
|
||||
handler as EventListener
|
||||
handler as EventListener,
|
||||
);
|
||||
return () =>
|
||||
return () => {
|
||||
if (scrollTimeoutId) clearTimeout(scrollTimeoutId);
|
||||
const container = scrollContainerRef.current;
|
||||
if (container) {
|
||||
container.removeEventListener(
|
||||
"scroll",
|
||||
updateScrollIndicators,
|
||||
);
|
||||
}
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
}
|
||||
window.removeEventListener(
|
||||
ST_POPOVER_OPEN_EVENT,
|
||||
handler as EventListener
|
||||
handler as EventListener,
|
||||
);
|
||||
};
|
||||
}, [open]);
|
||||
return null;
|
||||
})()}
|
||||
@@ -252,7 +434,7 @@ export const PopoverMenu = ({
|
||||
// Use global close so moving to another hover popover cancels this
|
||||
scheduleGlobalClose(
|
||||
() => close(),
|
||||
mode === "hoverSelect" ? 200 : 120
|
||||
mode === "hoverSelect" ? 200 : 120,
|
||||
);
|
||||
}}
|
||||
>
|
||||
@@ -324,14 +506,14 @@ export const PopoverMenu = ({
|
||||
"absolute transform-gpu z-50",
|
||||
positionClasses[position],
|
||||
alignClasses[align],
|
||||
position === "bottom" ? "origin-top" : "origin-bottom"
|
||||
position === "bottom" ? "origin-top" : "origin-bottom",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={twMerge(
|
||||
"z-10 min-w-48 mt-2 rounded-lg bg-ui-panel p-1.5 shadow-lg border border-ui-panel-border",
|
||||
"z-10 min-w-48 mt-2 rounded-lg bg-ui-panel p-1.5 shadow-lg border border-ui-panel-border overflow-visible",
|
||||
position === "top" ? "mb-2" : "mt-2",
|
||||
panelClassName
|
||||
panelClassName,
|
||||
)}
|
||||
onMouseEnter={handlePanelMouseEnter}
|
||||
onMouseLeave={() => handlePanelMouseLeave(close)}
|
||||
@@ -359,110 +541,44 @@ export const PopoverMenu = ({
|
||||
: children}
|
||||
</div>
|
||||
) : mode === "hoverSelect" ? (
|
||||
<div className="flex flex-col gap-0 text-sm text-base-fg">
|
||||
{items.map((item, index) => (
|
||||
<div key={index}>
|
||||
{item.hoverTooltip ? (
|
||||
<Tooltip
|
||||
content={
|
||||
typeof item.hoverTooltip === "function"
|
||||
? (
|
||||
item.hoverTooltip as (
|
||||
close: () => void
|
||||
) => ReactNode
|
||||
)(close)
|
||||
: item.hoverTooltip
|
||||
<div className="relative flex flex-col text-sm text-base-fg overflow-visible">
|
||||
{maxListHeight && canScrollUp && (
|
||||
<div className="absolute top-0 left-0 right-0 z-20 flex justify-center bg-gradient-to-b from-ui-panel via-ui-panel/80 to-transparent py-1.5 pointer-events-none">
|
||||
<FontAwesomeIcon
|
||||
icon={faChevronUp}
|
||||
className="text-base-fg/60 text-xs animate-bounce"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
data-scroll-container
|
||||
className="flex flex-col gap-0 [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none] transition-opacity duration-75"
|
||||
style={
|
||||
maxListHeight
|
||||
? {
|
||||
maxHeight: maxListHeight,
|
||||
overflowY: "auto",
|
||||
opacity: scrollReady ? 1 : 0,
|
||||
}
|
||||
position="right"
|
||||
delay={item.tooltipDelayMs ?? 300}
|
||||
interactive
|
||||
className="!pointer-events-auto z-50 min-w-48 rounded-lg bg-ui-panel p-1.5 shadow-lg border border-ui-panel-border"
|
||||
onOpenChange={(open) =>
|
||||
setOpenTooltipIdx((prev) =>
|
||||
open ? index : prev === index ? null : prev
|
||||
)
|
||||
}
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
if (!item.disabled) {
|
||||
handleItemClick(item, close);
|
||||
}
|
||||
}}
|
||||
className={twMerge(
|
||||
"group flex cursor-pointer items-start gap-2 rounded-lg px-2 py-2 transition-all",
|
||||
item.selected
|
||||
? "bg-ui-controls/70 border-l-4 border-primary"
|
||||
: "hover:bg-ui-controls/50",
|
||||
!item.selected && openTooltipIdx === index
|
||||
? "bg-ui-controls/50"
|
||||
: "",
|
||||
item.disabled
|
||||
? "!cursor-not-allowed opacity-50"
|
||||
: ""
|
||||
)}
|
||||
style={{ minHeight: 48 }}
|
||||
>
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<div className="flex items-start gap-2 grow">
|
||||
{showIconsInList && (
|
||||
<span className="mt-1 flex h-5 w-5 items-center justify-center text-lg text-base-fg/80">
|
||||
{item.icon}
|
||||
</span>
|
||||
)}
|
||||
<div className="flex flex-1 flex-col min-w-0">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<span className="truncate font-semibold text-base-fg text-base">
|
||||
{item.label}
|
||||
</span>
|
||||
</div>
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{items.map((item, index) => {
|
||||
const tooltipContent =
|
||||
typeof item.hoverTooltip === "function"
|
||||
? (
|
||||
item.hoverTooltip as (
|
||||
close: () => void,
|
||||
) => ReactNode
|
||||
)(close)
|
||||
: item.hoverTooltip;
|
||||
|
||||
{item.description && (
|
||||
<div className="truncate text-xs text-base-fg/60 mt-0.5">
|
||||
{item.description}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-row gap-1 flex-wrap mt-1.5">
|
||||
{item.badges &&
|
||||
Array.isArray(item.badges) &&
|
||||
item.badges.map((badge, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center gap-1 min-w-0"
|
||||
>
|
||||
<span className="inline-flex items-center rounded bg-ui-badge px-1.5 py-0.5 text-xs font-medium text-base-fg gap-1">
|
||||
{badge?.icon && (
|
||||
<span>{badge.icon}</span>
|
||||
)}
|
||||
{badge?.label || ""}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Optional trailing content (e.g., tooltip trigger) */}
|
||||
{item.trailing && (
|
||||
<div className="ml-2 mr-1 flex items-center">
|
||||
{item.trailing}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.selected &&
|
||||
(item.selectedRight ?? (
|
||||
<span className="text-primary text-xl font-bold bg-white rounded-full p-0 h-4 w-4 flex items-center justify-center mr-1">
|
||||
<FontAwesomeIcon
|
||||
icon={faCircleCheck}
|
||||
/>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
const itemRow = (
|
||||
<div
|
||||
data-selected={
|
||||
item.selected ? "true" : undefined
|
||||
}
|
||||
onClick={() => {
|
||||
if (!item.disabled) {
|
||||
handleItemClick(item, close);
|
||||
@@ -473,9 +589,12 @@ export const PopoverMenu = ({
|
||||
item.selected
|
||||
? "bg-ui-controls/70 border-l-4 border-primary"
|
||||
: "hover:bg-ui-controls/50",
|
||||
!item.selected && openTooltipIdx === index
|
||||
? "bg-ui-controls/50"
|
||||
: "",
|
||||
item.disabled
|
||||
? "!cursor-not-allowed opacity-50"
|
||||
: ""
|
||||
: "",
|
||||
)}
|
||||
style={{ minHeight: 48 }}
|
||||
>
|
||||
@@ -519,26 +638,80 @@ export const PopoverMenu = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Optional trailing content (e.g., tooltip trigger) */}
|
||||
{item.trailing && (
|
||||
<div className="ml-2 mr-1 flex items-center">
|
||||
{item.trailing}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.selected && (
|
||||
<span className="text-primary text-xl font-bold bg-white rounded-full p-0 h-4 w-4 flex items-center justify-center mr-1">
|
||||
<FontAwesomeIcon icon={faCircleCheck} />
|
||||
</span>
|
||||
)}
|
||||
{item.selected &&
|
||||
(item.selectedRight ?? (
|
||||
<span className="text-primary text-xl font-bold bg-white rounded-full p-0 h-4 w-4 flex items-center justify-center mr-1">
|
||||
<FontAwesomeIcon icon={faCircleCheck} />
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{item.divider && (
|
||||
<div className="my-1 border-t border-white/10" />
|
||||
)}
|
||||
);
|
||||
|
||||
return (
|
||||
<div key={index}>
|
||||
{item.hoverTooltip ? (
|
||||
maxListHeight ? (
|
||||
<PortalTooltip
|
||||
content={tooltipContent}
|
||||
delay={item.tooltipDelayMs ?? 300}
|
||||
className="min-w-48"
|
||||
onOpenChange={(open) =>
|
||||
setOpenTooltipIdx((prev) =>
|
||||
open
|
||||
? index
|
||||
: prev === index
|
||||
? null
|
||||
: prev,
|
||||
)
|
||||
}
|
||||
>
|
||||
{itemRow}
|
||||
</PortalTooltip>
|
||||
) : (
|
||||
<Tooltip
|
||||
content={tooltipContent}
|
||||
position="right"
|
||||
delay={item.tooltipDelayMs ?? 300}
|
||||
interactive
|
||||
className="!pointer-events-auto z-50 min-w-48 rounded-lg bg-ui-panel p-1.5 shadow-lg border border-ui-panel-border"
|
||||
onOpenChange={(open) =>
|
||||
setOpenTooltipIdx((prev) =>
|
||||
open
|
||||
? index
|
||||
: prev === index
|
||||
? null
|
||||
: prev,
|
||||
)
|
||||
}
|
||||
>
|
||||
{itemRow}
|
||||
</Tooltip>
|
||||
)
|
||||
) : (
|
||||
itemRow
|
||||
)}
|
||||
{item.divider && (
|
||||
<div className="my-1 border-t border-white/10" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{maxListHeight && canScrollDown && (
|
||||
<div className="absolute bottom-0 left-0 right-0 z-20 flex justify-center bg-gradient-to-t from-ui-panel via-ui-panel/80 to-transparent py-1.5 pointer-events-none">
|
||||
<FontAwesomeIcon
|
||||
icon={faChevronDown}
|
||||
className="text-base-fg/60 text-xs animate-bounce"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
{showAddButton && onAdd && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
@@ -546,7 +719,7 @@ export const PopoverMenu = ({
|
||||
"w-full mb-0.5 mt-2 border-none py-1",
|
||||
disableAddButton
|
||||
? "cursor-not-allowed bg-[#7B7B84]/50 opacity-50"
|
||||
: "bg-[#7B7B84] hover:bg-[#8c8c96]"
|
||||
: "bg-[#7B7B84] hover:bg-[#8c8c96]",
|
||||
)}
|
||||
onClick={onAdd}
|
||||
disabled={disableAddButton}
|
||||
@@ -565,7 +738,7 @@ export const PopoverMenu = ({
|
||||
typeof item.hoverTooltip === "function"
|
||||
? (
|
||||
item.hoverTooltip as (
|
||||
close: () => void
|
||||
close: () => void,
|
||||
) => ReactNode
|
||||
)(close)
|
||||
: item.hoverTooltip
|
||||
@@ -576,7 +749,7 @@ export const PopoverMenu = ({
|
||||
className="!pointer-events-auto z-50 min-w-48 rounded-lg bg-ui-panel p-1.5 shadow-lg border border-ui-panel-border"
|
||||
onOpenChange={(open) =>
|
||||
setOpenTooltipIdx((prev) =>
|
||||
open ? index : prev === index ? null : prev
|
||||
open ? index : prev === index ? null : prev,
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -593,7 +766,7 @@ export const PopoverMenu = ({
|
||||
item.disabled
|
||||
? "!cursor-not-allowed opacity-50"
|
||||
: "",
|
||||
"border-0"
|
||||
"border-0",
|
||||
)}
|
||||
onClick={() =>
|
||||
!item.disabled &&
|
||||
@@ -610,7 +783,7 @@ export const PopoverMenu = ({
|
||||
"truncate",
|
||||
item.selected
|
||||
? "text-base-fg"
|
||||
: "text-base-fg/70"
|
||||
: "text-base-fg/70",
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
@@ -640,7 +813,7 @@ export const PopoverMenu = ({
|
||||
"ml-2 h-5 w-5 rounded-full border flex items-center justify-center transition-colors",
|
||||
item.selected
|
||||
? "border-primary bg-primary"
|
||||
: "border-transparent bg-transparent"
|
||||
: "border-transparent bg-transparent",
|
||||
)}
|
||||
>
|
||||
{item.selected && (
|
||||
@@ -664,7 +837,7 @@ export const PopoverMenu = ({
|
||||
item.disabled
|
||||
? "!cursor-not-allowed opacity-50"
|
||||
: "",
|
||||
"border-0"
|
||||
"border-0",
|
||||
)}
|
||||
onClick={() =>
|
||||
!item.disabled && handleItemClick(item, close)
|
||||
@@ -680,7 +853,7 @@ export const PopoverMenu = ({
|
||||
"truncate",
|
||||
item.selected
|
||||
? "text-base-fg"
|
||||
: "text-base-fg/70"
|
||||
: "text-base-fg/70",
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
@@ -704,7 +877,7 @@ export const PopoverMenu = ({
|
||||
"ml-2 h-5 w-5 rounded-full border flex items-center justify-center transition-colors",
|
||||
item.selected
|
||||
? "border-primary bg-primary"
|
||||
: "border-transparent bg-transparent"
|
||||
: "border-transparent bg-transparent",
|
||||
)}
|
||||
>
|
||||
{item.selected && (
|
||||
@@ -729,7 +902,7 @@ export const PopoverMenu = ({
|
||||
"w-full mb-0.5 mt-2 py-1 border-0",
|
||||
disableAddButton
|
||||
? "cursor-not-allowed opacity-50"
|
||||
: ""
|
||||
: "",
|
||||
)}
|
||||
onClick={onAdd}
|
||||
disabled={disableAddButton}
|
||||
|
||||
@@ -153,7 +153,7 @@ export const Tooltip = ({
|
||||
show={isShowing}
|
||||
enter={twMerge(
|
||||
"transition ease-out duration-200",
|
||||
delay ? `delay-[${delay}ms]` : "delay-[300ms]"
|
||||
delay ? `delay-[${delay}ms]` : "delay-[300ms]",
|
||||
)}
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
@@ -184,7 +184,7 @@ export const Tooltip = ({
|
||||
? "pointer-events-auto p-3"
|
||||
: "px-2.5 py-1.5 text-[13px] font-medium pointer-events-none",
|
||||
"text-base-fg",
|
||||
className ? className : ""
|
||||
className ? className : "",
|
||||
)}
|
||||
>
|
||||
{interactive ? (
|
||||
|
||||
Reference in new issue
Block a user