typescript

This commit is contained in:
2025-11-13 01:07:55 +01:00
parent b812d748ea
commit ada089c05b
147 changed files with 7427 additions and 2534 deletions
@@ -1,17 +1,28 @@
import React from 'react';
import React, { ElementType, ReactNode } from 'react';
import composeClassName from './classNames';
const PanelHeader = ({
type TitleProps = Record<string, unknown>;
interface PanelHeaderProps {
className?: string;
leading?: ReactNode;
title?: ReactNode;
titleTag?: ElementType;
titleProps?: TitleProps;
actions?: ReactNode;
}
const PanelHeader: React.FC<PanelHeaderProps> = ({
className = '',
leading = null,
title = null,
titleTag = 'h2',
titleTag: HeadingTag = 'h2',
titleProps = {},
actions = null,
}) => {
const headerClassName = composeClassName('panel-header', className);
const renderTitle = () => {
const renderTitle = (): ReactNode => {
if (title == null) {
return null;
}
@@ -25,14 +36,14 @@ const PanelHeader = ({
);
}
const mergedClassName = composeClassName('panel-header__title', title.props.className || '');
const existingClassName = (title.props as { className?: string })?.className ?? '';
const mergedClassName = composeClassName('panel-header__title', existingClassName);
return React.cloneElement(title, {
...titleProps,
className: mergedClassName,
});
}
const HeadingTag = titleTag;
return (
<HeadingTag className="panel-header__title" {...titleProps}>
{title}
@@ -1,12 +1,22 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { CSSProperties, ReactNode, FormEvent, MutableRefObject } from 'react';
import { PlusIcon } from './icons';
import useFloatingMenu from './useFloatingMenu';
const normalizeOption = (option, index) => {
type QuickAddOption = string | number | { id?: string | number; label?: string; name?: string; [key: string]: unknown };
interface NormalizedOption {
id?: string | number;
label: string;
original: QuickAddOption;
index: number;
}
const normalizeOption = (option: QuickAddOption | null | undefined, index: number): NormalizedOption | null => {
if (option == null) {
return null;
}
if (option && typeof option === 'object') {
if (typeof option === 'object') {
const label = option.label ?? option.name;
if (label == null) {
return null;
@@ -27,6 +37,36 @@ const normalizeOption = (option, index) => {
};
};
interface FloatingMenuState {
isOpen: boolean;
toggle: () => void;
close: () => void;
menuRef: MutableRefObject<HTMLDivElement | null>;
menuStyle: CSSProperties | null;
updatePosition: () => void;
}
type CSSVarStyle = CSSProperties & Record<string, string | number>;
export interface QuickAddMenuProps {
onSelectOption?: (value: QuickAddOption, normalized: NormalizedOption) => Promise<void> | void;
onCreate?: (value: string) => Promise<void> | void;
options?: QuickAddOption[];
placeholder?: string;
createLabel?: string;
emptyMessage?: string;
className?: string;
triggerAriaLabel?: string;
triggerTitle?: string;
renderOption?: (value: QuickAddOption, normalized: NormalizedOption) => ReactNode;
menuMinWidth?: number;
triggerClassName?: string;
triggerContent?: ReactNode;
disabled?: boolean;
align?: 'start' | 'center' | 'end' | (string & {});
positionStrategy?: 'fixed' | 'absolute' | (string & {});
}
const QuickAddMenu = ({
onSelectOption,
onCreate,
@@ -44,7 +84,7 @@ const QuickAddMenu = ({
disabled = false,
align = 'start',
positionStrategy = 'fixed',
}) => {
}: QuickAddMenuProps) => {
const anchorRef = useRef(null);
const inputRef = useRef(null);
const [query, setQuery] = useState('');
@@ -63,7 +103,7 @@ const QuickAddMenu = ({
matchAnchorWidth: false,
align,
positionStrategy,
});
}) as FloatingMenuState;
useEffect(() => {
if (disabled && isOpen) {
@@ -87,11 +127,11 @@ const QuickAddMenu = ({
return () => cancelAnimationFrame(frame);
}, [isOpen, updatePosition]);
const normalizedOptions = useMemo(
const normalizedOptions = useMemo<NormalizedOption[]>(
() =>
options
.map((option, index) => normalizeOption(option, index))
.filter(Boolean),
.filter((option): option is NormalizedOption => Boolean(option)),
[options],
);
@@ -104,7 +144,7 @@ const QuickAddMenu = ({
}, [normalizedOptions, query]);
const handleSelect = useCallback(
async (option) => {
async (option: NormalizedOption) => {
if (!option || !onSelectOption) {
return;
}
@@ -122,7 +162,7 @@ const QuickAddMenu = ({
);
const handleCreate = useCallback(
async (event) => {
async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!onCreate) {
return;
@@ -154,9 +194,9 @@ const QuickAddMenu = ({
...(menuStyle.width ? { width: menuStyle.width } : null),
}
: undefined;
const menuInlineStyle = isAnchoredMenu ? anchoredMenuStyle : menuStyle || undefined;
const menuInlineStyle = (isAnchoredMenu ? anchoredMenuStyle : menuStyle || undefined) as CSSVarStyle | undefined;
const hasFloatingWidthVar = Boolean(menuInlineStyle && Object.prototype.hasOwnProperty.call(menuInlineStyle, '--floating-min-width'));
const menuStyleWithVar = hasFloatingWidthVar
const menuStyleWithVar: CSSVarStyle | undefined = hasFloatingWidthVar
? menuInlineStyle
: {
...(menuInlineStyle || {}),
-3
View File
@@ -1,3 +0,0 @@
export const composeClassName = (base, extra) => (extra ? `${base} ${extra}` : base);
export default composeClassName;
+4
View File
@@ -0,0 +1,4 @@
export const composeClassName = (base: string, extra?: string | null): string =>
extra ? `${base} ${extra}` : base;
export default composeClassName;
@@ -1,3 +1,6 @@
import React from 'react';
import type { JSX } from 'react';
import type { IconProps as TablerIconProps } from '@tabler/icons-react';
import {
IconChevronRight as TablerChevronRight,
IconDownload as TablerDownload,
@@ -47,9 +50,12 @@ import {
IconAlertTriangle,
} from '@tabler/icons-react';
import FolderSvg from '../assets/folder.svg';
import LogoSvg from '../assets/logo.svg';
import composeClassName from './classNames';
export const ChevronIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
type TablerIconComponent = (props: TablerIconProps) => JSX.Element;
export const ChevronIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<TablerChevronRight
className={composeClassName('icon', className)}
size={size}
@@ -58,7 +64,7 @@ export const ChevronIcon = ({ className, size = '1em', stroke = 1.6, ...rest })
/>
);
export const TrashIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const TrashIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconTrash
className={composeClassName('icon', className)}
size={size}
@@ -67,7 +73,7 @@ export const TrashIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) =>
/>
);
export const EditIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const EditIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconPencil
className={composeClassName('icon', className)}
size={size}
@@ -76,7 +82,7 @@ export const EditIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) =>
/>
);
export const FolderIcon = ({ className, size = 16, title, ...rest }) => {
export const FolderIcon: TablerIconComponent = ({ className, size = 16, title, ...rest }) => {
const dimensionProps = typeof size === 'number' ? { width: size, height: size } : {};
return (
@@ -92,7 +98,17 @@ export const FolderIcon = ({ className, size = 16, title, ...rest }) => {
);
};
export const TagIcon = ({ className, size = '1em', stroke = 0, ...rest }) => (
export const LogoIcon: React.FC<{ className?: string; width?: number; height?: number }> = ({ className, width = 24, height = 24 }) => (
<LogoSvg
className={composeClassName('logo-icon', className)}
width={width}
height={height}
aria-hidden="true"
focusable="false"
/>
);
export const TagIcon: TablerIconComponent = ({ className, size = '1em', stroke = 0, ...rest }) => (
<IconTagFilled
className={composeClassName('icon icon--fill', className)}
size={size}
@@ -101,7 +117,7 @@ export const TagIcon = ({ className, size = '1em', stroke = 0, ...rest }) => (
/>
);
export const CorrespondentIcon = ({ className, size = '1em', stroke = 0, ...rest }) => (
export const CorrespondentIcon: TablerIconComponent = ({ className, size = '1em', stroke = 0, ...rest }) => (
<IconUserFilled
className={composeClassName('icon icon--fill', className)}
size={size}
@@ -110,7 +126,7 @@ export const CorrespondentIcon = ({ className, size = '1em', stroke = 0, ...rest
/>
);
export const DownloadIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const DownloadIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<TablerDownload
className={composeClassName('icon', className)}
size={size}
@@ -119,7 +135,7 @@ export const DownloadIcon = ({ className, size = '1em', stroke = 1.6, ...rest })
/>
);
export const IconZoomInArea = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const IconZoomInArea: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<TablerZoomInArea
className={composeClassName('icon', className)}
size={size}
@@ -128,7 +144,7 @@ export const IconZoomInArea = ({ className, size = '1em', stroke = 1.6, ...rest
/>
);
export const ViewListIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const ViewListIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconLayoutList
className={composeClassName('icon', className)}
size={size}
@@ -137,7 +153,7 @@ export const ViewListIcon = ({ className, size = '1em', stroke = 1.6, ...rest })
/>
);
export const ViewGridIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const ViewGridIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconLayoutGrid
className={composeClassName('icon', className)}
size={size}
@@ -146,7 +162,7 @@ export const ViewGridIcon = ({ className, size = '1em', stroke = 1.6, ...rest })
/>
);
export const UploadIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const UploadIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconUpload
className={composeClassName('icon', className)}
size={size}
@@ -155,7 +171,7 @@ export const UploadIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) =
/>
);
export const ArrowLeftIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const ArrowLeftIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconArrowLeft
className={composeClassName('icon', className)}
size={size}
@@ -164,7 +180,7 @@ export const ArrowLeftIcon = ({ className, size = '1em', stroke = 1.6, ...rest }
/>
);
export const ArrowRightIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const ArrowRightIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconArrowRight
className={composeClassName('icon', className)}
size={size}
@@ -173,7 +189,7 @@ export const ArrowRightIcon = ({ className, size = '1em', stroke = 1.6, ...rest
/>
);
export const SidebarCollapseIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const SidebarCollapseIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconLayoutSidebarLeftCollapse
className={composeClassName('icon', className)}
size={size}
@@ -182,7 +198,7 @@ export const SidebarCollapseIcon = ({ className, size = '1em', stroke = 1.6, ...
/>
);
export const SidebarExpandIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const SidebarExpandIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconLayoutSidebarLeftExpand
className={composeClassName('icon', className)}
size={size}
@@ -191,7 +207,7 @@ export const SidebarExpandIcon = ({ className, size = '1em', stroke = 1.6, ...re
/>
);
export const InfoIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const InfoIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconInfoCircle
className={composeClassName('icon', className)}
size={size}
@@ -200,7 +216,7 @@ export const InfoIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) =>
/>
);
export const FileInfoIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const FileInfoIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconFileInfo
className={composeClassName('icon', className)}
size={size}
@@ -209,7 +225,7 @@ export const FileInfoIcon = ({ className, size = '1em', stroke = 1.6, ...rest })
/>
);
export const DetailPanelCollapseIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const DetailPanelCollapseIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconLayoutSidebarRightCollapse
className={composeClassName('icon', className)}
size={size}
@@ -218,7 +234,7 @@ export const DetailPanelCollapseIcon = ({ className, size = '1em', stroke = 1.6,
/>
);
export const BottombarCollapseIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const BottombarCollapseIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconLayoutBottombarCollapse
className={composeClassName('icon', className)}
size={size}
@@ -227,7 +243,7 @@ export const BottombarCollapseIcon = ({ className, size = '1em', stroke = 1.6, .
/>
);
export const BottombarExpandIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const BottombarExpandIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconLayoutBottombarExpand
className={composeClassName('icon', className)}
size={size}
@@ -236,7 +252,7 @@ export const BottombarExpandIcon = ({ className, size = '1em', stroke = 1.6, ...
/>
);
export const ClearAllIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const ClearAllIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconClearAll
className={composeClassName('icon', className)}
size={size}
@@ -245,7 +261,7 @@ export const ClearAllIcon = ({ className, size = '1em', stroke = 1.6, ...rest })
/>
);
export const FolderPlusIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const FolderPlusIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconFolderPlus
className={composeClassName('icon', className)}
size={size}
@@ -254,7 +270,7 @@ export const FolderPlusIcon = ({ className, size = '1em', stroke = 1.6, ...rest
/>
);
export const FoldersIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const FoldersIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconFolders
className={composeClassName('icon', className)}
size={size}
@@ -263,7 +279,7 @@ export const FoldersIcon = ({ className, size = '1em', stroke = 1.6, ...rest })
/>
);
export const FoldersOffIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const FoldersOffIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconFoldersOff
className={composeClassName('icon', className)}
size={size}
@@ -272,7 +288,7 @@ export const FoldersOffIcon = ({ className, size = '1em', stroke = 1.6, ...rest
/>
);
export const RefreshIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const RefreshIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconRefresh
className={composeClassName('icon', className)}
size={size}
@@ -281,7 +297,7 @@ export const RefreshIcon = ({ className, size = '1em', stroke = 1.6, ...rest })
/>
);
export const RestoreIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const RestoreIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconRestore
className={composeClassName('icon', className)}
size={size}
@@ -290,7 +306,7 @@ export const RestoreIcon = ({ className, size = '1em', stroke = 1.6, ...rest })
/>
);
export const ArrowUpIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const ArrowUpIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconArrowUp
className={composeClassName('icon', className)}
size={size}
@@ -299,7 +315,7 @@ export const ArrowUpIcon = ({ className, size = '1em', stroke = 1.6, ...rest })
/>
);
export const MinusVerticalIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const MinusVerticalIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconMinusVertical
className={composeClassName('icon', className)}
size={size}
@@ -308,7 +324,7 @@ export const MinusVerticalIcon = ({ className, size = '1em', stroke = 1.6, ...re
/>
);
export const IconFileStack = ({ className, size = 24, stroke = 160, ...rest }) => {
export const IconFileStack: TablerIconComponent = ({ className, size = 24, stroke = 160, ...rest }) => {
return (
<svg
className={composeClassName('icon', className)}
@@ -337,7 +353,7 @@ export const IconFileStack = ({ className, size = 24, stroke = 160, ...rest }) =
);
};
export const SortAscendingLettersIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const SortAscendingLettersIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconSortAscendingLetters
className={composeClassName('icon', className)}
size={size}
@@ -346,7 +362,7 @@ export const SortAscendingLettersIcon = ({ className, size = '1em', stroke = 1.6
/>
);
export const SortDescendingLettersIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const SortDescendingLettersIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconSortDescendingLetters
className={composeClassName('icon', className)}
size={size}
@@ -355,7 +371,7 @@ export const SortDescendingLettersIcon = ({ className, size = '1em', stroke = 1.
/>
);
export const IconX = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const IconX: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<TablerIconX
className={composeClassName('icon', className)}
size={size}
@@ -364,7 +380,7 @@ export const IconX = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
/>
);
export const CloseIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const CloseIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconX
className={className}
size={size}
@@ -373,7 +389,7 @@ export const CloseIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) =>
/>
);
export const SettingsIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const SettingsIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconSettings
className={composeClassName('icon', className)}
size={size}
@@ -382,7 +398,7 @@ export const SettingsIcon = ({ className, size = '1em', stroke = 1.6, ...rest })
/>
);
export const PlusIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const PlusIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconPlus
className={composeClassName('icon', className)}
size={size}
@@ -391,7 +407,7 @@ export const PlusIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) =>
/>
);
export const SunIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const SunIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconSun
className={composeClassName('icon', className)}
size={size}
@@ -400,7 +416,7 @@ export const SunIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
/>
);
export const MoonIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const MoonIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconMoon
className={composeClassName('icon', className)}
size={size}
@@ -409,7 +425,7 @@ export const MoonIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) =>
/>
);
export const DesktopIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const DesktopIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconDeviceLaptop
className={composeClassName('icon', className)}
size={size}
@@ -418,7 +434,7 @@ export const DesktopIcon = ({ className, size = '1em', stroke = 1.6, ...rest })
/>
);
export const CheckIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const CheckIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconCheck
className={composeClassName('icon', className)}
size={size}
@@ -427,7 +443,7 @@ export const CheckIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) =>
/>
);
export const CircleDashedCheckIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const CircleDashedCheckIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconCircleDashedCheck
className={composeClassName('icon', className)}
size={size}
@@ -436,7 +452,7 @@ export const CircleDashedCheckIcon = ({ className, size = '1em', stroke = 1.6, .
/>
);
export const FileIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const FileIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconFile
className={composeClassName('icon', className)}
size={size}
@@ -445,7 +461,7 @@ export const FileIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) =>
/>
);
export const FolderOutlineIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const FolderOutlineIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconFolder
className={composeClassName('icon', className)}
size={size}
@@ -454,7 +470,7 @@ export const FolderOutlineIcon = ({ className, size = '1em', stroke = 1.6, ...re
/>
);
export const AnalyzeIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const AnalyzeIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconAnalyze
className={composeClassName('icon', className)}
size={size}
@@ -463,7 +479,7 @@ export const AnalyzeIcon = ({ className, size = '1em', stroke = 1.6, ...rest })
/>
);
export const LoaderIcon = ({ className, size = '1em', stroke = 1.8, ...rest }) => (
export const LoaderIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.8, ...rest }) => (
<IconLoader
className={composeClassName('icon', className)}
size={size}
@@ -472,7 +488,7 @@ export const LoaderIcon = ({ className, size = '1em', stroke = 1.8, ...rest }) =
/>
);
export const WarningIcon = ({ className, size = '1em', stroke = 1.8, ...rest }) => (
export const WarningIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.8, ...rest }) => (
<IconAlertTriangle
className={composeClassName('icon', className)}
size={size}
@@ -481,7 +497,7 @@ export const WarningIcon = ({ className, size = '1em', stroke = 1.8, ...rest })
/>
);
export const WindowMaximizeIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const WindowMaximizeIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconWindowMaximize
className={composeClassName('icon', className)}
size={size}
@@ -490,7 +506,7 @@ export const WindowMaximizeIcon = ({ className, size = '1em', stroke = 1.6, ...r
/>
);
export const LogoutIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const LogoutIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconLogout
className={composeClassName('icon', className)}
size={size}
@@ -499,7 +515,7 @@ export const LogoutIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) =
/>
);
export const ChevronDownIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const ChevronDownIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconChevronDown
className={composeClassName('icon', className)}
size={size}
@@ -532,7 +548,7 @@ export default {
ChevronDownIcon,
};
export const TextScanIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
export const TextScanIcon: TablerIconComponent = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconTextScan2
className={composeClassName('icon', className)}
size={size}
@@ -1,28 +1,41 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import type { MutableRefObject, CSSProperties } from 'react';
import { clamp } from '../utils/math';
const DEFAULT_VIEWPORT_MARGIN = 8;
type PositionStrategy = 'fixed' | 'absolute' | (string & {});
interface FloatingMenuMetrics {
strategy: PositionStrategy;
top: number;
left: number;
minWidth?: number;
width?: number;
}
type FloatingMenuStyle = (CSSProperties & { '--floating-min-width'?: string }) | null;
const resolveViewportWidth = () => window.innerWidth || document.documentElement.clientWidth || 0;
const computeWidth = (anchorWidth, minWidth, matchAnchorWidth) => {
const computeWidth = (anchorWidth: number, minWidth: number, matchAnchorWidth: boolean) => {
if (matchAnchorWidth) {
return Math.max(anchorWidth, minWidth);
}
return Math.max(minWidth || 0, anchorWidth || 0);
};
const formatStyle = (metrics) => {
const formatStyle = (metrics: FloatingMenuMetrics | null): FloatingMenuStyle => {
if (!metrics) {
return null;
}
const style = {
const style: FloatingMenuStyle = {
position: metrics.strategy === 'absolute' ? 'absolute' : 'fixed',
top: metrics.top,
left: metrics.left,
};
if (Number.isFinite(metrics.minWidth)) {
style['--floating-min-width'] = `${Math.max(metrics.minWidth, 0)}px`;
style['--floating-min-width'] = `${Math.max(metrics.minWidth ?? 0, 0)}px`;
}
if (metrics.width) {
style.width = metrics.width;
@@ -30,6 +43,17 @@ const formatStyle = (metrics) => {
return style;
};
interface UseFloatingMenuOptions {
anchorRef?: MutableRefObject<HTMLElement | null>;
offset?: number;
minWidth?: number;
matchAnchorWidth?: boolean;
align?: 'start' | 'center' | 'end' | (string & {});
viewportMargin?: number;
onOpenChange?: (isOpen: boolean) => void;
positionStrategy?: PositionStrategy;
}
const useFloatingMenu = ({
anchorRef,
offset = 6,
@@ -39,9 +63,9 @@ const useFloatingMenu = ({
viewportMargin = DEFAULT_VIEWPORT_MARGIN,
onOpenChange,
positionStrategy = 'fixed',
} = {}) => {
const menuRef = useRef(null);
const [menuMetrics, setMenuMetrics] = useState(null);
}: UseFloatingMenuOptions = {}) => {
const menuRef = useRef<HTMLDivElement | null>(null);
const [menuMetrics, setMenuMetrics] = useState<FloatingMenuMetrics | null>(null);
const [isOpen, setIsOpen] = useState(false);
const onOpenChangeRef = useRef(onOpenChange);
-73
View File
@@ -1,73 +0,0 @@
import { useCallback, useEffect, useRef } from 'react';
const defaultFilter = (event) => {
if (!event) {
return false;
}
const { type, button, pointerType, isPrimary } = event;
const isPointerUp = type === 'pointerup';
const buttonValid =
button == null || button === 0 || (isPointerUp && (button === -1 || button === 0));
if (!buttonValid) {
return false;
}
if (pointerType === 'touch' && isPrimary === false) {
return false;
}
return true;
};
const usePointerTap = ({
onSingle,
onDouble,
delay = 240,
filter = defaultFilter,
} = {}) => {
const timerRef = useRef(null);
useEffect(() => () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
}, []);
return useCallback(
(event, metadata = undefined) => {
if (!filter(event)) {
return;
}
if (typeof event.persist === 'function') {
event.persist();
}
const context = {
clientX: event.clientX,
clientY: event.clientY,
pointerType: event.pointerType,
event,
data: metadata,
};
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
if (typeof onDouble === 'function') {
onDouble(context);
}
return;
}
timerRef.current = setTimeout(() => {
timerRef.current = null;
if (typeof onSingle === 'function') {
onSingle(context);
}
}, delay);
},
[delay, filter, onDouble, onSingle],
);
};
export default usePointerTap;
+94
View File
@@ -0,0 +1,94 @@
import { useCallback, useEffect, useRef } from 'react';
import type { PointerEvent as ReactPointerEvent } from 'react';
export interface PointerTapContext<T = unknown> {
clientX: number;
clientY: number;
pointerType: string;
event: PointerEventWithPersist;
data?: T;
}
type PointerEventWithPersist = (PointerEvent | ReactPointerEvent) & { persist?: () => void };
type PointerTapFilter = (event?: PointerEventWithPersist | null) => boolean;
export interface UsePointerTapOptions<T = unknown> {
onSingle?: (context: PointerTapContext<T>) => void;
onDouble?: (context: PointerTapContext<T>) => void;
delay?: number;
filter?: PointerTapFilter;
}
const defaultFilter: PointerTapFilter = (event) => {
if (!event) {
return false;
}
const { type, button, pointerType, isPrimary } = event;
const isPointerUp = type === 'pointerup';
const buttonValid =
button == null || button === 0 || (isPointerUp && (button === -1 || button === 0));
if (!buttonValid) {
return false;
}
if (pointerType === 'touch' && isPrimary === false) {
return false;
}
return true;
};
const usePointerTap = <T,>({
onSingle,
onDouble,
delay = 240,
filter = defaultFilter,
}: UsePointerTapOptions<T> = {}) => {
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(
() => () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
},
[],
);
return useCallback(
(event?: PointerEventWithPersist | null, metadata?: T) => {
if (!filter(event)) {
return;
}
if (!event) {
return;
}
event.persist?.();
const context: PointerTapContext<T> = {
clientX: event.clientX ?? 0,
clientY: event.clientY ?? 0,
pointerType: event.pointerType ?? 'mouse',
event,
data: metadata,
};
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
onDouble?.(context);
return;
}
timerRef.current = setTimeout(() => {
timerRef.current = null;
onSingle?.(context);
}, delay);
},
[delay, filter, onDouble, onSingle],
);
};
export default usePointerTap;