@thorprovider/blocks 1.0.0 → 2.0.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 (91) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +156 -40
  3. package/package.json +1 -1
  4. package/src/blocks/application/login-forms/login-page.test.tsx +190 -0
  5. package/src/blocks/application/login-forms/login-page.tsx +240 -0
  6. package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +6 -8
  7. package/src/blocks/application/signup-forms/signup-page-1.tsx +127 -129
  8. package/src/blocks/application/verification/verify-1.tsx +2 -2
  9. package/src/blocks/application/viewport-ownership.test.tsx +41 -0
  10. package/src/blocks/e-commerce/category-filters/category-listing-filter-fields.tsx +218 -0
  11. package/src/blocks/e-commerce/category-filters/{category-filter-2.test.tsx → category-listing.test.tsx} +287 -199
  12. package/src/blocks/e-commerce/category-filters/category-listing.tsx +934 -0
  13. package/src/blocks/e-commerce/checkout-forms/checkout-contact-step.tsx +149 -0
  14. package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-country-field.tsx → checkout-country-field.tsx} +10 -8
  15. package/src/blocks/e-commerce/checkout-forms/checkout-order-summary.tsx +230 -0
  16. package/src/blocks/e-commerce/checkout-forms/checkout-payment-step.tsx +100 -0
  17. package/src/blocks/e-commerce/checkout-forms/checkout-progress.tsx +62 -0
  18. package/src/blocks/e-commerce/checkout-forms/checkout-review-step.tsx +59 -0
  19. package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-shipping-step.tsx → checkout-shipping-step.tsx} +65 -41
  20. package/src/blocks/e-commerce/checkout-forms/checkout.test.tsx +478 -0
  21. package/src/blocks/e-commerce/checkout-forms/checkout.tsx +550 -0
  22. package/src/blocks/{marketing/navbars/navbar-4-account-menu.test.tsx → e-commerce/navigation/storefront-navbar-account-menu.test.tsx} +8 -8
  23. package/src/blocks/{marketing/navbars/navbar-4-account-menu.tsx → e-commerce/navigation/storefront-navbar-account-menu.tsx} +3 -3
  24. package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.test.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.test.tsx} +22 -22
  25. package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.tsx} +3 -3
  26. package/src/blocks/{marketing/navbars/navbar-4-mobile-nav.tsx → e-commerce/navigation/storefront-navbar-mobile-nav.tsx} +4 -12
  27. package/src/blocks/{marketing/navbars/navbar-4-search-bar.tsx → e-commerce/navigation/storefront-navbar-search-bar.tsx} +3 -3
  28. package/src/blocks/{marketing/navbars/navbar-4.test.tsx → e-commerce/navigation/storefront-navbar.test.tsx} +40 -40
  29. package/src/blocks/{marketing/navbars/navbar-4.tsx → e-commerce/navigation/storefront-navbar.tsx} +17 -17
  30. package/src/blocks/e-commerce/product-list/product-card.tsx +63 -40
  31. package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +2 -2
  32. package/src/blocks/marketing/footers/footer-section-accessibility.test.tsx +118 -0
  33. package/src/blocks/marketing/footers/footer-section-actions.test.tsx +131 -0
  34. package/src/blocks/marketing/footers/footer-section-data.test.tsx +127 -0
  35. package/src/blocks/marketing/footers/footer-section-native-links.test.tsx +100 -0
  36. package/src/blocks/marketing/footers/footer-section-public-api.test.tsx +11 -0
  37. package/src/blocks/marketing/footers/footer-section.test.tsx +46 -0
  38. package/src/blocks/marketing/footers/footer-section.tsx +532 -0
  39. package/src/blocks/marketing/navbars/navbar-public-api.test.tsx +57 -0
  40. package/src/blocks/marketing/navbars/navbar.mega.test.tsx +149 -0
  41. package/src/blocks/marketing/navbars/navbar.test.tsx +155 -0
  42. package/src/blocks/marketing/navbars/navbar.tsx +735 -0
  43. package/src/blocks/marketing/testimonials/testimonial-section.test.tsx +171 -0
  44. package/src/blocks/marketing/testimonials/testimonial-section.tsx +237 -0
  45. package/src/index.ts +80 -81
  46. package/src/lib/contracts-curated.test.ts +5 -5
  47. package/src/lib/contracts.test.ts +55 -0
  48. package/src/lib/contracts.ts +64 -2
  49. package/tsup.config.ts +1 -1
  50. package/src/blocks/application/login-forms/login-page-1.tsx +0 -246
  51. package/src/blocks/application/login-forms/login-page-2.tsx +0 -207
  52. package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +0 -260
  53. package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +0 -222
  54. package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +0 -921
  55. package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +0 -57
  56. package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +0 -124
  57. package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +0 -170
  58. package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +0 -52
  59. package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +0 -57
  60. package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +0 -68
  61. package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +0 -298
  62. package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +0 -47
  63. package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +0 -9
  64. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +0 -96
  65. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +0 -252
  66. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +0 -188
  67. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +0 -36
  68. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.test.tsx +0 -216
  69. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +0 -338
  70. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +0 -130
  71. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +0 -175
  72. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +0 -190
  73. package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +0 -312
  74. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +0 -91
  75. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +0 -110
  76. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +0 -137
  77. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +0 -68
  78. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +0 -79
  79. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +0 -172
  80. package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +0 -392
  81. package/src/blocks/marketing/footers/footer-section-1.test.tsx +0 -117
  82. package/src/blocks/marketing/footers/footer-section-1.tsx +0 -178
  83. package/src/blocks/marketing/footers/footer-section-3.tsx +0 -192
  84. package/src/blocks/marketing/footers/footer-section-4.tsx +0 -355
  85. package/src/blocks/marketing/navbars/navbar-1.tsx +0 -267
  86. package/src/blocks/marketing/navbars/navbar-2.tsx +0 -351
  87. package/src/blocks/marketing/navbars/navbar-3.tsx +0 -303
  88. package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +0 -177
  89. package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +0 -219
  90. package/src/blocks/marketing/testimonials/testimonial-1.tsx +0 -141
  91. package/src/blocks/marketing/testimonials/testimonial-2.tsx +0 -108
@@ -3,6 +3,7 @@ import { z } from 'zod'
3
3
  import {
4
4
  categoryCardPropsSchema,
5
5
  categoryGridPropsSchema,
6
+ navbarPropsSchema,
6
7
  productCardPropsSchema,
7
8
  productGridPropsSchema,
8
9
  storeCategorySchema,
@@ -171,3 +172,57 @@ describe('category cell and listing contracts', () => {
171
172
  expect(categoryCardPropsSchema.safeParse({ categories: [category] }).success).toBe(false)
172
173
  })
173
174
  })
175
+
176
+ describe('navbarPropsSchema', () => {
177
+ const full = {
178
+ presentation: 'mega' as const,
179
+ logoText: 'ElectroCuba',
180
+ logoHref: '/',
181
+ currentPath: '/tienda',
182
+ links: [
183
+ {
184
+ label: 'Catálogo',
185
+ groups: [
186
+ {
187
+ title: 'Esenciales',
188
+ items: [{ label: 'Camisas', href: '/camisas', description: 'Cortes diarios', icon: 'shirt' }],
189
+ },
190
+ ],
191
+ },
192
+ { label: 'Nosotros', href: '/nosotros', emphasis: true },
193
+ ],
194
+ featured: {
195
+ eyebrow: 'Nueva temporada',
196
+ title: 'Lino',
197
+ description: 'Capas transpirables',
198
+ href: '/lino',
199
+ linkLabel: 'Descubrir',
200
+ image: '/lino.jpg',
201
+ imageAlt: 'Camisa de lino',
202
+ },
203
+ signIn: { label: 'Entrar', href: '/cuenta' },
204
+ cta: { label: 'Comprar' },
205
+ className: 'bg-brand-nav text-brand-nav-foreground',
206
+ }
207
+
208
+ it('round-trips a fully populated header', () => {
209
+ expect(navbarPropsSchema.parse(full)).toEqual(full)
210
+ })
211
+
212
+ it('requires the brand text and the links list', () => {
213
+ expect(navbarPropsSchema.safeParse({ ...full, logoText: undefined }).success).toBe(false)
214
+ expect(navbarPropsSchema.safeParse({ logoText: 'ElectroCuba' }).success).toBe(false)
215
+ })
216
+
217
+ it('rejects non-string hrefs and unknown presentations', () => {
218
+ expect(navbarPropsSchema.safeParse({ logoText: 'X', links: [{ label: 'Y', href: 42 }] }).success).toBe(false)
219
+ expect(navbarPropsSchema.safeParse({ ...full, presentation: 'sidebar' }).success).toBe(false)
220
+ })
221
+
222
+ it('cannot carry host bindings across the serializable boundary', () => {
223
+ const parsed = navbarPropsSchema.parse({ ...full, iconMap: { shirt: () => null }, onSignIn: () => {} })
224
+
225
+ expect(parsed).not.toHaveProperty('iconMap')
226
+ expect(parsed).not.toHaveProperty('onSignIn')
227
+ })
228
+ })
@@ -495,7 +495,12 @@ export const heroImageSchema = z.object({
495
495
  })
496
496
  export type HeroImage = z.infer<typeof heroImageSchema>
497
497
 
498
- export const navbar4PropsSchema = z.object({
498
+ /**
499
+ * Serializable contract for the canonical commerce storefront shell
500
+ * (`StorefrontNavbar`). Identity, cart, and search state stay host-owned: the
501
+ * schema carries content and display state only, never cart arithmetic.
502
+ */
503
+ export const storefrontNavbarPropsSchema = z.object({
499
504
  storeCategories: z.array(storeCategorySchema),
500
505
  shippingNote: z.string().optional(),
501
506
  utilityLinks: z.array(utilityLinkSchema),
@@ -519,7 +524,7 @@ export const navbar4PropsSchema = z.object({
519
524
  accountPending: z.boolean().optional(),
520
525
  accountUnavailable: z.boolean().optional(),
521
526
  })
522
- export type Navbar4SpecProps = z.infer<typeof navbar4PropsSchema>
527
+ export type StorefrontNavbarSpecProps = z.infer<typeof storefrontNavbarPropsSchema>
523
528
 
524
529
  /**
525
530
  * Serializable contracts for the two bounded editorial sections (`faq`, `cta`).
@@ -533,6 +538,63 @@ export const ctaSchema = z.object({
533
538
  })
534
539
  export type CtaSpec = z.infer<typeof ctaSchema>
535
540
 
541
+ /** Bounded presentations of the page-level navigation header. */
542
+ export const navbarPresentationSchema = z.enum(['bar', 'mega', 'floating'])
543
+ export type NavbarPresentationSpec = z.infer<typeof navbarPresentationSchema>
544
+
545
+ export const navbarItemSchema = z.object({
546
+ label: z.string(),
547
+ href: z.string(),
548
+ description: z.string().optional(),
549
+ /** Key resolved through the consumer's host-bound `iconMap`. */
550
+ icon: z.string().optional(),
551
+ })
552
+ export type NavbarItemSpec = z.infer<typeof navbarItemSchema>
553
+
554
+ export const navbarGroupSchema = z.object({
555
+ title: z.string().optional(),
556
+ items: z.array(navbarItemSchema),
557
+ })
558
+ export type NavbarGroupSpec = z.infer<typeof navbarGroupSchema>
559
+
560
+ export const navbarLinkSchema = z.object({
561
+ label: z.string(),
562
+ href: z.string().optional(),
563
+ groups: z.array(navbarGroupSchema).optional(),
564
+ emphasis: z.boolean().optional(),
565
+ })
566
+ export type NavbarLinkSpec = z.infer<typeof navbarLinkSchema>
567
+
568
+ export const navbarFeaturedSchema = z.object({
569
+ eyebrow: z.string(),
570
+ title: z.string(),
571
+ description: z.string().optional(),
572
+ href: z.string(),
573
+ linkLabel: z.string().optional(),
574
+ image: z.string().optional(),
575
+ imageAlt: z.string().optional(),
576
+ })
577
+ export type NavbarFeaturedSpec = z.infer<typeof navbarFeaturedSchema>
578
+
579
+ /**
580
+ * Serializable contract for the canonical page-level header (`Navbar`). The
581
+ * serializable subset excludes the React-only bindings: `iconMap`, `onSignIn`,
582
+ * and `onCtaClick` are bound by the host, exactly like the footer's icon map.
583
+ */
584
+ export const navbarPropsSchema = z.object({
585
+ presentation: navbarPresentationSchema.optional(),
586
+ logoText: z.string(),
587
+ logoHref: z.string().optional(),
588
+ currentPath: z.string().optional(),
589
+ links: z.array(navbarLinkSchema),
590
+ featured: navbarFeaturedSchema.optional(),
591
+ signIn: ctaSchema.optional(),
592
+ cta: ctaSchema.optional(),
593
+ /** Consumer-supplied classes merged onto the header surface last. */
594
+ className: z.string().optional(),
595
+ })
596
+ export type NavbarSpecProps = z.infer<typeof navbarPropsSchema>
597
+
536
598
  /**
537
599
  * Serializable subset of the canonical `StorefrontHero`'s props (callbacks
538
600
  * excluded). Every popularity, review, count, or countdown field is optional
package/tsup.config.ts CHANGED
@@ -3,7 +3,7 @@ import { defineConfig } from 'tsup'
3
3
  export default defineConfig({
4
4
  // Emit one module per source file (no bundling) so consumers can import a
5
5
  // single block and bundle only what they import. `outbase: 'src'` preserves
6
- // the source layout under dist/ (e.g. dist/blocks/.../navbar-4.mjs).
6
+ // the source layout under dist/ (e.g. dist/blocks/.../navbar.mjs).
7
7
  entry: ['src/**/*.ts', 'src/**/*.tsx', '!src/**/*.test.ts', '!src/**/*.test.tsx'],
8
8
  format: ['cjs', 'esm'],
9
9
  // Declarations are emitted per-file by `tsc -p tsconfig.build.json` (see the
@@ -1,246 +0,0 @@
1
- 'use client'
2
-
3
- import { useState, type FormEvent } from 'react'
4
- import { Eye, EyeOff, Lock, Mail, type LucideIcon } from 'lucide-react'
5
-
6
- import { Button } from '../../../ui/button'
7
- import { Card } from '../../../ui/card'
8
- import { CheckboxCheck } from '../../e-commerce/checkout-forms/checkbox-check'
9
- import { Input } from '../../../ui/input'
10
- import { cn } from '../../../lib/utils'
11
-
12
- export interface LoginPage1SocialButton {
13
- name: string
14
- icon: LucideIcon
15
- }
16
-
17
- export interface LoginPage1FormData {
18
- email: string
19
- password: string
20
- remember: boolean
21
- }
22
-
23
- export interface LoginPage1Props {
24
- logoText?: string
25
- welcomeTitleLabel?: string
26
- welcomeDescriptionLabel?: string
27
- emailLabel?: string
28
- emailPlaceholder?: string
29
- passwordLabel?: string
30
- passwordPlaceholder?: string
31
- rememberMeLabel?: string
32
- forgotPasswordLabel?: string
33
- signInLabel?: string
34
- orContinueWithLabel?: string
35
- dontHaveAccountLabel?: string
36
- createAccountLabel?: string
37
- /** Template with a `{provider}` placeholder for the social login buttons' aria-labels. */
38
- socialAriaLabelTemplate?: string
39
- showPasswordAriaLabel?: string
40
- hidePasswordAriaLabel?: string
41
- socialButtons?: LoginPage1SocialButton[]
42
- onSubmit?: (data: LoginPage1FormData) => void
43
- onForgotPassword?: () => void
44
- onCreateAccount?: () => void
45
- onSocialLogin?: (providerName: string) => void
46
- }
47
-
48
- export function LoginPage1({
49
- logoText,
50
- welcomeTitleLabel = 'Bienvenido de nuevo',
51
- welcomeDescriptionLabel = 'Inicia sesión para continuar',
52
- emailLabel = 'Correo electrónico',
53
- emailPlaceholder = 'taller@correo.com',
54
- passwordLabel = 'Contraseña',
55
- passwordPlaceholder = '••••••••',
56
- rememberMeLabel = 'Recuérdame',
57
- forgotPasswordLabel = '¿Olvidaste tu contraseña?',
58
- signInLabel = 'Iniciar sesión',
59
- orContinueWithLabel = 'O continúa con',
60
- dontHaveAccountLabel = '¿No tienes una cuenta?',
61
- createAccountLabel = 'Crea una cuenta',
62
- socialAriaLabelTemplate = 'Iniciar sesión con {provider}',
63
- showPasswordAriaLabel = 'Mostrar contraseña',
64
- hidePasswordAriaLabel = 'Ocultar contraseña',
65
- socialButtons = [],
66
- onSubmit,
67
- onForgotPassword,
68
- onCreateAccount,
69
- onSocialLogin,
70
- }: LoginPage1Props) {
71
- const [showPassword, setShowPassword] = useState(false)
72
- const [email, setEmail] = useState('')
73
- const [password, setPassword] = useState('')
74
- const [remember, setRemember] = useState(false)
75
-
76
- const handleSubmit = (event: FormEvent) => {
77
- event.preventDefault()
78
- onSubmit?.({ email, password, remember })
79
- }
80
-
81
- const forgotPasswordClassName = cn(
82
- 'ms-auto inline-block text-sm underline-offset-4',
83
- onForgotPassword ? 'cursor-pointer hover:underline' : 'text-muted-foreground'
84
- )
85
-
86
- const createAccountClassName = cn('underline underline-offset-4', onCreateAccount ? 'cursor-pointer' : undefined)
87
-
88
- return (
89
- <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'>
90
- <div className='relative z-10 container mx-auto flex min-h-dvh items-center justify-center px-4 py-12'>
91
- <Card className='relative w-full max-w-md ring-0 p-8 shadow-2xl'>
92
- <div className='mb-8 flex flex-col items-center'>
93
- <div className='my-4 flex justify-center'>
94
- <div className='bg-secondary relative size-14 rounded-full border'>
95
- <div className='flex h-full items-center justify-center'>
96
- <svg width='32' height='32' viewBox='0 0 32 32' fill='none' xmlns='http://www.w3.org/2000/svg' aria-hidden='true'>
97
- <path
98
- 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'
99
- fill='currentColor'
100
- />
101
- <path
102
- 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'
103
- fill='currentColor'
104
- />
105
- <path
106
- 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'
107
- fill='currentColor'
108
- />
109
- <path
110
- 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'
111
- fill='currentColor'
112
- />
113
- <path
114
- 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'
115
- fill='currentColor'
116
- />
117
- <path d='M21.1694 10.5806L14.5651 17.1849' stroke='currentColor' />
118
- <path d='M22.1694 14.5806L18.5632 18.1868' stroke='currentColor' />
119
- <circle cx='13.1' cy='26.1' r='1.7' stroke='currentColor' />
120
- <circle cx='22.1' cy='26.1' r='1.7' stroke='currentColor' />
121
- </svg>
122
- </div>
123
- </div>
124
- </div>
125
- {logoText && <p className='text-muted-foreground mb-2 text-sm font-semibold'>{logoText}</p>}
126
- <h1 className='mb-2 text-center text-2xl font-bold tracking-tight'>{welcomeTitleLabel}</h1>
127
- <p className='text-muted-foreground text-center text-sm'>{welcomeDescriptionLabel}</p>
128
- </div>
129
-
130
- <form onSubmit={handleSubmit} className='flex flex-col gap-6'>
131
- <div className='relative'>
132
- <Input
133
- type='email'
134
- placeholder={emailPlaceholder}
135
- aria-label={emailLabel}
136
- autoComplete='email'
137
- value={email}
138
- onChange={event => setEmail(event.target.value)}
139
- className='bg-transparent ps-10 h-9 text-sm'
140
- />
141
- <Mail className='text-muted-foreground absolute start-3 top-1/2 size-5 -translate-y-1/2' aria-hidden='true' />
142
- </div>
143
-
144
- <div className='relative'>
145
- <Input
146
- type={showPassword ? 'text' : 'password'}
147
- placeholder={passwordPlaceholder}
148
- aria-label={passwordLabel}
149
- autoComplete='current-password'
150
- value={password}
151
- onChange={event => setPassword(event.target.value)}
152
- className='bg-transparent ps-10 h-9 text-sm'
153
- />
154
- <Lock className='text-muted-foreground absolute start-3 top-1/2 size-5 -translate-y-1/2' aria-hidden='true' />
155
- <Button
156
- type='button'
157
- variant='ghost'
158
- aria-label={showPassword ? hidePasswordAriaLabel : showPasswordAriaLabel}
159
- className='absolute end-0 top-0 h-full cursor-pointer px-3 py-2 hover:bg-transparent'
160
- onClick={() => setShowPassword(prev => !prev)}
161
- >
162
- {showPassword ? (
163
- <EyeOff className='text-muted-foreground' />
164
- ) : (
165
- <Eye className='text-muted-foreground' />
166
- )}
167
- </Button>
168
- </div>
169
-
170
- <div className='flex items-center justify-between'>
171
- <div className='flex items-center gap-2'>
172
- <CheckboxCheck
173
- id='remember'
174
- checked={remember}
175
- onCheckedChange={value => setRemember(value === true)}
176
- className='data-checked:bg-primary bg-transparent'
177
- />
178
- <label
179
- htmlFor='remember'
180
- className='text-sm leading-none font-normal text-muted-foreground peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
181
- >
182
- {rememberMeLabel}
183
- </label>
184
- </div>
185
- {onForgotPassword ? (
186
- <button type='button' onClick={onForgotPassword} className={forgotPasswordClassName}>
187
- {forgotPasswordLabel}
188
- </button>
189
- ) : (
190
- <span className={forgotPasswordClassName}>{forgotPasswordLabel}</span>
191
- )}
192
- </div>
193
-
194
- <Button type='submit' className='h-9 w-full cursor-pointer px-4 py-2'>
195
- {signInLabel}
196
- </Button>
197
-
198
- {socialButtons.length > 0 && (
199
- <>
200
- <div className='relative my-6 text-center'>
201
- <div className='absolute inset-0 flex items-center'>
202
- <div className='w-full border-t' />
203
- </div>
204
- <div className='relative flex justify-center text-xs uppercase'>
205
- <span className='bg-card text-muted-foreground px-2'>{orContinueWithLabel}</span>
206
- </div>
207
- </div>
208
-
209
- <div className='grid grid-cols-3 gap-3'>
210
- {socialButtons.map(button => {
211
- const Icon = button.icon
212
- return (
213
- <Button
214
- key={button.name}
215
- type='button'
216
- variant='outline'
217
- aria-label={socialAriaLabelTemplate.replace('{provider}', button.name)}
218
- className='h-9 w-full cursor-pointer px-4 py-2'
219
- onClick={onSocialLogin ? () => onSocialLogin(button.name) : undefined}
220
- >
221
- <Icon className='size-5' aria-hidden='true' />
222
- </Button>
223
- )
224
- })}
225
- </div>
226
- </>
227
- )}
228
- </form>
229
-
230
- <p className='mt-6 flex justify-center gap-1 text-center text-sm'>
231
- <span>{dontHaveAccountLabel}</span>
232
- {onCreateAccount ? (
233
- <button type='button' onClick={onCreateAccount} className={createAccountClassName}>
234
- {createAccountLabel}
235
- </button>
236
- ) : (
237
- <span className={createAccountClassName}>{createAccountLabel}</span>
238
- )}
239
- </p>
240
- </Card>
241
- </div>
242
- </section>
243
- )
244
- }
245
-
246
- export default LoginPage1
@@ -1,207 +0,0 @@
1
- 'use client'
2
-
3
- import { useState, type FormEvent } from 'react'
4
- import { Eye, EyeOff, type LucideIcon } from 'lucide-react'
5
-
6
- import { Button } from '../../../ui/button'
7
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../../ui/card'
8
- import { Input } from '../../../ui/input'
9
- import { Label } from '../../../ui/label'
10
- import { Separator } from '../../../ui/separator'
11
- import { CheckboxCheck } from '../../e-commerce/checkout-forms/checkbox-check'
12
-
13
- export interface LoginPage2SocialButton {
14
- name: string
15
- icon: LucideIcon
16
- }
17
-
18
- export interface LoginPage2Props {
19
- /** Brand text rendered next to the block logo. Omit to hide the logo. */
20
- logoText?: string
21
- welcomeTitleLabel?: string
22
- welcomeDescriptionLabel?: string
23
- emailLabel?: string
24
- emailPlaceholder?: string
25
- passwordLabel?: string
26
- passwordPlaceholder?: string
27
- rememberMeLabel?: string
28
- forgotPasswordLabel?: string
29
- signInLabel?: string
30
- orContinueWithLabel?: string
31
- /** Social login buttons rendered in a 2-column grid. Pass an empty array to hide. */
32
- socialButtons?: LoginPage2SocialButton[]
33
- dontHaveAccountLabel?: string
34
- signUpLabel?: string
35
- showPasswordLabel?: string
36
- hidePasswordLabel?: string
37
- onSubmit?: (email: string, password: string) => void
38
- onForgotPassword?: () => void
39
- onSignUp?: () => void
40
- }
41
-
42
- export function LoginPage2({
43
- logoText,
44
- welcomeTitleLabel = 'Bienvenido de nuevo',
45
- welcomeDescriptionLabel = 'Ingresa tus credenciales para acceder a tu cuenta',
46
- emailLabel = 'Correo electrónico',
47
- emailPlaceholder = 'tucorreo@ejemplo.com',
48
- passwordLabel = 'Contraseña',
49
- passwordPlaceholder = 'Tu contraseña',
50
- rememberMeLabel = 'Recuérdame',
51
- forgotPasswordLabel = '¿Olvidaste tu contraseña?',
52
- signInLabel = 'Iniciar sesión',
53
- orContinueWithLabel = 'O continúa con',
54
- socialButtons = [],
55
- dontHaveAccountLabel = '¿No tienes una cuenta?',
56
- signUpLabel = 'Regístrate aquí',
57
- showPasswordLabel = 'Mostrar contraseña',
58
- hidePasswordLabel = 'Ocultar contraseña',
59
- onSubmit,
60
- onForgotPassword,
61
- onSignUp,
62
- }: LoginPage2Props) {
63
- const [showPassword, setShowPassword] = useState(false)
64
- const [email, setEmail] = useState('')
65
- const [password, setPassword] = useState('')
66
-
67
- const handleSubmit = (event: FormEvent) => {
68
- event.preventDefault()
69
- onSubmit?.(email, password)
70
- }
71
-
72
- return (
73
- <div className='bg-background flex min-h-dvh items-center justify-center p-4 py-12'>
74
- <div className='flex w-full max-w-md flex-col gap-6'>
75
- {logoText && (
76
- <div className='mb-3 flex items-center gap-2 self-center font-medium'>
77
- {/* Logo mark uses the bg-muted token so it inherits the active theme. */}
78
- <div className='bg-muted flex size-10 items-center justify-center rounded-lg'>
79
- <svg width='32' height='32' className='text-primary' viewBox='0 0 32 32' fill='none' xmlns='http://www.w3.org/2000/svg'>
80
- <path
81
- 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'
82
- fill='currentColor'
83
- />
84
- <path
85
- 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'
86
- fill='currentColor'
87
- />
88
- <path
89
- 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'
90
- fill='currentColor'
91
- />
92
- <path
93
- 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'
94
- fill='currentColor'
95
- />
96
- <path
97
- 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'
98
- fill='currentColor'
99
- />
100
- <path d='M21.1694 10.5806L14.5651 17.1849' stroke='currentColor' />
101
- <path d='M22.1694 14.5806L18.5632 18.1868' stroke='currentColor' />
102
- <circle cx='13.1' cy='26.1' r='1.7' stroke='currentColor' />
103
- <circle cx='22.1' cy='26.1' r='1.7' stroke='currentColor' />
104
- </svg>
105
- </div>
106
- <div className='text-2xl font-medium'>{logoText}</div>
107
- </div>
108
- )}
109
- <Card className='w-full max-w-md'>
110
- <CardHeader className='text-center'>
111
- <CardTitle className='text-2xl font-bold text-balance'>{welcomeTitleLabel}</CardTitle>
112
- <CardDescription>{welcomeDescriptionLabel}</CardDescription>
113
- </CardHeader>
114
- <CardContent className='flex flex-col gap-5'>
115
- <form className='flex flex-col gap-5' onSubmit={handleSubmit}>
116
- <div className='flex flex-col gap-3'>
117
- <Label htmlFor='email'>{emailLabel}</Label>
118
- <Input
119
- id='email'
120
- type='email'
121
- placeholder={emailPlaceholder}
122
- value={email}
123
- onChange={event => setEmail(event.target.value)}
124
- className='h-9 text-sm'
125
- />
126
- </div>
127
- <div className='flex flex-col gap-3'>
128
- <Label htmlFor='password'>{passwordLabel}</Label>
129
- <div className='relative'>
130
- <Input
131
- id='password'
132
- type={showPassword ? 'text' : 'password'}
133
- placeholder={passwordPlaceholder}
134
- value={password}
135
- onChange={event => setPassword(event.target.value)}
136
- className='h-9 pe-10 text-sm'
137
- />
138
- <Button
139
- type='button'
140
- variant='ghost'
141
- size='icon'
142
- className='absolute end-0 top-1/2 size-9 -translate-y-1/2 cursor-pointer hover:bg-transparent'
143
- onClick={() => setShowPassword(!showPassword)}
144
- >
145
- {showPassword ? (
146
- <EyeOff className='text-muted-foreground' />
147
- ) : (
148
- <Eye className='text-muted-foreground' />
149
- )}
150
- <span className='sr-only'>{showPassword ? hidePasswordLabel : showPasswordLabel}</span>
151
- </Button>
152
- </div>
153
- </div>
154
- <div className='flex items-center justify-between'>
155
- <div className='flex items-center gap-2'>
156
- <CheckboxCheck id='remember-me' />
157
- <Label
158
- htmlFor='remember-me'
159
- className='text-sm leading-none font-normal text-muted-foreground peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
160
- >
161
- {rememberMeLabel}
162
- </Label>
163
- </div>
164
- <Button variant='link' className='cursor-pointer p-0 text-sm' onClick={onForgotPassword}>
165
- {forgotPasswordLabel}
166
- </Button>
167
- </div>
168
- <Button type='submit' className='h-9 w-full cursor-pointer px-4 py-2'>
169
- {signInLabel}
170
- </Button>
171
- </form>
172
-
173
- {socialButtons.length > 0 && (
174
- <>
175
- {/* Flex divider rather than a rule masked by an opaque label: the mask has to
176
- match whatever surface it sits on, and on a card it did not. */}
177
- <div className='flex items-center gap-3'>
178
- <Separator className='flex-1' />
179
- <span className='text-muted-foreground text-xs uppercase'>{orContinueWithLabel}</span>
180
- <Separator className='flex-1' />
181
- </div>
182
-
183
- <div className='grid grid-cols-2 gap-4'>
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
- {button.name}
188
- </Button>
189
- ))}
190
- </div>
191
- </>
192
- )}
193
-
194
- <div className='text-muted-foreground text-center text-sm'>
195
- {dontHaveAccountLabel}{' '}
196
- <Button variant='link' className='cursor-pointer p-0 text-sm' onClick={onSignUp}>
197
- {signUpLabel}
198
- </Button>
199
- </div>
200
- </CardContent>
201
- </Card>
202
- </div>
203
- </div>
204
- )
205
- }
206
-
207
- export default LoginPage2