@authdog/react-elements 0.0.29 → 0.0.31

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/dist/styles.css CHANGED
@@ -630,6 +630,10 @@ video {
630
630
  margin-left: 0.5rem;
631
631
  margin-right: 0.5rem;
632
632
  }
633
+ .mx-auto {
634
+ margin-left: auto;
635
+ margin-right: auto;
636
+ }
633
637
  .my-1 {
634
638
  margin-top: 0.25rem;
635
639
  margin-bottom: 0.25rem;
@@ -637,12 +641,12 @@ video {
637
641
  .mb-2 {
638
642
  margin-bottom: 0.5rem;
639
643
  }
644
+ .mb-3 {
645
+ margin-bottom: 0.75rem;
646
+ }
640
647
  .mb-4 {
641
648
  margin-bottom: 1rem;
642
649
  }
643
- .mb-6 {
644
- margin-bottom: 1.5rem;
645
- }
646
650
  .ml-1 {
647
651
  margin-left: 0.25rem;
648
652
  }
@@ -658,6 +662,9 @@ video {
658
662
  .mr-4 {
659
663
  margin-right: 1rem;
660
664
  }
665
+ .mt-1 {
666
+ margin-top: 0.25rem;
667
+ }
661
668
  .mt-auto {
662
669
  margin-top: auto;
663
670
  }
@@ -711,6 +718,9 @@ video {
711
718
  .h-12 {
712
719
  height: 3rem;
713
720
  }
721
+ .h-14 {
722
+ height: 3.5rem;
723
+ }
714
724
  .h-16 {
715
725
  height: 4rem;
716
726
  }
@@ -720,6 +730,9 @@ video {
720
730
  .h-4 {
721
731
  height: 1rem;
722
732
  }
733
+ .h-6 {
734
+ height: 1.5rem;
735
+ }
723
736
  .h-8 {
724
737
  height: 2rem;
725
738
  }
@@ -735,9 +748,6 @@ video {
735
748
  .h-px {
736
749
  height: 1px;
737
750
  }
738
- .h-screen {
739
- height: 100vh;
740
- }
741
751
  .min-h-4 {
742
752
  min-height: 1rem;
743
753
  }
@@ -753,6 +763,9 @@ video {
753
763
  .w-12 {
754
764
  width: 3rem;
755
765
  }
766
+ .w-16 {
767
+ width: 4rem;
768
+ }
756
769
  .w-3\/4 {
757
770
  width: 75%;
758
771
  }
@@ -762,6 +775,9 @@ video {
762
775
  .w-56 {
763
776
  width: 14rem;
764
777
  }
778
+ .w-6 {
779
+ width: 1.5rem;
780
+ }
765
781
  .w-8 {
766
782
  width: 2rem;
767
783
  }
@@ -819,8 +835,8 @@ video {
819
835
  .grid-cols-\[0_1fr\] {
820
836
  grid-template-columns: 0 1fr;
821
837
  }
822
- .grid-cols-\[16rem\2c 1fr\] {
823
- grid-template-columns: 16rem 1fr;
838
+ .grid-cols-\[14rem\2c 1fr\] {
839
+ grid-template-columns: 14rem 1fr;
824
840
  }
825
841
  .grid-rows-\[auto_auto\] {
826
842
  grid-template-rows: auto auto;
@@ -871,6 +887,11 @@ video {
871
887
  margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
872
888
  margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
873
889
  }
890
+ .space-y-2\.5 > :not([hidden]) ~ :not([hidden]) {
891
+ --tw-space-y-reverse: 0;
892
+ margin-top: calc(0.625rem * calc(1 - var(--tw-space-y-reverse)));
893
+ margin-bottom: calc(0.625rem * var(--tw-space-y-reverse));
894
+ }
874
895
  .space-y-3 > :not([hidden]) ~ :not([hidden]) {
875
896
  --tw-space-y-reverse: 0;
876
897
  margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse)));
@@ -881,10 +902,15 @@ video {
881
902
  margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
882
903
  margin-bottom: calc(1rem * var(--tw-space-y-reverse));
883
904
  }
884
- .space-y-8 > :not([hidden]) ~ :not([hidden]) {
905
+ .space-y-5 > :not([hidden]) ~ :not([hidden]) {
906
+ --tw-space-y-reverse: 0;
907
+ margin-top: calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));
908
+ margin-bottom: calc(1.25rem * var(--tw-space-y-reverse));
909
+ }
910
+ .space-y-6 > :not([hidden]) ~ :not([hidden]) {
885
911
  --tw-space-y-reverse: 0;
886
- margin-top: calc(2rem * calc(1 - var(--tw-space-y-reverse)));
887
- margin-bottom: calc(2rem * var(--tw-space-y-reverse));
912
+ margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));
913
+ margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
888
914
  }
889
915
  .self-start {
890
916
  align-self: flex-start;
@@ -925,6 +951,9 @@ video {
925
951
  .border {
926
952
  border-width: 1px;
927
953
  }
954
+ .border-2 {
955
+ border-width: 2px;
956
+ }
928
957
  .border-b {
929
958
  border-bottom-width: 1px;
930
959
  }
@@ -937,6 +966,13 @@ video {
937
966
  .border-t {
938
967
  border-top-width: 1px;
939
968
  }
969
+ .border-none {
970
+ border-style: none;
971
+ }
972
+ .border-green-200 {
973
+ --tw-border-opacity: 1;
974
+ border-color: rgb(187 247 208 / var(--tw-border-opacity, 1));
975
+ }
940
976
  .border-input {
941
977
  border-color: hsl(var(--input));
942
978
  }
@@ -949,6 +985,10 @@ video {
949
985
  .bg-black\/50 {
950
986
  background-color: rgb(0 0 0 / 0.5);
951
987
  }
988
+ .bg-blue-100 {
989
+ --tw-bg-opacity: 1;
990
+ background-color: rgb(219 234 254 / var(--tw-bg-opacity, 1));
991
+ }
952
992
  .bg-border {
953
993
  background-color: hsl(var(--border));
954
994
  }
@@ -966,6 +1006,14 @@ video {
966
1006
  --tw-bg-opacity: 1;
967
1007
  background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1));
968
1008
  }
1009
+ .bg-green-100 {
1010
+ --tw-bg-opacity: 1;
1011
+ background-color: rgb(220 252 231 / var(--tw-bg-opacity, 1));
1012
+ }
1013
+ .bg-green-50 {
1014
+ --tw-bg-opacity: 1;
1015
+ background-color: rgb(240 253 244 / var(--tw-bg-opacity, 1));
1016
+ }
969
1017
  .bg-muted {
970
1018
  background-color: hsl(var(--muted));
971
1019
  }
@@ -999,18 +1047,18 @@ video {
999
1047
  .fill-current {
1000
1048
  fill: currentColor;
1001
1049
  }
1050
+ .p-0 {
1051
+ padding: 0px;
1052
+ }
1002
1053
  .p-1 {
1003
1054
  padding: 0.25rem;
1004
1055
  }
1005
- .p-10 {
1006
- padding: 2.5rem;
1056
+ .p-3 {
1057
+ padding: 0.75rem;
1007
1058
  }
1008
1059
  .p-4 {
1009
1060
  padding: 1rem;
1010
1061
  }
1011
- .p-6 {
1012
- padding: 1.5rem;
1013
- }
1014
1062
  .px-2 {
1015
1063
  padding-left: 0.5rem;
1016
1064
  padding-right: 0.5rem;
@@ -1068,6 +1116,9 @@ video {
1068
1116
  .pr-2 {
1069
1117
  padding-right: 0.5rem;
1070
1118
  }
1119
+ .pt-6 {
1120
+ padding-top: 1.5rem;
1121
+ }
1071
1122
  .text-center {
1072
1123
  text-align: center;
1073
1124
  }
@@ -1079,6 +1130,10 @@ video {
1079
1130
  font-size: 1rem;
1080
1131
  line-height: 1.5rem;
1081
1132
  }
1133
+ .text-lg {
1134
+ font-size: 1.125rem;
1135
+ line-height: 1.75rem;
1136
+ }
1082
1137
  .text-sm {
1083
1138
  font-size: 0.875rem;
1084
1139
  line-height: 1.25rem;
@@ -1112,6 +1167,10 @@ video {
1112
1167
  .tracking-widest {
1113
1168
  letter-spacing: 0.1em;
1114
1169
  }
1170
+ .text-blue-600 {
1171
+ --tw-text-opacity: 1;
1172
+ color: rgb(37 99 235 / var(--tw-text-opacity, 1));
1173
+ }
1115
1174
  .text-card-foreground {
1116
1175
  color: hsl(var(--card-foreground));
1117
1176
  }
@@ -1136,6 +1195,18 @@ video {
1136
1195
  --tw-text-opacity: 1;
1137
1196
  color: rgb(17 24 39 / var(--tw-text-opacity, 1));
1138
1197
  }
1198
+ .text-green-600 {
1199
+ --tw-text-opacity: 1;
1200
+ color: rgb(22 163 74 / var(--tw-text-opacity, 1));
1201
+ }
1202
+ .text-green-700 {
1203
+ --tw-text-opacity: 1;
1204
+ color: rgb(21 128 61 / var(--tw-text-opacity, 1));
1205
+ }
1206
+ .text-green-900 {
1207
+ --tw-text-opacity: 1;
1208
+ color: rgb(20 83 45 / var(--tw-text-opacity, 1));
1209
+ }
1139
1210
  .text-muted-foreground {
1140
1211
  color: hsl(var(--muted-foreground));
1141
1212
  }
@@ -1283,6 +1354,10 @@ video {
1283
1354
  .placeholder\:text-muted-foreground::placeholder {
1284
1355
  color: hsl(var(--muted-foreground));
1285
1356
  }
1357
+ .focus-within\:border-blue-500:focus-within {
1358
+ --tw-border-opacity: 1;
1359
+ border-color: rgb(59 130 246 / var(--tw-border-opacity, 1));
1360
+ }
1286
1361
  .hover\:bg-accent:hover {
1287
1362
  background-color: hsl(var(--accent));
1288
1363
  }
@@ -1538,6 +1613,10 @@ video {
1538
1613
  }
1539
1614
  @media (min-width: 768px) {
1540
1615
 
1616
+ .md\:mb-4 {
1617
+ margin-bottom: 1rem;
1618
+ }
1619
+
1541
1620
  .md\:flex {
1542
1621
  display: flex;
1543
1622
  }
@@ -1546,6 +1625,20 @@ video {
1546
1625
  display: none;
1547
1626
  }
1548
1627
 
1628
+ .md\:space-y-6 > :not([hidden]) ~ :not([hidden]) {
1629
+ --tw-space-y-reverse: 0;
1630
+ margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));
1631
+ margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
1632
+ }
1633
+
1634
+ .md\:p-4 {
1635
+ padding: 1rem;
1636
+ }
1637
+
1638
+ .md\:p-5 {
1639
+ padding: 1.25rem;
1640
+ }
1641
+
1549
1642
  .md\:px-6 {
1550
1643
  padding-left: 1.5rem;
1551
1644
  padding-right: 1.5rem;
package/package.json CHANGED
@@ -1,17 +1,18 @@
1
1
  {
2
2
  "name": "@authdog/react-elements",
3
- "version": "0.0.29",
3
+ "version": "0.0.31",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.mts",
7
7
  "peerDependencies": {
8
- "react": "^18.3.1",
9
- "react-dom": "^18.3.1"
8
+ "react": "^19.1.0",
9
+ "react-dom": "^19.1.0"
10
10
  },
11
11
  "devDependencies": {
12
12
  "@ladle/react": "^2.0.0",
13
13
  "@types/node": "^20",
14
- "@types/react": "^18.3.11",
14
+ "@types/react": "^19.1.0",
15
+ "@types/react-dom": "^19.1.0",
15
16
  "@vitejs/plugin-react": "^4.4.1",
16
17
  "autoprefixer": "^10",
17
18
  "css-loader": "^6.8.1",
@@ -33,7 +34,7 @@
33
34
  "@radix-ui/react-dropdown-menu": "^2.1.14",
34
35
  "@radix-ui/react-label": "^2.1.6",
35
36
  "@radix-ui/react-separator": "^1.1.6",
36
- "@radix-ui/react-slot": "^1.2.2",
37
+ "@radix-ui/react-slot": "^1.2.3",
37
38
  "class-variance-authority": "^0.7.0",
38
39
  "clsx": "^2.1.1",
39
40
  "lucide-react": "^0.451.0",
@@ -50,9 +50,9 @@ export const UserProfile = ({
50
50
  }
51
51
 
52
52
  return (
53
- <div className="grid grid-cols-[16rem,1fr] h-screen bg-gray-100">
54
- <div className="h-full border-r p-6 bg-white flex flex-col min-w-0">
55
- <div className="mb-6">
53
+ <div className="grid grid-cols-[14rem,1fr] w-full bg-transparent">
54
+ <div className="h-full border-r p-3 md:p-4 bg-transparent flex flex-col min-w-0">
55
+ <div className="mb-3 md:mb-4">
56
56
  <h1 className="text-xl font-bold">Account</h1>
57
57
  <p className="text-sm text-gray-500">Manage your account info.</p>
58
58
  </div>
@@ -79,8 +79,8 @@ export const UserProfile = ({
79
79
  </nav>
80
80
  </div>
81
81
 
82
- <div className="h-full p-10 overflow-y-auto min-w-0 bg-white">
83
- <div className="flex justify-between items-center mb-6">
82
+ <div className="h-full p-3 md:p-5 min-w-0 bg-transparent">
83
+ <div className="flex justify-between items-center mb-3 md:mb-4">
84
84
  <h2 className="text-xl font-semibold">
85
85
  {activeTab === "profile" ? "Profile details" : "Security settings"}
86
86
  </h2>
@@ -90,10 +90,10 @@ export const UserProfile = ({
90
90
  </div>
91
91
 
92
92
  {activeTab === "profile" ? (
93
- <div className="space-y-8">
93
+ <div className="space-y-5 md:space-y-6">
94
94
  {/* Profile Section */}
95
95
  <div>
96
- <h3 className="text-sm font-medium mb-4">Profile</h3>
96
+ <h3 className="text-sm font-medium mb-3">Profile</h3>
97
97
  <div className="flex items-center justify-between">
98
98
  <div className="flex items-center">
99
99
  <Avatar className="h-12 w-12 mr-4 border">
@@ -110,8 +110,8 @@ export const UserProfile = ({
110
110
 
111
111
  {/* Email Addresses Section */}
112
112
  <div>
113
- <h3 className="text-sm font-medium mb-4">Email addresses</h3>
114
- <div className="space-y-3">
113
+ <h3 className="text-sm font-medium mb-3">Email addresses</h3>
114
+ <div className="space-y-2.5">
115
115
 
116
116
  {/* {JSON.stringify(user)} */}
117
117
 
@@ -126,18 +126,16 @@ export const UserProfile = ({
126
126
  </div>
127
127
  ))} */}
128
128
 
129
- {
130
- user.emails.map((email: any, idx: number) => (
131
- <div className="flex items-center justify-between" key={email.value}>
132
- <span>{email.value}</span>
133
- {idx === 0 && (
134
- <Badge variant="outline" className="text-xs bg-gray-100 text-gray-700 hover:bg-gray-100">
135
- Primary
136
- </Badge>
137
- )}
138
- </div>
139
- ))
140
- }
129
+ {user.emails.map((email: any, idx: number) => (
130
+ <div className="flex items-center justify-between" key={email.value}>
131
+ <span>{email.value}</span>
132
+ {idx === 0 && (
133
+ <Badge variant="outline" className="text-xs bg-gray-100 text-gray-700 hover:bg-gray-100">
134
+ Primary
135
+ </Badge>
136
+ )}
137
+ </div>
138
+ ))}
141
139
  {/* <Button variant="ghost" size="sm" className="flex items-center text-gray-700">
142
140
  {renderIcon(PlusCircle)}
143
141
  Add email address
@@ -147,8 +145,8 @@ export const UserProfile = ({
147
145
 
148
146
  {/* Phone Number Section */}
149
147
  {/* <div>
150
- <h3 className="text-sm font-medium mb-4">Phone number</h3>
151
- <div className="space-y-3">
148
+ <h3 className="text-sm font-medium mb-3">Phone number</h3>
149
+ <div className="space-y-2.5">
152
150
  <div className="flex items-center justify-between">
153
151
  <span>+1 (555) 123-4567</span>
154
152
  <Badge variant="outline" className="text-xs bg-gray-100 text-gray-700 hover:bg-gray-100">
@@ -164,8 +162,8 @@ export const UserProfile = ({
164
162
 
165
163
  {/* Connected Accounts Section */}
166
164
  <div>
167
- <h3 className="text-sm font-medium mb-4">Connected accounts</h3>
168
- <div className="space-y-3">
165
+ <h3 className="text-sm font-medium mb-3">Connected accounts</h3>
166
+ <div className="space-y-2.5">
169
167
  <div className="flex items-center justify-between" key={user.provider}>
170
168
  <div className="flex items-center">
171
169
  <div className="mr-2">
@@ -178,11 +176,11 @@ export const UserProfile = ({
178
176
  </div>
179
177
  </div>
180
178
  ) : (
181
- <div className="space-y-8">
179
+ <div className="space-y-5 md:space-y-6">
182
180
  {/* Security Settings */}
183
181
  <div key="two-factor">
184
- <h3 className="text-sm font-medium mb-4">Two-factor authentication</h3>
185
- <div className="space-y-3">
182
+ <h3 className="text-sm font-medium mb-3">Two-factor authentication</h3>
183
+ <div className="space-y-2.5">
186
184
  <div className="flex items-center justify-between">
187
185
  <div>
188
186
  <p className="font-medium">Two-factor authentication</p>
@@ -196,8 +194,8 @@ export const UserProfile = ({
196
194
  </div>
197
195
 
198
196
  <div key="password">
199
- <h3 className="text-sm font-medium mb-4">Password</h3>
200
- <div className="space-y-3">
197
+ <h3 className="text-sm font-medium mb-3">Password</h3>
198
+ <div className="space-y-2.5">
201
199
  <div className="flex items-center justify-between">
202
200
  <div>
203
201
  <p className="font-medium">Change password</p>
@@ -211,8 +209,8 @@ export const UserProfile = ({
211
209
  </div>
212
210
 
213
211
  <div key="sessions">
214
- <h3 className="text-sm font-medium mb-4">Active sessions</h3>
215
- <div className="space-y-3">
212
+ <h3 className="text-sm font-medium mb-3">Active sessions</h3>
213
+ <div className="space-y-2.5">
216
214
  <div className="flex items-center justify-between">
217
215
  <div>
218
216
  <p className="font-medium">Current session</p>
@@ -0,0 +1,231 @@
1
+ "use client"
2
+
3
+ import type React from "react"
4
+
5
+ import { useState, useRef } from "react"
6
+ import { Button } from "../../components/ui/button"
7
+ import { Card, CardContent } from "../../components/ui/card"
8
+ import { Alert, AlertDescription } from "../../components/ui/alert"
9
+ import { Shield, CheckCircle, AlertCircle } from "lucide-react"
10
+
11
+ interface TOTPValidatorProps {
12
+ onValidate: (code: string) => Promise<void>;
13
+ }
14
+
15
+ export const TOTPValidator = (
16
+ {
17
+ onValidate
18
+ }: TOTPValidatorProps
19
+ ) => {
20
+ const [code, setCode] = useState(["", "", "", "", "", ""])
21
+ const [loading, setLoading] = useState(false)
22
+ const [error, setError] = useState("")
23
+ const [success, setSuccess] = useState(false)
24
+ const inputRefs = useRef<(HTMLInputElement | null)[]>([])
25
+
26
+ const handleInputChange = (index: number, value: string) => {
27
+ // Only allow digits
28
+ if (!/^\d*$/.test(value)) return
29
+
30
+ const newCode = [...code]
31
+ newCode[index] = value.slice(-1) // Only take the last character
32
+
33
+ setCode(newCode)
34
+ setError("")
35
+ setSuccess(false)
36
+
37
+ // Auto-focus next input
38
+ if (value && index < 5) {
39
+ inputRefs.current[index + 1]?.focus()
40
+ }
41
+ }
42
+
43
+ const handleKeyDown = (index: number, e: React.KeyboardEvent) => {
44
+ // Handle backspace
45
+ if (e.key === "Backspace" && !code[index] && index > 0) {
46
+ inputRefs.current[index - 1]?.focus()
47
+ }
48
+
49
+ // Handle paste
50
+ if (e.key === "v" && (e.ctrlKey || e.metaKey)) {
51
+ e.preventDefault()
52
+ navigator.clipboard.readText().then((text) => {
53
+ const digits = text.replace(/\D/g, "").slice(0, 6).split("")
54
+ const newCode = [...code]
55
+ digits.forEach((digit, i) => {
56
+ if (i < 6) newCode[i] = digit
57
+ })
58
+ setCode(newCode)
59
+
60
+ // Focus the next empty input or the last one
61
+ const nextIndex = Math.min(digits.length, 5)
62
+ inputRefs.current[nextIndex]?.focus()
63
+ })
64
+ }
65
+ }
66
+
67
+ const validateTOTP = async () => {
68
+ const totpCode = code.join("")
69
+
70
+ if (totpCode.length !== 6) {
71
+ setError("Please enter all 6 digits")
72
+ return
73
+ }
74
+
75
+ setLoading(true)
76
+ setError("")
77
+
78
+ try {
79
+ await onValidate(totpCode)
80
+ setSuccess(true)
81
+ } catch (error) {
82
+ setError("Invalid TOTP code. Please try again.")
83
+ } finally {
84
+ setLoading(false)
85
+ }
86
+
87
+
88
+
89
+ // try {
90
+ // Call your TOTP validation endpoint
91
+ // const response = await fetch("/api/validate-totp", {
92
+ // method: "POST",
93
+ // headers: {
94
+ // "Content-Type": "application/json",
95
+ // },
96
+ // body: JSON.stringify({ code: totpCode }),
97
+ // })
98
+
99
+ // // Check if response is JSON
100
+ // const contentType = response.headers.get("content-type")
101
+ // if (!contentType || !contentType.includes("application/json")) {
102
+ // throw new Error("Server returned non-JSON response")
103
+ // }
104
+
105
+ // const result = await response.json()
106
+
107
+ // if (response.ok && result.valid) {
108
+ // setSuccess(true)
109
+ // setError("")
110
+ // } else {
111
+ // setError(result.message || "Invalid TOTP code. Please try again.")
112
+ // // Clear the code on error
113
+ // setCode(["", "", "", "", "", ""])
114
+ // inputRefs.current[0]?.focus()
115
+ // }
116
+ // } catch (err) {
117
+ // console.error("TOTP validation error:", err)
118
+ // if (err instanceof Error && err.message.includes("non-JSON")) {
119
+ // setError("Server error. Please try again later.")
120
+ // } else {
121
+ // setError("Network error. Please try again.")
122
+ // }
123
+ // // Clear the code on error
124
+ // setCode(["", "", "", "", "", ""])
125
+ // inputRefs.current[0]?.focus()
126
+ // } finally {
127
+ // setLoading(false)
128
+ // }
129
+ }
130
+
131
+ const handleSubmit = (e: React.FormEvent) => {
132
+ e.preventDefault()
133
+ validateTOTP()
134
+ }
135
+
136
+ const clearCode = () => {
137
+ setCode(["", "", "", "", "", ""])
138
+ setError("")
139
+ setSuccess(false)
140
+ setLoading(false)
141
+ inputRefs.current[0]?.focus()
142
+ }
143
+
144
+ if (success) {
145
+ return (
146
+ <div className="max-w-md mx-auto">
147
+ <Card className="border-green-200 bg-green-50">
148
+ <CardContent className="pt-6">
149
+ <div className="text-center space-y-4">
150
+ <div className="mx-auto w-16 h-16 bg-green-100 rounded-full flex items-center justify-center">
151
+ <CheckCircle className="w-8 h-8 text-green-600" />
152
+ </div>
153
+ <div>
154
+ <h3 className="text-lg font-semibold text-green-900">Verification Successful!</h3>
155
+ <p className="text-sm text-green-700 mt-1">Your TOTP code has been validated.</p>
156
+ </div>
157
+ <Button onClick={clearCode} variant="outline" className="bg-white">
158
+ Verify Another Code
159
+ </Button>
160
+ </div>
161
+ </CardContent>
162
+ </Card>
163
+ </div>
164
+ )
165
+ }
166
+
167
+ return (
168
+ <div className="max-w-md mx-auto">
169
+ <Card>
170
+ <CardContent className="pt-6">
171
+ <div className="text-center space-y-6">
172
+ <div>
173
+ <div className="mx-auto w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center mb-4">
174
+ <Shield className="w-6 h-6 text-blue-600" />
175
+ </div>
176
+ <h3 className="text-lg font-semibold">Enter Verification Code</h3>
177
+ <p className="text-sm text-muted-foreground mt-1">Enter the 6-digit code from your authenticator app</p>
178
+ </div>
179
+
180
+ <form onSubmit={handleSubmit} className="space-y-6">
181
+ <div className="flex justify-center gap-2">
182
+ {code.map((digit, index) => (
183
+ <Card key={index} className="w-12 h-14 border-2 focus-within:border-blue-500 transition-colors">
184
+ <CardContent className="p-0 h-full flex items-center justify-center">
185
+ <input
186
+ ref={(el) => {
187
+ inputRefs.current[index] = el
188
+ }}
189
+ type="tel"
190
+ inputMode="numeric"
191
+ maxLength={1}
192
+ value={digit}
193
+ onChange={(e) => handleInputChange(index, e.target.value)}
194
+ onKeyDown={(e) => handleKeyDown(index, e)}
195
+ className="w-full h-full text-center text-2xl font-bold border-none outline-none bg-transparent"
196
+ autoComplete="one-time-code"
197
+ disabled={loading}
198
+ style={{
199
+ height: 'auto'
200
+ }}
201
+ />
202
+ </CardContent>
203
+ </Card>
204
+ ))}
205
+ </div>
206
+
207
+ {error && (
208
+ <Alert variant="destructive">
209
+ <AlertCircle className="h-4 w-4" />
210
+ <AlertDescription>{error}</AlertDescription>
211
+ </Alert>
212
+ )}
213
+
214
+ <div className="space-y-3">
215
+ <Button type="submit" className="w-full" disabled={loading || code.some((digit) => digit === "")}>
216
+ {loading ? "Verifying..." : "Verify Code"}
217
+ </Button>
218
+
219
+ <Button type="button" variant="ghost" size="sm" onClick={clearCode} className="w-full text-xs">
220
+ Clear Code
221
+ </Button>
222
+ </div>
223
+ </form>
224
+
225
+ <p className="text-xs text-muted-foreground">Codes refresh every 30 seconds</p>
226
+ </div>
227
+ </CardContent>
228
+ </Card>
229
+ </div>
230
+ )
231
+ }
package/src/index.ts CHANGED
@@ -2,4 +2,5 @@ export {Button} from "./components/ui/button";
2
2
  export { ClientOnly } from "./components/core/client-only";
3
3
  export {Navbar} from "./components/core/navbar";
4
4
  export {UserProfile} from "./components/core/user-profile";
5
- export {PlaceholderAlert} from "./components/core/placeholder-alert";
5
+ export {PlaceholderAlert} from "./components/core/placeholder-alert";
6
+ export {TOTPValidator} from "./components/flow/totp-validator";