@bharat-ui/react 0.1.0

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.
package/README.md ADDED
@@ -0,0 +1,159 @@
1
+ # @bharat-ui/react
2
+
3
+ React components for Indian products — PAN input, Aadhaar OTP, UPI payments, pincode lookup and ₹ amount entry.
4
+ Built on top of [`@bharat-ui/validators`](https://npmjs.com/package/@bharat-ui/validators). Requires React 19.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npm install @bharat-ui/react
10
+ ```
11
+
12
+ ## Components
13
+
14
+ Import each component from its own subpath:
15
+
16
+ ```ts
17
+ import { AmountInput } from '@bharat-ui/react/AmountInput';
18
+ import { OTPInput } from '@bharat-ui/react/OTPInput';
19
+ import { PANInput } from '@bharat-ui/react/PANInput';
20
+ import { PincodeInput } from '@bharat-ui/react/PincodeInput';
21
+ import { UPIButton } from '@bharat-ui/react/UPIButton';
22
+ ```
23
+
24
+ ---
25
+
26
+ ### `<AmountInput />`
27
+
28
+ Currency input with live INR formatting. Displays ₹ formatted output while storing the raw number internally.
29
+
30
+ ```tsx
31
+ <AmountInput
32
+ label="Loan amount"
33
+ value={amount}
34
+ onChange={(raw, formatted) => setAmount(raw)}
35
+ />
36
+ ```
37
+
38
+ | Prop | Type | Default |
39
+ |---|---|---|
40
+ | `value` | `number \| undefined` | — |
41
+ | `onChange` | `(raw: number, formatted: string) => void` | — |
42
+ | `label` | `string` | — |
43
+ | `placeholder` | `string` | `'0'` |
44
+ | `error` | `string` | — |
45
+ | `disabled` | `boolean` | `false` |
46
+ | `showCompact` | `boolean` | `true` |
47
+
48
+ ---
49
+
50
+ ### `<OTPInput />`
51
+
52
+ Individual-box OTP entry with auto-advance, backspace handling, paste support and a built-in resend countdown timer.
53
+
54
+ ```tsx
55
+ <OTPInput
56
+ label="Enter OTP sent to +91 98765 43210"
57
+ length={6}
58
+ resendAfterSeconds={30}
59
+ onComplete={(otp) => verifyOTP(otp)}
60
+ onResend={() => requestNewOTP()}
61
+ />
62
+ ```
63
+
64
+ | Prop | Type | Default |
65
+ |---|---|---|
66
+ | `length` | `4 \| 6` | `6` |
67
+ | `label` | `string` | — |
68
+ | `onComplete` | `(otp: string) => void` | — |
69
+ | `onChange` | `(otp: string) => void` | — |
70
+ | `resendAfterSeconds` | `number` | `30` |
71
+ | `onResend` | `() => void` | — |
72
+ | `error` | `string` | — |
73
+ | `disabled` | `boolean` | `false` |
74
+
75
+ ---
76
+
77
+ ### `<PANInput />`
78
+
79
+ Text input that auto-uppercases, validates PAN format on each keystroke, and shows the taxpayer type badge on valid input.
80
+
81
+ ```tsx
82
+ <PANInput
83
+ label="PAN number"
84
+ value={pan}
85
+ onChange={(value, valid) => setPan(value)}
86
+ />
87
+ ```
88
+
89
+ | Prop | Type | Default |
90
+ |---|---|---|
91
+ | `value` | `string` | `''` |
92
+ | `onChange` | `(value: string, valid: boolean) => void` | — |
93
+ | `label` | `string` | — |
94
+ | `error` | `string` | — |
95
+ | `disabled` | `boolean` | `false` |
96
+ | `showTypeBadge` | `boolean` | `true` |
97
+
98
+ ---
99
+
100
+ ### `<PincodeInput />`
101
+
102
+ Numeric input that looks up the 6-digit pincode in the India Post dataset and shows the resolved district and state on valid input — no network calls.
103
+
104
+ ```tsx
105
+ <PincodeInput
106
+ label="Pincode"
107
+ value={pincode}
108
+ onChange={(value, valid) => setPincode(value)}
109
+ />
110
+ ```
111
+
112
+ | Prop | Type | Default |
113
+ |---|---|---|
114
+ | `value` | `string` | `''` |
115
+ | `onChange` | `(value: string, valid: boolean) => void` | — |
116
+ | `label` | `string` | — |
117
+ | `error` | `string` | — |
118
+ | `disabled` | `boolean` | `false` |
119
+ | `showCascade` | `boolean` | `true` |
120
+
121
+ ---
122
+
123
+ ### `<UPIButton />`
124
+
125
+ Renders a row of UPI app buttons (GPay, PhonePe, Paytm, BHIM) that each generate a deep-link payment URI. No SDK or backend required.
126
+
127
+ ```tsx
128
+ <UPIButton
129
+ vpa="merchant@okaxis"
130
+ amount={499}
131
+ merchantName="Acme Store"
132
+ transactionNote="Order #1234"
133
+ />
134
+ ```
135
+
136
+ | Prop | Type | Default |
137
+ |---|---|---|
138
+ | `vpa` | `string` | — |
139
+ | `amount` | `number` | — |
140
+ | `merchantName` | `string` | — |
141
+ | `transactionNote` | `string` | — |
142
+ | `currency` | `string` | `'INR'` |
143
+ | `onSuccess` | `() => void` | — |
144
+ | `onError` | `(error: string) => void` | — |
145
+
146
+ ---
147
+
148
+ ## Peer dependencies
149
+
150
+ ```json
151
+ {
152
+ "react": "^19.0.0",
153
+ "react-dom": "^19.0.0"
154
+ }
155
+ ```
156
+
157
+ ## License
158
+
159
+ MIT
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@bharat-ui/react",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "exports": {
6
+ "./AmountInput": "./src/AmountInput.tsx",
7
+ "./OTPInput": "./src/OTPInput.tsx",
8
+ "./PANInput": "./src/PANInput.tsx",
9
+ "./PincodeInput": "./src/PincodeInput.tsx",
10
+ "./UPIButton": "./src/UPIButton.tsx"
11
+ },
12
+ "scripts": {
13
+ "lint": "eslint . --max-warnings 0",
14
+ "check-types": "tsc --noEmit",
15
+ "build": "tsc"
16
+ },
17
+ "dependencies": {
18
+ "@bharat-ui/validators": "0.1.0",
19
+ "@bharat-ui/data": "0.1.0"
20
+ },
21
+ "devDependencies": {
22
+ "@bharat-ui/eslint-config": "*",
23
+ "@bharat-ui/typescript-config": "*",
24
+ "@types/node": "^22.15.3",
25
+ "@types/react": "19.2.2",
26
+ "@types/react-dom": "19.2.2",
27
+ "eslint": "^9.39.1",
28
+ "typescript": "5.9.2"
29
+ },
30
+ "files": [
31
+ "src",
32
+ "README.md"
33
+ ],
34
+ "peerDependencies": {
35
+ "react": "^19.0.0",
36
+ "react-dom": "^19.0.0"
37
+ }
38
+ }
@@ -0,0 +1,131 @@
1
+ "use client";
2
+
3
+ import { formatINR, formatINRCompact, parseINR } from "@bharat-ui/validators";
4
+ import * as React from "react";
5
+
6
+ export interface AmountInputProps {
7
+ value?: number;
8
+ onChange?: (raw: number, formatted: string) => void;
9
+ placeholder?: string;
10
+ disabled?: boolean;
11
+ label?: string;
12
+ error?: string;
13
+ showCompact?: boolean;
14
+ }
15
+
16
+ export function AmountInput({
17
+ value,
18
+ onChange,
19
+ placeholder = "0",
20
+ disabled = false,
21
+ label,
22
+ error,
23
+ showCompact = true,
24
+ }: AmountInputProps) {
25
+ const [display, setDisplay] = React.useState(value ? formatINR(value) : "");
26
+ const [focused, setFocused] = React.useState(false);
27
+
28
+ const handleFocus = () => {
29
+ setFocused(true);
30
+ if (display) {
31
+ setDisplay(String(parseINR(display)));
32
+ }
33
+ };
34
+
35
+ const handleBlur = () => {
36
+ setFocused(false);
37
+ const raw = parseINR(display);
38
+ if (!isNaN(raw) && raw > 0) {
39
+ setDisplay(formatINR(raw));
40
+ onChange?.(raw, formatINR(raw));
41
+ }
42
+ };
43
+
44
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
45
+ const val = e.target.value.replace(/[^\d]/g, "");
46
+ setDisplay(val);
47
+ };
48
+
49
+ const raw = parseINR(display);
50
+ const compact = !isNaN(raw) && raw > 0 ? formatINRCompact(raw) : null;
51
+
52
+ return (
53
+ <div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
54
+ {label && (
55
+ <label
56
+ style={{
57
+ fontSize: "13px",
58
+ fontWeight: 500,
59
+ color: error ? "#dc2626" : "#6b7280",
60
+ }}
61
+ >
62
+ {label}
63
+ </label>
64
+ )}
65
+ <div
66
+ style={{
67
+ display: "flex",
68
+ alignItems: "center",
69
+ border: `1.5px solid ${error ? "#dc2626" : focused ? "#B45309" : "#d1d5db"}`,
70
+ borderRadius: "8px",
71
+ padding: "0 12px",
72
+ gap: "6px",
73
+ background: disabled ? "#f9fafb" : "#ffffff",
74
+ boxShadow: focused ? "0 0 0 3px rgba(180,83,9,0.12)" : "none",
75
+ transition: "all 0.15s ease",
76
+ }}
77
+ >
78
+ <span
79
+ style={{
80
+ fontSize: "16px",
81
+ fontWeight: 500,
82
+ color: "#6b7280",
83
+ userSelect: "none",
84
+ }}
85
+ >
86
+
87
+ </span>
88
+ <input
89
+ type="text"
90
+ inputMode="numeric"
91
+ value={display}
92
+ placeholder={placeholder}
93
+ disabled={disabled}
94
+ onChange={handleChange}
95
+ onFocus={handleFocus}
96
+ onBlur={handleBlur}
97
+ style={{
98
+ border: "none",
99
+ outline: "none",
100
+ fontSize: "16px",
101
+ fontWeight: 500,
102
+ padding: "10px 0",
103
+ width: "100%",
104
+ background: "transparent",
105
+ color: "#111827",
106
+ }}
107
+ />
108
+ </div>
109
+ {showCompact && compact && !focused && (
110
+ <span
111
+ style={{
112
+ fontSize: "12px",
113
+ color: "#6b7280",
114
+ }}
115
+ >
116
+ {compact}
117
+ </span>
118
+ )}
119
+ {error && (
120
+ <span
121
+ style={{
122
+ fontSize: "12px",
123
+ color: "#dc2626",
124
+ }}
125
+ >
126
+ {error}
127
+ </span>
128
+ )}
129
+ </div>
130
+ );
131
+ }
@@ -0,0 +1,202 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ export interface OTPInputProps {
6
+ length?: 4 | 6;
7
+ onComplete?: (otp: string) => void;
8
+ onChange?: (otp: string) => void;
9
+ resendAfterSeconds?: number;
10
+ onResend?: () => void;
11
+ error?: string;
12
+ disabled?: boolean;
13
+ label?: string;
14
+ }
15
+
16
+ export function OTPInput({
17
+ length = 6,
18
+ onComplete,
19
+ onChange,
20
+ resendAfterSeconds = 30,
21
+ onResend,
22
+ error,
23
+ disabled = false,
24
+ label,
25
+ }: OTPInputProps) {
26
+ const [values, setValues] = React.useState<string[]>(Array(length).fill(""));
27
+ const [seconds, setSeconds] = React.useState(resendAfterSeconds);
28
+ const [focused, setFocused] = React.useState<number | null>(null);
29
+ const refs = React.useRef<(HTMLInputElement | null)[]>([]);
30
+
31
+ React.useEffect(() => {
32
+ if (seconds <= 0) return;
33
+ const timer = setTimeout(() => setSeconds((s) => s - 1), 1000);
34
+ return () => clearTimeout(timer);
35
+ }, [seconds]);
36
+
37
+ const handleChange = (index: number, val: string) => {
38
+ const digit = val.replace(/\D/g, "").slice(-1);
39
+ const next = [...values];
40
+ next[index] = digit;
41
+ setValues(next);
42
+
43
+ const otp = next.join("");
44
+ onChange?.(otp);
45
+
46
+ if (digit && index < length - 1) {
47
+ refs.current[index + 1]?.focus();
48
+ }
49
+
50
+ if (next.every((v) => v !== "") && otp.length === length) {
51
+ onComplete?.(otp);
52
+ }
53
+ };
54
+
55
+ const handleKeyDown = (
56
+ index: number,
57
+ e: React.KeyboardEvent<HTMLInputElement>,
58
+ ) => {
59
+ if (e.key === "Backspace") {
60
+ if (values[index]) {
61
+ const next = [...values];
62
+ next[index] = "";
63
+ setValues(next);
64
+ onChange?.(next.join(""));
65
+ } else if (index > 0) {
66
+ refs.current[index - 1]?.focus();
67
+ }
68
+ }
69
+ };
70
+
71
+ const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
72
+ e.preventDefault();
73
+ const pasted = e.clipboardData
74
+ .getData("text")
75
+ .replace(/\D/g, "")
76
+ .slice(0, length);
77
+
78
+ if (!pasted) return;
79
+
80
+ const next = Array(length).fill("");
81
+ pasted.split("").forEach((char, i) => {
82
+ next[i] = char;
83
+ });
84
+ setValues(next);
85
+ onChange?.(next.join(""));
86
+
87
+ const lastFilled = Math.min(pasted.length, length - 1);
88
+ refs.current[lastFilled]?.focus();
89
+
90
+ if (pasted.length === length) {
91
+ onComplete?.(pasted);
92
+ }
93
+ };
94
+
95
+ const handleResend = () => {
96
+ setSeconds(resendAfterSeconds);
97
+ setValues(Array(length).fill(""));
98
+ refs.current[0]?.focus();
99
+ onResend?.();
100
+ };
101
+
102
+ return (
103
+ <div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
104
+ {label && (
105
+ <label
106
+ style={{
107
+ fontSize: "13px",
108
+ fontWeight: 500,
109
+ color: error ? "#dc2626" : "#6b7280",
110
+ }}
111
+ >
112
+ {label}
113
+ </label>
114
+ )}
115
+
116
+ <div
117
+ style={{ display: "flex", gap: "8px" }}
118
+ role="group"
119
+ aria-label={label ?? "OTP input"}
120
+ >
121
+ {values.map((val, i) => (
122
+ <input
123
+ key={i}
124
+ ref={(el) => {
125
+ refs.current[i] = el;
126
+ }}
127
+ type="text"
128
+ inputMode="numeric"
129
+ maxLength={1}
130
+ value={val}
131
+ disabled={disabled}
132
+ aria-label={`Digit ${i + 1} of ${length}`}
133
+ onChange={(e) => handleChange(i, e.target.value)}
134
+ onKeyDown={(e) => handleKeyDown(i, e)}
135
+ onPaste={handlePaste}
136
+ onFocus={() => setFocused(i)}
137
+ onBlur={() => setFocused(null)}
138
+ style={{
139
+ width: "44px",
140
+ height: "52px",
141
+ textAlign: "center",
142
+ fontSize: "20px",
143
+ fontWeight: 600,
144
+ border: `1.5px solid ${
145
+ error
146
+ ? "#dc2626"
147
+ : focused === i
148
+ ? "#B45309"
149
+ : val
150
+ ? "#B45309"
151
+ : "#d1d5db"
152
+ }`,
153
+ borderRadius: "8px",
154
+ outline: "none",
155
+ background: disabled ? "#f9fafb" : "#ffffff",
156
+ color: "#111827",
157
+ boxShadow:
158
+ focused === i ? "0 0 0 3px rgba(180,83,9,0.12)" : "none",
159
+ transition: "all 0.15s ease",
160
+ }}
161
+ />
162
+ ))}
163
+ </div>
164
+
165
+ {error && (
166
+ <span style={{ fontSize: "12px", color: "#dc2626" }}>{error}</span>
167
+ )}
168
+
169
+ {onResend && (
170
+ <div style={{ fontSize: "13px", color: "#6b7280", marginTop: "4px" }}>
171
+ {seconds > 0 ? (
172
+ <>
173
+ Didn't get it?{" "}
174
+ <span style={{ color: "#B45309" }}>
175
+ Resend in 0:{String(seconds).padStart(2, "0")}
176
+ </span>
177
+ </>
178
+ ) : (
179
+ <>
180
+ Didn't get it?{" "}
181
+ <button
182
+ onClick={handleResend}
183
+ style={{
184
+ background: "none",
185
+ border: "none",
186
+ color: "#B45309",
187
+ cursor: "pointer",
188
+ fontSize: "13px",
189
+ fontWeight: 500,
190
+ padding: 0,
191
+ textDecoration: "underline",
192
+ }}
193
+ >
194
+ Resend OTP
195
+ </button>
196
+ </>
197
+ )}
198
+ </div>
199
+ )}
200
+ </div>
201
+ );
202
+ }
@@ -0,0 +1,128 @@
1
+ "use client";
2
+
3
+ import { validatePAN } from "@bharat-ui/validators";
4
+ import * as React from "react";
5
+
6
+ export interface PANInputProps {
7
+ value?: string;
8
+ onChange?: (value: string, valid: boolean) => void;
9
+ label?: string;
10
+ error?: string;
11
+ disabled?: boolean;
12
+ showTypeBadge?: boolean;
13
+ }
14
+
15
+ const TYPE_COLORS: Record<string, { bg: string; text: string }> = {
16
+ P: { bg: "#f0fdf4", text: "#15803d" },
17
+ C: { bg: "#eff6ff", text: "#1d4ed8" },
18
+ H: { bg: "#fef3c7", text: "#92400e" },
19
+ F: { bg: "#fdf4ff", text: "#7e22ce" },
20
+ T: { bg: "#fff1f2", text: "#be123c" },
21
+ };
22
+
23
+ export function PANInput({
24
+ value = "",
25
+ onChange,
26
+ label,
27
+ error,
28
+ disabled = false,
29
+ showTypeBadge = true,
30
+ }: PANInputProps) {
31
+ const [focused, setFocused] = React.useState(false);
32
+
33
+ const result = value.length > 0 ? validatePAN(value) : null;
34
+ const typeCode = result?.meta?.typeCode ?? null;
35
+ const badgeColor = typeCode
36
+ ? (TYPE_COLORS[typeCode] ?? { bg: "#f3f4f6", text: "#374151" })
37
+ : null;
38
+
39
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
40
+ const cleaned = e.target.value
41
+ .toUpperCase()
42
+ .replace(/[^A-Z0-9]/g, "")
43
+ .slice(0, 10);
44
+ onChange?.(cleaned, validatePAN(cleaned).valid);
45
+ };
46
+
47
+ return (
48
+ <div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
49
+ {label && (
50
+ <label
51
+ style={{
52
+ fontSize: "13px",
53
+ fontWeight: 500,
54
+ color: error ? "#dc2626" : "#6b7280",
55
+ }}
56
+ >
57
+ {label}
58
+ </label>
59
+ )}
60
+
61
+ <div
62
+ style={{
63
+ display: "flex",
64
+ alignItems: "center",
65
+ border: `1.5px solid ${
66
+ error ? "#dc2626" : focused ? "#B45309" : "#d1d5db"
67
+ }`,
68
+ borderRadius: "8px",
69
+ padding: "0 12px",
70
+ gap: "8px",
71
+ background: disabled ? "#f9fafb" : "#ffffff",
72
+ boxShadow: focused ? "0 0 0 3px rgba(180,83,9,0.12)" : "none",
73
+ transition: "all 0.15s ease",
74
+ }}
75
+ >
76
+ <input
77
+ type="text"
78
+ value={value}
79
+ onChange={handleChange}
80
+ onFocus={() => setFocused(true)}
81
+ onBlur={() => setFocused(false)}
82
+ disabled={disabled}
83
+ maxLength={10}
84
+ placeholder="ABCDE1234F"
85
+ style={{
86
+ border: "none",
87
+ outline: "none",
88
+ fontSize: "15px",
89
+ fontFamily: "var(--font-mono, monospace)",
90
+ letterSpacing: "0.12em",
91
+ padding: "10px 0",
92
+ width: "100%",
93
+ background: "transparent",
94
+ color: "#111827",
95
+ textTransform: "uppercase",
96
+ }}
97
+ />
98
+
99
+ {showTypeBadge && result?.valid && badgeColor && (
100
+ <span
101
+ style={{
102
+ fontSize: "11px",
103
+ fontWeight: 500,
104
+ padding: "2px 8px",
105
+ borderRadius: "99px",
106
+ background: badgeColor.bg,
107
+ color: badgeColor.text,
108
+ whiteSpace: "nowrap",
109
+ flexShrink: 0,
110
+ }}
111
+ >
112
+ {result.meta?.type}
113
+ </span>
114
+ )}
115
+ </div>
116
+
117
+ {!error && result && !result.valid && value.length === 10 && (
118
+ <span style={{ fontSize: "12px", color: "#dc2626" }}>
119
+ {result.error}
120
+ </span>
121
+ )}
122
+
123
+ {error && (
124
+ <span style={{ fontSize: "12px", color: "#dc2626" }}>{error}</span>
125
+ )}
126
+ </div>
127
+ );
128
+ }
@@ -0,0 +1,153 @@
1
+ "use client";
2
+
3
+ import { validatePincode } from "@bharat-ui/validators";
4
+ import * as React from "react";
5
+
6
+ export interface PincodeInputProps {
7
+ value?: string;
8
+ onChange?: (value: string, valid: boolean) => void;
9
+ label?: string;
10
+ error?: string;
11
+ disabled?: boolean;
12
+ showCascade?: boolean;
13
+ }
14
+
15
+ export function PincodeInput({
16
+ value = "",
17
+ onChange,
18
+ label,
19
+ error,
20
+ disabled = false,
21
+ showCascade = true,
22
+ }: PincodeInputProps) {
23
+ const [focused, setFocused] = React.useState(false);
24
+
25
+ const result = value.length === 6 ? validatePincode(value) : null;
26
+
27
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
28
+ const cleaned = e.target.value.replace(/\D/g, "").slice(0, 6);
29
+ onChange?.(
30
+ cleaned,
31
+ cleaned.length === 6 ? validatePincode(cleaned).valid : false,
32
+ );
33
+ };
34
+
35
+ return (
36
+ <div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
37
+ {label && (
38
+ <label
39
+ style={{
40
+ fontSize: "13px",
41
+ fontWeight: 500,
42
+ color: error ? "#dc2626" : "#6b7280",
43
+ }}
44
+ >
45
+ {label}
46
+ </label>
47
+ )}
48
+
49
+ <input
50
+ type="text"
51
+ inputMode="numeric"
52
+ value={value}
53
+ onChange={handleChange}
54
+ onFocus={() => setFocused(true)}
55
+ onBlur={() => setFocused(false)}
56
+ disabled={disabled}
57
+ maxLength={6}
58
+ placeholder="390001"
59
+ style={{
60
+ border: `1.5px solid ${
61
+ error ? "#dc2626" : focused ? "#B45309" : "#d1d5db"
62
+ }`,
63
+ borderRadius: "8px",
64
+ padding: "10px 12px",
65
+ fontSize: "15px",
66
+ fontFamily: "var(--font-mono, monospace)",
67
+ letterSpacing: "0.08em",
68
+ outline: "none",
69
+ background: disabled ? "#f9fafb" : "#ffffff",
70
+ color: "#111827",
71
+ width: "100%",
72
+ boxSizing: "border-box",
73
+ boxShadow: focused ? "0 0 0 3px rgba(180,83,9,0.12)" : "none",
74
+ transition: "all 0.15s ease",
75
+ }}
76
+ />
77
+
78
+ {showCascade && result?.valid && result.meta && (
79
+ <div
80
+ style={{
81
+ display: "flex",
82
+ flexDirection: "column",
83
+ gap: "6px",
84
+ padding: "10px 12px",
85
+ background: "#f9fafb",
86
+ borderRadius: "8px",
87
+ border: "0.5px solid #e5e7eb",
88
+ marginTop: "2px",
89
+ }}
90
+ >
91
+ <div
92
+ style={{
93
+ display: "flex",
94
+ justifyContent: "space-between",
95
+ fontSize: "13px",
96
+ }}
97
+ >
98
+ <span style={{ color: "#6b7280" }}>District</span>
99
+ <span style={{ fontWeight: 500, color: "#111827" }}>
100
+ {result.meta.district}
101
+ </span>
102
+ </div>
103
+ <div
104
+ style={{
105
+ display: "flex",
106
+ justifyContent: "space-between",
107
+ fontSize: "13px",
108
+ }}
109
+ >
110
+ <span style={{ color: "#6b7280" }}>State</span>
111
+ <span style={{ fontWeight: 500, color: "#111827" }}>
112
+ {result.meta.state}
113
+ </span>
114
+ </div>
115
+ <div
116
+ style={{
117
+ display: "flex",
118
+ justifyContent: "space-between",
119
+ fontSize: "13px",
120
+ }}
121
+ >
122
+ <span style={{ color: "#6b7280" }}>Zone</span>
123
+ <span style={{ fontWeight: 500, color: "#111827" }}>
124
+ {result.meta.zone}
125
+ </span>
126
+ </div>
127
+ <div
128
+ style={{
129
+ display: "flex",
130
+ justifyContent: "space-between",
131
+ fontSize: "13px",
132
+ }}
133
+ >
134
+ <span style={{ color: "#6b7280" }}>Head PO</span>
135
+ <span style={{ fontWeight: 500, color: "#111827" }}>
136
+ {result.meta.headPO}
137
+ </span>
138
+ </div>
139
+ </div>
140
+ )}
141
+
142
+ {showCascade && result && !result.valid && value.length === 6 && (
143
+ <span style={{ fontSize: "12px", color: "#dc2626" }}>
144
+ {result.error}
145
+ </span>
146
+ )}
147
+
148
+ {error && (
149
+ <span style={{ fontSize: "12px", color: "#dc2626" }}>{error}</span>
150
+ )}
151
+ </div>
152
+ );
153
+ }
@@ -0,0 +1,233 @@
1
+ "use client";
2
+
3
+ import { formatINR } from "@bharat-ui/validators";
4
+ import * as React from "react";
5
+
6
+ export interface UPIApp {
7
+ name: string;
8
+ packageName: string;
9
+ deepLinkScheme: string;
10
+ }
11
+
12
+ const UPI_APPS: UPIApp[] = [
13
+ {
14
+ name: "GPay",
15
+ packageName: "com.google.android.apps.nbu.paisa.user",
16
+ deepLinkScheme: "gpay",
17
+ },
18
+ {
19
+ name: "PhonePe",
20
+ packageName: "com.phonepe.app",
21
+ deepLinkScheme: "phonepe",
22
+ },
23
+ { name: "Paytm", packageName: "net.one97.paytm", deepLinkScheme: "paytmmp" },
24
+ { name: "BHIM", packageName: "in.org.npci.upiapp", deepLinkScheme: "upi" },
25
+ ];
26
+
27
+ export interface UPIButtonProps {
28
+ vpa: string;
29
+ amount: number;
30
+ merchantName: string;
31
+ transactionNote?: string;
32
+ currency?: string;
33
+ onSuccess?: () => void;
34
+ onError?: (error: string) => void;
35
+ }
36
+
37
+ function buildUPIUrl(params: {
38
+ vpa: string;
39
+ amount: number;
40
+ merchantName: string;
41
+ transactionNote: string;
42
+ currency: string;
43
+ }): string {
44
+ const query = new URLSearchParams({
45
+ pa: params.vpa,
46
+ pn: params.merchantName,
47
+ am: params.amount.toFixed(2),
48
+ cu: params.currency,
49
+ tn: params.transactionNote,
50
+ });
51
+ return `upi://pay?${query.toString()}`;
52
+ }
53
+
54
+ export function UPIButton({
55
+ vpa,
56
+ amount,
57
+ merchantName,
58
+ transactionNote = "Payment",
59
+ currency = "INR",
60
+ onSuccess,
61
+ onError,
62
+ }: UPIButtonProps) {
63
+ const [expanded, setExpanded] = React.useState(false);
64
+ const [isDesktop, setIsDesktop] = React.useState(false);
65
+
66
+ React.useEffect(() => {
67
+ setIsDesktop(window.innerWidth > 768);
68
+ }, []);
69
+
70
+ const upiUrl = buildUPIUrl({
71
+ vpa,
72
+ amount,
73
+ merchantName,
74
+ transactionNote,
75
+ currency,
76
+ });
77
+
78
+ const handlePayClick = () => {
79
+ if (isDesktop) {
80
+ setExpanded((e) => !e);
81
+ return;
82
+ }
83
+ window.location.href = upiUrl;
84
+ };
85
+
86
+ const handleAppClick = (app: UPIApp) => {
87
+ const appUrl = upiUrl.replace("upi://", `${app.deepLinkScheme}://`);
88
+ try {
89
+ window.location.href = appUrl;
90
+ onSuccess?.();
91
+ } catch {
92
+ onError?.(`Could not open ${app.name}`);
93
+ }
94
+ };
95
+
96
+ return (
97
+ <div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
98
+ <button
99
+ onClick={handlePayClick}
100
+ style={{
101
+ display: "flex",
102
+ alignItems: "center",
103
+ justifyContent: "center",
104
+ gap: "8px",
105
+ padding: "12px 20px",
106
+ borderRadius: "8px",
107
+ border: "1.5px solid #B45309",
108
+ background: "transparent",
109
+ color: "#B45309",
110
+ fontSize: "15px",
111
+ fontWeight: 500,
112
+ cursor: "pointer",
113
+ width: "100%",
114
+ transition: "all 0.15s ease",
115
+ }}
116
+ onMouseEnter={(e) => {
117
+ (e.target as HTMLButtonElement).style.background =
118
+ "rgba(180,83,9,0.06)";
119
+ }}
120
+ onMouseLeave={(e) => {
121
+ (e.target as HTMLButtonElement).style.background = "transparent";
122
+ }}
123
+ >
124
+ <svg
125
+ width="18"
126
+ height="18"
127
+ viewBox="0 0 24 24"
128
+ fill="none"
129
+ stroke="currentColor"
130
+ strokeWidth="2"
131
+ strokeLinecap="round"
132
+ strokeLinejoin="round"
133
+ >
134
+ <rect x="2" y="5" width="20" height="14" rx="2" />
135
+ <line x1="2" y1="10" x2="22" y2="10" />
136
+ </svg>
137
+ Pay {formatINR(amount)} via UPI
138
+ </button>
139
+
140
+ {expanded && isDesktop && (
141
+ <div
142
+ style={{
143
+ display: "flex",
144
+ flexDirection: "column",
145
+ gap: "4px",
146
+ border: "0.5px solid #e5e7eb",
147
+ borderRadius: "8px",
148
+ overflow: "hidden",
149
+ }}
150
+ >
151
+ <div
152
+ style={{
153
+ padding: "8px 12px",
154
+ fontSize: "11px",
155
+ fontWeight: 500,
156
+ color: "#6b7280",
157
+ background: "#f9fafb",
158
+ letterSpacing: "0.06em",
159
+ textTransform: "uppercase",
160
+ }}
161
+ >
162
+ Choose UPI app
163
+ </div>
164
+ {UPI_APPS.map((app) => (
165
+ <button
166
+ key={app.name}
167
+ onClick={() => handleAppClick(app)}
168
+ style={{
169
+ display: "flex",
170
+ alignItems: "center",
171
+ justifyContent: "space-between",
172
+ padding: "12px 16px",
173
+ border: "none",
174
+ borderTop: "0.5px solid #f3f4f6",
175
+ background: "#ffffff",
176
+ cursor: "pointer",
177
+ fontSize: "14px",
178
+ color: "#111827",
179
+ textAlign: "left",
180
+ transition: "background 0.1s ease",
181
+ }}
182
+ onMouseEnter={(e) => {
183
+ (e.currentTarget as HTMLButtonElement).style.background =
184
+ "#fef3c7";
185
+ }}
186
+ onMouseLeave={(e) => {
187
+ (e.currentTarget as HTMLButtonElement).style.background =
188
+ "#ffffff";
189
+ }}
190
+ >
191
+ <span>{app.name}</span>
192
+ <svg
193
+ width="14"
194
+ height="14"
195
+ viewBox="0 0 24 24"
196
+ fill="none"
197
+ stroke="#9ca3af"
198
+ strokeWidth="2"
199
+ strokeLinecap="round"
200
+ strokeLinejoin="round"
201
+ >
202
+ <polyline points="9 18 15 12 9 6" />
203
+ </svg>
204
+ </button>
205
+ ))}
206
+ </div>
207
+ )}
208
+
209
+ <div
210
+ style={{
211
+ display: "flex",
212
+ gap: "6px",
213
+ flexWrap: "wrap",
214
+ }}
215
+ >
216
+ {UPI_APPS.map((app) => (
217
+ <span
218
+ key={app.name}
219
+ style={{
220
+ fontSize: "11px",
221
+ padding: "3px 8px",
222
+ borderRadius: "99px",
223
+ border: "0.5px solid #e5e7eb",
224
+ color: "#6b7280",
225
+ }}
226
+ >
227
+ {app.name}
228
+ </span>
229
+ ))}
230
+ </div>
231
+ </div>
232
+ );
233
+ }
package/src/button.tsx ADDED
@@ -0,0 +1,20 @@
1
+ "use client";
2
+
3
+ import { ReactNode } from "react";
4
+
5
+ interface ButtonProps {
6
+ children: ReactNode;
7
+ className?: string;
8
+ appName: string;
9
+ }
10
+
11
+ export const Button = ({ children, className, appName }: ButtonProps) => {
12
+ return (
13
+ <button
14
+ className={className}
15
+ onClick={() => alert(`Hello from your ${appName} app!`)}
16
+ >
17
+ {children}
18
+ </button>
19
+ );
20
+ };
package/src/card.tsx ADDED
@@ -0,0 +1,27 @@
1
+ import { type JSX } from "react";
2
+
3
+ export function Card({
4
+ className,
5
+ title,
6
+ children,
7
+ href,
8
+ }: {
9
+ className?: string;
10
+ title: string;
11
+ children: React.ReactNode;
12
+ href: string;
13
+ }): JSX.Element {
14
+ return (
15
+ <a
16
+ className={className}
17
+ href={`${href}?utm_source=create-turbo&utm_medium=basic&utm_campaign=create-turbo"`}
18
+ rel="noopener noreferrer"
19
+ target="_blank"
20
+ >
21
+ <h2>
22
+ {title} <span>-&gt;</span>
23
+ </h2>
24
+ <p>{children}</p>
25
+ </a>
26
+ );
27
+ }
package/src/code.tsx ADDED
@@ -0,0 +1,11 @@
1
+ import { type JSX } from "react";
2
+
3
+ export function Code({
4
+ children,
5
+ className,
6
+ }: {
7
+ children: React.ReactNode;
8
+ className?: string;
9
+ }): JSX.Element {
10
+ return <code className={className}>{children}</code>;
11
+ }