Skip to content

Commit

Permalink
feat(ui): TextInput updates (#1942)
Browse files Browse the repository at this point in the history
* feat: update TextInput styles

* feat: add `variant` property to Text component, simplify code

* feat(ui): add `variant` prop to Density component

* fix(ui): match AddressView with the latest styles

* chore: changeset
  • Loading branch information
VanishMax authored Dec 11, 2024
1 parent 3709889 commit 7969f77
Show file tree
Hide file tree
Showing 10 changed files with 172 additions and 248 deletions.
5 changes: 5 additions & 0 deletions .changeset/slow-crabs-whisper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@penumbra-zone/ui': minor
---

Sync TextInput with the latest designs
39 changes: 10 additions & 29 deletions packages/ui/src/AddressView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { bech32mAddress } from '@penumbra-zone/bech32m/penumbra';
import { CopyToClipboardButton } from '../CopyToClipboardButton';
import { AddressIcon } from './AddressIcon';
import { Text } from '../Text';
import { Density, useDensity } from '../utils/density';
import { useDensity } from '../utils/density';
import { Density } from '../Density';

export interface AddressViewProps {
addressView: AddressView | undefined;
Expand All @@ -13,16 +14,6 @@ export interface AddressViewProps {
truncate?: boolean;
}

export const getIconSize = (density: Density): number => {
if (density === 'compact') {
return 16;
}
if (density === 'slim') {
return 12;
}
return 24;
};

// Renders an address or an address view.
// If the view is given and is "visible", the account information will be displayed instead.
export const AddressViewComponent = ({
Expand Down Expand Up @@ -54,39 +45,29 @@ export const AddressViewComponent = ({
<div className='shrink'>
<AddressIcon
address={addressView.addressView.value.address}
size={getIconSize(density)}
size={density === 'sparse' ? 24 : 16}
/>
</div>
)}

<div className={truncate ? 'max-w-[150px] truncate' : ''}>
{/* eslint-disable-next-line no-nested-ternary -- can alternatively use dynamic prop object like {...fontProps} */}
{addressIndex ? (
density === 'sparse' ? (
<Text strong-bold truncate={truncate}>
{isRandomized && 'IBC Deposit Address for '}
{getAccountLabel(addressIndex.account)}
</Text>
) : (
<Text small truncate={truncate}>
{isRandomized && 'IBC Deposit Address for '}
{getAccountLabel(addressIndex.account)}
</Text>
)
) : density === 'sparse' ? (
<Text strong-bold truncate={truncate}>
{encodedAddress}
<Text variant={density === 'sparse' ? 'strong' : 'small'} truncate={truncate}>
{isRandomized && 'IBC Deposit Address for '}
{getAccountLabel(addressIndex.account)}
</Text>
) : (
<Text small truncate={truncate}>
<Text variant={density === 'sparse' ? 'strong' : 'small'} truncate={truncate}>
{encodedAddress}
</Text>
)}
</div>

{copyable && !isRandomized && (
<div className='shrink'>
<CopyToClipboardButton text={encodedAddress} />
<Density variant={density === 'sparse' ? 'compact' : 'slim'}>
<CopyToClipboardButton text={encodedAddress} />
</Density>
</div>
)}
</div>
Expand Down
24 changes: 18 additions & 6 deletions packages/ui/src/Density/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { ReactNode } from 'react';
import { Density as TDensity, DensityContext } from '../utils/density';

export type DensityPropType =
| { sparse: true; slim?: never; compact?: never }
| { slim: true; sparse?: never; compact?: never }
| { compact: true; sparse?: never; slim?: never };
type DensityType = {
[K in TDensity]: Record<K, true> & Partial<Record<Exclude<TDensity, K>, never>>;
}[TDensity];

type DensityPropType =
| (DensityType & { variant?: never })
| (Partial<Record<TDensity, never>> & {
/** dynamic density variant as a string: `'sparse' | 'compact' | 'slim'` */
variant?: TDensity;
});

export type DensityProps = DensityPropType & {
children?: ReactNode;
Expand Down Expand Up @@ -70,10 +76,16 @@ export type DensityProps = DensityPropType & {
* }
* />
* ```
*
* If you need to change density dynamically, you can use the `variant` property.
*
* ```tsx
* <Density variant={isDense ? 'compact' : 'sparse'} />
* ```
*/
export const Density = ({ children, sparse, slim, compact }: DensityProps) => {
export const Density = ({ children, sparse, slim, compact, variant }: DensityProps) => {
const density: TDensity =
(sparse && 'sparse') ?? (compact && 'compact') ?? (slim && 'slim') ?? 'sparse';
variant ?? (sparse && 'sparse') ?? (compact && 'compact') ?? (slim && 'slim') ?? 'sparse';

return <DensityContext.Provider value={density}>{children}</DensityContext.Provider>;
};
9 changes: 6 additions & 3 deletions packages/ui/src/Text/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react';

import { Text } from '.';
import { useArgs } from '@storybook/preview-api';
import { TextVariant } from './types';

const meta: Meta<typeof Text> = {
component: Text,
Expand Down Expand Up @@ -41,7 +42,7 @@ const OPTIONS = [
'small',
'technical',
'detailTechnical',
] as const;
] as TextVariant[];

const Option = ({
value,
Expand All @@ -57,7 +58,6 @@ const Option = ({
type='radio'
name='textStyle'
value={value}
defaultChecked={checked}
checked={checked}
onChange={() => onSelect(value)}
/>
Expand Down Expand Up @@ -86,12 +86,15 @@ export const KitchenSink: StoryObj<typeof Text> = {
),
);

const isChecked = (option: TextVariant): boolean =>
Object.keys(props).some(key => key === option);

return (
<form className='flex flex-col gap-2 text-text-primary'>
<div className='flex items-center gap-2'>
<Text>Text style:</Text>
{OPTIONS.map(option => (
<Option key={option} value={option} checked={!!props[option]} onSelect={onSelect} />
<Option key={option} value={option} checked={isChecked(option)} onSelect={onSelect} />
))}
</div>

Expand Down
82 changes: 32 additions & 50 deletions packages/ui/src/Text/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ import {
} from '../utils/typography';
import { ElementType, ReactNode } from 'react';
import { ThemeColor } from '../utils/color';
import { TextType } from './types';
import { TextVariant, TypographyProps } from './types';

export type TextProps = TextType & {
export type TextProps = TypographyProps & {
children?: ReactNode;
/**
* Which component or HTML element to render this text as.
Expand Down Expand Up @@ -120,6 +120,23 @@ const getTextOptionClasses = ({
);
};

const VARIANT_MAP: Record<TextVariant, { element: ElementType; classes: string }> = {
h1: { element: 'h1', classes: h1 },
h2: { element: 'h2', classes: h2 },
h3: { element: 'h3', classes: h3 },
h4: { element: 'h4', classes: h4 },
xxl: { element: 'span', classes: xxl },
large: { element: 'span', classes: large },
p: { element: 'p', classes: p },
strong: { element: 'span', classes: strong },
detail: { element: 'span', classes: detail },
xxs: { element: 'span', classes: xxs },
small: { element: 'span', classes: small },
detailTechnical: { element: 'span', classes: detailTechnical },
technical: { element: 'span', classes: technical },
body: { element: 'span', classes: body },
};

/**
* All-purpose text wrapper for quickly styling text per the Penumbra UI
* guidelines.
Expand Down Expand Up @@ -147,57 +164,22 @@ const getTextOptionClasses = ({
* This will render with the h1 style, but inside an inline span tag.
* </Text>
* ```
*
* If you need to use dynamic Text styles, use `variant` property with a string value.
* However, it is recommended to use the static Text styles for most cases:
*
* ```tsx
* <Text variant={emphasized ? 'strong' : 'body'}>Content</Text>
* ```
*/
export const Text = (props: TextProps) => {
const classes = getTextOptionClasses(props);
const SpanElement = props.as ?? 'span';

if (props.h1) {
const Element = props.as ?? 'h1';
return <Element className={cn(h1, classes)}>{props.children}</Element>;
}
if (props.h2) {
const Element = props.as ?? 'h2';
return <Element className={cn(h2, classes)}>{props.children}</Element>;
}
if (props.h3) {
const Element = props.as ?? 'h3';
return <Element className={cn(h3, classes)}>{props.children}</Element>;
}
if (props.h4) {
const Element = props.as ?? 'h4';
return <Element className={cn(h4, classes)}>{props.children}</Element>;
}

if (props.xxl) {
return <SpanElement className={cn(xxl, classes)}>{props.children}</SpanElement>;
}
if (props.large) {
return <SpanElement className={cn(large, classes)}>{props.children}</SpanElement>;
}
if (props.strong) {
return <SpanElement className={cn(strong, classes)}>{props.children}</SpanElement>;
}
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>;
}
if (props.detailTechnical) {
return <SpanElement className={cn(detailTechnical, classes)}>{props.children}</SpanElement>;
}
if (props.technical) {
return <SpanElement className={cn(technical, classes)}>{props.children}</SpanElement>;
}

if (props.p) {
const Element = props.as ?? 'p';
return <Element className={cn(p, classes)}>{props.children}</Element>;
}
const variantKey: TextVariant =
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- the default fallback is necessary
(Object.keys(props).find(key => VARIANT_MAP[key as TextVariant]) as TextVariant) ?? 'body';
const variant = VARIANT_MAP[variantKey];
const Element = props.as ?? variant.element;

return <SpanElement className={cn(body, classes)}>{props.children}</SpanElement>;
return <Element className={cn(variant.classes, classes)}>{props.children}</Element>;
};
Loading

0 comments on commit 7969f77

Please sign in to comment.