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, XMarkIcon } from '@heroicons/react/24/solid';
import { ChevronRightIcon } from '@heroicons/react/24/solid';
import { Button } from '@sd/ui';
import clsx from 'clsx';
import { List, X } from 'phosphor-react';
import { PropsWithChildren, useEffect, useState } from 'react';
import { PropsWithChildren, useState } from 'react';
import pkg from 'react-burger-menu';
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">
<Button
onClick={() => setMenuOpen(!menuOpen)}
icon={<X weight="bold" className="w-6 h-6" />}
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} />
</div>
</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="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">
<Button
onClick={() => setMenuOpen(!menuOpen)}
icon={<List weight="bold" className="w-6 h-6" />}
className="!px-2 ml-1 !border-none"
/>
<Button onClick={() => setMenuOpen(!menuOpen)} className="!px-2 ml-1 !border-none">
<List weight="bold" className="w-6 h-6" />
</Button>
</div>
{props.doc?.url.split('/').map((item, index) => {
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 { Dropdown, DropdownItem } from '@sd/ui';
import clsx from 'clsx';
import { DotsThreeVertical } from 'phosphor-react';
import { PropsWithChildren, useEffect, useState } from 'react';
import { positions } from '../pages/careers.page';
import { getWindow } from '../utils';
@ -96,7 +94,7 @@ export default function NavBar() {
) : null}
</div>
</div>
<Dropdown
<Dropdown.Root
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"
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 { LocationCreateArgs } from '@sd/client';
import { Button, CategoryHeading, Dropdown, OverlayPanel } from '@sd/ui';
import { restyle } from '@sd/ui';
import clsx from 'clsx';
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 { useOperatingSystem } from '../../hooks/useOperatingSystem';
@ -157,6 +158,8 @@ export function Sidebar() {
const os = useOperatingSystem();
const { library, libraries, isLoading: isLoadingLibraries, switchLibrary } = useCurrentLibrary();
const itemStyles = macOnly(os, 'dark:hover:bg-gray-550 dark:hover:bg-opacity-50');
return (
<div
className={clsx(
@ -166,52 +169,59 @@ export function Sidebar() {
>
<WindowControls />
<Dropdown
buttonProps={{
justify: 'left',
className: clsx(
`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`,
macOnly(
os,
'dark:!bg-opacity-40 dark:hover:!bg-opacity-70 dark:!border-[#333949] dark:hover:!border-[#394052]'
)
),
variant: 'gray'
}}
<Dropdown.Root
className="mt-2"
button={
<Dropdown.Button
variant="gray"
className={clsx(
`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`,
(library === null || isLoadingLibraries) && 'text-gray-300',
macOnly(
os,
'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
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
buttonText={isLoadingLibraries ? 'Loading...' : library ? library.config.name : ' '}
buttonTextClassName={library === null || isLoadingLibraries ? 'text-gray-300' : undefined}
items={[
libraries?.map((lib) => ({
name: lib.config.name,
selected: lib.uuid === library?.uuid,
onPress: () => switchLibrary(lib.uuid)
})) || [],
[
{
name: 'Library Settings',
icon: CogIcon,
to: 'settings/library'
},
{
name: 'Add Library',
icon: PlusIcon,
wrapItemComponent: CreateLibraryDialog
},
{
name: 'Lock',
icon: LockClosedIcon,
disabled: true,
onPress: () => {
alert('TODO: Not implemented yet!');
}
}
]
]}
/>
>
<Dropdown.Section>
{libraries?.map((lib) => (
<Dropdown.Item
className={itemStyles}
selected={lib.uuid === library?.uuid}
key={lib.uuid}
onClick={() => switchLibrary(lib.uuid)}
>
{lib.config.name}
</Dropdown.Item>
))}
</Dropdown.Section>
<Dropdown.Section>
<Dropdown.Item className={itemStyles} icon={CogIcon} to="settings/library">
Library Settings
</Dropdown.Item>
<CreateLibraryDialog>
<Dropdown.Item className={itemStyles} icon={PlusIcon}>
Add Library
</Dropdown.Item>
</CreateLibraryDialog>
<Dropdown.Item
className={itemStyles}
icon={LockClosedIcon}
onClick={() => alert('TODO: Not implemented yet!')}
>
Lock
</Dropdown.Item>
</Dropdown.Section>
</Dropdown.Root>
<div className="pt-1">
<SidebarLink to="/overview">
<Icon component={Planet} />

View file

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

View file

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

View file

@ -60,28 +60,29 @@ export const SubMenu = ({
);
};
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
`
const itemStyles = cva(
[
'flex flex-row items-center justify-start flex-1',
'px-2 py-1 space-x-2',
'cursor-default rounded',
'focus:outline-none'
],
{
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'
}
},
defaultVariants: {
variant: 'default'
}
});
);
interface ItemProps extends VariantProps<typeof itemStyles> {
icon?: Icon;

View file

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

View file

@ -1,155 +1,114 @@
import { Menu, Transition } from '@headlessui/react';
import { ChevronDownIcon } from '@heroicons/react/24/solid';
import { VariantProps, cva } from 'class-variance-authority';
import clsx from 'clsx';
import { Fragment, PropsWithChildren } from 'react';
import { Link } from 'react-router-dom';
import { Button } from './Button';
import * as UI from '.';
import { tw } from './utils';
export type DropdownItem = (
| {
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 const Section = tw.div`px-1 py-1 space-y-[2px]`;
export interface DropdownProps {
items: DropdownItem[];
buttonText?: string;
buttonTextClassName?: string;
buttonProps?: React.ComponentProps<typeof Button>;
buttonComponent?: React.ReactNode;
buttonIcon?: any;
const itemStyles = cva(
'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',
{
variants: {
selected: {
true: 'bg-gray-300 dark:bg-primary dark:hover:bg-primary'
},
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;
itemsClassName?: string;
itemButtonClassName?: string;
align?: 'left' | 'right';
}
export const Dropdown: React.FC<DropdownProps> = (props) => {
export const Root = (props: PropsWithChildren<DropdownRootProps>) => {
return (
<div className={clsx('w-full mt-2', props.className)}>
<Menu as="div" className="relative flex w-full text-left">
<Menu.Button as="div" className="flex-1 outline-none">
{props.buttonComponent ? (
props.buttonComponent
) : (
<Button size="sm" {...props.buttonProps}>
{props.buttonIcon}
{props.buttonText && (
<>
<span className={clsx('w-32 truncate', props.buttonTextClassName)}>
{props.buttonText}
</span>
<div className="flex-grow" />
<ChevronDownIcon
className="w-5 h-5 ml-2 -mr-1 text-violet-200 hover:text-violet-100 "
aria-hidden="true"
/>
</>
)}
</Button>
<Menu as="div" className={clsx('relative flex w-full text-left', props.className)}>
<Menu.Button as="div" className="flex-1 outline-none">
{props.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
className={clsx(
'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' }
)}
</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
className={clsx(
'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' }
)}
>
{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>
{props.children}
</Menu.Items>
</Transition>
</Menu>
);
};

View file

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

View file

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