Skip to content

Commit 6637067

Browse files
committed
SD-16: Add token list component
1 parent 96126e6 commit 6637067

File tree

14 files changed

+2210
-2051
lines changed

14 files changed

+2210
-2051
lines changed

.storybook/preview.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { Preview } from '@storybook/react';
22

3+
import Providers from '../src/domains/common/providers/Providers';
4+
35
// TODO: Add after chromatic setup
46
// MotionGlobalConfig.skipAnimations = isChromatic();
57

@@ -30,6 +32,9 @@ const preview = {
3032
},
3133
},
3234
},
35+
decorators: [
36+
Story => <Providers><Story /></Providers>,
37+
],
3338
} satisfies Preview;
3439

3540
export default preview;

eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export default tseslint.config(
140140
},
141141
],
142142
patterns: [{
143-
regex: 'src/(?!(domains/.*/(components|utils|assets|hooks|providers)/.*))',
143+
regex: 'src/(?!(domains/.*/(components|utils|assets|hooks|providers|types|styles)/.*))',
144144
message: 'Organize modules in predefined groups under domains.',
145145
}, {
146146
/*

package-lock.json

Lines changed: 1790 additions & 2049 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@
1818
"generate-icons": "tsx scripts/generateIcons.ts"
1919
},
2020
"dependencies": {
21-
"motion": "^12.4.10",
21+
"@types/styled-components": "^5.1.34",
22+
"bignumber.js": "^9.1.2",
23+
"motion": "^12.4.12",
2224
"react": "^19.0.0",
2325
"react-dom": "^19.0.0",
2426
"styled-components": "^6.1.15",
2527
"styled-reset": "^4.5.2",
2628
"usehooks-ts": "^3.1.1",
29+
"utility-types": "^3.11.0",
2730
"vite-plugin-svgr": "^4.3.0",
2831
"zustand": "^5.0.3"
2932
},
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { useMotionValueEvent, useScroll } from 'motion/react';
2+
import { ReactNode, useEffect, useRef, useState } from 'react';
3+
import styled, { css } from 'styled-components';
4+
5+
type Props = {
6+
children: ReactNode,
7+
className?: string,
8+
};
9+
10+
const ScrollShadow = ({ children, className }: Props) => {
11+
const ref = useRef<HTMLDivElement | null>(null);
12+
const { scrollY } = useScroll({ container: ref });
13+
const [isScrolledTop, setIsScrolledTop] = useState(false);
14+
const [isScrolledBottom, setIsScrolledBottom] = useState(false);
15+
16+
const updateScrollState = () => {
17+
const el = ref.current;
18+
if (!el) return;
19+
20+
const { clientHeight, scrollHeight, scrollTop } = el;
21+
22+
setIsScrolledTop(scrollTop > 0);
23+
setIsScrolledBottom(scrollHeight > clientHeight && scrollTop < (scrollHeight - clientHeight));
24+
};
25+
26+
useEffect(() => {
27+
updateScrollState();
28+
}, []);
29+
30+
useMotionValueEvent(scrollY, 'change', updateScrollState);
31+
32+
return (
33+
<Container
34+
$isScrolledTop={isScrolledTop}
35+
$isScrolledBottom={isScrolledBottom}
36+
ref={ref}
37+
className={className}
38+
>
39+
{children}
40+
</Container>
41+
);
42+
};
43+
44+
export default ScrollShadow;
45+
46+
const Container = styled.div<{ $isScrolledTop: boolean, $isScrolledBottom: boolean }>`
47+
width: min(100vw, 440px);
48+
overflow-y: auto;
49+
50+
mask-composite: intersect;
51+
52+
${({ $isScrolledTop, $isScrolledBottom }) => {
53+
if ($isScrolledTop && $isScrolledBottom) {
54+
return css`
55+
mask-image: linear-gradient(to bottom, transparent, black 40px),
56+
linear-gradient(to top, transparent, black 40px);
57+
`;
58+
} else if ($isScrolledTop) {
59+
return css`
60+
mask-image: linear-gradient(to bottom, transparent, black 40px);
61+
`;
62+
} else if ($isScrolledBottom) {
63+
return css`
64+
mask-image: linear-gradient(to top, transparent, black 40px);
65+
`;
66+
}
67+
}}
68+
`;
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import type { ComponentProps } from 'react';
2+
import styled from 'styled-components';
3+
4+
import CIcon, { IconName } from 'src/domains/common/components/CIcon';
5+
import { vars } from 'src/domains/common/styles/utils';
6+
7+
type Size = number | `${string}%` | `${string}px`;
8+
9+
type Props = {
10+
icon?: IconName,
11+
size?: Size,
12+
iconProps?: Omit<ComponentProps<typeof CIcon>, 'icon'>,
13+
};
14+
15+
const TokenIcon = ({
16+
icon,
17+
size,
18+
iconProps,
19+
}: Props) => {
20+
21+
return (
22+
<Container $size={size}>
23+
{icon && (
24+
<CIcon
25+
icon={icon}
26+
color={vars('--color-neutral-foreground-inverted-1-rest')}
27+
size="100%"
28+
{...iconProps}
29+
/>
30+
)}
31+
</Container>
32+
);
33+
};
34+
35+
export default TokenIcon;
36+
37+
const getSize = ({ $size = 16 }: { $size?: Size }) =>
38+
typeof $size === 'number' ? `${$size}px` : $size;
39+
40+
const Container = styled.div<{
41+
$withBorder?: boolean,
42+
$size?: Size,
43+
}>`
44+
display: flex;
45+
46+
align-items: center;
47+
48+
max-width: ${getSize};
49+
max-height: ${getSize};
50+
min-height: ${getSize};
51+
min-width: ${getSize};
52+
53+
background-color: ${vars('--color-neutral-background-inverted-rest')};
54+
border-radius: ${vars('--border-radius-circular')};
55+
`;

src/domains/common/types/types.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { IconName } from 'src/domains/common/components/CIcon';
2+
3+
export type Token = {
4+
chain: 'alephEvm',
5+
name: string | undefined,
6+
symbol: string | undefined,
7+
address: string,
8+
decimals: number | undefined,
9+
icon: IconName | undefined,
10+
balance?: {
11+
atomic: bigint | string,
12+
usd?: number,
13+
},
14+
usdPrice?: number,
15+
};
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import BigNumber from 'bignumber.js';
2+
3+
import getBalanceWithDecimals from './getBalanceWithDecimals';
4+
5+
const formatConfig = {
6+
decimalSeparator: '.',
7+
groupSeparator: ',',
8+
groupSize: 3,
9+
secondaryGroupSize: 0,
10+
};
11+
12+
type Options = Partial<{
13+
formatDecimals: number,
14+
prefix: string,
15+
withPlusSign: boolean,
16+
}>;
17+
18+
const defaultOptions = {
19+
formatDecimals: 2,
20+
prefix: '',
21+
withPlusSign: false,
22+
} satisfies Options;
23+
24+
type Params = {
25+
balance: bigint | BigNumber | string | number,
26+
decimals: number,
27+
options?: Options,
28+
};
29+
30+
/**
31+
* Get an easily readable format of the balance, ready to be displayed.
32+
*/
33+
export default ({ balance, decimals, options }: Params) => {
34+
const { formatDecimals, prefix, withPlusSign } = {
35+
...defaultOptions,
36+
...options,
37+
};
38+
const bigNumberBalance = BigNumber(balance.toString());
39+
const isNegative = bigNumberBalance.isNegative();
40+
const plusOrMinus = isNegative ? '-' : withPlusSign ? '+' : '';
41+
const signWithPrefix = `${plusOrMinus}${prefix}`;
42+
43+
if (decimals === 0) {
44+
const newBalance = bigNumberBalance
45+
.dp(formatDecimals, BigNumber.ROUND_DOWN) // ROUND_DOWN rounds both positive and negative numbers towards zero
46+
.abs()
47+
.toFormat(formatConfig);
48+
return `${signWithPrefix}${newBalance}`;
49+
}
50+
51+
const newBalance = (
52+
getBalanceWithDecimals(bigNumberBalance, decimals)
53+
.dp(formatDecimals, BigNumber.ROUND_DOWN)
54+
.abs()
55+
.toFormat(formatConfig)
56+
);
57+
58+
const isVerySmall = Number(newBalance) === 0;
59+
const isZero = bigNumberBalance.isZero();
60+
61+
if (isZero) return `${prefix}0.00`;
62+
63+
if (isVerySmall) {
64+
return `${plusOrMinus ? `${plusOrMinus} ` : ''}<${prefix}0.${'1'.padStart(formatDecimals, '0')}`;
65+
}
66+
67+
return `${signWithPrefix}${newBalance}`;
68+
};
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import BigNumber from 'bignumber.js';
2+
3+
export default (balance: BigNumber | bigint, decimals: number) => {
4+
const bigNumberBalance = BigNumber.isBigNumber(balance) ? balance : BigNumber(balance.toString());
5+
6+
if (decimals === 0) return bigNumberBalance;
7+
8+
return bigNumberBalance.shiftedBy(decimals * -1);
9+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { isNullish } from 'utility-types';
2+
3+
export default (val: unknown): val is NonNullable<unknown> => !isNullish(val);

0 commit comments

Comments
 (0)