@thorprovider/blocks 0.1.2 → 0.1.3

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 (41) hide show
  1. package/.turbo/turbo-build.log +15 -15
  2. package/.turbo/turbo-test.log +37 -27
  3. package/README.md +42 -1
  4. package/dist/.tsbuildinfo +1 -1
  5. package/dist/contracts.d.mts +162 -14
  6. package/dist/contracts.d.ts +162 -14
  7. package/dist/contracts.js +36 -3
  8. package/dist/contracts.js.map +1 -1
  9. package/dist/contracts.mjs +34 -3
  10. package/dist/contracts.mjs.map +1 -1
  11. package/dist/index.d.mts +294 -12
  12. package/dist/index.d.ts +294 -12
  13. package/dist/index.js +3577 -2882
  14. package/dist/index.js.map +1 -1
  15. package/dist/index.mjs +3431 -2750
  16. package/dist/index.mjs.map +1 -1
  17. package/package.json +2 -2
  18. package/src/blocks/e-commerce/navigation/back-to-top.test.tsx +27 -0
  19. package/src/blocks/e-commerce/navigation/back-to-top.tsx +71 -0
  20. package/src/blocks/e-commerce/navigation/breadcrumb.test.tsx +174 -0
  21. package/src/blocks/e-commerce/navigation/breadcrumb.tsx +130 -0
  22. package/src/blocks/e-commerce/navigation/mobile-bottom-nav.test.tsx +74 -0
  23. package/src/blocks/e-commerce/navigation/mobile-bottom-nav.tsx +78 -0
  24. package/src/blocks/e-commerce/navigation/skip-to-content.test.tsx +35 -0
  25. package/src/blocks/e-commerce/navigation/skip-to-content.tsx +37 -0
  26. package/src/blocks/e-commerce/product-list/product-card-1.test.tsx +88 -0
  27. package/src/blocks/e-commerce/product-list/product-card-1.tsx +141 -80
  28. package/src/blocks/e-commerce/product-list/product-showcase-1.test.tsx +78 -0
  29. package/src/blocks/e-commerce/product-list/product-showcase-1.tsx +277 -0
  30. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.test.tsx +92 -0
  31. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.tsx +86 -25
  32. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx +7 -0
  33. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +1 -1
  34. package/src/blocks/marketing/navbars/navbar-4-account-menu.test.tsx +38 -0
  35. package/src/blocks/marketing/navbars/navbar-4-account-menu.tsx +30 -14
  36. package/src/blocks/marketing/navbars/navbar-4-cart-sheet.test.tsx +150 -0
  37. package/src/blocks/marketing/navbars/navbar-4-cart-sheet.tsx +102 -9
  38. package/src/blocks/marketing/navbars/navbar-4.test.tsx +121 -0
  39. package/src/blocks/marketing/navbars/navbar-4.tsx +134 -32
  40. package/src/index.ts +18 -0
  41. package/src/lib/contracts.ts +53 -3
@@ -2,14 +2,14 @@
2
2
 
3
3
  import { useEffect, useMemo, useRef, useState } from 'react'
4
4
  import { AnimatePresence, motion, useReducedMotion } from 'motion/react'
5
- import { Menu, Search, ShoppingBag, X } from 'lucide-react'
5
+ import { Menu, Search, ShoppingBag, UserRound, RotateCcw, X } from 'lucide-react'
6
6
 
7
7
  import { Button } from '../../../ui/button'
8
8
  import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '../../../ui/sheet'
9
9
  import { cn } from '../../../lib/utils'
10
10
 
11
11
  import { Navbar4AccountMenu, type AccountMenuItem, type StoreUser } from './navbar-4-account-menu'
12
- import { Navbar4CartSheet, type CartItem } from './navbar-4-cart-sheet'
12
+ import { Navbar4CartSheet, type CartItem, type CartSheetState } from './navbar-4-cart-sheet'
13
13
  import type { UtilityLink } from './navbar-4-mobile-nav'
14
14
  import { Navbar4SearchBar } from './navbar-4-search-bar'
15
15
  import { MegaMenu } from '../../e-commerce/navigation/mega-menu'
@@ -163,7 +163,12 @@ function CartBadge({ count }: CartBadgeProps) {
163
163
 
164
164
  export interface Navbar4Props {
165
165
  storeCategories: StoreCategory[]
166
- shippingNote: string
166
+ /**
167
+ * Optional note shown in the desktop utility strip. When omitted (or empty)
168
+ * and there are no `utilityLinks`, the strip is not rendered at all, so a
169
+ * block with no optional content paints no empty decorative chrome.
170
+ */
171
+ shippingNote?: string
167
172
  utilityLinks: UtilityLink[]
168
173
  logoText: string
169
174
  logoHref?: string
@@ -173,7 +178,7 @@ export interface Navbar4Props {
173
178
  * `logoText`, the wordmark renders unchanged.
174
179
  */
175
180
  logoAccent?: LogoAccent
176
- /** When provided, a mobile-only "Account" link is appended to the utility links. */
181
+ /** Account destination for anonymous shoppers on every viewport. */
177
182
  accountHref?: string
178
183
  /**
179
184
  * When `false`, no search control is rendered at all — neither the desktop
@@ -182,6 +187,11 @@ export interface Navbar4Props {
182
187
  * storefront passes `false` while no verified global search contract exists
183
188
  * (E2 task 5.4), so no global search is presented as working.
184
189
  */
190
+ /**
191
+ * The storefront's current route path. When it matches a destination the link
192
+ * is marked as the current page (accessibly, beyond color alone).
193
+ */
194
+ currentPath?: string
185
195
  showSearch?: boolean
186
196
  searchQuery?: string
187
197
  onSearchChange?: (value: string) => void
@@ -204,8 +214,40 @@ export interface Navbar4Props {
204
214
  accountMenuItems?: AccountMenuItem[]
205
215
  onHelp?: () => void
206
216
  onSignOut?: () => void
217
+ accountPending?: boolean
218
+ accountUnavailable?: boolean
219
+ onAccountRetry?: () => void
207
220
  onCheckout?: () => void
208
221
  onViewCart?: () => void
222
+ /**
223
+ * Controlled visibility for the cart sheet. When provided, the sheet opens
224
+ * and closes from this value and `onCartOpenChange` instead of internal
225
+ * state, so a host can share one cart surface across the navbar trigger and
226
+ * successful add-to-cart. Omit both to keep the legacy uncontrolled sheet.
227
+ */
228
+ cartOpen?: boolean
229
+ /** Change handler paired with `cartOpen` for a controlled sheet. */
230
+ onCartOpenChange?: (open: boolean) => void
231
+ /**
232
+ * Backend-authoritative cart rendering for the sheet. When `true`, line
233
+ * amounts use each item's canonical `lineTotal` Money and never fall back to
234
+ * numeric `price * qty`. Omit for the legacy numeric rendering.
235
+ */
236
+ canonicalCart?: boolean
237
+ /** Cart read/mutation lifecycle for the sheet. */
238
+ cartState?: CartSheetState
239
+ /** Message shown when `cartState` is `error`. */
240
+ cartErrorLabel?: string
241
+ /** Recovery action label for the error state. */
242
+ cartRetryLabel?: string
243
+ /** Recovery action invoked from the error state. */
244
+ onCartRetry?: () => void
245
+ /** When `true`, the sheet's checkout action is suppressed (checkout not installed). */
246
+ checkoutUnavailable?: boolean
247
+ /** When `true`, the sheet's checkout action is disabled (cart not ready). */
248
+ checkoutDisabled?: boolean
249
+ /** When `true`, the sheet's review action is suppressed (cart view not installed). */
250
+ viewCartUnavailable?: boolean
209
251
  /**
210
252
  * Consumer-supplied classes merged onto the outer `<header>` as the final
211
253
  * argument, so a semantic surface such as `bg-brand-nav
@@ -217,12 +259,13 @@ export interface Navbar4Props {
217
259
 
218
260
  export function Navbar4({
219
261
  storeCategories,
220
- shippingNote,
262
+ shippingNote = '',
221
263
  utilityLinks,
222
264
  logoText,
223
265
  logoHref,
224
266
  logoAccent,
225
267
  accountHref,
268
+ currentPath,
226
269
  showSearch = true,
227
270
  searchQuery: searchQueryProp,
228
271
  onSearchChange,
@@ -236,8 +279,21 @@ export function Navbar4({
236
279
  accountMenuItems,
237
280
  onHelp,
238
281
  onSignOut,
282
+ accountPending = false,
283
+ accountUnavailable = false,
284
+ onAccountRetry,
239
285
  onCheckout,
240
286
  onViewCart,
287
+ cartOpen,
288
+ onCartOpenChange,
289
+ canonicalCart,
290
+ cartState,
291
+ cartErrorLabel,
292
+ cartRetryLabel,
293
+ onCartRetry,
294
+ checkoutUnavailable,
295
+ checkoutDisabled,
296
+ viewCartUnavailable,
241
297
  className,
242
298
  }: Navbar4Props) {
243
299
  /**
@@ -300,6 +356,13 @@ export function Navbar4({
300
356
  // default foreground (and the default hover surface).
301
357
  const navLinkColor =
302
358
  className !== undefined ? 'text-inherit hover:text-foreground' : 'text-foreground'
359
+ // The current page is marked beyond color: an accessible current state plus a
360
+ // visible weight/underline, and nothing when no destination matches.
361
+ const isCurrent = (href?: string) => href !== undefined && href === currentPath
362
+
363
+ // The desktop utility strip is decorative chrome: it is painted only when it
364
+ // has content (a non-empty note or at least one utility link), never empty.
365
+ const hasUtilityContent = shippingNote.trim().length > 0 || utilityLinks.length > 0
303
366
 
304
367
  return (
305
368
  <header
@@ -308,23 +371,25 @@ export function Navbar4({
308
371
  className ?? 'bg-background/95 supports-backdrop-filter:bg-background/80',
309
372
  )}
310
373
  >
311
- <div className='bg-foreground text-background hidden lg:block'>
312
- <div className='mx-auto flex h-9 w-full max-w-7xl items-center justify-between px-4 text-xs tracking-wide sm:px-6 lg:px-8'>
313
- <p className='text-background/80'>{shippingNote}</p>
314
- <ul className='flex items-center gap-5'>
315
- {utilityLinks.map(link => (
316
- <li key={`${link.href}:${link.label}`}>
317
- <a
318
- href={link.href}
319
- className='text-background/80 hover:text-background transition-colors duration-150'
320
- >
321
- {link.label}
322
- </a>
323
- </li>
324
- ))}
325
- </ul>
374
+ {hasUtilityContent ? (
375
+ <div className='bg-foreground text-background hidden lg:block'>
376
+ <div className='mx-auto flex h-9 w-full max-w-7xl items-center justify-between px-4 text-xs tracking-wide sm:px-6 lg:px-8'>
377
+ <p className='text-background/80'>{shippingNote}</p>
378
+ <ul className='flex items-center gap-5'>
379
+ {utilityLinks.map(link => (
380
+ <li key={`${link.href}:${link.label}`}>
381
+ <a
382
+ href={link.href}
383
+ className='text-background/80 hover:text-background transition-colors duration-150'
384
+ >
385
+ {link.label}
386
+ </a>
387
+ </li>
388
+ ))}
389
+ </ul>
390
+ </div>
326
391
  </div>
327
- </div>
392
+ ) : null}
328
393
 
329
394
  <div className='mx-auto flex h-16 w-full max-w-7xl items-center gap-3 px-4 sm:px-6 lg:px-8'>
330
395
  <Button
@@ -361,9 +426,11 @@ export function Navbar4({
361
426
  <li key={category.href} className='group relative'>
362
427
  <a
363
428
  href={category.href}
429
+ aria-current={isCurrent(category.href) ? 'page' : undefined}
364
430
  className={cn(
365
431
  'hover:bg-muted focus-visible:ring-ring rounded-md px-3 py-2 text-sm font-medium transition-colors duration-150 focus-visible:ring-2 focus-visible:outline-none',
366
432
  category.emphasis ? 'text-destructive' : navLinkColor,
433
+ isCurrent(category.href) && 'font-semibold underline underline-offset-4',
367
434
  )}
368
435
  >
369
436
  {category.label}
@@ -381,9 +448,11 @@ export function Navbar4({
381
448
  <li key={category.href}>
382
449
  <a
383
450
  href={category.href}
451
+ aria-current={isCurrent(category.href) ? 'page' : undefined}
384
452
  className={cn(
385
453
  'hover:bg-muted focus-visible:ring-ring rounded-md px-3 py-2 text-sm font-medium transition-colors duration-150 focus-visible:ring-2 focus-visible:outline-none',
386
454
  category.emphasis ? 'text-destructive' : navLinkColor,
455
+ isCurrent(category.href) && 'font-semibold underline underline-offset-4',
387
456
  )}
388
457
  >
389
458
  {category.label}
@@ -419,18 +488,43 @@ export function Navbar4({
419
488
  </>
420
489
  ) : null}
421
490
 
422
- {user ? (
423
- <div className='hidden lg:block'>
424
- <Navbar4AccountMenu
425
- user={user}
426
- items={accountMenuItems}
427
- onHelp={onHelp}
428
- onSignOut={onSignOut}
429
- />
430
- </div>
491
+ {accountUnavailable ? (
492
+ <Button
493
+ variant='ghost'
494
+ size='icon'
495
+ className='size-11'
496
+ disabled={accountPending || !onAccountRetry}
497
+ aria-label='Retry account session'
498
+ title='Account session unavailable'
499
+ onClick={onAccountRetry}
500
+ >
501
+ <RotateCcw aria-hidden='true' />
502
+ </Button>
503
+ ) : user ? (
504
+ <Navbar4AccountMenu
505
+ user={user}
506
+ items={accountMenuItems}
507
+ onHelp={onHelp}
508
+ onSignOut={onSignOut}
509
+ pending={accountPending}
510
+ />
511
+ ) : accountHref ? (
512
+ accountPending ? (
513
+ <Button variant='ghost' size='icon' className='size-11' disabled aria-label='Loading account session'>
514
+ <UserRound aria-hidden='true' />
515
+ </Button>
516
+ ) : (
517
+ <Button variant='ghost' size='icon' className='size-11' asChild>
518
+ <a href={accountHref} aria-label='Account' aria-current={isCurrent(accountHref) ? 'page' : undefined}>
519
+ <UserRound aria-hidden='true' />
520
+ </a>
521
+ </Button>
522
+ )
431
523
  ) : null}
432
524
 
433
- <Sheet>
525
+ <Sheet
526
+ {...(cartOpen !== undefined ? { open: cartOpen, onOpenChange: onCartOpenChange } : {})}
527
+ >
434
528
  <SheetTrigger asChild>
435
529
  <Button
436
530
  variant='ghost'
@@ -457,6 +551,14 @@ export function Navbar4({
457
551
  formatMoney={formatMoney}
458
552
  onCheckout={onCheckout}
459
553
  onViewCart={onViewCart}
554
+ canonical={canonicalCart}
555
+ state={cartState}
556
+ errorLabel={cartErrorLabel}
557
+ retryLabel={cartRetryLabel}
558
+ onRetry={onCartRetry}
559
+ checkoutUnavailable={checkoutUnavailable}
560
+ checkoutDisabled={checkoutDisabled}
561
+ viewCartUnavailable={viewCartUnavailable}
460
562
  />
461
563
  </SheetContent>
462
564
  </Sheet>
@@ -481,4 +583,4 @@ export function Navbar4({
481
583
  )
482
584
  }
483
585
 
484
- export default Navbar4
586
+ export default Navbar4
package/src/index.ts CHANGED
@@ -56,6 +56,12 @@ export { default as ProductCard1 } from './blocks/e-commerce/product-list/produc
56
56
  export { default as ProductCard2 } from './blocks/e-commerce/product-list/product-card-2'
57
57
  export { default as ProductCard3 } from './blocks/e-commerce/product-list/product-card-3'
58
58
  export { default as ProductCard5 } from './blocks/e-commerce/product-list/product-card-5'
59
+ export { default as ProductShowcase1 } from './blocks/e-commerce/product-list/product-showcase-1'
60
+ export type {
61
+ ProductCard1Product,
62
+ ProductCard1Props,
63
+ } from './blocks/e-commerce/product-list/product-card-1'
64
+ export type { ProductShowcase1Props } from './blocks/e-commerce/product-list/product-showcase-1'
59
65
  export { default as ProductOverview1 } from './blocks/e-commerce/product-overview/product-overview-1'
60
66
  export type {
61
67
  ProductOverview1Props,
@@ -87,6 +93,14 @@ export { MobileCategoryDrawer, mobileCategoryDrawerPropsSchema } from './blocks/
87
93
  export type { MobileCategoryDrawerProps } from './blocks/e-commerce/navigation/mobile-category-drawer'
88
94
  export { CategoryIcon, categoryIconNameSchema, categoryIconPropsSchema } from './blocks/e-commerce/navigation/category-icon'
89
95
  export type { CategoryIconName, CategoryIconProps } from './blocks/e-commerce/navigation/category-icon'
96
+ export { Breadcrumb, breadcrumbItemSchema, breadcrumbPropsSchema } from './blocks/e-commerce/navigation/breadcrumb'
97
+ export type { BreadcrumbItemData, BreadcrumbProps } from './blocks/e-commerce/navigation/breadcrumb'
98
+ export { SkipToContent, skipToContentPropsSchema } from './blocks/e-commerce/navigation/skip-to-content'
99
+ export type { SkipToContentProps } from './blocks/e-commerce/navigation/skip-to-content'
100
+ export { BackToTop, backToTopPropsSchema, resolveScrollBehavior } from './blocks/e-commerce/navigation/back-to-top'
101
+ export type { BackToTopProps } from './blocks/e-commerce/navigation/back-to-top'
102
+ export { MobileBottomNav, mobileBottomNavItemSchema, mobileBottomNavPropsSchema } from './blocks/e-commerce/navigation/mobile-bottom-nav'
103
+ export type { MobileBottomNavItem, MobileBottomNavProps } from './blocks/e-commerce/navigation/mobile-bottom-nav'
90
104
  export { menuItemSchema, collectionItemSchema } from './lib/types/menu'
91
105
  export type { MenuItem, CollectionItem } from './lib/types/menu'
92
106
  export { default as CategoryFilter1 } from './blocks/e-commerce/category-filters/category-filter-1'
@@ -205,6 +219,8 @@ export {
205
219
  buttonPropsSchema,
206
220
  storeContainer1PropsSchema,
207
221
  productCard1PropsSchema,
222
+ productPresentationSchema,
223
+ productShowcase1PropsSchema,
208
224
  shoppingCart4PropsSchema,
209
225
  navbar4PropsSchema,
210
226
  paymentMethodSchema,
@@ -225,6 +241,8 @@ export type {
225
241
  ButtonSpecProps,
226
242
  StoreContainer1SpecProps,
227
243
  ProductCard1SpecProps,
244
+ ProductPresentation,
245
+ ProductShowcase1SpecProps,
228
246
  ShoppingCart4SpecProps,
229
247
  Navbar4SpecProps,
230
248
  CardProduct,
@@ -203,7 +203,7 @@ export type NavCartItem = z.infer<typeof navCartItemSchema>
203
203
  export const storeUserSchema = z.object({
204
204
  name: z.string(),
205
205
  email: z.string(),
206
- avatar: z.string(),
206
+ avatar: z.string().optional(),
207
207
  initials: z.string(),
208
208
  })
209
209
  export type StoreUser = z.infer<typeof storeUserSchema>
@@ -282,6 +282,54 @@ export const productCard1PropsSchema = z.object({
282
282
  })
283
283
  export type ProductCard1SpecProps = z.infer<typeof productCard1PropsSchema>
284
284
 
285
+ /**
286
+ * Canonical product presentation — the serializable projection a storefront host
287
+ * produces from a canonical `Product` for the merchandising sections. Unlike
288
+ * `cardProductSchema` (legacy, numeric `price`), `price`/`compareAtPrice` carry
289
+ * canonical `Money` (decimal major-unit string + currency code), so the block
290
+ * formats in the shopper's currency and locale and renders a truthful
291
+ * unavailable state instead of `$0`. Optional fields are omitted, never `""`.
292
+ *
293
+ * This contract is additive: `cardProductSchema` and its consumers are
294
+ * unchanged, and a legacy numeric `price` still renders the legacy output.
295
+ */
296
+ export const productPresentationSchema = z.object({
297
+ id: z.union([z.string(), z.number()]),
298
+ title: z.string(),
299
+ /** Omitted (never `""`) when the backend has no image. */
300
+ image: z.string().optional(),
301
+ imageAlt: z.string().optional(),
302
+ /** Product destination; omitted when unknown. */
303
+ href: z.string().optional(),
304
+ price: moneySchema,
305
+ /** Comparison ("was") price; rendered only when valid and higher than `price`. */
306
+ compareAtPrice: moneySchema.optional(),
307
+ /** Only from real evidence; omitted otherwise (never fabricated). */
308
+ rating: z.number().optional(),
309
+ badge: z.string().optional(),
310
+ })
311
+ export type ProductPresentation = z.infer<typeof productPresentationSchema>
312
+
313
+ /**
314
+ * Serializable contract for the combined grid-and-carousel product
315
+ * presentation (`ProductShowcase1`). The grid pages are fixed-size, navigated
316
+ * manually (prev/next) with a visible current-page indication; there is no
317
+ * autoplay and no infinite loop. React-only callbacks are excluded.
318
+ */
319
+ export const productShowcase1PropsSchema = z.object({
320
+ products: z.array(productPresentationSchema),
321
+ heading: z.string().optional(),
322
+ description: z.string().optional(),
323
+ pageSize: z.number().int().positive().optional(),
324
+ prevLabel: z.string().optional(),
325
+ nextLabel: z.string().optional(),
326
+ pageLabel: z.string().optional(),
327
+ unavailablePriceLabel: z.string().optional(),
328
+ emptyLabel: z.string().optional(),
329
+ addToCartLabel: z.string().optional(),
330
+ })
331
+ export type ProductShowcase1SpecProps = z.infer<typeof productShowcase1PropsSchema>
332
+
285
333
  export const shoppingCart4PropsSchema = z.object({
286
334
  items: z.array(cartItemSchema),
287
335
  promotions: z.array(promotionSchema).default([]),
@@ -349,7 +397,7 @@ export type HeroImage = z.infer<typeof heroImageSchema>
349
397
 
350
398
  export const navbar4PropsSchema = z.object({
351
399
  storeCategories: z.array(storeCategorySchema),
352
- shippingNote: z.string(),
400
+ shippingNote: z.string().optional(),
353
401
  utilityLinks: z.array(utilityLinkSchema),
354
402
  logoText: z.string(),
355
403
  logoHref: z.string().optional(),
@@ -368,6 +416,8 @@ export const navbar4PropsSchema = z.object({
368
416
  subtotalUnavailableLabel: z.string().optional(),
369
417
  user: storeUserSchema.optional(),
370
418
  accountMenuItems: z.array(accountMenuItemSchema).optional(),
419
+ accountPending: z.boolean().optional(),
420
+ accountUnavailable: z.boolean().optional(),
371
421
  })
372
422
  export type Navbar4SpecProps = z.infer<typeof navbar4PropsSchema>
373
423
 
@@ -410,4 +460,4 @@ export type CtaSection1SpecProps = z.infer<typeof ctaSection1PropsSchema>
410
460
  // Pure monetary helpers, re-exported from the server-safe contracts entry so a
411
461
  // host boundary can validate/format `Money` without importing the React entry.
412
462
  export { isCompleteFiniteDecimal, isAvailableMoney, formatMoney } from './money'
413
- export { resolveAbsoluteQuantity } from './cart'
463
+ export { resolveAbsoluteQuantity } from './cart'