new context menu

This commit is contained in:
Brendan Allan 2022-09-05 10:54:54 +08:00
parent 92ba328b18
commit cedbe86381
5 changed files with 651 additions and 843 deletions

View file

@ -1,182 +1,122 @@
import { explorerStore, useLibraryMutation, useLibraryQuery } from '@sd/client';
import { ExplorerData } from '@sd/core';
import {
ArrowBendUpRight,
LockSimple,
Package,
Plus,
Share,
TagSimple,
Trash,
TrashSimple
ArrowBendUpRight,
FilePlus,
FileX,
LockSimple,
Package,
Plus,
Share,
TagSimple,
Trash,
TrashSimple,
} from 'phosphor-react';
import React from 'react';
import { useSnapshot } from 'valtio';
import { WithContextMenu } from '../layout/MenuOverlay';
import { NewContextMenu as CM } from "@sd/ui"
const AssignTagMenuItems = (props: { objectId: number }) => {
const tags = useLibraryQuery(['tags.getAll'], { suspense: true });
const tagsForFile = useLibraryQuery(['tags.getForFile', props.objectId], { suspense: true });
const { mutate: assignTag } = useLibraryMutation('tags.assign');
return (
<>
{tags.data?.map(tag => {
const active = !!tagsForFile.data?.find(t => t.id === tag.id)
return <CM.Item
key={tag.id}
onClick={(e) => {
e.preventDefault();
if (props.objectId === null) return;
assignTag({
tag_id: tag.id,
file_id: props.objectId,
unassign: active
});
}}
>
<div
className="block w-[15px] h-[15px] mr-0.5 border rounded-full"
style={{
backgroundColor: active
? tag.color || '#efefef'
: 'transparent' || '#efefef',
borderColor: tag.color || '#efefef'
}}
/>
<p>{tag.name}</p>
</CM.Item>
})}
</>
);
}
interface Props {
children: React.ReactNode;
children: React.ReactNode;
}
export default function ExplorerContextMenu(props: Props) {
const store = useSnapshot(explorerStore);
const store = useSnapshot(explorerStore);
const { data: tags } = useLibraryQuery(['tags.getAll'], {});
return (
<div className="relative" >
<CM.ContextMenu
trigger={props.children}
>
<CM.Item label="Open" />
<CM.Item label="Open with..." />
const { mutate: assignTag } = useLibraryMutation('tags.assign');
<CM.Separator />
const { data: tagsForFile } = useLibraryQuery([
'tags.getForFile',
store.contextMenuObjectId || -1
]);
return (
<div className="relative">
<WithContextMenu
menu={[
[
// `file-${props.identifier}`,
{
label: 'Open'
},
{
label: 'Open with...'
}
],
[
{
label: 'Quick view'
},
{
label: 'Open in Finder'
}
],
[
{
label: 'Rename'
},
{
label: 'Duplicate'
}
],
[
{
label: 'Share',
icon: Share,
onClick(e) {
e.preventDefault();
navigator.share?.({
title: 'Spacedrive',
text: 'Check out this cool app',
url: 'https://spacedrive.com'
});
}
}
],
[
{
label: 'Assign tag',
icon: TagSimple,
children: [
tags?.map((tag) => {
const active = !!tagsForFile?.find((t) => t.id === tag.id);
return {
label: tag.name || '',
<CM.Item label="Quick view" />
<CM.Item label="Open in Finder" />
// leftItem: <Checkbox checked={!!tagsForFile?.find((t) => t.id === tag.id)} />,
leftItem: (
<div className="relative">
<div
className="block w-[15px] h-[15px] mr-0.5 border rounded-full"
style={{
backgroundColor: active
? tag.color || '#efefef'
: 'transparent' || '#efefef',
borderColor: tag.color || '#efefef'
}}
/>
</div>
),
onClick(e) {
e.preventDefault();
if (store.contextMenuObjectId != null)
assignTag({
tag_id: tag.id,
file_id: store.contextMenuObjectId,
unassign: active
});
}
};
}) || []
]
}
],
[
{
label: 'More actions...',
icon: Plus,
<CM.Separator />
children: [
// [
// {
// label: 'Move to library',
// icon: FilePlus,
// children: [libraries?.map((library) => ({ label: library.config.name })) || []]
// },
// {
// label: 'Remove from library',
// icon: FileX
// }
// ],
[
{
label: 'Encrypt',
icon: LockSimple
},
{
label: 'Compress',
icon: Package
},
{
label: 'Convert to',
icon: ArrowBendUpRight,
<CM.Item label="Rename" />
<CM.Item label="Duplicate" />
children: [
[
{
label: 'PNG'
},
{
label: 'WebP'
}
]
]
}
// {
// label: 'Mint NFT',
// icon: TrashIcon
// }
],
[
{
label: 'Secure delete',
icon: TrashSimple
}
]
]
}
],
[
{
label: 'Delete',
icon: Trash,
danger: true
}
]
]}
>
{props.children}
</WithContextMenu>
</div>
);
<CM.Separator />
<CM.Item label="Share" icon={Share} onClick={e => {
e.preventDefault();
navigator.share?.({
title: 'Spacedrive',
text: 'Check out this cool app',
url: 'https://spacedrive.com'
});
}} />
<CM.Separator />
{store.contextMenuObjectId && <CM.SubMenu label="Assign tag" icon={TagSimple}>
<AssignTagMenuItems objectId={store.contextMenuObjectId} />
</CM.SubMenu>}
<CM.SubMenu label="More actions..." icon={Plus}>
<CM.SubMenu label="Move to library" icon={FilePlus}>
{/* {libraries.map(library => <CM.Item key={library.id} label={library.config.name} />)} */}
<CM.Item label="Remove from library" icon={FileX} />
</CM.SubMenu>
<CM.Separator />
<CM.Item label="Encrypt" icon={LockSimple} />
<CM.Item label="Compress" icon={Package} />
<CM.SubMenu label="Convert to" icon={ArrowBendUpRight}>
<CM.Item label="PNG" />
<CM.Item label="WebP" />
</CM.SubMenu>
<CM.Item label="Secure delete" icon={TrashSimple} />
</CM.SubMenu>
<CM.Separator />
<CM.Item icon={Trash} label="Delete" variant="danger" />
</CM.ContextMenu>
</div >
);
}

View file

@ -21,11 +21,13 @@
"@heroicons/react": "^2.0.10",
"@radix-ui/react-context-menu": "^1.0.0",
"@tailwindcss/forms": "^0.5.2",
"class-variance-authority": "^0.2.3",
"clsx": "^1.2.1",
"phosphor-react": "^1.4.1",
"postcss": "^8.4.14",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"storybook": "^6.5.10",
"tailwindcss": "^3.1.6"
},
"devDependencies": {

View file

@ -0,0 +1,107 @@
import * as RadixCM from "@radix-ui/react-context-menu"
import clsx from "clsx";
import { CaretRight, Icon } from "phosphor-react";
import { HTMLAttributes, PropsWithChildren, Suspense, } from "react"
import React from 'react'
import { cva, VariantProps } from "class-variance-authority";
interface Props extends RadixCM.MenuContentProps {
trigger: React.ReactNode,
}
const MENU_CLASSES = `
flex flex-col
min-w-[11rem] p-2 space-y-1
text-left text-sm dark:text-gray-100 text-gray-800
bg-gray-50 border-gray-200 dark:bg-gray-950
shadow-md shadow-gray-300 dark:shadow-gray-750
select-none cursor-default rounded-lg
`;
export const ContextMenu = ({ trigger, children, className, ...props }: PropsWithChildren<Props>) => {
return (
<RadixCM.Root>
<RadixCM.Trigger>{trigger}</RadixCM.Trigger>
<RadixCM.Portal>
<RadixCM.Content {...props} className={clsx(MENU_CLASSES, className)}>
{children}
</RadixCM.Content>
</RadixCM.Portal>
</RadixCM.Root>
)
}
export const Separator = () =>
<RadixCM.Separator className="mx-2 border-0 border-b pointer-events-none border-b-gray-300 dark:border-b-gray-600" />
export const SubMenu = ({ label, icon, className, ...props }: RadixCM.MenuSubContentProps & ItemProps) => {
return (
<RadixCM.Sub>
<RadixCM.SubTrigger className="[&[data-state='open']_div]:bg-primary focus:outline-none">
<DivItem rightArrow {...{ label, icon }} />
</RadixCM.SubTrigger>
<RadixCM.Portal>
<Suspense fallback={null}>
<RadixCM.SubContent {...props} className={clsx(MENU_CLASSES, "-mt-2", className)} />
</Suspense>
</RadixCM.Portal>
</RadixCM.Sub>
)
}
const ITEM_CLASSES = `
flex flex-row items-center justify-start flex-1
px-2 py-1 space-x-2
cursor-default rounded
focus:outline-none
`;
const itemStyles = cva([ITEM_CLASSES], {
variants: {
variant: {
default: 'hover:bg-primary focus:bg-primary',
danger: `
text-red-600 dark:text-red-400
hover:text-white focus:text-white
hover:bg-red-500 focus:bg-red-500
`
}
},
defaultVariants: {
variant: 'default'
}
})
interface ItemProps extends VariantProps<typeof itemStyles> {
icon?: Icon,
rightArrow?: boolean,
label?: string,
}
export const Item = ({ icon, label, rightArrow, children, variant, ...props }: ItemProps & RadixCM.MenuItemProps) => (
<RadixCM.Item {...props} className={itemStyles({ variant })}>
{children ? children : <ItemInternals {...{icon, label, rightArrow}} />}
</RadixCM.Item>
)
const DivItem = ({ variant, ...props }: ItemProps) => (
<div className={itemStyles({ variant })}>
<ItemInternals {...props} />
</div>
)
const ItemInternals = ({ icon, label, rightArrow }: ItemProps) => {
const ItemIcon = icon;
return (
<>
{ItemIcon && <ItemIcon size={18} />}
{label && <p>{label}</p>}
{rightArrow && <>
<div className="flex-1"/>
<CaretRight weight="fill" size={12} alt=""/>
</>}
</>
)
}

View file

@ -1,4 +1,5 @@
export * from './Button';
export * from './Dropdown';
export * from './ContextMenu';
export * as NewContextMenu from "./ContextMenu/index"
export * from './Input';

File diff suppressed because it is too large Load diff