@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,139 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { Gift, Shield, Truck } from 'lucide-react'
5
+ import { Badge } from '../../../ui/badge'
6
+ import { Button } from '../../../ui/button'
7
+ import { Card, CardContent, CardTitle } from '../../../ui/card'
8
+ import { Field, FieldLabel } from '../../../ui/field'
9
+ import { Input } from '../../../ui/input'
10
+ import { Separator } from '../../../ui/separator'
11
+ import type { CheckoutForm1OrderSummary } from './checkout-form-1'
12
+
13
+ export interface CheckoutForm1OrderSummaryProps {
14
+ orderSummary: CheckoutForm1OrderSummary
15
+ }
16
+
17
+ export function CheckoutForm1OrderSummary({ orderSummary }: CheckoutForm1OrderSummaryProps) {
18
+ const [promoCode, setPromoCode] = useState('')
19
+
20
+ const subtotal = orderSummary.items.reduce(
21
+ (sum, item) => sum + item.price * item.quantity,
22
+ 0
23
+ )
24
+ const total =
25
+ subtotal +
26
+ orderSummary.shipping +
27
+ orderSummary.tax -
28
+ (orderSummary.discount ?? 0) -
29
+ (orderSummary.promoDiscount ?? 0)
30
+
31
+ return (
32
+ <Card className="sticky top-8">
33
+ <CardContent className="flex flex-col gap-4 p-6">
34
+ <CardTitle>Order Summary</CardTitle>
35
+
36
+ <div className="flex flex-col gap-4">
37
+ {orderSummary.items.map((item) => (
38
+ <div key={item.id} className="flex items-center gap-3">
39
+ <div className="relative">
40
+ <img
41
+ src={item.image || undefined}
42
+ alt={item.name}
43
+ width={64}
44
+ height={64}
45
+ className="size-16 rounded-lg object-cover"
46
+ />
47
+ <Badge
48
+ variant="secondary"
49
+ className="absolute -right-2 -top-2 size-6 justify-center rounded-full p-0 text-xs"
50
+ >
51
+ {item.quantity}
52
+ </Badge>
53
+ </div>
54
+ <div className="flex-1">
55
+ <p className="text-sm font-medium">{item.name}</p>
56
+ <p className="text-sm text-muted-foreground">{item.variant}</p>
57
+ </div>
58
+ <p className="text-sm font-medium">${item.price.toFixed(2)}</p>
59
+ </div>
60
+ ))}
61
+ </div>
62
+
63
+ <Separator />
64
+
65
+ <Field className="gap-2">
66
+ <FieldLabel>Promo code</FieldLabel>
67
+ <div className="flex gap-2">
68
+ <Input
69
+ placeholder="Enter code"
70
+ value={promoCode}
71
+ onChange={(e) => setPromoCode(e.target.value)}
72
+ />
73
+ <Button type="button" variant="outline">
74
+ Apply
75
+ </Button>
76
+ </div>
77
+ </Field>
78
+
79
+ <Separator />
80
+
81
+ <div className="flex flex-col gap-2 text-sm">
82
+ <div className="flex items-center justify-between">
83
+ <span className="text-muted-foreground">Subtotal</span>
84
+ <span className="font-medium">${subtotal.toFixed(2)}</span>
85
+ </div>
86
+ <div className="flex items-center justify-between">
87
+ <span className="flex items-center gap-1.5 text-muted-foreground">
88
+ <Truck className="size-4" />
89
+ Shipping
90
+ </span>
91
+ <span className="font-medium">${orderSummary.shipping.toFixed(2)}</span>
92
+ </div>
93
+ <div className="flex items-center justify-between">
94
+ <span className="text-muted-foreground">Tax</span>
95
+ <span className="font-medium">${orderSummary.tax.toFixed(2)}</span>
96
+ </div>
97
+ {(orderSummary.discount ?? 0) > 0 && (
98
+ <div className="flex items-center justify-between">
99
+ <span className="text-muted-foreground">Discount</span>
100
+ <span className="font-medium text-destructive">
101
+ -${(orderSummary.discount ?? 0).toFixed(2)}
102
+ </span>
103
+ </div>
104
+ )}
105
+ {(orderSummary.promoDiscount ?? 0) > 0 && (
106
+ <div className="flex items-center justify-between">
107
+ <span className="text-muted-foreground">Promo discount</span>
108
+ <span className="font-medium text-success">
109
+ -${(orderSummary.promoDiscount ?? 0).toFixed(2)}
110
+ </span>
111
+ </div>
112
+ )}
113
+ </div>
114
+
115
+ <Separator />
116
+
117
+ <div className="flex items-center justify-between">
118
+ <span className="font-semibold">Total</span>
119
+ <span className="font-semibold">${total.toFixed(2)}</span>
120
+ </div>
121
+
122
+ <div className="flex flex-col gap-2 text-sm text-muted-foreground">
123
+ <span className="flex items-center gap-2">
124
+ <Shield className="size-4" />
125
+ SSL encrypted checkout
126
+ </span>
127
+ <span className="flex items-center gap-2">
128
+ <Truck className="size-4" />
129
+ Free shipping on orders over $75
130
+ </span>
131
+ <span className="flex items-center gap-2">
132
+ <Gift className="size-4" />
133
+ 30-day return policy
134
+ </span>
135
+ </div>
136
+ </CardContent>
137
+ </Card>
138
+ )
139
+ }
@@ -0,0 +1,184 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { Eye, EyeOff } from 'lucide-react'
5
+ import { Field, FieldContent, FieldLabel, FieldLegend, FieldSet } from '../../../ui/field'
6
+ import { Input } from '../../../ui/input'
7
+ import {
8
+ InputGroup,
9
+ InputGroupAddon,
10
+ InputGroupButton,
11
+ InputGroupInput,
12
+ } from '../../../ui/input-group'
13
+ import { RadioGroup, RadioGroupItem } from '../../../ui/radio-group'
14
+ import { CheckboxCheck } from './checkbox-check'
15
+ import { CheckoutFieldFeedback, checkoutFieldAria } from './checkout-field-feedback'
16
+ import type { PaymentMethod } from '@thorprovider/types'
17
+ import type {
18
+ CheckoutForm1ChangeHandler,
19
+ CheckoutForm1FieldDescriptions,
20
+ CheckoutForm1FieldErrors,
21
+ CheckoutForm1FormData,
22
+ } from './checkout-form-1'
23
+
24
+ export interface CheckoutForm1PaymentStepProps {
25
+ formData: CheckoutForm1FormData
26
+ onChange: CheckoutForm1ChangeHandler
27
+ onCardNumberChange: (value: string) => void
28
+ onExpiryChange: (value: string) => void
29
+ paymentMethods?: PaymentMethod[]
30
+ noPaymentMethodsLabel?: string
31
+ fieldErrors?: CheckoutForm1FieldErrors
32
+ fieldDescriptions?: CheckoutForm1FieldDescriptions
33
+ }
34
+
35
+ export function CheckoutForm1PaymentStep({
36
+ formData,
37
+ onChange,
38
+ onCardNumberChange,
39
+ onExpiryChange,
40
+ paymentMethods = [],
41
+ noPaymentMethodsLabel = 'No payment methods are available for this region.',
42
+ fieldErrors,
43
+ fieldDescriptions,
44
+ }: CheckoutForm1PaymentStepProps) {
45
+ const [showCvv, setShowCvv] = useState(false)
46
+
47
+ const selectedMethod = paymentMethods.find(method => method.id === formData.paymentMethod)
48
+ const isCard = selectedMethod?.type === 'card'
49
+
50
+ return (
51
+ <FieldSet>
52
+ <FieldLegend>Payment method</FieldLegend>
53
+ {paymentMethods.length === 0 ? (
54
+ <p className="text-muted-foreground text-sm">{noPaymentMethodsLabel}</p>
55
+ ) : (
56
+ <RadioGroup value={formData.paymentMethod} onValueChange={value => value && onChange('paymentMethod', value)} className="grid gap-3">
57
+ {paymentMethods.map(method => (
58
+ <Field key={method.id} orientation="horizontal" className="justify-between rounded-lg border p-4">
59
+ <FieldContent>
60
+ <FieldLabel htmlFor={`payment-${method.id}`} className="flex flex-col gap-0.5">
61
+ <span>{method.name}</span>
62
+ {method.description ? (
63
+ <span className="text-muted-foreground text-xs font-normal">{method.description}</span>
64
+ ) : null}
65
+ </FieldLabel>
66
+ </FieldContent>
67
+ <RadioGroupItem value={method.id} id={`payment-${method.id}`} />
68
+ </Field>
69
+ ))}
70
+ </RadioGroup>
71
+ )}
72
+
73
+ {selectedMethod && !isCard ? (
74
+ <div className="bg-muted/50 rounded-lg p-4 text-sm">
75
+ <p className="font-medium">{selectedMethod.name}</p>
76
+ {selectedMethod.description ? <p className="text-muted-foreground mt-1">{selectedMethod.description}</p> : null}
77
+ </div>
78
+ ) : null}
79
+
80
+ {isCard ? (
81
+ <>
82
+ <Field data-invalid={!!fieldErrors?.cardNumber?.length}>
83
+ <FieldLabel htmlFor="checkout-card-number">Card number</FieldLabel>
84
+ <Input
85
+ id="checkout-card-number"
86
+ inputMode="numeric"
87
+ maxLength={19}
88
+ placeholder="1234 5678 9012 3456"
89
+ value={formData.cardNumber}
90
+ onChange={(e) => onCardNumberChange(e.target.value)}
91
+ {...checkoutFieldAria('checkout-card-number', fieldErrors?.cardNumber)}
92
+ />
93
+ <CheckoutFieldFeedback
94
+ id="checkout-card-number"
95
+ errors={fieldErrors?.cardNumber}
96
+ description={fieldDescriptions?.cardNumber}
97
+ />
98
+ </Field>
99
+ <div className="grid gap-4 md:grid-cols-2">
100
+ <Field data-invalid={!!fieldErrors?.expiryDate?.length}>
101
+ <FieldLabel htmlFor="checkout-expiry">Expiry date</FieldLabel>
102
+ <Input
103
+ id="checkout-expiry"
104
+ inputMode="numeric"
105
+ maxLength={5}
106
+ placeholder="MM/YY"
107
+ value={formData.expiryDate}
108
+ onChange={(e) => onExpiryChange(e.target.value)}
109
+ {...checkoutFieldAria('checkout-expiry', fieldErrors?.expiryDate)}
110
+ />
111
+ <CheckoutFieldFeedback
112
+ id="checkout-expiry"
113
+ errors={fieldErrors?.expiryDate}
114
+ description={fieldDescriptions?.expiryDate}
115
+ />
116
+ </Field>
117
+ <Field data-invalid={!!fieldErrors?.cvv?.length}>
118
+ <FieldLabel htmlFor="checkout-cvv">CVV</FieldLabel>
119
+ <InputGroup>
120
+ <InputGroupInput
121
+ id="checkout-cvv"
122
+ inputMode="numeric"
123
+ maxLength={4}
124
+ placeholder="123"
125
+ type={showCvv ? 'text' : 'password'}
126
+ value={formData.cvv}
127
+ onChange={(e) => onChange('cvv', e.target.value)}
128
+ {...checkoutFieldAria('checkout-cvv', fieldErrors?.cvv)}
129
+ />
130
+ <InputGroupAddon inline-end>
131
+ <InputGroupButton
132
+ type="button"
133
+ variant="ghost"
134
+ size="icon-xs"
135
+ aria-label={showCvv ? 'Hide CVV' : 'Show CVV'}
136
+ onClick={() => setShowCvv((s) => !s)}
137
+ >
138
+ {showCvv ? <EyeOff /> : <Eye />}
139
+ </InputGroupButton>
140
+ </InputGroupAddon>
141
+ </InputGroup>
142
+ <CheckoutFieldFeedback
143
+ id="checkout-cvv"
144
+ errors={fieldErrors?.cvv}
145
+ description={fieldDescriptions?.cvv}
146
+ />
147
+ </Field>
148
+ </div>
149
+ <Field data-invalid={!!fieldErrors?.cardName?.length}>
150
+ <FieldLabel htmlFor="checkout-card-name">Name on card</FieldLabel>
151
+ <Input
152
+ id="checkout-card-name"
153
+ placeholder="John Doe"
154
+ value={formData.cardName}
155
+ onChange={(e) => onChange('cardName', e.target.value)}
156
+ {...checkoutFieldAria('checkout-card-name', fieldErrors?.cardName)}
157
+ />
158
+ <CheckoutFieldFeedback
159
+ id="checkout-card-name"
160
+ errors={fieldErrors?.cardName}
161
+ description={fieldDescriptions?.cardName}
162
+ />
163
+ </Field>
164
+ <Field orientation="horizontal" className="gap-2" data-invalid={!!fieldErrors?.saveInfo?.length}>
165
+ <CheckboxCheck
166
+ id="checkout-save-info"
167
+ checked={formData.saveInfo}
168
+ onCheckedChange={(checked) => onChange('saveInfo', checked === true)}
169
+ {...checkoutFieldAria('checkout-save-info', fieldErrors?.saveInfo)}
170
+ />
171
+ <FieldContent>
172
+ <FieldLabel htmlFor="checkout-save-info">Save payment info for next time</FieldLabel>
173
+ <CheckoutFieldFeedback
174
+ id="checkout-save-info"
175
+ errors={fieldErrors?.saveInfo}
176
+ description={fieldDescriptions?.saveInfo}
177
+ />
178
+ </FieldContent>
179
+ </Field>
180
+ </>
181
+ ) : null}
182
+ </FieldSet>
183
+ )
184
+ }
@@ -0,0 +1,36 @@
1
+ 'use client'
2
+
3
+ import { cn } from '../../../lib/utils'
4
+
5
+ const STEP_COUNT = 3
6
+
7
+ export interface CheckoutForm1ProgressProps {
8
+ step: number
9
+ }
10
+
11
+ export function CheckoutForm1Progress({ step }: CheckoutForm1ProgressProps) {
12
+ return (
13
+ <div className="flex items-center justify-center gap-2">
14
+ {Array.from({ length: STEP_COUNT }, (_, index) => {
15
+ const current = index + 1
16
+ const isDone = current <= step
17
+ return (
18
+ <div key={current} className="flex items-center gap-2">
19
+ {index > 0 && (
20
+ <div aria-hidden="true" className={cn('h-1 w-16', isDone ? 'bg-primary' : 'bg-muted')} />
21
+ )}
22
+ <div
23
+ aria-current={current === step ? 'step' : undefined}
24
+ className={cn(
25
+ 'flex size-10 items-center justify-center rounded-full text-sm font-medium',
26
+ isDone ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'
27
+ )}
28
+ >
29
+ {current}
30
+ </div>
31
+ </div>
32
+ )
33
+ })}
34
+ </div>
35
+ )
36
+ }
@@ -0,0 +1,107 @@
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
+ CheckoutForm1CountryOption,
9
+ CheckoutForm1FieldDescriptions,
10
+ CheckoutForm1FieldErrors,
11
+ CheckoutForm1FormData,
12
+ } from './checkout-form-1'
13
+ import { CheckoutForm1CountryField } from './checkout-form-1-country-field'
14
+
15
+ export interface CheckoutForm1ShippingStepProps {
16
+ formData: CheckoutForm1FormData
17
+ onChange: CheckoutForm1ChangeHandler
18
+ countries?: CheckoutForm1CountryOption[]
19
+ fieldErrors?: CheckoutForm1FieldErrors
20
+ fieldDescriptions?: CheckoutForm1FieldDescriptions
21
+ }
22
+
23
+ export function CheckoutForm1ShippingStep({
24
+ formData,
25
+ onChange,
26
+ countries,
27
+ fieldErrors,
28
+ fieldDescriptions,
29
+ }: CheckoutForm1ShippingStepProps) {
30
+ return (
31
+ <FieldGroup className="grid gap-4">
32
+ <Field data-invalid={!!fieldErrors?.address?.length}>
33
+ <FieldLabel htmlFor="checkout-address">Street address</FieldLabel>
34
+ <Input
35
+ id="checkout-address"
36
+ placeholder="123 Main St"
37
+ value={formData.address}
38
+ onChange={(e) => onChange('address', e.target.value)}
39
+ {...checkoutFieldAria('checkout-address', fieldErrors?.address)}
40
+ />
41
+ <CheckoutFieldFeedback
42
+ id="checkout-address"
43
+ errors={fieldErrors?.address}
44
+ description={fieldDescriptions?.address}
45
+ />
46
+ </Field>
47
+ <div className="grid gap-4 md:grid-cols-2">
48
+ <Field data-invalid={!!fieldErrors?.city?.length}>
49
+ <FieldLabel htmlFor="checkout-city">City</FieldLabel>
50
+ <Input
51
+ id="checkout-city"
52
+ placeholder="San Francisco"
53
+ value={formData.city}
54
+ onChange={(e) => onChange('city', e.target.value)}
55
+ {...checkoutFieldAria('checkout-city', fieldErrors?.city)}
56
+ />
57
+ <CheckoutFieldFeedback
58
+ id="checkout-city"
59
+ errors={fieldErrors?.city}
60
+ description={fieldDescriptions?.city}
61
+ />
62
+ </Field>
63
+ <Field data-invalid={!!fieldErrors?.state?.length}>
64
+ <FieldLabel htmlFor="checkout-state">State / Province</FieldLabel>
65
+ <Input
66
+ id="checkout-state"
67
+ placeholder="CA"
68
+ value={formData.state}
69
+ onChange={(e) => onChange('state', e.target.value)}
70
+ {...checkoutFieldAria('checkout-state', fieldErrors?.state)}
71
+ />
72
+ <CheckoutFieldFeedback
73
+ id="checkout-state"
74
+ errors={fieldErrors?.state}
75
+ description={fieldDescriptions?.state}
76
+ />
77
+ </Field>
78
+ </div>
79
+ <div className="grid gap-4 md:grid-cols-2">
80
+ <Field data-invalid={!!fieldErrors?.zipCode?.length}>
81
+ <FieldLabel htmlFor="checkout-zip">ZIP / Postal code</FieldLabel>
82
+ <Input
83
+ id="checkout-zip"
84
+ placeholder="94103"
85
+ value={formData.zipCode}
86
+ onChange={(e) => onChange('zipCode', e.target.value)}
87
+ {...checkoutFieldAria('checkout-zip', fieldErrors?.zipCode)}
88
+ />
89
+ <CheckoutFieldFeedback
90
+ id="checkout-zip"
91
+ errors={fieldErrors?.zipCode}
92
+ description={fieldDescriptions?.zipCode}
93
+ />
94
+ </Field>
95
+ {countries && countries.length > 0 && (
96
+ <CheckoutForm1CountryField
97
+ countries={countries}
98
+ value={formData.country}
99
+ onValueChange={(value) => onChange('country', value)}
100
+ errors={fieldErrors?.country}
101
+ description={fieldDescriptions?.country}
102
+ />
103
+ )}
104
+ </div>
105
+ </FieldGroup>
106
+ )
107
+ }