diff --git a/apps/examples/ui-test-app/src/tests/CodeInput.test.tsx b/apps/examples/ui-test-app/src/tests/CodeInput.test.tsx
index 249173fdc..e256b83ee 100644
--- a/apps/examples/ui-test-app/src/tests/CodeInput.test.tsx
+++ b/apps/examples/ui-test-app/src/tests/CodeInput.test.tsx
@@ -1,6 +1,7 @@
import { jest } from "@jest/globals";
import { CodeInput } from "@lightsparkdev/ui/components/CodeInput/CodeInput";
import { fireEvent, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
import { render } from "./render";
describe("CodeInput", () => {
@@ -8,10 +9,13 @@ describe("CodeInput", () => {
const mockClipboardReadWithoutNumbers = jest.fn(() =>
Promise.resolve("sdkjfnsd"),
);
- Object.assign(navigator, {
- clipboard: {
+ // `userEvent.setup()` may install `navigator.clipboard` as a getter-only prop.
+ // Use `defineProperty` so this mock is resilient regardless of that setup.
+ Object.defineProperty(navigator, "clipboard", {
+ value: {
readText: mockClipboardReadWithoutNumbers,
},
+ configurable: true,
});
});
@@ -134,4 +138,61 @@ describe("CodeInput", () => {
expect(inputFields[3]).toHaveValue(null);
expect(inputFields[2]).toHaveFocus();
});
+
+ it("redirects focus to first empty input when clicking on empty input in unified variant", async () => {
+ const user = userEvent.setup();
+ render();
+ const inputFields = screen.getAllByRole("textbox");
+ expect(inputFields).toHaveLength(6);
+
+ // Enter some digits in the first two positions
+ fireEvent.keyDown(inputFields[0], { key: "1" });
+ fireEvent.keyDown(inputFields[1], { key: "2" });
+
+ // Now focus should be on the third input (index 2)
+ expect(inputFields[2]).toHaveFocus();
+
+ // Simulate clicking on the 5th input (index 4) - an empty position
+ // onMouseDown should redirect focus to the first empty input (index 2)
+ await user.click(inputFields[4]);
+ expect(inputFields[2]).toHaveFocus();
+ });
+
+ it("allows clicking on any filled input in unified variant", async () => {
+ const user = userEvent.setup();
+ render();
+ const inputFields = screen.getAllByRole("textbox");
+
+ // Enter some digits
+ fireEvent.keyDown(inputFields[0], { key: "1" });
+ fireEvent.keyDown(inputFields[1], { key: "2" });
+ fireEvent.keyDown(inputFields[2], { key: "3" });
+
+ // Focus should be on the 4th input (index 3)
+ expect(inputFields[3]).toHaveFocus();
+
+ // Clicking on a filled input (index 1) should work normally - no redirect
+ await user.click(inputFields[1]);
+ expect(inputFields[1]).toHaveFocus();
+
+ // Can also click on index 0
+ await user.click(inputFields[0]);
+ expect(inputFields[0]).toHaveFocus();
+
+ // Can also click on index 2
+ await user.click(inputFields[2]);
+ expect(inputFields[2]).toHaveFocus();
+ });
+
+ it("focuses first input when all inputs are empty in unified variant", () => {
+ render();
+ const inputFields = screen.getAllByRole("textbox");
+
+ // Blur the auto-focused first input
+ fireEvent.blur(inputFields[0]);
+
+ // Click on a middle input when all are empty
+ fireEvent.mouseDown(inputFields[3]);
+ expect(inputFields[0]).toHaveFocus();
+ });
});
diff --git a/packages/core/src/utils/currency.ts b/packages/core/src/utils/currency.ts
index 56b40a2a0..cc02068b9 100644
--- a/packages/core/src/utils/currency.ts
+++ b/packages/core/src/utils/currency.ts
@@ -43,6 +43,7 @@ export const CurrencyUnit = {
XAF: "XAF",
MWK: "MWK",
RWF: "RWF",
+ ZMW: "ZMW",
USDT: "USDT",
USDC: "USDC",
@@ -110,6 +111,7 @@ const standardUnitConversionObj = {
[CurrencyUnit.XAF]: (v: number) => v,
[CurrencyUnit.MWK]: (v: number) => v,
[CurrencyUnit.RWF]: (v: number) => v,
+ [CurrencyUnit.ZMW]: (v: number) => v,
[CurrencyUnit.USDT]: (v: number) => v,
[CurrencyUnit.USDC]: (v: number) => v,
};
@@ -161,6 +163,7 @@ const CONVERSION_MAP = {
[CurrencyUnit.XAF]: toBitcoinConversion,
[CurrencyUnit.MWK]: toBitcoinConversion,
[CurrencyUnit.RWF]: toBitcoinConversion,
+ [CurrencyUnit.ZMW]: toBitcoinConversion,
[CurrencyUnit.USDT]: toBitcoinConversion,
[CurrencyUnit.USDC]: toBitcoinConversion,
},
@@ -196,6 +199,7 @@ const CONVERSION_MAP = {
[CurrencyUnit.XAF]: toMicrobitcoinConversion,
[CurrencyUnit.MWK]: toMicrobitcoinConversion,
[CurrencyUnit.RWF]: toMicrobitcoinConversion,
+ [CurrencyUnit.ZMW]: toMicrobitcoinConversion,
[CurrencyUnit.USDT]: toMicrobitcoinConversion,
[CurrencyUnit.USDC]: toMicrobitcoinConversion,
},
@@ -231,6 +235,7 @@ const CONVERSION_MAP = {
[CurrencyUnit.XAF]: toMillibitcoinConversion,
[CurrencyUnit.MWK]: toMillibitcoinConversion,
[CurrencyUnit.RWF]: toMillibitcoinConversion,
+ [CurrencyUnit.ZMW]: toMillibitcoinConversion,
[CurrencyUnit.USDT]: toMillibitcoinConversion,
[CurrencyUnit.USDC]: toMillibitcoinConversion,
},
@@ -266,6 +271,7 @@ const CONVERSION_MAP = {
[CurrencyUnit.XAF]: toMillisatoshiConversion,
[CurrencyUnit.MWK]: toMillisatoshiConversion,
[CurrencyUnit.RWF]: toMillisatoshiConversion,
+ [CurrencyUnit.ZMW]: toMillisatoshiConversion,
[CurrencyUnit.USDT]: toMillisatoshiConversion,
[CurrencyUnit.USDC]: toMillisatoshiConversion,
},
@@ -301,6 +307,7 @@ const CONVERSION_MAP = {
[CurrencyUnit.XAF]: toNanobitcoinConversion,
[CurrencyUnit.MWK]: toNanobitcoinConversion,
[CurrencyUnit.RWF]: toNanobitcoinConversion,
+ [CurrencyUnit.ZMW]: toNanobitcoinConversion,
[CurrencyUnit.USDT]: toNanobitcoinConversion,
[CurrencyUnit.USDC]: toNanobitcoinConversion,
},
@@ -336,6 +343,7 @@ const CONVERSION_MAP = {
[CurrencyUnit.XAF]: toSatoshiConversion,
[CurrencyUnit.MWK]: toSatoshiConversion,
[CurrencyUnit.RWF]: toSatoshiConversion,
+ [CurrencyUnit.ZMW]: toSatoshiConversion,
[CurrencyUnit.USDT]: toSatoshiConversion,
[CurrencyUnit.USDC]: toSatoshiConversion,
},
@@ -364,6 +372,7 @@ const CONVERSION_MAP = {
[CurrencyUnit.XAF]: standardUnitConversionObj,
[CurrencyUnit.MWK]: standardUnitConversionObj,
[CurrencyUnit.RWF]: standardUnitConversionObj,
+ [CurrencyUnit.ZMW]: standardUnitConversionObj,
[CurrencyUnit.USDT]: standardUnitConversionObj,
[CurrencyUnit.USDC]: standardUnitConversionObj,
};
@@ -452,6 +461,7 @@ export type CurrencyMap = {
[CurrencyUnit.XAF]: number;
[CurrencyUnit.MWK]: number;
[CurrencyUnit.RWF]: number;
+ [CurrencyUnit.ZMW]: number;
[CurrencyUnit.USDT]: number;
[CurrencyUnit.USDC]: number;
[CurrencyUnit.FUTURE_VALUE]: number;
@@ -490,6 +500,7 @@ export type CurrencyMap = {
[CurrencyUnit.XAF]: string;
[CurrencyUnit.MWK]: string;
[CurrencyUnit.RWF]: string;
+ [CurrencyUnit.ZMW]: string;
[CurrencyUnit.USDT]: string;
[CurrencyUnit.USDC]: string;
[CurrencyUnit.FUTURE_VALUE]: string;
@@ -709,6 +720,7 @@ function convertCurrencyAmountValues(
xaf: CurrencyUnit.XAF,
mwk: CurrencyUnit.MWK,
rwf: CurrencyUnit.RWF,
+ zmw: CurrencyUnit.ZMW,
mibtc: CurrencyUnit.MICROBITCOIN,
mlbtc: CurrencyUnit.MILLIBITCOIN,
nbtc: CurrencyUnit.NANOBITCOIN,
@@ -792,6 +804,7 @@ export function mapCurrencyAmount(
xaf,
mwk,
rwf,
+ zmw,
usdt,
usdc,
} = convertCurrencyAmountValues(unit, value, unitsPerBtc, conversionOverride);
@@ -825,6 +838,7 @@ export function mapCurrencyAmount(
[CurrencyUnit.XAF]: xaf,
[CurrencyUnit.MWK]: mwk,
[CurrencyUnit.RWF]: rwf,
+ [CurrencyUnit.ZMW]: zmw,
[CurrencyUnit.MICROBITCOIN]: mibtc,
[CurrencyUnit.MILLIBITCOIN]: mlbtc,
[CurrencyUnit.NANOBITCOIN]: nbtc,
@@ -956,6 +970,10 @@ export function mapCurrencyAmount(
value: rwf,
unit: CurrencyUnit.RWF,
}),
+ [CurrencyUnit.ZMW]: formatCurrencyStr({
+ value: zmw,
+ unit: CurrencyUnit.ZMW,
+ }),
[CurrencyUnit.USDT]: formatCurrencyStr({
value: usdt,
unit: CurrencyUnit.USDT,
@@ -1086,6 +1104,8 @@ export const abbrCurrencyUnit = (unit: CurrencyUnitType) => {
return "MWK";
case CurrencyUnit.RWF:
return "RWF";
+ case CurrencyUnit.ZMW:
+ return "ZMW";
}
return "Unsupported CurrencyUnit";
};
diff --git a/packages/ui/src/components/CardForm/CardForm.tsx b/packages/ui/src/components/CardForm/CardForm.tsx
index d9e512c39..1a3779a04 100644
--- a/packages/ui/src/components/CardForm/CardForm.tsx
+++ b/packages/ui/src/components/CardForm/CardForm.tsx
@@ -460,6 +460,7 @@ const CardFormContentFull = styled.div<{ paddingBottom?: number | undefined }>`
flex-direction: column;
align-self: center;
height: 100%;
+ width: 100%;
padding-bottom: ${({ paddingBottom }) => paddingBottom ?? 0}px;
`;
diff --git a/packages/ui/src/components/CardPage.tsx b/packages/ui/src/components/CardPage.tsx
index 8846e4fdc..1957b5880 100644
--- a/packages/ui/src/components/CardPage.tsx
+++ b/packages/ui/src/components/CardPage.tsx
@@ -28,6 +28,7 @@ type Props = {
maxContentWidth?: number;
rightContent?: React.ReactNode;
preHeaderContent?: React.ReactNode;
+ headerRightContent?: React.ReactNode;
expandRight?: boolean;
id?: string;
};
@@ -43,6 +44,9 @@ export function CardPage(props: Props) {
{props.title}
+ {props.headerRightContent && (
+ {props.headerRightContent}
+ )}
) : null;
@@ -350,6 +354,11 @@ const CardPageHeader = styled.div<{ headerMarginBottom?: number }>`
}
`;
+const CardPageHeaderRight = styled.div`
+ display: flex;
+ align-items: center;
+`;
+
export const CardPageContent = styled.div`
${({
maxContentWidth,
diff --git a/packages/ui/src/components/CodeInput/CodeInput.tsx b/packages/ui/src/components/CodeInput/CodeInput.tsx
index e5e38d02a..b6af4a32d 100644
--- a/packages/ui/src/components/CodeInput/CodeInput.tsx
+++ b/packages/ui/src/components/CodeInput/CodeInput.tsx
@@ -331,6 +331,27 @@ export function CodeInput({
const inputsPerGroup = Math.ceil(codeLength / 2);
+ /**
+ * When clicking on the unified code input container, handle focus appropriately
+ * Uses onMouseDown instead of onClick because mousedown fires before focus,
+ * allowing us to prevent the default focus behavior and redirect to the correct input.
+ */
+ const onContainerMouseDown = useCallback(
+ (event: React.MouseEvent) => {
+ const target = event.target as HTMLInputElement;
+ const isClickingFilledInput =
+ target.tagName === "INPUT" && inputState[target.id]?.value !== "";
+ if (!isClickingFilledInput) {
+ event.preventDefault();
+ const firstEmptyIndex = codeFromInputState(inputState).length;
+ const targetIndex =
+ firstEmptyIndex < codeLength ? firstEmptyIndex : codeLength - 1;
+ getRef(getInputId(targetIndex), inputRefs)?.focus();
+ }
+ },
+ [codeLength, getInputId, inputState, inputRefs],
+ );
+
const inputs = [];
for (let i = 0; i < codeLength; i += 1) {
const inputId = getInputId(i);
@@ -413,7 +434,11 @@ export function CodeInput({
}
if (variant === "unified") {
- return {inputs};
+ return (
+
+ {inputs}
+
+ );
}
return (
diff --git a/packages/ui/src/components/DataManagerTable/AppliedButtonsContainer.tsx b/packages/ui/src/components/DataManagerTable/AppliedButtonsContainer.tsx
new file mode 100644
index 000000000..780d7b8db
--- /dev/null
+++ b/packages/ui/src/components/DataManagerTable/AppliedButtonsContainer.tsx
@@ -0,0 +1,14 @@
+import styled from "@emotion/styled";
+import { Spacing } from "../../styles/tokens/spacing.js";
+import { ButtonSelector } from "../Button.js";
+
+export const AppliedButtonsContainer = styled.div`
+ margin-top: ${Spacing.px.sm};
+ display: flex;
+ gap: ${Spacing.px.xs};
+ flex-wrap: wrap;
+
+ ${ButtonSelector()} {
+ max-width: 100%;
+ }
+`;
diff --git a/packages/ui/src/components/DataManagerTable/EnumFilter.tsx b/packages/ui/src/components/DataManagerTable/EnumFilter.tsx
index 31e149ab6..6b435c91d 100644
--- a/packages/ui/src/components/DataManagerTable/EnumFilter.tsx
+++ b/packages/ui/src/components/DataManagerTable/EnumFilter.tsx
@@ -1,9 +1,8 @@
-import styled from "@emotion/styled";
import { ensureArray } from "@lightsparkdev/core";
-import { Spacing } from "../../styles/tokens/spacing.js";
import { z } from "../../styles/z-index.js";
import { Button } from "../Button.js";
import Select from "../Select.js";
+import { AppliedButtonsContainer } from "./AppliedButtonsContainer.js";
import { Filter, type FilterState } from "./Filter.js";
import { FilterType, type EnumFilterValue } from "./filters.js";
@@ -105,10 +104,3 @@ export const EnumFilter = ({
>
);
};
-
-const AppliedButtonsContainer = styled.div`
- margin-top: ${Spacing.px.sm};
- display: flex;
- gap: ${Spacing.px.xs};
- flex-wrap: wrap;
-`;
diff --git a/packages/ui/src/components/DataManagerTable/IdFilter.tsx b/packages/ui/src/components/DataManagerTable/IdFilter.tsx
index ffd45f31d..d9a8d2819 100644
--- a/packages/ui/src/components/DataManagerTable/IdFilter.tsx
+++ b/packages/ui/src/components/DataManagerTable/IdFilter.tsx
@@ -1,7 +1,6 @@
-import styled from "@emotion/styled";
-import { Spacing } from "../../styles/tokens/spacing.js";
import { Button } from "../Button.js";
import { TextInput } from "../TextInput.js";
+import { AppliedButtonsContainer } from "./AppliedButtonsContainer.js";
import { Filter, type FilterState } from "./Filter.js";
import { FilterType } from "./filters.js";
@@ -148,10 +147,3 @@ export const IdFilter = ({
>
);
};
-
-const AppliedButtonsContainer = styled.div`
- margin-top: ${Spacing.px.sm};
- display: flex;
- gap: ${Spacing.px.xs};
- flex-wrap: wrap;
-`;
diff --git a/packages/ui/src/components/DataManagerTable/StringFilter.tsx b/packages/ui/src/components/DataManagerTable/StringFilter.tsx
index 79a9d842c..d6eb6666a 100644
--- a/packages/ui/src/components/DataManagerTable/StringFilter.tsx
+++ b/packages/ui/src/components/DataManagerTable/StringFilter.tsx
@@ -1,7 +1,6 @@
-import styled from "@emotion/styled";
-import { Spacing } from "../../styles/tokens/spacing.js";
import { Button } from "../Button.js";
import { TextInput } from "../TextInput.js";
+import { AppliedButtonsContainer } from "./AppliedButtonsContainer.js";
import { Filter, type FilterState } from "./Filter.js";
import { FilterType } from "./filters.js";
@@ -82,10 +81,3 @@ export const StringFilter = ({
>
);
};
-
-const AppliedButtonsContainer = styled.div`
- margin-top: ${Spacing.px.sm};
- display: flex;
- gap: ${Spacing.px.xs};
- flex-wrap: wrap;
-`;
diff --git a/packages/ui/src/icons/central/BankSolid.tsx b/packages/ui/src/icons/central/BankSolid.tsx
new file mode 100644
index 000000000..716f15e8b
--- /dev/null
+++ b/packages/ui/src/icons/central/BankSolid.tsx
@@ -0,0 +1,18 @@
+export function BankSolid() {
+ return (
+
+ );
+}
diff --git a/packages/ui/src/icons/central/index.tsx b/packages/ui/src/icons/central/index.tsx
index 44f5028dd..fe01e739c 100644
--- a/packages/ui/src/icons/central/index.tsx
+++ b/packages/ui/src/icons/central/index.tsx
@@ -16,6 +16,7 @@ export { ArrowUpRight as CentralArrowUpRight } from "./ArrowUpRight.js";
export { At as CentralAt } from "./At.js";
export { Bank as CentralBank } from "./Bank.js";
export { BankBold as CentralBankBold } from "./BankBold.js";
+export { BankSolid as CentralBankSolid } from "./BankSolid.js";
export { BarsThree as CentralBarsThree } from "./BarsThree.js";
export { Bell as CentralBell } from "./Bell.js";
export { Bell2 as CentralBell2 } from "./Bell2.js";