selection

This commit is contained in:
2025-11-03 02:31:46 +01:00
parent 1c261bf0a2
commit 152f824e2e
15 changed files with 1331 additions and 164 deletions
+12
View File
@@ -40,6 +40,9 @@ const QuickAddMenu = ({
menuMinWidth = 220,
triggerClassName = 'icon-button quick-add__trigger',
triggerContent = null,
disabled = false,
align = 'start',
positionStrategy = 'fixed',
}) => {
const anchorRef = useRef(null);
const inputRef = useRef(null);
@@ -57,8 +60,16 @@ const QuickAddMenu = ({
anchorRef,
minWidth: menuMinWidth,
matchAnchorWidth: false,
align,
positionStrategy,
});
useEffect(() => {
if (disabled && isOpen) {
close();
}
}, [disabled, isOpen, close]);
useEffect(() => {
if (!isOpen) {
return undefined;
@@ -145,6 +156,7 @@ const QuickAddMenu = ({
onClick={toggle}
aria-label={triggerAriaLabel}
title={triggerTitle}
disabled={disabled}
>
{triggerContent ?? <PlusIcon />}
</button>
+30
View File
@@ -30,6 +30,9 @@ import {
IconLayoutSidebarLeftExpand,
IconLayoutSidebarRightCollapse,
IconInfoCircle,
IconCircleDashedCheck,
IconFile,
IconFolder,
} from '@tabler/icons-react';
import FolderSvg from '../assets/folder.svg';
@@ -330,6 +333,33 @@ export const CheckIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) =>
/>
);
export const CircleDashedCheckIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconCircleDashedCheck
className={composeClassName('icon', className)}
size={size}
stroke={stroke}
{...rest}
/>
);
export const FileIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconFile
className={composeClassName('icon', className)}
size={size}
stroke={stroke}
{...rest}
/>
);
export const FolderOutlineIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconFolder
className={composeClassName('icon', className)}
size={size}
stroke={stroke}
{...rest}
/>
);
export const AnalyzeIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconAnalyze
className={composeClassName('icon', className)}
+58 -6
View File
@@ -26,7 +26,7 @@ const formatStyle = (metrics) => {
return null;
}
const style = {
position: 'fixed',
position: metrics.strategy === 'absolute' ? 'absolute' : 'fixed',
top: metrics.top,
left: metrics.left,
minWidth: metrics.minWidth,
@@ -45,6 +45,7 @@ const useFloatingMenu = ({
align = 'start',
viewportMargin = DEFAULT_VIEWPORT_MARGIN,
onOpenChange,
positionStrategy = 'fixed',
} = {}) => {
const menuRef = useRef(null);
const [menuMetrics, setMenuMetrics] = useState(null);
@@ -63,22 +64,64 @@ const useFloatingMenu = ({
const rect = anchor.getBoundingClientRect();
const desiredWidth = computeWidth(rect.width, minWidth, matchAnchorWidth);
const menu = menuRef.current;
const measuredWidth = menu?.offsetWidth ?? desiredWidth;
const widthForAlignment = matchAnchorWidth ? desiredWidth : Math.max(desiredWidth, measuredWidth);
if (positionStrategy === 'absolute') {
const anchor = anchorRef?.current;
if (!anchor) {
return false;
}
const offsetParent = (menu && menu.offsetParent) || anchor.offsetParent || anchor.parentElement;
if (!offsetParent) {
// Fall back to fixed positioning if we cannot resolve a relative parent.
setMenuMetrics({
strategy: 'fixed',
top: rect.bottom + offset,
left: rect.left,
minWidth: desiredWidth,
width: matchAnchorWidth ? desiredWidth : undefined,
});
return true;
}
let left;
if (align === 'end') {
left = anchor.offsetLeft + anchor.offsetWidth - widthForAlignment;
} else if (align === 'center') {
left = anchor.offsetLeft + anchor.offsetWidth / 2 - widthForAlignment / 2;
} else {
left = anchor.offsetLeft;
}
const top = anchor.offsetTop + anchor.offsetHeight + offset;
setMenuMetrics({
strategy: 'absolute',
top,
left,
minWidth: desiredWidth,
width: matchAnchorWidth ? desiredWidth : undefined,
});
return true;
}
const viewportWidth = resolveViewportWidth();
const viewportHeight = typeof window !== 'undefined' ? window.innerHeight : 0;
const safeMargin = viewportMargin ?? DEFAULT_VIEWPORT_MARGIN;
const menu = menuRef.current;
const menuHeight = menu?.offsetHeight ?? 0;
let left;
if (align === 'end') {
left = rect.right - desiredWidth;
left = rect.right - widthForAlignment;
} else if (align === 'center') {
left = rect.left + rect.width / 2 - desiredWidth / 2;
left = rect.left + rect.width / 2 - widthForAlignment / 2;
} else {
left = rect.left;
}
const maxLeft = viewportWidth > 0 ? viewportWidth - desiredWidth - safeMargin : left;
const maxLeft = viewportWidth > 0 ? viewportWidth - widthForAlignment - safeMargin : left;
const clampedLeft = viewportWidth > 0 ? clamp(left, safeMargin, Math.max(maxLeft, safeMargin)) : left;
let top = rect.bottom + offset;
@@ -91,6 +134,7 @@ const useFloatingMenu = ({
}
setMenuMetrics({
strategy: 'fixed',
top,
left: clampedLeft,
minWidth: desiredWidth,
@@ -98,7 +142,15 @@ const useFloatingMenu = ({
});
return true;
}, [anchorRef, align, matchAnchorWidth, minWidth, offset, viewportMargin]);
}, [
anchorRef,
align,
matchAnchorWidth,
minWidth,
offset,
positionStrategy,
viewportMargin,
]);
const close = useCallback(() => {
setIsOpen((prev) => {