@thorprovider/blocks 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.
Files changed (184) hide show
  1. package/.turbo/turbo-build.log +24 -0
  2. package/.turbo/turbo-test.log +10 -0
  3. package/.turbo/turbo-type-check.log +0 -0
  4. package/AGENTS.md +1 -0
  5. package/README.md +161 -0
  6. package/components.json +33 -0
  7. package/dist/.tsbuildinfo +1 -0
  8. package/dist/contracts.d.mts +563 -0
  9. package/dist/contracts.d.ts +563 -0
  10. package/dist/contracts.js +207 -0
  11. package/dist/contracts.js.map +1 -0
  12. package/dist/contracts.mjs +168 -0
  13. package/dist/contracts.mjs.map +1 -0
  14. package/dist/index.d.mts +2408 -0
  15. package/dist/index.d.ts +2408 -0
  16. package/dist/index.js +16422 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/index.mjs +16352 -0
  19. package/dist/index.mjs.map +1 -0
  20. package/docs/standards/data-contracts.md +70 -0
  21. package/docs/standards/theming.md +85 -0
  22. package/package.json +62 -0
  23. package/src/blocks/application/app-shells/app-shell-1-sidebar.tsx +198 -0
  24. package/src/blocks/application/app-shells/app-shell-1.tsx +196 -0
  25. package/src/blocks/application/login-forms/login-page-1.tsx +246 -0
  26. package/src/blocks/application/login-forms/login-page-2.tsx +207 -0
  27. package/src/blocks/application/password-recovery/forgot-password-page-1-brand-mark.tsx +38 -0
  28. package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +320 -0
  29. package/src/blocks/application/signup-forms/signup-page-1.tsx +231 -0
  30. package/src/blocks/application/verification/verify-1.tsx +307 -0
  31. package/src/blocks/application/verification/verify-confetti.tsx +192 -0
  32. package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +260 -0
  33. package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +179 -0
  34. package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +341 -0
  35. package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +57 -0
  36. package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +124 -0
  37. package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +170 -0
  38. package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +52 -0
  39. package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +57 -0
  40. package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +68 -0
  41. package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +298 -0
  42. package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +47 -0
  43. package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +9 -0
  44. package/src/blocks/e-commerce/checkout-forms/checkbox-check.tsx +74 -0
  45. package/src/blocks/e-commerce/checkout-forms/checkout-confirmation-fulfilment-progress.tsx +189 -0
  46. package/src/blocks/e-commerce/checkout-forms/checkout-confirmation.tsx +283 -0
  47. package/src/blocks/e-commerce/checkout-forms/checkout-field-feedback.tsx +29 -0
  48. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +96 -0
  49. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-country-field.tsx +51 -0
  50. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +139 -0
  51. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +184 -0
  52. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +36 -0
  53. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-shipping-step.tsx +107 -0
  54. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +235 -0
  55. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +130 -0
  56. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +175 -0
  57. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +190 -0
  58. package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +312 -0
  59. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +91 -0
  60. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +110 -0
  61. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +137 -0
  62. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +68 -0
  63. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +79 -0
  64. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +172 -0
  65. package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +392 -0
  66. package/src/blocks/e-commerce/checkout-forms/checkout-form-types.ts +7 -0
  67. package/src/blocks/e-commerce/navigation/category-icon.test.tsx +54 -0
  68. package/src/blocks/e-commerce/navigation/category-icon.tsx +94 -0
  69. package/src/blocks/e-commerce/navigation/mega-menu.test.tsx +139 -0
  70. package/src/blocks/e-commerce/navigation/mega-menu.tsx +271 -0
  71. package/src/blocks/e-commerce/navigation/mobile-category-drawer.test.tsx +152 -0
  72. package/src/blocks/e-commerce/navigation/mobile-category-drawer.tsx +276 -0
  73. package/src/blocks/e-commerce/order-history/order-history-1.tsx +159 -0
  74. package/src/blocks/e-commerce/order-history/order-history-2.tsx +222 -0
  75. package/src/blocks/e-commerce/product-categories/avatar-group.tsx +86 -0
  76. package/src/blocks/e-commerce/product-categories/product-category-1.tsx +126 -0
  77. package/src/blocks/e-commerce/product-categories/product-category-2.tsx +100 -0
  78. package/src/blocks/e-commerce/product-categories/product-category-4.tsx +129 -0
  79. package/src/blocks/e-commerce/product-categories/product-category-5.tsx +218 -0
  80. package/src/blocks/e-commerce/product-list/product-card-1.tsx +131 -0
  81. package/src/blocks/e-commerce/product-list/product-card-2.tsx +71 -0
  82. package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +166 -0
  83. package/src/blocks/e-commerce/product-list/product-card-3.tsx +215 -0
  84. package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +98 -0
  85. package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +171 -0
  86. package/src/blocks/e-commerce/product-list/product-card-5.tsx +367 -0
  87. package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +201 -0
  88. package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +272 -0
  89. package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +59 -0
  90. package/src/blocks/e-commerce/product-overview/product-overview-5-lightbox-tab.tsx +104 -0
  91. package/src/blocks/e-commerce/product-overview/product-overview-5-reviews-tab.tsx +82 -0
  92. package/src/blocks/e-commerce/product-overview/product-overview-5-tabs.tsx +69 -0
  93. package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +274 -0
  94. package/src/blocks/e-commerce/product-overview/product-quick-view-card.tsx +135 -0
  95. package/src/blocks/e-commerce/product-overview/product-quick-view-dialog.tsx +393 -0
  96. package/src/blocks/e-commerce/product-overview/product-quick-view.tsx +136 -0
  97. package/src/blocks/e-commerce/reviews/review-rating-1-review-form.tsx +133 -0
  98. package/src/blocks/e-commerce/reviews/review-rating-1.tsx +178 -0
  99. package/src/blocks/e-commerce/reviews/review-rating-2.tsx +246 -0
  100. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +142 -0
  101. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +291 -0
  102. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +110 -0
  103. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +51 -0
  104. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +146 -0
  105. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +295 -0
  106. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +142 -0
  107. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +342 -0
  108. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +146 -0
  109. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.tsx +391 -0
  110. package/src/blocks/e-commerce/store-containers/store-container-1.tsx +29 -0
  111. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +244 -0
  112. package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +283 -0
  113. package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +318 -0
  114. package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +205 -0
  115. package/src/blocks/marketing/banners/avatar-group.tsx +69 -0
  116. package/src/blocks/marketing/banners/banner-1.tsx +188 -0
  117. package/src/blocks/marketing/cta/cta-section-1.tsx +112 -0
  118. package/src/blocks/marketing/faq/faq-block-1.tsx +45 -0
  119. package/src/blocks/marketing/features/feature-1.tsx +97 -0
  120. package/src/blocks/marketing/features/feature-2.tsx +82 -0
  121. package/src/blocks/marketing/footers/footer-section-1.tsx +141 -0
  122. package/src/blocks/marketing/footers/footer-section-3.tsx +192 -0
  123. package/src/blocks/marketing/footers/footer-section-4.tsx +355 -0
  124. package/src/blocks/marketing/hero-sections/avatar-group.tsx +86 -0
  125. package/src/blocks/marketing/hero-sections/hero-section-1.tsx +135 -0
  126. package/src/blocks/marketing/logo-clouds/logo-cloud-1-logo-icon.tsx +52 -0
  127. package/src/blocks/marketing/logo-clouds/logo-cloud-1.tsx +56 -0
  128. package/src/blocks/marketing/navbars/navbar-1.tsx +267 -0
  129. package/src/blocks/marketing/navbars/navbar-2.tsx +351 -0
  130. package/src/blocks/marketing/navbars/navbar-3.tsx +303 -0
  131. package/src/blocks/marketing/navbars/navbar-4-account-menu.tsx +118 -0
  132. package/src/blocks/marketing/navbars/navbar-4-cart-sheet.tsx +248 -0
  133. package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +177 -0
  134. package/src/blocks/marketing/navbars/navbar-4-mobile-nav.tsx +104 -0
  135. package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +219 -0
  136. package/src/blocks/marketing/navbars/navbar-4-search-bar.tsx +52 -0
  137. package/src/blocks/marketing/navbars/navbar-4.tsx +411 -0
  138. package/src/blocks/marketing/pricing/pricing-section-1.tsx +94 -0
  139. package/src/blocks/marketing/statistics/statistic-1.tsx +93 -0
  140. package/src/blocks/marketing/testimonials/testimonial-1.tsx +141 -0
  141. package/src/blocks/marketing/testimonials/testimonial-2.tsx +108 -0
  142. package/src/blocks/shared/reaction-toggle.tsx +145 -0
  143. package/src/hooks/use-mobile.ts +19 -0
  144. package/src/index.ts +200 -0
  145. package/src/lib/contracts.test.ts +32 -0
  146. package/src/lib/contracts.ts +235 -0
  147. package/src/lib/types/menu.ts +43 -0
  148. package/src/lib/types.ts +4 -0
  149. package/src/lib/utils.ts +10 -0
  150. package/src/ui/accordion.tsx +65 -0
  151. package/src/ui/avatar.tsx +108 -0
  152. package/src/ui/badge.tsx +47 -0
  153. package/src/ui/breadcrumb.tsx +108 -0
  154. package/src/ui/button-group.tsx +83 -0
  155. package/src/ui/button.tsx +63 -0
  156. package/src/ui/card.tsx +91 -0
  157. package/src/ui/carousel.tsx +241 -0
  158. package/src/ui/dialog.tsx +158 -0
  159. package/src/ui/drawer.tsx +134 -0
  160. package/src/ui/dropdown-menu.tsx +256 -0
  161. package/src/ui/empty.tsx +103 -0
  162. package/src/ui/field.tsx +248 -0
  163. package/src/ui/input-group.tsx +170 -0
  164. package/src/ui/input-otp.tsx +76 -0
  165. package/src/ui/input.tsx +20 -0
  166. package/src/ui/label.tsx +23 -0
  167. package/src/ui/navigation-menu.tsx +167 -0
  168. package/src/ui/pagination.tsx +127 -0
  169. package/src/ui/progress.tsx +30 -0
  170. package/src/ui/radio-group.tsx +44 -0
  171. package/src/ui/select.tsx +189 -0
  172. package/src/ui/separator.tsx +27 -0
  173. package/src/ui/sheet.tsx +142 -0
  174. package/src/ui/sidebar.tsx +726 -0
  175. package/src/ui/skeleton.tsx +13 -0
  176. package/src/ui/table.tsx +115 -0
  177. package/src/ui/tabs.tsx +90 -0
  178. package/src/ui/textarea.tsx +17 -0
  179. package/src/ui/toggle-group.tsx +83 -0
  180. package/src/ui/toggle.tsx +46 -0
  181. package/src/ui/tooltip.tsx +56 -0
  182. package/tsconfig.json +20 -0
  183. package/tsup.config.ts +16 -0
  184. package/vitest.config.ts +9 -0
@@ -0,0 +1,320 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { Eye, EyeOff, Lock, Mail } from 'lucide-react'
5
+
6
+ import { Button } from '../../../ui/button'
7
+ import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from '../../../ui/card'
8
+ import { Input } from '../../../ui/input'
9
+ import { InputOTP, InputOTPGroup, InputOTPSlot } from '../../../ui/input-otp'
10
+ import { Progress } from '../../../ui/progress'
11
+
12
+ import ForgotPasswordPage1BrandMark from './forgot-password-page-1-brand-mark'
13
+
14
+ export interface ForgotPasswordPage1Props {
15
+ /** Callback fired with the email when the user submits the reset-code step. */
16
+ onSendResetCode?: (email: string) => void
17
+ /** Callback fired with the 6-digit code when the user verifies it. */
18
+ onVerifyCode?: (code: string) => void
19
+ /** Callback fired when the user requests a new code. */
20
+ onResendCode?: () => void
21
+ /** Callback fired with the new password when the user sets it. */
22
+ onSetNewPassword?: (password: string) => void
23
+ /** Callback fired when the user goes back to login from the success step. */
24
+ onBackToLogin?: () => void
25
+ step1TitleLabel?: string
26
+ step1DescriptionLabel?: string
27
+ emailLabel?: string
28
+ emailPlaceholder?: string
29
+ sendResetCodeLabel?: string
30
+ step2TitleLabel?: string
31
+ step2DescriptionLabel?: string
32
+ codeLabel?: string
33
+ verifyCodeLabel?: string
34
+ resendCodeLabel?: string
35
+ resendCodeCooldownLabel?: string
36
+ didNotReceiveLabel?: string
37
+ step3TitleLabel?: string
38
+ step3DescriptionLabel?: string
39
+ newPasswordLabel?: string
40
+ setNewPasswordLabel?: string
41
+ strengthWeakLabel?: string
42
+ strengthMediumLabel?: string
43
+ strengthStrongLabel?: string
44
+ strengthIndicatorLabel?: string
45
+ step4TitleLabel?: string
46
+ step4DescriptionLabel?: string
47
+ backToLoginLabel?: string
48
+ dontHaveAccountLabel?: string
49
+ signUpLabel?: string
50
+ showPasswordLabel?: string
51
+ hidePasswordLabel?: string
52
+ }
53
+
54
+ const RESEND_COOLDOWN_SECONDS = 30
55
+
56
+ /**
57
+ * Returns a password strength score from 0 to 100.
58
+ * Adds 25 points per satisfied criterion: length >= 8, contains an
59
+ * uppercase letter, contains a digit, and contains a non-alphanumeric
60
+ * character. Used to gate the "Set new password" step and drive the
61
+ * strength progress bar.
62
+ */
63
+ function passwordStrength(password: string) {
64
+ let strength = 0
65
+ if (password.length >= 8) strength += 25
66
+ if (/[A-Z]/.test(password)) strength += 25
67
+ if (/[0-9]/.test(password)) strength += 25
68
+ if (/[^A-Za-z0-9]/.test(password)) strength += 25
69
+ return strength
70
+ }
71
+
72
+ export function ForgotPasswordPage1({
73
+ onSendResetCode,
74
+ onVerifyCode,
75
+ onResendCode,
76
+ onSetNewPassword,
77
+ onBackToLogin,
78
+ step1TitleLabel = '¿Olvidaste tu contraseña?',
79
+ step1DescriptionLabel = 'Ingresa tu correo para restablecer tu contraseña',
80
+ emailLabel = 'Correo electrónico',
81
+ emailPlaceholder = 'tucorreo@ejemplo.com',
82
+ sendResetCodeLabel = 'Enviar código de restablecimiento',
83
+ step2TitleLabel = 'Revisa tu correo',
84
+ step2DescriptionLabel = 'Ingresa el código de 6 dígitos enviado a {email}',
85
+ codeLabel = 'Código de verificación',
86
+ verifyCodeLabel = 'Verificar código',
87
+ resendCodeLabel = 'Reenviar código',
88
+ resendCodeCooldownLabel = 'Reenviar en {n}s',
89
+ didNotReceiveLabel = '¿No lo recibiste?',
90
+ step3TitleLabel = 'Crea una nueva contraseña',
91
+ step3DescriptionLabel = 'Elige una contraseña segura para proteger tu cuenta',
92
+ newPasswordLabel = 'Nueva contraseña',
93
+ setNewPasswordLabel = 'Establecer nueva contraseña',
94
+ strengthWeakLabel = 'Débil',
95
+ strengthMediumLabel = 'Media',
96
+ strengthStrongLabel = 'Fuerte',
97
+ strengthIndicatorLabel = 'Indicador de fuerza de contraseña',
98
+ step4TitleLabel = '¡Felicitaciones!',
99
+ step4DescriptionLabel = 'Restableciste tu contraseña correctamente',
100
+ backToLoginLabel = 'Volver al inicio de sesión',
101
+ dontHaveAccountLabel = '¿Aún no tienes una cuenta?',
102
+ signUpLabel = 'Regístrate',
103
+ showPasswordLabel = 'Mostrar contraseña',
104
+ hidePasswordLabel = 'Ocultar contraseña',
105
+ }: ForgotPasswordPage1Props) {
106
+ const [step, setStep] = useState(1)
107
+ const [email, setEmail] = useState('')
108
+ const [code, setCode] = useState('')
109
+ const [password, setPassword] = useState('')
110
+ const [showPassword, setShowPassword] = useState(false)
111
+ const [resendCooldown, setResendCooldown] = useState(0)
112
+
113
+ useEffect(() => {
114
+ if (resendCooldown <= 0) return
115
+ const timer = setInterval(() => {
116
+ setResendCooldown(prev => (prev <= 1 ? 0 : prev - 1))
117
+ }, 1000)
118
+ return () => clearInterval(timer)
119
+ }, [resendCooldown])
120
+
121
+ const handleResend = () => {
122
+ onResendCode?.()
123
+ setResendCooldown(RESEND_COOLDOWN_SECONDS)
124
+ }
125
+
126
+ const renderStepContent = () => {
127
+ switch (step) {
128
+ case 1:
129
+ return (
130
+ <>
131
+ <CardHeader className="pt-0 text-center">
132
+ <CardTitle className="text-2xl">{step1TitleLabel}</CardTitle>
133
+ <CardDescription>{step1DescriptionLabel}</CardDescription>
134
+ </CardHeader>
135
+
136
+ <div className="flex flex-col gap-6">
137
+ <div className="relative">
138
+ <Input
139
+ id="email-forgot-password-page-1"
140
+ type="email"
141
+ placeholder={emailPlaceholder}
142
+ className="bg-transparent ps-10 h-9 text-sm"
143
+ autoComplete="email"
144
+ aria-label={emailLabel}
145
+ value={email}
146
+ onChange={e => setEmail(e.target.value)}
147
+ required
148
+ />
149
+ <Mail className="text-muted-foreground absolute start-3 top-1/2 size-5 -translate-y-1/2" />
150
+ </div>
151
+ <Button
152
+ type="button"
153
+ className="h-9 px-4 py-2 w-full cursor-pointer"
154
+ onClick={() => {
155
+ onSendResetCode?.(email)
156
+ setStep(2)
157
+ }}
158
+ disabled={!email}
159
+ >
160
+ {sendResetCodeLabel}
161
+ </Button>
162
+ <p className="mt-6 flex justify-center gap-1 text-center text-sm">
163
+ <span>{dontHaveAccountLabel}</span>
164
+ <a href="#" className="underline underline-offset-4">
165
+ {signUpLabel}
166
+ </a>
167
+ </p>
168
+ </div>
169
+ </>
170
+ )
171
+
172
+ case 2:
173
+ return (
174
+ <>
175
+ <div className="mb-6 text-center">
176
+ <h1 className="mb-2 text-2xl font-bold tracking-tight text-balance">{step2TitleLabel}</h1>
177
+ <p className="text-muted-foreground text-sm text-balance">
178
+ {step2DescriptionLabel.replace('{email}', email || 'tu correo')}
179
+ </p>
180
+ </div>
181
+
182
+ <div className="flex flex-col gap-6">
183
+ <InputOTP
184
+ maxLength={6}
185
+ value={code}
186
+ onChange={value => setCode(value)}
187
+ aria-label={codeLabel}
188
+ >
189
+ <InputOTPGroup className="grid w-full grid-cols-6 gap-2 sm:gap-3">
190
+ <InputOTPSlot index={0} className="h-12 w-auto flex-1 rounded-md border-l text-lg" />
191
+ <InputOTPSlot index={1} className="h-12 w-auto flex-1 rounded-md border-l text-lg" />
192
+ <InputOTPSlot index={2} className="h-12 w-auto flex-1 rounded-md border-l text-lg" />
193
+ <InputOTPSlot index={3} className="h-12 w-auto flex-1 rounded-md border-l text-lg" />
194
+ <InputOTPSlot index={4} className="h-12 w-auto flex-1 rounded-md border-l text-lg" />
195
+ <InputOTPSlot index={5} className="h-12 w-auto flex-1 rounded-md border-l text-lg" />
196
+ </InputOTPGroup>
197
+ </InputOTP>
198
+ <Button
199
+ type="button"
200
+ className="h-9 px-4 py-2 w-full cursor-pointer"
201
+ onClick={() => {
202
+ onVerifyCode?.(code)
203
+ setStep(3)
204
+ }}
205
+ disabled={code.length < 6}
206
+ >
207
+ {verifyCodeLabel}
208
+ </Button>
209
+ <p className="text-muted-foreground text-center text-sm">
210
+ {didNotReceiveLabel}{' '}
211
+ <Button
212
+ variant="link"
213
+ className="cursor-pointer px-1 font-medium underline underline-offset-4 hover:underline"
214
+ onClick={handleResend}
215
+ disabled={resendCooldown > 0}
216
+ >
217
+ {resendCooldown > 0
218
+ ? resendCodeCooldownLabel.replace('{n}', String(resendCooldown))
219
+ : resendCodeLabel}
220
+ </Button>
221
+ </p>
222
+ </div>
223
+ </>
224
+ )
225
+
226
+ case 3: {
227
+ const strength = passwordStrength(password)
228
+ const strengthLabel =
229
+ strength === 0 ? '' : strength < 50 ? strengthWeakLabel : strength < 75 ? strengthMediumLabel : strengthStrongLabel
230
+ return (
231
+ <>
232
+ <div className="mb-6 text-center">
233
+ <h1 className="mb-2 text-2xl font-bold tracking-tight text-balance">{step3TitleLabel}</h1>
234
+ <p className="text-muted-foreground text-sm text-balance">{step3DescriptionLabel}</p>
235
+ </div>
236
+
237
+ <div className="flex flex-col gap-6">
238
+ <div className="relative">
239
+ <Input
240
+ id="new-password-forgot-password-page-1"
241
+ type={showPassword ? 'text' : 'password'}
242
+ placeholder={newPasswordLabel}
243
+ className="bg-transparent ps-10 text-sm"
244
+ autoComplete="new-password"
245
+ aria-label={newPasswordLabel}
246
+ value={password}
247
+ onChange={e => setPassword(e.target.value)}
248
+ required
249
+ />
250
+ <Lock className="text-muted-foreground absolute start-3 top-1/2 size-5 -translate-y-1/2" />
251
+ <Button
252
+ type="button"
253
+ variant="ghost"
254
+ size="icon"
255
+ className="size-9 text-muted-foreground absolute end-1 top-1/2 -translate-y-1/2 cursor-pointer hover:bg-transparent"
256
+ onClick={() => setShowPassword(prev => !prev)}
257
+ aria-label={showPassword ? hidePasswordLabel : showPasswordLabel}
258
+ >
259
+ {showPassword ? <EyeOff /> : <Eye />}
260
+ </Button>
261
+ </div>
262
+ <div className="flex flex-col gap-1">
263
+ <Progress value={strength} className="h-2" aria-label={strengthIndicatorLabel} />
264
+ <small className="text-muted-foreground block text-end text-xs">{strengthLabel}</small>
265
+ </div>
266
+
267
+ <Button
268
+ type="button"
269
+ className="h-9 px-4 py-2 w-full cursor-pointer"
270
+ onClick={() => {
271
+ onSetNewPassword?.(password)
272
+ setStep(4)
273
+ }}
274
+ disabled={strength < 75}
275
+ >
276
+ {setNewPasswordLabel}
277
+ </Button>
278
+ </div>
279
+ </>
280
+ )
281
+ }
282
+
283
+ case 4:
284
+ return (
285
+ <>
286
+ <CardHeader className="mb-4 pt-0 text-center">
287
+ <CardTitle className="text-2xl">{step4TitleLabel}</CardTitle>
288
+ <CardDescription>{step4DescriptionLabel}</CardDescription>
289
+ </CardHeader>
290
+ <CardFooter>
291
+ <Button
292
+ className="h-9 px-4 py-2 w-full cursor-pointer"
293
+ onClick={() => {
294
+ onBackToLogin?.()
295
+ setStep(1)
296
+ }}
297
+ >
298
+ {backToLoginLabel}
299
+ </Button>
300
+ </CardFooter>
301
+ </>
302
+ )
303
+ default:
304
+ return null
305
+ }
306
+ }
307
+
308
+ return (
309
+ <section className="from-background to-muted/50 relative isolate flex min-h-dvh w-full items-center justify-center overflow-hidden bg-linear-to-br">
310
+ <div className="relative z-10 container mx-auto flex min-h-dvh items-center justify-center px-4 py-12 sm:py-16">
311
+ <Card className="bg-background/80 relative w-full max-w-md ring-0 p-6 shadow-2xl backdrop-blur-md sm:p-8">
312
+ <ForgotPasswordPage1BrandMark />
313
+ {renderStepContent()}
314
+ </Card>
315
+ </div>
316
+ </section>
317
+ )
318
+ }
319
+
320
+ export default ForgotPasswordPage1
@@ -0,0 +1,231 @@
1
+ import type { FormEvent } from 'react'
2
+ import { Lock, Mail, User, type LucideIcon } from 'lucide-react'
3
+
4
+ import { Button } from '../../../ui/button'
5
+ import { Card } from '../../../ui/card'
6
+ import { Input } from '../../../ui/input'
7
+
8
+ export interface SignUpPage1SocialButton {
9
+ name: string
10
+ icon: LucideIcon
11
+ }
12
+
13
+ export interface SignUpPage1Data {
14
+ fullName: string
15
+ email: string
16
+ password: string
17
+ }
18
+
19
+ export interface SignUpPage1Props {
20
+ /** Brand text shown under the logo mark. Omit to hide it. */
21
+ logoText?: string
22
+ createAccountTitleLabel?: string
23
+ createAccountDescriptionLabel?: string
24
+ fullNameLabel?: string
25
+ fullNamePlaceholder?: string
26
+ emailLabel?: string
27
+ emailPlaceholder?: string
28
+ passwordLabel?: string
29
+ passwordPlaceholder?: string
30
+ createAccountLabel?: string
31
+ orSignUpWithLabel?: string
32
+ /** aria-label template for each social button. Use `{name}` as the button name placeholder. */
33
+ socialAriaLabel?: string
34
+ /** Social sign-up buttons rendered in a 3-column grid. Pass an empty array to hide. */
35
+ socialButtons?: SignUpPage1SocialButton[]
36
+ alreadyHaveAccountLabel?: string
37
+ signInLabel?: string
38
+ termsLabel?: string
39
+ termsLinkLabel?: string
40
+ termsHref?: string
41
+ termsAndLabel?: string
42
+ privacyLinkLabel?: string
43
+ privacyHref?: string
44
+ onSubmit?: (data: SignUpPage1Data) => void
45
+ onSignIn?: () => void
46
+ }
47
+
48
+ export function SignUpPage1({
49
+ logoText,
50
+ createAccountTitleLabel = 'Crea tu cuenta',
51
+ createAccountDescriptionLabel = 'Completa el formulario para comenzar',
52
+ fullNameLabel = 'Nombre completo',
53
+ fullNamePlaceholder = 'Nombre completo',
54
+ emailLabel = 'Correo electrónico',
55
+ emailPlaceholder = 'me@ejemplo.com',
56
+ passwordLabel = 'Contraseña',
57
+ passwordPlaceholder = 'Crea una contraseña',
58
+ createAccountLabel = 'Crear cuenta',
59
+ orSignUpWithLabel = 'O regístrate con',
60
+ socialAriaLabel = 'Continuar con {name}',
61
+ socialButtons = [],
62
+ alreadyHaveAccountLabel = '¿Ya tienes una cuenta?',
63
+ signInLabel = 'Inicia sesión',
64
+ termsLabel = 'Al crear una cuenta, aceptas los',
65
+ termsLinkLabel = 'Términos de servicio',
66
+ termsHref,
67
+ termsAndLabel = 'y',
68
+ privacyLinkLabel = 'Política de privacidad',
69
+ privacyHref,
70
+ onSubmit,
71
+ onSignIn,
72
+ }: SignUpPage1Props) {
73
+ const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
74
+ event.preventDefault()
75
+ const formData = new FormData(event.currentTarget)
76
+ onSubmit?.({
77
+ fullName: String(formData.get('fullName') ?? ''),
78
+ email: String(formData.get('email') ?? ''),
79
+ password: String(formData.get('password') ?? ''),
80
+ })
81
+ }
82
+
83
+ return (
84
+ <section className='from-background to-muted/50 relative isolate flex min-h-dvh w-full flex-col items-center justify-center overflow-hidden bg-linear-to-br'>
85
+ <div className='relative z-10 container mx-auto flex min-h-dvh flex-col items-center justify-center px-4 py-12'>
86
+ <Card className='relative w-full max-w-lg p-8 shadow-2xl ring-0'>
87
+ <div className='mb-8 flex flex-col items-center'>
88
+ <div className='my-4 flex justify-center'>
89
+ <div className='bg-secondary relative size-14 rounded-full border'>
90
+ <div className='flex h-full items-center justify-center'>
91
+ <svg width='32' height='32' viewBox='0 0 32 32' fill='none' xmlns='http://www.w3.org/2000/svg'>
92
+ <path
93
+ d='M26 24.75C26.4142 24.75 26.75 24.4142 26.75 24C26.75 23.5858 26.4142 23.25 26 23.25V24.75ZM26 23.25H11V24.75H26V23.25ZM8.75 21V15H7.25V21H8.75ZM11 23.25C9.75736 23.25 8.75 22.2426 8.75 21H7.25C7.25 23.0711 8.92893 24.75 11 24.75V23.25Z'
94
+ fill='currentColor'
95
+ />
96
+ <path
97
+ d='M1.5 3.25C1.08579 3.25 0.75 3.58579 0.75 4C0.75 4.41421 1.08579 4.75 1.5 4.75V3.25ZM1.5 4.75H6V3.25H1.5V4.75ZM7.25 6V21H8.75V6H7.25ZM6 4.75C6.69036 4.75 7.25 5.30964 7.25 6H8.75C8.75 4.48122 7.51878 3.25 6 3.25V4.75Z'
98
+ fill='currentColor'
99
+ />
100
+ <path
101
+ d='M22 21.75C22.4142 21.75 22.75 21.4142 22.75 21C22.75 20.5858 22.4142 20.25 22 20.25V21.75ZM22 20.25H11V21.75H22V20.25ZM8.75 18V12H7.25V18H8.75ZM11 20.25C9.75736 20.25 8.75 19.2426 8.75 18H7.25C7.25 20.0711 8.92893 21.75 11 21.75V20.25Z'
102
+ fill='currentColor'
103
+ />
104
+ <path
105
+ d='M27.2057 19.754C27.0654 20.1438 26.6357 20.346 26.246 20.2057C25.8562 20.0654 25.654 19.6357 25.7943 19.246L27.2057 19.754ZM30.0361 9.67744L29.3305 9.4234L29.3305 9.4234L30.0361 9.67744ZM25.7943 19.246L29.3305 9.4234L30.7418 9.93148L27.2057 19.754L25.7943 19.246ZM28.1543 7.75L8 7.75V6.25L28.1543 6.25V7.75ZM29.3305 9.4234C29.6237 8.60882 29.0201 7.75 28.1543 7.75V6.25C30.059 6.25 31.3869 8.13941 30.7418 9.93148L29.3305 9.4234Z'
106
+ fill='currentColor'
107
+ />
108
+ <path
109
+ d='M13.5 21.75C13.0858 21.75 12.75 21.4142 12.75 21C12.75 20.5858 13.0858 20.25 13.5 20.25V21.75ZM26.7111 19.009L27.4174 19.2613L27.4174 19.2613L26.7111 19.009ZM13.5 20.25H23.8858V21.75H13.5V20.25ZM26.0048 18.7568L27.7937 13.7477L29.2063 14.2523L27.4174 19.2613L26.0048 18.7568ZM23.8858 20.25C24.8367 20.25 25.6849 19.6522 26.0048 18.7568L27.4174 19.2613C26.8843 20.7537 25.4706 21.75 23.8858 21.75V20.25Z'
110
+ fill='currentColor'
111
+ />
112
+ <path d='M21.1694 10.5806L14.5651 17.1849' stroke='currentColor' />
113
+ <path d='M22.1694 14.5806L18.5632 18.1868' stroke='currentColor' />
114
+ <circle cx='13.1' cy='26.1' r='1.7' stroke='currentColor' />
115
+ <circle cx='22.1' cy='26.1' r='1.7' stroke='currentColor' />
116
+ </svg>
117
+ </div>
118
+ </div>
119
+ </div>
120
+ {logoText && <div className='text-base font-semibold tracking-tight'>{logoText}</div>}
121
+ <h1 className='mb-2 mt-4 text-center text-2xl font-bold tracking-tight'>{createAccountTitleLabel}</h1>
122
+ <p className='text-muted-foreground text-center text-sm'>{createAccountDescriptionLabel}</p>
123
+ </div>
124
+
125
+ <form className='flex flex-col gap-6' onSubmit={handleSubmit}>
126
+ <div className='relative'>
127
+ <Input
128
+ id='fullname'
129
+ name='fullName'
130
+ type='text'
131
+ placeholder={fullNamePlaceholder}
132
+ aria-label={fullNameLabel}
133
+ className='bg-transparent h-9 ps-10 text-sm'
134
+ autoComplete='name'
135
+ required
136
+ />
137
+ <User className='text-muted-foreground absolute start-3 top-1/2 size-5 -translate-y-1/2' />
138
+ </div>
139
+
140
+ <div className='relative'>
141
+ <Input
142
+ id='email'
143
+ name='email'
144
+ type='email'
145
+ placeholder={emailPlaceholder}
146
+ aria-label={emailLabel}
147
+ className='bg-transparent h-9 ps-10 text-sm'
148
+ autoComplete='email'
149
+ required
150
+ />
151
+ <Mail className='text-muted-foreground absolute start-3 top-1/2 size-5 -translate-y-1/2' />
152
+ </div>
153
+
154
+ <div className='relative'>
155
+ <Input
156
+ id='password'
157
+ name='password'
158
+ type='password'
159
+ placeholder={passwordPlaceholder}
160
+ aria-label={passwordLabel}
161
+ className='bg-transparent h-9 ps-10 text-sm'
162
+ autoComplete='new-password'
163
+ required
164
+ />
165
+ <Lock className='text-muted-foreground absolute start-3 top-1/2 size-5 -translate-y-1/2' />
166
+ </div>
167
+
168
+ <Button className='h-9 w-full cursor-pointer px-4 py-2' type='submit'>
169
+ {createAccountLabel}
170
+ </Button>
171
+
172
+ {socialButtons.length > 0 && (
173
+ <>
174
+ <div className='relative my-6 text-center'>
175
+ <div className='absolute inset-0 flex items-center'>
176
+ <div className='w-full border-t' />
177
+ </div>
178
+ <div className='relative flex justify-center text-xs uppercase'>
179
+ <span className='bg-card text-muted-foreground px-2'>{orSignUpWithLabel}</span>
180
+ </div>
181
+ </div>
182
+
183
+ <div className='grid grid-cols-3 gap-3'>
184
+ {socialButtons.map(button => (
185
+ <Button key={button.name} variant='outline' className='h-9 w-full cursor-pointer px-4 py-2'>
186
+ <button.icon />
187
+ <span className='sr-only'>{socialAriaLabel.replace('{name}', button.name)}</span>
188
+ </Button>
189
+ ))}
190
+ </div>
191
+ </>
192
+ )}
193
+ </form>
194
+
195
+ <p className='mt-6 flex justify-center gap-1 text-center text-sm'>
196
+ <span>{alreadyHaveAccountLabel}</span>
197
+ {onSignIn ? (
198
+ <button type='button' onClick={onSignIn} className='underline underline-offset-4'>
199
+ {signInLabel}
200
+ </button>
201
+ ) : (
202
+ <span className='underline underline-offset-4'>{signInLabel}</span>
203
+ )}
204
+ </p>
205
+ </Card>
206
+
207
+ <p className='text-muted-foreground relative z-10 mt-8 max-w-lg px-8 text-center text-xs text-balance'>
208
+ {termsLabel}{' '}
209
+ {termsHref ? (
210
+ <a href={termsHref} className='hover:text-primary underline underline-offset-4'>
211
+ {termsLinkLabel}
212
+ </a>
213
+ ) : (
214
+ <span className='hover:text-primary underline underline-offset-4'>{termsLinkLabel}</span>
215
+ )}{' '}
216
+ {termsAndLabel}{' '}
217
+ {privacyHref ? (
218
+ <a href={privacyHref} className='hover:text-primary underline underline-offset-4'>
219
+ {privacyLinkLabel}
220
+ </a>
221
+ ) : (
222
+ <span className='hover:text-primary underline underline-offset-4'>{privacyLinkLabel}</span>
223
+ )}
224
+ .
225
+ </p>
226
+ </div>
227
+ </section>
228
+ )
229
+ }
230
+
231
+ export default SignUpPage1