@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,235 @@
1
+ import { z } from 'zod'
2
+ import type { Money, PaymentMethod, ProductPreview } from '@thorprovider/types'
3
+
4
+ /**
5
+ * Serialization contracts for spec-driven storefronts.
6
+ *
7
+ * These Zod schemas are the *serializable subset* of what a spec may write for
8
+ * a block. The component keeps its rich React interface; the catalog/registry
9
+ * is the adapter that maps these props to real component props and binds
10
+ * callbacks (see storefronts-on-blocks design D3). `z.infer` is the typed
11
+ * boundary between spec and catalog — a prop outside the schema is a compile
12
+ * error, not a runtime surprise.
13
+ *
14
+ * Data shapes are projections of the @thorprovider/types canon so that
15
+ * @thorprovider/adapters output validates without conflict:
16
+ * - Prices are plain numbers here while the canon `Money` is a decimal string
17
+ * ({ amount: "45.00", currencyCode }). The projection happens at the
18
+ * loader boundary.
19
+ * - Blocks use `title` while the canon `ProductPreview` uses `name`. Reconcile
20
+ * via the canonical aliases below or the catalog/loader adapter.
21
+ */
22
+ export type CanonMoney = Money
23
+ export type CanonProduct = ProductPreview
24
+
25
+ /**
26
+ * Fallback data-URI for required image fields when the data source has no
27
+ * image. Image fields are non-optional `z.string()` in these contracts, so the
28
+ * boundary must never emit `""` (React warns about `src=""`); it must emit a
29
+ * real, renderable value. Hosts may override with their own branded fallback
30
+ * (e.g. electrostore's `CATEGORY_IMAGE_FALLBACK`), but blocks ship a neutral
31
+ * one so direct consumers are safe by default.
32
+ */
33
+ export const PRODUCT_IMAGE_FALLBACK =
34
+ 'data:image/svg+xml;charset=utf-8,' +
35
+ encodeURIComponent(
36
+ `<svg xmlns='http://www.w3.org/2000/svg' width='600' height='600'><rect width='100%' height='100%' fill='#e2e8f0'/><g fill='none' stroke='#94a3b8' stroke-width='12' stroke-linecap='round'><path d='M150 300h300M300 150v300'/><path d='M220 230h50M220 300h50M220 370h50'/><path d='M330 230h50M330 300h50M330 370h50'/></g></svg>`,
37
+ )
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Data contracts
41
+ // ---------------------------------------------------------------------------
42
+
43
+ export const cardProductSchema = z.object({
44
+ id: z.union([z.string(), z.number()]),
45
+ title: z.string(),
46
+ price: z.number(),
47
+ originalPrice: z.number().optional(),
48
+ image: z.string(),
49
+ badge: z.string().optional(),
50
+ })
51
+ export type CardProduct = z.infer<typeof cardProductSchema>
52
+
53
+ export const cartItemSchema = z.object({
54
+ id: z.string(),
55
+ name: z.string(),
56
+ price: z.number(),
57
+ quantity: z.number().int().min(0),
58
+ image: z.string(),
59
+ variant: z.string().optional(),
60
+ })
61
+ export type CartItem = z.infer<typeof cartItemSchema>
62
+
63
+ export const recentlyViewedItemSchema = z.object({
64
+ id: z.string(),
65
+ name: z.string(),
66
+ price: z.number(),
67
+ image: z.string(),
68
+ })
69
+ export type RecentlyViewedItem = z.infer<typeof recentlyViewedItemSchema>
70
+
71
+ export const promotionSchema = z.object({
72
+ threshold: z.number(),
73
+ reward: z.string(),
74
+ icon: z.string().default('gift'),
75
+ })
76
+ export type Promotion = z.infer<typeof promotionSchema>
77
+
78
+ const storeCategoryBase = {
79
+ label: z.string(),
80
+ href: z.string(),
81
+ emphasis: z.boolean().optional(),
82
+ }
83
+
84
+ export const storeCategorySchema: z.ZodType<StoreCategory> = z.lazy(() =>
85
+ z.object({
86
+ ...storeCategoryBase,
87
+ children: z.array(storeCategorySchema).optional(),
88
+ }),
89
+ )
90
+ export type StoreCategory = {
91
+ label: string
92
+ href: string
93
+ children?: StoreCategory[]
94
+ emphasis?: boolean
95
+ }
96
+
97
+ export const utilityLinkSchema = z.object({
98
+ label: z.string(),
99
+ href: z.string(),
100
+ })
101
+ export type UtilityLink = z.infer<typeof utilityLinkSchema>
102
+
103
+ export const navCartItemSchema = z.object({
104
+ id: z.string(),
105
+ name: z.string(),
106
+ variant: z.string(),
107
+ price: z.number(),
108
+ qty: z.number().int().min(0),
109
+ image: z.string(),
110
+ })
111
+ export type NavCartItem = z.infer<typeof navCartItemSchema>
112
+
113
+ export const storeUserSchema = z.object({
114
+ name: z.string(),
115
+ email: z.string(),
116
+ avatar: z.string(),
117
+ initials: z.string(),
118
+ })
119
+ export type StoreUser = z.infer<typeof storeUserSchema>
120
+
121
+ export const accountMenuItemSchema = z.object({
122
+ label: z.string(),
123
+ href: z.string(),
124
+ description: z.string().optional(),
125
+ icon: z.enum(['orders', 'wishlist', 'addresses', 'settings']).optional(),
126
+ })
127
+ export type AccountMenuItem = z.infer<typeof accountMenuItemSchema>
128
+
129
+ /**
130
+ * Serialization contract for a payment method offered at checkout.
131
+ *
132
+ * A projection of `@thorprovider/types` `PaymentMethod` — the storefront-facing
133
+ * shape produced by the adapters transform. Checkout blocks render the method
134
+ * list from this data instead of hardcoding providers.
135
+ */
136
+ export const paymentMethodSchema: z.ZodType<PaymentMethod> = z.object({
137
+ id: z.string(),
138
+ name: z.string(),
139
+ type: z.enum([
140
+ 'card',
141
+ 'wallet',
142
+ 'bank_transfer',
143
+ 'buy_now_pay_later',
144
+ 'cash_on_delivery',
145
+ 'crypto',
146
+ 'other',
147
+ ]),
148
+ isEnabled: z.boolean(),
149
+ requiresSetup: z.boolean(),
150
+ description: z.string().optional(),
151
+ metadata: z.record(z.string(), z.unknown()).optional(),
152
+ features: z
153
+ .object({
154
+ saveForLater: z.boolean().optional(),
155
+ requires3DS: z.boolean().optional(),
156
+ supportsRefunds: z.boolean().optional(),
157
+ minAmount: z.number().optional(),
158
+ maxAmount: z.number().optional(),
159
+ supportedCurrencies: z.array(z.string()).optional(),
160
+ requiresVerification: z.boolean().optional(),
161
+ })
162
+ .optional(),
163
+ logo: z.string().optional(),
164
+ })
165
+
166
+ // ---------------------------------------------------------------------------
167
+ // Block prop schemas
168
+ // ---------------------------------------------------------------------------
169
+
170
+ export const buttonPropsSchema = z.object({
171
+ variant: z
172
+ .enum(['default', 'destructive', 'outline', 'secondary', 'ghost', 'link'])
173
+ .optional(),
174
+ size: z
175
+ .enum(['default', 'xs', 'sm', 'lg', 'icon', 'icon-xs', 'icon-sm', 'icon-lg'])
176
+ .optional(),
177
+ disabled: z.boolean().optional(),
178
+ className: z.string().optional(),
179
+ })
180
+ export type ButtonSpecProps = z.infer<typeof buttonPropsSchema>
181
+
182
+ export const storeContainer1PropsSchema = z.object({
183
+ size: z.enum(['default', 'narrow', 'wide', 'full']).optional(),
184
+ className: z.string().optional(),
185
+ })
186
+ export type StoreContainer1SpecProps = z.infer<typeof storeContainer1PropsSchema>
187
+
188
+ export const productCard1PropsSchema = z.object({
189
+ products: z.array(cardProductSchema),
190
+ heading: z.string().optional(),
191
+ maxItems: z.number().int().positive().optional(),
192
+ })
193
+ export type ProductCard1SpecProps = z.infer<typeof productCard1PropsSchema>
194
+
195
+ export const shoppingCart4PropsSchema = z.object({
196
+ items: z.array(cartItemSchema),
197
+ promotions: z.array(promotionSchema).default([]),
198
+ recentlyViewed: z.array(recentlyViewedItemSchema).optional(),
199
+ taxRate: z.number().optional(),
200
+ titleLabel: z.string().optional(),
201
+ subtotalLabel: z.string().optional(),
202
+ taxLabel: z.string().optional(),
203
+ shippingLabel: z.string().optional(),
204
+ shippingFreeLabel: z.string().optional(),
205
+ totalLabel: z.string().optional(),
206
+ awayFromLabel: z.string().optional(),
207
+ activatedLabel: z.string().optional(),
208
+ itemsLabel: z.string().optional(),
209
+ expressCheckoutLabel: z.string().optional(),
210
+ expressLabel: z.string().optional(),
211
+ standardLabel: z.string().optional(),
212
+ estimatedDeliveryLabel: z.string().optional(),
213
+ secureCheckoutLabel: z.string().optional(),
214
+ recentlyViewedLabel: z.string().optional(),
215
+ collapseLabel: z.string().optional(),
216
+ expandLabel: z.string().optional(),
217
+ checkoutLabel: z.string().optional(),
218
+ quickCheckoutLabel: z.string().optional(),
219
+ checkoutNowLabel: z.string().optional(),
220
+ })
221
+ export type ShoppingCart4SpecProps = z.infer<typeof shoppingCart4PropsSchema>
222
+
223
+ export const navbar4PropsSchema = z.object({
224
+ storeCategories: z.array(storeCategorySchema),
225
+ shippingNote: z.string(),
226
+ utilityLinks: z.array(utilityLinkSchema),
227
+ logoText: z.string(),
228
+ logoHref: z.string().optional(),
229
+ accountHref: z.string().optional(),
230
+ searchQuery: z.string().optional(),
231
+ items: z.array(navCartItemSchema).optional(),
232
+ user: storeUserSchema.optional(),
233
+ accountMenuItems: z.array(accountMenuItemSchema).optional(),
234
+ })
235
+ export type Navbar4SpecProps = z.infer<typeof navbar4PropsSchema>
@@ -0,0 +1,43 @@
1
+ import { z } from 'zod'
2
+
3
+ /**
4
+ * Canonical navigation types: recursive menu item and collection item.
5
+ *
6
+ * Ported from the legacy `@thorprovider/components` navigation library
7
+ * (source: branch `feat/storefronts`) into the blocks catalog so any
8
+ * consumer can feed a multi-level category tree to the navigation blocks.
9
+ */
10
+
11
+ export const menuItemSchema: z.ZodType<{
12
+ id: string
13
+ label: string
14
+ icon?: string
15
+ image?: string
16
+ href?: string
17
+ badge?: string
18
+ featured?: boolean
19
+ children?: MenuItem[]
20
+ }> = z.lazy(() =>
21
+ z.object({
22
+ id: z.string(),
23
+ label: z.string(),
24
+ icon: z.string().optional(),
25
+ image: z.string().optional(),
26
+ href: z.string().optional(),
27
+ badge: z.string().optional(),
28
+ featured: z.boolean().optional(),
29
+ children: z.array(menuItemSchema).optional(),
30
+ }),
31
+ )
32
+
33
+ export type MenuItem = z.infer<typeof menuItemSchema>
34
+
35
+ export const collectionItemSchema = z.object({
36
+ id: z.string(),
37
+ label: z.string(),
38
+ description: z.string().optional(),
39
+ image: z.string().optional(),
40
+ href: z.string().optional(),
41
+ })
42
+
43
+ export type CollectionItem = z.infer<typeof collectionItemSchema>
@@ -0,0 +1,4 @@
1
+ export interface CTA {
2
+ label: string
3
+ href?: string
4
+ }
@@ -0,0 +1,10 @@
1
+ import { clsx, type ClassValue } from 'clsx'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ /**
5
+ * Merge Tailwind CSS class names safely.
6
+ * Deduplicates conflicting utilities (e.g. `p-4` vs `p-2` → keeps last).
7
+ */
8
+ export function cn(...inputs: ClassValue[]) {
9
+ return twMerge(clsx(inputs))
10
+ }
@@ -0,0 +1,65 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "cn"
5
+ import { ChevronDownIcon } from "lucide-react"
6
+ import { Accordion as AccordionPrimitive } from "radix-ui"
7
+
8
+ function Accordion({
9
+ ...props
10
+ }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
11
+ return <AccordionPrimitive.Root data-slot="accordion" {...props} />
12
+ }
13
+
14
+ function AccordionItem({
15
+ className,
16
+ ...props
17
+ }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
18
+ return (
19
+ <AccordionPrimitive.Item
20
+ data-slot="accordion-item"
21
+ className={cn("border-b last:border-b-0", className)}
22
+ {...props}
23
+ />
24
+ )
25
+ }
26
+
27
+ function AccordionTrigger({
28
+ className,
29
+ children,
30
+ ...props
31
+ }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
32
+ return (
33
+ <AccordionPrimitive.Header className="flex">
34
+ <AccordionPrimitive.Trigger
35
+ data-slot="accordion-trigger"
36
+ className={cn(
37
+ "flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
38
+ className
39
+ )}
40
+ {...props}
41
+ >
42
+ {children}
43
+ <ChevronDownIcon className="pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200" />
44
+ </AccordionPrimitive.Trigger>
45
+ </AccordionPrimitive.Header>
46
+ )
47
+ }
48
+
49
+ function AccordionContent({
50
+ className,
51
+ children,
52
+ ...props
53
+ }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
54
+ return (
55
+ <AccordionPrimitive.Content
56
+ data-slot="accordion-content"
57
+ className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
58
+ {...props}
59
+ >
60
+ <div className={cn("pt-0 pb-4", className)}>{children}</div>
61
+ </AccordionPrimitive.Content>
62
+ )
63
+ }
64
+
65
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
@@ -0,0 +1,108 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "cn"
5
+ import { Avatar as AvatarPrimitive } from "radix-ui"
6
+
7
+ function Avatar({
8
+ className,
9
+ size = "default",
10
+ ...props
11
+ }: React.ComponentProps<typeof AvatarPrimitive.Root> & {
12
+ size?: "default" | "sm" | "lg"
13
+ }) {
14
+ return (
15
+ <AvatarPrimitive.Root
16
+ data-slot="avatar"
17
+ data-size={size}
18
+ className={cn(
19
+ "group/avatar relative flex size-8 shrink-0 overflow-hidden rounded-full select-none data-[size=lg]:size-10 data-[size=sm]:size-6",
20
+ className
21
+ )}
22
+ {...props}
23
+ />
24
+ )
25
+ }
26
+
27
+ function AvatarImage({
28
+ className,
29
+ ...props
30
+ }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
31
+ return (
32
+ <AvatarPrimitive.Image
33
+ data-slot="avatar-image"
34
+ className={cn("aspect-square size-full", className)}
35
+ {...props}
36
+ />
37
+ )
38
+ }
39
+
40
+ function AvatarFallback({
41
+ className,
42
+ ...props
43
+ }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
44
+ return (
45
+ <AvatarPrimitive.Fallback
46
+ data-slot="avatar-fallback"
47
+ className={cn(
48
+ "flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs",
49
+ className
50
+ )}
51
+ {...props}
52
+ />
53
+ )
54
+ }
55
+
56
+ function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
57
+ return (
58
+ <span
59
+ data-slot="avatar-badge"
60
+ className={cn(
61
+ "absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground ring-2 ring-background select-none",
62
+ "group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
63
+ "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
64
+ "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
65
+ className
66
+ )}
67
+ {...props}
68
+ />
69
+ )
70
+ }
71
+
72
+ function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
73
+ return (
74
+ <div
75
+ data-slot="avatar-group"
76
+ className={cn(
77
+ "group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background",
78
+ className
79
+ )}
80
+ {...props}
81
+ />
82
+ )
83
+ }
84
+
85
+ function AvatarGroupCount({
86
+ className,
87
+ ...props
88
+ }: React.ComponentProps<"div">) {
89
+ return (
90
+ <div
91
+ data-slot="avatar-group-count"
92
+ className={cn(
93
+ "relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
94
+ className
95
+ )}
96
+ {...props}
97
+ />
98
+ )
99
+ }
100
+
101
+ export {
102
+ Avatar,
103
+ AvatarImage,
104
+ AvatarFallback,
105
+ AvatarBadge,
106
+ AvatarGroup,
107
+ AvatarGroupCount,
108
+ }
@@ -0,0 +1,47 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { cn } from "cn"
4
+ import { Slot } from "radix-ui"
5
+
6
+ const badgeVariants = cva(
7
+ "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
12
+ secondary:
13
+ "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
14
+ destructive:
15
+ "bg-destructive text-destructive-foreground focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90",
16
+ outline:
17
+ "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
18
+ ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
19
+ link: "text-primary underline-offset-4 [a&]:hover:underline",
20
+ },
21
+ },
22
+ defaultVariants: {
23
+ variant: "default",
24
+ },
25
+ }
26
+ )
27
+
28
+ function Badge({
29
+ className,
30
+ variant = "default",
31
+ asChild = false,
32
+ ...props
33
+ }: React.ComponentProps<"span"> &
34
+ VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
35
+ const Comp = asChild ? Slot.Root : "span"
36
+
37
+ return (
38
+ <Comp
39
+ data-slot="badge"
40
+ data-variant={variant}
41
+ className={cn(badgeVariants({ variant }), className)}
42
+ {...props}
43
+ />
44
+ )
45
+ }
46
+
47
+ export { Badge, badgeVariants }
@@ -0,0 +1,108 @@
1
+ import * as React from "react"
2
+ import { cn } from "cn"
3
+ import { ChevronRight, MoreHorizontal } from "lucide-react"
4
+ import { Slot } from "radix-ui"
5
+
6
+ function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
7
+ return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
8
+ }
9
+
10
+ function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
11
+ return (
12
+ <ol
13
+ data-slot="breadcrumb-list"
14
+ className={cn(
15
+ "flex flex-wrap items-center gap-1.5 text-sm break-words text-muted-foreground sm:gap-2.5",
16
+ className
17
+ )}
18
+ {...props}
19
+ />
20
+ )
21
+ }
22
+
23
+ function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
24
+ return (
25
+ <li
26
+ data-slot="breadcrumb-item"
27
+ className={cn("inline-flex items-center gap-1.5", className)}
28
+ {...props}
29
+ />
30
+ )
31
+ }
32
+
33
+ function BreadcrumbLink({
34
+ asChild,
35
+ className,
36
+ ...props
37
+ }: React.ComponentProps<"a"> & {
38
+ asChild?: boolean
39
+ }) {
40
+ const Comp = asChild ? Slot.Root : "a"
41
+
42
+ return (
43
+ <Comp
44
+ data-slot="breadcrumb-link"
45
+ className={cn("transition-colors hover:text-foreground", className)}
46
+ {...props}
47
+ />
48
+ )
49
+ }
50
+
51
+ function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
52
+ return (
53
+ <span
54
+ data-slot="breadcrumb-page"
55
+ role="link"
56
+ aria-disabled="true"
57
+ aria-current="page"
58
+ className={cn("font-normal text-foreground", className)}
59
+ {...props}
60
+ />
61
+ )
62
+ }
63
+
64
+ function BreadcrumbSeparator({
65
+ children,
66
+ className,
67
+ ...props
68
+ }: React.ComponentProps<"li">) {
69
+ return (
70
+ <li
71
+ data-slot="breadcrumb-separator"
72
+ role="presentation"
73
+ aria-hidden="true"
74
+ className={cn("[&>svg]:size-3.5", className)}
75
+ {...props}
76
+ >
77
+ {children ?? <ChevronRight />}
78
+ </li>
79
+ )
80
+ }
81
+
82
+ function BreadcrumbEllipsis({
83
+ className,
84
+ ...props
85
+ }: React.ComponentProps<"span">) {
86
+ return (
87
+ <span
88
+ data-slot="breadcrumb-ellipsis"
89
+ role="presentation"
90
+ aria-hidden="true"
91
+ className={cn("flex size-9 items-center justify-center", className)}
92
+ {...props}
93
+ >
94
+ <MoreHorizontal className="size-4" />
95
+ <span className="sr-only">More</span>
96
+ </span>
97
+ )
98
+ }
99
+
100
+ export {
101
+ Breadcrumb,
102
+ BreadcrumbList,
103
+ BreadcrumbItem,
104
+ BreadcrumbLink,
105
+ BreadcrumbPage,
106
+ BreadcrumbSeparator,
107
+ BreadcrumbEllipsis,
108
+ }