diff --git a/.changeset/curvy-experts-fall.md b/.changeset/curvy-experts-fall.md new file mode 100644 index 000000000..6ada7601b --- /dev/null +++ b/.changeset/curvy-experts-fall.md @@ -0,0 +1,5 @@ +--- +"@lightsparkdev/core": patch +--- + +Adding currency support for XOF, XAF, MWK, RWF diff --git a/.changeset/little-hands-sip.md b/.changeset/little-hands-sip.md new file mode 100644 index 000000000..ecfe83f79 --- /dev/null +++ b/.changeset/little-hands-sip.md @@ -0,0 +1,5 @@ +--- +"@lightsparkdev/ui": patch +--- + +UI updates and fixes for bank linking, kyc, 2fa, filters diff --git a/.changeset/nice-pumas-rhyme.md b/.changeset/nice-pumas-rhyme.md new file mode 100644 index 000000000..36fccdf6e --- /dev/null +++ b/.changeset/nice-pumas-rhyme.md @@ -0,0 +1,5 @@ +--- +"@lightsparkdev/lightspark-sdk": patch +--- + +Adding ability to fail HTLCs and add an idempotency key to requestWithdrawal diff --git a/.mise.toml b/.mise.toml index f1755ec6d..8f90efb7e 100644 --- a/.mise.toml +++ b/.mise.toml @@ -45,7 +45,7 @@ description = "Start ops frontend connected to minikube" run = "VITE_PROXY_TARGET=http://app.minikube.local yarn start ops" [tasks.site] -description = "Start CN2 frontend connected to minikube" +description = "Start CN2 frontend development server" run = "yarn start site" [tasks.site-minikube] diff --git a/AGENTS.md b/AGENTS.md new file mode 120000 index 000000000..681311eb9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +CLAUDE.md \ No newline at end of file diff --git a/packages/core/src/utils/currency.ts b/packages/core/src/utils/currency.ts index 45381bbca..56b40a2a0 100644 --- a/packages/core/src/utils/currency.ts +++ b/packages/core/src/utils/currency.ts @@ -39,6 +39,10 @@ export const CurrencyUnit = { TZS: "TZS", UGX: "UGX", BWP: "BWP", + XOF: "XOF", + XAF: "XAF", + MWK: "MWK", + RWF: "RWF", USDT: "USDT", USDC: "USDC", @@ -102,6 +106,10 @@ const standardUnitConversionObj = { [CurrencyUnit.TZS]: (v: number) => v, [CurrencyUnit.UGX]: (v: number) => v, [CurrencyUnit.BWP]: (v: number) => v, + [CurrencyUnit.XOF]: (v: number) => v, + [CurrencyUnit.XAF]: (v: number) => v, + [CurrencyUnit.MWK]: (v: number) => v, + [CurrencyUnit.RWF]: (v: number) => v, [CurrencyUnit.USDT]: (v: number) => v, [CurrencyUnit.USDC]: (v: number) => v, }; @@ -149,6 +157,10 @@ const CONVERSION_MAP = { [CurrencyUnit.TZS]: toBitcoinConversion, [CurrencyUnit.UGX]: toBitcoinConversion, [CurrencyUnit.BWP]: toBitcoinConversion, + [CurrencyUnit.XOF]: toBitcoinConversion, + [CurrencyUnit.XAF]: toBitcoinConversion, + [CurrencyUnit.MWK]: toBitcoinConversion, + [CurrencyUnit.RWF]: toBitcoinConversion, [CurrencyUnit.USDT]: toBitcoinConversion, [CurrencyUnit.USDC]: toBitcoinConversion, }, @@ -180,6 +192,10 @@ const CONVERSION_MAP = { [CurrencyUnit.TZS]: toMicrobitcoinConversion, [CurrencyUnit.UGX]: toMicrobitcoinConversion, [CurrencyUnit.BWP]: toMicrobitcoinConversion, + [CurrencyUnit.XOF]: toMicrobitcoinConversion, + [CurrencyUnit.XAF]: toMicrobitcoinConversion, + [CurrencyUnit.MWK]: toMicrobitcoinConversion, + [CurrencyUnit.RWF]: toMicrobitcoinConversion, [CurrencyUnit.USDT]: toMicrobitcoinConversion, [CurrencyUnit.USDC]: toMicrobitcoinConversion, }, @@ -211,6 +227,10 @@ const CONVERSION_MAP = { [CurrencyUnit.TZS]: toMillibitcoinConversion, [CurrencyUnit.UGX]: toMillibitcoinConversion, [CurrencyUnit.BWP]: toMillibitcoinConversion, + [CurrencyUnit.XOF]: toMillibitcoinConversion, + [CurrencyUnit.XAF]: toMillibitcoinConversion, + [CurrencyUnit.MWK]: toMillibitcoinConversion, + [CurrencyUnit.RWF]: toMillibitcoinConversion, [CurrencyUnit.USDT]: toMillibitcoinConversion, [CurrencyUnit.USDC]: toMillibitcoinConversion, }, @@ -242,6 +262,10 @@ const CONVERSION_MAP = { [CurrencyUnit.TZS]: toMillisatoshiConversion, [CurrencyUnit.UGX]: toMillisatoshiConversion, [CurrencyUnit.BWP]: toMillisatoshiConversion, + [CurrencyUnit.XOF]: toMillisatoshiConversion, + [CurrencyUnit.XAF]: toMillisatoshiConversion, + [CurrencyUnit.MWK]: toMillisatoshiConversion, + [CurrencyUnit.RWF]: toMillisatoshiConversion, [CurrencyUnit.USDT]: toMillisatoshiConversion, [CurrencyUnit.USDC]: toMillisatoshiConversion, }, @@ -273,6 +297,10 @@ const CONVERSION_MAP = { [CurrencyUnit.TZS]: toNanobitcoinConversion, [CurrencyUnit.UGX]: toNanobitcoinConversion, [CurrencyUnit.BWP]: toNanobitcoinConversion, + [CurrencyUnit.XOF]: toNanobitcoinConversion, + [CurrencyUnit.XAF]: toNanobitcoinConversion, + [CurrencyUnit.MWK]: toNanobitcoinConversion, + [CurrencyUnit.RWF]: toNanobitcoinConversion, [CurrencyUnit.USDT]: toNanobitcoinConversion, [CurrencyUnit.USDC]: toNanobitcoinConversion, }, @@ -304,6 +332,10 @@ const CONVERSION_MAP = { [CurrencyUnit.TZS]: toSatoshiConversion, [CurrencyUnit.UGX]: toSatoshiConversion, [CurrencyUnit.BWP]: toSatoshiConversion, + [CurrencyUnit.XOF]: toSatoshiConversion, + [CurrencyUnit.XAF]: toSatoshiConversion, + [CurrencyUnit.MWK]: toSatoshiConversion, + [CurrencyUnit.RWF]: toSatoshiConversion, [CurrencyUnit.USDT]: toSatoshiConversion, [CurrencyUnit.USDC]: toSatoshiConversion, }, @@ -328,6 +360,10 @@ const CONVERSION_MAP = { [CurrencyUnit.TZS]: standardUnitConversionObj, [CurrencyUnit.UGX]: standardUnitConversionObj, [CurrencyUnit.BWP]: standardUnitConversionObj, + [CurrencyUnit.XOF]: standardUnitConversionObj, + [CurrencyUnit.XAF]: standardUnitConversionObj, + [CurrencyUnit.MWK]: standardUnitConversionObj, + [CurrencyUnit.RWF]: standardUnitConversionObj, [CurrencyUnit.USDT]: standardUnitConversionObj, [CurrencyUnit.USDC]: standardUnitConversionObj, }; @@ -412,6 +448,10 @@ export type CurrencyMap = { [CurrencyUnit.TZS]: number; [CurrencyUnit.UGX]: number; [CurrencyUnit.BWP]: number; + [CurrencyUnit.XOF]: number; + [CurrencyUnit.XAF]: number; + [CurrencyUnit.MWK]: number; + [CurrencyUnit.RWF]: number; [CurrencyUnit.USDT]: number; [CurrencyUnit.USDC]: number; [CurrencyUnit.FUTURE_VALUE]: number; @@ -446,6 +486,10 @@ export type CurrencyMap = { [CurrencyUnit.TZS]: string; [CurrencyUnit.UGX]: string; [CurrencyUnit.BWP]: string; + [CurrencyUnit.XOF]: string; + [CurrencyUnit.XAF]: string; + [CurrencyUnit.MWK]: string; + [CurrencyUnit.RWF]: string; [CurrencyUnit.USDT]: string; [CurrencyUnit.USDC]: string; [CurrencyUnit.FUTURE_VALUE]: string; @@ -661,6 +705,10 @@ function convertCurrencyAmountValues( tzs: CurrencyUnit.TZS, ugx: CurrencyUnit.UGX, bwp: CurrencyUnit.BWP, + xof: CurrencyUnit.XOF, + xaf: CurrencyUnit.XAF, + mwk: CurrencyUnit.MWK, + rwf: CurrencyUnit.RWF, mibtc: CurrencyUnit.MICROBITCOIN, mlbtc: CurrencyUnit.MILLIBITCOIN, nbtc: CurrencyUnit.NANOBITCOIN, @@ -740,6 +788,10 @@ export function mapCurrencyAmount( tzs, ugx, bwp, + xof, + xaf, + mwk, + rwf, usdt, usdc, } = convertCurrencyAmountValues(unit, value, unitsPerBtc, conversionOverride); @@ -769,6 +821,10 @@ export function mapCurrencyAmount( [CurrencyUnit.TZS]: tzs, [CurrencyUnit.UGX]: ugx, [CurrencyUnit.BWP]: bwp, + [CurrencyUnit.XOF]: xof, + [CurrencyUnit.XAF]: xaf, + [CurrencyUnit.MWK]: mwk, + [CurrencyUnit.RWF]: rwf, [CurrencyUnit.MICROBITCOIN]: mibtc, [CurrencyUnit.MILLIBITCOIN]: mlbtc, [CurrencyUnit.NANOBITCOIN]: nbtc, @@ -884,6 +940,22 @@ export function mapCurrencyAmount( value: bwp, unit: CurrencyUnit.BWP, }), + [CurrencyUnit.XOF]: formatCurrencyStr({ + value: xof, + unit: CurrencyUnit.XOF, + }), + [CurrencyUnit.XAF]: formatCurrencyStr({ + value: xaf, + unit: CurrencyUnit.XAF, + }), + [CurrencyUnit.MWK]: formatCurrencyStr({ + value: mwk, + unit: CurrencyUnit.MWK, + }), + [CurrencyUnit.RWF]: formatCurrencyStr({ + value: rwf, + unit: CurrencyUnit.RWF, + }), [CurrencyUnit.USDT]: formatCurrencyStr({ value: usdt, unit: CurrencyUnit.USDT, @@ -1006,6 +1078,14 @@ export const abbrCurrencyUnit = (unit: CurrencyUnitType) => { return "UGX"; case CurrencyUnit.BWP: return "BWP"; + case CurrencyUnit.XOF: + return "XOF"; + case CurrencyUnit.XAF: + return "XAF"; + case CurrencyUnit.MWK: + return "MWK"; + case CurrencyUnit.RWF: + return "RWF"; } return "Unsupported CurrencyUnit"; }; @@ -1175,6 +1255,13 @@ export function formatCurrencyStr( const unitStr = isUmaCurrencyAmount(amount) ? amount.currency.code : abbrCurrencyUnit(unit as CurrencyUnitType); + + // Skip appending if the formatted string already contains the currency code + // This happens for currencies like XOF where Intl.NumberFormat uses the code as the symbol + if (formattedStr.includes(unitStr)) { + return formattedStr; + } + const unitSuffix = options?.appendUnits?.plural && num > 1 ? "s" : ""; const unitStrWithSuffix = `${unitStr}${unitSuffix}`; formattedStr += ` ${ diff --git a/packages/core/src/utils/tests/currency.test.ts b/packages/core/src/utils/tests/currency.test.ts index 25f590e52..e2f8e5797 100644 --- a/packages/core/src/utils/tests/currency.test.ts +++ b/packages/core/src/utils/tests/currency.test.ts @@ -436,3 +436,22 @@ describe("formatCurrencyStr", () => { ).toBe("$1,000.12 MXN"); }); }); + +it("should not append XOF if it's already in the formatted string", () => { + /* This test verifies the fix for PQA-394 where XOF was appearing twice. + * If Intl.NumberFormat formats with currencyDisplay: 'code', it will include + * XOF in the output, and we shouldn't append it again. */ + const formatted = formatCurrencyStr( + { value: 221900, unit: CurrencyUnit.XOF }, + { appendUnits: { plural: false, lowercase: false } }, + ); + + /* Count occurrences of 'XOF' or 'CFA' in the result */ + const xofCount = (formatted.match(/XOF/g) || []).length; + const cfaCount = (formatted.match(/CFA/g) || []).length; + + /* Should have either XOF or CFA, but not both, and only once */ + expect(xofCount + cfaCount).toBeGreaterThan(0); + expect(xofCount).toBeLessThanOrEqual(1); + expect(cfaCount).toBeLessThanOrEqual(1); +}); diff --git a/packages/lightspark-sdk/src/client.ts b/packages/lightspark-sdk/src/client.ts index f54ff438d..cefb83b3d 100644 --- a/packages/lightspark-sdk/src/client.ts +++ b/packages/lightspark-sdk/src/client.ts @@ -45,6 +45,7 @@ import { CreateUmaInvitationWithPayment } from "./graphql/CreateUmaInvitationWit import { CreateUmaInvoice } from "./graphql/CreateUmaInvoice.js"; import { DecodeInvoice } from "./graphql/DecodeInvoice.js"; import { DeleteApiToken } from "./graphql/DeleteApiToken.js"; +import { FailHtlcs } from "./graphql/FailHtlcs.js"; import { FetchUmaInvitation } from "./graphql/FetchUmaInvitation.js"; import { FundNode } from "./graphql/FundNode.js"; import { IncomingPaymentsForInvoice } from "./graphql/IncomingPaymentsForInvoice.js"; @@ -76,6 +77,8 @@ import type ComplianceProvider from "./objects/ComplianceProvider.js"; import type CreateApiTokenOutput from "./objects/CreateApiTokenOutput.js"; import type CurrencyAmount from "./objects/CurrencyAmount.js"; import { CurrencyAmountFromJson } from "./objects/CurrencyAmount.js"; +import type FailHtlcsOutput from "./objects/FailHtlcsOutput.js"; +import { FailHtlcsOutputFromJson } from "./objects/FailHtlcsOutput.js"; import type FeeEstimate from "./objects/FeeEstimate.js"; import { FeeEstimateFromJson } from "./objects/FeeEstimate.js"; import type IncomingPayment from "./objects/IncomingPayment.js"; @@ -736,6 +739,28 @@ class LightsparkClient { return InvoiceFromJson(invoiceJson); } + /** + * Fails all pending HTLCs (Hash Time Locked Contracts) for a given invoice. + * + * @param invoiceId The ID of the invoice for which to fail HTLCs. + * @param cancelInvoice Whether to also cancel the invoice after failing the HTLCs. + * @returns The output containing the invoice ID, or undefined if the operation failed. + */ + public async failHtlcs( + invoiceId: string, + cancelInvoice: boolean, + ): Promise { + const response = await this.requester.makeRawRequest(FailHtlcs, { + invoice_id: invoiceId, + cancel_invoice: cancelInvoice, + }); + const output = response.fail_htlcs; + if (!output) { + return undefined; + } + return FailHtlcsOutputFromJson(output); + } + /** * Decodes an encoded lightning invoice string. * @@ -1206,15 +1231,20 @@ class LightsparkClient { amountSats: number, bitcoinAddress: string, mode: WithdrawalMode, + idempotencyKey: string | undefined = undefined, ): Promise { + const variables: Record = { + node_id: nodeId, + amount_sats: amountSats, + bitcoin_address: bitcoinAddress, + withdrawal_mode: mode, + }; + if (idempotencyKey !== undefined) { + variables.idempotency_key = idempotencyKey; + } const response = await this.requester.makeRawRequest( RequestWithdrawal, - { - node_id: nodeId, - amount_sats: amountSats, - bitcoin_address: bitcoinAddress, - withdrawal_mode: mode, - }, + variables, nodeId, ); return WithdrawalRequestFromJson(response.request_withdrawal.request); diff --git a/packages/lightspark-sdk/src/graphql/FailHtlcs.ts b/packages/lightspark-sdk/src/graphql/FailHtlcs.ts new file mode 100644 index 000000000..a3f14c24f --- /dev/null +++ b/packages/lightspark-sdk/src/graphql/FailHtlcs.ts @@ -0,0 +1,19 @@ +// Copyright ©, 2023-present, Lightspark Group, Inc. - All Rights Reserved + +import { FRAGMENT as FailHtlcsOutputFragment } from "../objects/FailHtlcsOutput.js"; + +export const FailHtlcs = ` + mutation FailHtlcs( + $invoice_id: ID!, + $cancel_invoice: Boolean! + ) { + fail_htlcs(input: { + invoice_id: $invoice_id, + cancel_invoice: $cancel_invoice + }) { + ...FailHtlcsOutputFragment + } + } + +${FailHtlcsOutputFragment} +`; diff --git a/packages/lightspark-sdk/src/graphql/RequestWithdrawal.ts b/packages/lightspark-sdk/src/graphql/RequestWithdrawal.ts index ab6d48c5a..0a16daf9e 100644 --- a/packages/lightspark-sdk/src/graphql/RequestWithdrawal.ts +++ b/packages/lightspark-sdk/src/graphql/RequestWithdrawal.ts @@ -8,12 +8,14 @@ export const RequestWithdrawal = ` $bitcoin_address: String! $amount_sats: Long! $withdrawal_mode: WithdrawalMode! + $idempotency_key: String ) { request_withdrawal(input: { node_id: $node_id bitcoin_address: $bitcoin_address amount_sats: $amount_sats withdrawal_mode: $withdrawal_mode + idempotency_key: $idempotency_key }) { request { ...WithdrawalRequestFragment diff --git a/packages/ui/src/components/BirthdayInput.tsx b/packages/ui/src/components/BirthdayInput.tsx index 67e7ba2e6..6d797397e 100644 --- a/packages/ui/src/components/BirthdayInput.tsx +++ b/packages/ui/src/components/BirthdayInput.tsx @@ -1,6 +1,7 @@ import { TextInput } from "@lightsparkdev/ui/src/components"; import dayjs from "dayjs"; import customParseFormat from "dayjs/plugin/customParseFormat.js"; +import { type FocusEvent, useEffect, useRef } from "react"; dayjs.extend(customParseFormat); @@ -9,6 +10,8 @@ interface BirthdayInputProps { invalidBirthdayError: string; setDate: (date: string) => void; dateFormat?: "US" | "INTL"; + onFocus?: (event: FocusEvent) => void; + onError?: (error: string) => void; } const DATE_FORMATS = { @@ -146,10 +149,26 @@ export function BirthdayInput({ setDate, invalidBirthdayError, dateFormat = "US", + onFocus, + onError, }: BirthdayInputProps) { const formatConfig = DATE_FORMATS[dateFormat]; const birthdayFieldBlurred = Boolean(date.trim()); + const isCompleteDate = date.length === 10; + const isInvalid = + isCompleteDate && !isValidBirthday(date, undefined, undefined, dateFormat); + const showError = birthdayFieldBlurred && isInvalid; + + // Track previous error state to detect when error first appears + const prevShowErrorRef = useRef(false); + useEffect(() => { + if (showError && !prevShowErrorRef.current && onError) { + onError(invalidBirthdayError); + } + prevShowErrorRef.current = showError; + }, [showError, onError, invalidBirthdayError]); + const handleChange = (newValue: string): void => { let value = newValue; @@ -168,10 +187,6 @@ export function BirthdayInput({ setDate(formattedValue); }; - const isCompleteDate = date.length === 10; - const isInvalid = - isCompleteDate && !isValidBirthday(date, undefined, undefined, dateFormat); - return ( <> {iconSide === "left" && currentIcon} diff --git a/packages/ui/src/components/CodeInput/CodeInput.tsx b/packages/ui/src/components/CodeInput/CodeInput.tsx index 0ddf87ffb..e5e38d02a 100644 --- a/packages/ui/src/components/CodeInput/CodeInput.tsx +++ b/packages/ui/src/components/CodeInput/CodeInput.tsx @@ -11,6 +11,7 @@ import type { import { createRef, useCallback, useRef, useState } from "react"; import { isChrome, isMobile, isMobileSafari } from "react-device-detect"; import { textInputBorderColor } from "../../styles/fields.js"; +import { Spacing } from "../../styles/tokens/spacing.js"; import { SingleCodeInput } from "./SingleCodeInput.js"; type InputState = { @@ -21,6 +22,7 @@ type InputState = { export type OnSubmitCode = (code: string) => void; export type OnChangeCode = (code: string) => void; +export type CodeInputVariant = "default" | "unified"; type CodeInputProps = { onChange?: OnChangeCode | undefined; onSubmit?: OnSubmitCode | undefined; @@ -28,6 +30,7 @@ type CodeInputProps = { inputCSS?: SerializedStyles; autoFocus?: boolean; onBlur?: (() => void) | undefined; + variant?: CodeInputVariant; }; function codeFromInputState(inputState: InputState): string { @@ -60,6 +63,7 @@ export function CodeInput({ inputCSS, autoFocus = true, onBlur, + variant = "default", }: CodeInputProps): JSX.Element { const componentId = useRef(nanoid(5)); const inputRefs = useRef({}); @@ -397,12 +401,21 @@ export function CodeInput({ ariaLabel={`Code input ${i + 1} of ${codeLength}`} cssProp={inputCSS} onBlur={onBlurCb} - width={`${(100 / inputsPerGroup).toFixed(2)}%`} + width={ + variant === "unified" + ? "12px" + : `${(100 / inputsPerGroup).toFixed(2)}%` + } max={9} + variant={variant} />, ); } + if (variant === "unified") { + return {inputs}; + } + return ( {inputs.slice(0, inputsPerGroup)} @@ -441,3 +454,20 @@ const CodeInputGroupSeparator = styled.div` border-radius: 2px; margin: 0 ${separatorMargin}px; `; + +const UnifiedCodeInputContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + gap: ${Spacing.px.xs}; + padding: 12px 24px; + border: 0.5px solid ${({ theme }) => theme.border}; + border-radius: 16px; + background-color: ${({ theme }) => theme.bg}; + width: 100%; + + &:focus-within { + border: 2px solid ${({ theme }) => theme.text}; + } +`; diff --git a/packages/ui/src/components/CodeInput/SingleCodeInput.tsx b/packages/ui/src/components/CodeInput/SingleCodeInput.tsx index 5849a9bd8..59b14ef80 100644 --- a/packages/ui/src/components/CodeInput/SingleCodeInput.tsx +++ b/packages/ui/src/components/CodeInput/SingleCodeInput.tsx @@ -8,6 +8,7 @@ import type { Ref, } from "react"; import { textInputStyle } from "../../styles/fields.js"; +import type { CodeInputVariant } from "./CodeInput.js"; type CounterProps = { id?: string; @@ -34,6 +35,7 @@ type CounterProps = { disabled?: boolean; onBlur?: (event: React.FocusEvent) => void; width: string; + variant?: CodeInputVariant; }; const defaultWidth = "42px"; @@ -55,8 +57,37 @@ export function SingleCodeInput({ disabled, onBlur, width = defaultWidth, + variant = "default", }: CounterProps): JSX.Element { const theme = useTheme(); + + const commonCSS = css` + text-align: center; + + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + display: none; + margin: 0; + } + -moz-appearance: textfield; /* Firefox */ + `; + + const unifiedCSS = css` + ${commonCSS} + width: ${width}; + padding: 0; + border: none; + background: transparent; + font: inherit; + color: ${theme.text}; + outline: none; + caret-color: ${theme.info}; + + &::placeholder { + color: ${theme.secondary}; + } + `; + const defaultCSS = css( ` ${ @@ -70,7 +101,6 @@ export function SingleCodeInput({ width: ${width}; max-width: ${defaultWidth}; padding: ${vPadding}px 0px; - text-align: center; &:not(:last-of-type):not(:focus-visible) { border-right: none; } @@ -89,17 +119,13 @@ export function SingleCodeInput({ &:active { padding: ${vPadding - 1}px 0px !important; } - - &::-webkit-outer-spin-button, - &::-webkit-inner-spin-button { - display: none; - margin: 0; - } - -moz-appearance:textfield; /* Firefox */ `, + commonCSS, cssProp, ); + const inputStyles = variant === "unified" ? unifiedCSS : defaultCSS; + return ( onChange(event, id)} value={value} ref={inputRef} - type={type} + type={variant === "unified" ? "text" : type} aria-label={ariaLabel} onKeyUp={(event) => onKeyUp(event, id)} onKeyDown={(event) => onKeyDown(event, id)} onPaste={(event) => onPaste(event)} max={max} - css={defaultCSS} + css={inputStyles} onBlur={onBlur} inputMode="numeric" pattern="[0-9]*" + placeholder={variant === "unified" ? "-" : undefined} /> ); } diff --git a/packages/ui/src/components/DataManagerTable/DataManagerTable.tsx b/packages/ui/src/components/DataManagerTable/DataManagerTable.tsx index 429081f78..962162474 100644 --- a/packages/ui/src/components/DataManagerTable/DataManagerTable.tsx +++ b/packages/ui/src/components/DataManagerTable/DataManagerTable.tsx @@ -94,6 +94,15 @@ interface FilterOptions< ) => QueryVariablesType; refetch: (fetchVariables: QueryVariablesType) => Promise; initialQueryVariables: QueryVariablesType; + /** + * Called when a filter state changes. + * Use setFilterStates to modify other filters (e.g., for mutual exclusivity). + */ + onFilterStateChange?: ( + changedKey: keyof T, + newState: FilterState, + setFilterStates: Dispatch>>, + ) => void; } interface ShowMoreOptions { @@ -860,6 +869,19 @@ export function DataManagerTable< } }; + const createFilterStateUpdater = + (filter: Filter) => (state: FilterState) => { + setFilterStates((prevState) => ({ + ...prevState, + [filter.accessorKey]: state, + })); + props.filterOptions?.onFilterStateChange?.( + filter.accessorKey, + state, + setFilterStates, + ); + }; + const filterSections = props.filterOptions ? props.filterOptions.filters.map((filter: Filter) => { switch (filter.type) { @@ -867,12 +889,7 @@ export function DataManagerTable< return (
{ - setFilterStates((prevState) => ({ - ...prevState, - [filter.accessorKey]: state, - })); - }} + updateFilterState={createFilterStateUpdater(filter)} state={filterStates[filter.accessorKey] as DateFilterState} />
@@ -881,12 +898,7 @@ export function DataManagerTable< return (
{ - setFilterStates((prevState) => ({ - ...prevState, - [filter.accessorKey]: state, - })); - }} + updateFilterState={createFilterStateUpdater(filter)} options={filter.enumValues} label={filter.label} placeholder={filter.placeholder} @@ -899,12 +911,7 @@ export function DataManagerTable< return (
{ - setFilterStates((prevState) => ({ - ...prevState, - [filter.accessorKey]: state, - })); - }} + updateFilterState={createFilterStateUpdater(filter)} label={filter.label} placeholder={filter.placeholder} state={filterStates[filter.accessorKey] as StringFilterState} @@ -915,12 +922,7 @@ export function DataManagerTable< return (
{ - setFilterStates((prevState) => ({ - ...prevState, - [filter.accessorKey]: state, - })); - }} + updateFilterState={createFilterStateUpdater(filter)} label={filter.label} placeholder={filter.placeholder} state={filterStates[filter.accessorKey] as IdFilterState} @@ -931,12 +933,7 @@ export function DataManagerTable< return (
{ - setFilterStates((prevState) => ({ - ...prevState, - [filter.accessorKey]: state, - })); - }} + updateFilterState={createFilterStateUpdater(filter)} label={filter.label} state={filterStates[filter.accessorKey] as BooleanFilterState} /> @@ -946,12 +943,7 @@ export function DataManagerTable< return (
{ - setFilterStates((prevState) => ({ - ...prevState, - [filter.accessorKey]: state, - })); - }} + updateFilterState={createFilterStateUpdater(filter)} label={filter.label} state={ filterStates[filter.accessorKey] as CurrencyFilterState @@ -1087,8 +1079,20 @@ export function DataManagerTable< }} onUpdateFilter={(newFilterState) => { setFilterStates((prevStates) => { - const newStates = { ...prevStates }; + let newStates = { ...prevStates }; newStates[filter.accessorKey] = newFilterState; + // Call onFilterStateChange to allow mutual exclusivity logic + props.filterOptions?.onFilterStateChange?.( + filter.accessorKey, + newFilterState, + (updater) => { + if (typeof updater === "function") { + newStates = updater(newStates); + } else { + newStates = updater; + } + }, + ); void handleApplyFilters( newStates, props.filterOptions!, @@ -1119,6 +1123,7 @@ export function DataManagerTable< setShowFilterEditor, customDropdownComponent: props.customComponents?.dropdownComponent, + onFilterStateChange: props.filterOptions.onFilterStateChange, })} isOpen={showFilterEditor} onOpen={() => { @@ -1411,6 +1416,7 @@ function getPillDropdownItems< pageSize, setShowFilterEditor, customDropdownComponent, + onFilterStateChange, }: { filterOptions: FilterOptions; filterStates: DataManagerTableState; @@ -1425,7 +1431,31 @@ function getPillDropdownItems< customDropdownComponent?: | React.ComponentType> | undefined; + onFilterStateChange?: FilterOptions< + T, + QueryVariablesType, + QueryResultType + >["onFilterStateChange"]; }) { + // Helper to apply a new filter state with mutual exclusivity support + const applyFilterState = (filter: Filter, newFilterState: FilterState) => { + setFilterStates((prevStates: DataManagerTableState) => { + let newStates = { ...prevStates }; + newStates[filter.accessorKey] = newFilterState; + // Call onFilterStateChange for mutual exclusivity logic + onFilterStateChange?.(filter.accessorKey, newFilterState, (updater) => { + if (typeof updater === "function") { + newStates = updater(newStates); + } else { + newStates = updater; + } + }); + void handleApplyFilters(newStates, filterOptions, pageSize); + setShowFilterEditor(false); + return newStates; + }); + }; + const getDropdownItemForFilter = (filter: Filter): DropdownItemType => { let filterSubDropdownOptions: DropdownItemType[] = []; if (filter.type === FilterType.ENUM) { @@ -1448,18 +1478,12 @@ function getPillDropdownItems< updatedAppliedValues = [...optionValues]; } - setFilterStates((prevStates: DataManagerTableState) => { - const newStates = { ...prevStates }; - newStates[filter.accessorKey] = { - ...getDefaultFilterState(filter), - value: updatedAppliedValues.join(", "), - isApplied: true, - appliedValues: updatedAppliedValues, - } as unknown as FilterState; - void handleApplyFilters(newStates, filterOptions, pageSize); - setShowFilterEditor(false); - return newStates; - }); + applyFilterState(filter, { + ...getDefaultFilterState(filter), + value: updatedAppliedValues.join(", "), + isApplied: true, + appliedValues: updatedAppliedValues, + } as unknown as FilterState); }, })); } @@ -1487,30 +1511,18 @@ function getPillDropdownItems< ? [...state.appliedValues] : []; - setFilterStates((prevStates: DataManagerTableState) => { - const newStates = { ...prevStates }; - newStates[filter.accessorKey] = { - ...getDefaultFilterState(filter), - value: updatedAppliedValues.join(", "), - isApplied: true, - appliedValues: updatedAppliedValues, - } as unknown as FilterState; - void handleApplyFilters(newStates, filterOptions, pageSize); - setShowFilterEditor(false); - return newStates; - }); + applyFilterState(filter, { + ...getDefaultFilterState(filter), + value: updatedAppliedValues.join(", "), + isApplied: true, + appliedValues: updatedAppliedValues, + } as unknown as FilterState); } else if (filter.type === FilterType.BOOLEAN) { - setFilterStates((prevStates: DataManagerTableState) => { - const newStates = { ...prevStates }; - newStates[filter.accessorKey] = { - ...getDefaultFilterState(filter), - value: filter.value as boolean, - isApplied: true, - } as unknown as FilterState; - void handleApplyFilters(newStates, filterOptions, pageSize); - setShowFilterEditor(false); - return newStates; - }); + applyFilterState(filter, { + ...getDefaultFilterState(filter), + value: filter.value as boolean, + isApplied: true, + } as unknown as FilterState); } else if (filter.type === FilterType.ID) { const state = filterStates[filter.accessorKey] as IdFilterState; let updatedAppliedValues: string[] = []; @@ -1524,34 +1536,23 @@ function getPillDropdownItems< : state.appliedValues ? [...state.appliedValues] : []; - setFilterStates((prevStates: DataManagerTableState) => { - const newStates = { ...prevStates }; - newStates[filter.accessorKey] = { - ...getDefaultFilterState(filter), - value: updatedAppliedValues.join(", "), - isApplied: true, - appliedValues: updatedAppliedValues, - } as unknown as FilterState; - void handleApplyFilters(newStates, filterOptions, pageSize); - setShowFilterEditor(false); - return newStates; - }); + + applyFilterState(filter, { + ...getDefaultFilterState(filter), + value: updatedAppliedValues.join(", "), + isApplied: true, + appliedValues: updatedAppliedValues, + } as unknown as FilterState); } else if (filter.type === FilterType.CURRENCY) { // TODO: Currency filter state, design tbd } else if (filter.type === FilterType.DATE) { - setFilterStates((prevStates: DataManagerTableState) => { - const newStates = { ...prevStates }; - newStates[filter.accessorKey] = { - ...getDefaultFilterState(filter), - preset: DatePreset.Custom, - start: null, - end: null, - isApplied: true, - } as unknown as FilterState; - void handleApplyFilters(newStates, filterOptions, pageSize); - setShowFilterEditor(false); - return newStates; - }); + applyFilterState(filter, { + ...getDefaultFilterState(filter), + preset: DatePreset.Custom, + start: null, + end: null, + isApplied: true, + } as unknown as FilterState); } }, subDropdown: diff --git a/packages/ui/src/components/Drawer.tsx b/packages/ui/src/components/Drawer.tsx index 7482bbfa8..4a15e14a5 100644 --- a/packages/ui/src/components/Drawer.tsx +++ b/packages/ui/src/components/Drawer.tsx @@ -2,6 +2,8 @@ import styled from "@emotion/styled"; import { useRef, useState } from "react"; +import { useIOSScrollLock } from "../hooks/useIOSScrollLock.js"; +import { useKeyboardOffset } from "../hooks/useKeyboardOffset.js"; import { standardFocusOutline } from "../styles/common.js"; import { Spacing } from "../styles/tokens/spacing.js"; import { z } from "../styles/z-index.js"; @@ -33,6 +35,10 @@ export const Drawer = (props: Props) => { const [grabbing, setGrabbing] = useState(false); const drawerContainerRef = useRef(null); + useIOSScrollLock(); + + const keyboardOffset = useKeyboardOffset(); + const handleClose = () => { if (props.nonDismissable) { return; @@ -99,6 +105,7 @@ export const Drawer = (props: Props) => { isOpen={isOpen} fractionVisible={fractionVisible} onClick={handleClose} + onTouchMove={(e) => e.preventDefault()} /> { totalDeltaY={totalDeltaY} grabbing={grabbing} ref={drawerContainerRef} + keyboardOffset={keyboardOffset} + data-drawer-scrollable top={props.top} > { const Background = styled.div<{ isOpen: boolean; fractionVisible: number }>` position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; + inset: 0; + max-height: 100dvh; + overflow: hidden; background-color: rgba(0, 0, 0, 0.5); z-index: ${z.modalOverlay}; @@ -178,19 +186,18 @@ const DrawerContainer = styled.div<{ isOpen: boolean; totalDeltaY: number; grabbing: boolean; + keyboardOffset: number; top?: number | undefined; }>` position: fixed; max-height: 100dvh; width: 100%; - right: 0; - bottom: 0; + bottom: ${(props) => props.keyboardOffset ?? 0}px; transform: translateY(${(props) => `${props.totalDeltaY}px`}); z-index: ${z.modalContainer}; display: flex; flex-direction: column; align-items: center; - ${(props) => props.top && `top: ${props.top}px;`} // Only smooth transition when not grabbing, otherwise dragging will feel very laggy @@ -234,7 +241,9 @@ const DrawerInnerContainer = styled.div<{ width: ${(props) => props.kind === "floating" ? `calc(100% - ${Spacing.md * 2}px)` : "100%"}; min-width: 320px; - ${(props) => (props.kind === "floating" ? `bottom: ${Spacing.px.md};` : "")} + ${(props) => (props.kind === "floating" ? `bottom: 0;` : "")} + margin-bottom: ${Spacing.px.md}; + margin-top: ${Spacing.px.md}; height: auto; border-radius: ${(props) => props.kind === "floating" diff --git a/packages/ui/src/components/Flex.tsx b/packages/ui/src/components/Flex.tsx index 0029bacfc..f20be4e6c 100644 --- a/packages/ui/src/components/Flex.tsx +++ b/packages/ui/src/components/Flex.tsx @@ -37,7 +37,7 @@ type FlexProps = { | "pre-line" | undefined; height?: number | "auto" | "100%" | undefined; - width?: number | "auto" | "100%" | undefined; + width?: number | "auto" | "50%" | "100%" | undefined; mt?: number | "auto" | undefined; mr?: number | "auto" | undefined; mb?: number | "auto" | undefined; diff --git a/packages/ui/src/components/Modal.tsx b/packages/ui/src/components/Modal.tsx index 2c986812c..77a8b6eb4 100644 --- a/packages/ui/src/components/Modal.tsx +++ b/packages/ui/src/components/Modal.tsx @@ -420,9 +420,9 @@ export function Modal({ handleBack={handleBack} kind={drawerKind ?? "default"} padding={drawerPadding !== undefined ? `${drawerPadding}px` : undefined} - top={top} alignBottom={drawerAlignBottom} disableTouchMove={drawerDisableTouchMove ?? false} + top={top} > {modalContent} @@ -500,7 +500,7 @@ const ModalOverlay = styled.div<{ left: 0; z-index: ${z.modalOverlay}; width: 100vw; - height: 100vh; + height: 100dvh; background: ${({ overlayBackground, theme }) => overlayBackground ? getColor(theme, overlayBackground) diff --git a/packages/ui/src/components/TextInput.tsx b/packages/ui/src/components/TextInput.tsx index 9e5eadbb9..e57834352 100644 --- a/packages/ui/src/components/TextInput.tsx +++ b/packages/ui/src/components/TextInput.tsx @@ -111,7 +111,9 @@ export type TextInputProps = { onBlur?: (event: FocusEvent) => void; onChange: (newValue: string, event: ChangeEvent) => void; onEnter?: () => void; - onFocus?: (event: FocusEvent) => void; + onFocus?: + | ((event: FocusEvent) => void) + | undefined; onPaste?: (event: ClipboardEvent) => void; onKeyDown?: ( keyValue: string, diff --git a/packages/ui/src/hooks/index.tsx b/packages/ui/src/hooks/index.tsx index 8df1054ae..43ba6ecce 100644 --- a/packages/ui/src/hooks/index.tsx +++ b/packages/ui/src/hooks/index.tsx @@ -2,7 +2,9 @@ export { useClipboard } from "./useClipboard.js"; export { useDebounce } from "./useDebounce.js"; export { useDocumentTitle } from "./useDocumentTitle.js"; export { default as useFields } from "./useFields.js"; +export { useIOSScrollLock } from "./useIOSScrollLock.js"; export { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect.js"; +export { useKeyboardOffset } from "./useKeyboardOffset.js"; export { useLiveRef } from "./useLiveRef.js"; export { useMaxScaleIOS } from "./useMaxScaleIOS.js"; export { useNumberInput } from "./useNumberInput/useNumberInput.js"; diff --git a/packages/ui/src/hooks/useFields.tsx b/packages/ui/src/hooks/useFields.tsx index db9b67552..567009b22 100644 --- a/packages/ui/src/hooks/useFields.tsx +++ b/packages/ui/src/hooks/useFields.tsx @@ -36,7 +36,7 @@ const defaultMsgs = { }; const regexp = { - phone: /^[2-9]{1}[0-9]{9}$/, + phone: /^[0-9]{7,15}$/, postalCode: /(^\d{5}$)|(^\d{5}-\d{4}$)/, email: /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/, state: diff --git a/packages/ui/src/hooks/useIOSScrollLock.tsx b/packages/ui/src/hooks/useIOSScrollLock.tsx new file mode 100644 index 000000000..5edd3b88a --- /dev/null +++ b/packages/ui/src/hooks/useIOSScrollLock.tsx @@ -0,0 +1,127 @@ +"use client"; + +import { useEffect } from "react"; + +/** + * Find the closest scrollable ancestor element + */ +function getScrollableAncestor( + element: HTMLElement | null, +): HTMLElement | null { + while (element && element !== document.body) { + const style = window.getComputedStyle(element); + const overflowY = style.overflowY; + const isScrollable = + (overflowY === "auto" || overflowY === "scroll") && + element.scrollHeight > element.clientHeight; + if (isScrollable) { + return element; + } + element = element.parentElement; + } + return null; +} + +/** + * iOS scroll lock hook - prevents body scrolling when a modal/drawer is open. + * This is necessary because iOS Safari doesn't respect overflow:hidden on body. + * + * Uses data-drawer-scrollable attribute to identify scrollable containers. + */ +export function useIOSScrollLock() { + useEffect(() => { + const scrollY = window.scrollY; + const body = document.body; + const html = document.documentElement; + + const originalBodyStyles = { + position: body.style.position, + top: body.style.top, + left: body.style.left, + right: body.style.right, + overflow: body.style.overflow, + width: body.style.width, + height: body.style.height, + }; + const originalHtmlStyles = { + overflow: html.style.overflow, + height: html.style.height, + }; + + // Lock the body + body.style.position = "fixed"; + body.style.top = `-${scrollY}px`; + body.style.left = "0"; + body.style.right = "0"; + body.style.overflow = "hidden"; + body.style.width = "100%"; + body.style.height = "100%"; + + // Also lock html element for iOS + html.style.overflow = "hidden"; + html.style.height = "100%"; + + // Track touch start for direction detection + let touchStartY = 0; + + const handleTouchStart = (e: TouchEvent) => { + touchStartY = e.touches[0].clientY; + }; + + const handleTouchMove = (e: TouchEvent) => { + const target = e.target as HTMLElement; + + // Check if inside the drawer + if (!target.closest("[data-drawer-scrollable]")) { + e.preventDefault(); + return; + } + + // Find actual scrollable element + const scrollable = getScrollableAncestor(target); + if (!scrollable) { + // No scrollable content, prevent to avoid bleed + e.preventDefault(); + return; + } + + // Check scroll boundaries + const { scrollTop, scrollHeight, clientHeight } = scrollable; + const touchY = e.touches[0].clientY; + const isScrollingUp = touchY > touchStartY; + const isScrollingDown = touchY < touchStartY; + const isAtTop = scrollTop <= 0; + const isAtBottom = scrollTop + clientHeight >= scrollHeight - 1; + + // Prevent if at boundary and trying to scroll past it + if ((isAtTop && isScrollingUp) || (isAtBottom && isScrollingDown)) { + e.preventDefault(); + } + }; + + document.addEventListener("touchstart", handleTouchStart, { + passive: true, + }); + document.addEventListener("touchmove", handleTouchMove, { passive: false }); + + return () => { + // Restore original styles + body.style.position = originalBodyStyles.position; + body.style.top = originalBodyStyles.top; + body.style.left = originalBodyStyles.left; + body.style.right = originalBodyStyles.right; + body.style.overflow = originalBodyStyles.overflow; + body.style.width = originalBodyStyles.width; + body.style.height = originalBodyStyles.height; + + html.style.overflow = originalHtmlStyles.overflow; + html.style.height = originalHtmlStyles.height; + + // Restore scroll position + window.scrollTo(0, scrollY); + + document.removeEventListener("touchstart", handleTouchStart); + document.removeEventListener("touchmove", handleTouchMove); + }; + }, []); +} diff --git a/packages/ui/src/hooks/useKeyboardOffset.tsx b/packages/ui/src/hooks/useKeyboardOffset.tsx new file mode 100644 index 000000000..9a3e4061e --- /dev/null +++ b/packages/ui/src/hooks/useKeyboardOffset.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { useEffect, useState } from "react"; + +/** + * Hook to handle iOS keyboard viewport changes. + * Returns the offset needed to keep elements above the keyboard. + */ +export function useKeyboardOffset() { + const [keyboardOffset, setKeyboardOffset] = useState(0); + + useEffect(() => { + const viewport = window.visualViewport; + if (!viewport) return; + + const handleResize = () => { + // Calculate the difference between window height and visual viewport height + // This difference is the keyboard height + const offset = window.innerHeight - viewport.height; + setKeyboardOffset(Math.max(0, offset)); + }; + + // Initial check + handleResize(); + + viewport.addEventListener("resize", handleResize); + viewport.addEventListener("scroll", handleResize); + + return () => { + viewport.removeEventListener("resize", handleResize); + viewport.removeEventListener("scroll", handleResize); + }; + }, []); + + return keyboardOffset; +} diff --git a/packages/ui/src/icons/central/ChevronDoubleDown.tsx b/packages/ui/src/icons/central/ChevronDoubleDown.tsx new file mode 100644 index 000000000..ab351638e --- /dev/null +++ b/packages/ui/src/icons/central/ChevronDoubleDown.tsx @@ -0,0 +1,25 @@ +import { type PathProps } from "../types.js"; + +export function ChevronDoubleDown({ + strokeWidth = "1.5", + strokeLinecap = "round", + strokeLinejoin = "round", +}: PathProps) { + return ( + + + + ); +} diff --git a/packages/ui/src/icons/central/ChevronDoubleUp.tsx b/packages/ui/src/icons/central/ChevronDoubleUp.tsx new file mode 100644 index 000000000..d60b89aba --- /dev/null +++ b/packages/ui/src/icons/central/ChevronDoubleUp.tsx @@ -0,0 +1,25 @@ +import { type PathProps } from "../types.js"; + +export function ChevronDoubleUp({ + strokeWidth = "1.5", + strokeLinecap = "round", + strokeLinejoin = "round", +}: PathProps) { + return ( + + + + ); +} diff --git a/packages/ui/src/icons/central/FileText.tsx b/packages/ui/src/icons/central/FileText.tsx new file mode 100644 index 000000000..536d3ce72 --- /dev/null +++ b/packages/ui/src/icons/central/FileText.tsx @@ -0,0 +1,36 @@ +import { type PathProps } from "../types.js"; + +export function FileText({ + strokeWidth = "2", + strokeLinecap = "round", + strokeLinejoin = "round", +}: PathProps) { + return ( + + + + + + ); +} diff --git a/packages/ui/src/icons/central/QuestionCircle.tsx b/packages/ui/src/icons/central/QuestionCircle.tsx new file mode 100644 index 000000000..76aca9d35 --- /dev/null +++ b/packages/ui/src/icons/central/QuestionCircle.tsx @@ -0,0 +1,26 @@ +// Copyright ©, 2022, Lightspark Group, Inc. - All Rights Reserved + +import { type PathProps } from "../types.js"; + +export function QuestionCircle({ + strokeWidth = "1.5", + strokeLinecap = "round", + strokeLinejoin = "round", +}: PathProps) { + return ( + + + + ); +} diff --git a/packages/ui/src/icons/central/WarningSign.tsx b/packages/ui/src/icons/central/WarningSign.tsx new file mode 100644 index 000000000..68c4e1351 --- /dev/null +++ b/packages/ui/src/icons/central/WarningSign.tsx @@ -0,0 +1,27 @@ +// Copyright ©, 2022, Lightspark Group, Inc. - All Rights Reserved + +import { type PathProps } from "../types.js"; + +export function WarningSign({ + strokeWidth = "2", + strokeLinecap = "round", +}: PathProps) { + return ( + + + + + + ); +} diff --git a/packages/ui/src/icons/central/index.tsx b/packages/ui/src/icons/central/index.tsx index 66327a807..44f5028dd 100644 --- a/packages/ui/src/icons/central/index.tsx +++ b/packages/ui/src/icons/central/index.tsx @@ -30,6 +30,8 @@ export { Checkmark2Small as CentralCheckmark2Small } from "./Checkmark2Small.js" export { ChevronBottom as CentralChevronBottom } from "./ChevronBottom.js"; export { ChevronBottomSm as CentralChevronBottomSm } from "./ChevronBottomSm.js"; export { ChevronBottomXs as CentralChevronBottomXs } from "./ChevronBottomXs.js"; +export { ChevronDoubleDown as CentralChevronDoubleDown } from "./ChevronDoubleDown.js"; +export { ChevronDoubleUp as CentralChevronDoubleUp } from "./ChevronDoubleUp.js"; export { ChevronDownSmall as CentralChevronDownSmall } from "./ChevronDownSmall.js"; export { ChevronLeft as CentralChevronLeft } from "./ChevronLeft.js"; export { ChevronLeftSm as CentralChevronLeftSm } from "./ChevronLeftSm.js"; @@ -68,6 +70,7 @@ export { EmptyStrengthIcon as CentralEmptyStrengthIcon } from "./EmptyStrength.j export { EyeOpen as CentralEyeOpen } from "./EyeOpen.js"; export { EyeSlash as CentralEyeSlash } from "./EyeSlash.js"; export { FileBend as CentralFileBend } from "./FileBend.js"; +export { FileText as CentralFileText } from "./FileText.js"; export { Filter2 as CentralFilter2 } from "./Filter2.js"; export { FingerPrint1 as CentralFingerPrint1 } from "./FingerPrint1.js"; export { Gift as CentralGift } from "./Gift.js"; @@ -113,6 +116,7 @@ export { PlusSmall as CentralPlusSmall } from "./PlusSmall.js"; export { PointChart as CentralPointChart } from "./PointChart.js"; export { Postcard as CentralPostcard } from "./Postcard.js"; export { QrCode as CentralQrCode } from "./QrCode.js"; +export { QuestionCircle as CentralQuestionCircle } from "./QuestionCircle.js"; export { ReceiptBill as CentralReceiptBill } from "./ReceiptBill.js"; export { RunShortcut as CentralRunShortcut } from "./RunShortcut.js"; export { SecretPhrase as CentralSecretPhrase } from "./SecretPhrase.js"; @@ -140,5 +144,6 @@ export { Turtle as CentralTurtle } from "./Turtle.js"; export { Variables as CentralVariables } from "./Variables.js"; export { Vignette as CentralVignette } from "./Vignette.js"; export { Wallet3 as CentralWallet3 } from "./Wallet3.js"; +export { WarningSign as CentralWarningSign } from "./WarningSign.js"; export { WeakStrengthIcon as CentralWeakStrengthIcon } from "./WeakStrength.js"; export { Wrench as CentralWrench } from "./Wrench.js"; diff --git a/packages/ui/src/styles/colors.tsx b/packages/ui/src/styles/colors.tsx index c4fba9b31..7f532a1a3 100644 --- a/packages/ui/src/styles/colors.tsx +++ b/packages/ui/src/styles/colors.tsx @@ -188,6 +188,14 @@ const baseColors = { transparent: "transparent", transparenta02: "#00000005", transparenta08: "#00000014", + // hardcore radioactive green + radioactiveGreen: "#39FF14", + radioactiveGreenDark: "#2ECC0F", + radioactiveGreenDarker: "#23990B", + radioactiveGreenLight: "#5FFF45", + radioactiveGreen10: "#39FF1419", + radioactiveGreen20: "#39FF1433", + radioactiveGreen30: "#39FF144D", } as const; /* We only want `as const` to affect keys, the values should be widened to strings: */ diff --git a/packages/ui/src/styles/themeDefaults/buttons.tsx b/packages/ui/src/styles/themeDefaults/buttons.tsx index 85fd06323..c77c1014a 100644 --- a/packages/ui/src/styles/themeDefaults/buttons.tsx +++ b/packages/ui/src/styles/themeDefaults/buttons.tsx @@ -5,7 +5,7 @@ import { type TokenSizeKey } from "../tokens/typography.js"; export type PaddingYKey = "short" | "regular"; export type PaddingY = number | { [key in PaddingYKey]: number }; -export const buttonBorderRadiuses = [4, 8, 32, 999, "100%"] as const; +export const buttonBorderRadiuses = [0, 4, 8, 32, 999, "100%"] as const; export type ButtonBorderRadius = (typeof buttonBorderRadiuses)[number]; export const allowedButtonTypographyTypes = [ "Body", diff --git a/packages/ui/src/styles/themeDefaults/cardForm.tsx b/packages/ui/src/styles/themeDefaults/cardForm.tsx index e714f9480..1f71d4db7 100644 --- a/packages/ui/src/styles/themeDefaults/cardForm.tsx +++ b/packages/ui/src/styles/themeDefaults/cardForm.tsx @@ -7,7 +7,7 @@ export type CardFormPaddingX = 0 | 24 | 32 | 40 | 56; export type CardFormPaddingTop = 0 | 24 | 32 | 40 | 56; export type CardFormPaddingBottom = 0 | 24 | 32 | 40 | 56 | 64; export type CardFormBorderWidth = 0 | 1; -export type CardFormBorderRadius = 8 | 24 | 32; +export type CardFormBorderRadius = 0 | 8 | 24 | 32; export type CardFormShadow = "soft" | "hard" | "none"; export type CardFormTextAlign = "center" | "left"; export type CardFormBorderColor = "vlcNeutral" | "grayBlue94" | "gray3"; diff --git a/packages/ui/src/styles/themes.tsx b/packages/ui/src/styles/themes.tsx index 8b34de430..1ec31b30a 100644 --- a/packages/ui/src/styles/themes.tsx +++ b/packages/ui/src/styles/themes.tsx @@ -41,6 +41,7 @@ export enum Themes { BridgeDark = "bridgeDark", NageLight = "nageLight", NageDark = "nageDark", + Hardcore = "hardcore", } export function isTheme(theme: unknown): theme is Themes { @@ -869,6 +870,100 @@ const nageDarkTheme = extend(darkTheme, { type: Themes.NageDark, }); +const hardcoreButtons = merge(buttonsThemeBase, { + defaultTypographyType: "Title", + defaultSize: "Medium", + defaultBorderRadius: 0, + kinds: { + primary: { + defaultColor: "black", + defaultBackgroundColor: "radioactiveGreen", + defaultBorderColor: "radioactiveGreen", + defaultHoverBackgroundColor: "radioactiveGreenLight", + defaultHoverBorderColor: "radioactiveGreenLight", + defaultActiveBackgroundColor: "radioactiveGreenDark", + defaultActiveBorderColor: "radioactiveGreenDark", + }, + secondary: { + defaultColor: "radioactiveGreen", + defaultBackgroundColor: "transparent", + defaultBorderColor: "radioactiveGreen", + defaultHoverBackgroundColor: "radioactiveGreen10", + defaultHoverBorderColor: "radioactiveGreenLight", + defaultActiveBackgroundColor: "radioactiveGreen20", + defaultActiveBorderColor: "radioactiveGreenDark", + }, + tertiary: { + defaultColor: "radioactiveGreen", + defaultBackgroundColor: "gray10", + defaultBorderColor: "radioactiveGreen", + defaultHoverBackgroundColor: "gray15", + defaultHoverBorderColor: "radioactiveGreenLight", + defaultActiveBackgroundColor: "gray20", + defaultActiveBorderColor: "radioactiveGreenDark", + }, + ghost: { + defaultColor: "radioactiveGreen", + defaultBackgroundColor: "transparent", + defaultBorderColor: "radioactiveGreen", + defaultHoverBackgroundColor: "radioactiveGreen10", + defaultHoverBorderColor: "radioactiveGreenLight", + }, + danger: { + defaultColor: "black", + defaultBackgroundColor: "danger", + defaultBorderColor: "danger", + defaultHoverBackgroundColor: "red50", + defaultHoverBorderColor: "red50", + }, + }, +}); + +const hardcoreBaseTheme: BaseTheme = { + ...darkBaseTheme, + type: Themes.Hardcore, + bg: colors.black, + smBg: colors.black, + text: colors.radioactiveGreen, + hcNeutral: colors.radioactiveGreen, + mcNeutral: colors.radioactiveGreenDark, + lcNeutral: colors.radioactiveGreenDarker, + vlcNeutral: colors.gray15, + secondary: colors.radioactiveGreenDark, + tertiary: colors.radioactiveGreen, + primary: colors.radioactiveGreen, + link: colors.radioactiveGreenLight, + linkLight: colors.radioactiveGreen10, + success: colors.radioactiveGreen, + info: colors.radioactiveGreen, + c05Neutral: colors.radioactiveGreenDarker, + c1Neutral: colors.radioactiveGreenDarker, + c15Neutral: colors.radioactiveGreenDark, + c2Neutral: colors.radioactiveGreenDark, + c3Neutral: colors.radioactiveGreen, + c4Neutral: colors.radioactiveGreenLight, + border: colors.radioactiveGreen30, + inputBackground: colors.black, + typography: getTypography(TypographyGroup.Hardcore, { + main: "Roboto Mono", + code: "Roboto Mono", + }), + buttons: hardcoreButtons, + badge: { + bg: "c15Neutral" as ThemeOrColorKey, + }, +}; + +const hardcoreTheme = extend(hardcoreBaseTheme, { + header: extendBase(hardcoreBaseTheme, {}), + nav: extendBase(hardcoreBaseTheme, {}), + content: extendBase(hardcoreBaseTheme, {}), + controls: extendBase(hardcoreBaseTheme, { + bg: colors.gray10, + smBg: colors.gray10, + }), +}); + export const themes = { [Themes.Light]: lightTheme, [Themes.Dark]: darkTheme, @@ -882,6 +977,7 @@ export const themes = { [Themes.BridgeDark]: bridgeDarkTheme, [Themes.NageLight]: nageLightTheme, [Themes.NageDark]: nageDarkTheme, + [Themes.Hardcore]: hardcoreTheme, } as const; function extend(obj: BaseTheme, rest: Partial) { @@ -1000,6 +1096,7 @@ export const isDark = (theme: Theme) => Themes.LightsparkDocsDark, Themes.UmameDocsDark, Themes.UmaAuthSdkDark, + Themes.Hardcore, ].includes(theme.type); export const isLight = (theme: Theme) => [ diff --git a/packages/ui/src/styles/tokens/typography.ts b/packages/ui/src/styles/tokens/typography.ts index 22e75bde7..e96ae04e5 100644 --- a/packages/ui/src/styles/tokens/typography.ts +++ b/packages/ui/src/styles/tokens/typography.ts @@ -8,6 +8,7 @@ export enum TypographyGroup { UmameDocs = "UmameDocs", Bridge = "Bridge", Nage = "Nage", + Hardcore = "Hardcore", } export type TypographyTokenGroups = ReturnType; @@ -109,6 +110,22 @@ const LINE_HEIGHTS = { xl: "24px", xs: "16px", }, + [TypographyGroup.Hardcore]: { + "8xl": "72px", + "7xl": "60px", + "6xl": "44px", + "5xl": "40px", + "4xl": "36px", + "3xl": "32px", + "3xs": "12px", + "2xs": "14px", + "2xl": "28px", + lg: "22px", + md: "20px", + sm: "18px", + xl: "24px", + xs: "16px", + }, } as const; export type LineHeightKey = keyof (typeof LINE_HEIGHTS)[TypographyGroup]; @@ -218,6 +235,19 @@ const FONT_SIZE = { "6xl": "48px", "7xl": "64px", }, + [TypographyGroup.Hardcore]: { + "2xs": "10px", + xs: "12px", + md: "14px", + lg: "16px", + xl: "20px", + "2xl": "24px", + "3xl": "28px", + "4xl": "32px", + "5xl": "36px", + "6xl": "48px", + "7xl": "64px", + }, }; const LETTER_SPACING = { @@ -259,6 +289,11 @@ const LETTER_SPACING = { wide: "0.2px", wider: "0.7px", }, + [TypographyGroup.Hardcore]: { + tight: "-0.2px", + normal: "0", + wide: "0.2px", + }, }; const TEXT_CASE = { @@ -5433,6 +5468,432 @@ function getTypographyTypes(fontFamilies: FontFamilies): TypographyTypes { }, }, }, + [TypographyGroup.Hardcore]: { + Mobile: { + [TypographyType.Display]: { + [TokenSize.Large]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["7xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["6xl"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Medium]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["6xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["5xl"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Small]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["5xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["4xl"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + }, + [TypographyType.Headline]: { + [TokenSize.Large]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["4xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["3xl"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Medium]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["3xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["2xl"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Small]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["2xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["xl"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + }, + [TypographyType.Title]: { + [TokenSize.Large]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["lg"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Medium]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["lg"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["md"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Small]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["md"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["xs"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + }, + [TypographyType.Body]: { + [TokenSize.Large]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["lg"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["md"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Medium]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["md"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["xs"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Small]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["sm"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["2xs"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + }, + [TypographyType.Label]: { + [TokenSize.Large]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["md"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["xs"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].wide}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.uppercase}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Medium]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["sm"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["2xs"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].wide}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.uppercase}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Small]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["xs"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["2xs"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].wide}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.uppercase}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + }, + [TypographyType.Code]: { + [TokenSize.Large]: { + fontFamily: `${fontFamilies.code}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["lg"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["md"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Medium]: { + fontFamily: `${fontFamilies.code}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["md"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["xs"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Small]: { + fontFamily: `${fontFamilies.code}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["sm"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["2xs"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + }, + }, + Desktop: { + [TypographyType.Display]: { + [TokenSize.Large]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["8xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["7xl"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Medium]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["7xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["6xl"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Small]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["6xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["5xl"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + }, + [TypographyType.Headline]: { + [TokenSize.Large]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["5xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["4xl"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Medium]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["4xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["3xl"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Small]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["3xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["2xl"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + }, + [TypographyType.Title]: { + [TokenSize.Large]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["2xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["xl"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Medium]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["lg"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Small]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["lg"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["md"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + }, + [TypographyType.Body]: { + [TokenSize.Large]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["lg"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Medium]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["lg"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["md"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Small]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["md"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["xs"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + }, + [TypographyType.Label]: { + [TokenSize.Large]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["lg"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["md"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].wide}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.uppercase}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Medium]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["md"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["xs"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].wide}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.uppercase}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Small]: { + fontFamily: `${fontFamilies.main}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["sm"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["2xs"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].wide}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.uppercase}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + }, + [TypographyType.Code]: { + [TokenSize.Large]: { + fontFamily: `${fontFamilies.code}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["xl"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["lg"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Medium]: { + fontFamily: `${fontFamilies.code}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["lg"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["md"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + [TokenSize.Small]: { + fontFamily: `${fontFamilies.code}`, + fontWeight: `${FONT_WEIGHTS.main.Regular}`, + lineHeight: `${LINE_HEIGHTS[TypographyGroup.Hardcore]["md"]}`, + fontSize: `${FONT_SIZE[TypographyGroup.Hardcore]["xs"]}`, + letterSpacing: `${LETTER_SPACING[TypographyGroup.Hardcore].normal}`, + paragraphSpacing: `${PARAGRAPH_SPACING[0]}`, + paragraphIndent: `${PARAGRAPH_INDENT[0]}`, + textCase: `${TEXT_CASE.none}`, + textDecoration: `${TEXT_DECORATION.none}`, + }, + }, + }, + }, }; }