@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
@@ -0,0 +1,240 @@
1
+ 'use client'
2
+
3
+ import { useId, useState, type FormEvent } from 'react'
4
+ import { Eye, EyeOff, Lock, 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 { CheckboxCheck } from '../../e-commerce/checkout-forms/checkbox-check'
11
+
12
+ export interface LoginPageFormData {
13
+ email: string
14
+ password: string
15
+ remember: boolean
16
+ }
17
+
18
+ export interface LoginPageProvider {
19
+ /** Must be unique within one `providers` list. */
20
+ id: string
21
+ label: string
22
+ }
23
+
24
+ export interface LoginPageProps {
25
+ presentation: 'card' | 'social'
26
+ logoText?: string
27
+ welcomeTitleLabel?: string
28
+ welcomeDescriptionLabel?: string
29
+ emailLabel?: string
30
+ emailPlaceholder?: string
31
+ passwordLabel?: string
32
+ passwordPlaceholder?: string
33
+ rememberMeLabel?: string
34
+ forgotPasswordLabel?: string
35
+ signInLabel?: string
36
+ orContinueWithLabel?: string
37
+ dontHaveAccountLabel?: string
38
+ createAccountLabel?: string
39
+ showPasswordAriaLabel?: string
40
+ hidePasswordAriaLabel?: string
41
+ providers?: LoginPageProvider[]
42
+ providerIcons?: Record<string, LucideIcon>
43
+ onSubmit?: (data: { email: string; password: string; remember: boolean }) => void
44
+ onForgotPassword?: () => void
45
+ onCreateAccount?: () => void
46
+ onProviderSelect?: (providerId: string) => void
47
+ }
48
+
49
+ export function LoginPage({
50
+ presentation,
51
+ logoText,
52
+ welcomeTitleLabel = 'Bienvenido de nuevo',
53
+ welcomeDescriptionLabel = 'Inicia sesión para continuar',
54
+ emailLabel = 'Correo electrónico',
55
+ emailPlaceholder = 'taller@correo.com',
56
+ passwordLabel = 'Contraseña',
57
+ passwordPlaceholder = '••••••••',
58
+ rememberMeLabel = 'Recuérdame',
59
+ forgotPasswordLabel = '¿Olvidaste tu contraseña?',
60
+ signInLabel = 'Iniciar sesión',
61
+ orContinueWithLabel = 'O continúa con',
62
+ dontHaveAccountLabel = '¿No tienes una cuenta?',
63
+ createAccountLabel = 'Crea una cuenta',
64
+ showPasswordAriaLabel = 'Mostrar contraseña',
65
+ hidePasswordAriaLabel = 'Ocultar contraseña',
66
+ providers = [],
67
+ providerIcons = {},
68
+ onSubmit,
69
+ onForgotPassword,
70
+ onCreateAccount,
71
+ onProviderSelect,
72
+ }: LoginPageProps) {
73
+ if (presentation !== 'card' && presentation !== 'social') {
74
+ throw new Error('LoginPage requires a valid presentation: "card" or "social".')
75
+ }
76
+
77
+ const providerIds = new Set<string>()
78
+ for (const provider of providers ?? []) {
79
+ if (providerIds.has(provider.id)) {
80
+ throw new Error('LoginPage provider ids must be unique.')
81
+ }
82
+ providerIds.add(provider.id)
83
+ }
84
+
85
+ const id = useId()
86
+ const [email, setEmail] = useState('')
87
+ const [password, setPassword] = useState('')
88
+ const [remember, setRemember] = useState(false)
89
+ const [showPassword, setShowPassword] = useState(false)
90
+
91
+ const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
92
+ event.preventDefault()
93
+ onSubmit?.({ email, password, remember })
94
+ }
95
+
96
+ const fields = (
97
+ <form onSubmit={handleSubmit} className='flex flex-col gap-5'>
98
+ <div className='flex flex-col gap-2'>
99
+ <Label htmlFor={`${id}-email`}>{emailLabel}</Label>
100
+ <Input
101
+ id={`${id}-email`}
102
+ type='email'
103
+ autoComplete='email'
104
+ placeholder={emailPlaceholder}
105
+ value={email}
106
+ onChange={event => setEmail(event.target.value)}
107
+ />
108
+ </div>
109
+
110
+ <div className='flex flex-col gap-2'>
111
+ <Label htmlFor={`${id}-password`}>{passwordLabel}</Label>
112
+ <div className='relative'>
113
+ <Input
114
+ id={`${id}-password`}
115
+ type={showPassword ? 'text' : 'password'}
116
+ autoComplete='current-password'
117
+ placeholder={passwordPlaceholder}
118
+ value={password}
119
+ onChange={event => setPassword(event.target.value)}
120
+ className='pe-10'
121
+ />
122
+ <Button
123
+ type='button'
124
+ variant='ghost'
125
+ size='icon'
126
+ aria-label={showPassword ? hidePasswordAriaLabel : showPasswordAriaLabel}
127
+ className='absolute end-0 top-1/2 size-9 -translate-y-1/2 cursor-pointer hover:bg-transparent'
128
+ onClick={() => setShowPassword(value => !value)}
129
+ >
130
+ {showPassword ? <EyeOff aria-hidden='true' /> : <Eye aria-hidden='true' />}
131
+ </Button>
132
+ </div>
133
+ </div>
134
+
135
+ <div className='flex flex-wrap items-center justify-between gap-3'>
136
+ <div className='flex items-center gap-2'>
137
+ <CheckboxCheck
138
+ id={`${id}-remember`}
139
+ checked={remember}
140
+ onCheckedChange={value => setRemember(value === true)}
141
+ />
142
+ <Label htmlFor={`${id}-remember`} className='text-sm font-normal text-muted-foreground'>
143
+ {rememberMeLabel}
144
+ </Label>
145
+ </div>
146
+ {onForgotPassword && (
147
+ <Button type='button' variant='link' className='h-auto cursor-pointer p-0 text-sm' onClick={onForgotPassword}>
148
+ {forgotPasswordLabel}
149
+ </Button>
150
+ )}
151
+ </div>
152
+
153
+ <Button type='submit' className='h-9 w-full cursor-pointer px-4 py-2'>
154
+ {signInLabel}
155
+ </Button>
156
+ </form>
157
+ )
158
+
159
+ const providerSection = providers.length > 0 && onProviderSelect && (
160
+ <div className='flex flex-col gap-4'>
161
+ <div className='flex items-center gap-3' aria-hidden='true'>
162
+ <span className='h-px flex-1 bg-border' />
163
+ <span className='text-muted-foreground text-xs uppercase'>{orContinueWithLabel}</span>
164
+ <span className='h-px flex-1 bg-border' />
165
+ </div>
166
+ <div
167
+ className={presentation === 'card' ? 'grid grid-cols-3 gap-3' : 'grid grid-cols-2 gap-4'}
168
+ role='group'
169
+ aria-label={orContinueWithLabel}
170
+ >
171
+ {providers.map(provider => {
172
+ const Icon = providerIcons[provider.id] as LucideIcon | undefined
173
+ return (
174
+ <Button
175
+ key={provider.id}
176
+ type='button'
177
+ variant='outline'
178
+ aria-label={provider.label}
179
+ className='h-9 w-full cursor-pointer px-3 py-2'
180
+ onClick={() => onProviderSelect(provider.id)}
181
+ >
182
+ {Icon ? <Icon aria-hidden='true' className='size-5' /> : provider.label}
183
+ {presentation === 'social' && Icon ? <span>{provider.label}</span> : null}
184
+ </Button>
185
+ )
186
+ })}
187
+ </div>
188
+ </div>
189
+ )
190
+
191
+ const createAccount = onCreateAccount && (
192
+ <p className='flex justify-center gap-1 text-center text-sm'>
193
+ <span>{dontHaveAccountLabel}</span>
194
+ <Button type='button' variant='link' className='h-auto cursor-pointer p-0 text-sm' onClick={onCreateAccount}>
195
+ {createAccountLabel}
196
+ </Button>
197
+ </p>
198
+ )
199
+
200
+ if (presentation === 'card') {
201
+ return (
202
+ <div className='w-full max-w-md'>
203
+ <Card className='w-full p-8 shadow-2xl'>
204
+ <header className='mb-8 flex flex-col items-center text-center'>
205
+ <div className='bg-secondary mb-4 flex size-14 items-center justify-center rounded-full border'>
206
+ <Lock aria-hidden='true' className='size-6' />
207
+ </div>
208
+ {logoText && <p className='text-muted-foreground mb-2 text-sm font-semibold'>{logoText}</p>}
209
+ <h1 className='mb-2 text-2xl font-bold tracking-tight'>{welcomeTitleLabel}</h1>
210
+ <p className='text-muted-foreground text-sm'>{welcomeDescriptionLabel}</p>
211
+ </header>
212
+ <div className='flex flex-col gap-6'>
213
+ {fields}
214
+ {providerSection}
215
+ {createAccount}
216
+ </div>
217
+ </Card>
218
+ </div>
219
+ )
220
+ }
221
+
222
+ return (
223
+ <div className='flex w-full max-w-md flex-col gap-6'>
224
+ {logoText && <div className='text-center text-2xl font-medium'>{logoText}</div>}
225
+ <Card>
226
+ <CardHeader className='text-center'>
227
+ <CardTitle className='text-2xl font-bold text-balance'>{welcomeTitleLabel}</CardTitle>
228
+ <CardDescription>{welcomeDescriptionLabel}</CardDescription>
229
+ </CardHeader>
230
+ <CardContent className='flex flex-col gap-5'>
231
+ {fields}
232
+ {providerSection}
233
+ {createAccount}
234
+ </CardContent>
235
+ </Card>
236
+ </div>
237
+ )
238
+ }
239
+
240
+ export default LoginPage
@@ -306,14 +306,12 @@ export function ForgotPasswordPage1({
306
306
  }
307
307
 
308
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>
309
+ <div className="w-full max-w-md">
310
+ <Card className="bg-background/80 relative w-full max-w-md ring-0 p-6 shadow-2xl backdrop-blur-md sm:p-8">
311
+ <ForgotPasswordPage1BrandMark />
312
+ {renderStepContent()}
313
+ </Card>
314
+ </div>
317
315
  )
318
316
  }
319
317
 
@@ -81,150 +81,148 @@ export function SignUpPage1({
81
81
  }
82
82
 
83
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>
84
+ <div className='flex w-full max-w-lg flex-col items-center'>
85
+ <Card className='relative w-full max-w-lg p-8 shadow-2xl ring-0'>
86
+ <div className='mb-8 flex flex-col items-center'>
87
+ <div className='my-4 flex justify-center'>
88
+ <div className='bg-secondary relative size-14 rounded-full border'>
89
+ <div className='flex h-full items-center justify-center'>
90
+ <svg width='32' height='32' viewBox='0 0 32 32' fill='none' xmlns='http://www.w3.org/2000/svg'>
91
+ <path
92
+ 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'
93
+ fill='currentColor'
94
+ />
95
+ <path
96
+ 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'
97
+ fill='currentColor'
98
+ />
99
+ <path
100
+ 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'
101
+ fill='currentColor'
102
+ />
103
+ <path
104
+ 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'
105
+ fill='currentColor'
106
+ />
107
+ <path
108
+ 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'
109
+ fill='currentColor'
110
+ />
111
+ <path d='M21.1694 10.5806L14.5651 17.1849' stroke='currentColor' />
112
+ <path d='M22.1694 14.5806L18.5632 18.1868' stroke='currentColor' />
113
+ <circle cx='13.1' cy='26.1' r='1.7' stroke='currentColor' />
114
+ <circle cx='22.1' cy='26.1' r='1.7' stroke='currentColor' />
115
+ </svg>
118
116
  </div>
119
117
  </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
118
  </div>
119
+ {logoText && <div className='text-base font-semibold tracking-tight'>{logoText}</div>}
120
+ <h1 className='mb-2 mt-4 text-center text-2xl font-bold tracking-tight'>{createAccountTitleLabel}</h1>
121
+ <p className='text-muted-foreground text-center text-sm'>{createAccountDescriptionLabel}</p>
122
+ </div>
124
123
 
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>
124
+ <form className='flex flex-col gap-6' onSubmit={handleSubmit}>
125
+ <div className='relative'>
126
+ <Input
127
+ id='fullname'
128
+ name='fullName'
129
+ type='text'
130
+ placeholder={fullNamePlaceholder}
131
+ aria-label={fullNameLabel}
132
+ className='bg-transparent h-9 ps-10 text-sm'
133
+ autoComplete='name'
134
+ required
135
+ />
136
+ <User className='text-muted-foreground absolute start-3 top-1/2 size-5 -translate-y-1/2' />
137
+ </div>
139
138
 
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>
139
+ <div className='relative'>
140
+ <Input
141
+ id='email'
142
+ name='email'
143
+ type='email'
144
+ placeholder={emailPlaceholder}
145
+ aria-label={emailLabel}
146
+ className='bg-transparent h-9 ps-10 text-sm'
147
+ autoComplete='email'
148
+ required
149
+ />
150
+ <Mail className='text-muted-foreground absolute start-3 top-1/2 size-5 -translate-y-1/2' />
151
+ </div>
153
152
 
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>
153
+ <div className='relative'>
154
+ <Input
155
+ id='password'
156
+ name='password'
157
+ type='password'
158
+ placeholder={passwordPlaceholder}
159
+ aria-label={passwordLabel}
160
+ className='bg-transparent h-9 ps-10 text-sm'
161
+ autoComplete='new-password'
162
+ required
163
+ />
164
+ <Lock className='text-muted-foreground absolute start-3 top-1/2 size-5 -translate-y-1/2' />
165
+ </div>
167
166
 
168
- <Button className='h-9 w-full cursor-pointer px-4 py-2' type='submit'>
169
- {createAccountLabel}
170
- </Button>
167
+ <Button className='h-9 w-full cursor-pointer px-4 py-2' type='submit'>
168
+ {createAccountLabel}
169
+ </Button>
171
170
 
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>
171
+ {socialButtons.length > 0 && (
172
+ <>
173
+ <div className='relative my-6 text-center'>
174
+ <div className='absolute inset-0 flex items-center'>
175
+ <div className='w-full border-t' />
181
176
  </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
- ))}
177
+ <div className='relative flex justify-center text-xs uppercase'>
178
+ <span className='bg-card text-muted-foreground px-2'>{orSignUpWithLabel}</span>
190
179
  </div>
191
- </>
192
- )}
193
- </form>
180
+ </div>
194
181
 
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>
182
+ <div className='grid grid-cols-3 gap-3'>
183
+ {socialButtons.map(button => (
184
+ <Button key={button.name} variant='outline' className='h-9 w-full cursor-pointer px-4 py-2'>
185
+ <button.icon />
186
+ <span className='sr-only'>{socialAriaLabel.replace('{name}', button.name)}</span>
187
+ </Button>
188
+ ))}
189
+ </div>
190
+ </>
191
+ )}
192
+ </form>
206
193
 
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>
194
+ <p className='mt-6 flex justify-center gap-1 text-center text-sm'>
195
+ <span>{alreadyHaveAccountLabel}</span>
196
+ {onSignIn ? (
197
+ <button type='button' onClick={onSignIn} className='underline underline-offset-4'>
198
+ {signInLabel}
199
+ </button>
221
200
  ) : (
222
- <span className='hover:text-primary underline underline-offset-4'>{privacyLinkLabel}</span>
201
+ <span className='underline underline-offset-4'>{signInLabel}</span>
223
202
  )}
224
- .
225
203
  </p>
226
- </div>
227
- </section>
204
+ </Card>
205
+
206
+ <p className='text-muted-foreground relative z-10 mt-8 max-w-lg px-8 text-center text-xs text-balance'>
207
+ {termsLabel}{' '}
208
+ {termsHref ? (
209
+ <a href={termsHref} className='hover:text-primary underline underline-offset-4'>
210
+ {termsLinkLabel}
211
+ </a>
212
+ ) : (
213
+ <span className='hover:text-primary underline underline-offset-4'>{termsLinkLabel}</span>
214
+ )}{' '}
215
+ {termsAndLabel}{' '}
216
+ {privacyHref ? (
217
+ <a href={privacyHref} className='hover:text-primary underline underline-offset-4'>
218
+ {privacyLinkLabel}
219
+ </a>
220
+ ) : (
221
+ <span className='hover:text-primary underline underline-offset-4'>{privacyLinkLabel}</span>
222
+ )}
223
+ .
224
+ </p>
225
+ </div>
228
226
  )
229
227
  }
230
228
 
@@ -162,7 +162,7 @@ export function Verify1({
162
162
 
163
163
  if (verified) {
164
164
  return (
165
- <div className='bg-muted/30 relative flex min-h-dvh w-full items-center justify-center overflow-hidden px-4 py-12'>
165
+ <div className='relative w-full max-w-md'>
166
166
  {/* Fired once, over the whole field rather than the card, so the paper has somewhere
167
167
  to fall. The success is stated in the heading below; this only decorates it. */}
168
168
  <Confetti fire origin={{ x: 0.5, y: 0.5 }} />
@@ -187,7 +187,7 @@ export function Verify1({
187
187
  }
188
188
 
189
189
  return (
190
- <div className='bg-muted/30 flex min-h-dvh w-full items-center justify-center px-4 py-12'>
190
+ <div className='w-full max-w-md'>
191
191
  <Card className='w-full max-w-md gap-6 py-8 shadow-lg'>
192
192
  <CardHeader className='gap-2 px-8 text-center'>
193
193
  <span
@@ -0,0 +1,41 @@
1
+ // @vitest-environment jsdom
2
+ //
3
+ // Invariant: an auth composition renders into the host page and never owns it.
4
+ // A page (route shell, or a template the host owns) supplies the canvas: height,
5
+ // backdrop, centring. Keeping this assertion next to the blocks means a new auth
6
+ // surface cannot quietly reintroduce a second viewport owner.
7
+ import { beforeAll, describe, expect, it } from 'vitest';
8
+ import { render, screen } from '@testing-library/react';
9
+ import { LoginPage } from './login-forms/login-page';
10
+ import { SignUpPage1 } from './signup-forms/signup-page-1';
11
+ import { ForgotPasswordPage1 } from './password-recovery/forgot-password-page-1';
12
+ import { Verify1 } from './verification/verify-1';
13
+
14
+ beforeAll(() => {
15
+ Object.assign(SVGElement.prototype, { getTotalLength: () => 15 });
16
+ });
17
+
18
+ const PAGE_SHELL = /min-h-dvh|min-h-screen|h-dvh|h-screen|bg-linear-to-br/;
19
+
20
+ const surfaces = [
21
+ ['LoginPage (card)', () => <LoginPage presentation="card" />],
22
+ ['LoginPage (social)', () => <LoginPage presentation="social" />],
23
+ ['SignUpPage1', () => <SignUpPage1 />],
24
+ ['ForgotPasswordPage1', () => <ForgotPasswordPage1 />],
25
+ ['Verify1', () => <Verify1 />],
26
+ ] as const;
27
+
28
+ describe.each(surfaces)('%s', (_name, renderBlock) => {
29
+ it('does not declare a page landmark', () => {
30
+ render(renderBlock());
31
+ expect(screen.queryByRole('main')).toBeNull();
32
+ });
33
+
34
+ it('renders into a neutral content-sized root', () => {
35
+ const { container } = render(renderBlock());
36
+ const root = container.firstElementChild;
37
+ expect(root?.tagName).toBe('DIV');
38
+ expect(root?.className).not.toMatch(PAGE_SHELL);
39
+ expect(container.innerHTML).not.toMatch(PAGE_SHELL);
40
+ });
41
+ });