feat(utils): use i18next (#5269)

Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
This commit is contained in:
Bartłomiej Głownia 2023-11-19 23:00:45 +01:00 committed by GitHub
parent 5827b87f89
commit bb47747501
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 277 additions and 143 deletions

View File

@ -8,7 +8,7 @@ import {
doesValueEquateToParam,
} from '@vegaprotocol/proposals';
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
import { validateJson } from '@vegaprotocol/utils';
import { useValidateJson } from '@vegaprotocol/utils';
import {
NetworkParams,
useNetworkParams,
@ -41,6 +41,7 @@ export interface NewAssetProposalFormFields {
const DOCS_LINK = '/new-asset-proposal';
export const ProposeNewAsset = () => {
const validateJson = useValidateJson();
const {
params,
loading: networkParamsLoading,

View File

@ -7,7 +7,7 @@ import {
doesValueEquateToParam,
} from '@vegaprotocol/proposals';
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
import { validateJson } from '@vegaprotocol/utils';
import { useValidateJson } from '@vegaprotocol/utils';
import {
NetworkParams,
useNetworkParams,
@ -39,6 +39,7 @@ export interface NewMarketProposalFormFields {
const DOCS_LINK = '/new-market-proposal';
export const ProposeNewMarket = () => {
const validateJson = useValidateJson();
const {
params,
loading: networkParamsLoading,

View File

@ -14,7 +14,7 @@ import {
RoundedWrapper,
TextArea,
} from '@vegaprotocol/ui-toolkit';
import { validateJson } from '@vegaprotocol/utils';
import { useValidateJson } from '@vegaprotocol/utils';
import {
NetworkParams,
useNetworkParams,
@ -31,6 +31,7 @@ export interface RawProposalFormFields {
}
export const ProposeRaw = () => {
const validateJson = useValidateJson();
const {
params,
loading: networkParamsLoading,

View File

@ -7,7 +7,7 @@ import {
doesValueEquateToParam,
} from '@vegaprotocol/proposals';
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
import { validateJson } from '@vegaprotocol/utils';
import { useValidateJson } from '@vegaprotocol/utils';
import {
NetworkParams,
useNetworkParams,
@ -39,6 +39,7 @@ export interface UpdateAssetProposalFormFields {
const DOCS_LINK = '/update-asset-proposal';
export const ProposeUpdateAsset = () => {
const validateJson = useValidateJson();
const {
params,
loading: networkParamsLoading,

View File

@ -8,7 +8,7 @@ import {
useProposalSubmit,
} from '@vegaprotocol/proposals';
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
import { validateJson } from '@vegaprotocol/utils';
import { useValidateJson } from '@vegaprotocol/utils';
import {
NetworkParams,
useNetworkParams,
@ -53,6 +53,7 @@ export interface UpdateMarketProposalFormFields {
const DOCS_LINK = '/update-market-proposal';
export const ProposeUpdateMarket = () => {
const validateJson = useValidateJson();
const {
params,
loading: networkParamsLoading,
@ -260,7 +261,7 @@ export const ProposeUpdateMarket = () => {
</FormGroup>
{selectedMarket && (
<div className="mt-[-20px] mb-6">
<div className="mb-6 mt-[-20px]">
<KeyValueTable data-testid="update-market-details">
<KeyValueTableRow>
{t('MarketName')}

View File

@ -1,8 +1,8 @@
import sortBy from 'lodash/sortBy';
import {
maxSafe,
required,
vegaPublicKey,
useMaxSafe,
useRequired,
useVegaPublicKey,
addDecimal,
formatNumber,
addDecimalsFormatNumber,
@ -67,6 +67,9 @@ export const TransferForm = ({
minQuantumMultiple,
}: TransferFormProps) => {
const t = useT();
const maxSafe = useMaxSafe();
const required = useRequired();
const vegaPublicKey = useVegaPublicKey();
const {
control,
register,
@ -415,7 +418,7 @@ export const TransferForm = ({
{accountBalance && (
<button
type="button"
className="absolute top-0 right-0 ml-auto text-xs underline"
className="absolute right-0 top-0 ml-auto text-xs underline"
onClick={() =>
setValue('amount', parseFloat(accountBalance).toString(), {
shouldValidate: true,
@ -491,7 +494,7 @@ export const TransferFee = ({
const totalValue = new BigNumber(transferAmount).plus(fee).toString();
return (
<div className="flex flex-col mb-4 text-xs gap-2">
<div className="mb-4 flex flex-col gap-2 text-xs">
<div className="flex flex-wrap items-center justify-between gap-1">
<Tooltip
description={t(
@ -560,7 +563,7 @@ export const AddressField = ({
<button
type="button"
onClick={onChange}
className="absolute top-0 right-0 ml-auto text-xs underline"
className="absolute right-0 top-0 ml-auto text-xs underline"
>
{isInput ? t('Select from wallet') : t('Enter manually')}
</button>

View File

@ -1,7 +1,7 @@
import { Controller, type Control } from 'react-hook-form';
import type { Market } from '@vegaprotocol/markets';
import type { OrderFormValues } from '../../hooks/use-form-values';
import { toDecimal, validateAmount } from '@vegaprotocol/utils';
import { toDecimal, useValidateAmount } from '@vegaprotocol/utils';
import {
TradingFormGroup,
TradingInput,
@ -28,6 +28,7 @@ export const DealTicketSizeIceberg = ({
peakSize,
}: DealTicketSizeIcebergProps) => {
const t = useT();
const validateAmount = useValidateAmount();
const sizeStep = toDecimal(market?.positionDecimalPlaces);
const renderPeakSizeError = () => {

View File

@ -9,7 +9,7 @@ import {
formatValue,
removeDecimal,
toDecimal,
validateAmount,
useValidateAmount,
} from '@vegaprotocol/utils';
import { type Control, type UseFormWatch } from 'react-hook-form';
import { useForm, Controller, useController } from 'react-hook-form';
@ -109,6 +109,7 @@ const Trigger = ({
decimalPlaces: number;
}) => {
const t = useT();
const validateAmount = useValidateAmount();
const triggerType = watch(oco ? 'ocoTriggerType' : 'triggerType');
const triggerDirection = watch('triggerDirection');
const isPriceTrigger = triggerType === 'price';
@ -341,6 +342,7 @@ const Size = ({
assetUnit?: string;
}) => {
const t = useT();
const validateAmount = useValidateAmount();
return (
<Controller
name={oco ? 'ocoSize' : 'size'}
@ -401,6 +403,7 @@ const Price = ({
oco?: boolean;
}) => {
const t = useT();
const validateAmount = useValidateAmount();
if (watch(oco ? 'ocoType' : 'type') === Schema.OrderType.TYPE_MARKET) {
return null;
}

View File

@ -28,7 +28,7 @@ import { useOpenVolume } from '@vegaprotocol/positions';
import {
toBigNum,
removeDecimal,
validateAmount,
useValidateAmount,
toDecimal,
formatForInput,
formatValue,
@ -140,6 +140,7 @@ export const DealTicket = ({
onDeposit,
}: DealTicketProps) => {
const t = useT();
const validateAmount = useValidateAmount();
const { pubKey, isReadOnly } = useVegaWallet();
const setType = useDealTicketFormValues((state) => state.setType);
const storedFormValues = useDealTicketFormValues(

View File

@ -1,11 +1,11 @@
import type { Asset, AssetFieldsFragment } from '@vegaprotocol/assets';
import { AssetOption } from '@vegaprotocol/assets';
import {
ethereumAddress,
required,
vegaPublicKey,
minSafe,
maxSafe,
useEthereumAddress,
useRequired,
useVegaPublicKey,
useMinSafe,
useMaxSafe,
addDecimal,
isAssetTypeERC20,
formatNumber,
@ -85,6 +85,11 @@ export const DepositForm = ({
isFaucetable,
}: DepositFormProps) => {
const t = useT();
const ethereumAddress = useEthereumAddress();
const required = useRequired();
const vegaPublicKey = useVegaPublicKey();
const minSafe = useMinSafe();
const maxSafe = useMaxSafe();
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
const openDialog = useWeb3ConnectStore((store) => store.open);
const { isActive, account } = useWeb3React();
@ -459,7 +464,7 @@ const UseButton = (props: UseButtonProps) => {
<button
{...props}
type="button"
className="absolute top-0 right-0 ml-auto text-sm underline"
className="absolute right-0 top-0 ml-auto text-sm underline"
/>
);
};
@ -519,7 +524,7 @@ export const AddressField = ({
setIsInput((curr) => !curr);
onChange();
}}
className="absolute top-0 right-0 ml-auto text-sm underline"
className="absolute right-0 top-0 ml-auto text-sm underline"
data-testid="enter-pubkey-manually"
>
{isInput ? t('Select from wallet') : t('Enter manually')}

View File

@ -0,0 +1,15 @@
{
"Expired on {{date}}": "Expired on {{date}}",
"Not time-based": "Not time-based",
"Expired": "Expired",
"Mark": "Mark",
"Required": "Required",
"Invalid Ethereum address": "Invalid Ethereum address",
"Invalid Vega key": "Invalid Vega key",
"Value is below minimum": "Value is below minimum",
"Value is above maximum": "Value is above maximum",
"Must be valid JSON": "Must be valid JSON",
"{{field}} must be a multiple of {{step}} for this market": "{{field}} must be a multiple of {{step}} for this market",
"{{field}} must be whole numbers for this market": "{{field}} must be whole numbers for this market",
"{{field}} accepts up to {{decimals}} decimal places": "{{field}} accepts up to {{decimals}} decimal places"
}

View File

@ -3,7 +3,7 @@ import {
getDateTimeFormat,
addDecimal,
addDecimalsFormatNumber,
validateAmount,
useValidateAmount,
} from '@vegaprotocol/utils';
import { Size } from '@vegaprotocol/datagrid';
import * as Schema from '@vegaprotocol/types';
@ -39,6 +39,7 @@ export const OrderEditDialog = ({
onSubmit,
}: OrderEditDialogProps) => {
const t = useT();
const validateAmount = useValidateAmount();
const headerClassName = 'text-xs font-bold text-black dark:text-white';
const {
register,

View File

@ -3,7 +3,7 @@ import {
getDateTimeFormat,
isNumeric,
toBigNum,
formatTrigger,
useFormatTrigger,
} from '@vegaprotocol/utils';
import * as Schema from '@vegaprotocol/types';
import {
@ -52,6 +52,7 @@ export type StopOrdersTableProps = TypedDataAgGrid<StopOrder> & {
export const StopOrdersTable = memo(
({ onCancel, onMarketClick, onView, ...props }: StopOrdersTableProps) => {
const t = useT();
const formatTrigger = useFormatTrigger();
const showAllActions = !props.isReadOnly;
const columnDefs: ColDef[] = useMemo(
() => [
@ -282,7 +283,15 @@ export const StopOrdersTable = memo(
},
},
],
[onCancel, onMarketClick, onView, props.isReadOnly, showAllActions, t]
[
onCancel,
onMarketClick,
onView,
props.isReadOnly,
showAllActions,
t,
formatTrigger,
]
);
return (

View File

@ -0,0 +1,14 @@
export const useTranslation = () => ({
t: (label: string, replacements?: Record<string, string>) => {
let translatedLabel = label;
if (typeof replacements === 'object' && replacements !== null) {
Object.keys(replacements).forEach((key) => {
translatedLabel = translatedLabel.replace(
`{{${key}}}`,
replacements[key]
);
});
}
return translatedLabel;
},
});

View File

@ -1,27 +1,37 @@
import * as Schema from '@vegaprotocol/types';
import { t } from '@vegaprotocol/i18n';
import { addDecimalsFormatNumber } from './number';
import { useCallback } from 'react';
import { useT } from '../use-t';
export const formatTrigger = (
data: Pick<Schema.StopOrder, 'trigger' | 'triggerDirection'> | undefined,
marketDecimalPlaces: number,
defaultValue = '-'
) => {
if (data && data?.trigger?.__typename === 'StopOrderPrice') {
return `${t('Mark')} ${
data?.triggerDirection ===
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
? '<'
: '>'
} ${addDecimalsFormatNumber(data.trigger.price, marketDecimalPlaces)}`;
}
if (data && data?.trigger?.__typename === 'StopOrderTrailingPercentOffset') {
return `${t('Mark')} ${
data?.triggerDirection ===
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
? '+'
: '-'
}${(Number(data?.trigger.trailingPercentOffset) * 100).toFixed(1)}%`;
}
return defaultValue;
export const useFormatTrigger = () => {
const t = useT();
return useCallback(
(
data: Pick<Schema.StopOrder, 'trigger' | 'triggerDirection'> | undefined,
marketDecimalPlaces: number,
defaultValue = '-'
) => {
if (data && data?.trigger?.__typename === 'StopOrderPrice') {
return `${t('Mark')} ${
data?.triggerDirection ===
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
? '<'
: '>'
} ${addDecimalsFormatNumber(data.trigger.price, marketDecimalPlaces)}`;
}
if (
data &&
data?.trigger?.__typename === 'StopOrderTrailingPercentOffset'
) {
return `${t('Mark')} ${
data?.triggerDirection ===
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
? '+'
: '-'
}${(Number(data?.trigger.trailingPercentOffset) * 100).toFixed(1)}%`;
}
return defaultValue;
},
[t]
);
};

View File

@ -1,7 +1,7 @@
import { MarketState } from '@vegaprotocol/types';
import { t } from '@vegaprotocol/i18n';
import { isValid, parseISO } from 'date-fns';
import { getDateTimeFormat } from './format';
import { useT } from './use-t';
export const getMarketExpiryDate = (
tags?: ReadonlyArray<string> | null
@ -40,12 +40,15 @@ export const getExpiryDate = (
close: string | null,
state: MarketState
): string => {
const t = useT();
const metadataExpiryDate = getMarketExpiryDate(tags);
const marketTimestampCloseDate = close && new Date(close);
let content = null;
if (!metadataExpiryDate) {
content = marketTimestampCloseDate
? `Expired on ${getDateTimeFormat().format(marketTimestampCloseDate)}`
? t('Expired on {{date}}', {
date: getDateTimeFormat().format(marketTimestampCloseDate),
})
: t('Not time-based');
} else {
const isExpired =
@ -54,7 +57,9 @@ export const getExpiryDate = (
state === MarketState.STATE_SETTLED);
if (isExpired) {
content = marketTimestampCloseDate
? `Expired on ${getDateTimeFormat().format(marketTimestampCloseDate)}`
? t('Expired on {{date}}', {
date: getDateTimeFormat().format(marketTimestampCloseDate),
})
: t('Expired');
} else {
content = getDateTimeFormat().format(metadataExpiryDate);

View File

@ -0,0 +1,3 @@
import { useTranslation } from 'react-i18next';
export const ns = 'utils';
export const useT = () => useTranslation(ns).t;

View File

@ -1,6 +1,10 @@
import { ethereumAddress, vegaPublicKey } from './common';
import { renderHook } from '@testing-library/react';
import { useEthereumAddress, useVegaPublicKey } from './common';
it('ethereumAddress', () => {
const result = renderHook(useEthereumAddress);
const ethereumAddress = result.result.current;
const errorMessage = 'Invalid Ethereum address';
const validAddress = '0x72c22822A19D20DE7e426fB84aa047399Ddd8853';
@ -17,6 +21,9 @@ it('ethereumAddress', () => {
});
it('vegaPublicKey', () => {
const result = renderHook(useVegaPublicKey);
const vegaPublicKey = result.result.current;
const errorMessage = 'Invalid Vega key';
const validKey =

View File

@ -1,40 +1,71 @@
import BigNumber from 'bignumber.js';
import { t } from '@vegaprotocol/i18n';
import { useT } from '../use-t';
import { useCallback } from 'react';
export const required = (value: string) => {
if (value === null || value === undefined || value === '') {
return t('Required');
}
return true;
export const useRequired = () => {
const t = useT();
return useCallback(
(value: string) => {
if (value === null || value === undefined || value === '') {
return t('Required');
}
return true;
},
[t]
);
};
export const ethereumAddress = (value: string) => {
if (!/^0x[0-9a-fA-F]{40}$/i.test(value)) {
return t('Invalid Ethereum address');
}
return true;
export const useEthereumAddress = () => {
const t = useT();
return useCallback(
(value: string) => {
if (!/^0x[0-9a-fA-F]{40}$/i.test(value)) {
return t('Invalid Ethereum address');
}
return true;
},
[t]
);
};
export const VEGA_ID_REGEX = /^[A-Fa-f0-9]{64}$/i;
export const vegaPublicKey = (value: string) => {
if (!VEGA_ID_REGEX.test(value)) {
return t('Invalid Vega key');
}
return true;
export const useVegaPublicKey = () => {
const t = useT();
return useCallback(
(value: string) => {
if (!VEGA_ID_REGEX.test(value)) {
return t('Invalid Vega key');
}
return true;
},
[t]
);
};
export const minSafe = (min: BigNumber) => (value: string) => {
if (new BigNumber(value).isLessThan(min)) {
return t('Value is below minimum');
}
return true;
export const useMinSafe = () => {
const t = useT();
return useCallback(
(min: BigNumber) => (value: string) => {
if (new BigNumber(value).isLessThan(min)) {
return t('Value is below minimum');
}
return true;
},
[t]
);
};
export const maxSafe = (max: BigNumber) => (value: string) => {
if (new BigNumber(value).isGreaterThan(max)) {
return t('Value is above maximum');
}
return true;
export const useMaxSafe = () => {
const t = useT();
return useCallback(
(max: BigNumber) => (value: string) => {
if (new BigNumber(value).isGreaterThan(max)) {
return t('Value is above maximum');
}
return true;
},
[t]
);
};
export const suitableForSyntaxHighlighter = (str: string) => {
@ -46,11 +77,17 @@ export const suitableForSyntaxHighlighter = (str: string) => {
}
};
export const validateJson = (value: string) => {
try {
JSON.parse(value);
return true;
} catch (e) {
return t('Must be valid JSON');
}
export const useValidateJson = () => {
const t = useT();
return useCallback(
(value: string) => {
try {
JSON.parse(value);
return true;
} catch (e) {
return t('Must be valid JSON');
}
},
[t]
);
};

View File

@ -1,22 +1,40 @@
import { t } from '@vegaprotocol/i18n';
import { useCallback } from 'react';
import { useT } from '../use-t';
export const validateAmount = (step: number | string, field: string) => {
const [, stepDecimals = ''] = String(step).split('.');
export const useValidateAmount = () => {
const t = useT();
return useCallback(
(step: number | string, field: string) => {
const [, stepDecimals = ''] = String(step).split('.');
return (value?: string) => {
if (Number(step) > 1) {
if (Number(value) % Number(step) > 0) {
return t(`${field} must be a multiple of ${step} for this market`);
}
return true;
}
const [, valueDecimals = ''] = (value || '').split('.');
if (stepDecimals.length < valueDecimals.length) {
if (stepDecimals === '') {
return t(`${field} must be whole numbers for this market`);
}
return t(`${field} accepts up to ${stepDecimals.length} decimal places`);
}
return true;
};
return (value?: string) => {
if (Number(step) > 1) {
if (Number(value) % Number(step) > 0) {
return t(
'{{field}} must be a multiple of {{step}} for this market',
{
field,
step,
}
);
}
return true;
}
const [, valueDecimals = ''] = (value || '').split('.');
if (stepDecimals.length < valueDecimals.length) {
if (stepDecimals === '') {
return t('{{field}} must be whole numbers for this market', {
field,
});
}
return t('{{field}} accepts up to {{decimals}} decimal places', {
field,
decimals: stepDecimals.length,
});
}
return true;
};
},
[t]
);
};

View File

@ -40,7 +40,7 @@ import {
formatNumber,
toBigNum,
truncateByChars,
formatTrigger,
useFormatTrigger,
MAXGOINT64,
} from '@vegaprotocol/utils';
import { useAssetsMapProvider } from '@vegaprotocol/assets';
@ -260,6 +260,7 @@ const SubmitStopOrderSetup = ({
triggerDirection: Schema.StopOrderTriggerDirection;
market: Market;
}) => {
const formatTrigger = useFormatTrigger();
if (!market || !stopOrderSetup) return null;
const { price, size, side } = stopOrderSetup.orderSubmission;
@ -446,6 +447,7 @@ const CancelOrderDetails = ({
const CancelStopOrderDetails = ({ stopOrderId }: { stopOrderId: string }) => {
const t = useT();
const formatTrigger = useFormatTrigger();
const { data: orderById } = useStopOrderByIdQuery({
variables: { stopOrderId },
});
@ -732,7 +734,7 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
<p>{t('Your funds have been unlocked for withdrawal.')}</p>
{tx.txHash && (
<ExternalLink
className="block mb-[5px] break-all"
className="mb-[5px] block break-all"
href={explorerLink(EXPLORER_TX.replace(':hash', tx.txHash))}
rel="noreferrer"
>

View File

@ -1,10 +1,10 @@
import type { Asset } from '@vegaprotocol/assets';
import { AssetOption } from '@vegaprotocol/assets';
import {
ethereumAddress,
minSafe,
useEthereumAddress,
useRequired,
useMinSafe,
removeDecimal,
required,
isAssetTypeERC20,
formatNumber,
} from '@vegaprotocol/utils';
@ -23,7 +23,6 @@ import {
import { useWeb3React } from '@web3-react/core';
import BigNumber from 'bignumber.js';
import { useEffect, type ButtonHTMLAttributes } from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { formatDistanceToNow } from 'date-fns';
import { useForm, Controller, useWatch } from 'react-hook-form';
import { WithdrawLimits } from './withdraw-limits';
@ -112,6 +111,10 @@ export const WithdrawForm = ({
onSelectAsset,
submitWithdraw,
}: WithdrawFormProps) => {
const ethereumAddress = useEthereumAddress();
const required = useRequired();
const minSafe = useMinSafe();
const { account: address } = useWeb3React();
const {
register,
@ -150,36 +153,6 @@ export const WithdrawForm = ({
trigger('to');
}, [address, setValue, trigger]);
const renderAssetsSelector = ({
field,
}: {
field: ControllerRenderProps<FormFields, 'asset'>;
}) => {
return (
<TradingRichSelect
data-testid="select-asset"
id="asset"
name="asset"
required
onValueChange={(value) => {
onSelectAsset(value);
field.onChange(value);
}}
placeholder={t('Please select an asset')}
value={selectedAsset?.id}
hasError={Boolean(errors.asset?.message)}
>
{assets.filter(isAssetTypeERC20).map((a) => (
<AssetOption
key={a.id}
asset={a}
balance={<AssetBalance asset={a} />}
/>
))}
</TradingRichSelect>
);
};
const showWithdrawDelayNotification =
Boolean(delay) &&
Boolean(selectedAsset) &&
@ -189,7 +162,7 @@ export const WithdrawForm = ({
<>
<div className="mb-4 text-sm">
<p>{t('There are two steps required to make a withdrawal')}</p>
<ol className="pl-4 list-disc">
<ol className="list-disc pl-4">
<li>{t('Step 1 - Release funds from Vega')}</li>
<li>{t('Step 2 - Transfer funds to your Ethereum wallet')}</li>
</ol>
@ -208,7 +181,29 @@ export const WithdrawForm = ({
required: (value) => !!selectedAsset || required(value),
},
}}
render={renderAssetsSelector}
render={({ field }) => (
<TradingRichSelect
data-testid="select-asset"
id="asset"
name="asset"
required
onValueChange={(value) => {
onSelectAsset(value);
field.onChange(value);
}}
placeholder={t('Please select an asset')}
value={selectedAsset?.id}
hasError={Boolean(errors.asset?.message)}
>
{assets.filter(isAssetTypeERC20).map((a) => (
<AssetOption
key={a.id}
asset={a}
balance={<AssetBalance asset={a} />}
/>
))}
</TradingRichSelect>
)}
/>
{errors.asset?.message && (
<TradingInputError intent="danger">
@ -314,7 +309,7 @@ const UseButton = (props: UseButtonProps) => {
<button
{...props}
type="button"
className="absolute top-0 right-0 ml-auto text-sm underline"
className="absolute right-0 top-0 ml-auto text-sm underline"
/>
);
};