@fairmint/ui 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,25 @@
1
+ import { z } from 'zod';
2
+ import { TextInputProps } from '../../primitives/TextInput/TextInput';
3
+ export interface ZipSchemaOptions {
4
+ /** When true, accepts international postal codes (UK, Canada, Europe, etc.).
5
+ * Defaults to false — US ZIP codes only. */
6
+ international?: boolean;
7
+ }
8
+ /**
9
+ * Creates a Zod schema for postal/ZIP code validation.
10
+ *
11
+ * Default (US): accepts `94105` and `94105-1234` only.
12
+ * International: accepts `K1A 0B1`, `SW1A 2AA`, `12345`, etc.
13
+ * Input is trimmed before validation in both modes.
14
+ */
15
+ export declare function createZipSchema(options?: ZipSchemaOptions): z.ZodString;
16
+ /** Zod schema for US ZIP codes (`94105`, `94105-1234`). Use `createZipSchema({ international: true })` for international. */
17
+ export declare const zipSchema: z.ZodString;
18
+ export interface ZipFieldProps extends Omit<TextInputProps, "type"> {
19
+ error?: string;
20
+ /** When true, adjusts placeholder for international postal codes. */
21
+ international?: boolean;
22
+ }
23
+ /** Postal/ZIP code input. US ZIP only by default; pass `international` for global formats. */
24
+ export declare function ZipField({ error, international, ...rest }: ZipFieldProps): import("react/jsx-runtime").JSX.Element;
25
+ //# sourceMappingURL=ZipField.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ZipField.d.ts","sourceRoot":"","sources":["../../../../src/components/form/ZipField/ZipField.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sCAAsC,CAAC;AAa3E,MAAM,WAAW,gBAAgB;IAC/B;iDAC6C;IAC7C,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,gBAAgB,eAazD;AAED,6HAA6H;AAC7H,eAAO,MAAM,SAAS,aAAoB,CAAC;AAE3C,MAAM,WAAW,aAAc,SAAQ,IAAI,CAAC,cAAc,EAAE,MAAM,CAAC;IACjE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,8FAA8F;AAC9F,wBAAgB,QAAQ,CAAC,EAAE,KAAK,EAAE,aAAqB,EAAE,GAAG,IAAI,EAAE,EAAE,aAAa,2CAYhF"}
@@ -0,0 +1,45 @@
1
+ import { ReactNode } from 'react';
2
+ export interface Wallet {
3
+ /** Ethereum wallet address. */
4
+ address: string;
5
+ /** Wallet type identifier (e.g. "fairmint", "privy", "manual"). */
6
+ type: string;
7
+ /** Optional display label. */
8
+ label?: string | null;
9
+ }
10
+ export interface WalletListProps {
11
+ /** Wallets to display. */
12
+ wallets: Wallet[];
13
+ /** Called when user adds a new wallet address. */
14
+ onAdd?: (address: string) => void | Promise<void>;
15
+ /** Called when user confirms removal of a wallet address. */
16
+ onRemove?: (address: string) => void | Promise<void>;
17
+ /** Whether add/remove actions are disabled (e.g. admin viewing another user). */
18
+ readOnly?: boolean;
19
+ /** Whether an add operation is in progress. */
20
+ addLoading?: boolean;
21
+ /** Whether a remove operation is in progress. */
22
+ removeLoading?: boolean;
23
+ /** Icon for the empty state. Passed as ReactNode. */
24
+ emptyIcon?: ReactNode;
25
+ /** Icon for the copy button. Passed as ReactNode. */
26
+ copyIcon?: ReactNode;
27
+ /** Icon for the "copied" state. Passed as ReactNode. */
28
+ copiedIcon?: ReactNode;
29
+ /** Icon for the remove button. Passed as ReactNode. */
30
+ removeIcon?: ReactNode;
31
+ /** Icon for the add button. Passed as ReactNode. */
32
+ addIcon?: ReactNode;
33
+ /**
34
+ * Map a wallet type string to a display label.
35
+ * @default Built-in: "fairmint"|"privy" → "Fairmint Wallet", "manual" → "Manual", else as-is
36
+ */
37
+ typeLabels?: Record<string, string>;
38
+ /**
39
+ * Which wallet types can be removed.
40
+ * @default ["manual"]
41
+ */
42
+ removableTypes?: string[];
43
+ }
44
+ export declare function WalletList({ wallets, onAdd, onRemove, readOnly, addLoading, removeLoading, emptyIcon, copyIcon, copiedIcon, removeIcon, addIcon, typeLabels, removableTypes, }: WalletListProps): import("react/jsx-runtime").JSX.Element;
45
+ //# sourceMappingURL=WalletList.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WalletList.d.ts","sourceRoot":"","sources":["../../../../src/components/specific/WalletList/WalletList.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAA+B,MAAM,OAAO,CAAC;AAYpE,MAAM,WAAW,MAAM;IACrB,+BAA+B;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,mEAAmE;IACnE,IAAI,EAAE,MAAM,CAAC;IACb,8BAA8B;IAC9B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,0BAA0B;IAC1B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,kDAAkD;IAClD,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,iFAAiF;IACjF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+CAA+C;IAC/C,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,iDAAiD;IACjD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qDAAqD;IACrD,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,wDAAwD;IACxD,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB,uDAAuD;IACvD,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB,oDAAoD;IACpD,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAaD,wBAAgB,UAAU,CAAC,EACzB,OAAO,EACP,KAAK,EACL,QAAQ,EACR,QAAgB,EAChB,UAAkB,EAClB,aAAqB,EACrB,SAAS,EACT,QAAQ,EACR,UAAU,EACV,UAAU,EACV,OAAO,EACP,UAAU,EACV,cAA2B,GAC5B,EAAE,eAAe,2CAmMjB"}
@@ -24840,6 +24840,29 @@ function PhoneField({ error: error2, ...rest }) {
24840
24840
  }
24841
24841
  );
24842
24842
  }
24843
+ const US_ZIP_REGEX = /^\d{5}(-\d{4})?$/;
24844
+ const INTL_POSTAL_REGEX = /^[A-Z0-9]+(?:[ -][A-Z0-9]+)*$/i;
24845
+ function createZipSchema(options) {
24846
+ if (options?.international) {
24847
+ return stringType().trim().min(3, "Please enter a valid postal code").max(10, "Please enter a valid postal code").regex(INTL_POSTAL_REGEX, "Please enter a valid postal code");
24848
+ }
24849
+ return stringType().trim().regex(US_ZIP_REGEX, "Please enter a valid ZIP code (e.g. 94105 or 94105-1234)");
24850
+ }
24851
+ const zipSchema = createZipSchema();
24852
+ function ZipField({ error: error2, international = false, ...rest }) {
24853
+ return /* @__PURE__ */ jsxRuntime.jsx(
24854
+ TextInput,
24855
+ {
24856
+ type: "text",
24857
+ label: rest.label ?? "ZIP / Postal Code",
24858
+ placeholder: rest.placeholder ?? (international ? "SW1A 2AA" : "94105"),
24859
+ autoComplete: "postal-code",
24860
+ inputMode: "text",
24861
+ error: error2,
24862
+ ...rest
24863
+ }
24864
+ );
24865
+ }
24843
24866
  const ANIMATION_MAP = {
24844
24867
  up: "fadeUp",
24845
24868
  down: "fadeDown",
@@ -25533,6 +25556,211 @@ function InvestorProfileCard({
25533
25556
  children
25534
25557
  ] });
25535
25558
  }
25559
+ function truncateAddress(address, chars = 4) {
25560
+ if (address.length <= chars * 2 + 2) return address;
25561
+ return `${address.slice(0, chars + 2)}...${address.slice(-chars)}`;
25562
+ }
25563
+ const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
25564
+ const ETH_ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/;
25565
+ function isEthereumAddress(value) {
25566
+ return ETH_ADDRESS_RE.test(value) && value.toLowerCase() !== ZERO_ADDRESS;
25567
+ }
25568
+ function validateEthereumAddress(value) {
25569
+ if (!ETH_ADDRESS_RE.test(value))
25570
+ return "Must be a valid Ethereum address (0x + 40 hex characters)";
25571
+ if (value.toLowerCase() === ZERO_ADDRESS) return "Cannot use the zero address";
25572
+ return null;
25573
+ }
25574
+ const ETHEREUM_ADDRESS_REGEX = ETH_ADDRESS_RE;
25575
+ const ZERO_ETHEREUM_ADDRESS = ZERO_ADDRESS;
25576
+ const DEFAULT_TYPE_LABELS = {
25577
+ fairmint: "Fairmint Wallet",
25578
+ privy: "Fairmint Wallet",
25579
+ manual: "Manual"
25580
+ };
25581
+ function getTypeLabel(type, custom2) {
25582
+ if (custom2?.[type]) return custom2[type];
25583
+ return DEFAULT_TYPE_LABELS[type] ?? type;
25584
+ }
25585
+ function WalletList({
25586
+ wallets,
25587
+ onAdd,
25588
+ onRemove,
25589
+ readOnly = false,
25590
+ addLoading = false,
25591
+ removeLoading = false,
25592
+ emptyIcon,
25593
+ copyIcon,
25594
+ copiedIcon,
25595
+ removeIcon,
25596
+ addIcon,
25597
+ typeLabels,
25598
+ removableTypes = ["manual"]
25599
+ }) {
25600
+ const [newAddress, setNewAddress] = React.useState("");
25601
+ const [addressError, setAddressError] = React.useState(null);
25602
+ const [showAddForm, setShowAddForm] = React.useState(false);
25603
+ const [walletToRemove, setWalletToRemove] = React.useState(null);
25604
+ const [copiedAddress, setCopiedAddress] = React.useState(null);
25605
+ const copyTimeoutRef = React.useRef(void 0);
25606
+ React.useEffect(() => () => clearTimeout(copyTimeoutRef.current), []);
25607
+ const copyAddress = async (address) => {
25608
+ try {
25609
+ await navigator.clipboard.writeText(address);
25610
+ clearTimeout(copyTimeoutRef.current);
25611
+ setCopiedAddress(address);
25612
+ copyTimeoutRef.current = setTimeout(() => setCopiedAddress(null), 2e3);
25613
+ } catch {
25614
+ }
25615
+ };
25616
+ const canAdd = !readOnly && !!onAdd;
25617
+ const canRemove = !readOnly && !!onRemove;
25618
+ const handleAdd = async () => {
25619
+ const trimmed = newAddress.trim();
25620
+ const error2 = validateEthereumAddress(trimmed);
25621
+ if (error2) {
25622
+ setAddressError(error2);
25623
+ return;
25624
+ }
25625
+ setAddressError(null);
25626
+ try {
25627
+ await onAdd?.(trimmed);
25628
+ setNewAddress("");
25629
+ setShowAddForm(false);
25630
+ } catch {
25631
+ setAddressError("Failed to add wallet");
25632
+ }
25633
+ };
25634
+ const [removeError, setRemoveError] = React.useState(null);
25635
+ const handleRemove = async () => {
25636
+ if (!walletToRemove) return;
25637
+ setRemoveError(null);
25638
+ try {
25639
+ await onRemove?.(walletToRemove);
25640
+ setWalletToRemove(null);
25641
+ } catch {
25642
+ setRemoveError("Failed to remove wallet. Please try again.");
25643
+ }
25644
+ };
25645
+ if (wallets.length === 0 && !showAddForm) {
25646
+ return /* @__PURE__ */ jsxRuntime.jsx(
25647
+ EmptyState,
25648
+ {
25649
+ icon: emptyIcon,
25650
+ title: "No wallets connected",
25651
+ description: "Add a wallet address to receive equity on-chain.",
25652
+ actionLabel: canAdd ? "Add Wallet" : void 0,
25653
+ onAction: canAdd ? () => setShowAddForm(true) : void 0
25654
+ }
25655
+ );
25656
+ }
25657
+ return /* @__PURE__ */ jsxRuntime.jsxs(Stack, { gap: "sm", children: [
25658
+ wallets.map((w) => /* @__PURE__ */ jsxRuntime.jsxs(
25659
+ Group,
25660
+ {
25661
+ justify: "space-between",
25662
+ p: "xs",
25663
+ style: {
25664
+ borderRadius: "var(--mantine-radius-sm)",
25665
+ border: "1px solid var(--mantine-color-default-border)"
25666
+ },
25667
+ children: [
25668
+ /* @__PURE__ */ jsxRuntime.jsxs(Group, { gap: "xs", children: [
25669
+ /* @__PURE__ */ jsxRuntime.jsx(Badge, { size: "xs", variant: "light", children: getTypeLabel(w.type, typeLabels) }),
25670
+ w.label && /* @__PURE__ */ jsxRuntime.jsx(Text, { size: "xs", c: `${colors.neutral}.5`, children: w.label }),
25671
+ /* @__PURE__ */ jsxRuntime.jsx(Text, { size: "sm", ff: "monospace", children: truncateAddress(w.address) })
25672
+ ] }),
25673
+ /* @__PURE__ */ jsxRuntime.jsxs(Group, { gap: 4, children: [
25674
+ copyIcon && /* @__PURE__ */ jsxRuntime.jsx(Tooltip, { label: copiedAddress === w.address ? "Copied!" : "Copy address", children: /* @__PURE__ */ jsxRuntime.jsx(
25675
+ ActionIcon,
25676
+ {
25677
+ variant: "subtle",
25678
+ size: "sm",
25679
+ onClick: () => copyAddress(w.address),
25680
+ "aria-label": "Copy wallet address",
25681
+ children: copiedAddress === w.address ? copiedIcon ?? copyIcon : copyIcon
25682
+ }
25683
+ ) }),
25684
+ canRemove && removeIcon && removableTypes.includes(w.type) && /* @__PURE__ */ jsxRuntime.jsx(Tooltip, { label: "Remove wallet", children: /* @__PURE__ */ jsxRuntime.jsx(
25685
+ ActionIcon,
25686
+ {
25687
+ variant: "subtle",
25688
+ size: "sm",
25689
+ color: colors.error,
25690
+ onClick: () => setWalletToRemove(w.address),
25691
+ disabled: removeLoading,
25692
+ "aria-label": "Remove wallet",
25693
+ children: removeIcon
25694
+ }
25695
+ ) })
25696
+ ] })
25697
+ ]
25698
+ },
25699
+ w.address
25700
+ )),
25701
+ canAdd && (showAddForm ? /* @__PURE__ */ jsxRuntime.jsxs(Group, { gap: "xs", mt: "xs", children: [
25702
+ /* @__PURE__ */ jsxRuntime.jsx(
25703
+ TextInput,
25704
+ {
25705
+ placeholder: "0x...",
25706
+ "aria-label": "Wallet address",
25707
+ value: newAddress,
25708
+ onChange: (e) => {
25709
+ setNewAddress(e.currentTarget.value);
25710
+ setAddressError(null);
25711
+ },
25712
+ error: addressError,
25713
+ style: { flex: 1 }
25714
+ }
25715
+ ),
25716
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { size: "sm", onClick: handleAdd, loading: addLoading, children: "Add" }),
25717
+ /* @__PURE__ */ jsxRuntime.jsx(
25718
+ Button,
25719
+ {
25720
+ size: "sm",
25721
+ variant: "subtle",
25722
+ color: colors.neutral,
25723
+ onClick: () => {
25724
+ setShowAddForm(false);
25725
+ setNewAddress("");
25726
+ setAddressError(null);
25727
+ },
25728
+ children: "Cancel"
25729
+ }
25730
+ )
25731
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(
25732
+ Button,
25733
+ {
25734
+ variant: "light",
25735
+ size: "xs",
25736
+ leftSection: addIcon,
25737
+ onClick: () => setShowAddForm(true),
25738
+ mt: "xs",
25739
+ children: "Add Wallet"
25740
+ }
25741
+ )),
25742
+ /* @__PURE__ */ jsxRuntime.jsx(
25743
+ ConfirmDialog,
25744
+ {
25745
+ title: "Remove Wallet",
25746
+ message: /* @__PURE__ */ jsxRuntime.jsxs(Stack, { gap: "sm", children: [
25747
+ /* @__PURE__ */ jsxRuntime.jsx(Text, { size: "sm", children: "Are you sure you want to remove this wallet address? This action cannot be undone." }),
25748
+ walletToRemove && /* @__PURE__ */ jsxRuntime.jsx(Text, { size: "sm", ff: "monospace", c: `${colors.neutral}.5`, children: walletToRemove }),
25749
+ removeError && /* @__PURE__ */ jsxRuntime.jsx(Text, { size: "sm", c: colors.error, children: removeError })
25750
+ ] }),
25751
+ opened: walletToRemove !== null,
25752
+ confirmLabel: "Remove",
25753
+ confirmColor: colors.error,
25754
+ onConfirm: handleRemove,
25755
+ onCancel: () => {
25756
+ setWalletToRemove(null);
25757
+ setRemoveError(null);
25758
+ },
25759
+ loading: removeLoading
25760
+ }
25761
+ )
25762
+ ] });
25763
+ }
25536
25764
  function useDebouncedCallback(fn, delay) {
25537
25765
  const timer = React.useRef(null);
25538
25766
  const fnRef = React.useRef(fn);
@@ -25564,10 +25792,6 @@ const notify = {
25564
25792
  warning: ({ title, message, autoClose = 5e3 }) => notifications.show({ title, message, color: colors.warning, autoClose }),
25565
25793
  info: ({ title, message, autoClose = 4e3 }) => notifications.show({ title, message, color: colors.info, autoClose })
25566
25794
  };
25567
- function truncateAddress(address, chars = 4) {
25568
- if (address.length <= chars * 2 + 2) return address;
25569
- return `${address.slice(0, chars + 2)}...${address.slice(-chars)}`;
25570
- }
25571
25795
  exports.AVATAR_PALETTE = AVATAR_PALETTE;
25572
25796
  exports.ActionIcon = ActionIcon;
25573
25797
  exports.Alert = Alert;
@@ -25591,6 +25815,7 @@ exports.DetailDrawer = DetailDrawer;
25591
25815
  exports.Divider = Divider;
25592
25816
  exports.DocumentRow = DocumentRow;
25593
25817
  exports.Drawer = Drawer;
25818
+ exports.ETHEREUM_ADDRESS_REGEX = ETHEREUM_ADDRESS_REGEX;
25594
25819
  exports.EmailField = EmailField;
25595
25820
  exports.EmptyState = EmptyState;
25596
25821
  exports.EquityGrantCard = EquityGrantCard;
@@ -25634,8 +25859,12 @@ exports.TextInput = TextInput;
25634
25859
  exports.Title = Title;
25635
25860
  exports.Tooltip = Tooltip;
25636
25861
  exports.UserMenu = UserMenu;
25862
+ exports.WalletList = WalletList;
25863
+ exports.ZERO_ETHEREUM_ADDRESS = ZERO_ETHEREUM_ADDRESS;
25864
+ exports.ZipField = ZipField;
25637
25865
  exports.brandColors = brandColors;
25638
25866
  exports.colors = colors;
25867
+ exports.createZipSchema = createZipSchema;
25639
25868
  exports.emailSchema = emailSchema;
25640
25869
  exports.formatCurrency = formatCurrency;
25641
25870
  exports.formatCurrencyPrecise = formatCurrencyPrecise;
@@ -25644,6 +25873,7 @@ exports.formatNumber = formatNumber;
25644
25873
  exports.formatPercent = formatPercent;
25645
25874
  exports.formatPreciseWithCurrency = formatPreciseWithCurrency;
25646
25875
  exports.getAvatarColor = getAvatarColor;
25876
+ exports.isEthereumAddress = isEthereumAddress;
25647
25877
  exports.neutralColors = neutralColors;
25648
25878
  exports.notify = notify;
25649
25879
  exports.passwordSchema = passwordSchema;
@@ -25664,5 +25894,7 @@ exports.useIsMobile = useIsMobile;
25664
25894
  exports.useMediaQuery = useMediaQuery;
25665
25895
  exports.useScrollIntoView = useScrollIntoView;
25666
25896
  exports.useViewportSize = useViewportSize;
25897
+ exports.validateEthereumAddress = validateEthereumAddress;
25667
25898
  exports.z = z;
25899
+ exports.zipSchema = zipSchema;
25668
25900
  //# sourceMappingURL=fairmint-ui.cjs.map