@thorprovider/blocks 0.1.3 → 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 (99) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +73 -29
  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/product-categories/category-card-grid.test.tsx +122 -0
  7. package/src/blocks/e-commerce/product-categories/category-card.test.tsx +68 -0
  8. package/src/blocks/e-commerce/product-categories/category-card.tsx +172 -0
  9. package/src/blocks/e-commerce/product-categories/category-grid.test.tsx +67 -0
  10. package/src/blocks/e-commerce/product-categories/category-shared.tsx +109 -0
  11. package/src/blocks/e-commerce/product-categories/product-category-grid.tsx +170 -0
  12. package/src/blocks/e-commerce/product-categories/product-category.test.tsx +266 -0
  13. package/src/blocks/e-commerce/product-categories/product-category.tsx +462 -0
  14. package/src/blocks/e-commerce/product-list/product-card.test.tsx +263 -0
  15. package/src/blocks/e-commerce/product-list/product-card.tsx +424 -0
  16. package/src/blocks/e-commerce/product-list/product-grid.test.tsx +162 -0
  17. package/src/blocks/e-commerce/product-list/product-grid.tsx +114 -0
  18. package/src/blocks/e-commerce/product-overview/product-overview-gallery.tsx +71 -0
  19. package/src/blocks/e-commerce/product-overview/{product-overview-5-lightbox-tab.tsx → product-overview-lightbox-tab.tsx} +35 -23
  20. package/src/blocks/e-commerce/product-overview/{product-overview-5-reviews-tab.tsx → product-overview-reviews-tab.tsx} +40 -17
  21. package/src/blocks/e-commerce/product-overview/{product-overview-5-tabs.tsx → product-overview-tabs.tsx} +8 -7
  22. package/src/blocks/e-commerce/product-overview/product-overview.test.tsx +283 -0
  23. package/src/blocks/e-commerce/product-overview/product-overview.tsx +1186 -0
  24. package/src/blocks/e-commerce/reviews/review-rating-2.tsx +5 -6
  25. package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +399 -0
  26. package/src/blocks/e-commerce/shopping-carts/{shopping-cart-4.tsx → shopping-cart.tsx} +119 -85
  27. package/src/blocks/e-commerce/storefront-hero/storefront-hero.test.tsx +412 -0
  28. package/src/blocks/e-commerce/storefront-hero/storefront-hero.tsx +1283 -0
  29. package/src/blocks/marketing/features/feature.test.tsx +153 -0
  30. package/src/blocks/marketing/features/feature.tsx +195 -0
  31. package/src/blocks/marketing/navbars/navbar-4-account-menu.test.tsx +27 -9
  32. package/src/blocks/marketing/navbars/navbar-4.test.tsx +33 -2
  33. package/src/blocks/marketing/navbars/navbar-4.tsx +29 -11
  34. package/src/index.ts +77 -48
  35. package/src/lib/contracts-curated.test.ts +42 -24
  36. package/src/lib/contracts.test.ts +143 -2
  37. package/src/lib/contracts.ts +346 -14
  38. package/src/ui/button-group.tsx +1 -1
  39. package/src/ui/carousel.tsx +1 -1
  40. package/src/ui/dialog.tsx +1 -1
  41. package/src/ui/field.tsx +2 -2
  42. package/src/ui/input-group.tsx +3 -3
  43. package/src/ui/pagination.tsx +1 -1
  44. package/src/ui/sidebar.tsx +7 -7
  45. package/src/ui/toggle-group.tsx +1 -1
  46. package/tsconfig.build.json +12 -0
  47. package/tsup.config.ts +11 -7
  48. package/.turbo/turbo-build.log +0 -24
  49. package/.turbo/turbo-test.log +0 -47
  50. package/.turbo/turbo-type-check.log +0 -0
  51. package/dist/.tsbuildinfo +0 -1
  52. package/dist/contracts.d.mts +0 -1732
  53. package/dist/contracts.d.ts +0 -1732
  54. package/dist/contracts.js +0 -423
  55. package/dist/contracts.js.map +0 -1
  56. package/dist/contracts.mjs +0 -365
  57. package/dist/contracts.mjs.map +0 -1
  58. package/dist/index.d.mts +0 -3068
  59. package/dist/index.d.ts +0 -3068
  60. package/dist/index.js +0 -17834
  61. package/dist/index.js.map +0 -1
  62. package/dist/index.mjs +0 -17740
  63. package/dist/index.mjs.map +0 -1
  64. package/src/blocks/e-commerce/product-categories/avatar-group.tsx +0 -86
  65. package/src/blocks/e-commerce/product-categories/product-category-1.tsx +0 -126
  66. package/src/blocks/e-commerce/product-categories/product-category-2.test.tsx +0 -51
  67. package/src/blocks/e-commerce/product-categories/product-category-2.tsx +0 -120
  68. package/src/blocks/e-commerce/product-categories/product-category-4.tsx +0 -129
  69. package/src/blocks/e-commerce/product-categories/product-category-5.tsx +0 -218
  70. package/src/blocks/e-commerce/product-list/product-card-1.test.tsx +0 -88
  71. package/src/blocks/e-commerce/product-list/product-card-1.tsx +0 -192
  72. package/src/blocks/e-commerce/product-list/product-card-2.tsx +0 -71
  73. package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +0 -166
  74. package/src/blocks/e-commerce/product-list/product-card-3.tsx +0 -215
  75. package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +0 -98
  76. package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +0 -171
  77. package/src/blocks/e-commerce/product-list/product-card-5.tsx +0 -367
  78. package/src/blocks/e-commerce/product-overview/product-overview-1.test.tsx +0 -182
  79. package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +0 -370
  80. package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +0 -272
  81. package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +0 -59
  82. package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +0 -274
  83. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +0 -142
  84. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +0 -291
  85. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +0 -110
  86. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +0 -51
  87. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +0 -146
  88. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +0 -295
  89. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +0 -142
  90. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +0 -342
  91. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +0 -146
  92. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.test.tsx +0 -229
  93. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx +0 -72
  94. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +0 -263
  95. package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +0 -283
  96. package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +0 -318
  97. package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +0 -205
  98. package/src/blocks/marketing/features/feature-1.tsx +0 -97
  99. 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()]),
@@ -275,13 +309,6 @@ 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),
280
- heading: z.string().optional(),
281
- maxItems: z.number().int().positive().optional(),
282
- })
283
- export type ProductCard1SpecProps = z.infer<typeof productCard1PropsSchema>
284
-
285
312
  /**
286
313
  * Canonical product presentation — the serializable projection a storefront host
287
314
  * produces from a canonical `Product` for the merchandising sections. Unlike
@@ -310,6 +337,42 @@ export const productPresentationSchema = z.object({
310
337
  })
311
338
  export type ProductPresentation = z.infer<typeof productPresentationSchema>
312
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(),
367
+ heading: z.string().optional(),
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
+
313
376
  /**
314
377
  * Serializable contract for the combined grid-and-carousel product
315
378
  * presentation (`ProductShowcase1`). The grid pages are fixed-size, navigated
@@ -330,10 +393,45 @@ export const productShowcase1PropsSchema = z.object({
330
393
  })
331
394
  export type ProductShowcase1SpecProps = z.infer<typeof productShowcase1PropsSchema>
332
395
 
333
- export const shoppingCart4PropsSchema = z.object({
334
- 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),
335
432
  promotions: z.array(promotionSchema).default([]),
336
- recentlyViewed: z.array(recentlyViewedItemSchema).optional(),
433
+ recentlyViewed: z.array(shoppingCartSuggestionSchema).optional(),
434
+ presentation: z.enum(['compact', 'page']).optional(),
337
435
  /** Backend-authoritative totals; rendered verbatim, never computed locally. */
338
436
  totals: cartTotalsSchema.optional(),
339
437
  totalsState: z.enum(['idle', 'pending', 'error']).optional(),
@@ -341,7 +439,6 @@ export const shoppingCart4PropsSchema = z.object({
341
439
  totalsUnavailableLabel: z.string().optional(),
342
440
  amountUnavailableLabel: z.string().optional(),
343
441
  retryLabel: z.string().optional(),
344
- taxRate: z.number().optional(),
345
442
  titleLabel: z.string().optional(),
346
443
  subtotalLabel: z.string().optional(),
347
444
  discountLabel: z.string().optional(),
@@ -359,6 +456,9 @@ export const shoppingCart4PropsSchema = z.object({
359
456
  estimatedDeliveryLabel: z.string().optional(),
360
457
  secureCheckoutLabel: z.string().optional(),
361
458
  recentlyViewedLabel: z.string().optional(),
459
+ promoPromptLabel: z.string().optional(),
460
+ promoPlaceholderLabel: z.string().optional(),
461
+ promoApplyLabel: z.string().optional(),
362
462
  collapseLabel: z.string().optional(),
363
463
  expandLabel: z.string().optional(),
364
464
  checkoutLabel: z.string().optional(),
@@ -369,7 +469,7 @@ export const shoppingCart4PropsSchema = z.object({
369
469
  decreaseLabel: z.string().optional(),
370
470
  increaseLabel: z.string().optional(),
371
471
  })
372
- export type ShoppingCart4SpecProps = z.infer<typeof shoppingCart4PropsSchema>
472
+ export type ShoppingCartSpecProps = z.infer<typeof shoppingCartPropsSchema>
373
473
 
374
474
  /**
375
475
  * Optional brand seams shared by the existing blocks. These mirror the
@@ -433,6 +533,115 @@ export const ctaSchema = z.object({
433
533
  })
434
534
  export type CtaSpec = z.infer<typeof ctaSchema>
435
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
+
436
645
  export const faqItemSchema = z.object({
437
646
  value: z.string(),
438
647
  question: z.string(),
@@ -457,6 +666,129 @@ export const ctaSection1PropsSchema = z.object({
457
666
  })
458
667
  export type CtaSection1SpecProps = z.infer<typeof ctaSection1PropsSchema>
459
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
+
460
792
  // Pure monetary helpers, re-exported from the server-safe contracts entry so a
461
793
  // host boundary can validate/format `Money` without importing the React entry.
462
794
  export { isCompleteFiniteDecimal, isAvailableMoney, formatMoney } from './money'
@@ -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
- ESM dist/contracts.mjs 11.75 KB
10
- ESM dist/index.mjs 778.08 KB
11
- ESM dist/contracts.mjs.map 29.49 KB
12
- ESM dist/index.mjs.map 1.29 MB
13
- ESM ⚡️ Build success in 228ms
14
- CJS dist/contracts.js 16.83 KB
15
- CJS dist/index.js 878.89 KB
16
- CJS dist/contracts.js.map 29.69 KB
17
- CJS dist/index.js.map 1.27 MB
18
- CJS ⚡️ Build success in 229ms
19
- DTS Build start
20
- DTS ⚡️ Build success in 10197ms
21
- DTS dist/index.d.ts 120.86 KB
22
- DTS dist/contracts.d.ts 57.89 KB
23
- DTS dist/index.d.mts 120.86 KB
24
- DTS dist/contracts.d.mts 57.89 KB
@@ -1,47 +0,0 @@
1
-
2
- RUN v3.2.4 /home/nelson/dev/active/thorprovider/repos/thorcommerce/packages/blocks
3
-
4
- ✓ src/blocks/e-commerce/navigation/mega-menu.test.tsx (9 tests) 1189ms
5
- ✓ MegaMenu > renders category columns from the categories prop 627ms
6
- ✓ src/blocks/e-commerce/navigation/mobile-category-drawer.test.tsx (12 tests) 1191ms
7
- ✓ MobileCategoryDrawer > keeps the drawer mounted off-screen with aria-hidden when closed 728ms
8
- ✓ src/blocks/e-commerce/product-categories/product-category-2.test.tsx (5 tests) 329ms
9
- stderr | src/blocks/e-commerce/checkout-forms/checkout-form-1.test.tsx > CheckoutForm1 payment step — launch flow collects no raw card data > renders card fields only in the explicit legacy mode
10
- Received `true` for a non-boolean attribute `inline-end`.
11
-
12
- If you want to write it to the DOM, pass a string instead: inline-end="true" or inline-end={value.toString()}.
13
-
14
- ✓ src/blocks/e-commerce/checkout-forms/checkout-form-1.test.tsx (10 tests) 337ms
15
- ✓ src/blocks/e-commerce/checkout-forms/checkout-confirmation.test.tsx (7 tests) 567ms
16
- ✓ src/blocks/marketing/footers/footer-section-1.test.tsx (9 tests) 208ms
17
- ✓ src/blocks/e-commerce/shopping-carts/shopping-cart-4.test.tsx (17 tests) 704ms
18
- ✓ src/blocks/e-commerce/product-list/product-card-1.test.tsx (8 tests) 855ms
19
- ✓ src/blocks/marketing/navbars/navbar-4-cart-sheet.test.tsx (15 tests) 583ms
20
- ✓ Navbar4CartSheet — authoritative subtotal > never renders a fake zero for a supplied but absent/malformed subtotal 430ms
21
- ✓ src/blocks/e-commerce/navigation/skip-to-content.test.tsx (5 tests) 52ms
22
- ✓ src/blocks/marketing/navbars/navbar-4.test.tsx (19 tests) 851ms
23
- ✓ src/lib/money.test.ts (10 tests) 473ms
24
- ✓ formatMoney > uses the money currency rather than a hardcoded one 371ms
25
- ✓ src/lib/contracts-server-boundary.test.ts (3 tests) 92ms
26
- ✓ src/blocks/e-commerce/navigation/mobile-bottom-nav.test.tsx (7 tests) 446ms
27
- ✓ src/blocks/e-commerce/category-filters/category-filter-2.test.tsx (39 tests) 2736ms
28
- ✓ CategoryFilter2 — controlled canonical query > renders the query-selected search, category, sort and page values 428ms
29
- ✓ CategoryFilter2 — controlled canonical query > renders collection controls when collections are supplied 354ms
30
- ✓ CategoryFilter2 — no fabricated price or sort choices > omits the price control without a real backend range 301ms
31
- ✓ src/blocks/e-commerce/navigation/category-icon.test.tsx (7 tests) 584ms
32
- ✓ src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx (6 tests) 867ms
33
- ✓ StorefrontHero1 — optional editorial image > renders no editorial image when none is supplied 325ms
34
- ✓ src/lib/contracts.test.ts (3 tests) 33ms
35
- ✓ src/lib/cart.test.ts (2 tests) 23ms
36
- ✓ src/blocks/e-commerce/product-overview/product-overview-1.test.tsx (14 tests) 200ms
37
- ✓ src/blocks/e-commerce/navigation/breadcrumb.test.tsx (12 tests) 80ms
38
- ✓ src/blocks/e-commerce/navigation/back-to-top.test.tsx (5 tests) 23ms
39
- ✓ src/blocks/e-commerce/checkout-forms/card-formatting.test.ts (10 tests) 10ms
40
- ✓ src/lib/contracts-curated.test.ts (22 tests) 550ms
41
- ✓ src/blocks/e-commerce/product-list/product-showcase-1.test.tsx (7 tests) 498ms
42
-
43
- Test Files 25 passed (25)
44
- Tests 263 passed (263)
45
- Start at 11:37:52
46
- Duration 16.34s (transform 12.71s, setup 0ms, collect 81.98s, tests 13.48s, environment 33ms, prepare 18.67s)
47
-
File without changes