Merge remote-tracking branch 'origin/refactor-dropdown' into spacedrive-but-themable

This commit is contained in:
Jamie Pine 2022-10-20 21:54:50 -07:00
commit 90a267930a
10 changed files with 230 additions and 272 deletions

View file

@ -1,9 +1,7 @@
import { Disclosure, Transition } from '@headlessui/react'; import { ChevronRightIcon } from '@heroicons/react/24/solid';
import { ChevronRightIcon, XMarkIcon } from '@heroicons/react/24/solid';
import { Button } from '@sd/ui'; import { Button } from '@sd/ui';
import clsx from 'clsx';
import { List, X } from 'phosphor-react'; import { List, X } from 'phosphor-react';
import { PropsWithChildren, useEffect, useState } from 'react'; import { PropsWithChildren, useState } from 'react';
import pkg from 'react-burger-menu'; import pkg from 'react-burger-menu';
import { Doc, DocsNavigation, toTitleCase } from '../pages/docs/api'; import { Doc, DocsNavigation, toTitleCase } from '../pages/docs/api';
@ -32,9 +30,10 @@ export default function DocsLayout(props: Props) {
<div className="visible h-screen pb-20 overflow-x-hidden custom-scroll doc-sidebar-scroll bg-gray-650 pt-7 px-7 sm:invisible"> <div className="visible h-screen pb-20 overflow-x-hidden custom-scroll doc-sidebar-scroll bg-gray-650 pt-7 px-7 sm:invisible">
<Button <Button
onClick={() => setMenuOpen(!menuOpen)} onClick={() => setMenuOpen(!menuOpen)}
icon={<X weight="bold" className="w-6 h-6" />}
className="!px-1 -ml-0.5 mb-3 !border-none" className="!px-1 -ml-0.5 mb-3 !border-none"
/> >
<X weight="bold" className="w-6 h-6" />
</Button>
<DocsSidebar activePath={props?.doc?.url} navigation={props.navigation} /> <DocsSidebar activePath={props?.doc?.url} navigation={props.navigation} />
</div> </div>
</Menu> </Menu>
@ -45,11 +44,9 @@ export default function DocsLayout(props: Props) {
<div className="flex flex-col w-full sm:flex-row" id="page-container"> <div className="flex flex-col w-full sm:flex-row" id="page-container">
<div className="h-12 px-5 flex w-full border-t border-gray-600 border-b mt-[65px] sm:hidden items-center "> <div className="h-12 px-5 flex w-full border-t border-gray-600 border-b mt-[65px] sm:hidden items-center ">
<div className="flex sm:hidden"> <div className="flex sm:hidden">
<Button <Button onClick={() => setMenuOpen(!menuOpen)} className="!px-2 ml-1 !border-none">
onClick={() => setMenuOpen(!menuOpen)} <List weight="bold" className="w-6 h-6" />
icon={<List weight="bold" className="w-6 h-6" />} </Button>
className="!px-2 ml-1 !border-none"
/>
</div> </div>
{props.doc?.url.split('/').map((item, index) => { {props.doc?.url.split('/').map((item, index) => {
if (index === 2) return null; if (index === 2) return null;

View file

@ -9,11 +9,9 @@ import { Discord, Github } from '@icons-pack/react-simple-icons';
import AppLogo from '@sd/assets/images/logo.png'; import AppLogo from '@sd/assets/images/logo.png';
import { Dropdown, DropdownItem } from '@sd/ui'; import { Dropdown, DropdownItem } from '@sd/ui';
import clsx from 'clsx'; import clsx from 'clsx';
import { DotsThreeVertical } from 'phosphor-react'; import { DotsThreeVertical } from 'phosphor-react';
import { PropsWithChildren, useEffect, useState } from 'react'; import { PropsWithChildren, useEffect, useState } from 'react';
import { positions } from '../pages/careers.page'; import { positions } from '../pages/careers.page';
import { getWindow } from '../utils'; import { getWindow } from '../utils';
@ -96,7 +94,7 @@ export default function NavBar() {
) : null} ) : null}
</div> </div>
</div> </div>
<Dropdown <Dropdown.Root
className="absolute block h-6 text-white w-44 top-2 right-4 lg:hidden" className="absolute block h-6 text-white w-44 top-2 right-4 lg:hidden"
itemsClassName="!rounded-2xl shadow-2xl shadow-black p-2 !bg-gray-850 mt-2 !border-gray-500" itemsClassName="!rounded-2xl shadow-2xl shadow-black p-2 !bg-gray-850 mt-2 !border-gray-500"
itemButtonClassName="!py-1 !rounded-md text-[15px]" itemButtonClassName="!py-1 !rounded-md text-[15px]"

View file

@ -3,9 +3,10 @@ import { PlusIcon } from '@heroicons/react/24/solid';
import { useCurrentLibrary, useLibraryMutation, useLibraryQuery, usePlatform } from '@sd/client'; import { useCurrentLibrary, useLibraryMutation, useLibraryQuery, usePlatform } from '@sd/client';
import { LocationCreateArgs } from '@sd/client'; import { LocationCreateArgs } from '@sd/client';
import { Button, CategoryHeading, Dropdown, OverlayPanel } from '@sd/ui'; import { Button, CategoryHeading, Dropdown, OverlayPanel } from '@sd/ui';
import { restyle } from '@sd/ui';
import clsx from 'clsx'; import clsx from 'clsx';
import { CheckCircle, CirclesFour, Planet, WaveTriangle } from 'phosphor-react'; import { CheckCircle, CirclesFour, Planet, WaveTriangle } from 'phosphor-react';
import { PropsWithChildren } from 'react'; import { PropsWithChildren, forwardRef } from 'react';
import { NavLink, NavLinkProps, useNavigate } from 'react-router-dom'; import { NavLink, NavLinkProps, useNavigate } from 'react-router-dom';
import { useOperatingSystem } from '../../hooks/useOperatingSystem'; import { useOperatingSystem } from '../../hooks/useOperatingSystem';
@ -157,6 +158,8 @@ export function Sidebar() {
const os = useOperatingSystem(); const os = useOperatingSystem();
const { library, libraries, isLoading: isLoadingLibraries, switchLibrary } = useCurrentLibrary(); const { library, libraries, isLoading: isLoadingLibraries, switchLibrary } = useCurrentLibrary();
const itemStyles = macOnly(os, 'dark:hover:bg-gray-550 dark:hover:bg-opacity-50');
return ( return (
<div <div
className={clsx( className={clsx(
@ -166,52 +169,59 @@ export function Sidebar() {
> >
<WindowControls /> <WindowControls />
<Dropdown <Dropdown.Root
buttonProps={{ className="mt-2"
justify: 'left', button={
className: clsx( <Dropdown.Button
`flex w-full text-left max-w-full mb-1 mt-1 -mr-0.5 shadow-xs rounded !bg-gray-50 border-gray-150 hover:!bg-gray-1000 dark:!bg-gray-500 dark:hover:!bg-gray-500 dark:!border-gray-550 dark:hover:!border-gray-500`, variant="gray"
macOnly( className={clsx(
os, `flex w-full text-left max-w-full mb-1 mt-1 -mr-0.5 shadow-xs rounded !bg-gray-50 border-gray-150 hover:!bg-gray-1000 dark:!bg-gray-500 dark:hover:!bg-gray-500 dark:!border-gray-550 dark:hover:!border-gray-500`,
'dark:!bg-opacity-40 dark:hover:!bg-opacity-70 dark:!border-[#333949] dark:hover:!border-[#394052]' (library === null || isLoadingLibraries) && 'text-gray-300',
) macOnly(
), os,
variant: 'gray' 'dark:!bg-opacity-40 dark:hover:!bg-opacity-70 dark:!border-[#333949] dark:hover:!border-[#394052]'
}} )
)}
>
{/* this shouldn't default to "My Library", it is only this way for landing demo */}
<span className="w-32 truncate">
{isLoadingLibraries ? 'Loading...' : library ? library.config.name : ' '}
</span>
</Dropdown.Button>
}
// to support the transparent sidebar on macOS we use slightly adjusted styles // to support the transparent sidebar on macOS we use slightly adjusted styles
itemsClassName={macOnly(os, 'dark:bg-gray-800 dark:divide-gray-600')} itemsClassName={macOnly(os, 'dark:bg-gray-800 dark:divide-gray-600')}
itemButtonClassName={macOnly(os, 'dark:hover:bg-gray-550 dark:hover:bg-opacity-50')} >
// this shouldn't default to "My Library", it is only this way for landing demo <Dropdown.Section>
buttonText={isLoadingLibraries ? 'Loading...' : library ? library.config.name : ' '} {libraries?.map((lib) => (
buttonTextClassName={library === null || isLoadingLibraries ? 'text-gray-300' : undefined} <Dropdown.Item
items={[ className={itemStyles}
libraries?.map((lib) => ({ selected={lib.uuid === library?.uuid}
name: lib.config.name, key={lib.uuid}
selected: lib.uuid === library?.uuid, onClick={() => switchLibrary(lib.uuid)}
onPress: () => switchLibrary(lib.uuid) >
})) || [], {lib.config.name}
[ </Dropdown.Item>
{ ))}
name: 'Library Settings', </Dropdown.Section>
icon: CogIcon, <Dropdown.Section>
to: 'settings/library' <Dropdown.Item className={itemStyles} icon={CogIcon} to="settings/library">
}, Library Settings
{ </Dropdown.Item>
name: 'Add Library', <CreateLibraryDialog>
icon: PlusIcon, <Dropdown.Item className={itemStyles} icon={PlusIcon}>
wrapItemComponent: CreateLibraryDialog Add Library
}, </Dropdown.Item>
{ </CreateLibraryDialog>
name: 'Lock', <Dropdown.Item
icon: LockClosedIcon, className={itemStyles}
disabled: true, icon={LockClosedIcon}
onPress: () => { onClick={() => alert('TODO: Not implemented yet!')}
alert('TODO: Not implemented yet!'); >
} Lock
} </Dropdown.Item>
] </Dropdown.Section>
]} </Dropdown.Root>
/>
<div className="pt-1"> <div className="pt-1">
<SidebarLink to="/overview"> <SidebarLink to="/overview">
<Icon component={Planet} /> <Icon component={Planet} />

View file

@ -171,11 +171,8 @@ export default function OverviewScreen() {
// ctaAction={() => {}} // ctaAction={() => {}}
ctaLabel="Connect" ctaLabel="Connect"
trigger={ trigger={
<Button <Button size="sm" variant="gray">
size="sm" <PlusIcon className="inline w-4 h-4 -mt-0.5 xl:mr-1" />
icon={<PlusIcon className="inline w-4 h-4 -mt-0.5 xl:mr-1" />}
variant="gray"
>
<span className="hidden xl:inline-block">Add Device</span> <span className="hidden xl:inline-block">Add Device</span>
</Button> </Button>
} }

View file

@ -3,9 +3,7 @@ import clsx from 'clsx';
import { forwardRef } from 'react'; import { forwardRef } from 'react';
import { Link, LinkProps } from 'react-router-dom'; import { Link, LinkProps } from 'react-router-dom';
export interface ButtonBaseProps extends VariantProps<typeof styles> { export interface ButtonBaseProps extends VariantProps<typeof styles> {}
icon?: React.ReactNode;
}
export type ButtonProps = ButtonBaseProps & export type ButtonProps = ButtonBaseProps &
React.ButtonHTMLAttributes<HTMLButtonElement> & { React.ButtonHTMLAttributes<HTMLButtonElement> & {
@ -69,7 +67,8 @@ const styles = cva(
colored: ['text-white shadow-sm hover:bg-opacity-90 active:bg-opacity-100'], colored: ['text-white shadow-sm hover:bg-opacity-90 active:bg-opacity-100'],
selected: [ selected: [
'bg-gray-100 dark:bg-gray-500 text-black hover:text-black active:text-black dark:hover:text-white dark:text-white' 'bg-gray-100 dark:bg-gray-500 text-black hover:text-black active:text-black dark:hover:text-white dark:text-white'
] ],
bare: ''
} }
}, },
defaultVariants: { defaultVariants: {
@ -86,21 +85,10 @@ export const Button = forwardRef<
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
className = clsx(styles(props), className); className = clsx(styles(props), className);
let children = (
<>
{props.icon}
{props.children}
</>
);
return hasHref(props) ? ( return hasHref(props) ? (
<a {...props} ref={ref as any} className={clsx(className, 'no-underline inline-block')}> <a {...props} ref={ref as any} className={clsx(className, 'no-underline inline-block')} />
{children}
</a>
) : ( ) : (
<button {...(props as ButtonProps)} ref={ref as any} className={className}> <button {...(props as ButtonProps)} ref={ref as any} className={className} />
{children}
</button>
); );
}); });
@ -116,10 +104,7 @@ export const ButtonLink = forwardRef<
return ( return (
<Link to={to} ref={ref as any} className={className}> <Link to={to} ref={ref as any} className={className}>
<> {props.children}
{props.icon}
{props.children}
</>
</Link> </Link>
); );
}); });

View file

@ -60,28 +60,29 @@ export const SubMenu = ({
); );
}; };
const ITEM_CLASSES = ` const itemStyles = cva(
flex flex-row items-center justify-start flex-1 [
px-2 py-1 space-x-2 'flex flex-row items-center justify-start flex-1',
cursor-default rounded 'px-2 py-1 space-x-2',
focus:outline-none 'cursor-default rounded',
`; 'focus:outline-none'
],
const itemStyles = cva([ITEM_CLASSES], { {
variants: { variants: {
variant: { variant: {
default: 'hover:bg-primary focus:bg-primary', default: 'hover:bg-primary focus:bg-primary',
danger: ` danger: [
text-red-600 dark:text-red-400 'text-red-600 dark:text-red-400',
hover:text-white focus:text-white 'hover:text-white focus:text-white',
hover:bg-red-500 focus:bg-red-500 'hover:bg-red-500 focus:bg-red-500'
` ]
}
},
defaultVariants: {
variant: 'default'
} }
},
defaultVariants: {
variant: 'default'
} }
}); );
interface ItemProps extends VariantProps<typeof itemStyles> { interface ItemProps extends VariantProps<typeof itemStyles> {
icon?: Icon; icon?: Icon;

View file

@ -1,33 +1,33 @@
import { ComponentMeta, ComponentStory } from '@storybook/react'; import { ComponentMeta, ComponentStory } from '@storybook/react';
import { Dropdown } from './Dropdown'; import { Root } from './Dropdown';
export default { export default {
title: 'UI/Dropdown', title: 'UI/Dropdown',
component: Dropdown, component: Root,
argTypes: {}, argTypes: {},
parameters: { parameters: {
backgrounds: { backgrounds: {
default: 'dark' default: 'dark'
} }
} }
} as ComponentMeta<typeof Dropdown>; } as ComponentMeta<typeof Root>;
const Template: ComponentStory<typeof Dropdown> = (args) => <Dropdown {...args} />; const Template: ComponentStory<typeof Root> = (args) => <Root {...args} />;
export const Default = Template.bind({}); export const Default = Template.bind({});
Default.args = { // Default.args = {
buttonText: 'Item 1', // buttonText: 'Item 1',
items: [ // items: [
[ // [
{ // {
name: 'Item 1', // name: 'Item 1',
selected: true // selected: true
}, // },
{ // {
name: 'Item 2', // name: 'Item 2',
selected: false // selected: false
} // }
] // ]
] // ]
}; // };

View file

@ -1,155 +1,114 @@
import { Menu, Transition } from '@headlessui/react'; import { Menu, Transition } from '@headlessui/react';
import { ChevronDownIcon } from '@heroicons/react/24/solid'; import { ChevronDownIcon } from '@heroicons/react/24/solid';
import { VariantProps, cva } from 'class-variance-authority';
import clsx from 'clsx'; import clsx from 'clsx';
import { Fragment, PropsWithChildren } from 'react'; import { Fragment, PropsWithChildren } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Button } from './Button'; import * as UI from '.';
import { tw } from './utils';
export type DropdownItem = ( export const Section = tw.div`px-1 py-1 space-y-[2px]`;
| {
name: string;
icon?: any;
selected?: boolean;
to?: string;
wrapItemComponent?: React.FC<PropsWithChildren>;
}
| {
name: string;
icon?: any;
disabled?: boolean;
selected?: boolean;
onPress?: () => any;
to?: string;
wrapItemComponent?: React.FC<PropsWithChildren>;
}
)[];
export interface DropdownProps { const itemStyles = cva(
items: DropdownItem[]; 'text-sm group flex grow shrink-0 rounded items-center w-full whitespace-nowrap px-2 py-1 mb-[2px] dark:hover:bg-gray-650 disabled:opacity-50 disabled:cursor-not-allowed',
buttonText?: string; {
buttonTextClassName?: string; variants: {
buttonProps?: React.ComponentProps<typeof Button>; selected: {
buttonComponent?: React.ReactNode; true: 'bg-gray-300 dark:bg-primary dark:hover:bg-primary'
buttonIcon?: any; },
active: {
true: ''
// false: 'text-gray-900 dark:text-gray-200'
}
}
}
);
const itemIconStyles = cva('mr-2 w-4 h-4', {
variants: {
active: {
true: 'dark:text-gray-100',
false: 'text-gray-600 dark:text-gray-200'
}
}
});
type DropdownItemProps =
| PropsWithChildren<{
to?: string;
className?: string;
icon?: any;
onClick?: () => void;
}> &
VariantProps<typeof itemStyles>;
export const Item = ({ to, className, icon: Icon, children, ...props }: DropdownItemProps) => {
let content = (
<>
{Icon && <Icon className={itemIconStyles(props)} />}
<span className="text-left">{children}</span>
</>
);
return to ? (
<Link {...props} to={to} className={clsx(itemStyles(props), className)}>
{content}
</Link>
) : (
<button {...props} className={clsx(itemStyles(props), className)}>
{content}
</button>
);
};
export const Button = ({ children, ...props }: UI.ButtonProps) => {
return (
<UI.Button size="sm" {...props}>
{children}
<div className="flex-grow" />
<ChevronDownIcon
className="w-5 h-5 ml-2 -mr-1 text-violet-200 hover:text-violet-100"
aria-hidden="true"
/>
</UI.Button>
);
};
export interface DropdownRootProps {
button: React.ReactNode;
className?: string; className?: string;
itemsClassName?: string; itemsClassName?: string;
itemButtonClassName?: string;
align?: 'left' | 'right'; align?: 'left' | 'right';
} }
export const Dropdown: React.FC<DropdownProps> = (props) => { export const Root = (props: PropsWithChildren<DropdownRootProps>) => {
return ( return (
<div className={clsx('w-full mt-2', props.className)}> <Menu as="div" className={clsx('relative flex w-full text-left', props.className)}>
<Menu as="div" className="relative flex w-full text-left"> <Menu.Button as="div" className="flex-1 outline-none">
<Menu.Button as="div" className="flex-1 outline-none"> {props.button}
{props.buttonComponent ? ( </Menu.Button>
props.buttonComponent
) : ( <Transition
<Button size="sm" {...props.buttonProps}> as={Fragment}
{props.buttonIcon} enter="transition duration-100 ease-out"
{props.buttonText && ( enterFrom="transform scale-95 opacity-0"
<> enterTo="transform scale-100 opacity-100"
<span className={clsx('w-32 truncate', props.buttonTextClassName)}> leave="transition duration-75 ease-out"
{props.buttonText} leaveFrom="transform scale-100 opacity-100"
</span> leaveTo="transform scale-95 opacity-0"
<div className="flex-grow" /> >
<ChevronDownIcon <Menu.Items
className="w-5 h-5 ml-2 -mr-1 text-violet-200 hover:text-violet-100 " className={clsx(
aria-hidden="true" 'absolute z-50 min-w-fit w-full bg-white border divide-y divide-gray-100 rounded shadow-xl top-full dark:bg-gray-550 dark:divide-gray-500 dark:border-gray-600 ring-1 ring-black ring-opacity-5 focus:outline-none',
/> props.itemsClassName,
</> { 'left-0': props.align === 'left' },
)} { 'right-0': props.align === 'right' }
</Button>
)} )}
</Menu.Button>
<Transition
as={Fragment}
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
> >
<Menu.Items {props.children}
className={clsx( </Menu.Items>
'absolute z-50 min-w-fit w-full bg-white border divide-y divide-gray-100 rounded shadow-xl top-full dark:bg-gray-550 dark:divide-gray-500 dark:border-gray-600 ring-1 ring-black ring-opacity-5 focus:outline-none', </Transition>
props.itemsClassName, </Menu>
{ 'left-0': props.align === 'left' },
{ 'right-0': props.align === 'right' }
)}
>
{props.items.map((item, index) => (
<div key={index} className="px-1 py-1 space-y-[2px]">
{item.map((button, index) => (
<Menu.Item key={index}>
{({ active }) => {
const WrappedItem: any = button.wrapItemComponent
? button.wrapItemComponent
: (props: React.PropsWithChildren) => <>{props.children}</>;
return (
<WrappedItem>
{button.to ? (
<Link
to={button.to}
className={clsx(
'text-sm group flex grow shrink-0 rounded items-center w-full whitespace-nowrap px-2 py-1 mb-[2px] dark:hover:bg-gray-650 disabled:opacity-50 disabled:cursor-not-allowed',
{
'bg-gray-300 dark:bg-primary dark:hover:bg-primary':
button.selected
// 'text-gray-900 dark:text-gray-200': !active
},
props.itemButtonClassName
)}
>
{button.icon && (
<button.icon
className={clsx('mr-2 w-4 h-4', {
'dark:text-gray-100': active,
'text-gray-600 dark:text-gray-200': !active
})}
/>
)}
<span className="text-left">{button.name}</span>
</Link>
) : (
<button
onClick={(button as any).onPress}
disabled={(button as any)?.disabled === true}
className={clsx(
'text-sm group flex grow shrink-0 rounded items-center w-full whitespace-nowrap px-2 py-1 mb-[2px] dark:hover:bg-gray-650 disabled:opacity-50 disabled:cursor-not-allowed',
{
'bg-gray-300 dark:bg-primary dark:hover:bg-primary':
button.selected
// 'text-gray-900 dark:text-gray-200': !active
},
props.itemButtonClassName
)}
>
{button.icon && (
<button.icon
className={clsx('mr-2 w-4 h-4', {
'dark:text-gray-100': active,
'text-gray-600 dark:text-gray-200': !active
})}
/>
)}
<span className="text-left">{button.name}</span>
</button>
)}
</WrappedItem>
);
}}
</Menu.Item>
))}
</div>
))}
</Menu.Items>
</Transition>
</Menu>
</div>
); );
}; };

View file

@ -1,5 +1,5 @@
export * from './Button'; export * from './Button';
export * from './Dropdown'; export * as Dropdown from './Dropdown';
export * from './Dialog'; export * from './Dialog';
export * from './Loader'; export * from './Loader';
export * as ContextMenu from './ContextMenu'; export * as ContextMenu from './ContextMenu';
@ -8,5 +8,5 @@ export * from './Input';
export * from './Select'; export * from './Select';
export * as Tabs from './Tabs'; export * as Tabs from './Tabs';
export * from './Typography'; export * from './Typography';
export { tw } from './utils'; export * from './utils';
export { cva } from 'class-variance-authority'; export { cva } from 'class-variance-authority';

View file

@ -1,16 +1,9 @@
import clsx from 'clsx';
import React from 'react'; import React from 'react';
function twFactory(element: any) { function twFactory(element: any) {
return ([className, ..._]: TemplateStringsArray) => { return ([className, ..._]: TemplateStringsArray) => {
const Component = React.forwardRef(({ className: pClassName, ...props }: any, ref) => return restyle(element)(() => className);
React.createElement(element, {
...props,
className: [className, pClassName],
ref
})
);
return Component;
}; };
} }
@ -28,3 +21,21 @@ export const tw = new Proxy((() => {}) as unknown as TailwindFactory, {
get: (_, property: string) => twFactory(property), get: (_, property: string) => twFactory(property),
apply: (_, __, [el]: [React.ReactElement]) => twFactory(el) apply: (_, __, [el]: [React.ReactElement]) => twFactory(el)
}); });
export const restyle = <
T extends
| string
| React.FunctionComponent<{ className: string }>
| React.ComponentClass<{ className: string }>
>(
element: T
) => {
return (cls: () => string) =>
React.forwardRef(({ className, ...props }: any, ref) =>
React.createElement(element, {
...props,
className: clsx(cls(), className),
ref
})
);
};