@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,550 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useRef, useState } from 'react'
4
+ import { Lock } from 'lucide-react'
5
+ import { Button } from '../../../ui/button'
6
+ import { Card, CardContent } from '../../../ui/card'
7
+ import { cn } from '../../../lib/utils'
8
+ import { CheckoutContactStep } from './checkout-contact-step'
9
+ import { CheckoutShippingStep } from './checkout-shipping-step'
10
+ import { CheckoutPaymentStep } from './checkout-payment-step'
11
+ import { CheckoutReviewStep } from './checkout-review-step'
12
+ import { CheckoutOrderSummaryPanel } from './checkout-order-summary'
13
+ import { CheckoutProgress } from './checkout-progress'
14
+ import type { Money, PaymentMethod } from '@thorprovider/types'
15
+ import type { CheckoutFieldErrors as GenericCheckoutFieldErrors } from './checkout-form-types'
16
+
17
+ /**
18
+ * The canonical checkout is a page-level unit. The host owns the commercial
19
+ * state (values, selections, totals, submission); the block owns only the
20
+ * ephemeral step index of the `stepper` presentation.
21
+ */
22
+ export type CheckoutPresentation = 'single-page' | 'stepper'
23
+
24
+ export type CheckoutStepId = 'contact' | 'shipping' | 'payment' | 'review'
25
+
26
+ /** Flat, controlled form values. No card data: payment entry belongs to the host provider surface. */
27
+ export interface CheckoutValues {
28
+ email: string
29
+ firstName: string
30
+ lastName: string
31
+ phone: string
32
+ address: string
33
+ city: string
34
+ state: string
35
+ zipCode: string
36
+ country: string
37
+ paymentMethod: string
38
+ saveInfo: boolean
39
+ newsletter: boolean
40
+ promoCode: string
41
+ }
42
+
43
+ export type CheckoutValuesFieldErrors = GenericCheckoutFieldErrors<keyof CheckoutValues>
44
+
45
+ export type CheckoutFieldDescriptions = Partial<Record<keyof CheckoutValues, string>>
46
+
47
+ export interface CheckoutOrderLine {
48
+ /** Backend line identity — the stable React key. */
49
+ id: string | number
50
+ name: string
51
+ variant?: string
52
+ quantity: number
53
+ /** Backend-authoritative line total; rendered verbatim. */
54
+ lineTotal?: Money
55
+ /** Optional unit price shown as secondary text; never multiplied. */
56
+ unitPrice?: Money
57
+ /** Optional; an absent image is omitted rather than serialized as `""`. */
58
+ image?: string
59
+ }
60
+
61
+ export interface CheckoutTotals {
62
+ subtotal: Money
63
+ discount?: Money
64
+ tax?: Money
65
+ shipping?: Money
66
+ total: Money
67
+ }
68
+
69
+ export interface CheckoutOrderSummary {
70
+ items: CheckoutOrderLine[]
71
+ /** Backend-authoritative totals; rendered verbatim. */
72
+ totals?: CheckoutTotals
73
+ }
74
+
75
+ export type CheckoutTotalsState = 'idle' | 'pending' | 'error'
76
+
77
+ export interface CheckoutCountryOption {
78
+ value: string
79
+ label: string
80
+ }
81
+
82
+ export interface CheckoutShippingOption {
83
+ /** Backend shipping method id — the controlled selection identity. */
84
+ id: string
85
+ name: string
86
+ price?: Money
87
+ description?: string
88
+ estimatedDays?: string
89
+ }
90
+
91
+ export type CheckoutChangeHandler = (field: keyof CheckoutValues, value: string | boolean) => void
92
+
93
+ /**
94
+ * Every user-facing string is overridable. Neutral English defaults ship with
95
+ * the block; the host supplies its own copy through this object.
96
+ */
97
+ export interface CheckoutLabels {
98
+ title: string
99
+ description: string
100
+ orderSummaryTitle: string
101
+ stepProgressLabel: string
102
+ contactStepName: string
103
+ shippingStepName: string
104
+ paymentStepName: string
105
+ reviewStepName: string
106
+ contactHeading: string
107
+ contactDescription: string
108
+ shippingHeading: string
109
+ shippingDescription: string
110
+ paymentHeading: string
111
+ paymentDescription: string
112
+ reviewHeading: string
113
+ reviewDescription: string
114
+ emailLabel: string
115
+ firstNameLabel: string
116
+ lastNameLabel: string
117
+ phoneLabel: string
118
+ addressLabel: string
119
+ cityLabel: string
120
+ stateLabel: string
121
+ zipCodeLabel: string
122
+ countryLabel: string
123
+ countryPlaceholder: string
124
+ shippingMethodLegend: string
125
+ paymentMethodLegend: string
126
+ noPaymentMethodsLabel: string
127
+ unavailableMethodLabel: string
128
+ requiresSetupLabel: string
129
+ saveInfoLabel: string
130
+ newsletterLabel: string
131
+ promoCodeLabel: string
132
+ promoPlaceholder: string
133
+ promoApplyLabel: string
134
+ subtotalLabel: string
135
+ discountLabel: string
136
+ taxLabel: string
137
+ shippingLabel: string
138
+ totalLabel: string
139
+ eachLabel: string
140
+ totalsPendingLabel: string
141
+ totalsErrorLabel: string
142
+ totalsUnavailableLabel: string
143
+ amountUnavailableLabel: string
144
+ retryLabel: string
145
+ reviewContactLabel: string
146
+ reviewShipToLabel: string
147
+ reviewPaymentLabel: string
148
+ reviewShippingLabel: string
149
+ reviewEmptyLabel: string
150
+ backLabel: string
151
+ continueLabel: string
152
+ completeOrderLabel: string
153
+ processingLabel: string
154
+ submitErrorActionLabel: string
155
+ }
156
+
157
+ const DEFAULT_LABELS: CheckoutLabels = {
158
+ title: 'Checkout',
159
+ description: '',
160
+ orderSummaryTitle: 'Order summary',
161
+ stepProgressLabel: 'Checkout progress',
162
+ contactStepName: 'Contact',
163
+ shippingStepName: 'Shipping',
164
+ paymentStepName: 'Payment',
165
+ reviewStepName: 'Review',
166
+ contactHeading: 'Contact information',
167
+ contactDescription: 'We use these details to keep you informed about your order.',
168
+ shippingHeading: 'Shipping address',
169
+ shippingDescription: 'Tell us where the order should be delivered.',
170
+ paymentHeading: 'Payment',
171
+ paymentDescription: 'Choose how you want to pay for this order.',
172
+ reviewHeading: 'Review your order',
173
+ reviewDescription: 'Check the details below before placing the order.',
174
+ emailLabel: 'Email',
175
+ firstNameLabel: 'First name',
176
+ lastNameLabel: 'Last name',
177
+ phoneLabel: 'Phone (optional)',
178
+ addressLabel: 'Street address',
179
+ cityLabel: 'City',
180
+ stateLabel: 'State / Province',
181
+ zipCodeLabel: 'ZIP / Postal code',
182
+ countryLabel: 'Country',
183
+ countryPlaceholder: 'Select a country',
184
+ shippingMethodLegend: 'Shipping method',
185
+ paymentMethodLegend: 'Payment method',
186
+ noPaymentMethodsLabel: 'No payment methods are available for this region.',
187
+ unavailableMethodLabel: 'Unavailable',
188
+ requiresSetupLabel: 'Requires additional setup',
189
+ saveInfoLabel: 'Save this information for next time',
190
+ newsletterLabel: 'Email me about news and offers',
191
+ promoCodeLabel: 'Promotional code',
192
+ promoPlaceholder: 'Enter the code',
193
+ promoApplyLabel: 'Apply',
194
+ subtotalLabel: 'Subtotal',
195
+ discountLabel: 'Discount',
196
+ taxLabel: 'Tax',
197
+ shippingLabel: 'Shipping',
198
+ totalLabel: 'Total',
199
+ eachLabel: 'each',
200
+ totalsPendingLabel: 'Updating totals…',
201
+ totalsErrorLabel: 'We could not update the order totals.',
202
+ totalsUnavailableLabel: 'Totals are unavailable.',
203
+ amountUnavailableLabel: '—',
204
+ retryLabel: 'Retry',
205
+ reviewContactLabel: 'Contact',
206
+ reviewShipToLabel: 'Ship to',
207
+ reviewPaymentLabel: 'Payment',
208
+ reviewShippingLabel: 'Shipping method',
209
+ reviewEmptyLabel: 'No details provided yet.',
210
+ backLabel: 'Back',
211
+ continueLabel: 'Continue',
212
+ completeOrderLabel: 'Place order',
213
+ processingLabel: 'Placing order…',
214
+ submitErrorActionLabel: 'Try again',
215
+ }
216
+
217
+ const BASE_STEPS: CheckoutStepId[] = ['contact', 'shipping', 'payment']
218
+
219
+ const STEP_COPY: Record<
220
+ CheckoutStepId,
221
+ { name: keyof CheckoutLabels; heading: keyof CheckoutLabels; description: keyof CheckoutLabels }
222
+ > = {
223
+ contact: {
224
+ name: 'contactStepName',
225
+ heading: 'contactHeading',
226
+ description: 'contactDescription',
227
+ },
228
+ shipping: {
229
+ name: 'shippingStepName',
230
+ heading: 'shippingHeading',
231
+ description: 'shippingDescription',
232
+ },
233
+ payment: {
234
+ name: 'paymentStepName',
235
+ heading: 'paymentHeading',
236
+ description: 'paymentDescription',
237
+ },
238
+ review: {
239
+ name: 'reviewStepName',
240
+ heading: 'reviewHeading',
241
+ description: 'reviewDescription',
242
+ },
243
+ }
244
+
245
+ export interface CheckoutProps {
246
+ presentation: CheckoutPresentation
247
+ /** Appends a confirmation step (`stepper`) or section (`single-page`) before submission. */
248
+ showReviewStep?: boolean
249
+ values: CheckoutValues
250
+ onChange: CheckoutChangeHandler
251
+ orderSummary?: CheckoutOrderSummary
252
+ totalsState?: CheckoutTotalsState
253
+ onRetryTotals?: () => void
254
+ countries?: CheckoutCountryOption[]
255
+ shippingOptions?: CheckoutShippingOption[]
256
+ selectedShippingOptionId?: string
257
+ onSelectShippingOption?: (id: string) => void
258
+ paymentMethods?: PaymentMethod[]
259
+ onApplyPromo?: (code: string) => void
260
+ promoPending?: boolean
261
+ /** Optional trust copy; rendered only when supplied. */
262
+ trustNotes?: string[]
263
+ labels?: Partial<CheckoutLabels>
264
+ /** Override money formatting (locale, currency). Defaults to `Money.currencyCode`. */
265
+ formatMoney?: (money: Money) => string
266
+ fieldErrors?: CheckoutValuesFieldErrors
267
+ fieldDescriptions?: CheckoutFieldDescriptions
268
+ /** Consulted before advancing and for every applicable step before submitting. */
269
+ validateStep?: (step: CheckoutStepId) => boolean
270
+ /** Notification only — never an authoritative controlled value. */
271
+ onStepChange?: (step: CheckoutStepId) => void
272
+ onSubmit?: (values: CheckoutValues) => void
273
+ submitting?: boolean
274
+ submitError?: string
275
+ onRetrySubmit?: () => void
276
+ }
277
+
278
+ export function Checkout({
279
+ presentation,
280
+ showReviewStep = false,
281
+ values,
282
+ onChange,
283
+ orderSummary,
284
+ totalsState = 'idle',
285
+ onRetryTotals,
286
+ countries,
287
+ shippingOptions,
288
+ selectedShippingOptionId,
289
+ onSelectShippingOption,
290
+ paymentMethods = [],
291
+ onApplyPromo,
292
+ promoPending = false,
293
+ trustNotes,
294
+ labels,
295
+ formatMoney,
296
+ fieldErrors,
297
+ fieldDescriptions,
298
+ validateStep,
299
+ onStepChange,
300
+ onSubmit,
301
+ submitting = false,
302
+ submitError,
303
+ onRetrySubmit,
304
+ }: CheckoutProps) {
305
+ const resolved: CheckoutLabels = { ...DEFAULT_LABELS, ...labels }
306
+ const steps = showReviewStep ? [...BASE_STEPS, 'review' as const] : BASE_STEPS
307
+ const [stepIndex, setStepIndex] = useState(0)
308
+ const activeStep = steps[Math.min(stepIndex, steps.length - 1)]
309
+
310
+ const headingRef = useRef<HTMLHeadingElement>(null)
311
+ const hasMounted = useRef(false)
312
+
313
+ useEffect(() => {
314
+ if (presentation !== 'stepper') return
315
+ if (!hasMounted.current) {
316
+ hasMounted.current = true
317
+ return
318
+ }
319
+ headingRef.current?.focus()
320
+ }, [presentation, activeStep])
321
+
322
+ const moveTo = (nextIndex: number) => {
323
+ const next = steps[nextIndex]
324
+ if (!next) return
325
+ setStepIndex(nextIndex)
326
+ onStepChange?.(next)
327
+ }
328
+
329
+ const handleContinue = () => {
330
+ if (validateStep && !validateStep(activeStep)) return
331
+ moveTo(stepIndex + 1)
332
+ }
333
+
334
+ const handleBack = () => {
335
+ if (stepIndex === 0) return
336
+ moveTo(stepIndex - 1)
337
+ }
338
+
339
+ const handleSubmit = () => {
340
+ if (submitting) return
341
+ if (validateStep && !steps.every((step) => validateStep(step))) return
342
+ onSubmit?.(values)
343
+ }
344
+
345
+ const renderStep = (step: CheckoutStepId) => {
346
+ switch (step) {
347
+ case 'contact':
348
+ return (
349
+ <CheckoutContactStep
350
+ values={values}
351
+ onChange={onChange}
352
+ labels={resolved}
353
+ showSaveInfo={labels?.saveInfoLabel !== undefined}
354
+ showNewsletter={labels?.newsletterLabel !== undefined}
355
+ fieldErrors={fieldErrors}
356
+ fieldDescriptions={fieldDescriptions}
357
+ />
358
+ )
359
+ case 'shipping':
360
+ return (
361
+ <CheckoutShippingStep
362
+ values={values}
363
+ onChange={onChange}
364
+ labels={resolved}
365
+ countries={countries}
366
+ shippingOptions={shippingOptions}
367
+ selectedShippingOptionId={selectedShippingOptionId}
368
+ onSelectShippingOption={onSelectShippingOption}
369
+ formatMoney={formatMoney}
370
+ fieldErrors={fieldErrors}
371
+ fieldDescriptions={fieldDescriptions}
372
+ />
373
+ )
374
+ case 'payment':
375
+ return (
376
+ <CheckoutPaymentStep
377
+ values={values}
378
+ onChange={onChange}
379
+ labels={resolved}
380
+ paymentMethods={paymentMethods}
381
+ fieldErrors={fieldErrors}
382
+ fieldDescriptions={fieldDescriptions}
383
+ />
384
+ )
385
+ case 'review':
386
+ return (
387
+ <CheckoutReviewStep
388
+ values={values}
389
+ labels={resolved}
390
+ selectedShippingOptionId={selectedShippingOptionId}
391
+ shippingOptions={shippingOptions}
392
+ paymentMethods={paymentMethods}
393
+ />
394
+ )
395
+ }
396
+ }
397
+
398
+ const isLastStep = stepIndex === steps.length - 1
399
+
400
+ const submitAction = (
401
+ <Button type="button" onClick={handleSubmit} disabled={submitting}>
402
+ <Lock className="size-4" />
403
+ {submitting ? resolved.processingLabel : resolved.completeOrderLabel}
404
+ </Button>
405
+ )
406
+
407
+ const submitErrorNode = submitError ? (
408
+ <div
409
+ role="alert"
410
+ className="mt-6 flex flex-wrap items-center justify-between gap-3 rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive"
411
+ >
412
+ <span>{submitError}</span>
413
+ {onRetrySubmit ? (
414
+ <Button
415
+ type="button"
416
+ variant="outline"
417
+ size="sm"
418
+ onClick={onRetrySubmit}
419
+ disabled={submitting}
420
+ >
421
+ {resolved.submitErrorActionLabel}
422
+ </Button>
423
+ ) : null}
424
+ </div>
425
+ ) : null
426
+
427
+ const summaryNode = orderSummary ? (
428
+ <div
429
+ className={cn(
430
+ 'min-w-0 lg:col-span-1',
431
+ presentation === 'single-page' && 'lg:order-last',
432
+ )}
433
+ >
434
+ <CheckoutOrderSummaryPanel
435
+ summary={orderSummary}
436
+ labels={resolved}
437
+ totalsState={totalsState}
438
+ onRetryTotals={onRetryTotals}
439
+ promoCode={values.promoCode}
440
+ onPromoCodeChange={(value) => onChange('promoCode', value)}
441
+ onApplyPromo={onApplyPromo}
442
+ promoPending={promoPending}
443
+ trustNotes={trustNotes}
444
+ formatMoney={formatMoney}
445
+ />
446
+ </div>
447
+ ) : null
448
+
449
+ return (
450
+ <div className="mx-auto max-w-6xl px-4 py-12 sm:px-6 lg:px-8">
451
+ {(resolved.title || resolved.description) && (
452
+ <header className="mb-8 text-center">
453
+ {resolved.title ? (
454
+ <h1 className="text-3xl font-bold tracking-tight sm:text-4xl">{resolved.title}</h1>
455
+ ) : null}
456
+ {resolved.description ? (
457
+ <p className="mt-2 text-muted-foreground">{resolved.description}</p>
458
+ ) : null}
459
+ </header>
460
+ )}
461
+
462
+ {presentation === 'stepper' ? (
463
+ <CheckoutProgress
464
+ steps={steps.map((step) => ({
465
+ id: step,
466
+ label: resolved[STEP_COPY[step].name],
467
+ }))}
468
+ currentStep={activeStep}
469
+ ariaLabel={resolved.stepProgressLabel}
470
+ />
471
+ ) : null}
472
+
473
+ <div className={cn('mt-8 grid grid-cols-1 gap-6', orderSummary && 'lg:grid-cols-3')}>
474
+ {presentation === 'single-page' ? summaryNode : null}
475
+
476
+ <div className={cn('min-w-0', orderSummary ? 'lg:col-span-2' : 'lg:col-span-3')}>
477
+ <Card>
478
+ <CardContent className="p-6">
479
+ {presentation === 'stepper' ? (
480
+ <>
481
+ <div className="mb-6">
482
+ <h2 ref={headingRef} tabIndex={-1} className="text-lg font-semibold outline-none">
483
+ {resolved[STEP_COPY[activeStep].heading]}
484
+ </h2>
485
+ <p className="text-sm text-muted-foreground">
486
+ {resolved[STEP_COPY[activeStep].description]}
487
+ </p>
488
+ </div>
489
+
490
+ {renderStep(activeStep)}
491
+ </>
492
+ ) : (
493
+ <div className="flex flex-col gap-8">
494
+ {steps.map((step) => (
495
+ <section key={step} className="flex flex-col gap-4">
496
+ <div>
497
+ <h2 className="text-lg font-semibold">
498
+ {resolved[STEP_COPY[step].heading]}
499
+ </h2>
500
+ <p className="text-sm text-muted-foreground">
501
+ {resolved[STEP_COPY[step].description]}
502
+ </p>
503
+ </div>
504
+ {renderStep(step)}
505
+ </section>
506
+ ))}
507
+ </div>
508
+ )}
509
+
510
+ {submitErrorNode}
511
+
512
+ <div
513
+ className={cn(
514
+ 'mt-6 flex flex-wrap items-center gap-4',
515
+ presentation === 'stepper' ? 'justify-between' : 'justify-end',
516
+ )}
517
+ >
518
+ {presentation === 'stepper' ? (
519
+ <>
520
+ <Button
521
+ type="button"
522
+ variant="outline"
523
+ onClick={handleBack}
524
+ disabled={stepIndex === 0 || submitting}
525
+ >
526
+ {resolved.backLabel}
527
+ </Button>
528
+ {isLastStep ? (
529
+ submitAction
530
+ ) : (
531
+ <Button type="button" onClick={handleContinue} disabled={submitting}>
532
+ {resolved.continueLabel}
533
+ </Button>
534
+ )}
535
+ </>
536
+ ) : (
537
+ submitAction
538
+ )}
539
+ </div>
540
+ </CardContent>
541
+ </Card>
542
+ </div>
543
+
544
+ {presentation === 'stepper' ? summaryNode : null}
545
+ </div>
546
+ </div>
547
+ )
548
+ }
549
+
550
+ export default Checkout
@@ -1,7 +1,7 @@
1
1
  import { Children, isValidElement, type ReactNode } from 'react'
2
2
  import { renderToStaticMarkup } from 'react-dom/server'
3
3
  import { describe, expect, it, vi } from 'vitest'
4
- import { Navbar4AccountMenu } from './navbar-4-account-menu'
4
+ import { StorefrontNavbarAccountMenu } from './storefront-navbar-account-menu'
5
5
  import { DropdownMenuItem } from '../../../ui/dropdown-menu'
6
6
 
7
7
  // Inspect the block-owned menu items without mounting Radix's browser portal.
@@ -17,36 +17,36 @@ function menuItems(node: ReactNode): Record<string, unknown>[] {
17
17
 
18
18
  const user = { name: 'Ada', email: 'ada@example.test', initials: 'A' }
19
19
 
20
- describe('Navbar4AccountMenu absent avatar', () => {
20
+ describe('StorefrontNavbarAccountMenu absent avatar', () => {
21
21
  it('renders the fallback initials, never an empty image source, when avatar is absent', () => {
22
- const html = renderToStaticMarkup(<Navbar4AccountMenu user={user} />)
22
+ const html = renderToStaticMarkup(<StorefrontNavbarAccountMenu user={user} />)
23
23
  expect(html).toContain('>A<')
24
24
  expect(html).not.toContain('src=""')
25
25
  expect(html).not.toContain('<img')
26
26
  })
27
27
 
28
28
  it('treats an empty avatar string as absent rather than an empty source', () => {
29
- const html = renderToStaticMarkup(<Navbar4AccountMenu user={{ ...user, avatar: '' }} />)
29
+ const html = renderToStaticMarkup(<StorefrontNavbarAccountMenu user={{ ...user, avatar: '' }} />)
30
30
  expect(html).toContain('>A<')
31
31
  expect(html).not.toContain('src=""')
32
32
  })
33
33
  })
34
34
 
35
- describe('Navbar4AccountMenu consumer actions', () => {
35
+ describe('StorefrontNavbarAccountMenu consumer actions', () => {
36
36
  it('omits help and sign-out when no consumer actions exist', () => {
37
- expect(menuItems(Navbar4AccountMenu({ user }))).toEqual([])
37
+ expect(menuItems(StorefrontNavbarAccountMenu({ user }))).toEqual([])
38
38
  })
39
39
 
40
40
  it('dispatches sign-out to its consumer', () => {
41
41
  const onSignOut = vi.fn()
42
- const [item] = menuItems(Navbar4AccountMenu({ user, onSignOut }))
42
+ const [item] = menuItems(StorefrontNavbarAccountMenu({ user, onSignOut }))
43
43
  ;(item.onSelect as () => void)()
44
44
  expect(onSignOut).toHaveBeenCalledTimes(1)
45
45
  })
46
46
 
47
47
  it('prevents duplicate sign-out while the consumer reports the logout pending', () => {
48
48
  const onSignOut = vi.fn()
49
- const [item] = menuItems(Navbar4AccountMenu({ user, pending: true, onSignOut }))
49
+ const [item] = menuItems(StorefrontNavbarAccountMenu({ user, pending: true, onSignOut }))
50
50
  expect(item.disabled).toBe(true)
51
51
  const select = item.onSelect as () => void
52
52
  select()
@@ -26,7 +26,7 @@ export interface AccountMenuItem {
26
26
  icon?: 'orders' | 'wishlist' | 'addresses' | 'settings'
27
27
  }
28
28
 
29
- export interface Navbar4AccountMenuProps {
29
+ export interface StorefrontNavbarAccountMenuProps {
30
30
  /** Signed-in user. When omitted, the menu is not rendered. */
31
31
  user?: StoreUser
32
32
  /** Account links shown below the user header. Defaults to `[]`. */
@@ -43,13 +43,13 @@ const accountIcons = {
43
43
  settings: Settings,
44
44
  } as const
45
45
 
46
- export function Navbar4AccountMenu({
46
+ export function StorefrontNavbarAccountMenu({
47
47
  user,
48
48
  items = [],
49
49
  onHelp,
50
50
  onSignOut,
51
51
  pending = false,
52
- }: Navbar4AccountMenuProps) {
52
+ }: StorefrontNavbarAccountMenuProps) {
53
53
  if (!user) {
54
54
  return null
55
55
  }