Skip to content

Commit

Permalink
feat(ui): toggle component and more (#1913)
Browse files Browse the repository at this point in the history
* feat(ui): add toggle UI component

* feat(ui): add `medium` density

* feat(ui): add new text style `xxs`

* fix(ui): improve Tabs styles

* chore: changeset

* fix: after review
  • Loading branch information
VanishMax authored Nov 19, 2024
1 parent fa39e46 commit bf2e541
Show file tree
Hide file tree
Showing 13 changed files with 187 additions and 74 deletions.
8 changes: 8 additions & 0 deletions .changeset/spicy-rings-happen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@penumbra-zone/ui': minor
---

- Add Toggle UI component
- Add `medium` density
- Add `xxs` Text style
- Improve the styles of `Tabs` component
51 changes: 28 additions & 23 deletions packages/ui/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,35 @@ import '../src/theme/globals.css';
const DensityWrapper = ({ children, showDensityControl }) => {
const [density, setDensity] = useState('sparse');

return (
<ConditionalWrap
if={density === 'sparse'}
then={children => <Density sparse>{children}</Density>}
else={children => <Density compact>{children}</Density>}
>
<div className='flex flex-col gap-4'>
{showDensityControl && (
<Density sparse>
<Tabs
options={[
{ label: 'Sparse', value: 'sparse' },
{ label: 'Compact', value: 'compact' },
]}
value={density}
onChange={setDensity}
/>
</Density>
)}

{children}
</div>
</ConditionalWrap>
const densityTabs = (
<div className='flex flex-col gap-4'>
{showDensityControl && (
<Density sparse>
<Tabs
options={[
{ label: 'Sparse', value: 'sparse' },
{ label: 'Medium', value: 'medium' },
{ label: 'Compact', value: 'compact' },
]}
value={density}
onChange={setDensity}
/>
</Density>
)}

{children}
</div>
);

if (density === 'medium') {
return <Density medium>{densityTabs}</Density>;
}

if (density === 'compact') {
return <Density compact>{densityTabs}</Density>;
}

return <Density sparse>{densityTabs}</Density>;
};

const preview: Preview = {
Expand Down
1 change: 1 addition & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@radix-ui/react-radio-group": "^1.2.0",
"@radix-ui/react-slider": "^1.1.2",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7",
"clsx": "^2.1.1",
"lucide-react": "^0.378.0",
Expand Down
39 changes: 18 additions & 21 deletions packages/ui/src/Density/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { ReactNode } from 'react';
import { Density as TDensity, DensityContext } from '../utils/density';

export type DensityProps<SelectedDensity extends TDensity> = {
export type DensityPropType =
| { sparse: true; medium?: never; compact?: never }
| { medium: true; sparse?: never; compact?: never }
| { compact: true; sparse?: never; medium?: never };

export type DensityProps = DensityPropType & {
children?: ReactNode;
} & (SelectedDensity extends 'sparse'
? { sparse: true; compact?: never }
: { compact: true; sparse?: never });
};

/**
* Use the `<Density />` component to set the density for all descendants in the
Expand All @@ -21,9 +24,9 @@ export type DensityProps<SelectedDensity extends TDensity> = {
* which contain nested components with density variants. If we used a `density`
* prop, you'd need to set that prop on every single component in that tree.
*
* Instead, you can simply wrap the entire `<Table />` with `<Density sparse />`
* or `<Density compact />`, and it will set a density context value for all
* descendant components:
* Instead, you can simply wrap the entire `<Table />` with `<Density sparse />`,
* `<Density medium />` or `<Density compact />`, and it will set a density context value
* for all descendant components:
*
* ```tsx
* <Density compact>
Expand All @@ -37,19 +40,15 @@ export type DensityProps<SelectedDensity extends TDensity> = {
* </Density>
* ```
*
* Components that support density variants are recognizable because the use the
* Components that support density variants are recognizable because they use the
* `useDensity()` hook, and then style their elements based on the value they
* receive from that hook:
*
* ```tsx
* const SomeStyledComponent = styled.div<{ $density: Density }>`
* padding: ${props => props.theme.spacing(props.$density === 'sparse' ? 4 : 2)};
* `
*
* const MyComponent = () => {
* const density = useDensity();
*
* return <SomeStyledComponent $density={density} />
* return <div className={density === 'sparse' ? 'p-4' : 'p-1' } />
* }
* ```
*
Expand All @@ -72,11 +71,9 @@ export type DensityProps<SelectedDensity extends TDensity> = {
* />
* ```
*/
export const Density = <SelectedDensity extends TDensity>({
children,
sparse,
}: DensityProps<SelectedDensity>) => (
<DensityContext.Provider value={sparse ? 'sparse' : 'compact'}>
{children}
</DensityContext.Provider>
);
export const Density = ({ children, sparse, medium, compact }: DensityProps) => {
const density: TDensity =
(sparse && 'sparse') ?? (medium && 'medium') ?? (compact && 'compact') ?? 'sparse';

return <DensityContext.Provider value={density}>{children}</DensityContext.Provider>;
};
70 changes: 41 additions & 29 deletions packages/ui/src/Tabs/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { tab, tabSmall } from '../utils/typography';
import { buttonBase, getOverlays } from '../utils/button';
import * as RadixTabs from '@radix-ui/react-tabs';
import { ActionType } from '../utils/action-type';
import { useDensity } from '../utils/density';
import cn from 'clsx';
import { tab, tabMedium, tabSmall } from '../utils/typography';
import { ActionType, getFocusOutlineColorByActionType } from '../utils/action-type';
import { Density, useDensity } from '../utils/density';

type LimitedActionType = Exclude<ActionType, 'destructive'>;

Expand All @@ -27,6 +26,26 @@ const getBorderColor = (actionType: LimitedActionType): string => {
return cn('border-action-neutralFocusOutline');
};

const getDensityClasses = (density: Density): string => {
if (density === 'compact') {
return cn('h-7 gap-4');
}
if (density === 'medium') {
return cn('h-[44px] gap-2');
}
return cn('h-[44px] gap-4');
};

const getDensityItemClasses = (density: Density): string => {
if (density === 'medium') {
return cn(tabMedium, 'p-2');
}
if (density === 'compact') {
return cn(tabSmall, 'py-1 px-2');
}
return cn(tab, 'grow shrink basis-0 p-2');
};

export interface TabsTab {
value: string;
label: string;
Expand Down Expand Up @@ -64,12 +83,7 @@ export const Tabs = ({ value, onChange, options, actionType = 'default' }: TabsP
return (
<RadixTabs.Root value={value} onValueChange={onChange}>
<RadixTabs.List asChild>
<div
className={cn(
'flex items-stretch box-border gap-4',
density === 'sparse' ? 'h-[44px]' : 'h-7',
)}
>
<div className={cn(getDensityClasses(density), 'flex items-stretch box-border')}>
{options.map(option => (
<RadixTabs.Trigger
value={option.value}
Expand All @@ -81,27 +95,25 @@ export const Tabs = ({ value, onChange, options, actionType = 'default' }: TabsP
onClick={() => onChange(option.value)}
disabled={option.disabled}
className={cn(
buttonBase,
getOverlays({ actionType, density }),
'h-full relative whitespace-nowrap text-text-primary',
density === 'sparse'
? cn(tab, 'grow shrink basis-0 p-2')
: cn(tabSmall, 'py-1 px-2'),
'before:rounded-tl-xs before:rounded-tr-xs before:rounded-bl-none before:rounded-br-none',
'focus-within:outline-none',
'after:inset-[2px]',
'appearance-none border-none text-inherit cursor-pointer',
'h-full relative whitespace-nowrap rounded-t-xs',
'transition-[background-color,outline-color,color] duration-150',
value === option.value ? 'text-text-primary' : 'text-text-secondary',
getDensityItemClasses(density),
getFocusOutlineColorByActionType(actionType),
'focus:outline focus:outline-2',
'hover:bg-action-hoverOverlay',
)}
>
{value === option.value && (
<div
className={cn(
'absolute inset-0 -z-[1]',
'border-b-2 border-solid',
getIndicatorColor(actionType),
getBorderColor(actionType),
)}
/>
)}
<div
className={cn(
value === option.value ? 'opacity-100' : 'opacity-0',
'absolute inset-0 transition-opacity pointer-events-none',
'border-b-2 border-solid',
getIndicatorColor(actionType),
getBorderColor(actionType),
)}
/>
{option.label}
</button>
</RadixTabs.Trigger>
Expand Down
4 changes: 4 additions & 0 deletions packages/ui/src/Text/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
xxl,
p,
getTextBase,
xxs,
} from '../utils/typography';
import { ElementType, ReactNode } from 'react';
import { ThemeColor } from '../utils/color';
Expand Down Expand Up @@ -180,6 +181,9 @@ export const Text = (props: TextProps) => {
if (props.detail) {
return <SpanElement className={cn(detail, classes)}>{props.children}</SpanElement>;
}
if (props.xxs) {
return <SpanElement className={cn(xxs, classes)}>{props.children}</SpanElement>;
}
if (props.small) {
return <SpanElement className={cn(small, classes)}>{props.children}</SpanElement>;
}
Expand Down
10 changes: 10 additions & 0 deletions packages/ui/src/Text/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface NeverTextTypes {
p?: never;
strong?: never;
detail?: never;
xxs?: never;
small?: never;
detailTechnical?: never;
technical?: never;
Expand Down Expand Up @@ -93,6 +94,15 @@ export type TextType =
*/
detail: true;
})
| (Omit<NeverTextTypes, 'xxs'> & {
/**
* xxs text used for extra small bits of tertiary information.
*
* Renders a `<span />` by default; pass the `as` prop to use a different
* HTML element with the same styling.
*/
xxs: true;
})
| (Omit<NeverTextTypes, 'small'> & {
/**
* Small text used for secondary information.
Expand Down
28 changes: 28 additions & 0 deletions packages/ui/src/Toggle/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Meta, StoryObj } from '@storybook/react';
import { useArgs } from '@storybook/preview-api';

import { Toggle } from '.';

const meta: Meta<typeof Toggle> = {
component: Toggle,
tags: ['autodocs', '!dev', 'density'],
};
export default meta;

type Story = StoryObj<typeof Toggle>;

export const Basic: Story = {
args: {
value: false,
label: 'Label',
disabled: false,
},

render: function Render(props) {
const [, updateArgs] = useArgs();

const onChange = (value: boolean) => updateArgs({ value });

return <Toggle {...props} onChange={onChange} />;
},
};
39 changes: 39 additions & 0 deletions packages/ui/src/Toggle/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import cn from 'clsx';
import * as RadixToggle from '@radix-ui/react-toggle';
import { useDisabled } from '../utils/disabled-context';
import { useDensity } from '../utils/density';

export interface ToggleProps {
/** An accessibility label. */
label: string;
value: boolean;
onChange: (value: boolean) => void;
/** @todo: Implement disabled state visually. */
disabled?: boolean;
}

export const Toggle = ({ label, value, onChange, disabled }: ToggleProps) => {
const density = useDensity();

return (
<RadixToggle.Root
aria-label={label}
pressed={value}
onPressedChange={onChange}
disabled={useDisabled(disabled)}
className={cn(
'border border-solid border-other-tonalStroke rounded-full transition-colors cursor-pointer',
value ? 'bg-primary-main' : 'bg-base-transparent',
density === 'sparse' ? 'w-12' : 'w-8',
)}
>
<div
className={cn(
'rounded-full transition-all',
value ? 'bg-primary-contrast translate-x-[90%]' : 'bg-neutral-light translate-x-0',
density === 'sparse' ? 'size-6' : 'size-4',
)}
/>
</RadixToggle.Root>
);
};
2 changes: 2 additions & 0 deletions packages/ui/src/theme/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ export const theme = {
textBase: '1rem',
textSm: '0.875rem',
textXs: '0.75rem',
textXxs: '0.6875rem',
},
lineHeight: {
text9xl: '8.25rem',
Expand All @@ -268,6 +269,7 @@ export const theme = {
textBase: '1.5rem',
textSm: '1.25rem',
textXs: '1rem',
textXxs: '1rem',
},
spacing,
zIndex: {
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/utils/density.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { createContext, useContext } from 'react';
*
* See `<DensityContext />`
*/
export type Density = 'compact' | 'sparse';
export type Density = 'compact' | 'sparse' | 'medium';

/**
* This context is used internally by the `<Density />` component and the
Expand Down
4 changes: 4 additions & 0 deletions packages/ui/src/utils/typography.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,14 @@ export const detailTechnical = cn('font-mono text-textXs font-normal leading-tex

export const small = cn('font-default text-textSm font-normal leading-textXs');

export const xxs = cn('font-default text-textXxs font-normal leading-textXxs');

export const tab = cn('font-default text-textLg font-normal leading-textLg');

export const tabSmall = cn('font-default text-textSm font-medium leading-textSm');

export const tabMedium = cn('font-default text-textSm font-medium leading-textLg');

export const tableItem = cn('font-default text-textBase font-normal leading-textBase');

export const tableHeading = cn('font-default text-textBase font-medium leading-textBase');
Expand Down
Loading

0 comments on commit bf2e541

Please sign in to comment.