diff --git a/apps/minifront/src/components/root-router.tsx b/apps/minifront/src/components/root-router.tsx
index 7fb689d21e..ad16580bf9 100644
--- a/apps/minifront/src/components/root-router.tsx
+++ b/apps/minifront/src/components/root-router.tsx
@@ -14,8 +14,10 @@ import { StakingLayout } from './staking/layout';
import { IbcLayout } from './ibc/layout';
import { abortLoader } from '../abort-loader';
import type { Router } from '@remix-run/router';
+import { routes as v2Routes } from './v2/root-router';
export const rootRouter: Router = createHashRouter([
+ ...v2Routes,
{
path: '/',
element: ,
diff --git a/apps/minifront/src/components/v2/dashboard-layout/assets-card-title.tsx b/apps/minifront/src/components/v2/dashboard-layout/assets-card-title.tsx
new file mode 100644
index 0000000000..874d8d8923
--- /dev/null
+++ b/apps/minifront/src/components/v2/dashboard-layout/assets-card-title.tsx
@@ -0,0 +1,24 @@
+import { Button } from '@repo/ui/Button';
+import { Dialog } from '@repo/ui/Dialog';
+import { Text } from '@repo/ui/Text';
+import { Info } from 'lucide-react';
+
+export const AssetsCardTitle = () => (
+
+ Asset Balances
+
+
+);
diff --git a/apps/minifront/src/components/v2/dashboard-layout/assets-page/equivalent-values.tsx b/apps/minifront/src/components/v2/dashboard-layout/assets-page/equivalent-values.tsx
new file mode 100644
index 0000000000..8a752ce786
--- /dev/null
+++ b/apps/minifront/src/components/v2/dashboard-layout/assets-page/equivalent-values.tsx
@@ -0,0 +1,23 @@
+import { ValueView } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb.js';
+import { asValueView } from '@penumbra-zone/getters/equivalent-value';
+import { getDisplayDenomFromView, getEquivalentValues } from '@penumbra-zone/getters/value-view';
+import { ValueViewComponent } from '@repo/ui/ValueViewComponent';
+
+export const EquivalentValues = ({ valueView }: { valueView?: ValueView }) => {
+ const equivalentValuesAsValueViews = (getEquivalentValues.optional()(valueView) ?? []).map(
+ asValueView,
+ );
+
+ return (
+
+ {equivalentValuesAsValueViews.map(equivalentValueAsValueView => (
+
+ ))}
+
+ );
+};
diff --git a/apps/minifront/src/components/v2/dashboard-layout/assets-page/index.tsx b/apps/minifront/src/components/v2/dashboard-layout/assets-page/index.tsx
new file mode 100644
index 0000000000..e9f3122cad
--- /dev/null
+++ b/apps/minifront/src/components/v2/dashboard-layout/assets-page/index.tsx
@@ -0,0 +1,76 @@
+import { Density } from '@repo/ui/Density';
+import { Table } from '@repo/ui/Table';
+import { BalancesByAccount, groupByAccount, useBalancesResponses } from '../../../../state/shared';
+import { shouldDisplay } from '../../../../fetchers/balances/should-display';
+import { sortByPriorityScore } from '../../../../fetchers/balances/by-priority-score';
+import { BalancesResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb';
+import { getMetadataFromBalancesResponseOptional } from '@penumbra-zone/getters/balances-response';
+import { PagePath } from '../../../metadata/paths';
+import { getAddressIndex } from '@penumbra-zone/getters/address-view';
+import { AbridgedZQueryState } from '@penumbra-zone/zquery/src/types';
+import { ValueViewComponent } from '@repo/ui/ValueViewComponent';
+import { EquivalentValues } from './equivalent-values';
+import { TableTitle } from './table-title';
+import { Link } from 'react-router-dom';
+import { Button } from '@repo/ui/Button';
+import { ArrowRightLeft } from 'lucide-react';
+
+const getTradeLink = (balance: BalancesResponse): string => {
+ const metadata = getMetadataFromBalancesResponseOptional(balance);
+ const accountIndex = getAddressIndex(balance.accountAddress).account;
+ const accountQuery = accountIndex ? `&account=${accountIndex}` : '';
+ return metadata ? `${PagePath.SWAP}?from=${metadata.symbol}${accountQuery}` : PagePath.SWAP;
+};
+
+const filteredBalancesByAccountSelector = (
+ zQueryState: AbridgedZQueryState,
+): BalancesByAccount[] =>
+ zQueryState.data?.filter(shouldDisplay).sort(sortByPriorityScore).reduce(groupByAccount, []) ??
+ [];
+
+const BUTTON_CELL_WIDTH_PX = '56px';
+
+export const AssetsPage = () => {
+ const balancesByAccount = useBalancesResponses({
+ select: filteredBalancesByAccountSelector,
+ shouldReselect: (before, after) => before?.data !== after.data,
+ });
+
+ return (
+
+ {balancesByAccount?.map(account => (
+
}>
+
+
+ Asset
+ Estimate
+
+
+
+
+ {account.balances.map((balance, index) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+ ))}
+
+ );
+};
diff --git a/apps/minifront/src/components/v2/dashboard-layout/assets-page/table-title.tsx b/apps/minifront/src/components/v2/dashboard-layout/assets-page/table-title.tsx
new file mode 100644
index 0000000000..eb20d23318
--- /dev/null
+++ b/apps/minifront/src/components/v2/dashboard-layout/assets-page/table-title.tsx
@@ -0,0 +1,22 @@
+import { AddressViewComponent } from '@repo/ui/AddressViewComponent';
+import { BalancesByAccount } from '../../../../state/shared';
+import { AddressView } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb';
+import { useMemo } from 'react';
+
+export const TableTitle = ({ account }: { account: BalancesByAccount }) => {
+ const addressView = useMemo(
+ () =>
+ new AddressView({
+ addressView: {
+ case: 'decoded',
+ value: {
+ address: account.address,
+ index: { account: account.account },
+ },
+ },
+ }),
+ [account.address, account.account],
+ );
+
+ return ;
+};
diff --git a/apps/minifront/src/components/v2/dashboard-layout/index.tsx b/apps/minifront/src/components/v2/dashboard-layout/index.tsx
new file mode 100644
index 0000000000..fc6a5fea5e
--- /dev/null
+++ b/apps/minifront/src/components/v2/dashboard-layout/index.tsx
@@ -0,0 +1,49 @@
+import { Card } from '@repo/ui/Card';
+import { Outlet, useNavigate } from 'react-router-dom';
+import { Grid } from '@repo/ui/Grid';
+import { Tabs } from '@repo/ui/Tabs';
+import { usePagePath } from '../../../fetchers/page-path';
+import { PagePath } from '../../metadata/paths';
+import { AssetsCardTitle } from './assets-card-title';
+import { TransactionsCardTitle } from './transactions-card-title';
+
+/** @todo: Remove this function and its uses after we switch to v2 layout */
+const v2PathPrefix = (path: string) => `/v2${path}`;
+
+const CARD_TITLE_BY_PATH = {
+ [v2PathPrefix(PagePath.DASHBOARD)]: ,
+ [v2PathPrefix(PagePath.TRANSACTIONS)]: ,
+};
+
+const TABS_OPTIONS = [
+ { label: 'Assets', value: v2PathPrefix(PagePath.DASHBOARD) },
+ { label: 'Transactions', value: v2PathPrefix(PagePath.TRANSACTIONS) },
+];
+
+export const DashboardLayout = () => {
+ const pagePath = usePagePath();
+ const navigate = useNavigate();
+
+ return (
+
+
+
+
+
+
+ navigate(value)}
+ options={TABS_OPTIONS}
+ actionType='accent'
+ />
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/minifront/src/components/v2/dashboard-layout/transactions-card-title.tsx b/apps/minifront/src/components/v2/dashboard-layout/transactions-card-title.tsx
new file mode 100644
index 0000000000..616da931c5
--- /dev/null
+++ b/apps/minifront/src/components/v2/dashboard-layout/transactions-card-title.tsx
@@ -0,0 +1,23 @@
+import { Button } from '@repo/ui/Button';
+import { Dialog } from '@repo/ui/Dialog';
+import { Text } from '@repo/ui/Text';
+import { Info } from 'lucide-react';
+
+export const TransactionsCardTitle = () => (
+
+ Transactions List
+
+
+);
diff --git a/apps/minifront/src/components/v2/dashboard-layout/transactions-page/index.tsx b/apps/minifront/src/components/v2/dashboard-layout/transactions-page/index.tsx
new file mode 100644
index 0000000000..8515c65c1a
--- /dev/null
+++ b/apps/minifront/src/components/v2/dashboard-layout/transactions-page/index.tsx
@@ -0,0 +1,52 @@
+import { Table } from '@repo/ui/Table';
+import { useSummaries } from '../../../../state/transactions';
+import { Text } from '@repo/ui/Text';
+import { Link } from 'react-router-dom';
+import { SquareArrowOutUpRight } from 'lucide-react';
+import { Button } from '@repo/ui/Button';
+
+export const TransactionsPage = () => {
+ const summaries = useSummaries();
+
+ return (
+
+
+
+ Block Height
+ Description
+ Hash
+
+
+
+ {summaries.data?.map(summary => (
+
+
+ {summary.height}
+
+
+ {summary.description}
+
+
+
+
+
+ {summary.hash}
+
+
+
+
+
+
+
+
+ ))}
+
+
+ );
+};
diff --git a/apps/minifront/src/components/v2/layout.tsx b/apps/minifront/src/components/v2/layout.tsx
new file mode 100644
index 0000000000..67373191dd
--- /dev/null
+++ b/apps/minifront/src/components/v2/layout.tsx
@@ -0,0 +1,14 @@
+import { Display } from '@repo/ui/Display';
+import { HeadTag } from '../metadata/head-tag';
+import { Outlet } from 'react-router-dom';
+import { Toaster } from '@repo/ui/components/ui/toaster';
+import { SyncingDialog } from '../syncing-dialog';
+
+export const Layout = () => (
+
+
+
+
+
+
+);
diff --git a/apps/minifront/src/components/v2/root-router.tsx b/apps/minifront/src/components/v2/root-router.tsx
new file mode 100644
index 0000000000..25191c38cb
--- /dev/null
+++ b/apps/minifront/src/components/v2/root-router.tsx
@@ -0,0 +1,52 @@
+import { redirect, RouteObject } from 'react-router-dom';
+import { Layout } from './layout';
+import { abortLoader } from '../../abort-loader';
+import { PagePath } from '../metadata/paths';
+import { DashboardLayout } from './dashboard-layout';
+import { AssetsPage } from './dashboard-layout/assets-page';
+import { TransactionsPage } from './dashboard-layout/transactions-page';
+
+/** @todo: Delete this helper once we switch over to the v2 layout. */
+const temporarilyPrefixPathsWithV2 = (routes: RouteObject[]): RouteObject[] =>
+ routes.map(route => {
+ if (route.index) {
+ return route;
+ }
+
+ return {
+ ...route,
+ path: `/v2${route.path === '/' ? '' : route.path}`,
+ ...(route.children ? { children: temporarilyPrefixPathsWithV2(route.children) } : {}),
+ };
+ });
+
+/**
+ * @todo: Once we switch over to the v2 layout, we need to:
+ * 1) pass these routes to `createHashRouter()` and export the returned router,
+ * like in `../root-router.tsx`.
+ * 2) remove the call to `temporarilyPrefixPathsWithV2()`.
+ */
+export const routes: RouteObject[] = temporarilyPrefixPathsWithV2([
+ {
+ path: '/',
+ element: ,
+ loader: abortLoader,
+ children: [
+ { index: true, loader: () => redirect(`/v2${PagePath.DASHBOARD}`) },
+ {
+ path: PagePath.DASHBOARD,
+ element: ,
+ children: [
+ {
+ index: true,
+ element: ,
+ },
+ {
+ path: PagePath.TRANSACTIONS,
+ element: ,
+ },
+ ],
+ },
+ ],
+ },
+]);
diff --git a/apps/minifront/src/fetchers/page-path.ts b/apps/minifront/src/fetchers/page-path.ts
index d8c6b81eb6..1fc923ba94 100644
--- a/apps/minifront/src/fetchers/page-path.ts
+++ b/apps/minifront/src/fetchers/page-path.ts
@@ -13,6 +13,8 @@ export const usePagePath = () => {
};
export const matchPagePath = (str: string): PagePath => {
+ /** @todo: Remove next line after we switch to v2 layout */
+ str = str.replace('/v2', '');
const pathValues = Object.values(PagePath);
if (pathValues.includes(str as PagePath)) {
diff --git a/packages/ui/src/Button/index.tsx b/packages/ui/src/Button/index.tsx
index a1734b6cd2..b6a9980e74 100644
--- a/packages/ui/src/Button/index.tsx
+++ b/packages/ui/src/Button/index.tsx
@@ -158,7 +158,15 @@ interface RegularProps {
export type ButtonProps = BaseButtonProps & (IconOnlyProps | RegularProps);
-/** A component for all your button needs! */
+/**
+ * A component for all your button needs!
+ *
+ * See individual props for how to use `` in various forms.
+ *
+ * Note that, to use `` as a link, you can simply wrap it in an anchor
+ * (``) tag (or ``, if you're using e.g., React Router) and leave
+ * `onClick` undefined.
+ */
export const Button = ({
children,
disabled = false,
diff --git a/packages/ui/src/Card/index.tsx b/packages/ui/src/Card/index.tsx
index 6f3bbbd5a5..2a7ee2a0ee 100644
--- a/packages/ui/src/Card/index.tsx
+++ b/packages/ui/src/Card/index.tsx
@@ -1,10 +1,11 @@
import { ReactNode } from 'react';
import styled, { WebTarget } from 'styled-components';
import { large } from '../utils/typography';
+import { hexOpacity } from '../utils/hexOpacity';
const Root = styled.section``;
-const Title = styled.h1`
+const Title = styled.h2`
${large};
color: ${props => props.theme.color.base.white};
@@ -12,6 +13,11 @@ const Title = styled.h1`
`;
const Content = styled.div`
+ background: linear-gradient(
+ 136deg,
+ ${props => props.theme.color.neutral.contrast + hexOpacity(0.1)} 6.32%,
+ ${props => props.theme.color.neutral.contrast + hexOpacity(0.01)} 75.55%
+ );
backdrop-filter: blur(${props => props.theme.blur.lg});
border-radius: ${props => props.theme.borderRadius.xl};
padding: ${props => props.theme.spacing(3)};
@@ -28,7 +34,7 @@ export interface CardProps {
* ```
*/
as?: WebTarget;
- title?: string;
+ title?: ReactNode;
}
export const Card = ({ children, as = 'section', title }: CardProps) => {
diff --git a/packages/ui/src/Dialog/index.tsx b/packages/ui/src/Dialog/index.tsx
index b5a47ed3b0..f418fc41f5 100644
--- a/packages/ui/src/Dialog/index.tsx
+++ b/packages/ui/src/Dialog/index.tsx
@@ -1,60 +1,55 @@
import { createContext, ReactNode, useContext } from 'react';
-import styled, { keyframes } from 'styled-components';
+import styled from 'styled-components';
import * as RadixDialog from '@radix-ui/react-dialog';
import { Text } from '../Text';
import { X } from 'lucide-react';
import { ButtonGroup, ButtonGroupProps } from '../ButtonGroup';
import { Button } from '../Button';
import { Density } from '../Density';
-
-const gradualBlur = (blur: string) => keyframes`
- from {
- backdrop-filter: blur(0);
- }
-
- to {
- backdrop-filter: blur(${blur});
- }
-`;
+import { Display } from '../Display';
+import { Grid } from '../Grid';
const Overlay = styled(RadixDialog.Overlay)`
- animation: ${props => gradualBlur(props.theme.blur.xs)} 0.15s forwards;
- /* animation: name duration timing-function delay iteration-count direction fill-mode; */
+ backdrop-filter: blur(${props => props.theme.blur.xs});
+ background-color: ${props => props.theme.color.other.overlay};
position: fixed;
inset: 0;
z-index: ${props => props.theme.zIndex.dialogOverlay};
`;
-const fadeIn = keyframes`
- from {
- opacity: 0;
- }
+const FullHeightWrapper = styled.div`
+ height: 100%;
+ min-height: 100svh;
+ max-height: 100lvh;
+ position: relative;
- to {
- opacity: 1;
- };
+ display: flex;
+ align-items: center;
`;
-const TEN_PERCENT_OPACITY_IN_HEX = '1a';
-const ONE_PERCENT_OPACITY_IN_HEX = '03';
-const DialogContent = styled(RadixDialog.Content)`
- animation: ${fadeIn} 0.15s forwards;
-
+/**
+ * We make a full-screen wrapper around the dialog's content so that we can
+ * correctly position it using the same ``/`` as the
+ * underlying page uses. Note that we use a `styled.div` here, rather than
+ * `styled(RadixDialog.Content)`, because Radix adds an inline `pointer-events:
+ * auto` style to that element. We need to make sure there _aren't_ pointer
+ * events on the dialog content, because of the aforementioned full-screen
+ * wrapper that appears over the ``. We want to make sure that clicks
+ * on the full-screen wrapper pass through to the underlying ``, so
+ * that the dialog closes when the user clicks there.
+ */
+const DialogContent = styled.div`
position: fixed;
- left: 50%;
- top: 50%;
- transform: translate(-50%, -50%);
+ inset: 0;
z-index: ${props => props.theme.zIndex.dialogContent};
+ pointer-events: none;
+`;
- width: 472px;
- max-width: 100%;
+const DialogContentCard = styled.div`
+ width: 100%;
box-sizing: border-box;
- background: linear-gradient(
- 136deg,
- ${props => props.theme.color.neutral.contrast + TEN_PERCENT_OPACITY_IN_HEX},
- ${props => props.theme.color.neutral.contrast + ONE_PERCENT_OPACITY_IN_HEX}
- );
+ background: ${props => props.theme.color.other.dialogBackground};
border: 1px solid ${props => props.theme.color.other.tonalStroke};
border-radius: ${props => props.theme.borderRadius.xl};
backdrop-filter: blur(${props => props.theme.blur.xl});
@@ -67,6 +62,13 @@ const DialogContent = styled(RadixDialog.Content)`
display: flex;
flex-direction: column;
gap: ${props => props.theme.spacing(6)};
+
+ /**
+ * We add 'pointer-events: auto' here so that clicks _inside_ the content card
+ * work, even though the _outside_ clicks pass through to the underlying
+ * ''.
+ */
+ pointer-events: auto;
`;
const TitleAndCloseButton = styled.header`
@@ -211,29 +213,45 @@ const Content = ({
-
-
-
-
- {title}
-
-
+
+
+
+
+
- {showCloseButton && (
-
-
-
-
-
- )}
-
+
+
+
+
+
+
+ {title}
+
+
- {children}
+ {showCloseButton && (
+
+
+
+
+
+ )}
+
+
+ {children}
+
+ {buttonGroupProps && }
+
+
+
- {buttonGroupProps && }
-
+
+
+
+
+
);
};
diff --git a/packages/ui/src/Display/index.stories.tsx b/packages/ui/src/Display/index.stories.tsx
new file mode 100644
index 0000000000..d3a938c7fc
--- /dev/null
+++ b/packages/ui/src/Display/index.stories.tsx
@@ -0,0 +1,57 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { Display } from '.';
+import styled from 'styled-components';
+import { Text } from '../Text';
+
+const meta: Meta = {
+ component: Display,
+ tags: ['autodocs'],
+ argTypes: {
+ children: { control: false },
+ },
+ decorators: [
+ Story => (
+
+
+
+ ),
+ ],
+};
+export default meta;
+
+type Story = StoryObj;
+
+const OuterWidthIndicator = styled.div`
+ border: 1px solid ${props => props.theme.color.base.white};
+`;
+
+const InnerWidthIndicator = styled.div`
+ background: ${props => props.theme.color.base.white};
+ color: ${props => props.theme.color.base.black};
+ padding: ${props => props.theme.spacing(2)};
+`;
+
+export const FullWidth: Story = {
+ args: {
+ children: (
+
+
+ The white background that this text sits inside of represents the{' '}
+ inside width of the <Display />{' '}
+ component. The white border to the left and right of this white bar represent the{' '}
+ outside width of the <Display />{' '}
+ component.
+
+
+ You can resize your window to see how the margins at left and right change depending on
+ the size of the browser window.
+
+
+ To test <Display /> at full width, click the "Full
+ Width" item in the left sidebar, and try resizing your browser.
+
+
+ ),
+ },
+};
diff --git a/packages/ui/src/Display/index.tsx b/packages/ui/src/Display/index.tsx
new file mode 100644
index 0000000000..1c82dcc497
--- /dev/null
+++ b/packages/ui/src/Display/index.tsx
@@ -0,0 +1,42 @@
+import { ReactNode } from 'react';
+import styled from 'styled-components';
+import { media } from '../utils/media';
+
+const Root = styled.section`
+ padding: 0 ${props => props.theme.spacing(4)};
+
+ ${props => media.desktop`
+ padding: 0 ${props.theme.spacing(8)};
+ `}
+`;
+
+const ContentsWrapper = styled.div`
+ max-width: 1600px;
+ margin: 0 auto;
+`;
+
+export interface DisplayProps {
+ children?: ReactNode;
+}
+
+/**
+ * Wrap your top-level component for a given page (usually a ``) in
+ * `` to adhere to PenumbraUI guidelines regarding maximum layouts
+ * widths, horizontal margins, etc.
+ *
+ * ```tsx
+ *
+ *
+ * Column one
+ * Column two
+ *
+ *
+ * ```
+ */
+export const Display = ({ children }: DisplayProps) => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/packages/ui/src/Grid/index.tsx b/packages/ui/src/Grid/index.tsx
index 4d8c882456..049d470463 100644
--- a/packages/ui/src/Grid/index.tsx
+++ b/packages/ui/src/Grid/index.tsx
@@ -32,19 +32,19 @@ interface GridItemProps extends BaseGridProps {
* The mobile grid layout can only be split in half, so you can only set a
* grid item to 6 or 12 columns on mobile.
*/
- mobile?: 6 | 12;
+ mobile?: 0 | 6 | 12;
/**
* The number of columns this grid item should span on tablet.
*
* The tablet grid layout can only be split into six columns.
*/
- tablet?: 2 | 4 | 6 | 8 | 10 | 12;
+ tablet?: 0 | 2 | 4 | 6 | 8 | 10 | 12;
/** The number of columns this grid item should span on desktop. */
- desktop?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
+ desktop?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
/** The number of columns this grid item should span on large screens. */
- lg?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
+ lg?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
/** The number of columns this grid item should span on XL screens. */
- xl?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
+ xl?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
}
export type GridProps = PropsWithChildren;
diff --git a/packages/ui/src/PenumbraUIProvider/theme.ts b/packages/ui/src/PenumbraUIProvider/theme.ts
index d133e46340..ea1f6ba19b 100644
--- a/packages/ui/src/PenumbraUIProvider/theme.ts
+++ b/packages/ui/src/PenumbraUIProvider/theme.ts
@@ -1,3 +1,5 @@
+import { hexOpacity } from '../utils/hexOpacity';
+
/**
* Used for reference in the `theme` object below. Not intended to be used
* directly by consumers, but rather as a semantic reference for building the
@@ -95,11 +97,13 @@ const PALETTE = {
900: '#6B3F18',
950: '#201004',
},
+ base: {
+ black: '#000000',
+ white: '#ffffff',
+ transparent: 'transparent',
+ },
};
-const FIFTEEN_PERCENT_OPACITY_IN_HEX = '26';
-const EIGHTY_PERCENT_OPACITY_IN_HEX = 'cc';
-
export const theme = {
blur: {
none: '0px',
@@ -170,9 +174,9 @@ export const theme = {
contrast: PALETTE.green['50'],
},
base: {
- black: '#000',
- white: '#fff',
- transparent: 'transparent',
+ black: PALETTE.base.black,
+ white: PALETTE.base.white,
+ transparent: PALETTE.base.transparent,
},
text: {
primary: PALETTE.neutral['50'],
@@ -181,9 +185,9 @@ export const theme = {
special: PALETTE.orange['400'],
},
action: {
- hoverOverlay: PALETTE.teal['400'] + FIFTEEN_PERCENT_OPACITY_IN_HEX,
- activeOverlay: PALETTE.neutral['950'] + FIFTEEN_PERCENT_OPACITY_IN_HEX,
- disabledOverlay: PALETTE.neutral['950'] + EIGHTY_PERCENT_OPACITY_IN_HEX,
+ hoverOverlay: PALETTE.teal['400'] + hexOpacity(0.15),
+ activeOverlay: PALETTE.neutral['950'] + hexOpacity(0.15),
+ disabledOverlay: PALETTE.neutral['950'] + hexOpacity(0.8),
primaryFocusOutline: PALETTE.orange['400'],
secondaryFocusOutline: PALETTE.teal['400'],
unshieldFocusOutline: PALETTE.purple['400'],
@@ -191,8 +195,12 @@ export const theme = {
destructiveFocusOutline: PALETTE.red['400'],
},
other: {
- tonalStroke: PALETTE.neutral['50'] + FIFTEEN_PERCENT_OPACITY_IN_HEX,
+ tonalStroke: PALETTE.neutral['50'] + hexOpacity(0.15),
+ tonalFill5: PALETTE.neutral['50'] + hexOpacity(0.05),
+ tonalFill10: PALETTE.neutral['50'] + hexOpacity(0.1),
solidStroke: PALETTE.neutral['700'],
+ dialogBackground: PALETTE.teal['700'] + hexOpacity(0.1),
+ overlay: PALETTE.base.black + hexOpacity(0.5),
},
},
font: {
diff --git a/packages/ui/src/Table/index.tsx b/packages/ui/src/Table/index.tsx
index 51108e2c9f..8285a19f09 100644
--- a/packages/ui/src/Table/index.tsx
+++ b/packages/ui/src/Table/index.tsx
@@ -8,12 +8,13 @@ import { ConditionalWrap } from '../utils/ConditionalWrap';
const FIVE_PERCENT_OPACITY_IN_HEX = '0d';
// So named to avoid naming conflicts with ``
-const StyledTable = styled.table`
+const StyledTable = styled.table<{ $layout?: 'fixed' | 'auto' }>`
width: 100%;
background-color: ${props => props.theme.color.neutral.contrast + FIVE_PERCENT_OPACITY_IN_HEX};
padding-left: ${props => props.theme.spacing(3)};
padding-right: ${props => props.theme.spacing(3)};
border-radius: ${props => props.theme.borderRadius.lg};
+ table-layout: ${props => props.$layout ?? 'auto'};
`;
const TitleAndTableWrapper = styled.div`
@@ -29,6 +30,8 @@ export interface TableProps {
/** Content that will appear above the table. */
title?: ReactNode;
children: ReactNode;
+ /** Which CSS `table-layout` property to use. */
+ layout?: 'fixed' | 'auto';
}
/**
@@ -72,7 +75,7 @@ export interface TableProps {
*
* ```
*/
-export const Table = ({ title, ...props }: TableProps) => (
+export const Table = ({ title, children, layout }: TableProps) => (
(
@@ -82,7 +85,9 @@ export const Table = ({ title, ...props }: TableProps) => (
)}
>
-
+
+ {children}
+
);
@@ -107,6 +112,8 @@ interface CellStyledProps {
}
const cell = css`
+ box-sizing: border-box;
+
padding-left: ${props => props.theme.spacing(3)};
padding-right: ${props => props.theme.spacing(3)};
@@ -154,7 +161,6 @@ Table.Th = Th;
const StyledTd = styled.td`
border-bottom: 1px solid ${props => props.theme.color.other.tonalStroke};
color: ${props => props.theme.color.text.primary};
- ${props => props.$width && `width: ${props.$width};`}
${StyledTbody} > ${StyledTr}:last-child > & {
border-bottom: none;
diff --git a/packages/ui/src/ValueViewComponent/index.tsx b/packages/ui/src/ValueViewComponent/index.tsx
index cde5cc36c5..5ff3a26283 100644
--- a/packages/ui/src/ValueViewComponent/index.tsx
+++ b/packages/ui/src/ValueViewComponent/index.tsx
@@ -11,21 +11,13 @@ import { useDensity } from '../hooks/useDensity';
type Context = 'default' | 'table';
-const Row = styled.span<{ $context: Context; $priority: 'primary' | 'secondary' }>`
+const Row = styled.span`
display: flex;
gap: ${props => props.theme.spacing(2)};
align-items: center;
- width: min-content;
+ width: max-content;
max-width: 100%;
text-overflow: ellipsis;
-
- ${props =>
- props.$context === 'table' && props.$priority === 'secondary'
- ? `
- border-bottom: 2px dashed ${props.theme.color.other.tonalStroke};
- padding-bottom: ${props.theme.spacing(2)};
- `
- : ''};
`;
const AssetIconWrapper = styled.div`
@@ -37,7 +29,7 @@ const PillMarginOffsets = styled.div<{ $density: Density }>`
margin-right: ${props => props.theme.spacing(props.$density === 'sparse' ? -1 : 0)};
`;
-const Content = styled.div`
+const Content = styled.div<{ $context: Context; $priority: 'primary' | 'secondary' }>`
flex-grow: 1;
flex-shrink: 1;
@@ -46,6 +38,11 @@ const Content = styled.div`
align-items: center;
overflow: hidden;
+
+ ${props =>
+ props.$context === 'table' &&
+ props.$priority === 'secondary' &&
+ `border-bottom: 2px dashed ${props.theme.color.other.tonalStroke};`};
`;
const SymbolWrapper = styled.div`
@@ -58,7 +55,7 @@ const SymbolWrapper = styled.div`
`;
export interface ValueViewComponentProps {
- valueView: ValueView;
+ valueView?: ValueView;
/**
* A `ValueViewComponent` will be rendered differently depending on which
* context it's rendered in. By default, it'll be rendered in a pill. But in a
@@ -86,6 +83,11 @@ export const ValueViewComponent = (
priority = 'primary',
}: ValueViewComponentProps) => {
const density = useDensity();
+
+ if (!valueView) {
+ return null;
+ }
+
const formattedAmount = getFormattedAmtFromValueView(valueView, true);
const metadata = getMetadata.optional()(valueView);
// Symbol default is "" and thus cannot do nullish coalescing
@@ -101,12 +103,12 @@ export const ValueViewComponent = (
)}
>
-
+
-
+
{formattedAmount}
{symbol}
diff --git a/packages/ui/src/utils/hexOpacity.ts b/packages/ui/src/utils/hexOpacity.ts
new file mode 100644
index 0000000000..d15c560a9b
--- /dev/null
+++ b/packages/ui/src/utils/hexOpacity.ts
@@ -0,0 +1,12 @@
+/**
+ * Given a decimal opacity (between 0 and 1), returns a two-character string
+ * that can be appended to an RGB value for the alpha channel.
+ *
+ * ```ts
+ * `#000000${opacityInHex(0.5)}` // #00000080 -- i.e., black at 50% opacity
+ * ```
+ */
+export const hexOpacity = (opacity: number) =>
+ Math.round(opacity * 255)
+ .toString(16)
+ .padStart(2, '0');