myoperator-ui 0.0.141 → 0.0.142

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.
Files changed (2) hide show
  1. package/dist/index.js +1730 -0
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6609,6 +6609,1736 @@ export interface KeyValueRowProps {
6609
6609
  /** Callback when row is deleted */
6610
6610
  onDelete: (id: string) => void;
6611
6611
  }
6612
+ `, prefix)
6613
+ }
6614
+ ]
6615
+ },
6616
+ "api-feature-card": {
6617
+ name: "api-feature-card",
6618
+ description: "A card component for displaying API features with icon, title, description, and action button",
6619
+ dependencies: [
6620
+ "clsx",
6621
+ "tailwind-merge",
6622
+ "lucide-react"
6623
+ ],
6624
+ internalDependencies: [
6625
+ "button"
6626
+ ],
6627
+ isMultiFile: true,
6628
+ directory: "api-feature-card",
6629
+ mainFile: "api-feature-card.tsx",
6630
+ files: [
6631
+ {
6632
+ name: "api-feature-card.tsx",
6633
+ content: prefixTailwindClasses(`import * as React from "react";
6634
+ import { Button } from "@/components/ui/button";
6635
+ import { cn } from "../../../lib/utils";
6636
+
6637
+ export interface Capability {
6638
+ /** Unique identifier for the capability */
6639
+ id: string;
6640
+ /** Display text for the capability */
6641
+ label: string;
6642
+ }
6643
+
6644
+ export interface ApiFeatureCardProps
6645
+ extends React.HTMLAttributes<HTMLDivElement> {
6646
+ /** Icon component to display */
6647
+ icon: React.ReactNode;
6648
+ /** Card title */
6649
+ title: string;
6650
+ /** Card description */
6651
+ description: string;
6652
+ /** List of key capabilities */
6653
+ capabilities?: Capability[];
6654
+ /** Text for the action button */
6655
+ actionLabel?: string;
6656
+ /** Icon for the action button */
6657
+ actionIcon?: React.ReactNode;
6658
+ /** Callback when action button is clicked */
6659
+ onAction?: () => void;
6660
+ /** Label for the capabilities section */
6661
+ capabilitiesLabel?: string;
6662
+ }
6663
+
6664
+ /**
6665
+ * ApiFeatureCard displays an API feature with icon, title, description,
6666
+ * action button, and a list of key capabilities.
6667
+ *
6668
+ * @example
6669
+ * \`\`\`tsx
6670
+ * <ApiFeatureCard
6671
+ * icon={<Phone className="h-5 w-5" />}
6672
+ * title="Calling API"
6673
+ * description="Manage real-time call flow, recordings, and intelligent routing."
6674
+ * capabilities={[
6675
+ * { id: "1", label: "Real-time Call Control" },
6676
+ * { id: "2", label: "Live Call Events (Webhooks)" },
6677
+ * ]}
6678
+ * onAction={() => console.log("Manage clicked")}
6679
+ * />
6680
+ * \`\`\`
6681
+ */
6682
+ export const ApiFeatureCard = React.forwardRef<
6683
+ HTMLDivElement,
6684
+ ApiFeatureCardProps
6685
+ >(
6686
+ (
6687
+ {
6688
+ icon,
6689
+ title,
6690
+ description,
6691
+ capabilities = [],
6692
+ actionLabel = "Manage",
6693
+ actionIcon,
6694
+ onAction,
6695
+ capabilitiesLabel = "Key Capabilities",
6696
+ className,
6697
+ ...props
6698
+ },
6699
+ ref
6700
+ ) => {
6701
+ return (
6702
+ <div
6703
+ ref={ref}
6704
+ className={cn(
6705
+ "flex flex-col gap-6 rounded-lg border border-semantic-border-layout bg-semantic-bg-primary p-6 overflow-hidden",
6706
+ className
6707
+ )}
6708
+ {...props}
6709
+ >
6710
+ {/* Header Section */}
6711
+ <div className="flex items-center justify-between">
6712
+ <div className="flex items-center gap-2.5">
6713
+ {/* Icon Container */}
6714
+ <div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-[10px] bg-semantic-info-surface">
6715
+ <span className="text-[var(--color-primary-950)] [&_svg]:h-5 [&_svg]:w-5">
6716
+ {icon}
6717
+ </span>
6718
+ </div>
6719
+
6720
+ {/* Title and Description */}
6721
+ <div className="flex flex-col gap-1.5">
6722
+ <h3 className="m-0 text-base font-semibold text-semantic-text-primary">
6723
+ {title}
6724
+ </h3>
6725
+ <p className="m-0 text-sm text-semantic-text-muted tracking-[0.035px]">
6726
+ {description}
6727
+ </p>
6728
+ </div>
6729
+ </div>
6730
+
6731
+ {/* Action Button */}
6732
+ <Button
6733
+ variant="default"
6734
+ size="default"
6735
+ leftIcon={actionIcon}
6736
+ onClick={onAction}
6737
+ className="shrink-0"
6738
+ >
6739
+ {actionLabel}
6740
+ </Button>
6741
+ </div>
6742
+
6743
+ {/* Capabilities Section */}
6744
+ {capabilities.length > 0 && (
6745
+ <div className="flex flex-col gap-2.5 border-t border-semantic-border-layout bg-[var(--color-neutral-50)] -mx-6 -mb-6 p-6">
6746
+ <span className="text-sm font-semibold uppercase tracking-[0.014px] text-[var(--color-neutral-400)]">
6747
+ {capabilitiesLabel}
6748
+ </span>
6749
+ <div className="flex flex-wrap gap-x-6 gap-y-2">
6750
+ {capabilities.map((capability) => (
6751
+ <div
6752
+ key={capability.id}
6753
+ className="flex items-center gap-1.5"
6754
+ >
6755
+ <span className="h-1.5 w-1.5 shrink-0 rounded-full bg-[var(--color-neutral-400)]" />
6756
+ <span className="text-sm text-semantic-text-primary tracking-[0.035px]">
6757
+ {capability.label}
6758
+ </span>
6759
+ </div>
6760
+ ))}
6761
+ </div>
6762
+ </div>
6763
+ )}
6764
+ </div>
6765
+ );
6766
+ }
6767
+ );
6768
+
6769
+ ApiFeatureCard.displayName = "ApiFeatureCard";
6770
+ `, prefix)
6771
+ },
6772
+ {
6773
+ name: "index.ts",
6774
+ content: prefixTailwindClasses(`export { ApiFeatureCard } from "./api-feature-card";
6775
+ export type { ApiFeatureCardProps, Capability } from "./api-feature-card";
6776
+ `, prefix)
6777
+ }
6778
+ ]
6779
+ },
6780
+ "endpoint-details": {
6781
+ name: "endpoint-details",
6782
+ description: "A component for displaying API endpoint details with copy-to-clipboard and secret field support",
6783
+ dependencies: [
6784
+ "clsx",
6785
+ "tailwind-merge",
6786
+ "lucide-react"
6787
+ ],
6788
+ internalDependencies: [
6789
+ "readable-field"
6790
+ ],
6791
+ isMultiFile: true,
6792
+ directory: "endpoint-details",
6793
+ mainFile: "endpoint-details.tsx",
6794
+ files: [
6795
+ {
6796
+ name: "endpoint-details.tsx",
6797
+ content: prefixTailwindClasses(`import * as React from "react";
6798
+ import { XCircle } from "lucide-react";
6799
+ import { cn } from "../../../lib/utils";
6800
+ import { ReadableField } from "@/components/ui/readable-field";
6801
+
6802
+ export interface EndpointDetailsProps
6803
+ extends React.HTMLAttributes<HTMLDivElement> {
6804
+ /** Card title */
6805
+ title?: string;
6806
+ /** Variant determines field layout and visibility */
6807
+ variant?: "calling" | "whatsapp";
6808
+ /** Base URL for the API endpoint */
6809
+ baseUrl: string;
6810
+ /** Company ID */
6811
+ companyId: string;
6812
+ /** Authentication token (secret in calling variant, visible in whatsapp variant) */
6813
+ authToken: string;
6814
+ /** Secret key (secret) - only shown in calling variant */
6815
+ secretKey?: string;
6816
+ /** API key (visible) - only shown in calling variant */
6817
+ apiKey?: string;
6818
+ /** Callback when a field value is copied */
6819
+ onValueCopy?: (field: string, value: string) => void;
6820
+ /** Callback when regenerate is clicked for a field - only used in calling variant */
6821
+ onRegenerate?: (field: "authToken" | "secretKey") => void;
6822
+ /** Callback when revoke access is clicked - only used in calling variant */
6823
+ onRevokeAccess?: () => void;
6824
+ /** Whether to show the revoke access section - only used in calling variant */
6825
+ showRevokeSection?: boolean;
6826
+ /** Custom revoke section title */
6827
+ revokeTitle?: string;
6828
+ /** Custom revoke section description */
6829
+ revokeDescription?: string;
6830
+ }
6831
+
6832
+ /**
6833
+ * EndpointDetails displays API endpoint credentials with copy functionality.
6834
+ * Used for showing API keys, authentication tokens, and other sensitive credentials.
6835
+ *
6836
+ * Supports two variants:
6837
+ * - \`calling\` (default): Full version with all 5 fields + revoke section
6838
+ * - \`whatsapp\`: Simplified with 3 fields (baseUrl, companyId, authToken), no revoke
6839
+ *
6840
+ * @example
6841
+ * \`\`\`tsx
6842
+ * // Calling API (default)
6843
+ * <EndpointDetails
6844
+ * variant="calling"
6845
+ * baseUrl="https://api.myoperator.co/v3/voice/gateway"
6846
+ * companyId="12"
6847
+ * authToken="sk_live_abc123"
6848
+ * secretKey="whsec_xyz789"
6849
+ * apiKey="tpb0syNDbO4k49ZbyiWeU5k8gFWQ7ODBJ7GYr3UO"
6850
+ * onRegenerate={(field) => console.log(\`Regenerate \${field}\`)}
6851
+ * onRevokeAccess={() => console.log("Revoke access")}
6852
+ * />
6853
+ *
6854
+ * // WhatsApp API
6855
+ * <EndpointDetails
6856
+ * variant="whatsapp"
6857
+ * baseUrl="https://api.myoperator.co/whatsapp"
6858
+ * companyId="WA-12345"
6859
+ * authToken="waba_token_abc123"
6860
+ * />
6861
+ * \`\`\`
6862
+ */
6863
+ export const EndpointDetails = React.forwardRef<
6864
+ HTMLDivElement,
6865
+ EndpointDetailsProps
6866
+ >(
6867
+ (
6868
+ {
6869
+ title = "Endpoint Details",
6870
+ variant = "calling",
6871
+ baseUrl,
6872
+ companyId,
6873
+ authToken,
6874
+ secretKey,
6875
+ apiKey,
6876
+ onValueCopy,
6877
+ onRegenerate,
6878
+ onRevokeAccess,
6879
+ showRevokeSection = true,
6880
+ revokeTitle = "Revoke API Access",
6881
+ revokeDescription = "Revoking access will immediately disable all integrations using these keys.",
6882
+ className,
6883
+ ...props
6884
+ },
6885
+ ref
6886
+ ) => {
6887
+ const isCalling = variant === "calling";
6888
+
6889
+ const handleCopy = (field: string) => (value: string) => {
6890
+ onValueCopy?.(field, value);
6891
+ };
6892
+
6893
+ return (
6894
+ <div
6895
+ ref={ref}
6896
+ className={cn(
6897
+ "flex flex-col gap-6 rounded-lg border border-semantic-border-layout p-6",
6898
+ className
6899
+ )}
6900
+ {...props}
6901
+ >
6902
+ {/* Title */}
6903
+ <div className="flex items-start gap-5">
6904
+ <h2 className="m-0 text-base font-semibold text-semantic-text-primary">
6905
+ {title}
6906
+ </h2>
6907
+ </div>
6908
+
6909
+ {/* Credentials Grid */}
6910
+ <div className="flex flex-col gap-[30px]">
6911
+ {/* Row 1: Base URL + Company ID */}
6912
+ <div className="grid grid-cols-2 gap-[25px]">
6913
+ <ReadableField
6914
+ label="Base URL"
6915
+ value={baseUrl}
6916
+ onValueCopy={handleCopy("baseUrl")}
6917
+ />
6918
+ <ReadableField
6919
+ label="Company ID"
6920
+ value={companyId}
6921
+ onValueCopy={handleCopy("companyId")}
6922
+ />
6923
+ </div>
6924
+
6925
+ {/* Authentication field - different based on variant */}
6926
+ {isCalling ? (
6927
+ /* Calling variant: 2-col row with Authentication + Secret Key */
6928
+ <div className="grid grid-cols-2 gap-[25px]">
6929
+ <ReadableField
6930
+ label="Authentication"
6931
+ value={authToken}
6932
+ secret
6933
+ helperText="Used for client-side integrations."
6934
+ headerAction={{
6935
+ label: "Regenerate",
6936
+ onClick: () => onRegenerate?.("authToken"),
6937
+ }}
6938
+ onValueCopy={handleCopy("authToken")}
6939
+ />
6940
+ {secretKey && (
6941
+ <ReadableField
6942
+ label="Secret Key"
6943
+ value={secretKey}
6944
+ secret
6945
+ helperText="Never share this key or expose it in client-side code."
6946
+ headerAction={{
6947
+ label: "Regenerate",
6948
+ onClick: () => onRegenerate?.("secretKey"),
6949
+ }}
6950
+ onValueCopy={handleCopy("secretKey")}
6951
+ />
6952
+ )}
6953
+ </div>
6954
+ ) : (
6955
+ /* WhatsApp variant: full-width Authentication, NOT secret, NO regenerate */
6956
+ <ReadableField
6957
+ label="Authentication"
6958
+ value={authToken}
6959
+ onValueCopy={handleCopy("authToken")}
6960
+ />
6961
+ )}
6962
+
6963
+ {/* x-api-key (full width) - only for calling variant */}
6964
+ {isCalling && apiKey && (
6965
+ <ReadableField
6966
+ label="x-api-key"
6967
+ value={apiKey}
6968
+ onValueCopy={handleCopy("apiKey")}
6969
+ />
6970
+ )}
6971
+
6972
+ {/* Revoke Section - only for calling variant */}
6973
+ {isCalling && showRevokeSection && (
6974
+ <div className="flex items-center justify-between border-t border-semantic-border-layout pt-6">
6975
+ <div className="flex flex-col gap-1">
6976
+ <h3 className="m-0 text-base font-semibold text-semantic-text-primary">
6977
+ {revokeTitle}
6978
+ </h3>
6979
+ <p className="m-0 text-sm text-semantic-text-muted tracking-[0.035px]">
6980
+ {revokeDescription}
6981
+ </p>
6982
+ </div>
6983
+ <button
6984
+ type="button"
6985
+ onClick={onRevokeAccess}
6986
+ className="flex items-center gap-1 text-sm text-semantic-error-primary hover:text-semantic-error-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-semantic-error-primary transition-colors tracking-[0.035px] rounded"
6987
+ >
6988
+ <XCircle className="size-4" />
6989
+ <span>Revoke Access</span>
6990
+ </button>
6991
+ </div>
6992
+ )}
6993
+ </div>
6994
+ </div>
6995
+ );
6996
+ }
6997
+ );
6998
+
6999
+ EndpointDetails.displayName = "EndpointDetails";
7000
+ `, prefix)
7001
+ },
7002
+ {
7003
+ name: "index.ts",
7004
+ content: prefixTailwindClasses(`export { EndpointDetails } from "./endpoint-details";
7005
+ export type { EndpointDetailsProps } from "./endpoint-details";
7006
+ `, prefix)
7007
+ }
7008
+ ]
7009
+ },
7010
+ "alert-configuration": {
7011
+ name: "alert-configuration",
7012
+ description: "A configuration card for alert settings with inline editing modal",
7013
+ dependencies: [
7014
+ "clsx",
7015
+ "tailwind-merge",
7016
+ "lucide-react"
7017
+ ],
7018
+ internalDependencies: [
7019
+ "button",
7020
+ "form-modal",
7021
+ "input"
7022
+ ],
7023
+ isMultiFile: true,
7024
+ directory: "alert-configuration",
7025
+ mainFile: "alert-configuration.tsx",
7026
+ files: [
7027
+ {
7028
+ name: "alert-configuration.tsx",
7029
+ content: prefixTailwindClasses(`import * as React from "react";
7030
+ import { Button } from "@/components/ui/button";
7031
+ import { Pencil } from "lucide-react";
7032
+ import { cn } from "../../../lib/utils";
7033
+
7034
+ export interface AlertConfigurationProps {
7035
+ /** Minimum balance threshold */
7036
+ minimumBalance: number;
7037
+ /** Minimum top-up amount */
7038
+ minimumTopup: number;
7039
+ /** Currency symbol (default: \u20B9) */
7040
+ currencySymbol?: string;
7041
+ /** Callback when edit button is clicked */
7042
+ onEdit?: () => void;
7043
+ /** Custom className for the container */
7044
+ className?: string;
7045
+ }
7046
+
7047
+ /**
7048
+ * AlertConfiguration component displays current alert values for minimum balance and top-up.
7049
+ * Used in payment auto-pay setup to show notification thresholds.
7050
+ */
7051
+ export const AlertConfiguration = React.forwardRef<
7052
+ HTMLDivElement,
7053
+ AlertConfigurationProps
7054
+ >(
7055
+ (
7056
+ {
7057
+ minimumBalance,
7058
+ minimumTopup,
7059
+ currencySymbol = "\u20B9",
7060
+ onEdit,
7061
+ className,
7062
+ },
7063
+ ref
7064
+ ) => {
7065
+ const formatCurrency = (amount: number) => {
7066
+ const formatted = amount.toLocaleString("en-IN", {
7067
+ minimumFractionDigits: 2,
7068
+ maximumFractionDigits: 2,
7069
+ });
7070
+ return \`\${currencySymbol} \${formatted}\`;
7071
+ };
7072
+
7073
+ return (
7074
+ <div
7075
+ ref={ref}
7076
+ className={cn(
7077
+ "rounded-lg border border-semantic-border-layout bg-semantic-bg-primary",
7078
+ className
7079
+ )}
7080
+ >
7081
+ {/* Header */}
7082
+ <div className="flex items-center justify-between gap-3 px-4 py-4">
7083
+ <div className="flex flex-col gap-1">
7084
+ <h3 className="m-0 text-base font-semibold text-semantic-text-primary tracking-[0px]">
7085
+ Alert configurations
7086
+ </h3>
7087
+ <p className="m-0 text-sm text-semantic-text-muted tracking-[0.035px]">
7088
+ Define when and how you receive balance notifications
7089
+ </p>
7090
+ </div>
7091
+ <Button
7092
+ variant="default"
7093
+ size="default"
7094
+ leftIcon={<Pencil className="h-3.5 w-3.5" />}
7095
+ onClick={onEdit}
7096
+ className="shrink-0"
7097
+ >
7098
+ Edit alert values
7099
+ </Button>
7100
+ </div>
7101
+
7102
+ {/* Alert Values Section with Top Border */}
7103
+ <div className="border-t border-semantic-border-layout px-4 py-4">
7104
+ <div className="flex items-start justify-between gap-4">
7105
+ {/* Minimum Balance */}
7106
+ <div className="flex flex-col gap-1.5 flex-1">
7107
+ <div className="flex items-center justify-between">
7108
+ <span className="text-sm font-semibold text-semantic-text-primary tracking-[0.014px]">
7109
+ Minimum balance
7110
+ </span>
7111
+ <span
7112
+ className={cn(
7113
+ "text-sm tracking-[0.035px]",
7114
+ minimumBalance < 0
7115
+ ? "text-semantic-error-primary"
7116
+ : "text-semantic-text-primary"
7117
+ )}
7118
+ >
7119
+ {minimumBalance < 0 ? "-" : ""}{formatCurrency(Math.abs(minimumBalance))}
7120
+ </span>
7121
+ </div>
7122
+ <p className="m-0 text-sm text-semantic-text-muted tracking-[0.035px] leading-relaxed">
7123
+ You'll be notified by email and SMS when your balance falls below this level.
7124
+ </p>
7125
+ </div>
7126
+
7127
+ {/* Vertical Divider */}
7128
+ <div className="w-px h-14 bg-semantic-border-layout shrink-0" />
7129
+
7130
+ {/* Minimum Top-up */}
7131
+ <div className="flex flex-col gap-1.5 flex-1">
7132
+ <div className="flex items-center justify-between">
7133
+ <span className="text-sm font-semibold text-semantic-text-primary tracking-[0.014px]">
7134
+ Minimum topup
7135
+ </span>
7136
+ <span className="text-sm text-semantic-text-link tracking-[0.035px]">
7137
+ {formatCurrency(minimumTopup)}
7138
+ </span>
7139
+ </div>
7140
+ <p className="m-0 text-sm text-semantic-text-muted tracking-[0.035px] leading-relaxed">
7141
+ A suggested recharge amount to top up your balance when it falls below the minimum.
7142
+ </p>
7143
+ </div>
7144
+ </div>
7145
+ </div>
7146
+ </div>
7147
+ );
7148
+ }
7149
+ );
7150
+
7151
+ AlertConfiguration.displayName = "AlertConfiguration";
7152
+ `, prefix)
7153
+ },
7154
+ {
7155
+ name: "alert-values-modal.tsx",
7156
+ content: prefixTailwindClasses(`import * as React from "react";
7157
+ import { FormModal } from "@/components/ui/form-modal";
7158
+ import { Input } from "@/components/ui/input";
7159
+
7160
+ export interface AlertValuesModalProps {
7161
+ /** Whether the modal is open */
7162
+ open: boolean;
7163
+ /** Callback when modal should close */
7164
+ onOpenChange: (open: boolean) => void;
7165
+ /** Initial minimum balance value */
7166
+ initialMinimumBalance?: number;
7167
+ /** Initial minimum top-up value */
7168
+ initialMinimumTopup?: number;
7169
+ /** Currency symbol (default: \u20B9) */
7170
+ currencySymbol?: string;
7171
+ /** Callback when values are saved */
7172
+ onSave?: (values: { minimumBalance: number; minimumTopup: number }) => void;
7173
+ /** Loading state for save button */
7174
+ loading?: boolean;
7175
+ }
7176
+
7177
+ /**
7178
+ * AlertValuesModal component for editing alert configuration values.
7179
+ * Displays a form with inputs for minimum balance and minimum top-up.
7180
+ */
7181
+ export const AlertValuesModal = React.forwardRef<
7182
+ HTMLDivElement,
7183
+ AlertValuesModalProps
7184
+ >(
7185
+ (
7186
+ {
7187
+ open,
7188
+ onOpenChange,
7189
+ initialMinimumBalance = 0,
7190
+ initialMinimumTopup = 0,
7191
+ currencySymbol = "\u20B9",
7192
+ onSave,
7193
+ loading = false,
7194
+ },
7195
+ ref
7196
+ ) => {
7197
+ const [minimumBalance, setMinimumBalance] = React.useState(
7198
+ initialMinimumBalance.toString()
7199
+ );
7200
+ const [minimumTopup, setMinimumTopup] = React.useState(
7201
+ initialMinimumTopup.toString()
7202
+ );
7203
+
7204
+ // Update form values when initial values change
7205
+ React.useEffect(() => {
7206
+ setMinimumBalance(initialMinimumBalance.toString());
7207
+ setMinimumTopup(initialMinimumTopup.toString());
7208
+ }, [initialMinimumBalance, initialMinimumTopup, open]);
7209
+
7210
+ const handleSave = () => {
7211
+ const balanceValue = parseFloat(minimumBalance) || 0;
7212
+ const topupValue = parseFloat(minimumTopup) || 0;
7213
+
7214
+ onSave?.({
7215
+ minimumBalance: balanceValue,
7216
+ minimumTopup: topupValue,
7217
+ });
7218
+ };
7219
+
7220
+ const handleCancel = () => {
7221
+ // Reset to initial values
7222
+ setMinimumBalance(initialMinimumBalance.toString());
7223
+ setMinimumTopup(initialMinimumTopup.toString());
7224
+ };
7225
+
7226
+ return (
7227
+ <FormModal
7228
+ ref={ref}
7229
+ open={open}
7230
+ onOpenChange={onOpenChange}
7231
+ title="Alert values"
7232
+ onSave={handleSave}
7233
+ onCancel={handleCancel}
7234
+ loading={loading}
7235
+ size="sm"
7236
+ >
7237
+ {/* Minimum Balance Input */}
7238
+ <div className="grid gap-2">
7239
+ <label
7240
+ htmlFor="minimum-balance"
7241
+ className="text-sm font-medium text-semantic-text-secondary"
7242
+ >
7243
+ Minimum balance
7244
+ </label>
7245
+ <div className="relative">
7246
+ <span className="absolute left-4 top-1/2 -translate-y-1/2 text-sm text-semantic-text-muted">
7247
+ {currencySymbol}
7248
+ </span>
7249
+ <Input
7250
+ id="minimum-balance"
7251
+ type="number"
7252
+ value={minimumBalance}
7253
+ onChange={(e) => setMinimumBalance(e.target.value)}
7254
+ className="pl-8"
7255
+ placeholder="0"
7256
+ step="0.01"
7257
+ />
7258
+ </div>
7259
+ </div>
7260
+
7261
+ {/* Minimum Top-up Input */}
7262
+ <div className="grid gap-2">
7263
+ <label
7264
+ htmlFor="minimum-topup"
7265
+ className="text-sm font-medium text-semantic-text-secondary"
7266
+ >
7267
+ Minimum topup
7268
+ </label>
7269
+ <div className="relative">
7270
+ <span className="absolute left-4 top-1/2 -translate-y-1/2 text-sm text-semantic-text-muted">
7271
+ {currencySymbol}
7272
+ </span>
7273
+ <Input
7274
+ id="minimum-topup"
7275
+ type="number"
7276
+ value={minimumTopup}
7277
+ onChange={(e) => setMinimumTopup(e.target.value)}
7278
+ className="pl-8"
7279
+ placeholder="0"
7280
+ step="0.01"
7281
+ />
7282
+ </div>
7283
+ </div>
7284
+ </FormModal>
7285
+ );
7286
+ }
7287
+ );
7288
+
7289
+ AlertValuesModal.displayName = "AlertValuesModal";
7290
+ `, prefix)
7291
+ },
7292
+ {
7293
+ name: "index.ts",
7294
+ content: prefixTailwindClasses(`export { AlertConfiguration } from "./alert-configuration";
7295
+ export type { AlertConfigurationProps } from "./alert-configuration";
7296
+
7297
+ export { AlertValuesModal } from "./alert-values-modal";
7298
+ export type { AlertValuesModalProps } from "./alert-values-modal";
7299
+ `, prefix)
7300
+ }
7301
+ ]
7302
+ },
7303
+ "auto-pay-setup": {
7304
+ name: "auto-pay-setup",
7305
+ description: "A setup wizard component for configuring automatic payments with payment method selection",
7306
+ dependencies: [
7307
+ "clsx",
7308
+ "tailwind-merge",
7309
+ "lucide-react"
7310
+ ],
7311
+ internalDependencies: [
7312
+ "accordion",
7313
+ "button"
7314
+ ],
7315
+ isMultiFile: true,
7316
+ directory: "auto-pay-setup",
7317
+ mainFile: "auto-pay-setup.tsx",
7318
+ files: [
7319
+ {
7320
+ name: "auto-pay-setup.tsx",
7321
+ content: prefixTailwindClasses(`import * as React from "react";
7322
+ import { cn } from "../../../lib/utils";
7323
+ import { Button } from "@/components/ui/button";
7324
+ import {
7325
+ Accordion,
7326
+ AccordionItem,
7327
+ AccordionTrigger,
7328
+ AccordionContent,
7329
+ } from "@/components/ui/accordion";
7330
+ import type { AutoPaySetupProps } from "./types";
7331
+
7332
+ /**
7333
+ * AutoPaySetup provides a collapsible panel for setting up automatic payments.
7334
+ * It displays a description, an informational note callout, and a CTA button.
7335
+ *
7336
+ * @example
7337
+ * \`\`\`tsx
7338
+ * <AutoPaySetup
7339
+ * icon={<RefreshCw className="size-5 text-semantic-primary" />}
7340
+ * onCtaClick={() => console.log("Enable auto-pay")}
7341
+ * />
7342
+ * \`\`\`
7343
+ */
7344
+ export const AutoPaySetup = React.forwardRef<HTMLDivElement, AutoPaySetupProps>(
7345
+ (
7346
+ {
7347
+ title = "Auto-pay setup",
7348
+ subtitle = "Hassle-free monthly billing",
7349
+ icon,
7350
+ bodyText = "Link your internet banking account or enroll your card for recurring payments on MyOperator, where your linked account/card is charged automatically for your subsequent bills and usages on MyOperator",
7351
+ noteText = "For card based subscriptions, your card would be charged minimum of \\u20B91 every month even if there are no usages to keep the subscription active, and \\u20B91 will be added as prepaid amount for your service. Initial deduction of \\u20B95 would be made for subscription, which will be auto-refunded.",
7352
+ noteLabel = "Note:",
7353
+ showCta = true,
7354
+ ctaText = "Enable Auto-Pay",
7355
+ onCtaClick,
7356
+ loading = false,
7357
+ disabled = false,
7358
+ defaultOpen = true,
7359
+ className,
7360
+ },
7361
+ ref
7362
+ ) => {
7363
+ return (
7364
+ <div ref={ref} className={cn("w-full", className)}>
7365
+ <Accordion
7366
+ type="single"
7367
+ variant="bordered"
7368
+ defaultValue={defaultOpen ? ["auto-pay-setup"] : []}
7369
+ >
7370
+ <AccordionItem value="auto-pay-setup">
7371
+ <AccordionTrigger className="px-4 py-4">
7372
+ <div className="flex items-center gap-3">
7373
+ {icon && (
7374
+ <div className="flex items-center justify-center size-10 rounded-[10px] bg-[var(--semantic-info-surface)] shrink-0">
7375
+ {icon}
7376
+ </div>
7377
+ )}
7378
+ <div className="flex flex-col gap-1 text-left">
7379
+ <span className="text-sm font-semibold text-semantic-text-primary tracking-[0.01px]">
7380
+ {title}
7381
+ </span>
7382
+ <span className="text-xs font-normal text-semantic-text-muted tracking-[0.048px]">
7383
+ {subtitle}
7384
+ </span>
7385
+ </div>
7386
+ </div>
7387
+ </AccordionTrigger>
7388
+
7389
+ <AccordionContent>
7390
+ <div className="flex flex-col gap-4 border-t border-semantic-border-layout pt-4">
7391
+ {/* Description */}
7392
+ {bodyText && (
7393
+ <div className="m-0 text-sm font-normal text-semantic-text-primary leading-5 tracking-[0.035px]">
7394
+ {bodyText}
7395
+ </div>
7396
+ )}
7397
+
7398
+ {/* Note callout */}
7399
+ {noteText && (
7400
+ <div className="rounded bg-[var(--semantic-info-25,#f0f7ff)] border border-[#BEDBFF] px-4 py-3">
7401
+ <p className="m-0 text-sm font-normal text-semantic-text-muted leading-5 tracking-[0.035px]">
7402
+ {noteLabel && (
7403
+ <span className="font-medium text-semantic-text-primary">
7404
+ {noteLabel}{" "}
7405
+ </span>
7406
+ )}
7407
+ {noteText}
7408
+ </p>
7409
+ </div>
7410
+ )}
7411
+
7412
+ {/* CTA Button */}
7413
+ {showCta && (
7414
+ <Button
7415
+ variant="default"
7416
+ className="w-full"
7417
+ onClick={onCtaClick}
7418
+ loading={loading}
7419
+ disabled={disabled}
7420
+ >
7421
+ {ctaText}
7422
+ </Button>
7423
+ )}
7424
+ </div>
7425
+ </AccordionContent>
7426
+ </AccordionItem>
7427
+ </Accordion>
7428
+ </div>
7429
+ );
7430
+ }
7431
+ );
7432
+
7433
+ AutoPaySetup.displayName = "AutoPaySetup";
7434
+ `, prefix)
7435
+ },
7436
+ {
7437
+ name: "types.ts",
7438
+ content: prefixTailwindClasses(`import * as React from "react";
7439
+
7440
+ /**
7441
+ * Props for the AutoPaySetup component
7442
+ */
7443
+ export interface AutoPaySetupProps {
7444
+ // Header
7445
+ /** Title displayed in the accordion header */
7446
+ title?: string;
7447
+ /** Subtitle displayed below the title */
7448
+ subtitle?: string;
7449
+ /** Icon displayed in the header (rendered inside a rounded container) */
7450
+ icon?: React.ReactNode;
7451
+
7452
+ // Body
7453
+ /** Description content displayed below the header when expanded. Accepts a string or JSX (e.g. text with a link). */
7454
+ bodyText?: React.ReactNode;
7455
+
7456
+ // Note callout
7457
+ /** Note/callout text displayed in a highlighted box */
7458
+ noteText?: string;
7459
+ /** Label prefix for the note (e.g., "Note:") */
7460
+ noteLabel?: string;
7461
+
7462
+ // CTA
7463
+ /** Whether to show the CTA button (defaults to true) */
7464
+ showCta?: boolean;
7465
+ /** Text for the CTA button (defaults to "Enable Auto-Pay") */
7466
+ ctaText?: string;
7467
+ /** Callback when CTA button is clicked */
7468
+ onCtaClick?: () => void;
7469
+ /** Whether the CTA button shows loading state */
7470
+ loading?: boolean;
7471
+ /** Whether the CTA button is disabled */
7472
+ disabled?: boolean;
7473
+
7474
+ // Accordion
7475
+ /** Whether the accordion is open by default */
7476
+ defaultOpen?: boolean;
7477
+
7478
+ // Styling
7479
+ /** Additional className for the root element */
7480
+ className?: string;
7481
+ }
7482
+ `, prefix)
7483
+ },
7484
+ {
7485
+ name: "index.ts",
7486
+ content: prefixTailwindClasses(`export { AutoPaySetup } from "./auto-pay-setup";
7487
+ export type { AutoPaySetupProps } from "./types";
7488
+ `, prefix)
7489
+ }
7490
+ ]
7491
+ },
7492
+ "bank-details": {
7493
+ name: "bank-details",
7494
+ description: "A component for displaying bank account details with copy-to-clipboard functionality",
7495
+ dependencies: [
7496
+ "clsx",
7497
+ "tailwind-merge",
7498
+ "lucide-react"
7499
+ ],
7500
+ internalDependencies: [
7501
+ "accordion"
7502
+ ],
7503
+ isMultiFile: true,
7504
+ directory: "bank-details",
7505
+ mainFile: "bank-details.tsx",
7506
+ files: [
7507
+ {
7508
+ name: "bank-details.tsx",
7509
+ content: prefixTailwindClasses(`import * as React from "react";
7510
+ import { Copy, Check } from "lucide-react";
7511
+ import { cn } from "../../../lib/utils";
7512
+ import {
7513
+ Accordion,
7514
+ AccordionItem,
7515
+ AccordionTrigger,
7516
+ AccordionContent,
7517
+ } from "@/components/ui/accordion";
7518
+ import type { BankDetailsProps, BankDetailItem } from "./types";
7519
+
7520
+ /**
7521
+ * BankDetails displays bank account information inside a collapsible accordion
7522
+ * card. Each row shows a label-value pair, with optional copy-to-clipboard
7523
+ * support for individual values.
7524
+ *
7525
+ * @example
7526
+ * \`\`\`tsx
7527
+ * <BankDetails
7528
+ * icon={<Landmark className="size-5 text-semantic-primary" />}
7529
+ * items={[
7530
+ * { label: "Account holder's name", value: "MyOperator" },
7531
+ * { label: "Account Number", value: "2223330026552601", copyable: true },
7532
+ * { label: "IFSC Code", value: "UTIB000RAZP", copyable: true },
7533
+ * { label: "Bank Name", value: "AXIS BANK" },
7534
+ * ]}
7535
+ * />
7536
+ * \`\`\`
7537
+ */
7538
+ export const BankDetails = React.forwardRef<HTMLDivElement, BankDetailsProps>(
7539
+ (
7540
+ {
7541
+ title = "Bank details",
7542
+ subtitle = "Direct NEFT/RTGS transfer",
7543
+ icon,
7544
+ items,
7545
+ defaultOpen = true,
7546
+ onCopy,
7547
+ className,
7548
+ },
7549
+ ref
7550
+ ) => {
7551
+ return (
7552
+ <div ref={ref} className={cn("w-full", className)}>
7553
+ <Accordion
7554
+ type="single"
7555
+ variant="bordered"
7556
+ defaultValue={defaultOpen ? ["bank-details"] : []}
7557
+ >
7558
+ <AccordionItem value="bank-details">
7559
+ <AccordionTrigger className="px-4 py-4">
7560
+ <div className="flex items-center gap-3">
7561
+ {icon && (
7562
+ <div className="flex items-center justify-center size-10 rounded-[10px] bg-[var(--semantic-info-surface)] shrink-0">
7563
+ {icon}
7564
+ </div>
7565
+ )}
7566
+ <div className="flex flex-col gap-1 text-left">
7567
+ <span className="text-sm font-semibold text-semantic-text-primary tracking-[0.01px]">
7568
+ {title}
7569
+ </span>
7570
+ <span className="text-xs font-normal text-semantic-text-muted tracking-[0.048px]">
7571
+ {subtitle}
7572
+ </span>
7573
+ </div>
7574
+ </div>
7575
+ </AccordionTrigger>
7576
+
7577
+ <AccordionContent>
7578
+ <div className="border-t border-semantic-border-layout pt-4">
7579
+ <div className="rounded-md border border-[var(--semantic-info-200,#e8f1fc)] bg-[var(--semantic-info-25,#f6f8fd)] p-3">
7580
+ <div className="flex flex-col gap-4">
7581
+ {items.map((item, index) => (
7582
+ <BankDetailRow
7583
+ key={index}
7584
+ item={item}
7585
+ onCopy={onCopy}
7586
+ />
7587
+ ))}
7588
+ </div>
7589
+ </div>
7590
+ </div>
7591
+ </AccordionContent>
7592
+ </AccordionItem>
7593
+ </Accordion>
7594
+ </div>
7595
+ );
7596
+ }
7597
+ );
7598
+
7599
+ BankDetails.displayName = "BankDetails";
7600
+
7601
+ /* \u2500\u2500\u2500 Internal row component \u2500\u2500\u2500 */
7602
+
7603
+ function BankDetailRow({
7604
+ item,
7605
+ onCopy,
7606
+ }: {
7607
+ item: BankDetailItem;
7608
+ onCopy?: (item: BankDetailItem) => void;
7609
+ }) {
7610
+ const [copied, setCopied] = React.useState(false);
7611
+
7612
+ const handleCopy = async () => {
7613
+ try {
7614
+ await navigator.clipboard.writeText(item.value);
7615
+ setCopied(true);
7616
+ onCopy?.(item);
7617
+ setTimeout(() => setCopied(false), 2000);
7618
+ } catch {
7619
+ // Clipboard API may fail in insecure contexts; silently ignore
7620
+ }
7621
+ };
7622
+
7623
+ return (
7624
+ <div className="group flex items-center justify-between text-sm tracking-[0.035px]">
7625
+ <span className="text-semantic-text-muted">{item.label}</span>
7626
+ <div className="flex items-center">
7627
+ <span className="text-semantic-text-primary text-right">
7628
+ {item.value}
7629
+ </span>
7630
+ {item.copyable && (
7631
+ <button
7632
+ type="button"
7633
+ onClick={handleCopy}
7634
+ className={cn(
7635
+ "inline-flex items-center justify-center rounded p-0.5 transition-all duration-200 overflow-hidden",
7636
+ copied
7637
+ ? "w-5 ml-1.5 opacity-100 text-semantic-success-primary"
7638
+ : "w-0 opacity-0 group-hover:w-5 group-hover:ml-1.5 group-hover:opacity-100 text-semantic-text-muted hover:text-semantic-text-primary"
7639
+ )}
7640
+ aria-label={\`Copy \${item.label}\`}
7641
+ >
7642
+ {copied ? (
7643
+ <Check className="size-3.5 shrink-0" />
7644
+ ) : (
7645
+ <Copy className="size-3.5 shrink-0" />
7646
+ )}
7647
+ </button>
7648
+ )}
7649
+ </div>
7650
+ </div>
7651
+ );
7652
+ }
7653
+ `, prefix)
7654
+ },
7655
+ {
7656
+ name: "types.ts",
7657
+ content: prefixTailwindClasses(`import * as React from "react";
7658
+
7659
+ /**
7660
+ * A single row of bank detail information.
7661
+ */
7662
+ export interface BankDetailItem {
7663
+ /** Label text displayed on the left (e.g., "Account Number") */
7664
+ label: string;
7665
+ /** Value text displayed on the right (e.g., "2223330026552601") */
7666
+ value: string;
7667
+ /** Whether to show a copy-to-clipboard button for this item's value */
7668
+ copyable?: boolean;
7669
+ }
7670
+
7671
+ /**
7672
+ * Props for the BankDetails component
7673
+ */
7674
+ export interface BankDetailsProps {
7675
+ // Header
7676
+ /** Title displayed in the accordion header */
7677
+ title?: string;
7678
+ /** Subtitle displayed below the title */
7679
+ subtitle?: string;
7680
+ /** Icon displayed in the header (rendered inside a rounded container) */
7681
+ icon?: React.ReactNode;
7682
+
7683
+ // Data
7684
+ /** Array of bank detail items to display */
7685
+ items: BankDetailItem[];
7686
+
7687
+ // Accordion
7688
+ /** Whether the accordion is open by default */
7689
+ defaultOpen?: boolean;
7690
+
7691
+ // Callbacks
7692
+ /** Callback fired when a value is copied to clipboard */
7693
+ onCopy?: (item: BankDetailItem) => void;
7694
+
7695
+ // Styling
7696
+ /** Additional className for the root element */
7697
+ className?: string;
7698
+ }
7699
+ `, prefix)
7700
+ },
7701
+ {
7702
+ name: "index.ts",
7703
+ content: prefixTailwindClasses(`export { BankDetails } from "./bank-details";
7704
+ export type { BankDetailsProps, BankDetailItem } from "./types";
7705
+ `, prefix)
7706
+ }
7707
+ ]
7708
+ },
7709
+ "payment-summary": {
7710
+ name: "payment-summary",
7711
+ description: "A component for displaying payment summary with line items and total",
7712
+ dependencies: [
7713
+ "clsx",
7714
+ "tailwind-merge",
7715
+ "lucide-react"
7716
+ ],
7717
+ internalDependencies: [
7718
+ "tooltip"
7719
+ ],
7720
+ isMultiFile: true,
7721
+ directory: "payment-summary",
7722
+ mainFile: "payment-summary.tsx",
7723
+ files: [
7724
+ {
7725
+ name: "payment-summary.tsx",
7726
+ content: prefixTailwindClasses(`import * as React from "react";
7727
+ import { Info } from "lucide-react";
7728
+ import { cn } from "../../../lib/utils";
7729
+ import {
7730
+ Tooltip,
7731
+ TooltipContent,
7732
+ TooltipProvider,
7733
+ TooltipTrigger,
7734
+ TooltipArrow,
7735
+ } from "@/components/ui/tooltip";
7736
+
7737
+ /**
7738
+ * Represents a single row in the payment summary.
7739
+ */
7740
+ export interface PaymentSummaryItem {
7741
+ /** Label text displayed on the left */
7742
+ label: string;
7743
+ /** Value text displayed on the right */
7744
+ value: string;
7745
+ /** Color variant for the value text */
7746
+ valueColor?: "default" | "success" | "error";
7747
+ /** Tooltip text shown when hovering the info icon next to the label */
7748
+ tooltip?: string;
7749
+ /** Whether to render label in bold (semibold weight) */
7750
+ bold?: boolean;
7751
+ /** Font size for the value \u2014 "lg" renders at 18px semibold */
7752
+ valueSize?: "default" | "lg";
7753
+ }
7754
+
7755
+ export interface PaymentSummaryProps {
7756
+ /** Line items displayed in the top section */
7757
+ items: PaymentSummaryItem[];
7758
+ /** Summary items displayed below the divider (e.g. totals) */
7759
+ summaryItems?: PaymentSummaryItem[];
7760
+ /** Custom className for the outer container */
7761
+ className?: string;
7762
+ }
7763
+
7764
+ const valueColorMap: Record<string, string> = {
7765
+ default: "text-semantic-text-primary",
7766
+ success: "text-semantic-success-primary",
7767
+ error: "text-semantic-error-primary",
7768
+ };
7769
+
7770
+ const SummaryRow = ({ item }: { item: PaymentSummaryItem }) => (
7771
+ <div className="flex items-center justify-between w-full">
7772
+ <div className="flex items-center gap-1.5">
7773
+ <span
7774
+ className={cn(
7775
+ "text-sm tracking-[0.035px]",
7776
+ item.bold
7777
+ ? "font-semibold text-semantic-text-primary"
7778
+ : "text-semantic-text-muted"
7779
+ )}
7780
+ >
7781
+ {item.label}
7782
+ </span>
7783
+ {item.tooltip && (
7784
+ <Tooltip>
7785
+ <TooltipTrigger asChild>
7786
+ <button
7787
+ type="button"
7788
+ className="inline-flex items-center justify-center rounded-full w-5 h-5 text-semantic-text-muted hover:text-semantic-text-primary hover:bg-semantic-bg-ui transition-colors"
7789
+ aria-label={\`Info about \${item.label}\`}
7790
+ >
7791
+ <Info className="h-3.5 w-3.5" />
7792
+ </button>
7793
+ </TooltipTrigger>
7794
+ <TooltipContent>
7795
+ <TooltipArrow />
7796
+ {item.tooltip}
7797
+ </TooltipContent>
7798
+ </Tooltip>
7799
+ )}
7800
+ </div>
7801
+ <span
7802
+ className={cn(
7803
+ "tracking-[0.035px]",
7804
+ item.valueSize === "lg" ? "text-lg font-semibold" : "text-sm",
7805
+ valueColorMap[item.valueColor ?? "default"]
7806
+ )}
7807
+ >
7808
+ {item.value}
7809
+ </span>
7810
+ </div>
7811
+ );
7812
+
7813
+ /**
7814
+ * PaymentSummary displays a card with line-item rows and an optional totals section
7815
+ * separated by a divider. Values can be color-coded (default, success, error) and
7816
+ * labels can optionally show info tooltips.
7817
+ *
7818
+ * @example
7819
+ * \`\`\`tsx
7820
+ * <PaymentSummary
7821
+ * items={[
7822
+ * { label: "Pending Rental", value: "\u20B90.00" },
7823
+ * { label: "Current Usage", value: "\u20B9163.98" },
7824
+ * { label: "Prepaid Wallet", value: "\u20B978,682.92", valueColor: "success" },
7825
+ * ]}
7826
+ * summaryItems={[
7827
+ * { label: "Total amount due", value: "-\u20B978,518.94", valueColor: "error", valueSize: "lg", bold: true, tooltip: "Sum of all charges" },
7828
+ * { label: "Credit limit", value: "\u20B910,000.00", tooltip: "Your current credit limit" },
7829
+ * ]}
7830
+ * />
7831
+ * \`\`\`
7832
+ */
7833
+ export const PaymentSummary = React.forwardRef<
7834
+ HTMLDivElement,
7835
+ PaymentSummaryProps
7836
+ >(({ items, summaryItems, className }, ref) => {
7837
+ return (
7838
+ <TooltipProvider delayDuration={100}>
7839
+ <div
7840
+ ref={ref}
7841
+ className={cn(
7842
+ "rounded-lg border border-semantic-border-layout bg-semantic-bg-primary p-5",
7843
+ className
7844
+ )}
7845
+ >
7846
+ <div className="flex flex-col gap-5">
7847
+ {/* Line items */}
7848
+ {items.length > 0 && (
7849
+ <div
7850
+ className={cn(
7851
+ "flex flex-col gap-5",
7852
+ summaryItems && summaryItems.length > 0 &&
7853
+ "border-b border-semantic-border-layout pb-5"
7854
+ )}
7855
+ >
7856
+ {items.map((item, index) => (
7857
+ <SummaryRow key={index} item={item} />
7858
+ ))}
7859
+ </div>
7860
+ )}
7861
+
7862
+ {/* Summary items (below divider) */}
7863
+ {summaryItems && summaryItems.length > 0 && (
7864
+ <div className="flex flex-col gap-5">
7865
+ {summaryItems.map((item, index) => (
7866
+ <SummaryRow key={index} item={item} />
7867
+ ))}
7868
+ </div>
7869
+ )}
7870
+ </div>
7871
+ </div>
7872
+ </TooltipProvider>
7873
+ );
7874
+ });
7875
+
7876
+ PaymentSummary.displayName = "PaymentSummary";
7877
+ `, prefix)
7878
+ },
7879
+ {
7880
+ name: "index.ts",
7881
+ content: prefixTailwindClasses(`export { PaymentSummary } from "./payment-summary";
7882
+ export type {
7883
+ PaymentSummaryProps,
7884
+ PaymentSummaryItem,
7885
+ } from "./payment-summary";
7886
+ `, prefix)
7887
+ }
7888
+ ]
7889
+ },
7890
+ "wallet-topup": {
7891
+ name: "wallet-topup",
7892
+ description: "A component for wallet top-up with amount selection and coupon support",
7893
+ dependencies: [
7894
+ "clsx",
7895
+ "tailwind-merge",
7896
+ "lucide-react"
7897
+ ],
7898
+ internalDependencies: [
7899
+ "accordion",
7900
+ "button",
7901
+ "input"
7902
+ ],
7903
+ isMultiFile: true,
7904
+ directory: "wallet-topup",
7905
+ mainFile: "wallet-topup.tsx",
7906
+ files: [
7907
+ {
7908
+ name: "wallet-topup.tsx",
7909
+ content: prefixTailwindClasses(`import * as React from "react";
7910
+ import { Check, Ticket } from "lucide-react";
7911
+ import { cn } from "../../../lib/utils";
7912
+ import { Button } from "@/components/ui/button";
7913
+ import { Input } from "@/components/ui/input";
7914
+ import {
7915
+ Accordion,
7916
+ AccordionItem,
7917
+ AccordionTrigger,
7918
+ AccordionContent,
7919
+ } from "@/components/ui/accordion";
7920
+ import type { AmountOption, WalletTopupProps } from "./types";
7921
+
7922
+ /**
7923
+ * Normalize amount option to a consistent format
7924
+ */
7925
+ function normalizeAmountOption(option: number | AmountOption): AmountOption {
7926
+ return typeof option === "number" ? { value: option } : option;
7927
+ }
7928
+
7929
+ /**
7930
+ * Format currency amount with symbol
7931
+ */
7932
+ function formatCurrency(amount: number, symbol: string = "\u20B9"): string {
7933
+ return \`\${symbol}\${amount.toLocaleString("en-IN")}\`;
7934
+ }
7935
+
7936
+ /**
7937
+ * WalletTopup provides a collapsible panel for wallet recharge with
7938
+ * preset amount selection, custom amount input, voucher link, and payment CTA.
7939
+ *
7940
+ * @example
7941
+ * \`\`\`tsx
7942
+ * <WalletTopup
7943
+ * icon={<CreditCard className="size-5 text-semantic-primary" />}
7944
+ * amounts={[500, 1000, 5000, 10000]}
7945
+ * onPay={(amount) => console.log("Pay", amount)}
7946
+ * />
7947
+ * \`\`\`
7948
+ */
7949
+ export const WalletTopup = React.forwardRef<HTMLDivElement, WalletTopupProps>(
7950
+ (
7951
+ {
7952
+ title = "Instant wallet top-up",
7953
+ description = "Add funds to your account balance",
7954
+ icon,
7955
+ amounts = [500, 1000, 5000, 10000],
7956
+ selectedAmount: controlledAmount,
7957
+ defaultSelectedAmount,
7958
+ onAmountChange,
7959
+ amountSectionLabel = "Select Amount",
7960
+ customAmount: controlledCustomAmount,
7961
+ onCustomAmountChange,
7962
+ customAmountPlaceholder = "Enter amount",
7963
+ customAmountLabel = "Custom Amount",
7964
+ currencySymbol = "\u20B9",
7965
+ showVoucherLink = true,
7966
+ voucherLinkText = "Have an offline code or voucher?",
7967
+ voucherIcon = <Ticket className="size-4" />,
7968
+ onVoucherClick,
7969
+ voucherCode: controlledVoucherCode,
7970
+ onVoucherCodeChange,
7971
+ voucherCodePlaceholder = "XXXX-XXXX-XXXX",
7972
+ voucherCodeLabel = "Enter Offline Code",
7973
+ voucherCancelText = "Cancel",
7974
+ voucherCodePattern,
7975
+ validateVoucherCode,
7976
+ redeemText = "Redeem voucher",
7977
+ onRedeem,
7978
+ ctaText,
7979
+ onPay,
7980
+ loading = false,
7981
+ disabled = false,
7982
+ defaultOpen = true,
7983
+ className,
7984
+ },
7985
+ ref
7986
+ ) => {
7987
+ // Controlled/uncontrolled amount selection
7988
+ const isControlled = controlledAmount !== undefined;
7989
+ const [internalAmount, setInternalAmount] = React.useState<number | null>(
7990
+ defaultSelectedAmount ?? null
7991
+ );
7992
+ const selectedValue = isControlled ? controlledAmount : internalAmount;
7993
+
7994
+ // Custom amount state
7995
+ const isCustomControlled = controlledCustomAmount !== undefined;
7996
+ const [internalCustom, setInternalCustom] = React.useState("");
7997
+ const customValue = isCustomControlled
7998
+ ? controlledCustomAmount
7999
+ : internalCustom;
8000
+
8001
+ // Voucher code input state
8002
+ const [showVoucherInput, setShowVoucherInput] = React.useState(false);
8003
+ const isVoucherCodeControlled = controlledVoucherCode !== undefined;
8004
+ const [internalVoucherCode, setInternalVoucherCode] = React.useState("");
8005
+ const voucherCodeValue = isVoucherCodeControlled
8006
+ ? controlledVoucherCode
8007
+ : internalVoucherCode;
8008
+
8009
+ const handleVoucherLinkClick = () => {
8010
+ setShowVoucherInput(true);
8011
+ onVoucherClick?.();
8012
+ };
8013
+
8014
+ const handleVoucherCancel = () => {
8015
+ setShowVoucherInput(false);
8016
+ if (!isVoucherCodeControlled) {
8017
+ setInternalVoucherCode("");
8018
+ }
8019
+ onVoucherCodeChange?.("");
8020
+ };
8021
+
8022
+ const handleVoucherCodeChange = (
8023
+ e: React.ChangeEvent<HTMLInputElement>
8024
+ ) => {
8025
+ const value = e.target.value;
8026
+ if (!isVoucherCodeControlled) {
8027
+ setInternalVoucherCode(value);
8028
+ }
8029
+ onVoucherCodeChange?.(value);
8030
+ };
8031
+
8032
+ const isVoucherCodeValid = React.useMemo(() => {
8033
+ if (!voucherCodeValue) return false;
8034
+ if (validateVoucherCode) return validateVoucherCode(voucherCodeValue);
8035
+ if (voucherCodePattern) return voucherCodePattern.test(voucherCodeValue);
8036
+ return true;
8037
+ }, [voucherCodeValue, validateVoucherCode, voucherCodePattern]);
8038
+
8039
+ const handleRedeem = () => {
8040
+ if (isVoucherCodeValid) {
8041
+ onRedeem?.(voucherCodeValue);
8042
+ }
8043
+ };
8044
+
8045
+ const normalizedAmounts = amounts.map(normalizeAmountOption);
8046
+
8047
+ const handleAmountSelect = (value: number) => {
8048
+ const newValue = selectedValue === value ? null : value;
8049
+ if (!isControlled) {
8050
+ setInternalAmount(newValue);
8051
+ }
8052
+ // Clear custom amount when preset is selected
8053
+ if (!isCustomControlled && newValue !== null) {
8054
+ setInternalCustom("");
8055
+ }
8056
+ onAmountChange?.(newValue);
8057
+ };
8058
+
8059
+ const handleCustomAmountChange = (
8060
+ e: React.ChangeEvent<HTMLInputElement>
8061
+ ) => {
8062
+ const value = e.target.value;
8063
+ if (!isCustomControlled) {
8064
+ setInternalCustom(value);
8065
+ }
8066
+ // Clear preset selection when custom amount is entered
8067
+ if (value && !isControlled) {
8068
+ setInternalAmount(null);
8069
+ }
8070
+ if (value) {
8071
+ onAmountChange?.(null);
8072
+ }
8073
+ onCustomAmountChange?.(value);
8074
+ };
8075
+
8076
+ // Determine the effective pay amount
8077
+ const payAmount =
8078
+ selectedValue ?? (customValue ? Number(customValue) : 0);
8079
+
8080
+ const handlePay = () => {
8081
+ if (payAmount > 0) {
8082
+ onPay?.(payAmount);
8083
+ }
8084
+ };
8085
+
8086
+ const buttonText =
8087
+ ctaText ||
8088
+ (payAmount > 0
8089
+ ? \`Pay \${formatCurrency(payAmount, currencySymbol)} now\`
8090
+ : "Select an amount");
8091
+
8092
+ return (
8093
+ <div ref={ref} className={cn("w-full", className)}>
8094
+ <Accordion
8095
+ type="single"
8096
+ variant="bordered"
8097
+ defaultValue={defaultOpen ? ["wallet-topup"] : []}
8098
+ >
8099
+ <AccordionItem value="wallet-topup">
8100
+ <AccordionTrigger className="px-4 py-4">
8101
+ <div className="flex items-center gap-3">
8102
+ {icon && (
8103
+ <div className="flex items-center justify-center size-10 rounded-[10px] bg-[var(--semantic-info-surface)] shrink-0">
8104
+ {icon}
8105
+ </div>
8106
+ )}
8107
+ <div className="flex flex-col gap-1 text-left">
8108
+ <span className="text-sm font-semibold text-semantic-text-primary tracking-[0.01px]">
8109
+ {title}
8110
+ </span>
8111
+ <span className="text-xs font-normal text-semantic-text-muted tracking-[0.048px]">
8112
+ {description}
8113
+ </span>
8114
+ </div>
8115
+ </div>
8116
+ </AccordionTrigger>
8117
+
8118
+ <AccordionContent>
8119
+ <div className="flex flex-col gap-6 border-t border-semantic-border-layout pt-4">
8120
+ {/* Amount Selection */}
8121
+ <div className="flex flex-col gap-1.5">
8122
+ <label className="text-xs font-normal text-semantic-text-muted tracking-[0.048px]">
8123
+ {amountSectionLabel}
8124
+ </label>
8125
+ <div className="grid grid-cols-2 gap-4">
8126
+ {normalizedAmounts.map((option) => {
8127
+ const isSelected = selectedValue === option.value;
8128
+ return (
8129
+ <button
8130
+ key={option.value}
8131
+ type="button"
8132
+ role="radio"
8133
+ aria-checked={isSelected}
8134
+ onClick={() => handleAmountSelect(option.value)}
8135
+ className={cn(
8136
+ "flex items-center justify-between h-10 px-4 py-2.5 rounded text-sm text-semantic-text-primary transition-all cursor-pointer",
8137
+ isSelected
8138
+ ? "border border-semantic-primary shadow-sm"
8139
+ : "border border-semantic-border-input hover:border-semantic-text-muted"
8140
+ )}
8141
+ >
8142
+ <span>
8143
+ {option.label ||
8144
+ formatCurrency(option.value, currencySymbol)}
8145
+ </span>
8146
+ {isSelected && (
8147
+ <Check className="size-5 text-semantic-primary" />
8148
+ )}
8149
+ </button>
8150
+ );
8151
+ })}
8152
+ </div>
8153
+ </div>
8154
+
8155
+ {/* Custom Amount */}
8156
+ <div className="flex flex-col gap-1.5">
8157
+ <label className="text-xs font-normal text-semantic-text-muted tracking-[0.048px]">
8158
+ {customAmountLabel}
8159
+ </label>
8160
+ <Input
8161
+ type="number"
8162
+ placeholder={customAmountPlaceholder}
8163
+ value={customValue}
8164
+ onChange={handleCustomAmountChange}
8165
+ />
8166
+ </div>
8167
+
8168
+ {/* Voucher Link or Voucher Code Input */}
8169
+ {showVoucherLink && !showVoucherInput && (
8170
+ <button
8171
+ type="button"
8172
+ onClick={handleVoucherLinkClick}
8173
+ className="flex items-center gap-2 text-sm text-semantic-text-link tracking-[0.035px] hover:underline w-fit"
8174
+ >
8175
+ {voucherIcon}
8176
+ <span>{voucherLinkText}</span>
8177
+ </button>
8178
+ )}
8179
+
8180
+ {showVoucherInput && (
8181
+ <div className="flex flex-col gap-1.5">
8182
+ <div className="flex items-center justify-between">
8183
+ <label className="text-xs font-normal text-semantic-text-muted tracking-[0.048px]">
8184
+ {voucherCodeLabel}
8185
+ </label>
8186
+ <button
8187
+ type="button"
8188
+ onClick={handleVoucherCancel}
8189
+ className="text-xs text-semantic-text-link tracking-[0.048px] hover:underline"
8190
+ >
8191
+ {voucherCancelText}
8192
+ </button>
8193
+ </div>
8194
+ <Input
8195
+ placeholder={voucherCodePlaceholder}
8196
+ value={voucherCodeValue}
8197
+ onChange={handleVoucherCodeChange}
8198
+ />
8199
+ </div>
8200
+ )}
8201
+
8202
+ {/* CTA Button */}
8203
+ {showVoucherInput ? (
8204
+ <Button
8205
+ variant="default"
8206
+ className="w-full bg-[var(--semantic-success-primary)] hover:bg-[var(--semantic-success-hover)]"
8207
+ onClick={handleRedeem}
8208
+ loading={loading}
8209
+ disabled={disabled || !isVoucherCodeValid}
8210
+ >
8211
+ {redeemText}
8212
+ </Button>
8213
+ ) : (
8214
+ <Button
8215
+ variant="default"
8216
+ className="w-full"
8217
+ onClick={handlePay}
8218
+ loading={loading}
8219
+ disabled={disabled || payAmount <= 0}
8220
+ >
8221
+ {buttonText}
8222
+ </Button>
8223
+ )}
8224
+ </div>
8225
+ </AccordionContent>
8226
+ </AccordionItem>
8227
+ </Accordion>
8228
+ </div>
8229
+ );
8230
+ }
8231
+ );
8232
+
8233
+ WalletTopup.displayName = "WalletTopup";
8234
+ `, prefix)
8235
+ },
8236
+ {
8237
+ name: "types.ts",
8238
+ content: prefixTailwindClasses(`import * as React from "react";
8239
+
8240
+ /**
8241
+ * Represents a preset amount option in the selector grid
8242
+ */
8243
+ export interface AmountOption {
8244
+ /** The numeric value of the amount */
8245
+ value: number;
8246
+ /** Optional custom display label (defaults to formatted currency) */
8247
+ label?: string;
8248
+ }
8249
+
8250
+ /**
8251
+ * Props for the WalletTopup component
8252
+ */
8253
+ export interface WalletTopupProps {
8254
+ // Header
8255
+ /** Title displayed in the accordion header */
8256
+ title?: string;
8257
+ /** Description displayed below the title */
8258
+ description?: string;
8259
+ /** Icon displayed in the header (rendered inside a rounded container) */
8260
+ icon?: React.ReactNode;
8261
+
8262
+ // Amount selection
8263
+ /** Preset amount options to display in the grid */
8264
+ amounts?: number[] | AmountOption[];
8265
+ /** Currently selected amount (controlled) */
8266
+ selectedAmount?: number | null;
8267
+ /** Default selected amount (uncontrolled) */
8268
+ defaultSelectedAmount?: number;
8269
+ /** Callback when amount selection changes */
8270
+ onAmountChange?: (amount: number | null) => void;
8271
+ /** Label for the amount selection section */
8272
+ amountSectionLabel?: string;
8273
+
8274
+ // Custom amount
8275
+ /** Custom amount input value (controlled) */
8276
+ customAmount?: string;
8277
+ /** Callback when custom amount input changes */
8278
+ onCustomAmountChange?: (value: string) => void;
8279
+ /** Placeholder text for custom amount input */
8280
+ customAmountPlaceholder?: string;
8281
+ /** Label for the custom amount field */
8282
+ customAmountLabel?: string;
8283
+
8284
+ // Currency
8285
+ /** Currency symbol (default: "\u20B9") */
8286
+ currencySymbol?: string;
8287
+
8288
+ // Voucher link
8289
+ /** Whether to show the voucher/code link */
8290
+ showVoucherLink?: boolean;
8291
+ /** Custom text for the voucher link */
8292
+ voucherLinkText?: string;
8293
+ /** Icon for the voucher link */
8294
+ voucherIcon?: React.ReactNode;
8295
+ /** Callback when voucher link is clicked (also toggles inline code input) */
8296
+ onVoucherClick?: () => void;
8297
+
8298
+ // Voucher code input
8299
+ /** Voucher code value (controlled) */
8300
+ voucherCode?: string;
8301
+ /** Callback when voucher code changes */
8302
+ onVoucherCodeChange?: (code: string) => void;
8303
+ /** Placeholder for voucher code input */
8304
+ voucherCodePlaceholder?: string;
8305
+ /** Label for voucher code input */
8306
+ voucherCodeLabel?: string;
8307
+ /** Text for cancel link in voucher mode */
8308
+ voucherCancelText?: string;
8309
+ /** Regex pattern the voucher code must match to enable redeem (e.g. /^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/) */
8310
+ voucherCodePattern?: RegExp;
8311
+ /** Custom validator function \u2014 return true if code is valid. Takes priority over voucherCodePattern. */
8312
+ validateVoucherCode?: (code: string) => boolean;
8313
+ /** Text for the redeem button */
8314
+ redeemText?: string;
8315
+ /** Callback when redeem voucher is clicked */
8316
+ onRedeem?: (code: string) => void;
8317
+
8318
+ // CTA
8319
+ /** Text for the pay button (defaults to "Pay {amount} now") */
8320
+ ctaText?: string;
8321
+ /** Callback when pay button is clicked */
8322
+ onPay?: (amount: number) => void;
8323
+ /** Whether the pay button shows loading state */
8324
+ loading?: boolean;
8325
+ /** Whether the pay button is disabled */
8326
+ disabled?: boolean;
8327
+
8328
+ // Accordion
8329
+ /** Whether the accordion is open by default */
8330
+ defaultOpen?: boolean;
8331
+
8332
+ // Styling
8333
+ /** Additional className for the root element */
8334
+ className?: string;
8335
+ }
8336
+ `, prefix)
8337
+ },
8338
+ {
8339
+ name: "index.ts",
8340
+ content: prefixTailwindClasses(`export { WalletTopup } from "./wallet-topup";
8341
+ export type { WalletTopupProps, AmountOption } from "./types";
6612
8342
  `, prefix)
6613
8343
  }
6614
8344
  ]