@thorprovider/blocks 0.1.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +108 -23
  3. package/docs/standards/data-contracts.md +13 -2
  4. package/package.json +26 -10
  5. package/scripts/fix-esm-extensions.mjs +40 -0
  6. package/src/blocks/e-commerce/navigation/back-to-top.test.tsx +27 -0
  7. package/src/blocks/e-commerce/navigation/back-to-top.tsx +71 -0
  8. package/src/blocks/e-commerce/navigation/breadcrumb.test.tsx +174 -0
  9. package/src/blocks/e-commerce/navigation/breadcrumb.tsx +130 -0
  10. package/src/blocks/e-commerce/navigation/mobile-bottom-nav.test.tsx +74 -0
  11. package/src/blocks/e-commerce/navigation/mobile-bottom-nav.tsx +78 -0
  12. package/src/blocks/e-commerce/navigation/skip-to-content.test.tsx +35 -0
  13. package/src/blocks/e-commerce/navigation/skip-to-content.tsx +37 -0
  14. package/src/blocks/e-commerce/product-categories/category-card-grid.test.tsx +122 -0
  15. package/src/blocks/e-commerce/product-categories/category-card.test.tsx +68 -0
  16. package/src/blocks/e-commerce/product-categories/category-card.tsx +172 -0
  17. package/src/blocks/e-commerce/product-categories/category-grid.test.tsx +67 -0
  18. package/src/blocks/e-commerce/product-categories/category-shared.tsx +109 -0
  19. package/src/blocks/e-commerce/product-categories/product-category-grid.tsx +170 -0
  20. package/src/blocks/e-commerce/product-categories/product-category.test.tsx +266 -0
  21. package/src/blocks/e-commerce/product-categories/product-category.tsx +462 -0
  22. package/src/blocks/e-commerce/product-list/product-card.test.tsx +263 -0
  23. package/src/blocks/e-commerce/product-list/product-card.tsx +424 -0
  24. package/src/blocks/e-commerce/product-list/product-grid.test.tsx +162 -0
  25. package/src/blocks/e-commerce/product-list/product-grid.tsx +114 -0
  26. package/src/blocks/e-commerce/product-list/product-showcase-1.test.tsx +78 -0
  27. package/src/blocks/e-commerce/product-list/product-showcase-1.tsx +277 -0
  28. package/src/blocks/e-commerce/product-overview/product-overview-gallery.tsx +71 -0
  29. package/src/blocks/e-commerce/product-overview/{product-overview-5-lightbox-tab.tsx → product-overview-lightbox-tab.tsx} +35 -23
  30. package/src/blocks/e-commerce/product-overview/{product-overview-5-reviews-tab.tsx → product-overview-reviews-tab.tsx} +40 -17
  31. package/src/blocks/e-commerce/product-overview/{product-overview-5-tabs.tsx → product-overview-tabs.tsx} +8 -7
  32. package/src/blocks/e-commerce/product-overview/product-overview.test.tsx +283 -0
  33. package/src/blocks/e-commerce/product-overview/product-overview.tsx +1186 -0
  34. package/src/blocks/e-commerce/reviews/review-rating-2.tsx +5 -6
  35. package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +399 -0
  36. package/src/blocks/e-commerce/shopping-carts/{shopping-cart-4.tsx → shopping-cart.tsx} +204 -109
  37. package/src/blocks/e-commerce/storefront-hero/storefront-hero.test.tsx +412 -0
  38. package/src/blocks/e-commerce/storefront-hero/storefront-hero.tsx +1283 -0
  39. package/src/blocks/marketing/features/feature.test.tsx +153 -0
  40. package/src/blocks/marketing/features/feature.tsx +195 -0
  41. package/src/blocks/marketing/navbars/navbar-4-account-menu.test.tsx +56 -0
  42. package/src/blocks/marketing/navbars/navbar-4-account-menu.tsx +30 -14
  43. package/src/blocks/marketing/navbars/navbar-4-cart-sheet.test.tsx +150 -0
  44. package/src/blocks/marketing/navbars/navbar-4-cart-sheet.tsx +102 -9
  45. package/src/blocks/marketing/navbars/navbar-4.test.tsx +154 -2
  46. package/src/blocks/marketing/navbars/navbar-4.tsx +152 -32
  47. package/src/index.ts +93 -46
  48. package/src/lib/contracts-curated.test.ts +42 -24
  49. package/src/lib/contracts.test.ts +143 -2
  50. package/src/lib/contracts.ts +395 -13
  51. package/src/ui/button-group.tsx +1 -1
  52. package/src/ui/carousel.tsx +1 -1
  53. package/src/ui/dialog.tsx +1 -1
  54. package/src/ui/field.tsx +2 -2
  55. package/src/ui/input-group.tsx +3 -3
  56. package/src/ui/pagination.tsx +1 -1
  57. package/src/ui/sidebar.tsx +7 -7
  58. package/src/ui/toggle-group.tsx +1 -1
  59. package/tsconfig.build.json +12 -0
  60. package/tsup.config.ts +11 -7
  61. package/.turbo/turbo-build.log +0 -24
  62. package/.turbo/turbo-test.log +0 -37
  63. package/.turbo/turbo-type-check.log +0 -0
  64. package/dist/.tsbuildinfo +0 -1
  65. package/dist/contracts.d.mts +0 -1584
  66. package/dist/contracts.d.ts +0 -1584
  67. package/dist/contracts.js +0 -390
  68. package/dist/contracts.js.map +0 -1
  69. package/dist/contracts.mjs +0 -334
  70. package/dist/contracts.mjs.map +0 -1
  71. package/dist/index.d.mts +0 -2786
  72. package/dist/index.d.ts +0 -2786
  73. package/dist/index.js +0 -17139
  74. package/dist/index.js.map +0 -1
  75. package/dist/index.mjs +0 -17059
  76. package/dist/index.mjs.map +0 -1
  77. package/src/blocks/e-commerce/product-categories/avatar-group.tsx +0 -86
  78. package/src/blocks/e-commerce/product-categories/product-category-1.tsx +0 -126
  79. package/src/blocks/e-commerce/product-categories/product-category-2.test.tsx +0 -51
  80. package/src/blocks/e-commerce/product-categories/product-category-2.tsx +0 -120
  81. package/src/blocks/e-commerce/product-categories/product-category-4.tsx +0 -129
  82. package/src/blocks/e-commerce/product-categories/product-category-5.tsx +0 -218
  83. package/src/blocks/e-commerce/product-list/product-card-1.tsx +0 -131
  84. package/src/blocks/e-commerce/product-list/product-card-2.tsx +0 -71
  85. package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +0 -166
  86. package/src/blocks/e-commerce/product-list/product-card-3.tsx +0 -215
  87. package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +0 -98
  88. package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +0 -171
  89. package/src/blocks/e-commerce/product-list/product-card-5.tsx +0 -367
  90. package/src/blocks/e-commerce/product-overview/product-overview-1.test.tsx +0 -182
  91. package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +0 -370
  92. package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +0 -272
  93. package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +0 -59
  94. package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +0 -274
  95. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +0 -142
  96. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +0 -291
  97. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +0 -110
  98. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +0 -51
  99. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +0 -146
  100. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +0 -295
  101. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +0 -142
  102. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +0 -342
  103. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +0 -146
  104. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.test.tsx +0 -137
  105. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx +0 -65
  106. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +0 -263
  107. package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +0 -283
  108. package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +0 -318
  109. package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +0 -205
  110. package/src/blocks/marketing/features/feature-1.tsx +0 -97
  111. package/src/blocks/marketing/features/feature-2.tsx +0 -82
@@ -95,13 +95,31 @@ export const productDetailProductSchema = z.object({
95
95
  })
96
96
  export type ProductDetailProductSpec = z.infer<typeof productDetailProductSchema>
97
97
 
98
- export const productOverview1PropsSchema = z.object({
98
+ /**
99
+ * Canonical ProductOverview contract. Uses the canonical product projection and adds the
100
+ * bounded `presentation` option plus the `rich` presentation's optional data. Callbacks are not
101
+ * part of the schema (they are runtime props).
102
+ */
103
+ export const productOverviewReviewSchema = z.object({
104
+ id: z.number(),
105
+ author: z.string(),
106
+ rating: z.number(),
107
+ comment: z.string(),
108
+ date: z.string(),
109
+ helpful: z.number().optional(),
110
+ })
111
+
112
+ export const productOverviewPropsSchema = z.object({
99
113
  product: productDetailProductSchema,
114
+ presentation: z.enum(['standard', 'gallery', 'rich']).optional(),
100
115
  selectedVariantId: z.string().nullable().optional(),
101
116
  quantity: z.number().optional(),
102
117
  rating: z.object({ value: z.number(), count: z.number() }).optional(),
103
118
  deliveryEstimate: z.string().optional(),
104
119
  shipping: moneySchema.optional(),
120
+ specifications: z.record(z.string(), z.string()).optional(),
121
+ features: z.array(z.string()).optional(),
122
+ reviews: z.array(productOverviewReviewSchema).optional(),
105
123
  sizesLabel: z.string().optional(),
106
124
  variantLabel: z.string().optional(),
107
125
  reviewsLabel: z.string().optional(),
@@ -112,8 +130,24 @@ export const productOverview1PropsSchema = z.object({
112
130
  quantityLabel: z.string().optional(),
113
131
  deliveryEstimateLabel: z.string().optional(),
114
132
  shippingLabel: z.string().optional(),
133
+ badgeLabel: z.string().optional(),
134
+ availabilityLabel: z.string().optional(),
135
+ skuLabel: z.string().optional(),
136
+ overviewLabel: z.string().optional(),
137
+ specsLabel: z.string().optional(),
138
+ featuresLabel: z.string().optional(),
139
+ specificationsLabel: z.string().optional(),
140
+ galleryLabel: z.string().optional(),
141
+ photosLabel: z.string().optional(),
142
+ saveLabel: z.string().optional(),
143
+ savedLabel: z.string().optional(),
144
+ shareLabel: z.string().optional(),
145
+ basedOnReviewsLabel: z.string().optional(),
146
+ foundHelpfulLabel: z.string().optional(),
147
+ ofCounterLabel: z.string().optional(),
148
+ freeDeliveryLabel: z.string().optional(),
115
149
  })
116
- export type ProductOverview1SpecProps = z.infer<typeof productOverview1PropsSchema>
150
+ export type ProductOverviewSpecProps = z.infer<typeof productOverviewPropsSchema>
117
151
 
118
152
  export const cardProductSchema = z.object({
119
153
  id: z.union([z.string(), z.number()]),
@@ -203,7 +237,7 @@ export type NavCartItem = z.infer<typeof navCartItemSchema>
203
237
  export const storeUserSchema = z.object({
204
238
  name: z.string(),
205
239
  email: z.string(),
206
- avatar: z.string(),
240
+ avatar: z.string().optional(),
207
241
  initials: z.string(),
208
242
  })
209
243
  export type StoreUser = z.infer<typeof storeUserSchema>
@@ -275,17 +309,129 @@ export const storeContainer1PropsSchema = z.object({
275
309
  })
276
310
  export type StoreContainer1SpecProps = z.infer<typeof storeContainer1PropsSchema>
277
311
 
278
- export const productCard1PropsSchema = z.object({
279
- products: z.array(cardProductSchema),
312
+ /**
313
+ * Canonical product presentation — the serializable projection a storefront host
314
+ * produces from a canonical `Product` for the merchandising sections. Unlike
315
+ * `cardProductSchema` (legacy, numeric `price`), `price`/`compareAtPrice` carry
316
+ * canonical `Money` (decimal major-unit string + currency code), so the block
317
+ * formats in the shopper's currency and locale and renders a truthful
318
+ * unavailable state instead of `$0`. Optional fields are omitted, never `""`.
319
+ *
320
+ * This contract is additive: `cardProductSchema` and its consumers are
321
+ * unchanged, and a legacy numeric `price` still renders the legacy output.
322
+ */
323
+ export const productPresentationSchema = z.object({
324
+ id: z.union([z.string(), z.number()]),
325
+ title: z.string(),
326
+ /** Omitted (never `""`) when the backend has no image. */
327
+ image: z.string().optional(),
328
+ imageAlt: z.string().optional(),
329
+ /** Product destination; omitted when unknown. */
330
+ href: z.string().optional(),
331
+ price: moneySchema,
332
+ /** Comparison ("was") price; rendered only when valid and higher than `price`. */
333
+ compareAtPrice: moneySchema.optional(),
334
+ /** Only from real evidence; omitted otherwise (never fabricated). */
335
+ rating: z.number().optional(),
336
+ badge: z.string().optional(),
337
+ })
338
+ export type ProductPresentation = z.infer<typeof productPresentationSchema>
339
+
340
+ /**
341
+ * Canonical product card contract (item cell of a product listing). The cell
342
+ * consumes one canonical `ProductPresentation` projection only — the legacy
343
+ * numeric-price card shape is not accepted. `presentation` is the bounded
344
+ * structure switch (`classic`/`minimal`/`rich`) and `wishlisted` is the
345
+ * host-resolved membership state; callbacks and label strings are runtime
346
+ * props and are excluded here. Collection concerns (heading, item cap, loading
347
+ * and empty states) belong to `ProductGrid`.
348
+ */
349
+ export const productCardPropsSchema = z.object({
350
+ item: productPresentationSchema,
351
+ presentation: z.enum(['classic', 'minimal', 'rich']).optional(),
352
+ wishlisted: z.boolean().optional(),
353
+ })
354
+ export type ProductCardSpecProps = z.infer<typeof productCardPropsSchema>
355
+
356
+ /**
357
+ * Canonical product grid contract (listing). Consumes a collection of
358
+ * canonical `ProductPresentation` items and owns the listing's heading, item
359
+ * cap, wishlist membership, and loading/empty/ready states. `wishlistedIds` is
360
+ * resolved per cell before rendering, so the cell receives only its own
361
+ * boolean. Callbacks and label strings are runtime props and are excluded
362
+ * here. Error state belongs to the host.
363
+ */
364
+ export const productGridPropsSchema = z.object({
365
+ items: z.array(productPresentationSchema),
366
+ presentation: z.enum(['classic', 'minimal', 'rich']).optional(),
280
367
  heading: z.string().optional(),
281
368
  maxItems: z.number().int().positive().optional(),
369
+ wishlistedIds: z.array(z.union([z.string(), z.number()])).readonly().optional(),
370
+ loading: z.boolean().optional(),
371
+ skeletonCount: z.number().int().positive().optional(),
372
+ emptyLabel: z.string().optional(),
373
+ })
374
+ export type ProductGridSpecProps = z.infer<typeof productGridPropsSchema>
375
+
376
+ /**
377
+ * Serializable contract for the combined grid-and-carousel product
378
+ * presentation (`ProductShowcase1`). The grid pages are fixed-size, navigated
379
+ * manually (prev/next) with a visible current-page indication; there is no
380
+ * autoplay and no infinite loop. React-only callbacks are excluded.
381
+ */
382
+ export const productShowcase1PropsSchema = z.object({
383
+ products: z.array(productPresentationSchema),
384
+ heading: z.string().optional(),
385
+ description: z.string().optional(),
386
+ pageSize: z.number().int().positive().optional(),
387
+ prevLabel: z.string().optional(),
388
+ nextLabel: z.string().optional(),
389
+ pageLabel: z.string().optional(),
390
+ unavailablePriceLabel: z.string().optional(),
391
+ emptyLabel: z.string().optional(),
392
+ addToCartLabel: z.string().optional(),
282
393
  })
283
- export type ProductCard1SpecProps = z.infer<typeof productCard1PropsSchema>
394
+ export type ProductShowcase1SpecProps = z.infer<typeof productShowcase1PropsSchema>
284
395
 
285
- export const shoppingCart4PropsSchema = z.object({
286
- items: z.array(cartItemSchema),
396
+ /**
397
+ * Canonical shopping cart contract (consolidated shopping-cart family). Lines
398
+ * are keyed by the backend cart line-item id and carry canonical `Money`
399
+ * amounts only — the legacy numeric-price shape is not accepted: a line always
400
+ * carries the backend-authoritative `lineTotal`, and an optional `unitPrice` is
401
+ * carried verbatim (never used for a local `price * quantity` computation).
402
+ * Suggestions fold the numbered carts' recommendations: optional image (omitted
403
+ * when absent) and optional canonical `Money` price. `presentation` is the
404
+ * bounded structure switch (`compact`/`page`); callbacks and interaction state
405
+ * are runtime props and are excluded here.
406
+ */
407
+ export const shoppingCartItemSchema = z.object({
408
+ id: z.string(),
409
+ name: z.string(),
410
+ quantity: z.number().int().min(0),
411
+ /** Optional; absent images are omitted, never serialized as `""`. */
412
+ image: z.string().optional(),
413
+ variant: z.string().optional(),
414
+ /** Canonical backend line total; a malformed amount renders as unavailable. */
415
+ lineTotal: moneySchema,
416
+ /** Canonical backend unit price; carried verbatim, never used locally. */
417
+ unitPrice: moneySchema.optional(),
418
+ })
419
+ export type ShoppingCartItem = z.infer<typeof shoppingCartItemSchema>
420
+
421
+ export const shoppingCartSuggestionSchema = z.object({
422
+ id: z.string(),
423
+ name: z.string(),
424
+ /** Optional; absent images are omitted, never serialized as `""`. */
425
+ image: z.string().optional(),
426
+ unitPrice: moneySchema.optional(),
427
+ })
428
+ export type ShoppingCartSuggestion = z.infer<typeof shoppingCartSuggestionSchema>
429
+
430
+ export const shoppingCartPropsSchema = z.object({
431
+ items: z.array(shoppingCartItemSchema),
287
432
  promotions: z.array(promotionSchema).default([]),
288
- recentlyViewed: z.array(recentlyViewedItemSchema).optional(),
433
+ recentlyViewed: z.array(shoppingCartSuggestionSchema).optional(),
434
+ presentation: z.enum(['compact', 'page']).optional(),
289
435
  /** Backend-authoritative totals; rendered verbatim, never computed locally. */
290
436
  totals: cartTotalsSchema.optional(),
291
437
  totalsState: z.enum(['idle', 'pending', 'error']).optional(),
@@ -293,7 +439,6 @@ export const shoppingCart4PropsSchema = z.object({
293
439
  totalsUnavailableLabel: z.string().optional(),
294
440
  amountUnavailableLabel: z.string().optional(),
295
441
  retryLabel: z.string().optional(),
296
- taxRate: z.number().optional(),
297
442
  titleLabel: z.string().optional(),
298
443
  subtotalLabel: z.string().optional(),
299
444
  discountLabel: z.string().optional(),
@@ -311,6 +456,9 @@ export const shoppingCart4PropsSchema = z.object({
311
456
  estimatedDeliveryLabel: z.string().optional(),
312
457
  secureCheckoutLabel: z.string().optional(),
313
458
  recentlyViewedLabel: z.string().optional(),
459
+ promoPromptLabel: z.string().optional(),
460
+ promoPlaceholderLabel: z.string().optional(),
461
+ promoApplyLabel: z.string().optional(),
314
462
  collapseLabel: z.string().optional(),
315
463
  expandLabel: z.string().optional(),
316
464
  checkoutLabel: z.string().optional(),
@@ -321,7 +469,7 @@ export const shoppingCart4PropsSchema = z.object({
321
469
  decreaseLabel: z.string().optional(),
322
470
  increaseLabel: z.string().optional(),
323
471
  })
324
- export type ShoppingCart4SpecProps = z.infer<typeof shoppingCart4PropsSchema>
472
+ export type ShoppingCartSpecProps = z.infer<typeof shoppingCartPropsSchema>
325
473
 
326
474
  /**
327
475
  * Optional brand seams shared by the existing blocks. These mirror the
@@ -349,7 +497,7 @@ export type HeroImage = z.infer<typeof heroImageSchema>
349
497
 
350
498
  export const navbar4PropsSchema = z.object({
351
499
  storeCategories: z.array(storeCategorySchema),
352
- shippingNote: z.string(),
500
+ shippingNote: z.string().optional(),
353
501
  utilityLinks: z.array(utilityLinkSchema),
354
502
  logoText: z.string(),
355
503
  logoHref: z.string().optional(),
@@ -368,6 +516,8 @@ export const navbar4PropsSchema = z.object({
368
516
  subtotalUnavailableLabel: z.string().optional(),
369
517
  user: storeUserSchema.optional(),
370
518
  accountMenuItems: z.array(accountMenuItemSchema).optional(),
519
+ accountPending: z.boolean().optional(),
520
+ accountUnavailable: z.boolean().optional(),
371
521
  })
372
522
  export type Navbar4SpecProps = z.infer<typeof navbar4PropsSchema>
373
523
 
@@ -383,6 +533,115 @@ export const ctaSchema = z.object({
383
533
  })
384
534
  export type CtaSpec = z.infer<typeof ctaSchema>
385
535
 
536
+ /**
537
+ * Serializable subset of the canonical `StorefrontHero`'s props (callbacks
538
+ * excluded). Every popularity, review, count, or countdown field is optional
539
+ * host-supplied evidence: omitted fields render nothing (honest omission)
540
+ * instead of fabricated defaults, and prices are formatted from the supplied
541
+ * ISO 4217 `currency` via `Intl.NumberFormat` (never hardcoded).
542
+ */
543
+ export const storefrontHeroProductSchema = z.object({
544
+ id: z.string(),
545
+ name: z.string(),
546
+ price: z.number(),
547
+ originalPrice: z.number().optional(),
548
+ image: z.string().optional(),
549
+ imageAlt: z.string().optional(),
550
+ brand: z.string().optional(),
551
+ colors: z.array(z.string()).optional(),
552
+ rating: z.number().optional(),
553
+ reviewCount: z.number().optional(),
554
+ inStock: z.boolean().optional(),
555
+ isNew: z.boolean().optional(),
556
+ tag: z.string().optional(),
557
+ })
558
+ export type StorefrontHeroProduct = z.infer<typeof storefrontHeroProductSchema>
559
+
560
+ export const storefrontHeroPropsSchema = z.object({
561
+ title: z.string(),
562
+ description: z.string(),
563
+ presentation: z.enum(['compact', 'showcase', 'editorial', 'promotional']).optional(),
564
+ currency: z.string().optional(),
565
+ products: z.array(storefrontHeroProductSchema),
566
+ badge: z.string().optional(),
567
+ titleAccent: z.string().optional(),
568
+ primaryCta: ctaSchema.optional(),
569
+ secondaryCta: ctaSchema.optional(),
570
+ heroImage: heroImageSchema.optional(),
571
+ departments: z
572
+ .array(
573
+ z.object({
574
+ id: z.string(),
575
+ name: z.string(),
576
+ image: z.string().optional(),
577
+ imageAlt: z.string().optional(),
578
+ count: z.string().optional(),
579
+ href: z.string().optional(),
580
+ }),
581
+ )
582
+ .optional(),
583
+ featuredDeals: z
584
+ .array(
585
+ z.object({
586
+ id: z.string(),
587
+ name: z.string(),
588
+ description: z.string().optional(),
589
+ image: z.string().optional(),
590
+ imageAlt: z.string().optional(),
591
+ price: z.number(),
592
+ originalPrice: z.number().optional(),
593
+ cta: ctaSchema.optional(),
594
+ }),
595
+ )
596
+ .optional(),
597
+ storeLocator: z
598
+ .object({
599
+ title: z.string(),
600
+ description: z.string(),
601
+ ctaLabel: z.string(),
602
+ ctaHref: z.string().optional(),
603
+ })
604
+ .optional(),
605
+ shippingInfo: z.string().optional(),
606
+ deals: z
607
+ .array(
608
+ z.object({
609
+ id: z.string(),
610
+ title: z.string(),
611
+ discount: z.string().optional(),
612
+ endTime: z.string().optional(),
613
+ }),
614
+ )
615
+ .optional(),
616
+ categories: z
617
+ .array(
618
+ z.object({
619
+ id: z.string(),
620
+ name: z.string(),
621
+ image: z.string().optional(),
622
+ imageAlt: z.string().optional(),
623
+ count: z.string().optional(),
624
+ href: z.string().optional(),
625
+ }),
626
+ )
627
+ .optional(),
628
+ rotationInterval: z.number().optional(),
629
+ addToCartLabel: z.string().optional(),
630
+ buyNowLabel: z.string().optional(),
631
+ newLabel: z.string().optional(),
632
+ outOfStockLabel: z.string().optional(),
633
+ colorsLabel: z.string().optional(),
634
+ discountSuffixLabel: z.string().optional(),
635
+ searchPlaceholder: z.string().optional(),
636
+ searchLabel: z.string().optional(),
637
+ featuredDescription: z.string().optional(),
638
+ shopNowLabel: z.string().optional(),
639
+ endsInLabel: z.string().optional(),
640
+ productsLabel: z.string().optional(),
641
+ viewLabel: z.string().optional(),
642
+ })
643
+ export type StorefrontHeroSpecProps = z.infer<typeof storefrontHeroPropsSchema>
644
+
386
645
  export const faqItemSchema = z.object({
387
646
  value: z.string(),
388
647
  question: z.string(),
@@ -407,7 +666,130 @@ export const ctaSection1PropsSchema = z.object({
407
666
  })
408
667
  export type CtaSection1SpecProps = z.infer<typeof ctaSection1PropsSchema>
409
668
 
669
+ /**
670
+ * Serializable contract for the canonical `Feature` benefits section. The
671
+ * item `icon` is a Lucide icon NAME (e.g. `'Zap'`) that the registry resolves
672
+ * to a component at the seam; the React-only callbacks (`onCtaClick`,
673
+ * `onLearnMore`) are intentionally excluded — the catalog adapter binds them.
674
+ */
675
+ export const featureItemSpecSchema = z.object({
676
+ id: z.string(),
677
+ title: z.string(),
678
+ description: z.string(),
679
+ icon: z.string(),
680
+ })
681
+
682
+ /**
683
+ * `cards` branch: the card grid never owns an image. The presentation is
684
+ * optional so an omitted value resolves to `cards`.
685
+ */
686
+ export const featureCardsSpecSchema = z.object({
687
+ items: z.array(featureItemSpecSchema),
688
+ presentation: z.literal('cards').optional(),
689
+ badge: z.string().optional(),
690
+ title: z.string().optional(),
691
+ description: z.string().optional(),
692
+ ctaLabel: z.string().optional(),
693
+ learnMoreLabel: z.string().optional(),
694
+ className: z.string().optional(),
695
+ })
696
+ export type FeatureCardsSpecProps = z.infer<typeof featureCardsSpecSchema>
697
+
698
+ /**
699
+ * `split` branch: the editorial image is mandatory and must carry a non-empty
700
+ * `src`. `alt` stays required so the caller decides whether the image is
701
+ * informative or decorative. `cards`-only chrome is intentionally absent.
702
+ */
703
+ export const featureSplitSpecSchema = z.object({
704
+ items: z.array(featureItemSpecSchema),
705
+ presentation: z.literal('split'),
706
+ title: z.string().optional(),
707
+ description: z.string().optional(),
708
+ image: z.object({ src: z.string().trim().min(1), alt: z.string() }),
709
+ className: z.string().optional(),
710
+ })
711
+ export type FeatureSplitSpecProps = z.infer<typeof featureSplitSpecSchema>
712
+
713
+ /**
714
+ * Serialized `Feature` union. The split branch is listed first so
715
+ * `presentation: 'split'` resolves deterministically; an omitted presentation
716
+ * falls through to `cards`. A split input without a non-empty image matches
717
+ * neither branch and fails to parse.
718
+ */
719
+ export const featurePropsSchema = z.union([featureSplitSpecSchema, featureCardsSpecSchema])
720
+ export type FeatureSpecProps = z.infer<typeof featurePropsSchema>
721
+
722
+ /**
723
+ * Canonical category contract for the consolidated `ProductCategory` block.
724
+ *
725
+ * `categories` is the canonical category projection (a `@thorprovider/types`
726
+ * `ProductCategory` subset): ids are canonical strings, optional fields are
727
+ * omitted rather than serialized as `""`, and there are no demo-only required
728
+ * fields (no required subtitle, sector icon enum, or Lucide component). `icon`
729
+ * is a plain, serializable icon-key string resolved by the block against a
730
+ * bounded map with a neutral fallback. The `presentation` value is a bounded
731
+ * structure switch, never a per-store fork. Callbacks are runtime props and are
732
+ * excluded here (see the component).
733
+ */
734
+ export const productCategoryItemSchema = z.object({
735
+ id: z.string(),
736
+ handle: z.string().optional(),
737
+ name: z.string(),
738
+ description: z.string().optional(),
739
+ /** Optional real count; an unknown count is omitted, never fabricated as `0`. */
740
+ itemCount: z.number().optional(),
741
+ /** Omitted (never `""`) when the backend has no image. */
742
+ image: z.string().optional(),
743
+ trending: z.boolean().optional(),
744
+ /** Serializable icon key (e.g. `'shirt'`); unknown keys render a neutral fallback. */
745
+ icon: z.string().optional(),
746
+ subcategories: z.array(z.string()).optional(),
747
+ featured: z.boolean().optional(),
748
+ })
749
+ export type ProductCategoryItem = z.infer<typeof productCategoryItemSchema>
750
+
751
+ export const productCategoryPropsSchema = z.object({
752
+ categories: z.array(productCategoryItemSchema),
753
+ presentation: z.enum(['grid', 'icons', 'list']).optional(),
754
+ title: z.string().optional(),
755
+ description: z.string().optional(),
756
+ badge: z.string().optional(),
757
+ cta: ctaSchema.optional(),
758
+ /** Controlled search value; filtering is owned by the host. */
759
+ query: z.string().optional(),
760
+ searchPlaceholder: z.string().optional(),
761
+ searchLabel: z.string().optional(),
762
+ emptyLabel: z.string().optional(),
763
+ noResultsLabel: z.string().optional(),
764
+ browseLabel: z.string().optional(),
765
+ viewAllLabel: z.string().optional(),
766
+ trendingLabel: z.string().optional(),
767
+ featuredLabel: z.string().optional(),
768
+ itemsLabel: z.string().optional(),
769
+ imageFallback: z.string().optional(),
770
+ className: z.string().optional(),
771
+ })
772
+ export type ProductCategorySpecProps = z.infer<typeof productCategoryPropsSchema>
773
+
774
+ export const categoryCardPropsSchema = z.object({
775
+ category: productCategoryItemSchema,
776
+ presentation: z.enum(['grid', 'icons', 'list']).optional(),
777
+ browseLabel: z.string().optional(),
778
+ itemsLabel: z.string().optional(),
779
+ trendingLabel: z.string().optional(),
780
+ featuredLabel: z.string().optional(),
781
+ imageFallback: z.string().optional(),
782
+ })
783
+ export type CategoryCardSpecProps = z.infer<typeof categoryCardPropsSchema>
784
+
785
+ export const categoryGridPropsSchema = productCategoryPropsSchema.extend({
786
+ maxItems: z.number().int().positive().optional(),
787
+ loading: z.boolean().optional(),
788
+ skeletonCount: z.number().int().positive().optional(),
789
+ })
790
+ export type CategoryGridSpecProps = z.infer<typeof categoryGridPropsSchema>
791
+
410
792
  // Pure monetary helpers, re-exported from the server-safe contracts entry so a
411
793
  // host boundary can validate/format `Money` without importing the React entry.
412
794
  export { isCompleteFiniteDecimal, isAvailableMoney, formatMoney } from './money'
413
- export { resolveAbsoluteQuantity } from './cart'
795
+ export { resolveAbsoluteQuantity } from './cart'
@@ -2,7 +2,7 @@ import { cva, type VariantProps } from "class-variance-authority"
2
2
  import { cn } from "cn"
3
3
  import { Slot } from "radix-ui"
4
4
 
5
- import { Separator } from "@/ui/separator"
5
+ import { Separator } from "./separator"
6
6
 
7
7
  const buttonGroupVariants = cva(
8
8
  "flex w-fit items-stretch has-[>[data-slot=button-group]]:gap-2 [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
@@ -7,7 +7,7 @@ import useEmblaCarousel, {
7
7
  } from "embla-carousel-react"
8
8
  import { ArrowLeft, ArrowRight } from "lucide-react"
9
9
 
10
- import { Button } from "@/ui/button"
10
+ import { Button } from "./button"
11
11
 
12
12
  type CarouselApi = UseEmblaCarouselType[1]
13
13
  type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
package/src/ui/dialog.tsx CHANGED
@@ -5,7 +5,7 @@ import { cn } from "cn"
5
5
  import { XIcon } from "lucide-react"
6
6
  import { Dialog as DialogPrimitive } from "radix-ui"
7
7
 
8
- import { Button } from "@/ui/button"
8
+ import { Button } from "./button"
9
9
 
10
10
  function Dialog({
11
11
  ...props
package/src/ui/field.tsx CHANGED
@@ -4,8 +4,8 @@ import { useMemo } from "react"
4
4
  import { cva, type VariantProps } from "class-variance-authority"
5
5
  import { cn } from "cn"
6
6
 
7
- import { Label } from "@/ui/label"
8
- import { Separator } from "@/ui/separator"
7
+ import { Label } from "./label"
8
+ import { Separator } from "./separator"
9
9
 
10
10
  function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
11
11
  return (
@@ -4,9 +4,9 @@ import * as React from "react"
4
4
  import { cva, type VariantProps } from "class-variance-authority"
5
5
  import { cn } from "cn"
6
6
 
7
- import { Button } from "@/ui/button"
8
- import { Input } from "@/ui/input"
9
- import { Textarea } from "@/ui/textarea"
7
+ import { Button } from "./button"
8
+ import { Input } from "./input"
9
+ import { Textarea } from "./textarea"
10
10
 
11
11
  function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
12
12
  return (
@@ -6,7 +6,7 @@ import {
6
6
  MoreHorizontalIcon,
7
7
  } from "lucide-react"
8
8
 
9
- import { buttonVariants, type Button } from "@/ui/button"
9
+ import { buttonVariants, type Button } from "./button"
10
10
 
11
11
  function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
12
12
  return (
@@ -6,24 +6,24 @@ import { cn } from "cn"
6
6
  import { PanelLeftIcon } from "lucide-react"
7
7
  import { Slot } from "radix-ui"
8
8
 
9
- import { useIsMobile } from "@/hooks/use-mobile"
10
- import { Button } from "@/ui/button"
11
- import { Input } from "@/ui/input"
12
- import { Separator } from "@/ui/separator"
9
+ import { useIsMobile } from "../hooks/use-mobile"
10
+ import { Button } from "./button"
11
+ import { Input } from "./input"
12
+ import { Separator } from "./separator"
13
13
  import {
14
14
  Sheet,
15
15
  SheetContent,
16
16
  SheetDescription,
17
17
  SheetHeader,
18
18
  SheetTitle,
19
- } from "@/ui/sheet"
20
- import { Skeleton } from "@/ui/skeleton"
19
+ } from "./sheet"
20
+ import { Skeleton } from "./skeleton"
21
21
  import {
22
22
  Tooltip,
23
23
  TooltipContent,
24
24
  TooltipProvider,
25
25
  TooltipTrigger,
26
- } from "@/ui/tooltip"
26
+ } from "./tooltip"
27
27
 
28
28
  const SIDEBAR_COOKIE_NAME = "sidebar_state"
29
29
  const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
@@ -5,7 +5,7 @@ import { type VariantProps } from "class-variance-authority"
5
5
  import { cn } from "cn"
6
6
  import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui"
7
7
 
8
- import { toggleVariants } from "@/ui/toggle"
8
+ import { toggleVariants } from "./toggle"
9
9
 
10
10
  const ToggleGroupContext = React.createContext<
11
11
  VariantProps<typeof toggleVariants> & {
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "noEmit": false,
5
+ "emitDeclarationOnly": true,
6
+ "declaration": true,
7
+ "declarationMap": true,
8
+ "incremental": false
9
+ },
10
+ "include": ["src/**/*"],
11
+ "exclude": ["node_modules", "dist", "src/**/*.test.ts", "src/**/*.test.tsx"]
12
+ }
package/tsup.config.ts CHANGED
@@ -1,15 +1,19 @@
1
1
  import { defineConfig } from 'tsup'
2
2
 
3
3
  export default defineConfig({
4
- entry: {
5
- 'index': 'src/index.ts',
6
- 'contracts': 'src/lib/contracts.ts'
7
- },
4
+ // Emit one module per source file (no bundling) so consumers can import a
5
+ // single block and bundle only what they import. `outbase: 'src'` preserves
6
+ // the source layout under dist/ (e.g. dist/blocks/.../navbar-4.mjs).
7
+ entry: ['src/**/*.ts', 'src/**/*.tsx', '!src/**/*.test.ts', '!src/**/*.test.tsx'],
8
8
  format: ['cjs', 'esm'],
9
- dts: true,
9
+ // Declarations are emitted per-file by `tsc -p tsconfig.build.json` (see the
10
+ // build script); tsup's bundled dts step OOMs with 170+ entries.
11
+ dts: false,
10
12
  splitting: false,
11
13
  sourcemap: true,
12
14
  clean: true,
13
- external: ['react', 'react-dom', 'clsx', 'tailwind-merge', 'lucide-react'],
14
- onSuccess: process.env.SKIP_TYPE_CHECK === 'true' ? undefined : 'tsc --noEmit --skipLibCheck'
15
+ bundle: false,
16
+ esbuildOptions(options) {
17
+ options.outbase = 'src'
18
+ }
15
19
  })
@@ -1,24 +0,0 @@
1
- CLI Building entry: {"index":"src/index.ts","contracts":"src/lib/contracts.ts"}
2
- CLI Using tsconfig: tsconfig.json
3
- CLI tsup v8.5.1
4
- CLI Using tsup config: /home/nelson/dev/active/thorprovider/repos/thorcommerce/packages/blocks/tsup.config.ts
5
- CLI Target: esnext
6
- CLI Cleaning output folder
7
- CJS Build start
8
- ESM Build start
9
- DTS Build start
10
- ESM dist/contracts.mjs 10.65 KB
11
- ESM dist/index.mjs 751.64 KB
12
- ESM dist/contracts.mjs.map 26.52 KB
13
- ESM dist/index.mjs.map 1.24 MB
14
- ESM ⚡️ Build success in 1060ms
15
- CJS dist/contracts.js 15.38 KB
16
- CJS dist/index.js 849.47 KB
17
- CJS dist/contracts.js.map 26.72 KB
18
- CJS dist/index.js.map 1.22 MB
19
- CJS ⚡️ Build success in 1061ms
20
- DTS ⚡️ Build success in 15393ms
21
- DTS dist/index.d.ts 107.68 KB
22
- DTS dist/contracts.d.ts 52.25 KB
23
- DTS dist/index.d.mts 107.68 KB
24
- DTS dist/contracts.d.mts 52.25 KB