mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-13 10:44:08 +00:00
[Desktop, UI] Fix library dropdown overflow & navigation (#619)
* add radix dropdown menu
* [desktop] Improve Libraries Dropdown
* rename alignToParent prop and fix sizing
* Update pnpm-lock.yaml
* Revert "Update pnpm-lock.yaml"
This reverts commit 6113361c51
.
* fix pnpm lock
This commit is contained in:
parent
0fd53d1287
commit
29f0dfb338
|
@ -1,22 +1,19 @@
|
|||
import clsx from 'clsx';
|
||||
import { Gear, Lock, Plus } from 'phosphor-react';
|
||||
import { useClientContext } from '@sd/client';
|
||||
import { Dropdown, dialogManager } from '@sd/ui';
|
||||
import { Dropdown, DropdownMenu, dialogManager } from '@sd/ui';
|
||||
import CreateDialog from '../../settings/node/libraries/CreateDialog';
|
||||
|
||||
export default () => {
|
||||
const { library, libraries, currentLibraryId } = useClientContext();
|
||||
|
||||
return (
|
||||
<Dropdown.Root
|
||||
// we override the sidebar dropdown item's hover styles
|
||||
// because the dark style clashes with the sidebar
|
||||
itemsClassName="dark:bg-sidebar-box dark:border-sidebar-line mt-1 dark:divide-menu-selected/30 shadow-none"
|
||||
button={
|
||||
<DropdownMenu.Root
|
||||
trigger={
|
||||
<Dropdown.Button
|
||||
variant="gray"
|
||||
className={clsx(
|
||||
`text-ink w-full `,
|
||||
`text-ink w-full`,
|
||||
// these classname overrides are messy
|
||||
// but they work
|
||||
`!bg-sidebar-box !border-sidebar-line/50 active:!border-sidebar-line active:!bg-sidebar-button ui-open:!bg-sidebar-button ui-open:!border-sidebar-line ring-offset-sidebar`,
|
||||
|
@ -28,32 +25,43 @@ export default () => {
|
|||
</span>
|
||||
</Dropdown.Button>
|
||||
}
|
||||
// we override the sidebar dropdown item's hover styles
|
||||
// because the dark style clashes with the sidebar
|
||||
className="dark:bg-sidebar-box dark:border-sidebar-line dark:divide-menu-selected/30 mt-1 shadow-none"
|
||||
alignToTrigger
|
||||
animate
|
||||
>
|
||||
<Dropdown.Section>
|
||||
{libraries.data?.map((lib) => (
|
||||
<Dropdown.Item
|
||||
to={`/${lib.uuid}/overview`}
|
||||
key={lib.uuid}
|
||||
selected={lib.uuid === currentLibraryId}
|
||||
>
|
||||
{lib.config.name}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown.Section>
|
||||
<Dropdown.Section>
|
||||
<Dropdown.Item
|
||||
icon={Plus}
|
||||
onClick={() => dialogManager.create((dp) => <CreateDialog {...dp} />)}
|
||||
{libraries.data?.map((lib) => (
|
||||
<DropdownMenu.Item
|
||||
to={`/${lib.uuid}/overview`}
|
||||
key={lib.uuid}
|
||||
selected={lib.uuid === currentLibraryId}
|
||||
>
|
||||
New Library
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item icon={Gear} to="settings/library">
|
||||
Manage Library
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item icon={Lock} onClick={() => alert('TODO: Not implemented yet!')}>
|
||||
Lock
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Section>
|
||||
</Dropdown.Root>
|
||||
{lib.config.name}
|
||||
</DropdownMenu.Item>
|
||||
))}
|
||||
<DropdownMenu.Separator className="mx-0 my-0.5" />
|
||||
<DropdownMenu.Item
|
||||
label=" New Library"
|
||||
icon={Plus}
|
||||
iconProps={{ weight: 'bold', size: 16 }}
|
||||
onClick={() => dialogManager.create((dp) => <CreateDialog {...dp} />)}
|
||||
className="font-medium"
|
||||
/>
|
||||
<DropdownMenu.Item
|
||||
label="Manage Library"
|
||||
icon={Gear}
|
||||
iconProps={{ weight: 'bold', size: 16 }}
|
||||
to="settings/library/general"
|
||||
className="font-medium"
|
||||
/>
|
||||
<DropdownMenu.Item
|
||||
label="Lock"
|
||||
icon={Lock}
|
||||
iconProps={{ weight: 'bold', size: 16 }}
|
||||
onClick={() => alert('TODO: Not implemented yet!')}
|
||||
className="font-medium"
|
||||
/>
|
||||
</DropdownMenu.Root>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -72,6 +72,7 @@
|
|||
"storybook-tailwind-dark-mode": "^1.0.15",
|
||||
"style-loader": "^3.3.1",
|
||||
"tailwindcss": "^3.1.8",
|
||||
"tailwindcss-animate": "^1.0.5",
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +1,28 @@
|
|||
import * as RadixCM from '@radix-ui/react-context-menu';
|
||||
import { VariantProps, cva } from 'class-variance-authority';
|
||||
import clsx from 'clsx';
|
||||
import { CaretRight, Icon } from 'phosphor-react';
|
||||
import { CaretRight, Icon, IconProps } from 'phosphor-react';
|
||||
import { PropsWithChildren, Suspense } from 'react';
|
||||
|
||||
interface Props extends RadixCM.MenuContentProps {
|
||||
interface ContextMenuProps extends RadixCM.MenuContentProps {
|
||||
trigger: React.ReactNode;
|
||||
}
|
||||
|
||||
const MENU_CLASSES = `
|
||||
flex flex-col z-50
|
||||
min-w-[8rem] px-1 py-0.5 my-2
|
||||
text-left text-sm text-menu-ink
|
||||
bg-menu cool-shadow
|
||||
border border-menu-line
|
||||
select-none cursor-default rounded-md
|
||||
`;
|
||||
export const contextMenuClasses = clsx(
|
||||
'z-50 flex flex-col',
|
||||
'my-2 min-w-[8rem] px-1 py-0.5',
|
||||
'text-menu-ink text-left text-sm',
|
||||
'bg-menu cool-shadow',
|
||||
'border-menu-line border',
|
||||
'cursor-default select-none rounded-md'
|
||||
);
|
||||
|
||||
export const Root = ({ trigger, children, className, ...props }: PropsWithChildren<Props>) => {
|
||||
const Root = ({ trigger, children, className, ...props }: PropsWithChildren<ContextMenuProps>) => {
|
||||
return (
|
||||
<RadixCM.Root>
|
||||
<RadixCM.Trigger asChild>{trigger}</RadixCM.Trigger>
|
||||
<RadixCM.Portal>
|
||||
<RadixCM.Content {...props} className={clsx(MENU_CLASSES, className)}>
|
||||
<RadixCM.Content {...props} className={clsx(contextMenuClasses, className)}>
|
||||
{children}
|
||||
</RadixCM.Content>
|
||||
</RadixCM.Portal>
|
||||
|
@ -30,33 +30,39 @@ export const Root = ({ trigger, children, className, ...props }: PropsWithChildr
|
|||
);
|
||||
};
|
||||
|
||||
export const Separator = () => (
|
||||
<RadixCM.Separator className="border-b-menu-line pointer-events-none mx-2 border-0 border-b" />
|
||||
export const contextMenuSeparatorClassNames =
|
||||
'border-b-menu-line pointer-events-none mx-2 my-1 border-0 border-b';
|
||||
|
||||
const Separator = (props: { className?: string }) => (
|
||||
<RadixCM.Separator className={clsx(contextMenuSeparatorClassNames, props.className)} />
|
||||
);
|
||||
|
||||
export const SubMenu = ({
|
||||
export const contextSubMenuTriggerClassNames =
|
||||
"[&[data-state='open']_div]:bg-accent text-menu-ink py-[3px] focus:outline-none [&[data-state='open']_div]:text-white";
|
||||
|
||||
const SubMenu = ({
|
||||
label,
|
||||
icon,
|
||||
className,
|
||||
...props
|
||||
}: RadixCM.MenuSubContentProps & ItemProps) => {
|
||||
}: RadixCM.MenuSubContentProps & ContextMenuItemProps) => {
|
||||
return (
|
||||
<RadixCM.Sub>
|
||||
<RadixCM.SubTrigger className="[&[data-state='open']_div]:bg-accent text-menu-ink py-[3px] focus:outline-none [&[data-state='open']_div]:text-white">
|
||||
<RadixCM.SubTrigger className={contextSubMenuTriggerClassNames}>
|
||||
<DivItem rightArrow {...{ label, icon }} />
|
||||
</RadixCM.SubTrigger>
|
||||
<RadixCM.Portal>
|
||||
<Suspense fallback={null}>
|
||||
<RadixCM.SubContent {...props} className={clsx(MENU_CLASSES, '-mt-2', className)} />
|
||||
<RadixCM.SubContent {...props} className={clsx(contextMenuClasses, '-mt-2', className)} />
|
||||
</Suspense>
|
||||
</RadixCM.Portal>
|
||||
</RadixCM.Sub>
|
||||
);
|
||||
};
|
||||
|
||||
const itemStyles = cva(
|
||||
export const contextMenuItemStyles = cva(
|
||||
[
|
||||
'flex flex-1 flex-row items-center justify-start',
|
||||
'flex flex-1 flex-row items-center justify-start overflow-hidden',
|
||||
'space-x-2 px-2 py-[3px]',
|
||||
'cursor-default rounded',
|
||||
'focus:outline-none'
|
||||
|
@ -78,46 +84,49 @@ const itemStyles = cva(
|
|||
}
|
||||
);
|
||||
|
||||
interface ItemProps extends VariantProps<typeof itemStyles> {
|
||||
export interface ContextMenuItemProps extends VariantProps<typeof contextMenuItemStyles> {
|
||||
icon?: Icon;
|
||||
iconProps?: IconProps;
|
||||
rightArrow?: boolean;
|
||||
label?: string;
|
||||
keybind?: string;
|
||||
}
|
||||
|
||||
export const Item = ({
|
||||
const Item = ({
|
||||
icon,
|
||||
label,
|
||||
rightArrow,
|
||||
children,
|
||||
keybind,
|
||||
variant,
|
||||
|
||||
...props
|
||||
}: ItemProps & RadixCM.MenuItemProps) => {
|
||||
}: ContextMenuItemProps & RadixCM.MenuItemProps) => {
|
||||
return (
|
||||
<RadixCM.Item
|
||||
{...props}
|
||||
className="text-menu-ink group !cursor-default select-none py-0.5 focus:outline-none active:opacity-80"
|
||||
>
|
||||
<div className={itemStyles({ variant })}>
|
||||
<RadixCM.Item {...props} className="">
|
||||
<div className={contextMenuItemStyles({ variant })}>
|
||||
{children ? children : <ItemInternals {...{ icon, label, rightArrow, keybind }} />}
|
||||
</div>
|
||||
</RadixCM.Item>
|
||||
);
|
||||
};
|
||||
|
||||
const DivItem = ({ variant, ...props }: ItemProps) => (
|
||||
<div className={itemStyles({ variant })}>
|
||||
const DivItem = ({ variant, ...props }: ContextMenuItemProps) => (
|
||||
<div className={contextMenuItemStyles({ variant })}>
|
||||
<ItemInternals {...props} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const ItemInternals = ({ icon, label, rightArrow, keybind }: ItemProps) => {
|
||||
export const ItemInternals = ({
|
||||
icon,
|
||||
label,
|
||||
rightArrow,
|
||||
keybind,
|
||||
iconProps
|
||||
}: ContextMenuItemProps) => {
|
||||
const ItemIcon = icon;
|
||||
return (
|
||||
<>
|
||||
{ItemIcon && <ItemIcon size={18} />}
|
||||
{ItemIcon && <ItemIcon size={18} {...iconProps} />}
|
||||
{label && <p>{label}</p>}
|
||||
|
||||
{keybind && (
|
||||
|
@ -134,3 +143,10 @@ const ItemInternals = ({ icon, label, rightArrow, keybind }: ItemProps) => {
|
|||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ContextMenu = {
|
||||
Root,
|
||||
Item,
|
||||
Separator,
|
||||
SubMenu
|
||||
};
|
||||
|
|
|
@ -2,7 +2,7 @@ import { ReactComponent as CaretDown } from '@sd/assets/svgs/caret.svg';
|
|||
import { Menu, Transition } from '@headlessui/react';
|
||||
import { VariantProps, cva } from 'class-variance-authority';
|
||||
import clsx from 'clsx';
|
||||
import { Fragment, PropsWithChildren } from 'react';
|
||||
import { Fragment, PropsWithChildren, forwardRef } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import * as UI from '.';
|
||||
import { tw } from './utils';
|
||||
|
@ -60,18 +60,20 @@ export const Item = ({ to, className, icon: Icon, children, ...props }: Dropdown
|
|||
);
|
||||
};
|
||||
|
||||
export const Button = ({ children, className, ...props }: UI.ButtonProps) => {
|
||||
return (
|
||||
<UI.Button size="sm" {...props} className={clsx('flex text-left', className)}>
|
||||
{children}
|
||||
<span className="grow" />
|
||||
<CaretDown
|
||||
className="text-ink-dull ui-open:rotate-180 ui-open:translate-y-[-1px] w-[12px] translate-y-[1px] transition-transform"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</UI.Button>
|
||||
);
|
||||
};
|
||||
export const Button = forwardRef<HTMLButtonElement, UI.ButtonProps>(
|
||||
({ children, className, ...props }, ref) => {
|
||||
return (
|
||||
<UI.Button size="sm" ref={ref} className={clsx('group flex text-left', className)} {...props}>
|
||||
{children}
|
||||
<span className="grow" />
|
||||
<CaretDown
|
||||
className="text-ink-dull group-radix-state-open:rotate-180 group-radix-state-open:translate-y-[-1px] ui-open:rotate-180 ui-open:translate-y-[-1px] ml-2 w-[12px] shrink-0 translate-y-[1px] transition-transform"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</UI.Button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export interface DropdownRootProps {
|
||||
button: React.ReactNode;
|
||||
|
|
162
packages/ui/src/DropdownMenu.tsx
Normal file
162
packages/ui/src/DropdownMenu.tsx
Normal file
|
@ -0,0 +1,162 @@
|
|||
import * as RadixDM from '@radix-ui/react-dropdown-menu';
|
||||
import clsx from 'clsx';
|
||||
import React, { PropsWithChildren, Suspense, useCallback, useRef, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
ContextMenuItemProps,
|
||||
ItemInternals,
|
||||
contextMenuClasses,
|
||||
contextMenuItemStyles,
|
||||
contextMenuSeparatorClassNames,
|
||||
contextSubMenuTriggerClassNames
|
||||
} from './ContextMenu';
|
||||
|
||||
interface DropdownMenuProps
|
||||
extends RadixDM.MenuContentProps,
|
||||
Pick<RadixDM.DropdownMenuProps, 'onOpenChange'> {
|
||||
trigger: React.ReactNode;
|
||||
triggerClassName?: string;
|
||||
alignToTrigger?: boolean;
|
||||
animate?: boolean;
|
||||
}
|
||||
|
||||
const Root = ({
|
||||
trigger,
|
||||
children,
|
||||
className,
|
||||
asChild = true,
|
||||
triggerClassName,
|
||||
alignToTrigger,
|
||||
onOpenChange,
|
||||
animate,
|
||||
...props
|
||||
}: PropsWithChildren<DropdownMenuProps>) => {
|
||||
const [width, setWidth] = useState<number>();
|
||||
|
||||
const measureRef = useCallback((ref: HTMLButtonElement | null) => {
|
||||
alignToTrigger && ref && setWidth(ref.getBoundingClientRect().width);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<RadixDM.Root modal={false} onOpenChange={onOpenChange}>
|
||||
<RadixDM.Trigger ref={measureRef} asChild={asChild} className={triggerClassName}>
|
||||
{trigger}
|
||||
</RadixDM.Trigger>
|
||||
<RadixDM.Portal>
|
||||
<div>
|
||||
<div className="fixed inset-0"></div>
|
||||
<RadixDM.Content
|
||||
className={clsx(
|
||||
contextMenuClasses,
|
||||
animate && 'animate-in fade-in data-[side=bottom]:slide-in-from-top-2',
|
||||
'w-44',
|
||||
className
|
||||
)}
|
||||
align="start"
|
||||
collisionPadding={5}
|
||||
style={{ width }}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</RadixDM.Content>
|
||||
</div>
|
||||
</RadixDM.Portal>
|
||||
</RadixDM.Root>
|
||||
);
|
||||
};
|
||||
|
||||
const Separator = (props: { className?: string }) => (
|
||||
<RadixDM.Separator className={clsx(contextMenuSeparatorClassNames, props.className)} />
|
||||
);
|
||||
|
||||
const SubMenu = ({
|
||||
label,
|
||||
icon,
|
||||
className,
|
||||
...props
|
||||
}: RadixDM.MenuSubContentProps & ContextMenuItemProps) => {
|
||||
return (
|
||||
<RadixDM.Sub>
|
||||
<RadixDM.SubTrigger className={contextSubMenuTriggerClassNames}>
|
||||
<div
|
||||
className={contextMenuItemStyles({
|
||||
class: 'group-radix-state-open:bg-trinary/50 group-radix-state-open:text-primary'
|
||||
})}
|
||||
>
|
||||
<ItemInternals rightArrow {...{ label, icon }} />
|
||||
</div>
|
||||
</RadixDM.SubTrigger>
|
||||
<RadixDM.Portal>
|
||||
<Suspense fallback={null}>
|
||||
<RadixDM.SubContent
|
||||
className={clsx(contextMenuClasses, className)}
|
||||
collisionPadding={5}
|
||||
{...props}
|
||||
/>
|
||||
</Suspense>
|
||||
</RadixDM.Portal>
|
||||
</RadixDM.Sub>
|
||||
);
|
||||
};
|
||||
|
||||
interface DropdownItemProps extends ContextMenuItemProps, RadixDM.MenuItemProps {
|
||||
to?: string;
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
const Item = ({
|
||||
icon,
|
||||
iconProps,
|
||||
label,
|
||||
rightArrow,
|
||||
children,
|
||||
keybind,
|
||||
variant,
|
||||
className,
|
||||
selected,
|
||||
to,
|
||||
...props
|
||||
}: DropdownItemProps) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<RadixDM.Item
|
||||
className={clsx(
|
||||
'text-menu-ink group cursor-default select-none py-0.5 focus:outline-none active:opacity-80',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{to ? (
|
||||
<Link
|
||||
to={to}
|
||||
className={contextMenuItemStyles({
|
||||
variant,
|
||||
className: clsx(selected && 'bg-accent')
|
||||
})}
|
||||
onClick={() => ref.current?.click()}
|
||||
>
|
||||
{children ? (
|
||||
<span className="truncate">{children}</span>
|
||||
) : (
|
||||
<ItemInternals {...{ icon, iconProps, label, rightArrow, keybind }} />
|
||||
)}
|
||||
</Link>
|
||||
) : (
|
||||
<div
|
||||
className={contextMenuItemStyles({ variant, className: clsx(selected && 'bg-accent') })}
|
||||
>
|
||||
{children || <ItemInternals {...{ icon, iconProps, label, rightArrow, keybind }} />}
|
||||
</div>
|
||||
)}
|
||||
</RadixDM.Item>
|
||||
);
|
||||
};
|
||||
|
||||
export const DropdownMenu = {
|
||||
Root,
|
||||
Item,
|
||||
Separator,
|
||||
SubMenu
|
||||
};
|
|
@ -1,7 +1,8 @@
|
|||
export { cva, cx } from 'class-variance-authority';
|
||||
export * from './Button';
|
||||
export * from './CheckBox';
|
||||
export * as ContextMenu from './ContextMenu';
|
||||
export { ContextMenu } from './ContextMenu';
|
||||
export { DropdownMenu } from './DropdownMenu';
|
||||
export * from './Dialog';
|
||||
export * as Dropdown from './Dropdown';
|
||||
export * from './Input';
|
||||
|
|
|
@ -162,6 +162,7 @@ module.exports = function (app, options) {
|
|||
// addVariant('open', '&[data-state="open"]');
|
||||
// addVariant('closed', '&[data-state="closed"]');
|
||||
// }),
|
||||
require('tailwindcss-animate'),
|
||||
require('@headlessui/tailwindcss'),
|
||||
require('tailwindcss-radix')()
|
||||
]
|
||||
|
|
|
@ -593,6 +593,7 @@ importers:
|
|||
storybook-tailwind-dark-mode: ^1.0.15
|
||||
style-loader: ^3.3.1
|
||||
tailwindcss: ^3.1.8
|
||||
tailwindcss-animate: ^1.0.5
|
||||
tailwindcss-radix: ^2.6.0
|
||||
typescript: ^4.8.4
|
||||
dependencies:
|
||||
|
@ -646,6 +647,7 @@ importers:
|
|||
storybook-tailwind-dark-mode: 1.0.15_pmt6gdvpkbsejpsyufhkfrbkda
|
||||
style-loader: 3.3.1_webpack@5.75.0
|
||||
tailwindcss: 3.2.4_postcss@8.4.21
|
||||
tailwindcss-animate: 1.0.5_tailwindcss@3.2.4
|
||||
typescript: 4.9.4
|
||||
|
||||
packages:
|
||||
|
@ -20327,6 +20329,14 @@ packages:
|
|||
resolution: {integrity: sha512-qImOD23aDfnIDNqlG1NOehdB9IYsn1V9oByPjKY1nakv2MQYCEMyX033/q+aEtYCpmYK1cv2+NTmlH+ra6GA5A==}
|
||||
dev: true
|
||||
|
||||
/tailwindcss-animate/1.0.5_tailwindcss@3.2.4:
|
||||
resolution: {integrity: sha512-UU3qrOJ4lFQABY+MVADmBm+0KW3xZyhMdRvejwtXqYOL7YjHYxmuREFAZdmVG5LPe5E9CAst846SLC4j5I3dcw==}
|
||||
peerDependencies:
|
||||
tailwindcss: '>=3.0.0 || insiders'
|
||||
dependencies:
|
||||
tailwindcss: 3.2.4_postcss@8.4.21
|
||||
dev: true
|
||||
|
||||
/tailwindcss-radix/2.7.0:
|
||||
resolution: {integrity: sha512-fIVkT5zQYdsjT9+/Mvp+DTlJDdTFpRDuyS5+PLuJDAIIVr9+rWYKhK6rsB9QtjwUwwb0YF+BkAJN6CjZivOfLA==}
|
||||
dev: false
|
||||
|
|
Loading…
Reference in a new issue