@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
@@ -1,298 +0,0 @@
1
- 'use client'
2
-
3
- import { useState } from 'react'
4
- import { SlidersHorizontal } from 'lucide-react'
5
-
6
- import { Badge } from '../../../ui/badge'
7
- import { Button } from '../../../ui/button'
8
- import {
9
- Empty,
10
- EmptyContent,
11
- EmptyDescription,
12
- EmptyHeader,
13
- EmptyTitle,
14
- } from '../../../ui/empty'
15
-
16
- import { CategoryFilter4ChipRail } from './category-filter-4-chip-rail'
17
- import { CategoryFilter4Drawer } from './category-filter-4-drawer'
18
- import { CategoryFilter4Sidebar } from './category-filter-4-sidebar'
19
- import { CategoryFilter4SortSheet } from './category-filter-4-sort-sheet'
20
- import { emptyFilters, useCategoryFilter4State } from './category-filter-4-state'
21
-
22
- export interface CategoryFilter4SortOption {
23
- value: string
24
- label: string
25
- }
26
-
27
- export interface CategoryFilter4FacetOption {
28
- value: string
29
- label: string
30
- }
31
-
32
- export interface CategoryFilter4RangeBand {
33
- value: string
34
- label: string
35
- min: number
36
- max: number
37
- }
38
-
39
- export interface CategoryFilter4Facets {
40
- sortOptions: CategoryFilter4SortOption[]
41
- frameSizes: string[]
42
- modelTypes: CategoryFilter4FacetOption[]
43
- rangeBands: CategoryFilter4RangeBand[]
44
- featureOptions: CategoryFilter4FacetOption[]
45
- }
46
-
47
- export interface CategoryFilter4FilterState {
48
- modelTypes: string[]
49
- priceMax: number
50
- rangeBands: string[]
51
- frameSizes: string[]
52
- colourways: string[]
53
- features: string[]
54
- }
55
-
56
- export interface CategoryFilter4Product {
57
- id: string
58
- name: string
59
- modelType: string
60
- price: number
61
- rangeKm: number
62
- frameSize: string
63
- colourway: string
64
- features: string[]
65
- rating: number
66
- }
67
-
68
- export interface CategoryFilter4Props {
69
- products: CategoryFilter4Product[]
70
- facets: CategoryFilter4Facets
71
- colourNames: Record<string, string>
72
- fallbackImage: string
73
- maxPrice: number
74
- filtersLabel?: string
75
- sortLabel?: string
76
- sortByLabel?: string
77
- modelTypeLabel?: string
78
- rangeLabel?: string
79
- frameSizeLabel?: string
80
- colourLabel?: string
81
- featuresLabel?: string
82
- resultsCountLabel?: (count: number) => string
83
- clearAllLabel?: string
84
- showResultsLabel?: (count: number) => string
85
- noResultsLabel?: string
86
- noResultsTitle?: string
87
- noResultsDescription?: string
88
- noResultsCtaLabel?: string
89
- onFiltersChange?: (filters: CategoryFilter4FilterState) => void
90
- onSortChange?: (sort: string) => void
91
- onProductSelect?: (product: CategoryFilter4Product) => void
92
- }
93
-
94
- function activeFilterCount(
95
- filters: CategoryFilter4FilterState,
96
- empty: CategoryFilter4FilterState
97
- ): number {
98
- return (
99
- filters.modelTypes.length +
100
- filters.rangeBands.length +
101
- filters.frameSizes.length +
102
- filters.colourways.length +
103
- filters.features.length +
104
- (filters.priceMax < empty.priceMax ? 1 : 0)
105
- )
106
- }
107
-
108
- export function CategoryFilter4({
109
- products,
110
- facets,
111
- colourNames,
112
- fallbackImage,
113
- maxPrice,
114
- filtersLabel = 'Filtros',
115
- sortLabel = 'Ordenar',
116
- sortByLabel = 'Ordenar por',
117
- modelTypeLabel = 'Tipo de modelo',
118
- rangeLabel = 'Autonomía',
119
- frameSizeLabel = 'Talla de cuadro',
120
- colourLabel = 'Color',
121
- featuresLabel = 'Características',
122
- resultsCountLabel = count => `${count} ${count === 1 ? 'producto' : 'productos'}`,
123
- clearAllLabel = 'Borrar todos',
124
- showResultsLabel = count => `Mostrar ${count} ${count === 1 ? 'producto' : 'productos'}`,
125
- noResultsLabel = 'Sin coincidencias',
126
- noResultsTitle = 'No hay productos que coincidan con estos filtros',
127
- noResultsDescription = 'Prueba a relajar alguno de los filtros aplicados.',
128
- noResultsCtaLabel = 'Borrar todos los filtros',
129
- onFiltersChange,
130
- onSortChange,
131
- onProductSelect,
132
- }: CategoryFilter4Props) {
133
- const empty = emptyFilters(maxPrice)
134
- const { filters, setFilters, sort, setSort, filtered, countWith } = useCategoryFilter4State(
135
- products,
136
- facets,
137
- maxPrice
138
- )
139
- const [drawerOpen, setDrawerOpen] = useState(false)
140
- const [sortOpen, setSortOpen] = useState(false)
141
-
142
- const handleFiltersChange = (next: CategoryFilter4FilterState) => {
143
- setFilters(next)
144
- onFiltersChange?.(next)
145
- }
146
-
147
- const handleSortChange = (next: string) => {
148
- setSort(next)
149
- onSortChange?.(next)
150
- }
151
-
152
- const count = activeFilterCount(filters, empty)
153
- const selectedSortLabel = facets.sortOptions.find(option => option.value === sort)?.label ?? sortByLabel
154
-
155
- return (
156
- <div className="w-full">
157
- <div className="bg-background/95 sticky top-0 z-10 flex items-center justify-between gap-2 border-b px-4 py-2 backdrop-blur sm:px-6 lg:hidden">
158
- <Button variant="outline" size="sm" onClick={() => setDrawerOpen(true)}>
159
- <SlidersHorizontal className="size-3.5" />
160
- {filtersLabel}
161
- {count > 0 ? <Badge className="ml-1 size-4 justify-center rounded-full p-0 text-[10px]">{count}</Badge> : null}
162
- </Button>
163
- <Button variant="ghost" size="sm" onClick={() => setSortOpen(true)}>
164
- {sortLabel}: {selectedSortLabel}
165
- </Button>
166
- <span aria-live="polite" className="text-muted-foreground text-xs tabular-nums">
167
- {resultsCountLabel(filtered.length)}
168
- </span>
169
- </div>
170
-
171
- <div className="px-4 py-2 sm:px-6 lg:hidden">
172
- <CategoryFilter4ChipRail
173
- filters={filters}
174
- onChange={handleFiltersChange}
175
- empty={empty}
176
- clearAllLabel={clearAllLabel}
177
- />
178
- </div>
179
-
180
- <div className="flex gap-8 px-4 py-6 sm:px-6">
181
- <div className="hidden lg:block">
182
- <CategoryFilter4Sidebar
183
- filters={filters}
184
- onChange={handleFiltersChange}
185
- countWith={countWith}
186
- facets={facets}
187
- colourNames={colourNames}
188
- modelTypeLabel={modelTypeLabel}
189
- rangeLabel={rangeLabel}
190
- frameSizeLabel={frameSizeLabel}
191
- colourLabel={colourLabel}
192
- featuresLabel={featuresLabel}
193
- />
194
- </div>
195
-
196
- <div className="flex-1">
197
- <div className="hidden items-center justify-between gap-2 lg:flex">
198
- <CategoryFilter4ChipRail
199
- filters={filters}
200
- onChange={setFilters}
201
- empty={empty}
202
- clearAllLabel={clearAllLabel}
203
- />
204
- <span aria-live="polite" className="text-muted-foreground text-xs tabular-nums">
205
- {resultsCountLabel(filtered.length)}
206
- </span>
207
- </div>
208
-
209
- {filtered.length === 0 ? (
210
- <Empty className="mt-6">
211
- <EmptyHeader>
212
- <EmptyTitle>{noResultsTitle}</EmptyTitle>
213
- <EmptyDescription>{noResultsDescription}</EmptyDescription>
214
- </EmptyHeader>
215
- <EmptyContent>
216
- <Button size="sm" onClick={() => handleFiltersChange(empty)}>
217
- {noResultsCtaLabel}
218
- </Button>
219
- </EmptyContent>
220
- </Empty>
221
- ) : (
222
- <div className="mt-4 grid grid-cols-2 gap-4 sm:grid-cols-2 md:grid-cols-3">
223
- {filtered.map(product => {
224
- const card = (
225
- <>
226
- {/*
227
- An empty grey square told the reader nothing and made a filtered grid look
228
- broken rather than filtered. The alt is empty on purpose: the name and price
229
- directly beneath already carry everything the picture would say.
230
- */}
231
- <img
232
- src={fallbackImage || undefined}
233
- alt=""
234
- loading="lazy"
235
- decoding="async"
236
- className="bg-muted aspect-square w-full rounded-lg object-cover"
237
- />
238
- <p className="text-sm font-medium">{product.name}</p>
239
- <p className="text-muted-foreground text-xs tabular-nums">
240
- ${product.price.toLocaleString()} · {product.rangeKm} km
241
- </p>
242
- </>
243
- )
244
-
245
- return onProductSelect ? (
246
- <button
247
- key={product.id}
248
- type="button"
249
- onClick={() => onProductSelect(product)}
250
- className="flex cursor-pointer flex-col gap-1.5 text-left"
251
- >
252
- {card}
253
- </button>
254
- ) : (
255
- <div key={product.id} className="flex flex-col gap-1.5">
256
- {card}
257
- </div>
258
- )
259
- })}
260
- </div>
261
- )}
262
- </div>
263
- </div>
264
-
265
- <CategoryFilter4Drawer
266
- open={drawerOpen}
267
- onOpenChange={setDrawerOpen}
268
- filters={filters}
269
- onChange={handleFiltersChange}
270
- countWith={countWith}
271
- resultCount={filtered.length}
272
- empty={empty}
273
- facets={facets}
274
- colourNames={colourNames}
275
- filtersLabel={filtersLabel}
276
- modelTypeLabel={modelTypeLabel}
277
- rangeLabel={rangeLabel}
278
- frameSizeLabel={frameSizeLabel}
279
- colourLabel={colourLabel}
280
- featuresLabel={featuresLabel}
281
- clearAllLabel={clearAllLabel}
282
- showResultsLabel={showResultsLabel}
283
- noResultsLabel={noResultsLabel}
284
- noResultsDescription={noResultsDescription}
285
- />
286
- <CategoryFilter4SortSheet
287
- open={sortOpen}
288
- onOpenChange={setSortOpen}
289
- sort={sort}
290
- onSortChange={handleSortChange}
291
- sortOptions={facets.sortOptions}
292
- sortByLabel={sortByLabel}
293
- />
294
- </div>
295
- )
296
- }
297
-
298
- export default CategoryFilter4
@@ -1,47 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { formatCardNumber, formatExpiryDate } from './card-formatting'
3
-
4
- describe('formatCardNumber', () => {
5
- it('strips non-digit characters', () => {
6
- expect(formatCardNumber('4242 4242 4242 4242')).toBe('4242 4242 4242 4242')
7
- })
8
-
9
- it('groups digits in blocks of four', () => {
10
- expect(formatCardNumber('4242424242424242')).toBe('4242 4242 4242 4242')
11
- })
12
-
13
- it('caps at sixteen digits', () => {
14
- expect(formatCardNumber('42424242424242421234')).toBe('4242 4242 4242 4242')
15
- })
16
-
17
- it('does not add a trailing group separator', () => {
18
- expect(formatCardNumber('42424242')).toBe('4242 4242')
19
- })
20
-
21
- it('returns an empty string for no input', () => {
22
- expect(formatCardNumber('')).toBe('')
23
- })
24
- })
25
-
26
- describe('formatExpiryDate', () => {
27
- it('masks two-digit months without a slash', () => {
28
- expect(formatExpiryDate('12')).toBe('12')
29
- })
30
-
31
- it('masks MM/YY once a third digit is typed', () => {
32
- expect(formatExpiryDate('122')).toBe('12/2')
33
- expect(formatExpiryDate('1226')).toBe('12/26')
34
- })
35
-
36
- it('strips non-digit characters', () => {
37
- expect(formatExpiryDate('12/26')).toBe('12/26')
38
- })
39
-
40
- it('caps at four digits', () => {
41
- expect(formatExpiryDate('12261234')).toBe('12/26')
42
- })
43
-
44
- it('returns an empty string for no input', () => {
45
- expect(formatExpiryDate('')).toBe('')
46
- })
47
- })
@@ -1,9 +0,0 @@
1
- export function formatCardNumber(value: string, maxDigits = 16): string {
2
- const digits = value.replace(/\D/g, '').slice(0, maxDigits)
3
- return digits.replace(/(\d{4})(?=\d)/g, '$1 ')
4
- }
5
-
6
- export function formatExpiryDate(value: string, maxDigits = 4): string {
7
- const digits = value.replace(/\D/g, '').slice(0, maxDigits)
8
- return digits.length > 2 ? `${digits.slice(0, 2)}/${digits.slice(2)}` : digits
9
- }
@@ -1,96 +0,0 @@
1
- 'use client'
2
-
3
- import { Field, FieldGroup, FieldLabel } from '../../../ui/field'
4
- import { Input } from '../../../ui/input'
5
- import { CheckoutFieldFeedback, checkoutFieldAria } from './checkout-field-feedback'
6
- import type {
7
- CheckoutForm1ChangeHandler,
8
- CheckoutForm1FieldDescriptions,
9
- CheckoutForm1FieldErrors,
10
- CheckoutForm1FormData,
11
- } from './checkout-form-1'
12
-
13
- export interface CheckoutForm1ContactStepProps {
14
- formData: CheckoutForm1FormData
15
- onChange: CheckoutForm1ChangeHandler
16
- fieldErrors?: CheckoutForm1FieldErrors
17
- fieldDescriptions?: CheckoutForm1FieldDescriptions
18
- }
19
-
20
- export function CheckoutForm1ContactStep({
21
- formData,
22
- onChange,
23
- fieldErrors,
24
- fieldDescriptions,
25
- }: CheckoutForm1ContactStepProps) {
26
- return (
27
- <FieldGroup className="grid gap-4">
28
- <Field data-invalid={!!fieldErrors?.email?.length}>
29
- <FieldLabel htmlFor="checkout-email">Email</FieldLabel>
30
- <Input
31
- id="checkout-email"
32
- type="text"
33
- inputMode="email"
34
- placeholder="john@example.com"
35
- value={formData.email}
36
- onChange={(e) => onChange('email', e.target.value)}
37
- {...checkoutFieldAria('checkout-email', fieldErrors?.email)}
38
- />
39
- <CheckoutFieldFeedback
40
- id="checkout-email"
41
- errors={fieldErrors?.email}
42
- description={fieldDescriptions?.email}
43
- />
44
- </Field>
45
- <div className="grid gap-4 md:grid-cols-2">
46
- <Field data-invalid={!!fieldErrors?.firstName?.length}>
47
- <FieldLabel htmlFor="checkout-first-name">First name</FieldLabel>
48
- <Input
49
- id="checkout-first-name"
50
- placeholder="John"
51
- value={formData.firstName}
52
- onChange={(e) => onChange('firstName', e.target.value)}
53
- {...checkoutFieldAria('checkout-first-name', fieldErrors?.firstName)}
54
- />
55
- <CheckoutFieldFeedback
56
- id="checkout-first-name"
57
- errors={fieldErrors?.firstName}
58
- description={fieldDescriptions?.firstName}
59
- />
60
- </Field>
61
- <Field data-invalid={!!fieldErrors?.lastName?.length}>
62
- <FieldLabel htmlFor="checkout-last-name">Last name</FieldLabel>
63
- <Input
64
- id="checkout-last-name"
65
- placeholder="Doe"
66
- value={formData.lastName}
67
- onChange={(e) => onChange('lastName', e.target.value)}
68
- {...checkoutFieldAria('checkout-last-name', fieldErrors?.lastName)}
69
- />
70
- <CheckoutFieldFeedback
71
- id="checkout-last-name"
72
- errors={fieldErrors?.lastName}
73
- description={fieldDescriptions?.lastName}
74
- />
75
- </Field>
76
- </div>
77
- <Field data-invalid={!!fieldErrors?.phone?.length}>
78
- <FieldLabel htmlFor="checkout-phone">Phone (optional)</FieldLabel>
79
- <Input
80
- id="checkout-phone"
81
- type="text"
82
- inputMode="tel"
83
- placeholder="+1 (555) 000-0000"
84
- value={formData.phone}
85
- onChange={(e) => onChange('phone', e.target.value)}
86
- {...checkoutFieldAria('checkout-phone', fieldErrors?.phone)}
87
- />
88
- <CheckoutFieldFeedback
89
- id="checkout-phone"
90
- errors={fieldErrors?.phone}
91
- description={fieldDescriptions?.phone}
92
- />
93
- </Field>
94
- </FieldGroup>
95
- )
96
- }