Skip to content

Commit

Permalink
feat: ui dropdown and select, ref #4312 and #4417
Browse files Browse the repository at this point in the history
  • Loading branch information
fbwoolf committed Jan 3, 2024
1 parent 51320f9 commit d16eb6d
Show file tree
Hide file tree
Showing 18 changed files with 1,092 additions and 738 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@
"@octokit/types": "12.0.0",
"@radix-ui/colors": "3.0.0",
"@radix-ui/react-accessible-icon": "1.0.3",
"@radix-ui/react-dropdown-menu": "2.0.6",
"@radix-ui/react-select": "2.0.0",
"@radix-ui/themes": "2.0.2",
"@reduxjs/toolkit": "1.9.6",
"@scure/base": "1.1.3",
Expand Down
26 changes: 26 additions & 0 deletions src/app/ui/components/dowpdown-menu/dropdown-example.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ListItemType } from '../list/list-item';
import { DropdownMenu } from './dropdown-menu';
import { DropdownMenuItem } from './dropdown-menu-item';
import { DropdownMenuSectionLabel } from './dropdown-menu-section-label';

const items: ListItemType[] = [{ label: 'Label 1' }, { label: 'Label 2' }];

// Example implementation - remove with Storybook
// ts-unused-exports:disable-next-line
export function DropdownMenuExample() {
return (
<DropdownMenu>
<DropdownMenuSectionLabel label="Items" />
{items.map(item => {
return (
<DropdownMenuItem
key={item.label}
iconLeft={item.iconLeft}
iconRight={item.iconRight}
label={item.label}
/>
);
})}
</DropdownMenu>
);
}
14 changes: 14 additions & 0 deletions src/app/ui/components/dowpdown-menu/dropdown-menu-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu';

import { ListItem, ListItemType } from '../list/list-item';
import { listItemStyles } from '../list/styles';

type DropdownMenuItemProps = ListItemType;

export function DropdownMenuItem({ iconLeft, iconRight, label }: DropdownMenuItemProps) {
return (
<RadixDropdownMenu.Item className={listItemStyles}>
<ListItem iconLeft={iconLeft} iconRight={iconRight} label={label} />
</RadixDropdownMenu.Item>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu';

import { ListSectionLabel } from '../list/list-section-label';
import { listSectionLabelStyles } from '../list/styles';

export function DropdownMenuSectionLabel(props: { label: string }) {
return (
<RadixDropdownMenu.Label className={listSectionLabelStyles}>
<ListSectionLabel label={props.label} />
</RadixDropdownMenu.Label>
);
}
19 changes: 19 additions & 0 deletions src/app/ui/components/dowpdown-menu/dropdown-menu-trigger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu';
import { HStack, styled } from 'leather-styles/jsx';

import { defaultTriggerStyles } from '@app/ui/components/list/styles';

import { ChevronDownIcon } from '../icons/chevron-down-icon';

export function DropdownMenuTrigger({ placeholder = 'Options' }: { placeholder?: string }) {
return (
<RadixDropdownMenu.Trigger className={defaultTriggerStyles}>
<styled.button>
<HStack gap="space.02" width="100%">
<styled.span textStyle="label.02">{placeholder}</styled.span>
<ChevronDownIcon />
</HStack>
</styled.button>
</RadixDropdownMenu.Trigger>
);
}
20 changes: 20 additions & 0 deletions src/app/ui/components/dowpdown-menu/dropdown-menu.layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ReactNode } from 'react';

import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu';

import { listContentStyles } from '../list/styles';

interface DropdownMenuLayoutProps {
children: ReactNode;
trigger: ReactNode;
}
export function DropdownMenuLayout({ children, trigger }: DropdownMenuLayoutProps) {
return (
<RadixDropdownMenu.Root>
{trigger}
<RadixDropdownMenu.Content align="start" className={listContentStyles} sideOffset={8}>
{children}
</RadixDropdownMenu.Content>
</RadixDropdownMenu.Root>
);
}
14 changes: 14 additions & 0 deletions src/app/ui/components/dowpdown-menu/dropdown-menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ReactNode } from 'react';

import { DropdownMenuTrigger } from './dropdown-menu-trigger';
import { DropdownMenuLayout } from './dropdown-menu.layout';

interface DropdownMenuProps {
children: ReactNode;
trigger?: ReactNode;
}
export function DropdownMenu({ children, trigger }: DropdownMenuProps) {
return (
<DropdownMenuLayout trigger={trigger ?? <DropdownMenuTrigger />}>{children}</DropdownMenuLayout>
);
}
16 changes: 16 additions & 0 deletions src/app/ui/components/list/list-item.layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ReactNode } from 'react';

import { Flex } from 'leather-styles/jsx';

interface ListItemLayoutProps {
contentLeft?: ReactNode;
contentRight?: ReactNode;
}
export function ListItemLayout({ contentLeft, contentRight }: ListItemLayoutProps) {
return (
<Flex alignItems="center" justifyContent="space-between" p="space.03" width="100%">
{contentLeft}
{contentRight}
</Flex>
);
}
31 changes: 31 additions & 0 deletions src/app/ui/components/list/list-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ReactNode } from 'react';

import { HStack, styled } from 'leather-styles/jsx';

import { ListItemLayout } from './list-item.layout';

export interface ListItemType {
iconLeft?: ReactNode;
iconRight?: ReactNode;
label: string;
}

type ListItemProps = ListItemType;

function ItemContentLeft({ iconLeft, label }: Omit<ListItemProps, 'iconRight'>) {
return (
<HStack gap="space.02" minHeight="24px">
{iconLeft}
<styled.span textStyle="label.02">{label}</styled.span>
</HStack>
);
}

export function ListItem({ iconLeft, iconRight, label }: ListItemProps) {
return (
<ListItemLayout
contentLeft={<ItemContentLeft iconLeft={iconLeft} label={label} />}
contentRight={iconRight}
/>
);
}
16 changes: 16 additions & 0 deletions src/app/ui/components/list/list-section-label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { styled } from 'leather-styles/jsx';

export function ListSectionLabel(props: { label: string }) {
return (
<styled.span
color="accent.text-subdued"
display="block"
px="space.03"
py="space.02"
textStyle="body.02"
width="100%"
>
{props.label}
</styled.span>
);
}
59 changes: 59 additions & 0 deletions src/app/ui/components/list/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { css } from 'leather-styles/css';

export const defaultTriggerStyles = css({
bg: 'accent.background-primary',
borderRadius: '2px',
fontWeight: 500,
maxWidth: 'fit-content',
maxHeight: 'fit-content',
px: 'space.04',
py: 'space.03',
textStyle: 'label.02',

'&[data-state=open]': {
bg: 'accent.component-background-pressed',
},
});

export const listContentStyles = css({
animationDuration: '400ms',
animationTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)',
'--base-menu-padding': '0px',
bg: 'accent.background-primary',
borderRadius: '2px',
boxShadow:
'0px 12px 24px 0px rgba(18, 16, 15, 0.08), 0px 4px 8px 0px rgba(18, 16, 15, 0.08), 0px 0px 2px 0px rgba(18, 16, 15, 0.08)',
minWidth: '256px',
willChange: 'transform, opacity',
zIndex: 999,

'&[data-side=bottom]': {
animationName: 'slideUpAndFade',
},
});

export const listSectionLabelStyles = css({
height: 'auto',
px: 'space.02',
});

export const listItemStyles = css({
bg: 'accent.background-primary',
color: 'accent.text-primary',
height: 'auto',
outline: 'none',
px: 'space.02',

'&[data-highlighted]': {
bg: 'accent.component-background-hover',
},
});

// For use when needed
// ts-unused-exports:disable-next-line
export const listSeparator = css({
bg: 'accent.background-primary',
color: 'accent.border-default',
mx: '0px',
my: 'space.03',
});
30 changes: 30 additions & 0 deletions src/app/ui/components/select/select-example.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as RadixSelect from '@radix-ui/react-select';

import { ListItemType } from '../list/list-item';
import { Select } from './select';
import { SelectItem } from './select-item';
import { SelectSectionLabel } from './select-section-label';

const items: ListItemType[] = [{ label: 'Label 1' }, { label: 'Label 2' }];

// Example implementation - remove with Storybook
// ts-unused-exports:disable-next-line
export function SelectExample() {
return (
<Select>
<RadixSelect.Group>
<SelectSectionLabel label="Items" />
{items.map(item => {
return (
<SelectItem
key={item.label}
iconLeft={item.iconLeft}
iconRight={item.iconRight}
label={item.label}
/>
);
})}
</RadixSelect.Group>
</Select>
);
}
30 changes: 30 additions & 0 deletions src/app/ui/components/select/select-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as RadixSelect from '@radix-ui/react-select';
import { HStack, styled } from 'leather-styles/jsx';

import { CheckmarkIcon } from '../icons/checkmark-icon';
import { ListItemType } from '../list/list-item';
import { ListItemLayout } from '../list/list-item.layout';
import { listItemStyles } from '../list/styles';

type SelectItemProps = ListItemType;

export function SelectItem({ iconLeft, iconRight, label }: SelectItemProps) {
return (
<RadixSelect.Item className={listItemStyles} value={label}>
<ListItemLayout
contentLeft={
<HStack gap="space.02">
{iconLeft}
<RadixSelect.ItemText>
<styled.span textStyle="label.02">{label}</styled.span>
</RadixSelect.ItemText>
<RadixSelect.ItemIndicator>
<CheckmarkIcon />
</RadixSelect.ItemIndicator>
</HStack>
}
contentRight={iconRight}
/>
</RadixSelect.Item>
);
}
12 changes: 12 additions & 0 deletions src/app/ui/components/select/select-section-label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as RadixSelect from '@radix-ui/react-select';

import { ListSectionLabel } from '../list/list-section-label';
import { listSectionLabelStyles } from '../list/styles';

export function SelectSectionLabel(props: { label: string }) {
return (
<RadixSelect.Label className={listSectionLabelStyles}>
<ListSectionLabel label={props.label} />
</RadixSelect.Label>
);
}
19 changes: 19 additions & 0 deletions src/app/ui/components/select/select-trigger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as RadixSelect from '@radix-ui/react-select';
import { css } from 'leather-styles/css';

import { defaultTriggerStyles } from '@app/ui/components/list/styles';

import { ChevronDownIcon } from '../icons/chevron-down-icon';

const selectTriggerStyles = css({ alignItems: 'center', display: 'flex', gap: 'space.02' });

export function SelectTrigger({ placeholder = 'Options' }: { placeholder?: string }) {
return (
<RadixSelect.Trigger className={`${defaultTriggerStyles} ${selectTriggerStyles}`}>
<RadixSelect.Value placeholder={placeholder} />
<RadixSelect.Icon>
<ChevronDownIcon />
</RadixSelect.Icon>
</RadixSelect.Trigger>
);
}
25 changes: 25 additions & 0 deletions src/app/ui/components/select/select.layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ReactNode } from 'react';

import * as RadixSelect from '@radix-ui/react-select';

import { listContentStyles } from '../list/styles';

interface SelectLayoutProps {
children: ReactNode;
trigger: ReactNode;
}
export function SelectLayout({ children, trigger }: SelectLayoutProps) {
return (
<RadixSelect.Root>
{trigger}
<RadixSelect.Content
align="start"
className={listContentStyles}
position="popper"
sideOffset={8}
>
<RadixSelect.Viewport>{children}</RadixSelect.Viewport>
</RadixSelect.Content>
</RadixSelect.Root>
);
}
12 changes: 12 additions & 0 deletions src/app/ui/components/select/select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ReactNode } from 'react';

import { SelectTrigger } from './select-trigger';
import { SelectLayout } from './select.layout';

interface SelectProps {
children: ReactNode;
trigger?: ReactNode;
}
export function Select({ children, trigger }: SelectProps) {
return <SelectLayout trigger={trigger ?? <SelectTrigger />}>{children}</SelectLayout>;
}
Loading

0 comments on commit d16eb6d

Please sign in to comment.