From 6702e10a946e6dc3333c13d1fb8afa60b04d18cd Mon Sep 17 00:00:00 2001 From: Max Korsunov Date: Mon, 18 Nov 2024 17:14:24 +0400 Subject: [PATCH 1/6] feat(ui): add toggle UI component --- packages/ui/package.json | 1 + packages/ui/src/Toggle/index.stories.tsx | 28 +++++++++++++++++ packages/ui/src/Toggle/index.tsx | 39 ++++++++++++++++++++++++ pnpm-lock.yaml | 34 +++++++++++---------- 4 files changed, 86 insertions(+), 16 deletions(-) create mode 100644 packages/ui/src/Toggle/index.stories.tsx create mode 100644 packages/ui/src/Toggle/index.tsx diff --git a/packages/ui/package.json b/packages/ui/package.json index 48802c4eef..d242492f5a 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -44,6 +44,7 @@ "@radix-ui/react-progress": "^1.0.3", "@radix-ui/react-radio-group": "^1.2.0", "@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", diff --git a/packages/ui/src/Toggle/index.stories.tsx b/packages/ui/src/Toggle/index.stories.tsx new file mode 100644 index 0000000000..1b5fe665f2 --- /dev/null +++ b/packages/ui/src/Toggle/index.stories.tsx @@ -0,0 +1,28 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { useArgs } from '@storybook/preview-api'; + +import { Toggle } from '.'; + +const meta: Meta = { + component: Toggle, + tags: ['autodocs', '!dev', 'density'], +}; +export default meta; + +type Story = StoryObj; + +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 ; + }, +}; diff --git a/packages/ui/src/Toggle/index.tsx b/packages/ui/src/Toggle/index.tsx new file mode 100644 index 0000000000..9a844dbd70 --- /dev/null +++ b/packages/ui/src/Toggle/index.tsx @@ -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 ( + +
+ + ); +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5521af9d77..1edad52e22 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -647,6 +647,9 @@ importers: '@radix-ui/react-tabs': specifier: ^1.0.4 version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toggle': + specifier: ^1.0.3 + version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-tooltip': specifier: ^1.0.7 version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -918,7 +921,7 @@ importers: version: 8.1.11(react@18.3.1) '@storybook/addon-postcss': specifier: ^2.0.0 - version: 2.0.0(webpack@5.92.1(@swc/core@1.6.13(@swc/helpers@0.5.11))(esbuild@0.21.5)) + version: 2.0.0(webpack@5.92.1(@swc/core@1.6.13(@swc/helpers@0.5.11))) '@storybook/blocks': specifier: ^8.4.2 version: 8.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.2(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@5.0.10)) @@ -14732,13 +14735,13 @@ snapshots: storybook: 8.4.2(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@5.0.10) ts-dedent: 2.2.0 - '@storybook/addon-postcss@2.0.0(webpack@5.92.1(@swc/core@1.6.13(@swc/helpers@0.5.11))(esbuild@0.21.5))': + '@storybook/addon-postcss@2.0.0(webpack@5.92.1(@swc/core@1.6.13(@swc/helpers@0.5.11)))': dependencies: '@storybook/node-logger': 6.5.16 - css-loader: 3.6.0(webpack@5.92.1(@swc/core@1.6.13(@swc/helpers@0.5.11))(esbuild@0.21.5)) + css-loader: 3.6.0(webpack@5.92.1(@swc/core@1.6.13(@swc/helpers@0.5.11))) postcss: 7.0.39 - postcss-loader: 4.3.0(postcss@7.0.39)(webpack@5.92.1(@swc/core@1.6.13(@swc/helpers@0.5.11))(esbuild@0.21.5)) - style-loader: 1.3.0(webpack@5.92.1(@swc/core@1.6.13(@swc/helpers@0.5.11))(esbuild@0.21.5)) + postcss-loader: 4.3.0(postcss@7.0.39)(webpack@5.92.1(@swc/core@1.6.13(@swc/helpers@0.5.11))) + style-loader: 1.3.0(webpack@5.92.1(@swc/core@1.6.13(@swc/helpers@0.5.11))) transitivePeerDependencies: - webpack @@ -16990,7 +16993,7 @@ snapshots: css-color-keywords@1.0.0: {} - css-loader@3.6.0(webpack@5.92.1(@swc/core@1.6.13(@swc/helpers@0.5.11))(esbuild@0.21.5)): + css-loader@3.6.0(webpack@5.92.1(@swc/core@1.6.13(@swc/helpers@0.5.11))): dependencies: camelcase: 5.3.1 cssesc: 3.0.0 @@ -17005,7 +17008,7 @@ snapshots: postcss-value-parser: 4.2.0 schema-utils: 2.7.1 semver: 6.3.1 - webpack: 5.92.1(@swc/core@1.6.13(@swc/helpers@0.5.11))(esbuild@0.21.5) + webpack: 5.92.1(@swc/core@1.6.13(@swc/helpers@0.5.11)) css-to-react-native@3.2.0: dependencies: @@ -19417,7 +19420,7 @@ snapshots: postcss: 8.4.39 ts-node: 10.9.2(@swc/core@1.6.13(@swc/helpers@0.5.11))(@types/node@22.8.6)(typescript@5.5.3) - postcss-loader@4.3.0(postcss@7.0.39)(webpack@5.92.1(@swc/core@1.6.13(@swc/helpers@0.5.11))(esbuild@0.21.5)): + postcss-loader@4.3.0(postcss@7.0.39)(webpack@5.92.1(@swc/core@1.6.13(@swc/helpers@0.5.11))): dependencies: cosmiconfig: 7.1.0 klona: 2.0.6 @@ -19425,7 +19428,7 @@ snapshots: postcss: 7.0.39 schema-utils: 3.3.0 semver: 7.6.2 - webpack: 5.92.1(@swc/core@1.6.13(@swc/helpers@0.5.11))(esbuild@0.21.5) + webpack: 5.92.1(@swc/core@1.6.13(@swc/helpers@0.5.11)) postcss-modules-extract-imports@2.0.0: dependencies: @@ -20404,11 +20407,11 @@ snapshots: dependencies: js-tokens: 9.0.0 - style-loader@1.3.0(webpack@5.92.1(@swc/core@1.6.13(@swc/helpers@0.5.11))(esbuild@0.21.5)): + style-loader@1.3.0(webpack@5.92.1(@swc/core@1.6.13(@swc/helpers@0.5.11))): dependencies: loader-utils: 2.0.4 schema-utils: 2.7.1 - webpack: 5.92.1(@swc/core@1.6.13(@swc/helpers@0.5.11))(esbuild@0.21.5) + webpack: 5.92.1(@swc/core@1.6.13(@swc/helpers@0.5.11)) styled-components@6.1.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: @@ -20531,17 +20534,16 @@ snapshots: term-size@2.2.1: {} - terser-webpack-plugin@5.3.10(@swc/core@1.6.13(@swc/helpers@0.5.11))(esbuild@0.21.5)(webpack@5.92.1(@swc/core@1.6.13(@swc/helpers@0.5.11))(esbuild@0.21.5)): + terser-webpack-plugin@5.3.10(@swc/core@1.6.13(@swc/helpers@0.5.11))(webpack@5.92.1(@swc/core@1.6.13(@swc/helpers@0.5.11))): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 3.3.0 serialize-javascript: 6.0.2 terser: 5.36.0 - webpack: 5.92.1(@swc/core@1.6.13(@swc/helpers@0.5.11))(esbuild@0.21.5) + webpack: 5.92.1(@swc/core@1.6.13(@swc/helpers@0.5.11)) optionalDependencies: '@swc/core': 1.6.13(@swc/helpers@0.5.11) - esbuild: 0.21.5 terser@5.36.0: dependencies: @@ -21096,7 +21098,7 @@ snapshots: webpack-virtual-modules@0.6.2: {} - webpack@5.92.1(@swc/core@1.6.13(@swc/helpers@0.5.11))(esbuild@0.21.5): + webpack@5.92.1(@swc/core@1.6.13(@swc/helpers@0.5.11)): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.6 @@ -21119,7 +21121,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.10(@swc/core@1.6.13(@swc/helpers@0.5.11))(esbuild@0.21.5)(webpack@5.92.1(@swc/core@1.6.13(@swc/helpers@0.5.11))(esbuild@0.21.5)) + terser-webpack-plugin: 5.3.10(@swc/core@1.6.13(@swc/helpers@0.5.11))(webpack@5.92.1(@swc/core@1.6.13(@swc/helpers@0.5.11))) watchpack: 2.4.2 webpack-sources: 3.2.3 transitivePeerDependencies: From 8ee902b4efe3dd8969169f90775e0b87f1e19373 Mon Sep 17 00:00:00 2001 From: Max Korsunov Date: Mon, 18 Nov 2024 17:14:48 +0400 Subject: [PATCH 2/6] feat(ui): add `medium` density --- packages/ui/.storybook/preview.tsx | 51 +++++++++++++++------------- packages/ui/src/Density/index.tsx | 54 ++++++++++++++++++------------ packages/ui/src/utils/density.ts | 2 +- 3 files changed, 62 insertions(+), 45 deletions(-) diff --git a/packages/ui/.storybook/preview.tsx b/packages/ui/.storybook/preview.tsx index 77b9f12fa0..72e71ef66e 100644 --- a/packages/ui/.storybook/preview.tsx +++ b/packages/ui/.storybook/preview.tsx @@ -16,30 +16,35 @@ import '../src/theme/globals.css'; const DensityWrapper = ({ children, showDensityControl }) => { const [density, setDensity] = useState('sparse'); - return ( - {children}} - else={children => {children}} - > -
- {showDensityControl && ( - - - - )} - - {children} -
-
+ const densityTabs = ( +
+ {showDensityControl && ( + + + + )} + + {children} +
); + + if (density === 'medium') { + return {densityTabs}; + } + + if (density === 'compact') { + return {densityTabs}; + } + + return {densityTabs}; }; const preview: Preview = { diff --git a/packages/ui/src/Density/index.tsx b/packages/ui/src/Density/index.tsx index 452d389bb5..1b458b3ea1 100644 --- a/packages/ui/src/Density/index.tsx +++ b/packages/ui/src/Density/index.tsx @@ -1,11 +1,29 @@ import { ReactNode } from 'react'; import { Density as TDensity, DensityContext } from '../utils/density'; -export type DensityProps = { +/** + * Utility interface to be used below to ensure that only one density type is used at a time. + */ +interface NeverDensityTypes { + sparse?: never; + medium?: never; + compact?: never; +} + +export type DensityPropType = + | (Omit & { + sparse: true; + }) + | (Omit & { + medium: true; + }) + | (Omit & { + compact: true; + }); + +export type DensityProps = DensityPropType & { children?: ReactNode; -} & (SelectedDensity extends 'sparse' - ? { sparse: true; compact?: never } - : { compact: true; sparse?: never }); +}; /** * Use the `` component to set the density for all descendants in the @@ -21,9 +39,9 @@ export type DensityProps = { * 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 `` with `` - * or ``, and it will set a density context value for all - * descendant components: + * Instead, you can simply wrap the entire `
` with ``, + * `` or ``, and it will set a density context value + * for all descendant components: * * ```tsx * @@ -37,19 +55,15 @@ export type DensityProps = { * * ``` * - * 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 + * return
* } * ``` * @@ -72,11 +86,9 @@ export type DensityProps = { * /> * ``` */ -export const Density = ({ - children, - sparse, -}: DensityProps) => ( - - {children} - -); +export const Density = ({ children, sparse, medium, compact }: DensityProps) => { + const density: TDensity = + (sparse && 'sparse') ?? (medium && 'medium') ?? (compact && 'compact') ?? 'sparse'; + + return {children}; +}; diff --git a/packages/ui/src/utils/density.ts b/packages/ui/src/utils/density.ts index a5972aeb3b..c96f777614 100644 --- a/packages/ui/src/utils/density.ts +++ b/packages/ui/src/utils/density.ts @@ -8,7 +8,7 @@ import { createContext, useContext } from 'react'; * * See `` */ -export type Density = 'compact' | 'sparse'; +export type Density = 'compact' | 'sparse' | 'medium'; /** * This context is used internally by the `` component and the From 6c68693feac75e398a6b3e0944d673e3b3755d28 Mon Sep 17 00:00:00 2001 From: Max Korsunov Date: Mon, 18 Nov 2024 17:15:27 +0400 Subject: [PATCH 3/6] feat(ui): add new text style `xxs` --- packages/ui/src/Text/index.tsx | 4 ++++ packages/ui/src/Text/types.ts | 10 ++++++++++ packages/ui/src/theme/theme.ts | 2 ++ packages/ui/src/utils/typography.ts | 4 ++++ 4 files changed, 20 insertions(+) diff --git a/packages/ui/src/Text/index.tsx b/packages/ui/src/Text/index.tsx index b8b08fdc94..fa2bda3c41 100644 --- a/packages/ui/src/Text/index.tsx +++ b/packages/ui/src/Text/index.tsx @@ -15,6 +15,7 @@ import { xxl, p, getTextBase, + xxs, } from '../utils/typography'; import { ElementType, ReactNode } from 'react'; import { ThemeColor } from '../utils/color'; @@ -180,6 +181,9 @@ export const Text = (props: TextProps) => { if (props.detail) { return {props.children}; } + if (props.xxs) { + return {props.children}; + } if (props.small) { return {props.children}; } diff --git a/packages/ui/src/Text/types.ts b/packages/ui/src/Text/types.ts index a1ab764c5f..7ce5f8753c 100644 --- a/packages/ui/src/Text/types.ts +++ b/packages/ui/src/Text/types.ts @@ -12,6 +12,7 @@ interface NeverTextTypes { p?: never; strong?: never; detail?: never; + xxs?: never; small?: never; detailTechnical?: never; technical?: never; @@ -93,6 +94,15 @@ export type TextType = */ detail: true; }) + | (Omit & { + /** + * xxs text used for extra small bits of tertiary information. + * + * Renders a `` by default; pass the `as` prop to use a different + * HTML element with the same styling. + */ + xxs: true; + }) | (Omit & { /** * Small text used for secondary information. diff --git a/packages/ui/src/theme/theme.ts b/packages/ui/src/theme/theme.ts index 5370c6acff..d6672ed2c0 100644 --- a/packages/ui/src/theme/theme.ts +++ b/packages/ui/src/theme/theme.ts @@ -253,6 +253,7 @@ export const theme = { textBase: '1rem', textSm: '0.875rem', textXs: '0.75rem', + textXxs: '0.6875rem', }, lineHeight: { text9xl: '8.25rem', @@ -268,6 +269,7 @@ export const theme = { textBase: '1.5rem', textSm: '1.25rem', textXs: '1rem', + textXxs: '1rem', }, spacing, zIndex: { diff --git a/packages/ui/src/utils/typography.ts b/packages/ui/src/utils/typography.ts index 24534a1079..dbbcb679d2 100644 --- a/packages/ui/src/utils/typography.ts +++ b/packages/ui/src/utils/typography.ts @@ -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'); From fe770200b29e72be2d2ee23abd88ea82d71ed60d Mon Sep 17 00:00:00 2001 From: Max Korsunov Date: Mon, 18 Nov 2024 17:15:47 +0400 Subject: [PATCH 4/6] fix(ui): improve Tabs styles --- packages/ui/src/Tabs/index.tsx | 67 +++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/packages/ui/src/Tabs/index.tsx b/packages/ui/src/Tabs/index.tsx index f58afd9b78..31d4932a01 100644 --- a/packages/ui/src/Tabs/index.tsx +++ b/packages/ui/src/Tabs/index.tsx @@ -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; @@ -27,6 +26,23 @@ const getBorderColor = (actionType: LimitedActionType): string => { return cn('border-action-neutralFocusOutline'); }; +const getDensityClasses = (density: Density): string => { + if (density === 'compact') { + return cn('h-7'); + } + return cn('h-[44px]'); +}; + +const getDensityItemClasses = (density: Density): string => { + if (density === 'medium') { + return cn(tabMedium, 'grow shrink basis-0 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; @@ -64,12 +80,7 @@ export const Tabs = ({ value, onChange, options, actionType = 'default' }: TabsP return ( -
+
{options.map(option => ( 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 && ( -
- )} +
{option.label} From 7055f718b622ba4d3770cba5f0a1890209783ca1 Mon Sep 17 00:00:00 2001 From: Max Korsunov Date: Mon, 18 Nov 2024 17:17:07 +0400 Subject: [PATCH 5/6] chore: changeset --- .changeset/spicy-rings-happen.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/spicy-rings-happen.md diff --git a/.changeset/spicy-rings-happen.md b/.changeset/spicy-rings-happen.md new file mode 100644 index 0000000000..a200d27937 --- /dev/null +++ b/.changeset/spicy-rings-happen.md @@ -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 From a4e36a37a93b5489fb446af960c82c5883ccde14 Mon Sep 17 00:00:00 2001 From: Max Korsunov Date: Tue, 19 Nov 2024 14:14:58 +0400 Subject: [PATCH 6/6] fix: after review --- packages/ui/src/Density/index.tsx | 21 +++------------------ packages/ui/src/Tabs/index.tsx | 11 +++++++---- 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/packages/ui/src/Density/index.tsx b/packages/ui/src/Density/index.tsx index 1b458b3ea1..a6f0884ce5 100644 --- a/packages/ui/src/Density/index.tsx +++ b/packages/ui/src/Density/index.tsx @@ -1,25 +1,10 @@ import { ReactNode } from 'react'; import { Density as TDensity, DensityContext } from '../utils/density'; -/** - * Utility interface to be used below to ensure that only one density type is used at a time. - */ -interface NeverDensityTypes { - sparse?: never; - medium?: never; - compact?: never; -} - export type DensityPropType = - | (Omit & { - sparse: true; - }) - | (Omit & { - medium: true; - }) - | (Omit & { - compact: true; - }); + | { sparse: true; medium?: never; compact?: never } + | { medium: true; sparse?: never; compact?: never } + | { compact: true; sparse?: never; medium?: never }; export type DensityProps = DensityPropType & { children?: ReactNode; diff --git a/packages/ui/src/Tabs/index.tsx b/packages/ui/src/Tabs/index.tsx index 31d4932a01..82058b73cf 100644 --- a/packages/ui/src/Tabs/index.tsx +++ b/packages/ui/src/Tabs/index.tsx @@ -28,14 +28,17 @@ const getBorderColor = (actionType: LimitedActionType): string => { const getDensityClasses = (density: Density): string => { if (density === 'compact') { - return cn('h-7'); + return cn('h-7 gap-4'); } - return cn('h-[44px]'); + 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, 'grow shrink basis-0 p-2'); + return cn(tabMedium, 'p-2'); } if (density === 'compact') { return cn(tabSmall, 'py-1 px-2'); @@ -80,7 +83,7 @@ export const Tabs = ({ value, onChange, options, actionType = 'default' }: TabsP return ( -
+
{options.map(option => (