Skip to content

Commit

Permalink
Create more components in preparation for the v2 assets/transactions …
Browse files Browse the repository at this point in the history
…pages (#1634)

* Add table title functionality

* Start building v2 pages

* Build out Identicon properly

* Start building out assets table

* Add truncate to Text

* Add iconOnly='adornment' to Button

* Create the new CopyToClipboardButton

* Create Shrink0 util component

* Create AddressViewComponent

* Create initial SegmentedControl

* Convert to using Radix's RadioGroup

* Add docblock

* Update hover overlay

* Fix border

* Rename 'reset' to 'buttonBase'; add a cursor

* Use buttonBase elsewhere as well

* Add type to props

* Update AddressViewComponent tests

* Delete code that will be part of another PR

* Add tests

* Delete SegmentedControl for now

* Return null

* Create SegmentedControl component (#1635)

* Create SegmentedControl component

* Add density tag
  • Loading branch information
jessepinho authored Aug 5, 2024
1 parent 82383bf commit 33f70a1
Show file tree
Hide file tree
Showing 29 changed files with 807 additions and 73 deletions.
2 changes: 2 additions & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"@radix-ui/react-navigation-menu": "^1.1.4",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-radio-group": "^1.2.0",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slider": "^1.1.2",
"@radix-ui/react-slot": "^1.0.2",
Expand Down Expand Up @@ -84,6 +85,7 @@
"@storybook/react": "^8.1.1",
"@storybook/react-vite": "8.1.1",
"@storybook/theming": "^8.1.11",
"@testing-library/user-event": "^14.5.2",
"@types/humanize-duration": "^3.27.4",
"@types/murmurhash3js": "^3.0.7",
"@types/react": "^18.3.2",
Expand Down
15 changes: 15 additions & 0 deletions packages/ui/src/AddressViewComponent/AddressIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Address } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb.js';
import { bech32mAddress } from '@penumbra-zone/bech32m/penumbra';
import { Identicon } from '../Identicon';

export interface AddressIconProps {
address: Address;
size: number;
}

/**
* A simple component to display a consistently styled icon for a given address.
*/
export const AddressIcon = ({ address, size }: AddressIconProps) => (
<Identicon uniqueIdentifier={bech32mAddress(address)} size={size} type='gradient' />
);
67 changes: 67 additions & 0 deletions packages/ui/src/AddressViewComponent/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import type { Meta, StoryObj } from '@storybook/react';

import { AddressViewComponent } from '.';
import { AddressView } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb.js';
import { addressFromBech32m } from '@penumbra-zone/bech32m/penumbra';
import styled from 'styled-components';

const EXAMPLE_VIEW_DECODED = new AddressView({
addressView: {
case: 'decoded',

value: {
address: { inner: new Uint8Array(80) },
index: {
account: 0,
randomizer: new Uint8Array([0, 0, 0]),
},
},
},
});

const EXAMPLE_VIEW_OPAQUE = new AddressView({
addressView: {
case: 'opaque',
value: {
address: addressFromBech32m(
'penumbra1e8k5cyds484dxvapeamwveh5khqv4jsvyvaf5wwxaaccgfghm229qw03pcar3ryy8smptevstycch0qk3uu0rgkvtjpxy3cu3rjd0agawqtlz6erev28a6sg69u7cxy0t02nd4',
),
},
},
});

const MaxWidthWrapper = styled.div`
width: 100%;
overflow: hidden;
`;

const meta: Meta<typeof AddressViewComponent> = {
component: AddressViewComponent,
tags: ['autodocs', '!dev'],
argTypes: {
addressView: {
options: ['Sample decoded address view', 'Sample opaque address view'],
mapping: {
'Sample decoded address view': EXAMPLE_VIEW_DECODED,
'Sample opaque address view': EXAMPLE_VIEW_OPAQUE,
},
},
},
decorators: [
Story => (
<MaxWidthWrapper>
<Story />
</MaxWidthWrapper>
),
],
};
export default meta;

type Story = StoryObj<typeof AddressViewComponent>;

export const Basic: Story = {
args: {
addressView: EXAMPLE_VIEW_DECODED,
copyable: true,
},
};
82 changes: 82 additions & 0 deletions packages/ui/src/AddressViewComponent/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {
Address,
AddressIndex,
AddressView,
AddressView_Decoded,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb.js';
import { AddressViewComponent } from '.';
import { describe, expect, it } from 'vitest';
import { render } from '@testing-library/react';
import { PenumbraUIProvider } from '../PenumbraUIProvider';

const addressViewWithOneTimeAddress = new AddressView({
addressView: {
case: 'decoded',

value: new AddressView_Decoded({
address: new Address({ inner: new Uint8Array(80) }),
index: new AddressIndex({
account: 0,
// A one-time address is defined by a randomizer with at least one
// non-zero byte.
randomizer: new Uint8Array([1, 2, 3]),
}),
}),
},
});

const addressViewWithNormalAddress = new AddressView({
addressView: {
case: 'decoded',

value: new AddressView_Decoded({
address: new Address({ inner: new Uint8Array(80) }),
index: new AddressIndex({
account: 0,
randomizer: new Uint8Array([0, 0, 0]),
}),
}),
},
});

describe('<AddressViewComponent />', () => {
describe('when `copyable` is `true`', () => {
it('does not show the copy icon when the address is a one-time address', () => {
const { queryByLabelText } = render(
<AddressViewComponent addressView={addressViewWithOneTimeAddress} copyable />,
{ wrapper: PenumbraUIProvider },
);

expect(queryByLabelText('Copy')).toBeNull();
});

it('shows the copy icon when the address is not a one-time address', () => {
const { queryByLabelText } = render(
<AddressViewComponent addressView={addressViewWithNormalAddress} copyable />,
{ wrapper: PenumbraUIProvider },
);

expect(queryByLabelText('Copy')).not.toBeNull();
});
});

describe('when `copyable` is `false`', () => {
it('does not show the copy icon when the address is a one-time address', () => {
const { queryByLabelText } = render(
<AddressViewComponent addressView={addressViewWithOneTimeAddress} copyable={false} />,
{ wrapper: PenumbraUIProvider },
);

expect(queryByLabelText('Copy')).toBeNull();
});

it('does not show the copy icon when the address is not a one-time address', () => {
const { queryByLabelText } = render(
<AddressViewComponent addressView={addressViewWithNormalAddress} copyable={false} />,
{ wrapper: PenumbraUIProvider },
);

expect(queryByLabelText('Copy')).toBeNull();
});
});
});
66 changes: 66 additions & 0 deletions packages/ui/src/AddressViewComponent/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { AddressIcon } from './AddressIcon';
import { AddressView } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb.js';
import { bech32mAddress } from '@penumbra-zone/bech32m/penumbra';
import styled from 'styled-components';
import { Text } from '../Text';
import { CopyToClipboardButton } from '../CopyToClipboardButton';
import { Shrink0 } from '../utils/Shrink0';

const Root = styled.div`
display: flex;
gap: ${props => props.theme.spacing(2)};
align-items: center;
`;

export interface AddressViewProps {
addressView: AddressView | undefined;
copyable?: boolean;
}

// 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 = ({ addressView, copyable = true }: AddressViewProps) => {
if (!addressView?.addressView.value?.address) {
return null;
}

const accountIndex =
addressView.addressView.case === 'decoded'
? addressView.addressView.value.index?.account
: undefined;
const isOneTimeAddress =
addressView.addressView.case === 'decoded'
? !addressView.addressView.value.index?.randomizer.every(v => v === 0) // Randomized (and thus, a one-time address) if the randomizer is not all zeros.
: undefined;

const addressIndexLabel = isOneTimeAddress ? 'IBC Deposit Address for Account #' : 'Account #';

copyable = isOneTimeAddress ? false : copyable;

const encodedAddress = bech32mAddress(addressView.addressView.value.address);

return (
<Root>
<Shrink0>
<AddressIcon address={addressView.addressView.value.address} size={24} />
</Shrink0>

{accountIndex === undefined ? (
<Text technical truncate>
{encodedAddress}
</Text>
) : (
<Text strong truncate>
{addressIndexLabel}
{accountIndex}
</Text>
)}

{copyable && (
<Shrink0>
<CopyToClipboardButton text={encodedAddress} />
</Shrink0>
)}
</Root>
);
};
12 changes: 12 additions & 0 deletions packages/ui/src/Button/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,23 @@ describe('getBackgroundColor()', () => {
it('returns the corresponding color for other action types', () => {
expect(getBackgroundColor('destructive', 'primary', theme)).toBe('#f00');
});

describe('when `iconOnly` is `true`', () => {
it('returns the primary color for the `accent` action type', () => {
expect(getBackgroundColor('accent', 'primary', theme, true)).toBe('#aaa');
});
});
});

describe('when `priority` is `secondary`', () => {
it('returns `transparent`', () => {
expect(getBackgroundColor('accent', 'secondary', theme)).toBe('transparent');
});
});

describe('when `iconOnly` is `adornment`', () => {
it('returns `transparent`', () => {
expect(getBackgroundColor('accent', 'primary', theme, 'adornment')).toBe('transparent');
});
});
});
3 changes: 2 additions & 1 deletion packages/ui/src/Button/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ export const getBackgroundColor = (
actionType: ActionType,
priority: Priority,
theme: DefaultTheme,
iconOnly?: boolean | 'adornment',
): string => {
if (priority === 'secondary') {
if (priority === 'secondary' || iconOnly === 'adornment') {
return 'transparent';
}

Expand Down
5 changes: 5 additions & 0 deletions packages/ui/src/Button/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ const meta: Meta<typeof Button> = {
options: ['None', 'Check', 'ArrowLeftRight'],
mapping: { None: undefined, Check, ArrowLeftRight },
},
iconOnly: {
options: ['true', 'false', 'adornment'],
mapping: { true: true, false: false, adornment: 'adornment' },
},
onClick: { control: false },
},
};
Expand All @@ -26,5 +30,6 @@ export const Basic: Story = {
disabled: false,
icon: Check,
iconOnly: false,
type: 'button',
},
};
Loading

0 comments on commit 33f70a1

Please sign in to comment.