@thorprovider/blocks 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/.turbo/turbo-build.log +24 -0
  2. package/.turbo/turbo-test.log +10 -0
  3. package/.turbo/turbo-type-check.log +0 -0
  4. package/AGENTS.md +1 -0
  5. package/README.md +161 -0
  6. package/components.json +33 -0
  7. package/dist/.tsbuildinfo +1 -0
  8. package/dist/contracts.d.mts +563 -0
  9. package/dist/contracts.d.ts +563 -0
  10. package/dist/contracts.js +207 -0
  11. package/dist/contracts.js.map +1 -0
  12. package/dist/contracts.mjs +168 -0
  13. package/dist/contracts.mjs.map +1 -0
  14. package/dist/index.d.mts +2408 -0
  15. package/dist/index.d.ts +2408 -0
  16. package/dist/index.js +16422 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/index.mjs +16352 -0
  19. package/dist/index.mjs.map +1 -0
  20. package/docs/standards/data-contracts.md +70 -0
  21. package/docs/standards/theming.md +85 -0
  22. package/package.json +62 -0
  23. package/src/blocks/application/app-shells/app-shell-1-sidebar.tsx +198 -0
  24. package/src/blocks/application/app-shells/app-shell-1.tsx +196 -0
  25. package/src/blocks/application/login-forms/login-page-1.tsx +246 -0
  26. package/src/blocks/application/login-forms/login-page-2.tsx +207 -0
  27. package/src/blocks/application/password-recovery/forgot-password-page-1-brand-mark.tsx +38 -0
  28. package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +320 -0
  29. package/src/blocks/application/signup-forms/signup-page-1.tsx +231 -0
  30. package/src/blocks/application/verification/verify-1.tsx +307 -0
  31. package/src/blocks/application/verification/verify-confetti.tsx +192 -0
  32. package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +260 -0
  33. package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +179 -0
  34. package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +341 -0
  35. package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +57 -0
  36. package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +124 -0
  37. package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +170 -0
  38. package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +52 -0
  39. package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +57 -0
  40. package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +68 -0
  41. package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +298 -0
  42. package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +47 -0
  43. package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +9 -0
  44. package/src/blocks/e-commerce/checkout-forms/checkbox-check.tsx +74 -0
  45. package/src/blocks/e-commerce/checkout-forms/checkout-confirmation-fulfilment-progress.tsx +189 -0
  46. package/src/blocks/e-commerce/checkout-forms/checkout-confirmation.tsx +283 -0
  47. package/src/blocks/e-commerce/checkout-forms/checkout-field-feedback.tsx +29 -0
  48. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +96 -0
  49. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-country-field.tsx +51 -0
  50. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +139 -0
  51. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +184 -0
  52. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +36 -0
  53. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-shipping-step.tsx +107 -0
  54. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +235 -0
  55. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +130 -0
  56. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +175 -0
  57. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +190 -0
  58. package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +312 -0
  59. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +91 -0
  60. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +110 -0
  61. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +137 -0
  62. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +68 -0
  63. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +79 -0
  64. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +172 -0
  65. package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +392 -0
  66. package/src/blocks/e-commerce/checkout-forms/checkout-form-types.ts +7 -0
  67. package/src/blocks/e-commerce/navigation/category-icon.test.tsx +54 -0
  68. package/src/blocks/e-commerce/navigation/category-icon.tsx +94 -0
  69. package/src/blocks/e-commerce/navigation/mega-menu.test.tsx +139 -0
  70. package/src/blocks/e-commerce/navigation/mega-menu.tsx +271 -0
  71. package/src/blocks/e-commerce/navigation/mobile-category-drawer.test.tsx +152 -0
  72. package/src/blocks/e-commerce/navigation/mobile-category-drawer.tsx +276 -0
  73. package/src/blocks/e-commerce/order-history/order-history-1.tsx +159 -0
  74. package/src/blocks/e-commerce/order-history/order-history-2.tsx +222 -0
  75. package/src/blocks/e-commerce/product-categories/avatar-group.tsx +86 -0
  76. package/src/blocks/e-commerce/product-categories/product-category-1.tsx +126 -0
  77. package/src/blocks/e-commerce/product-categories/product-category-2.tsx +100 -0
  78. package/src/blocks/e-commerce/product-categories/product-category-4.tsx +129 -0
  79. package/src/blocks/e-commerce/product-categories/product-category-5.tsx +218 -0
  80. package/src/blocks/e-commerce/product-list/product-card-1.tsx +131 -0
  81. package/src/blocks/e-commerce/product-list/product-card-2.tsx +71 -0
  82. package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +166 -0
  83. package/src/blocks/e-commerce/product-list/product-card-3.tsx +215 -0
  84. package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +98 -0
  85. package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +171 -0
  86. package/src/blocks/e-commerce/product-list/product-card-5.tsx +367 -0
  87. package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +201 -0
  88. package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +272 -0
  89. package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +59 -0
  90. package/src/blocks/e-commerce/product-overview/product-overview-5-lightbox-tab.tsx +104 -0
  91. package/src/blocks/e-commerce/product-overview/product-overview-5-reviews-tab.tsx +82 -0
  92. package/src/blocks/e-commerce/product-overview/product-overview-5-tabs.tsx +69 -0
  93. package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +274 -0
  94. package/src/blocks/e-commerce/product-overview/product-quick-view-card.tsx +135 -0
  95. package/src/blocks/e-commerce/product-overview/product-quick-view-dialog.tsx +393 -0
  96. package/src/blocks/e-commerce/product-overview/product-quick-view.tsx +136 -0
  97. package/src/blocks/e-commerce/reviews/review-rating-1-review-form.tsx +133 -0
  98. package/src/blocks/e-commerce/reviews/review-rating-1.tsx +178 -0
  99. package/src/blocks/e-commerce/reviews/review-rating-2.tsx +246 -0
  100. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +142 -0
  101. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +291 -0
  102. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +110 -0
  103. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +51 -0
  104. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +146 -0
  105. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +295 -0
  106. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +142 -0
  107. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +342 -0
  108. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +146 -0
  109. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.tsx +391 -0
  110. package/src/blocks/e-commerce/store-containers/store-container-1.tsx +29 -0
  111. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +244 -0
  112. package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +283 -0
  113. package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +318 -0
  114. package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +205 -0
  115. package/src/blocks/marketing/banners/avatar-group.tsx +69 -0
  116. package/src/blocks/marketing/banners/banner-1.tsx +188 -0
  117. package/src/blocks/marketing/cta/cta-section-1.tsx +112 -0
  118. package/src/blocks/marketing/faq/faq-block-1.tsx +45 -0
  119. package/src/blocks/marketing/features/feature-1.tsx +97 -0
  120. package/src/blocks/marketing/features/feature-2.tsx +82 -0
  121. package/src/blocks/marketing/footers/footer-section-1.tsx +141 -0
  122. package/src/blocks/marketing/footers/footer-section-3.tsx +192 -0
  123. package/src/blocks/marketing/footers/footer-section-4.tsx +355 -0
  124. package/src/blocks/marketing/hero-sections/avatar-group.tsx +86 -0
  125. package/src/blocks/marketing/hero-sections/hero-section-1.tsx +135 -0
  126. package/src/blocks/marketing/logo-clouds/logo-cloud-1-logo-icon.tsx +52 -0
  127. package/src/blocks/marketing/logo-clouds/logo-cloud-1.tsx +56 -0
  128. package/src/blocks/marketing/navbars/navbar-1.tsx +267 -0
  129. package/src/blocks/marketing/navbars/navbar-2.tsx +351 -0
  130. package/src/blocks/marketing/navbars/navbar-3.tsx +303 -0
  131. package/src/blocks/marketing/navbars/navbar-4-account-menu.tsx +118 -0
  132. package/src/blocks/marketing/navbars/navbar-4-cart-sheet.tsx +248 -0
  133. package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +177 -0
  134. package/src/blocks/marketing/navbars/navbar-4-mobile-nav.tsx +104 -0
  135. package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +219 -0
  136. package/src/blocks/marketing/navbars/navbar-4-search-bar.tsx +52 -0
  137. package/src/blocks/marketing/navbars/navbar-4.tsx +411 -0
  138. package/src/blocks/marketing/pricing/pricing-section-1.tsx +94 -0
  139. package/src/blocks/marketing/statistics/statistic-1.tsx +93 -0
  140. package/src/blocks/marketing/testimonials/testimonial-1.tsx +141 -0
  141. package/src/blocks/marketing/testimonials/testimonial-2.tsx +108 -0
  142. package/src/blocks/shared/reaction-toggle.tsx +145 -0
  143. package/src/hooks/use-mobile.ts +19 -0
  144. package/src/index.ts +200 -0
  145. package/src/lib/contracts.test.ts +32 -0
  146. package/src/lib/contracts.ts +235 -0
  147. package/src/lib/types/menu.ts +43 -0
  148. package/src/lib/types.ts +4 -0
  149. package/src/lib/utils.ts +10 -0
  150. package/src/ui/accordion.tsx +65 -0
  151. package/src/ui/avatar.tsx +108 -0
  152. package/src/ui/badge.tsx +47 -0
  153. package/src/ui/breadcrumb.tsx +108 -0
  154. package/src/ui/button-group.tsx +83 -0
  155. package/src/ui/button.tsx +63 -0
  156. package/src/ui/card.tsx +91 -0
  157. package/src/ui/carousel.tsx +241 -0
  158. package/src/ui/dialog.tsx +158 -0
  159. package/src/ui/drawer.tsx +134 -0
  160. package/src/ui/dropdown-menu.tsx +256 -0
  161. package/src/ui/empty.tsx +103 -0
  162. package/src/ui/field.tsx +248 -0
  163. package/src/ui/input-group.tsx +170 -0
  164. package/src/ui/input-otp.tsx +76 -0
  165. package/src/ui/input.tsx +20 -0
  166. package/src/ui/label.tsx +23 -0
  167. package/src/ui/navigation-menu.tsx +167 -0
  168. package/src/ui/pagination.tsx +127 -0
  169. package/src/ui/progress.tsx +30 -0
  170. package/src/ui/radio-group.tsx +44 -0
  171. package/src/ui/select.tsx +189 -0
  172. package/src/ui/separator.tsx +27 -0
  173. package/src/ui/sheet.tsx +142 -0
  174. package/src/ui/sidebar.tsx +726 -0
  175. package/src/ui/skeleton.tsx +13 -0
  176. package/src/ui/table.tsx +115 -0
  177. package/src/ui/tabs.tsx +90 -0
  178. package/src/ui/textarea.tsx +17 -0
  179. package/src/ui/toggle-group.tsx +83 -0
  180. package/src/ui/toggle.tsx +46 -0
  181. package/src/ui/tooltip.tsx +56 -0
  182. package/tsconfig.json +20 -0
  183. package/tsup.config.ts +16 -0
  184. package/vitest.config.ts +9 -0
@@ -0,0 +1,189 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { Check, Clock, MapPin, Truck } from 'lucide-react'
5
+ import { Button } from '../../../ui/button'
6
+ import { Card, CardContent, CardHeader, CardTitle } from '../../../ui/card'
7
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '../../../ui/tabs'
8
+ import { cn } from '../../../lib/utils'
9
+ import type { FulfilmentStage, FulfilmentStageDetail } from './checkout-confirmation'
10
+
11
+ const longDate = new Intl.DateTimeFormat('en-US', { dateStyle: 'long' })
12
+ const stampDate = new Intl.DateTimeFormat('en-US', {
13
+ dateStyle: 'medium',
14
+ timeStyle: 'short',
15
+ })
16
+
17
+ const STAGE_LABELS: Record<FulfilmentStage, string> = {
18
+ Confirmed: 'Confirmado',
19
+ Packed: 'Empaquetado',
20
+ Shipped: 'Enviado',
21
+ Delivered: 'Entregado',
22
+ }
23
+
24
+ interface CheckoutConfirmationFulfilmentProgressProps {
25
+ order: {
26
+ currentStage: FulfilmentStage
27
+ estimatedDelivery: string
28
+ trackingHref?: string
29
+ }
30
+ /** Ordered by `Object.keys(fulfilmentDetails)` from the orchestrator. */
31
+ stages: FulfilmentStageDetail[]
32
+ progressLabel?: string
33
+ estimatedDeliveryLabel?: string
34
+ currentStageLabel?: string
35
+ inProgressLabel?: string
36
+ expectedByLabel?: string
37
+ completedLabel?: string
38
+ statusLabel?: string
39
+ locationLabel?: string
40
+ trackOrderLabel?: string
41
+ /** Kept for API symmetry with the orchestrator; the fulfilment card renders no prices. */
42
+ formatPrice?: (value: number) => string
43
+ }
44
+
45
+ /**
46
+ * The four fulfilment stages, as tabs that actually open.
47
+ *
48
+ * This was a static `<ol>` that looked exactly like a tab strip — four labels with an underline
49
+ * under each — and did nothing when pressed. A control that looks pressable and is not is worse
50
+ * than no control: the reader tries it, nothing happens, and they conclude the page is broken
51
+ * rather than that the row was decoration.
52
+ *
53
+ * So it is a real `Tabs`, and each stage opens the one thing a reader on this screen wants that
54
+ * the label alone cannot give them: when it happened, where, and what it means. Radix Tabs
55
+ * supplies roving focus, arrow-key movement and the tab/panel wiring; a row of buttons with a
56
+ * `useState` would have to fake all three.
57
+ *
58
+ * Every stage is reachable, including ones still ahead — a reader is entitled to look at what
59
+ * "Delivered" will mean before it happens. What a future stage does not get is a fabricated
60
+ * timestamp; it says what it is waiting for instead.
61
+ */
62
+ export function CheckoutConfirmationFulfilmentProgress({
63
+ order,
64
+ stages,
65
+ progressLabel = 'Progreso',
66
+ estimatedDeliveryLabel = 'Entrega estimada',
67
+ currentStageLabel = 'Etapa actual',
68
+ inProgressLabel = 'En curso',
69
+ expectedByLabel = 'Se espera para',
70
+ completedLabel = 'Completada',
71
+ statusLabel = 'Estado',
72
+ locationLabel = 'Ubicación',
73
+ trackOrderLabel = 'Seguir este pedido',
74
+ }: CheckoutConfirmationFulfilmentProgressProps) {
75
+ const currentIndex = stages.findIndex(stage => stage.stage === order.currentStage)
76
+ const [selected, setSelected] = useState<string>(order.currentStage)
77
+
78
+ return (
79
+ <Card className='mt-10'>
80
+ <CardHeader>
81
+ <CardTitle className='flex flex-wrap items-center justify-between gap-2 text-base'>
82
+ {progressLabel}
83
+ <span className='text-muted-foreground text-sm font-normal'>
84
+ {`${estimatedDeliveryLabel} ${longDate.format(new Date(order.estimatedDelivery))}`}
85
+ </span>
86
+ </CardTitle>
87
+ </CardHeader>
88
+
89
+ <CardContent>
90
+ <Tabs value={selected} onValueChange={value => setSelected(String(value))}>
91
+ {/*
92
+ `variant='line'` because these are stages along a route, not a segmented control —
93
+ the underline reads as progress in a way a filled pill does not.
94
+
95
+ `h-auto!` because `TabsList` pins itself to `h-8` through a
96
+ `group-data-horizontal/tabs:` variant, which a plain `h-auto` cannot outrank and
97
+ tailwind-merge does not treat as the same utility. Left alone it clipped the 24px
98
+ stage markers. The list scrolls rather than wrapping on a narrow screen, so the four
99
+ stages stay in one order.
100
+
101
+ `pb-2` is load-bearing, not spacing. `TabsTrigger` draws its active underline at
102
+ `bottom-[-5px]`, i.e. outside its own box, and CSS forces an axis to `auto` when the
103
+ other one is not `visible` — so asking for `overflow-x-auto` silently made the
104
+ vertical axis scrollable too, and those 7px of underline became a stray vertical
105
+ scrollbar beside the tabs at every width. The padding puts the underline back inside
106
+ the box, which leaves nothing to scroll.
107
+ */}
108
+ <TabsList variant='line' className='h-auto! w-full justify-start gap-2 overflow-x-auto p-0 pb-2'>
109
+ {stages.map((stage, index) => {
110
+ const done = index <= currentIndex
111
+
112
+ return (
113
+ <TabsTrigger key={stage.stage} value={stage.stage} className='h-auto flex-1 justify-start gap-2 px-1 py-2'>
114
+ <span
115
+ aria-hidden='true'
116
+ className={cn(
117
+ 'flex size-6 shrink-0 items-center justify-center rounded-full text-xs',
118
+ done ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'
119
+ )}
120
+ >
121
+ {done ? <Check className='size-3.5' /> : index + 1}
122
+ </span>
123
+ {STAGE_LABELS[stage.stage]}
124
+ {/*
125
+ The stage the order is actually at, marked on the strip itself. Without it
126
+ the only sign of "where we are" would be whichever tab happens to be open,
127
+ which changes the moment the reader looks at another one.
128
+ */}
129
+ {index === currentIndex ? <span className='sr-only'>{currentStageLabel}</span> : null}
130
+ {index === currentIndex ? (
131
+ <span aria-hidden='true' className='bg-primary size-1.5 shrink-0 rounded-full' />
132
+ ) : null}
133
+ </TabsTrigger>
134
+ )
135
+ })}
136
+ </TabsList>
137
+
138
+ {stages.map((detail, index) => {
139
+ const done = index <= currentIndex
140
+
141
+ return (
142
+ <TabsContent key={detail.stage} value={detail.stage} className='pt-4'>
143
+ {index === currentIndex ? (
144
+ <p className='text-primary mb-2 text-xs font-medium tracking-[0.12em] uppercase'>
145
+ {currentStageLabel}
146
+ </p>
147
+ ) : null}
148
+ <p className='text-sm'>{detail.summary}</p>
149
+
150
+ <dl className='text-muted-foreground mt-3 flex flex-col gap-1.5 text-sm sm:flex-row sm:gap-6'>
151
+ <div className='flex items-center gap-2'>
152
+ <dt className='sr-only'>{detail.at ? completedLabel : statusLabel}</dt>
153
+ <Clock className='size-4 shrink-0' aria-hidden='true' />
154
+ <dd className='tabular-nums'>
155
+ {detail.at
156
+ ? stampDate.format(new Date(detail.at))
157
+ : done
158
+ ? inProgressLabel
159
+ : `${expectedByLabel} ${longDate.format(new Date(order.estimatedDelivery))}`}
160
+ </dd>
161
+ </div>
162
+
163
+ <div className='flex items-center gap-2'>
164
+ <dt className='sr-only'>{locationLabel}</dt>
165
+ <MapPin className='size-4 shrink-0' aria-hidden='true' />
166
+ <dd>{detail.location}</dd>
167
+ </div>
168
+ </dl>
169
+ </TabsContent>
170
+ )
171
+ })}
172
+ </Tabs>
173
+
174
+ {/*
175
+ Render as an anchor through Radix `asChild` when a real tracking URL exists; with no
176
+ href the button is omitted rather than pointing at a fabricated destination.
177
+ */}
178
+ {order.trackingHref ? (
179
+ <Button className='mt-6 w-full sm:w-auto' asChild>
180
+ <a href={order.trackingHref}>
181
+ <Truck />
182
+ {trackOrderLabel}
183
+ </a>
184
+ </Button>
185
+ ) : null}
186
+ </CardContent>
187
+ </Card>
188
+ )
189
+ }
@@ -0,0 +1,283 @@
1
+ import { CircleCheck } from 'lucide-react'
2
+ import { Button } from '../../../ui/button'
3
+ import { Card, CardContent, CardHeader, CardTitle } from '../../../ui/card'
4
+ import { Separator } from '../../../ui/separator'
5
+ import { CheckoutConfirmationFulfilmentProgress } from './checkout-confirmation-fulfilment-progress'
6
+
7
+ export type FulfilmentStage = 'Confirmed' | 'Packed' | 'Shipped' | 'Delivered'
8
+
9
+ export interface OrderLine {
10
+ id: string
11
+ name: string
12
+ variant: string
13
+ quantity: number
14
+ price: number
15
+ image: string
16
+ }
17
+
18
+ export interface FulfilmentStageDetail {
19
+ stage: FulfilmentStage
20
+ /** What happened, in a sentence. Present tense for a stage that has not arrived yet. */
21
+ summary: string
22
+ /** Where it happened, or where it will. */
23
+ location: string
24
+ /**
25
+ * When it happened. `null` for a stage still ahead, which the panel renders as an
26
+ * expectation rather than as a fact — a date with no event behind it is a promise, and
27
+ * printing it in the same voice as a completed step is how a tracker starts lying.
28
+ */
29
+ at: string | null
30
+ }
31
+
32
+ export interface OrderAddress {
33
+ name: string
34
+ lines: string[]
35
+ method: string
36
+ }
37
+
38
+ export interface OrderTotals {
39
+ subtotal: number
40
+ shipping: number
41
+ tax: number
42
+ }
43
+
44
+ export interface Order {
45
+ number: string
46
+ /** ISO date */
47
+ placedAt: string
48
+ email: string
49
+ currentStage: FulfilmentStage
50
+ /** ISO date */
51
+ estimatedDelivery: string
52
+ trackingHref?: string
53
+ shipping: OrderAddress
54
+ billing: OrderAddress
55
+ totals: OrderTotals
56
+ }
57
+
58
+ export interface CheckoutConfirmationProps {
59
+ order: Order
60
+ orderLines: OrderLine[]
61
+ /**
62
+ * Keyed by `FulfilmentStage`. The stage strip is ordered by `Object.keys`,
63
+ * so define the record in the canonical `Confirmed → Packed → Shipped → Delivered` order.
64
+ */
65
+ fulfilmentDetails: Record<FulfilmentStage, FulfilmentStageDetail>
66
+ /** Override price formatting (locale, currency). Defaults to USD. */
67
+ formatPrice?: (value: number) => string
68
+ titleLabel?: string
69
+ /** Template with `{number}`/`{date}`/`{email}` placeholders, resolved internally. */
70
+ orderPlacedLabel?: string
71
+ itemsLabel?: string
72
+ quantityLabel?: string
73
+ subtotalLabel?: string
74
+ shippingLabel?: string
75
+ taxLabel?: string
76
+ totalLabel?: string
77
+ freeLabel?: string
78
+ shippingAddressLabel?: string
79
+ billingAddressLabel?: string
80
+ continueShoppingLabel?: string
81
+ /** Prefix of the support line; the `supportLabel` link renders right after it. */
82
+ contactSupportLabel?: string
83
+ supportLabel?: string
84
+ progressLabel?: string
85
+ estimatedDeliveryLabel?: string
86
+ currentStageLabel?: string
87
+ inProgressLabel?: string
88
+ expectedByLabel?: string
89
+ completedLabel?: string
90
+ statusLabel?: string
91
+ locationLabel?: string
92
+ trackOrderLabel?: string
93
+ continueShoppingHref?: string
94
+ supportHref?: string
95
+ onContinueShopping?: () => void
96
+ }
97
+
98
+ const usdFormatter = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' })
99
+ const longDate = new Intl.DateTimeFormat('en-US', { dateStyle: 'long' })
100
+
101
+ /**
102
+ * The screen that closes the purchase journey the checkout blocks open.
103
+ *
104
+ * This file has no hooks or handlers, so it still renders as a server component straight from
105
+ * your order record. Only the progress tabs are interactive, and they carry their own
106
+ * `'use client'` — the boundary is one child rather than the whole page.
107
+ */
108
+ export function CheckoutConfirmation({
109
+ order,
110
+ orderLines,
111
+ fulfilmentDetails,
112
+ formatPrice,
113
+ titleLabel = '¡Gracias! Tu pedido está en camino',
114
+ orderPlacedLabel = 'Pedido {number} realizado el {date}. Enviamos la confirmación a {email}.',
115
+ itemsLabel = 'Artículos',
116
+ quantityLabel = 'Cantidad',
117
+ subtotalLabel = 'Subtotal',
118
+ shippingLabel = 'Envío',
119
+ taxLabel = 'Impuestos',
120
+ totalLabel = 'Total',
121
+ freeLabel = 'Gratis',
122
+ shippingAddressLabel = 'Envío',
123
+ billingAddressLabel = 'Facturación',
124
+ continueShoppingLabel = 'Seguir comprando',
125
+ contactSupportLabel = '¿Algo falla con este pedido?',
126
+ supportLabel = 'Contacta con soporte',
127
+ progressLabel = 'Progreso',
128
+ estimatedDeliveryLabel = 'Entrega estimada',
129
+ currentStageLabel = 'Etapa actual',
130
+ inProgressLabel = 'En curso',
131
+ expectedByLabel = 'Se espera para',
132
+ completedLabel = 'Completada',
133
+ statusLabel = 'Estado',
134
+ locationLabel = 'Ubicación',
135
+ trackOrderLabel = 'Seguir este pedido',
136
+ continueShoppingHref,
137
+ supportHref,
138
+ onContinueShopping,
139
+ }: CheckoutConfirmationProps) {
140
+ const total = order.totals.subtotal + order.totals.shipping + order.totals.tax
141
+ const format = formatPrice ?? (value => usdFormatter.format(value))
142
+ const fulfilmentStages = Object.keys(fulfilmentDetails) as FulfilmentStage[]
143
+
144
+ return (
145
+ <div className='w-full py-16 sm:py-24'>
146
+ <div className='mx-auto w-full max-w-3xl px-4 sm:px-6 lg:px-8'>
147
+ <div className='text-center'>
148
+ <span
149
+ aria-hidden='true'
150
+ className='bg-primary/10 text-primary mx-auto flex size-12 items-center justify-center rounded-full'
151
+ >
152
+ <CircleCheck className='size-6' />
153
+ </span>
154
+ <h1 className='mt-4 text-3xl font-semibold tracking-tight'>{titleLabel}</h1>
155
+ <p className='text-muted-foreground mt-3'>
156
+ {orderPlacedLabel
157
+ .replace('{number}', order.number)
158
+ .replace('{date}', longDate.format(new Date(order.placedAt)))
159
+ .replace('{email}', order.email)}
160
+ </p>
161
+ </div>
162
+
163
+ <CheckoutConfirmationFulfilmentProgress
164
+ order={{
165
+ currentStage: order.currentStage,
166
+ estimatedDelivery: order.estimatedDelivery,
167
+ trackingHref: order.trackingHref,
168
+ }}
169
+ stages={fulfilmentStages.map(stage => fulfilmentDetails[stage])}
170
+ progressLabel={progressLabel}
171
+ estimatedDeliveryLabel={estimatedDeliveryLabel}
172
+ currentStageLabel={currentStageLabel}
173
+ inProgressLabel={inProgressLabel}
174
+ expectedByLabel={expectedByLabel}
175
+ completedLabel={completedLabel}
176
+ statusLabel={statusLabel}
177
+ locationLabel={locationLabel}
178
+ trackOrderLabel={trackOrderLabel}
179
+ />
180
+
181
+ <Card className='mt-6'>
182
+ <CardHeader>
183
+ <CardTitle className='text-base'>{itemsLabel}</CardTitle>
184
+ </CardHeader>
185
+ <CardContent>
186
+ <ul className='divide-border divide-y'>
187
+ {orderLines.map(line => (
188
+ <li key={line.id} className='flex gap-4 py-4 first:pt-0 last:pb-0'>
189
+ <img
190
+ src={line.image || undefined}
191
+ alt={line.name}
192
+ width={72}
193
+ height={88}
194
+ className='bg-muted h-22 w-18 shrink-0 rounded-md object-cover'
195
+ />
196
+ <div className='min-w-0 flex-1'>
197
+ <p className='text-sm font-medium'>{line.name}</p>
198
+ <p className='text-muted-foreground text-sm'>{line.variant}</p>
199
+ <p className='text-muted-foreground text-sm tabular-nums'>
200
+ {`${quantityLabel} ${line.quantity}`}
201
+ </p>
202
+ </div>
203
+ <p className='text-sm font-medium tabular-nums'>
204
+ {format(line.price * line.quantity)}
205
+ </p>
206
+ </li>
207
+ ))}
208
+ </ul>
209
+
210
+ <Separator className='my-4' />
211
+
212
+ <dl className='flex flex-col gap-2 text-sm'>
213
+ <div className='flex justify-between'>
214
+ <dt className='text-muted-foreground'>{subtotalLabel}</dt>
215
+ <dd className='tabular-nums'>{format(order.totals.subtotal)}</dd>
216
+ </div>
217
+ <div className='flex justify-between'>
218
+ <dt className='text-muted-foreground'>{shippingLabel}</dt>
219
+ <dd className='tabular-nums'>
220
+ {order.totals.shipping === 0 ? freeLabel : format(order.totals.shipping)}
221
+ </dd>
222
+ </div>
223
+ <div className='flex justify-between'>
224
+ <dt className='text-muted-foreground'>{taxLabel}</dt>
225
+ <dd className='tabular-nums'>{format(order.totals.tax)}</dd>
226
+ </div>
227
+ <div className='flex justify-between pt-2 text-base font-semibold'>
228
+ <dt>{totalLabel}</dt>
229
+ <dd className='tabular-nums'>{format(total)}</dd>
230
+ </div>
231
+ </dl>
232
+ </CardContent>
233
+ </Card>
234
+
235
+ <div className='mt-6 grid gap-6 sm:grid-cols-2'>
236
+ {[
237
+ { title: shippingAddressLabel, address: order.shipping },
238
+ { title: billingAddressLabel, address: order.billing },
239
+ ].map(section => (
240
+ <Card key={section.title}>
241
+ <CardHeader>
242
+ <CardTitle className='text-base'>{section.title}</CardTitle>
243
+ </CardHeader>
244
+ <CardContent className='text-sm'>
245
+ <address className='text-muted-foreground not-italic'>
246
+ <span className='text-foreground block font-medium'>{section.address.name}</span>
247
+ {section.address.lines.map(line => (
248
+ <span key={line} className='block'>
249
+ {line}
250
+ </span>
251
+ ))}
252
+ </address>
253
+ <p className='text-muted-foreground mt-3'>{section.address.method}</p>
254
+ </CardContent>
255
+ </Card>
256
+ ))}
257
+ </div>
258
+
259
+ <div className='mt-8 flex flex-col items-center gap-2'>
260
+ {continueShoppingHref ? (
261
+ <Button variant='ghost' asChild>
262
+ <a href={continueShoppingHref}>{continueShoppingLabel}</a>
263
+ </Button>
264
+ ) : onContinueShopping ? (
265
+ <Button variant='ghost' onClick={onContinueShopping}>
266
+ {continueShoppingLabel}
267
+ </Button>
268
+ ) : null}
269
+ {supportHref ? (
270
+ <p className='text-muted-foreground text-sm'>
271
+ {contactSupportLabel}{' '}
272
+ <a href={supportHref} className='underline underline-offset-4'>
273
+ {supportLabel}
274
+ </a>
275
+ </p>
276
+ ) : null}
277
+ </div>
278
+ </div>
279
+ </div>
280
+ )
281
+ }
282
+
283
+ export default CheckoutConfirmation
@@ -0,0 +1,29 @@
1
+ 'use client'
2
+
3
+ import { FieldDescription, FieldError } from '../../../ui/field'
4
+ import type { CheckoutFieldError } from './checkout-form-types'
5
+
6
+ export function checkoutFieldAria(id: string, errors?: CheckoutFieldError[]) {
7
+ const invalid = !!errors?.length
8
+ return {
9
+ 'aria-invalid': invalid || undefined,
10
+ 'aria-describedby': invalid ? `${id}-error` : undefined,
11
+ }
12
+ }
13
+
14
+ export function CheckoutFieldFeedback({
15
+ id,
16
+ errors,
17
+ description,
18
+ }: {
19
+ id: string
20
+ errors?: CheckoutFieldError[]
21
+ description?: string
22
+ }) {
23
+ return (
24
+ <>
25
+ {description && <FieldDescription id={`${id}-description`}>{description}</FieldDescription>}
26
+ {errors?.length ? <FieldError id={`${id}-error`} errors={errors} /> : null}
27
+ </>
28
+ )
29
+ }
@@ -0,0 +1,96 @@
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
+ }
@@ -0,0 +1,51 @@
1
+ 'use client'
2
+
3
+ import { Field, FieldLabel } from '../../../ui/field'
4
+ import {
5
+ Select,
6
+ SelectContent,
7
+ SelectGroup,
8
+ SelectItem,
9
+ SelectTrigger,
10
+ SelectValue,
11
+ } from '../../../ui/select'
12
+ import { CheckoutFieldFeedback, checkoutFieldAria } from './checkout-field-feedback'
13
+ import type { CheckoutFieldError } from './checkout-form-types'
14
+ import type { CheckoutForm1CountryOption } from './checkout-form-1'
15
+
16
+ export interface CheckoutForm1CountryFieldProps {
17
+ countries: CheckoutForm1CountryOption[]
18
+ value: string
19
+ onValueChange: (value: string) => void
20
+ errors?: CheckoutFieldError[]
21
+ description?: string
22
+ }
23
+
24
+ export function CheckoutForm1CountryField({
25
+ countries,
26
+ value,
27
+ onValueChange,
28
+ errors,
29
+ description,
30
+ }: CheckoutForm1CountryFieldProps) {
31
+ return (
32
+ <Field data-invalid={!!errors?.length}>
33
+ <FieldLabel htmlFor="checkout-country">Country</FieldLabel>
34
+ <Select value={value} onValueChange={onValueChange}>
35
+ <SelectTrigger id="checkout-country" {...checkoutFieldAria('checkout-country', errors)}>
36
+ <SelectValue placeholder="Select a country" />
37
+ </SelectTrigger>
38
+ <SelectContent>
39
+ <SelectGroup>
40
+ {countries.map((country) => (
41
+ <SelectItem key={country.value} value={country.value}>
42
+ {country.label}
43
+ </SelectItem>
44
+ ))}
45
+ </SelectGroup>
46
+ </SelectContent>
47
+ </Select>
48
+ <CheckoutFieldFeedback id="checkout-country" errors={errors} description={description} />
49
+ </Field>
50
+ )
51
+ }