Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): TextInput updates #1942

Merged
merged 5 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: variant property on density is a nice feature

{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
Loading