[ENG-972] quick view improvements (#1233)

* quick preview improvements

* fix ts

* improvements

* fix merge

* non-indexed support

* Update index.tsx

* Update pnpm-lock.yaml

* update quick preview

* sidebar icon weight

* fix thumb

* ts

* fix focus

* remove usePortal

* quick preview store

* Update index.tsx

* Update index.tsx

* cleanup

* add tooltip to name

* hide nav buttons and match explorer nav buttons

---------

Co-authored-by: Jamie Pine <32987599+jamiepine@users.noreply.github.com>
This commit is contained in:
nikec 2023-09-08 13:45:37 +02:00 committed by GitHub
parent 353d8d9a5a
commit 99ccb8f8c7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 926 additions and 461 deletions

View file

@ -1,6 +1,7 @@
import { Image, Package, Trash, TrashSimple } from 'phosphor-react';
import { libraryClient, useLibraryContext, useLibraryMutation } from '@sd/client';
import { ContextMenu, ModifierKeys, dialogManager, toast } from '@sd/ui';
import { Menu } from '~/components/Menu';
import { useKeybindFactory } from '~/hooks/useKeybindFactory';
import { isNonEmpty } from '~/util';
import { usePlatform } from '~/util/Platform';
@ -28,7 +29,7 @@ export const Delete = new ConditionalItem({
const keybind = useKeybindFactory();
return (
<ContextMenu.Item
<Menu.Item
icon={Trash}
label="Delete"
variant="danger"
@ -72,7 +73,7 @@ export const Compress = new ConditionalItem({
const keybind = useKeybindFactory();
return (
<ContextMenu.Item
<Menu.Item
label="Compress"
icon={Package}
keybind={keybind([ModifierKeys.Control], ['B'])}
@ -156,7 +157,7 @@ export const SecureDelete = new ConditionalItem({
return { locationId, selectedFilePaths };
},
Component: ({ locationId, selectedFilePaths }) => (
<ContextMenu.Item
<Menu.Item
variant="danger"
label="Secure delete"
icon={TrashSimple}
@ -242,11 +243,11 @@ export const OpenOrDownload = new ConditionalItem({
const { library } = useLibraryContext();
if (platform === 'web') return <ContextMenu.Item label="Download" />;
if (platform === 'web') return <Menu.Item label="Download" />;
else
return (
<>
<ContextMenu.Item
<Menu.Item
label="Open"
keybind={keybind([ModifierKeys.Control], ['O'])}
onClick={async () => {

View file

@ -1,7 +1,8 @@
import { useQuery } from '@tanstack/react-query';
import { Suspense } from 'react';
import { useLibraryContext } from '@sd/client';
import { ContextMenu, toast } from '@sd/ui';
import { toast } from '@sd/ui';
import { Menu } from '~/components/Menu';
import { Platform, usePlatform } from '~/util/Platform';
import { ConditionalItem } from '../ConditionalItem';
import { useContextMenuContext } from '../context';
@ -17,7 +18,7 @@ export default new ConditionalItem({
return { getFilePathOpenWithApps, openFilePathWith };
},
Component: ({ getFilePathOpenWithApps, openFilePathWith }) => (
<ContextMenu.SubMenu label="Open with">
<Menu.SubMenu label="Open with">
<Suspense>
<Items
actions={{
@ -26,7 +27,7 @@ export default new ConditionalItem({
}}
/>
</Suspense>
</ContextMenu.SubMenu>
</Menu.SubMenu>
)
});
@ -51,7 +52,7 @@ const Items = ({
<>
{Array.isArray(items.data) && items.data.length > 0 ? (
items.data.map((data, id) => (
<ContextMenu.Item
<Menu.Item
key={id}
onClick={async () => {
try {
@ -65,7 +66,7 @@ const Items = ({
}}
>
{data.name}
</ContextMenu.Item>
</Menu.Item>
))
) : (
<p className="w-full text-center text-sm text-gray-400"> No apps available </p>

View file

@ -3,6 +3,7 @@ import { useMemo } from 'react';
import { ObjectKind, type ObjectKindEnum, useLibraryMutation } from '@sd/client';
import { ContextMenu, toast } from '@sd/ui';
import AssignTagMenuItems from '~/components/AssignTagMenuItems';
import { Menu } from '~/components/Menu';
import { isNonEmpty } from '~/util';
import { ConditionalItem } from '../ConditionalItem';
import { useContextMenuContext } from '../context';
@ -47,9 +48,9 @@ export const AssignTag = new ConditionalItem({
return { selectedObjects };
},
Component: ({ selectedObjects }) => (
<ContextMenu.SubMenu label="Assign tag" icon={TagSimple}>
<Menu.SubMenu label="Assign tag" icon={TagSimple}>
<AssignTagMenuItems objects={selectedObjects} />
</ContextMenu.SubMenu>
</Menu.SubMenu>
)
});
@ -82,10 +83,10 @@ export const ConvertObject = new ConditionalItem({
return { kind };
},
Component: ({ kind }) => (
<ContextMenu.SubMenu label="Convert to" icon={ArrowBendUpRight}>
<Menu.SubMenu label="Convert to" icon={ArrowBendUpRight}>
{ObjectConversions[kind]?.map((ext) => (
<ContextMenu.Item key={ext} label={ext} disabled />
<Menu.Item key={ext} label={ext} disabled />
))}
</ContextMenu.SubMenu>
</Menu.SubMenu>
)
});

View file

@ -1,10 +1,12 @@
import { FileX, Share as ShareIcon } from 'phosphor-react';
import { useMemo } from 'react';
import { ContextMenu, ModifierKeys } from '@sd/ui';
import { Menu } from '~/components/Menu';
import { useKeybindFactory } from '~/hooks/useKeybindFactory';
import { isNonEmpty } from '~/util';
import { type Platform } from '~/util/Platform';
import { useExplorerContext } from '../Context';
import { getQuickPreviewStore } from '../QuickPreview/store';
import { RevealInNativeExplorerBase } from '../RevealInNativeExplorer';
import { useExplorerViewContext } from '../ViewContext';
import { getExplorerStore, useExplorerStore } from '../store';
@ -13,16 +15,12 @@ import { useContextMenuContext } from './context';
export const OpenQuickView = () => {
const keybind = useKeybindFactory();
const { selectedItems } = useContextMenuContext();
return (
<ContextMenu.Item
label="Quick view"
keybind={keybind([], [' '])}
onClick={() =>
// using [0] is not great
(getExplorerStore().quickViewObject = selectedItems[0])
}
onClick={() => (getQuickPreviewStore().open = true)}
/>
);
};
@ -149,7 +147,7 @@ export const Deselect = new ConditionalItem({
export const Share = () => {
return (
<>
<ContextMenu.Item
<Menu.Item
label="Share"
icon={ShareIcon}
onClick={(e) => {

View file

@ -1,5 +1,6 @@
import { Plus } from 'phosphor-react';
import { type ReactNode, useMemo } from 'react';
import { type PropsWithChildren, useMemo } from 'react';
import { ExplorerItem } from '@sd/client';
import { ContextMenu } from '@sd/ui';
import { isNonEmpty } from '~/util';
import { useExplorerContext } from '../Context';
@ -13,7 +14,7 @@ export * as FilePathItems from './FilePath/Items';
export * as ObjectItems from './Object/Items';
export * as SharedItems from './SharedItems';
const Items = ({ children }: { children?: () => ReactNode }) => (
const Items = ({ children }: PropsWithChildren) => (
<>
<Conditional items={[FilePathItems.OpenOrDownload]} />
<SharedItems.OpenQuickView />
@ -28,7 +29,8 @@ const Items = ({ children }: { children?: () => ReactNode }) => (
SharedItems.Deselect
]}
/>
{children?.()}
{children}
<ContextMenu.Separator />
<SharedItems.Share />
@ -56,15 +58,19 @@ const Items = ({ children }: { children?: () => ReactNode }) => (
</>
);
export default ({ children }: { children?: () => ReactNode }) => {
export default (props: PropsWithChildren<{ items?: ExplorerItem[]; custom?: boolean }>) => {
const explorer = useExplorerContext();
const selectedItems = useMemo(() => [...explorer.selectedItems], [explorer.selectedItems]);
const selectedItems = useMemo(
() => props.items || [...explorer.selectedItems],
[explorer.selectedItems, props.items]
);
if (!isNonEmpty(selectedItems)) return null;
return (
<ContextMenuContextProvider selectedItems={selectedItems}>
<Items>{children}</Items>
{props.custom ? <>{props.children}</> : <Items>{props.children}</Items>}
</ContextMenuContextProvider>
);
};
@ -72,7 +78,7 @@ export default ({ children }: { children?: () => ReactNode }) => {
/**
* A `Conditional` that inserts a `<ContextMenu.Separator />` above its items.
*/
const SeparatedConditional = ({ items, children }: ConditionalGroupProps) => (
export const SeparatedConditional = ({ items, children }: ConditionalGroupProps) => (
<Conditional items={items}>
{(c) => (
<>

View file

@ -1,11 +1,12 @@
import { ClipboardText } from 'phosphor-react';
import { ContextMenu, toast } from '@sd/ui';
import { toast } from '@sd/ui';
import { Menu } from '~/components/Menu';
export const CopyAsPathBase = (
props: { path: string } | { getPath: () => Promise<string | null> }
) => {
return (
<ContextMenu.Item
<Menu.Item
label="Copy as path"
icon={ClipboardText}
onClick={async () => {

View file

@ -141,10 +141,9 @@ export const RenameTextBoxBase = forwardRef<HTMLDivElement | null, Props>(
// Rename or blur on Enter key
useKey('Enter', (e) => {
e.preventDefault();
if (allowRename) blur();
else if (!disabled) {
e.preventDefault();
setAllowRename(true);
explorerView.setIsRenaming(true);
}

View file

@ -148,12 +148,12 @@ export const FileThumb = memo((props: ThumbProps) => {
{(() => {
if (!src) return;
const className = clsx(
childClassName,
const _childClassName =
typeof props.childClassName === 'function'
? props.childClassName(thumbType)
: props.childClassName
);
: props.childClassName;
const className = clsx(childClassName, _childClassName);
switch (thumbType) {
case ThumbType.Original: {
@ -186,7 +186,7 @@ export const FileThumb = memo((props: ThumbProps) => {
? 'overflow-hidden'
: 'overflow-auto',
className,
props.frame && [frameClassName, '!bg-none']
props.frame && [frameClassName, '!bg-none p-2']
)}
codeExtension={
((itemData.kind === 'Code' ||
@ -257,7 +257,10 @@ export const FileThumb = memo((props: ThumbProps) => {
decoding={props.size ? 'async' : 'sync'}
className={clsx(
props.cover
? 'min-h-full min-w-full object-cover object-center'
? [
'min-h-full min-w-full object-cover object-center',
_childClassName
]
: className,
props.frame && !(itemData.kind === 'Video' && props.blackBars)
? frameClassName
@ -289,7 +292,7 @@ export const FileThumb = memo((props: ThumbProps) => {
onLoad={onLoad}
onError={() => setLoaded(false)}
decoding={props.size ? 'async' : 'sync'}
className={childClassName}
className={className}
draggable={false}
/>
);

View file

@ -41,6 +41,7 @@ import { useIsDark } from '~/hooks';
import { isNonEmpty } from '~/util';
import { useExplorerContext } from '../Context';
import { FileThumb } from '../FilePath/Thumb';
import { useQuickPreviewStore } from '../QuickPreview/store';
import { useExplorerStore } from '../store';
import { uniqueId, useExplorerItemData } from '../util';
import FavoriteButton from './FavoriteButton';
@ -48,10 +49,10 @@ import MediaData from './MediaData';
import Note from './Note';
export const InfoPill = tw.span`inline border border-transparent px-1 text-[11px] font-medium shadow shadow-app-shade/5 bg-app-selected rounded-md text-ink-dull`;
export const PlaceholderPill = tw.span`inline border px-1 text-[11px] shadow shadow-app-shade/10 rounded-md bg-transparent border-dashed border-app-active transition hover:text-ink-faint hover:border-ink-faint font-medium text-ink-faint/70`;
export const PlaceholderPill = tw.span`cursor-default inline border px-1 text-[11px] shadow shadow-app-shade/10 rounded-md bg-transparent border-dashed border-app-active transition hover:text-ink-faint hover:border-ink-faint font-medium text-ink-faint/70`;
export const MetaContainer = tw.div`flex flex-col px-4 py-2 gap-1`;
export const MetaTitle = tw.h5`text-xs font-bold`;
export const MetaTitle = tw.h5`text-xs font-bold text-ink`;
export const INSPECTOR_WIDTH = 260;
@ -113,7 +114,7 @@ export const Inspector = forwardRef<HTMLDivElement, Props>(
);
const Thumbnails = ({ items }: { items: ExplorerItem[] }) => {
const explorerStore = useExplorerStore();
const quickPreviewStore = useQuickPreviewStore();
const lastThreeItems = items.slice(-3).reverse();
@ -128,7 +129,7 @@ const Thumbnails = ({ items }: { items: ExplorerItem[] }) => {
blackBars={thumbs.length === 1}
blackBarsSize={16}
extension={thumbs.length > 1}
pauseVideo={!!explorerStore.quickViewObject || thumbs.length > 1}
pauseVideo={quickPreviewStore.open || thumbs.length > 1}
className={clsx(
thumbs.length > 1 && '!absolute',
i === 0 && thumbs.length > 1 && 'z-30 !h-[76%] !w-[76%]',
@ -146,7 +147,7 @@ const Thumbnails = ({ items }: { items: ExplorerItem[] }) => {
);
};
const SingleItemMetadata = ({ item }: { item: ExplorerItem }) => {
export const SingleItemMetadata = ({ item }: { item: ExplorerItem }) => {
const objectData = getItemObject(item);
const readyToFetch = useIsFetchReady(item);
const isNonIndexed = item.type === 'NonIndexedPath';
@ -197,13 +198,13 @@ const SingleItemMetadata = ({ item }: { item: ExplorerItem }) => {
return (
<>
<h3 className="truncate px-3 pb-1 pt-2 text-base font-bold">
<h3 className="truncate px-3 pb-1 pt-2 text-base font-bold text-ink">
{name}
{extension && `.${extension}`}
</h3>
{objectData && (
<div className="mx-3 mb-0.5 mt-1 flex flex-row space-x-0.5">
<div className="mx-3 mb-0.5 mt-1 flex flex-row space-x-0.5 text-ink">
<Tooltip label="Favorite">
<FavoriteButton data={objectData} />
</Tooltip>

View file

@ -1,127 +1,587 @@
import * as Dialog from '@radix-ui/react-dialog';
import { animated, useTransition } from '@react-spring/web';
import { X } from 'phosphor-react';
import { useEffect, useState } from 'react';
import { subscribeKey } from 'valtio/utils';
import { type ExplorerItem, getExplorerItemData } from '@sd/client';
import { Button } from '@sd/ui';
import clsx from 'clsx';
import { ArrowLeft, ArrowRight, DotsThree, Plus, SidebarSimple, X } from 'phosphor-react';
import {
ButtonHTMLAttributes,
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState
} from 'react';
import {
ObjectKindKey,
getExplorerItemData,
getIndexedItemFilePath,
useLibraryContext,
useLibraryMutation,
useRspcLibraryContext,
useZodForm
} from '@sd/client';
import { DropdownMenu, Form, ModifierKeys, Tooltip, dialogManager, toast, z } from '@sd/ui';
import { useIsDark, useOperatingSystem } from '~/hooks';
import { useKeyBind } from '~/hooks/useKeyBind';
import { usePlatform } from '~/util/Platform';
import { useExplorerContext } from '../Context';
import ExplorerContextMenu, {
FilePathItems,
ObjectItems,
SeparatedConditional,
SharedItems
} from '../ContextMenu';
import { Conditional } from '../ContextMenu/ConditionalItem';
import DeleteDialog from '../FilePath/DeleteDialog';
import { FileThumb } from '../FilePath/Thumb';
import { getExplorerStore } from '../store';
import { SingleItemMetadata } from '../Inspector';
import { getQuickPreviewStore, useQuickPreviewStore } from './store';
const AnimatedDialogOverlay = animated(Dialog.Overlay);
const AnimatedDialogContent = animated(Dialog.Content);
export interface QuickPreviewProps extends Dialog.DialogProps {
transformOrigin?: string;
}
const heavyKinds: ObjectKindKey[] = ['Image', 'Video'];
const iconKinds: ObjectKindKey[] = ['Audio', 'Folder', 'Executable', 'Unknown'];
const withoutBackgroundKinds: ObjectKindKey[] = [
...iconKinds,
'Document',
'Config',
'Code',
'Text'
];
export function QuickPreview({ transformOrigin }: QuickPreviewProps) {
const [explorerItem, setExplorerItem] = useState<null | ExplorerItem>(null);
const explorerStore = getExplorerStore();
const [isOpen, setIsOpen] = useState<boolean>(false);
const QuickPreviewContext = createContext<{ background: boolean } | null>(null);
/**
* The useEffect hook with subscribe is used here, instead of useExplorerStore, because when
* explorerStore.quickViewObject is set to null the component will not close immediately.
* Instead, it will enter the beginning of the close transition and it must continue to display
* content for a few more seconds due to the ongoing animation. To handle this, the open state
* is decoupled from the store state, by assigning references to the required store properties
* to render the component in the subscribe callback.
*/
useEffect(
() =>
subscribeKey(explorerStore, 'quickViewObject', () => {
const { quickViewObject } = explorerStore;
if (quickViewObject != null) {
setIsOpen(true);
setExplorerItem(quickViewObject);
} else {
setIsOpen(false);
}
}),
[explorerStore]
const useQuickPreviewContext = () => {
const context = useContext(QuickPreviewContext);
if (!context) throw new Error('QuickPreviewContext.Provider not found!');
return context;
};
export const QuickPreview = () => {
const os = useOperatingSystem();
const rspc = useRspcLibraryContext();
const isDark = useIsDark();
const { library } = useLibraryContext();
const { openFilePaths, revealItems } = usePlatform();
const explorer = useExplorerContext();
const { open, itemIndex } = useQuickPreviewStore();
const [loadOriginal, setLoadOriginal] = useState(false);
const [showMetadata, setShowMetadata] = useState<boolean>(false);
const [isContextMenuOpen, setIsContextMenuOpen] = useState<boolean>(false);
const [isRenaming, setIsRenaming] = useState<boolean>(false);
const [newName, setNewName] = useState<string | null>(null);
const items = useMemo(
() => (open ? [...explorer.selectedItems] : []),
[explorer.selectedItems, open]
);
const transitions = useTransition(isOpen, {
const item = useMemo(() => items[itemIndex], [items, itemIndex]);
const transitions = useTransition(open, {
from: {
opacity: 0,
transform: `translateY(20px) scale(0.9)`,
transformOrigin: transformOrigin || 'center top'
transformOrigin: 'center top'
},
enter: { opacity: 1, transform: `translateY(0px) scale(1)` },
leave: { opacity: 0, transform: `translateY(40px) scale(0.9)` },
leave: { opacity: 0, immediate: true },
config: { mass: 0.2, tension: 300, friction: 20, bounce: 0 }
});
const renameFile = useLibraryMutation(['files.renameFile'], {
onError: () => setNewName(null),
onSuccess: () => rspc.queryClient.invalidateQueries(['search.paths'])
});
const changeCurrentItem = (index: number) => {
if (!items[index]) return;
getQuickPreviewStore().itemIndex = index;
setNewName(null);
setLoadOriginal(false);
};
useEffect(() => {
setLoadOriginal(false);
if (!open || !item) {
getQuickPreviewStore().itemIndex = 0;
setShowMetadata(false);
setNewName(null);
if (!item) getQuickPreviewStore().open = false;
return;
}
const { kind } = getExplorerItemData(item);
if (iconKinds.includes(kind) || !heavyKinds.includes(kind)) {
setLoadOriginal(true);
return;
}
const timeout = setTimeout(() => setLoadOriginal(true), 350);
return () => clearTimeout(timeout);
}, [item, open]);
// Toggle quick preview
useKeyBind(['space'], (e) => {
if (isRenaming) return;
e.preventDefault();
getQuickPreviewStore().open = !open;
});
// Move between items
useKeyBind([['left'], ['right']], (e) => {
if (isContextMenuOpen || isRenaming) return;
changeCurrentItem(e.key === 'ArrowLeft' ? itemIndex - 1 : itemIndex + 1);
});
// Toggle metadata
useKeyBind([os === 'macOS' ? ModifierKeys.Meta : ModifierKeys.Control, 'i'], () =>
setShowMetadata(!showMetadata)
);
// Open file
useKeyBind([os === 'macOS' ? ModifierKeys.Meta : ModifierKeys.Control, 'o'], () => {
if (!item || !openFilePaths) return;
try {
const path = getIndexedItemFilePath(item);
if (!path) throw 'No path found';
openFilePaths(library.uuid, [path.id]);
} catch (error) {
toast.error({
title: 'Failed to open file',
body: `Couldn't open file, due to an error: ${error}`
});
}
});
// Reveal in native explorer
useKeyBind([os === 'macOS' ? ModifierKeys.Meta : ModifierKeys.Control, 'y'], () => {
if (!item || !revealItems) return;
try {
const id = item.type === 'Location' ? item.item.id : getIndexedItemFilePath(item)?.id;
if (!id) throw 'No id found';
revealItems(library.uuid, [
{ ...(item.type === 'Location' ? { Location: { id } } : { FilePath: { id } }) }
]);
} catch (error) {
toast.error({
title: 'Failed to reveal',
body: `Couldn't reveal file, due to an error: ${error}`
});
}
});
// Open delete dialog
useKeyBind([os === 'macOS' ? ModifierKeys.Meta : ModifierKeys.Control, 'backspace'], () => {
if (!item) return;
const path = getIndexedItemFilePath(item);
if (!path || path.location_id === null) return;
dialogManager.create((dp) => (
<DeleteDialog {...dp} locationId={path.location_id!} pathIds={[path.id]} />
));
});
return (
<>
<Dialog.Root
open={isOpen}
onOpenChange={(open) => {
setIsOpen(open);
if (!open) explorerStore.quickViewObject = null;
}}
>
{transitions((styles, show) => {
if (!show || explorerItem == null) return null;
<Dialog.Root open={open} onOpenChange={(open) => (getQuickPreviewStore().open = open)}>
{transitions((styles, show) => {
if (!show || !item) return null;
const { name } = getExplorerItemData(explorerItem);
const { kind, ...itemData } = getExplorerItemData(item);
return (
<>
<Dialog.Portal forceMount>
<AnimatedDialogOverlay
style={{
opacity: styles.opacity
}}
className="z-49 absolute inset-0 m-[1px] grid place-items-center overflow-y-auto rounded-xl bg-app/50"
/>
<AnimatedDialogContent
style={styles}
className="!pointer-events-none absolute inset-0 z-50 grid h-screen place-items-center"
const name =
newName ||
`${itemData.name}${itemData.extension ? `.${itemData.extension}` : ''}`;
const background = !withoutBackgroundKinds.includes(kind);
const icon = iconKinds.includes(kind);
return (
<QuickPreviewContext.Provider value={{ background }}>
<Dialog.Portal forceMount>
<AnimatedDialogOverlay
className={clsx(
'absolute inset-0 z-50',
isDark ? 'bg-black/80' : 'bg-black/60'
)}
style={{ opacity: styles.opacity }}
onContextMenu={(e) => e.preventDefault()}
/>
<AnimatedDialogContent
className="fixed inset-[5%] z-50 outline-none"
style={styles}
onOpenAutoFocus={(e) => e.preventDefault()}
onEscapeKeyDown={(e) => isRenaming && e.preventDefault()}
onContextMenu={(e) => e.preventDefault()}
>
<div
className={clsx(
'flex h-full overflow-hidden rounded-md border',
isDark ? 'border-app-line/80' : 'border-app-line/10'
)}
>
<div className="!pointer-events-auto flex h-5/6 max-h-screen w-11/12 flex-col overflow-y-auto rounded-md border border-app-line bg-app-box text-ink shadow-app-shade">
<nav className="relative flex w-full flex-row">
<Dialog.Close
asChild
className="absolute m-2"
aria-label="Close"
>
<Button
size="icon"
variant="outline"
className="flex flex-row"
>
<X
weight="bold"
className=" h-3 w-3 text-ink-faint"
<div className="relative flex flex-1 flex-col overflow-hidden bg-app/80 backdrop-blur">
{background && (
<div className="absolute inset-0 overflow-hidden bg-black/90">
<FileThumb
data={item}
cover={true}
childClassName="opacity-75 blur-3xl scale-125"
/>
</div>
)}
<div
className={clsx(
'z-50 flex items-center p-2',
background ? 'text-white' : 'text-ink'
)}
>
<div className="flex flex-1">
<Tooltip label="Close">
<Dialog.Close asChild>
<IconButton>
<X weight="bold" />
</IconButton>
</Dialog.Close>
</Tooltip>
{items.length > 1 && (
<div className="ml-2 flex">
<Tooltip label="Back">
<IconButton
disabled={!items[itemIndex - 1]}
onClick={() =>
changeCurrentItem(itemIndex - 1)
}
className="rounded-r-none"
>
<ArrowLeft weight="bold" />
</IconButton>
</Tooltip>
<Tooltip label="Forward">
<IconButton
disabled={!items[itemIndex + 1]}
onClick={() =>
changeCurrentItem(itemIndex + 1)
}
className="rounded-l-none"
>
<ArrowRight weight="bold" />
</IconButton>
</Tooltip>
</div>
)}
</div>
<div className="flex w-1/2 items-center justify-center truncate text-sm">
{isRenaming && name ? (
<RenameInput
name={name}
onRename={(newName) => {
setIsRenaming(false);
if (
!('id' in item.item) ||
!newName ||
newName === name
)
return;
const filePathData =
getIndexedItemFilePath(item);
if (!filePathData) return;
const locationId =
filePathData.location_id;
if (locationId === null) return;
renameFile.mutate({
location_id: locationId,
kind: {
One: {
from_file_path_id:
item.item.id,
to: newName
}
}
});
setNewName(newName);
}}
/>
<span className="ml-1 text-tiny font-medium text-ink-faint">
ESC
</span>
</Button>
</Dialog.Close>
<Dialog.Title className="mx-auto my-2 font-bold">
Preview -{' '}
<span className="inline-block max-w-xs truncate align-sub text-sm text-ink-dull">
{name || 'Unknown Object'}
</span>
</Dialog.Title>
</nav>
<div className="flex h-full w-full shrink items-center justify-center overflow-hidden">
) : (
<Tooltip label={name} className="truncate">
<span
onClick={() =>
name &&
item.type !== 'NonIndexedPath' &&
setIsRenaming(true)
}
className={clsx(
item.type === 'NonIndexedPath'
? 'cursor-default'
: 'cursor-text'
)}
>
{name}
</span>
</Tooltip>
)}
</div>
<div className="flex flex-1 justify-end gap-1">
{item.type !== 'NonIndexedPath' && (
<DropdownMenu.Root
trigger={
<div className="flex">
<Tooltip label="More">
<IconButton>
<DotsThree
size={20}
weight="bold"
/>
</IconButton>
</Tooltip>
</div>
}
onOpenChange={setIsContextMenuOpen}
align="end"
sideOffset={-10}
>
<ExplorerContextMenu items={[item]} custom>
<Conditional
items={[
FilePathItems.OpenOrDownload,
SharedItems.RevealInNativeExplorer
]}
/>
<DropdownMenu.Item
label="Rename"
onClick={() =>
name && setIsRenaming(true)
}
/>
<SeparatedConditional
items={[ObjectItems.AssignTag]}
/>
<Conditional
items={[
FilePathItems.CopyAsPath,
FilePathItems.Crypto,
FilePathItems.Compress,
ObjectItems.ConvertObject,
FilePathItems.SecureDelete
]}
>
{(items) => (
<DropdownMenu.SubMenu
label="More actions..."
icon={Plus}
>
{items}
</DropdownMenu.SubMenu>
)}
</Conditional>
<SeparatedConditional
items={[FilePathItems.Delete]}
/>
</ExplorerContextMenu>
</DropdownMenu.Root>
)}
<Tooltip label="Show details">
<IconButton
onClick={() =>
setShowMetadata(!showMetadata)
}
active={showMetadata}
>
<SidebarSimple
className="rotate-180"
weight={
showMetadata ? 'fill' : 'regular'
}
/>
</IconButton>
</Tooltip>
</div>
</div>
{loadOriginal && (
<FileThumb
data={explorerItem}
data={item}
loadOriginal
mediaControls
className={clsx(
'm-3 !w-auto flex-1 !overflow-hidden rounded',
!background && !icon && 'bg-app-box shadow'
)}
childClassName={clsx(
'rounded',
kind === 'Text' && 'p-3',
!icon && 'h-full'
)}
/>
</div>
)}
</div>
</AnimatedDialogContent>
</Dialog.Portal>
</>
);
})}
</Dialog.Root>
</>
{showMetadata && (
<div className="no-scrollbar w-64 shrink-0 border-l border-app-line bg-app-darkBox py-1">
<SingleItemMetadata item={item} />
</div>
)}
</div>
</AnimatedDialogContent>
</Dialog.Portal>
</QuickPreviewContext.Provider>
);
})}
</Dialog.Root>
);
};
interface RenameInputProps {
name: string;
onRename: (name: string) => void;
}
const RenameInput = ({ name, onRename }: RenameInputProps) => {
const isDark = useIsDark();
const os = useOperatingSystem();
const quickPreview = useQuickPreviewContext();
const _ref = useRef<HTMLInputElement | null>(null);
const form = useZodForm({ schema: z.object({ name: z.string() }), defaultValues: { name } });
const onSubmit = form.handleSubmit(({ name }) => onRename(name));
const { ref, ...register } = form.register('name', {
onBlur: onSubmit
});
const highlightName = useCallback(() => {
const endRange = name.lastIndexOf('.');
setTimeout(() => _ref.current?.setSelectionRange(0, endRange || name.length));
}, [name]);
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
e.stopPropagation();
switch (e.key) {
case 'Tab': {
e.preventDefault();
onSubmit();
break;
}
case 'Escape': {
form.reset();
onSubmit();
break;
}
case 'z': {
if (os === 'macOS' ? e.metaKey : e.ctrlKey) {
form.reset();
highlightName();
}
}
}
};
useEffect(() => {
if (document.activeElement !== _ref.current) highlightName();
}, [highlightName]);
return (
<Form form={form} onSubmit={onSubmit} className="w-1/2">
<input
autoFocus
autoCorrect="off"
className={clsx(
'w-full rounded border px-2 py-1 text-center outline-none',
quickPreview.background
? 'border-white/[.12] bg-white/10 backdrop-blur-sm'
: isDark
? 'border-app-line bg-app-input'
: 'border-black/[.075] bg-black/[.075]'
)}
onKeyDown={handleKeyDown}
onFocus={() => highlightName()}
ref={(e) => {
ref(e);
_ref.current = e;
}}
{...register}
/>
</Form>
);
};
const IconButton = ({
className,
active,
...props
}: ButtonHTMLAttributes<HTMLButtonElement> & { active?: boolean }) => {
const isDark = useIsDark();
const quickPreview = useQuickPreviewContext();
return (
<button
className={clsx(
'text-md inline-flex h-[30px] w-[30px] items-center justify-center rounded opacity-80 outline-none backdrop-blur-none',
'hover:opacity-100 hover:backdrop-blur',
'focus:opacity-100 focus:backdrop-blur',
'disabled:pointer-events-none disabled:opacity-40',
isDark || quickPreview.background
? quickPreview.background
? 'hover:bg-white/[.15] focus:bg-white/[.15]'
: 'hover:bg-app-box focus:bg-app-box'
: 'hover:bg-black/[.075] focus:bg-black/[.075]',
active && [
'!opacity-100 backdrop-blur',
isDark || quickPreview.background
? quickPreview.background
? 'bg-white/[.15]'
: 'bg-app-box'
: 'bg-black/[.075]'
],
className
)}
{...props}
/>
);
};

View file

@ -0,0 +1,9 @@
import { proxy, useSnapshot } from 'valtio';
const store = proxy({
open: false,
itemIndex: 0
});
export const useQuickPreviewStore = () => useSnapshot(store);
export const getQuickPreviewStore = () => store;

View file

@ -1,5 +1,6 @@
import { useLibraryContext } from '@sd/client';
import { ContextMenu, ModifierKeys } from '@sd/ui';
import { ModifierKeys } from '@sd/ui';
import { Menu } from '~/components/Menu';
import { useOperatingSystem } from '~/hooks';
import { useKeybindFactory } from '~/hooks/useKeybindFactory';
import { NonEmptyArray } from '~/util';
@ -24,7 +25,7 @@ export const RevealInNativeExplorerBase = (props: { items: RevealItems }) => {
const osFileBrowserName = lookup[os] ?? 'file manager';
return (
<ContextMenu.Item
<Menu.Item
label={`Reveal in ${osFileBrowserName}`}
keybind={keybind([ModifierKeys.Control], ['Y'])}
onClick={() => revealItems(library.uuid, props.items)}

View file

@ -217,10 +217,10 @@ export default ({ children }: { children: RenderItem }) => {
}, [explorer.selectedItems]);
useKey(['ArrowUp', 'ArrowDown', 'ArrowRight', 'ArrowLeft'], (e) => {
if (explorer.selectedItems.size > 0) e.preventDefault();
if (!explorerView.selectable) return;
if (explorer.selectedItems.size > 0) e.preventDefault();
const lastItem = activeItem.current;
if (!lastItem) return;

View file

@ -4,6 +4,7 @@ import { type ExplorerItem, byteSize, getItemFilePath, getItemLocation } from '@
import { ViewItem } from '.';
import { useExplorerContext } from '../Context';
import { FileThumb } from '../FilePath/Thumb';
import { useQuickPreviewStore } from '../QuickPreview/store';
import { useExplorerViewContext } from '../ViewContext';
import GridList from './GridList';
import RenamableItemText from './RenamableItemText';
@ -67,6 +68,7 @@ const GridViewItem = memo(({ data, selected, cut, isRenaming, renamable }: GridV
export default () => {
const explorer = useExplorerContext();
const explorerView = useExplorerViewContext();
const quickPreviewStore = useQuickPreviewStore();
return (
<GridList>
@ -76,7 +78,7 @@ export default () => {
selected={selected}
cut={cut}
isRenaming={explorerView.isRenaming}
renamable={explorer.selectedItems.size === 1}
renamable={explorer.selectedItems.size === 1 && !quickPreviewStore.open}
/>
)}
</GridList>

View file

@ -6,7 +6,7 @@ import { Button } from '@sd/ui';
import { ViewItem } from '.';
import { useExplorerContext } from '../Context';
import { FileThumb } from '../FilePath/Thumb';
import { getExplorerStore, useExplorerStore } from '../store';
import { getQuickPreviewStore } from '../QuickPreview/store';
import GridList from './GridList';
interface MediaViewItemProps {
@ -44,7 +44,7 @@ const MediaViewItem = memo(({ data, selected, cut }: MediaViewItemProps) => {
variant="gray"
size="icon"
className="absolute right-2 top-2 hidden rounded-full shadow group-hover:block"
onClick={() => (getExplorerStore().quickViewObject = data)}
onClick={() => (getQuickPreviewStore().open = true)}
>
<ArrowsOutSimple />
</Button>

View file

@ -32,6 +32,7 @@ import CreateDialog from '../../settings/library/tags/CreateDialog';
import { useExplorerContext } from '../Context';
import { QuickPreview } from '../QuickPreview';
import { useQuickPreviewContext } from '../QuickPreview/Context';
import { getQuickPreviewStore, useQuickPreviewStore } from '../QuickPreview/store';
import { type ExplorerViewContext, ViewContext, useExplorerViewContext } from '../ViewContext';
import { useExplorerConfigStore } from '../config';
import { getExplorerStore } from '../store';
@ -57,10 +58,17 @@ export const ViewItem = ({ data, children, ...props }: ViewItemProps) => {
const updateAccessTime = useLibraryMutation('files.updateAccessTime');
const onDoubleClick = async () => {
const selectedItems = [...explorer.selectedItems].reduce(
(items, item) => {
const selectedItems = [...explorer.selectedItems];
if (!isNonEmpty(selectedItems)) return;
let itemIndex = 0;
const items = selectedItems.reduce(
(items, item, i) => {
const sameAsClicked = uniqueId(data) === uniqueId(item);
if (sameAsClicked) itemIndex = i;
switch (item.type) {
case 'Location': {
items.locations.splice(sameAsClicked ? 0 : -1, 0, item.item);
@ -99,32 +107,31 @@ export const ViewItem = ({ data, children, ...props }: ViewItemProps) => {
}
);
if (selectedItems.paths.length > 0 && !explorerView.isRenaming) {
if (items.paths.length > 0 && !explorerView.isRenaming) {
if (explorerConfig.openOnDoubleClick && openFilePaths) {
updateAccessTime
.mutateAsync(
selectedItems.paths.map(({ object_id }) => object_id!).filter(Boolean)
)
.mutateAsync(items.paths.map(({ object_id }) => object_id!).filter(Boolean))
.catch(console.error);
try {
await openFilePaths(
library.uuid,
selectedItems.paths.map(({ id }) => id)
items.paths.map(({ id }) => id)
);
} catch (error) {
toast.error({ title: 'Failed to open file', body: `Error: ${error}.` });
}
} else if (!explorerConfig.openOnDoubleClick) {
if (data.type !== 'Location' && !(isPath(data) && data.item.is_dir)) {
getExplorerStore().quickViewObject = data;
getQuickPreviewStore().itemIndex = itemIndex;
getQuickPreviewStore().open = true;
return;
}
}
}
if (selectedItems.dirs.length > 0) {
const [item] = selectedItems.dirs;
if (items.dirs.length > 0) {
const [item] = items.dirs;
if (item) {
navigate({
pathname: `../location/${item.location_id}`,
@ -136,8 +143,8 @@ export const ViewItem = ({ data, children, ...props }: ViewItemProps) => {
}
}
if (selectedItems.locations.length > 0) {
const [location] = selectedItems.locations;
if (items.locations.length > 0) {
const [location] = items.locations;
if (location) {
navigate({
pathname: `../location/${location.id}`,
@ -149,8 +156,8 @@ export const ViewItem = ({ data, children, ...props }: ViewItemProps) => {
}
}
if (selectedItems.non_indexed.length > 0) {
const [non_indexed] = selectedItems.non_indexed;
if (items.non_indexed.length > 0) {
const [non_indexed] = items.non_indexed;
if (non_indexed) {
navigate({
search: createSearchParams({ path: non_indexed.path }).toString()
@ -189,8 +196,9 @@ export interface ExplorerViewProps
export default memo(({ className, style, emptyNotice, ...contextProps }: ExplorerViewProps) => {
const explorer = useExplorerContext();
const quickPreviewStore = useQuickPreviewStore();
const quickPreviewCtx = useQuickPreviewContext();
const quickPreview = useQuickPreviewContext();
const { layoutMode } = explorer.useSettingsSnapshot();
@ -200,20 +208,9 @@ export default memo(({ className, style, emptyNotice, ...contextProps }: Explore
const [isRenaming, setIsRenaming] = useState(false);
useKeyDownHandlers({
isRenaming
disabled: isRenaming || quickPreviewStore.open
});
useEffect(() => {
// using .next() is not great
const explorerStore = getExplorerStore();
const selectedItem = explorer.selectedItems.values().next().value as
| ExplorerItem
| undefined;
if (explorerStore.quickViewObject != null && selectedItem) {
explorerStore.quickViewObject = selectedItem;
}
}, [explorer.selectedItems]);
return (
<>
<div
@ -228,17 +225,18 @@ export default memo(({ className, style, emptyNotice, ...contextProps }: Explore
>
{explorer.items === null || (explorer.items && explorer.items.length > 0) ? (
<ViewContext.Provider
value={
{
...contextProps,
selectable:
explorer.selectable && !isContextMenuOpen && !isRenaming,
setIsContextMenuOpen,
isRenaming,
setIsRenaming,
ref
} as ExplorerViewContext
}
value={{
...contextProps,
selectable:
explorer.selectable &&
!isContextMenuOpen &&
!isRenaming &&
!quickPreviewStore.open,
setIsContextMenuOpen,
isRenaming,
setIsRenaming,
ref
}}
>
{layoutMode === 'grid' && <GridView />}
{layoutMode === 'list' && <ListView />}
@ -248,7 +246,8 @@ export default memo(({ className, style, emptyNotice, ...contextProps }: Explore
emptyNotice
)}
</div>
{quickPreviewCtx.ref && createPortal(<QuickPreview />, quickPreviewCtx.ref)}
{quickPreview.ref && createPortal(<QuickPreview />, quickPreview.ref)}
</>
);
});
@ -284,7 +283,7 @@ export const EmptyNotice = (props: { icon?: Icon | ReactNode; message?: ReactNod
);
};
const useKeyDownHandlers = ({ isRenaming }: { isRenaming: boolean }) => {
const useKeyDownHandlers = ({ disabled }: { disabled: boolean }) => {
const explorer = useExplorerContext();
const os = useOperatingSystem();
@ -316,7 +315,7 @@ const useKeyDownHandlers = ({ isRenaming }: { isRenaming: boolean }) => {
const handleOpenShortcut = useCallback(
async (event: KeyboardEvent) => {
if (
event.code.toUpperCase() !== 'O' ||
event.key.toUpperCase() !== 'O' ||
!event.getModifierState(
os === 'macOS' ? ModifierKeys.Meta : ModifierKeys.Control
) ||
@ -345,22 +344,6 @@ const useKeyDownHandlers = ({ isRenaming }: { isRenaming: boolean }) => {
[os, library.uuid, openFilePaths, explorer.selectedItems]
);
const handleOpenQuickPreview = useCallback(
async (event: KeyboardEvent) => {
if (event.key !== ' ') return;
if (!getExplorerStore().quickViewObject) {
// ENG-973 - Don't use Set -> Array -> First Item
const items = [...explorer.selectedItems];
if (!isNonEmpty(items)) return;
getExplorerStore().quickViewObject = items[0];
} else {
getExplorerStore().quickViewObject = null;
}
},
[explorer.selectedItems]
);
const handleExplorerShortcut = useCallback(
(event: KeyboardEvent) => {
if (
@ -375,23 +358,12 @@ const useKeyDownHandlers = ({ isRenaming }: { isRenaming: boolean }) => {
);
useEffect(() => {
const handlers = [
handleNewTag,
handleOpenShortcut,
handleOpenQuickPreview,
handleExplorerShortcut
];
const handlers = [handleNewTag, handleOpenShortcut, handleExplorerShortcut];
const handler = (event: KeyboardEvent) => {
if (isRenaming) return;
if (event.repeat || disabled) return;
for (const handler of handlers) handler(event);
};
document.body.addEventListener('keydown', handler);
return () => document.body.removeEventListener('keydown', handler);
}, [
isRenaming,
handleNewTag,
handleOpenShortcut,
handleOpenQuickPreview,
handleExplorerShortcut
]);
}, [disabled, handleNewTag, handleOpenShortcut, handleExplorerShortcut]);
};

View file

@ -106,7 +106,6 @@ const state = {
mediaPlayerVolume: 0.7,
newThumbnails: proxySet() as Set<string>,
cutCopyState: { type: 'Idle' } as CutCopyState,
quickViewObject: null as ExplorerItem | null,
isDragging: false,
gridGap: 8
};

View file

@ -47,7 +47,7 @@ export const Component = () => {
className={settings.layoutMode === 'list' ? 'min-w-0' : undefined}
contextMenu={
<ContextMenu>
{() => <Conditional items={[ObjectItems.RemoveFromRecents]} />}
<Conditional items={[ObjectItems.RemoveFromRecents]} />
</ContextMenu>
}
emptyNotice={

View file

@ -3,18 +3,12 @@ import clsx from 'clsx';
import { Plus } from 'phosphor-react';
import { useRef } from 'react';
import { Object, useLibraryMutation, useLibraryQuery, usePlausibleEvent } from '@sd/client';
import {
ContextMenu,
DropdownMenu,
ModifierKeys,
dialogManager,
useContextMenu,
useDropdownMenu
} from '@sd/ui';
import { ModifierKeys, dialogManager } from '@sd/ui';
import CreateDialog from '~/app/$libraryId/settings/library/tags/CreateDialog';
import { useOperatingSystem } from '~/hooks';
import { useScrolled } from '~/hooks/useScrolled';
import { keybindForOs } from '~/util/keybinds';
import { Menu } from './Menu';
export default (props: { objects: Object[] }) => {
const os = useOperatingSystem();
@ -44,11 +38,6 @@ export default (props: { objects: Object[] }) => {
const { isScrolled } = useScrolled(parentRef, 10);
const isDropdownMenu = useDropdownMenu();
const isContextMenu = useContextMenu();
const Menu = isDropdownMenu ? DropdownMenu : isContextMenu ? ContextMenu : undefined;
if (!Menu) return null;
return (
<>
<Menu.Item

View file

@ -0,0 +1,48 @@
import { useMemo } from 'react';
import { ContextMenu, DropdownMenu, useContextMenuContext, useDropdownMenuContext } from '@sd/ui';
export const useMenu = (): typeof DropdownMenu | typeof ContextMenu | undefined => {
const isDropdownMenu = useDropdownMenuContext();
const isContextMenu = useContextMenuContext();
const menu = useMemo(
() => (isDropdownMenu ? DropdownMenu : isContextMenu ? ContextMenu : undefined),
[isDropdownMenu, isContextMenu]
);
return menu;
};
const Separator = (
props: Parameters<typeof ContextMenu.Separator | typeof DropdownMenu.Separator>[0]
) => {
const Menu = useMenu();
if (!Menu) return null;
return <Menu.Separator {...props} />;
};
const SubMenu = (
props: Parameters<typeof ContextMenu.SubMenu | typeof DropdownMenu.SubMenu>[0]
) => {
const Menu = useMenu();
if (!Menu) return null;
return <Menu.SubMenu {...props} />;
};
const Item = (props: Parameters<typeof ContextMenu.Item | typeof DropdownMenu.Item>[0]) => {
const ContextMenu = useMenu();
if (!ContextMenu) return null;
return <ContextMenu.Item {...props} />;
};
export const Menu = {
Item,
Separator,
SubMenu
};

View file

@ -70,6 +70,7 @@ export const TextViewer = memo(
ref={ref}
tabIndex={0}
className={clsx(
'text-ink',
className,
highlight && ['relative !pl-[3.8em]', `language-${highlight.language}`]
)}

View file

@ -0,0 +1,35 @@
import { DependencyList } from 'react';
import { HotkeyCallback, Options, useHotkeys } from 'react-hotkeys-hook';
interface UseKeyBindOptions extends Options {
repeatable?: boolean;
}
type UseKeyBindOptionsOrDependencyArray = UseKeyBindOptions | DependencyList;
export const useKeyBind = (
keys: string | string[] | string[][],
callback: HotkeyCallback,
options?: UseKeyBindOptionsOrDependencyArray,
dependencies?: UseKeyBindOptionsOrDependencyArray
) => {
const keyCombination = Array.isArray(keys)
? Array.isArray(keys[0])
? keys.map((k) => (k as string[]).join('+'))
: keys.join('+')
: keys;
const repeatable =
typeof options === 'object' && 'repeatable' in options
? options.repeatable
: typeof dependencies === 'object' && 'repeatable' in dependencies
? dependencies.repeatable
: false;
return useHotkeys(
keyCombination,
(e, k) => (repeatable || !e.repeat) && callback(e, k),
options,
dependencies
);
};

View file

@ -55,6 +55,7 @@
"react-dom": "^18.2.0",
"react-error-boundary": "^3.1.4",
"react-hook-form": "~7.45.2",
"react-hotkeys-hook": "^4.4.1",
"react-json-view": "^1.21.3",
"react-loading-skeleton": "^3.1.0",
"react-qr-code": "^2.0.11",

View file

@ -12,6 +12,14 @@ export function getItemFilePath(data: ExplorerItem) {
return (data.type === 'Object' && data.item.file_paths[0]) || null;
}
export function getIndexedItemFilePath(data: ExplorerItem) {
return data.type === 'Path'
? data.item
: data.type === 'Object'
? data.item.file_paths[0] ?? null
: null;
}
export function getItemLocation(data: ExplorerItem) {
return data.type === 'Location' ? data.item : null;
}

View file

@ -23,9 +23,9 @@
"@headlessui/tailwindcss": "^0.1.1",
"@hookform/resolvers": "^3.1.0",
"@radix-ui/react-checkbox": "^1.0.3",
"@radix-ui/react-context-menu": "^1.0.0",
"@radix-ui/react-context-menu": "^2.1.4",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^1.0.0",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-popover": "^1.0.6",
"@radix-ui/react-radio-group": "^1.1.0",
"@radix-ui/react-select": "^1.1.2",

View file

@ -2,7 +2,7 @@ import * as RadixCM from '@radix-ui/react-context-menu';
import { VariantProps, cva } from 'class-variance-authority';
import clsx from 'clsx';
import { CaretRight, Icon, IconProps } from 'phosphor-react';
import { PropsWithChildren, Suspense, createContext, useContext } from 'react';
import { ContextType, PropsWithChildren, Suspense, createContext, useContext } from 'react';
interface ContextMenuProps extends RadixCM.MenuContentProps {
trigger: React.ReactNode;
@ -19,8 +19,17 @@ export const contextMenuClassNames = clsx(
'animate-in fade-in'
);
const context = createContext<boolean>(false);
export const useContextMenu = () => useContext(context);
const ContextMenuContext = createContext<boolean | null>(null);
export const useContextMenuContext = <T extends boolean>({ suspense }: { suspense?: T } = {}) => {
const ctx = useContext(ContextMenuContext);
if (suspense && ctx === null) throw new Error('ContextMenuContext.Provider not found!');
return ctx as T extends true
? NonNullable<ContextType<typeof ContextMenuContext>>
: NonNullable<ContextType<typeof ContextMenuContext>> | undefined;
};
const Root = ({
trigger,
@ -37,7 +46,9 @@ const Root = ({
</RadixCM.Trigger>
<RadixCM.Portal>
<RadixCM.Content className={clsx(contextMenuClassNames, className)} {...props}>
<context.Provider value={true}>{children}</context.Provider>
<ContextMenuContext.Provider value={true}>
{children}
</ContextMenuContext.Provider>
</RadixCM.Content>
</RadixCM.Portal>
</RadixCM.Root>

View file

@ -1,8 +1,10 @@
import * as RadixDM from '@radix-ui/react-dropdown-menu';
import clsx from 'clsx';
import React, {
ContextType,
PropsWithChildren,
Suspense,
createContext,
useCallback,
useContext,
useRef,
@ -17,24 +19,38 @@ import {
contextMenuSeparatorClassNames
} from './ContextMenu';
interface DropdownMenuProps extends RadixDM.MenuContentProps {
interface DropdownMenuProps
extends RadixDM.MenuContentProps,
Pick<RadixDM.DropdownMenuProps, 'onOpenChange'> {
trigger: React.ReactNode;
triggerClassName?: string;
alignToTrigger?: boolean;
}
const context = React.createContext<boolean>(false);
export const useDropdownMenu = () => useContext(context);
const DropdownMenuContext = createContext<boolean | null>(null);
export const useDropdownMenuContext = <T extends boolean>({ suspense }: { suspense?: T } = {}) => {
const ctx = useContext(DropdownMenuContext);
if (suspense && ctx === null) throw new Error('DropdownMenuContext.Provider not found!');
return ctx as T extends true
? NonNullable<ContextType<typeof DropdownMenuContext>>
: NonNullable<ContextType<typeof DropdownMenuContext>> | undefined;
};
const Root = (props: PropsWithChildren<DropdownMenuProps>) => {
const {
alignToTrigger,
onOpenChange,
trigger,
triggerClassName,
asChild = true,
className,
children,
...contentProps
} = props;
const Root = ({
trigger,
children,
className,
asChild = true,
triggerClassName,
alignToTrigger,
...props
}: PropsWithChildren<DropdownMenuProps>) => {
const [width, setWidth] = useState<number>();
const measureRef = useCallback(
@ -45,8 +61,8 @@ const Root = ({
);
return (
<RadixDM.Root>
<RadixDM.Trigger ref={measureRef} asChild={asChild} className={triggerClassName}>
<RadixDM.Root onOpenChange={onOpenChange}>
<RadixDM.Trigger ref={measureRef} className={triggerClassName} asChild={asChild}>
{trigger}
</RadixDM.Trigger>
<RadixDM.Portal>
@ -54,9 +70,11 @@ const Root = ({
className={clsx(contextMenuClassNames, width && '!min-w-0', className)}
align="start"
style={{ width }}
{...props}
{...contentProps}
>
<context.Provider value={true}>{children}</context.Provider>
<DropdownMenuContext.Provider value={true}>
{children}
</DropdownMenuContext.Provider>
</RadixDM.Content>
</RadixDM.Portal>
</RadixDM.Root>

View file

@ -8,11 +8,12 @@ export interface TooltipProps extends PropsWithChildren {
className?: string;
tooltipClassName?: string;
asChild?: boolean;
hoverable?: boolean;
}
export const Tooltip = ({ position = 'bottom', ...props }: TooltipProps) => {
export const Tooltip = ({ position = 'bottom', hoverable = true, ...props }: TooltipProps) => {
return (
<TooltipPrimitive.Provider>
<TooltipPrimitive.Provider disableHoverableContent={!hoverable}>
<TooltipPrimitive.Root>
<TooltipPrimitive.Trigger asChild>
{props.asChild ? (

View file

@ -1,8 +1,8 @@
export { cva, cx } from 'class-variance-authority';
export * from './Button';
export * from './CheckBox';
export { ContextMenu, useContextMenu } from './ContextMenu';
export { DropdownMenu, useDropdownMenu } from './DropdownMenu';
export { ContextMenu, useContextMenuContext } from './ContextMenu';
export { DropdownMenu, useDropdownMenuContext } from './DropdownMenu';
export * from './Dialog';
export * as Dropdown from './Dropdown';
export * from './Input';

View file

@ -727,6 +727,9 @@ importers:
react-hook-form:
specifier: ~7.45.2
version: 7.45.2(react@18.2.0)
react-hotkeys-hook:
specifier: ^4.4.1
version: 4.4.1(react-dom@18.2.0)(react@18.2.0)
react-json-view:
specifier: ^1.21.3
version: 1.21.3(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0)
@ -933,14 +936,14 @@ importers:
specifier: ^1.0.3
version: 1.0.3(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-context-menu':
specifier: ^1.0.0
version: 1.0.0(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0)
specifier: ^2.1.4
version: 2.1.4(@types/react-dom@18.2.4)(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-dialog':
specifier: ^1.0.4
version: 1.0.4(@types/react-dom@18.2.4)(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-dropdown-menu':
specifier: ^1.0.0
version: 1.0.0(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0)
specifier: ^2.0.5
version: 2.0.5(@types/react-dom@18.2.4)(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-popover':
specifier: ^1.0.6
version: 1.0.6(@types/react-dom@18.2.4)(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0)
@ -6385,18 +6388,6 @@ packages:
'@babel/runtime': 7.22.11
dev: false
/@radix-ui/react-arrow@1.0.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-1MUuv24HCdepi41+qfv125EwMuxgQ+U+h0A9K3BjCO/J8nVRREKHHpkD9clwfnjEDk9hgGzCnff4aUKCPiRepw==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.22.11
'@radix-ui/react-primitive': 1.0.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-arrow@1.0.1(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==}
peerDependencies:
@ -6449,21 +6440,6 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-collection@1.0.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-8i1pf5dKjnq90Z8udnnXKzdCEV3/FYrfw0n/b6NvB6piXEn3fO1bOh7HBcpG8XrnIXzxlYu2oCcR38QpyLS/mg==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.22.11
'@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
'@radix-ui/react-context': 1.0.0(react@18.2.0)
'@radix-ui/react-primitive': 1.0.0(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-slot': 1.0.0(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-collection@1.0.1(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-uuiFbs+YCKjn3X1DTSx9G7BHApu4GHbi3kgiwsnFUbOKCrwejAJv4eE4Vc8C0Oaxt9T0aV4ox0WCOdx+39Xo+g==}
peerDependencies:
@ -6526,23 +6502,30 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-context-menu@1.0.0(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-JkwOgdXwErwEEpsmgu0Ob8zD3gzWS1brPXnNGPyZEtR6/EYyDgruQYKiihXVsCrPCdrNUHawop9I1+6JTdXPTA==}
/@radix-ui/react-context-menu@2.1.4(@types/react-dom@18.2.4)(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-HVHLUtZOBiR2Fh5l07qQ9y0IgX4dGZF0S9Gwdk4CVA+DL9afSphvFNa4nRiw6RNgb6quwLV4dLPF/gFDvNaOcQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.22.11
'@radix-ui/primitive': 1.0.0
'@radix-ui/react-context': 1.0.0(react@18.2.0)
'@radix-ui/react-menu': 1.0.0(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.0(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.0.0(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.0(react@18.2.0)
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-context': 1.0.1(@types/react@18.0.38)(react@18.2.0)
'@radix-ui/react-menu': 2.0.5(@types/react-dom@18.2.4)(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.0.38)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.0.38)(react@18.2.0)
'@types/react': 18.0.38
'@types/react-dom': 18.2.4
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
dev: false
/@radix-ui/react-context@1.0.0(react@18.2.0):
@ -6641,24 +6624,31 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-dropdown-menu@1.0.0(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-Ptben3TxPWrZLbInO7zjAK73kmjYuStsxfg6ujgt+EywJyREoibhZYnsSNqC+UiOtl4PdW/MOHhxVDtew5fouQ==}
/@radix-ui/react-dropdown-menu@2.0.5(@types/react-dom@18.2.4)(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-xdOrZzOTocqqkCkYo8yRPCib5OkTkqN7lqNCdxwPOdE466DOaNl4N8PkUIlsXthQvW5Wwkd+aEmWpfWlBoDPEw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.22.11
'@radix-ui/primitive': 1.0.0
'@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
'@radix-ui/react-context': 1.0.0(react@18.2.0)
'@radix-ui/react-id': 1.0.0(react@18.2.0)
'@radix-ui/react-menu': 1.0.0(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.0(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.0(react@18.2.0)
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.0.38)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.0.38)(react@18.2.0)
'@radix-ui/react-id': 1.0.1(@types/react@18.0.38)(react@18.2.0)
'@radix-ui/react-menu': 2.0.5(@types/react-dom@18.2.4)(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.0.38)(react@18.2.0)
'@types/react': 18.0.38
'@types/react-dom': 18.2.4
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
dev: false
/@radix-ui/react-focus-guards@1.0.0(react@18.2.0):
@ -6684,20 +6674,6 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-focus-scope@1.0.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-C4SWtsULLGf/2L4oGeIHlvWQx7Rf+7cX/vKOAD2dXW0A1b5QXwi3wWeaEgW+wn+SEVrraMUk05vLU9fZZz5HbQ==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.22.11
'@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
'@radix-ui/react-primitive': 1.0.0(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.0.0(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-focus-scope@1.0.1(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-Ej2MQTit8IWJiS2uuujGUmxXjF/y5xZptIIQnyd2JHLwtV0R2j9NRVoRj/1j/gJ7e3REdaBw4Hjf4a1ImhkZcQ==}
peerDependencies:
@ -6760,35 +6736,42 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-menu@1.0.0(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-icW4C64T6nHh3Z4Q1fxO1RlSShouFF4UpUmPV8FLaJZfphDljannKErDuALDx4ClRLihAPZ9i+PrLNPoWS2DMA==}
/@radix-ui/react-menu@2.0.5(@types/react-dom@18.2.4)(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-Gw4f9pwdH+w5w+49k0gLjN0PfRDHvxmAgG16AbyJZ7zhwZ6PBHKtWohvnSwfusfnK3L68dpBREHpVkj8wEM7ZA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.22.11
'@radix-ui/primitive': 1.0.0
'@radix-ui/react-collection': 1.0.0(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
'@radix-ui/react-context': 1.0.0(react@18.2.0)
'@radix-ui/react-direction': 1.0.0(react@18.2.0)
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.0.38)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.0.38)(react@18.2.0)
'@radix-ui/react-direction': 1.0.1(@types/react@18.0.38)(react@18.2.0)
'@radix-ui/react-dismissable-layer': 1.0.2(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-focus-guards': 1.0.0(react@18.2.0)
'@radix-ui/react-focus-scope': 1.0.0(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-id': 1.0.0(react@18.2.0)
'@radix-ui/react-popper': 1.0.0(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-portal': 1.0.0(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-presence': 1.0.0(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.0(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-roving-focus': 1.0.0(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-slot': 1.0.0(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.0.0(react@18.2.0)
'@radix-ui/react-focus-guards': 1.0.1(@types/react@18.0.38)(react@18.2.0)
'@radix-ui/react-focus-scope': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-id': 1.0.1(@types/react@18.0.38)(react@18.2.0)
'@radix-ui/react-popper': 1.1.2(@types/react-dom@18.2.4)(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-portal': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.4)(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.4)(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-slot': 1.0.2(@types/react@18.0.38)(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.0.38)(react@18.2.0)
'@types/react': 18.0.38
'@types/react-dom': 18.2.4
aria-hidden: 1.2.3
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-remove-scroll: 2.5.4(@types/react@18.0.38)(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
react-remove-scroll: 2.5.5(@types/react@18.0.38)(react@18.2.0)
dev: false
/@radix-ui/react-popover@1.0.6(@types/react-dom@18.2.4)(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0):
@ -6826,28 +6809,6 @@ packages:
react-remove-scroll: 2.5.5(@types/react@18.0.38)(react@18.2.0)
dev: false
/@radix-ui/react-popper@1.0.0(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-k2dDd+1Wl0XWAMs9ZvAxxYsB9sOsEhrFQV4CINd7IUZf0wfdye4OHen9siwxvZImbzhgVeKTJi68OQmPRvVdMg==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.22.11
'@floating-ui/react-dom': 0.7.2(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-arrow': 1.0.0(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
'@radix-ui/react-context': 1.0.0(react@18.2.0)
'@radix-ui/react-primitive': 1.0.0(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-layout-effect': 1.0.0(react@18.2.0)
'@radix-ui/react-use-rect': 1.0.0(react@18.2.0)
'@radix-ui/react-use-size': 1.0.0(react@18.2.0)
'@radix-ui/rect': 1.0.0
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
dev: false
/@radix-ui/react-popper@1.0.1(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-J4Vj7k3k+EHNWgcKrE+BLlQfpewxA7Zd76h5I0bIa+/EqaIZ3DuwrbPj49O3wqN+STnXsBuxiHLiF0iU3yfovw==}
peerDependencies:
@ -6900,18 +6861,6 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-portal@1.0.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-a8qyFO/Xb99d8wQdu4o7qnigNjTPG123uADNecz0eX4usnQEj7o+cG4ZX4zkqq98NYekT7UoEQIjxBNWIFuqTA==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.22.11
'@radix-ui/react-primitive': 1.0.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-portal@1.0.1(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-NY2vUWI5WENgAT1nfC6JS7RU5xRYBfjZVLq0HmgEN1Ezy3rk/UruMV4+Rd0F40PEaFC5SrLS1ixYvcYIQrb4Ig==}
peerDependencies:
@ -6980,18 +6929,6 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-primitive@1.0.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.22.11
'@radix-ui/react-slot': 1.0.0(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-primitive@1.0.1(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA==}
peerDependencies:
@ -7071,26 +7008,6 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-roving-focus@1.0.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-lHvO4MhvoWpeNbiJAoyDsEtbKqP2jkkdwsMVJ3kfqbkC71J/aXE6Th6gkZA1xHEqSku+t+UgoDjvE7Z3gsBpcg==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.22.11
'@radix-ui/primitive': 1.0.0
'@radix-ui/react-collection': 1.0.0(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
'@radix-ui/react-context': 1.0.0(react@18.2.0)
'@radix-ui/react-direction': 1.0.0(react@18.2.0)
'@radix-ui/react-id': 1.0.0(react@18.2.0)
'@radix-ui/react-primitive': 1.0.0(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.0.0(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.0(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-roving-focus@1.0.1(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-TB76u5TIxKpqMpUAuYH2VqMhHYKa+4Vs1NHygo/llLvlffN6mLVsFhz0AnSFlSBAvTBYVHYAkHAyEt7x1gPJOA==}
peerDependencies:
@ -7257,16 +7174,6 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-slot@1.0.0(react@18.2.0):
resolution: {integrity: sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.22.11
'@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
react: 18.2.0
dev: false
/@radix-ui/react-slot@1.0.1(react@18.2.0):
resolution: {integrity: sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==}
peerDependencies:
@ -12750,12 +12657,12 @@ packages:
peerDependencies:
webpack: ^5.0.0
dependencies:
icss-utils: 5.1.0(postcss@8.4.23)
postcss: 8.4.23
postcss-modules-extract-imports: 3.0.0(postcss@8.4.23)
postcss-modules-local-by-default: 4.0.3(postcss@8.4.23)
postcss-modules-scope: 3.0.0(postcss@8.4.23)
postcss-modules-values: 4.0.0(postcss@8.4.23)
icss-utils: 5.1.0(postcss@8.4.28)
postcss: 8.4.28
postcss-modules-extract-imports: 3.0.0(postcss@8.4.28)
postcss-modules-local-by-default: 4.0.3(postcss@8.4.28)
postcss-modules-scope: 3.0.0(postcss@8.4.28)
postcss-modules-values: 4.0.0(postcss@8.4.28)
postcss-value-parser: 4.2.0
semver: 7.5.4
webpack: 5.88.2(esbuild@0.17.19)
@ -15789,13 +15696,13 @@ packages:
safer-buffer: 2.1.2
optional: true
/icss-utils@5.1.0(postcss@8.4.23):
/icss-utils@5.1.0(postcss@8.4.28):
resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==}
engines: {node: ^10 || ^12 || >= 14}
peerDependencies:
postcss: ^8.1.0
dependencies:
postcss: 8.4.23
postcss: 8.4.28
dev: false
/ieee754@1.2.1:
@ -19487,45 +19394,45 @@ packages:
webpack: 5.88.2(esbuild@0.17.19)
dev: false
/postcss-modules-extract-imports@3.0.0(postcss@8.4.23):
/postcss-modules-extract-imports@3.0.0(postcss@8.4.28):
resolution: {integrity: sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==}
engines: {node: ^10 || ^12 || >= 14}
peerDependencies:
postcss: ^8.1.0
dependencies:
postcss: 8.4.23
postcss: 8.4.28
dev: false
/postcss-modules-local-by-default@4.0.3(postcss@8.4.23):
/postcss-modules-local-by-default@4.0.3(postcss@8.4.28):
resolution: {integrity: sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==}
engines: {node: ^10 || ^12 || >= 14}
peerDependencies:
postcss: ^8.1.0
dependencies:
icss-utils: 5.1.0(postcss@8.4.23)
postcss: 8.4.23
icss-utils: 5.1.0(postcss@8.4.28)
postcss: 8.4.28
postcss-selector-parser: 6.0.13
postcss-value-parser: 4.2.0
dev: false
/postcss-modules-scope@3.0.0(postcss@8.4.23):
/postcss-modules-scope@3.0.0(postcss@8.4.28):
resolution: {integrity: sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==}
engines: {node: ^10 || ^12 || >= 14}
peerDependencies:
postcss: ^8.1.0
dependencies:
postcss: 8.4.23
postcss: 8.4.28
postcss-selector-parser: 6.0.13
dev: false
/postcss-modules-values@4.0.0(postcss@8.4.23):
/postcss-modules-values@4.0.0(postcss@8.4.28):
resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==}
engines: {node: ^10 || ^12 || >= 14}
peerDependencies:
postcss: ^8.1.0
dependencies:
icss-utils: 5.1.0(postcss@8.4.23)
postcss: 8.4.23
icss-utils: 5.1.0(postcss@8.4.28)
postcss: 8.4.28
dev: false
/postcss-nested@6.0.1(postcss@8.4.23):
@ -20146,6 +20053,16 @@ packages:
react: 18.2.0
dev: false
/react-hotkeys-hook@4.4.1(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-sClBMBioFEgFGYLTWWRKvhxcCx1DRznd+wkFHwQZspnRBkHTgruKIHptlK/U/2DPX8BhHoRGzpMVWUXMmdZlmw==}
peerDependencies:
react: '>=16.8.1'
react-dom: '>=16.8.1'
dependencies:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/react-inspector@6.0.2(react@18.2.0):
resolution: {integrity: sha512-x+b7LxhmHXjHoU/VrFAzw5iutsILRoYyDq97EDYdFpPLcvqtEzk4ZSZSQjnFPbr5T57tLXnHcqFYoN1pI6u8uQ==}
peerDependencies:
@ -20438,25 +20355,6 @@ packages:
tslib: 2.6.2
dev: false
/react-remove-scroll@2.5.4(@types/react@18.0.38)(react@18.2.0):
resolution: {integrity: sha512-xGVKJJr0SJGQVirVFAUZ2k1QLyO6m+2fy0l8Qawbp5Jgrv3DeLalrfMNBFSlmz5kriGGzsVBtGVnf4pTKIhhWA==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.0.38
react: 18.2.0
react-remove-scroll-bar: 2.3.4(@types/react@18.0.38)(react@18.2.0)
react-style-singleton: 2.2.1(@types/react@18.0.38)(react@18.2.0)
tslib: 2.6.2
use-callback-ref: 1.3.0(@types/react@18.0.38)(react@18.2.0)
use-sidecar: 1.1.2(@types/react@18.0.38)(react@18.2.0)
dev: false
/react-remove-scroll@2.5.5(@types/react@18.0.38)(react@18.2.0):
resolution: {integrity: sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==}
engines: {node: '>=10'}
@ -21030,7 +20928,7 @@ packages:
adjust-sourcemap-loader: 4.0.0
convert-source-map: 1.9.0
loader-utils: 2.0.4
postcss: 8.4.23
postcss: 8.4.28
source-map: 0.6.1
dev: false
@ -23663,6 +23561,7 @@ packages:
rollup: 3.28.1
optionalDependencies:
fsevents: 2.3.3
dev: true
/vite@4.3.9(less@4.2.0):
resolution: {integrity: sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==}