Merge pull request #85 from vegaprotocol/feat/40-tx-styling
Feat/40 Styles for transaction and transaction details page
This commit is contained in:
commit
c888ca1087
@ -7,4 +7,5 @@ module.exports = {
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||
coverageDirectory: '../../coverage/apps/explorer',
|
||||
setupFilesAfterEnv: ['./src/app/setupTests.ts'],
|
||||
};
|
||||
|
@ -19,7 +19,9 @@ export const RenderFetched = ({
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <StatusMessage className={className}>Error: {error}</StatusMessage>;
|
||||
return (
|
||||
<StatusMessage className={className}>Error retrieving data</StatusMessage>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
|
@ -4,7 +4,7 @@ interface SecondsAgoProps {
|
||||
date: string | undefined;
|
||||
}
|
||||
|
||||
export const SecondsAgo = ({ date }: SecondsAgoProps) => {
|
||||
export const SecondsAgo = ({ date, ...props }: SecondsAgoProps) => {
|
||||
const [now, setNow] = useState(Date.now());
|
||||
|
||||
useEffect(() => {
|
||||
@ -18,10 +18,11 @@ export const SecondsAgo = ({ date }: SecondsAgoProps) => {
|
||||
return <>Date unknown</>;
|
||||
}
|
||||
|
||||
|
||||
const timeAgoInSeconds = Math.floor((now - new Date(date).getTime()) / 1000);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div {...props}>
|
||||
{timeAgoInSeconds === 1 ? '1 second' : `${timeAgoInSeconds} seconds`} ago
|
||||
</div>
|
||||
);
|
||||
|
@ -0,0 +1,36 @@
|
||||
import { render, screen, act } from '@testing-library/react';
|
||||
import { SecondsAgo } from './index';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe('Seconds ago', () => {
|
||||
it('should render successfully', () => {
|
||||
const dateInString = new Date().toString();
|
||||
render(<SecondsAgo data-testid="test-seconds-ago" date={dateInString} />);
|
||||
|
||||
expect(screen.getByTestId('test-seconds-ago')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show the correct amount of seconds ago', () => {
|
||||
const secondsToWait = 10;
|
||||
const dateInString = new Date().toString();
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(secondsToWait * 1000);
|
||||
});
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
render(<SecondsAgo data-testid="test-seconds-ago" date={dateInString} />);
|
||||
|
||||
expect(screen.getByTestId('test-seconds-ago')).toHaveTextContent(
|
||||
`${secondsToWait} seconds ago`
|
||||
);
|
||||
});
|
||||
});
|
@ -6,7 +6,15 @@ interface StatusMessageProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const StatusMessage = ({ children, className }: StatusMessageProps) => {
|
||||
export const StatusMessage = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: StatusMessageProps) => {
|
||||
const classes = classnames('font-alpha text-h4 mb-28', className);
|
||||
return <h3 className={classes}>{children}</h3>;
|
||||
return (
|
||||
<h3 className={classes} {...props}>
|
||||
{children}
|
||||
</h3>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,11 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { StatusMessage } from './index';
|
||||
|
||||
describe('Status message', () => {
|
||||
it('should render successfully', () => {
|
||||
render(
|
||||
<StatusMessage data-testid="status-message-test">test</StatusMessage>
|
||||
);
|
||||
expect(screen.getByTestId('status-message-test')).toBeInTheDocument();
|
||||
});
|
||||
});
|
79
apps/explorer/src/app/components/table/table.spec.tsx
Normal file
79
apps/explorer/src/app/components/table/table.spec.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { Table, TableRow, TableHeader, TableCell } from './index';
|
||||
|
||||
describe('Renders all table components', () => {
|
||||
render(
|
||||
<Table data-testid="test-table">
|
||||
<TableRow data-testid="test-tr">
|
||||
<TableHeader data-testid="test-th">Title</TableHeader>
|
||||
<TableCell data-testid="test-td">Content</TableCell>
|
||||
</TableRow>
|
||||
</Table>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('test-table')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('test-tr')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('test-th')).toHaveTextContent('Title');
|
||||
expect(screen.getByTestId('test-td')).toHaveTextContent('Content');
|
||||
});
|
||||
|
||||
describe('Table row', () => {
|
||||
it('should include classes based on custom "modifier" prop', () => {
|
||||
render(
|
||||
<Table>
|
||||
<TableRow data-testid="modifier-test" modifier="bordered">
|
||||
<TableCell>With modifier</TableCell>
|
||||
</TableRow>
|
||||
</Table>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('modifier-test')).toHaveClass('border-white-40');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Table header', () => {
|
||||
it('should accept props i.e. scope="row"', () => {
|
||||
render(
|
||||
<Table>
|
||||
<TableRow>
|
||||
<TableHeader data-testid="props-test" scope="row">
|
||||
Test
|
||||
</TableHeader>
|
||||
</TableRow>
|
||||
</Table>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('props-test')).toHaveAttribute('scope');
|
||||
});
|
||||
|
||||
it('should include custom class based on scope="row"', () => {
|
||||
render(
|
||||
<Table>
|
||||
<TableRow>
|
||||
<TableHeader data-testid="scope-class-test" scope="row">
|
||||
With scope attribute
|
||||
</TableHeader>
|
||||
</TableRow>
|
||||
</Table>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('scope-class-test')).toHaveClass('text-left');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Table cell', () => {
|
||||
it('should include class based on custom "modifier" prop', () => {
|
||||
render(
|
||||
<Table>
|
||||
<TableRow>
|
||||
<TableCell data-testid="modifier-class-test" modifier="bordered">
|
||||
With modifier
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</Table>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('modifier-class-test')).toHaveClass('py-4');
|
||||
});
|
||||
});
|
39
apps/explorer/src/app/components/truncate/truncate.spec.tsx
Normal file
39
apps/explorer/src/app/components/truncate/truncate.spec.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { TruncateInline } from './truncate';
|
||||
|
||||
describe('Truncate', () => {
|
||||
it('should render successfully', () => {
|
||||
render(
|
||||
<TruncateInline data-testid="truncate-test" text={'Texty McTextFace'} />
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('truncate-test')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('it truncates as expected', () => {
|
||||
const test = 'randomstringblahblah';
|
||||
const startChars = 3;
|
||||
const endChars = 3;
|
||||
const expectedString = `${test.slice(0, startChars)}…${test.slice(
|
||||
-endChars
|
||||
)}`;
|
||||
|
||||
render(
|
||||
<TruncateInline text={test} startChars={startChars} endChars={endChars} />
|
||||
);
|
||||
|
||||
expect(screen.getByText(expectedString)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("it doesn't truncate if the string is too short", () => {
|
||||
const test = 'randomstringblahblah';
|
||||
const startChars = test.length;
|
||||
const endChars = test.length;
|
||||
|
||||
render(
|
||||
<TruncateInline text={test} startChars={startChars} endChars={endChars} />
|
||||
);
|
||||
|
||||
expect(screen.getByText(test)).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -22,6 +22,7 @@ export function TruncateInline({
|
||||
children,
|
||||
startChars,
|
||||
endChars,
|
||||
...props
|
||||
}: TruncateInlineProps) {
|
||||
if (text === null) {
|
||||
return <span data-testid="empty-truncation" />;
|
||||
@ -31,6 +32,7 @@ export function TruncateInline({
|
||||
const wrapperProps = {
|
||||
title: text,
|
||||
className,
|
||||
...props,
|
||||
};
|
||||
|
||||
if (children !== undefined) {
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { ChainExplorerTxResponse } from '../../../routes/types/chain-explorer-response';
|
||||
import { Table } from '../../table';
|
||||
import { SyntaxHighlighter } from '../../syntax-highlighter';
|
||||
import { Table, TableRow, TableHeader, TableCell } from '../../table';
|
||||
import { TxOrderType } from '../tx-order-type';
|
||||
import { StatusMessage } from '../../status-message';
|
||||
|
||||
interface TxContentProps {
|
||||
data: ChainExplorerTxResponse | undefined;
|
||||
@ -8,29 +10,26 @@ interface TxContentProps {
|
||||
|
||||
export const TxContent = ({ data }: TxContentProps) => {
|
||||
if (!data?.Command) {
|
||||
return <>Awaiting decoded transaction data</>;
|
||||
return (
|
||||
<StatusMessage>Could not retrieve transaction content</StatusMessage>
|
||||
);
|
||||
}
|
||||
|
||||
const { marketId, type, side, size } = JSON.parse(data.Command);
|
||||
|
||||
const displayCode = {
|
||||
market: marketId,
|
||||
type,
|
||||
side,
|
||||
size,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{data.Type}</td>
|
||||
</tr>
|
||||
<Table className="mb-12">
|
||||
<TableRow modifier="bordered">
|
||||
<TableHeader scope="row" className="w-[160px]">
|
||||
Type
|
||||
</TableHeader>
|
||||
<TableCell modifier="bordered">
|
||||
<TxOrderType orderType={data.Type} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</Table>
|
||||
|
||||
<h3>Decoded transaction content</h3>
|
||||
<SyntaxHighlighter data={displayCode} />
|
||||
<h3 className="font-mono mb-8">Decoded transaction content</h3>
|
||||
<SyntaxHighlighter data={JSON.parse(data.Command)} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,50 +1,63 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Routes } from '../../../routes/router-config';
|
||||
import { Result } from '../../../routes/txs/tendermint-transaction-response.d';
|
||||
import { Table, TableRow, TableCell } from '../../table';
|
||||
import { Table, TableRow, TableCell, TableHeader } from '../../table';
|
||||
import { TruncateInline } from '../../truncate/truncate';
|
||||
|
||||
interface TxDetailsProps {
|
||||
txData: Result | undefined;
|
||||
pubKey: string | undefined;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const TxDetails = ({ txData, pubKey }: TxDetailsProps) => {
|
||||
const truncateLength = 30;
|
||||
|
||||
export const TxDetails = ({ txData, pubKey, className }: TxDetailsProps) => {
|
||||
if (!txData) {
|
||||
return <>Awaiting Tendermint transaction details</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableRow>
|
||||
<Table className={className}>
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>Hash</TableCell>
|
||||
<TableCell data-testid="hash">{txData.hash}</TableCell>
|
||||
<TableCell modifier="bordered" data-testid="hash">
|
||||
{txData.hash}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{pubKey ? (
|
||||
<TableRow>
|
||||
<td>Submitted by</td>
|
||||
<td data-testid="submitted-by">
|
||||
<Link to={`/${Routes.PARTIES}/${pubKey}`}>{pubKey}</Link>
|
||||
</td>
|
||||
</TableRow>
|
||||
) : (
|
||||
<TableRow>
|
||||
<td>Submitted by</td>
|
||||
<td>Awaiting decoded transaction data</td>
|
||||
</TableRow>
|
||||
)}
|
||||
{txData.height ? (
|
||||
<TableRow>
|
||||
<td>Block</td>
|
||||
<td data-testid="block">
|
||||
<Link to={`/${Routes.BLOCKS}/${txData.height}`}>
|
||||
{txData.height}
|
||||
</Link>
|
||||
</td>
|
||||
</TableRow>
|
||||
) : null}
|
||||
<TableRow>
|
||||
<td>Encoded tnx</td>
|
||||
<td data-testid="encoded-tnx">{txData.tx}</td>
|
||||
<TableRow modifier="bordered">
|
||||
<TableHeader scope="row" className="w-[160px]">
|
||||
Submitted by
|
||||
</TableHeader>
|
||||
<TableCell modifier="bordered" data-testid="submitted-by">
|
||||
<Link
|
||||
className="text-vega-yellow"
|
||||
to={`/${Routes.PARTIES}/${pubKey}`}
|
||||
>
|
||||
{pubKey}
|
||||
</Link>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>Block</TableCell>
|
||||
<TableCell modifier="bordered" data-testid="block">
|
||||
<Link
|
||||
className="text-vega-yellow"
|
||||
to={`/${Routes.BLOCKS}/${txData.height}`}
|
||||
>
|
||||
{txData.height}
|
||||
</Link>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>Encoded tnx</TableCell>
|
||||
<TableCell modifier="bordered" data-testid="encoded-tnx">
|
||||
<TruncateInline
|
||||
text={txData.tx}
|
||||
startChars={truncateLength}
|
||||
endChars={truncateLength}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</Table>
|
||||
);
|
||||
|
43
apps/explorer/src/app/components/txs/tx-order-type/index.tsx
Normal file
43
apps/explorer/src/app/components/txs/tx-order-type/index.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { Lozenge } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
interface TxOrderTypeProps {
|
||||
orderType: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface StringMap {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
// Using https://github.com/vegaprotocol/protos/blob/e0f646ce39aab1fc66a9200ceec0262306d3beb3/commands/transaction.go#L93 as a reference
|
||||
const displayString: StringMap = {
|
||||
OrderSubmission: 'Order Submission',
|
||||
OrderCancellation: 'Order Cancellation',
|
||||
OrderAmendment: 'Order Amendment',
|
||||
VoteSubmission: 'Vote Submission',
|
||||
WithdrawSubmission: 'Withdraw Submission',
|
||||
LiquidityProvisionSubmission: 'Liquidity Provision',
|
||||
LiquidityProvisionCancellation: 'Liquidity Cancellation',
|
||||
LiquidityProvisionAmendment: 'Liquidity Amendment',
|
||||
ProposalSubmission: 'Governance Proposal',
|
||||
AnnounceNode: 'Node Announcement',
|
||||
NodeVote: 'Node Vote',
|
||||
NodeSignature: 'Node Signature',
|
||||
ChainEvent: 'Chain Event',
|
||||
OracleDataSubmission: 'Oracle Data',
|
||||
DelegateSubmission: 'Delegation',
|
||||
UndelegateSubmission: 'Undelegation',
|
||||
KeyRotateSubmission: 'Key Rotation',
|
||||
StateVariableProposal: 'State Variable Proposal',
|
||||
Transfer: 'Transfer',
|
||||
CancelTransfer: 'Cancel Transfer',
|
||||
ValidatorHeartbeat: 'Validator Heartbeat',
|
||||
};
|
||||
|
||||
export const TxOrderType = ({ orderType, className }: TxOrderTypeProps) => {
|
||||
return (
|
||||
<Lozenge className={className}>
|
||||
{displayString[orderType] || orderType}
|
||||
</Lozenge>
|
||||
);
|
||||
};
|
@ -5,6 +5,7 @@ import { DATA_SOURCES } from '../../../config';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { RenderFetched } from '../../render-fetched';
|
||||
import { TruncateInline } from '../../truncate/truncate';
|
||||
import { TxOrderType } from '../tx-order-type';
|
||||
|
||||
interface TxsPerBlockProps {
|
||||
blockHeight: string | undefined;
|
||||
@ -62,7 +63,9 @@ export const TxsPerBlock = ({ blockHeight }: TxsPerBlockProps) => {
|
||||
className="font-mono"
|
||||
/>
|
||||
</td>
|
||||
<td>{Type}</td>
|
||||
<td>
|
||||
<TxOrderType className="mb-4" orderType={Type} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
|
@ -1,20 +1,24 @@
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { TxContent, TxDetails } from '../../../components/txs';
|
||||
import { DATA_SOURCES } from '../../../config';
|
||||
import useFetch from '../../../hooks/use-fetch';
|
||||
import { ChainExplorerTxResponse } from '../../types/chain-explorer-response';
|
||||
import { TendermintTransactionResponse } from '../tendermint-transaction-response.d';
|
||||
import { ChainExplorerTxResponse } from '../../types/chain-explorer-response';
|
||||
import { DATA_SOURCES } from '../../../config';
|
||||
import { TxContent, TxDetails } from '../../../components/txs';
|
||||
import { RouteTitle } from '../../../components/route-title';
|
||||
import { RenderFetched } from '../../../components/render-fetched';
|
||||
|
||||
const Tx = () => {
|
||||
const { txHash } = useParams<{ txHash: string }>();
|
||||
|
||||
const {
|
||||
state: { data: transactionData },
|
||||
state: { data: tTxData, loading: tTxLoading, error: tTxError },
|
||||
} = useFetch<TendermintTransactionResponse>(
|
||||
`${DATA_SOURCES.tendermintUrl}/tx?hash=${txHash}`
|
||||
);
|
||||
|
||||
const {
|
||||
state: { data: decodedData },
|
||||
state: { data: ceTxData, loading: ceTxLoading, error: ceTxError },
|
||||
} = useFetch<ChainExplorerTxResponse>(DATA_SOURCES.chainExplorerUrl, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
@ -25,13 +29,20 @@ const Tx = () => {
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h1>Transaction details</h1>
|
||||
<TxDetails
|
||||
txData={transactionData?.result}
|
||||
pubKey={decodedData?.PubKey}
|
||||
/>
|
||||
<h2>Transaction content</h2>
|
||||
<TxContent data={decodedData} />
|
||||
<RouteTitle>Transaction details</RouteTitle>
|
||||
|
||||
<RenderFetched error={tTxError} loading={tTxLoading}>
|
||||
<TxDetails
|
||||
className="mb-28"
|
||||
txData={tTxData?.result}
|
||||
pubKey={ceTxData?.PubKey}
|
||||
/>
|
||||
</RenderFetched>
|
||||
|
||||
<h2 className="text-h4 uppercase mb-16">Transaction content</h2>
|
||||
<RenderFetched error={ceTxError} loading={ceTxLoading}>
|
||||
<TxContent data={ceTxData} />
|
||||
</RenderFetched>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
@ -46,6 +46,7 @@ module.exports = {
|
||||
prompt: '#EDFF22',
|
||||
success: '#26FF8A',
|
||||
help: '#494949',
|
||||
highlight: '#E5E5E5',
|
||||
},
|
||||
'intent-background': {
|
||||
danger: '#9E0025', // for white text
|
||||
@ -93,6 +94,14 @@ module.exports = {
|
||||
1: '1px',
|
||||
4: '4px',
|
||||
},
|
||||
borderRadius: {
|
||||
none: '0',
|
||||
sm: '0.125rem',
|
||||
DEFAULT: '0.225rem',
|
||||
md: '0.3rem',
|
||||
lg: '0.5rem',
|
||||
full: '9999px',
|
||||
},
|
||||
fontFamily: {
|
||||
mono: defaultTheme.fontFamily.mono,
|
||||
serif: defaultTheme.fontFamily.serif,
|
||||
|
1
libs/ui-toolkit/src/components/lozenge/index.ts
Normal file
1
libs/ui-toolkit/src/components/lozenge/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './lozenge';
|
10
libs/ui-toolkit/src/components/lozenge/lozenge.spec.tsx
Normal file
10
libs/ui-toolkit/src/components/lozenge/lozenge.spec.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { Lozenge } from './lozenge';
|
||||
|
||||
describe('Lozenge', () => {
|
||||
it('should render successfully', () => {
|
||||
const { baseElement } = render(<Lozenge>Lozenge</Lozenge>);
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
});
|
36
libs/ui-toolkit/src/components/lozenge/lozenge.stories.tsx
Normal file
36
libs/ui-toolkit/src/components/lozenge/lozenge.stories.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { Story, Meta } from '@storybook/react';
|
||||
import { Lozenge } from './lozenge';
|
||||
|
||||
export default {
|
||||
component: Lozenge,
|
||||
title: 'Lozenge',
|
||||
} as Meta;
|
||||
|
||||
const Template: Story = (args) => <Lozenge {...args}>lozenge</Lozenge>;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
|
||||
export const WithDetails = Template.bind({});
|
||||
WithDetails.args = {
|
||||
details: 'details text',
|
||||
};
|
||||
|
||||
export const Highlight = Template.bind({});
|
||||
Highlight.args = {
|
||||
variant: 'highlight',
|
||||
};
|
||||
|
||||
export const Success = Template.bind({});
|
||||
Success.args = {
|
||||
variant: 'success',
|
||||
};
|
||||
|
||||
export const Warning = Template.bind({});
|
||||
Warning.args = {
|
||||
variant: 'warning',
|
||||
};
|
||||
|
||||
export const Danger = Template.bind({});
|
||||
Danger.args = {
|
||||
variant: 'danger',
|
||||
};
|
38
libs/ui-toolkit/src/components/lozenge/lozenge.tsx
Normal file
38
libs/ui-toolkit/src/components/lozenge/lozenge.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import { ReactNode } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface LozengeProps {
|
||||
children: ReactNode;
|
||||
variant?: 'success' | 'warning' | 'danger' | 'highlight';
|
||||
className?: string;
|
||||
details?: string;
|
||||
}
|
||||
|
||||
const getWrapperClasses = (className: LozengeProps['className']) => {
|
||||
return classNames('inline-flex items-center gap-4', className);
|
||||
};
|
||||
|
||||
const getLozengeClasses = (variant: LozengeProps['variant']) => {
|
||||
return classNames(['rounded-md', 'font-mono', 'leading-none', 'p-4'], {
|
||||
'bg-intent-success text-black': variant === 'success',
|
||||
'bg-intent-danger text-white': variant === 'danger',
|
||||
'bg-intent-warning text-black': variant === 'warning',
|
||||
'bg-intent-highlight text-black': variant === 'highlight',
|
||||
'bg-intent-help text-white': !variant,
|
||||
});
|
||||
};
|
||||
|
||||
export const Lozenge = ({
|
||||
children,
|
||||
variant,
|
||||
className,
|
||||
details,
|
||||
}: LozengeProps) => {
|
||||
return (
|
||||
<span className={getWrapperClasses(className)}>
|
||||
<span className={getLozengeClasses(variant)}>{children}</span>
|
||||
|
||||
{details && <span>{details}</span>}
|
||||
</span>
|
||||
);
|
||||
};
|
@ -10,6 +10,7 @@ export { FormGroup } from './components/form-group';
|
||||
export { Icon } from './components/icon';
|
||||
export { Input } from './components/input';
|
||||
export { InputError } from './components/input-error';
|
||||
export { Lozenge } from './components/lozenge';
|
||||
export { Loader } from './components/loader';
|
||||
export { Select } from './components/select';
|
||||
export { Splash } from './components/splash';
|
||||
|
Loading…
Reference in New Issue
Block a user