@thorprovider/blocks 0.1.1 → 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 (70) hide show
  1. package/.turbo/turbo-build.log +15 -15
  2. package/.turbo/turbo-test.log +42 -5
  3. package/AGENTS.md +1 -1
  4. package/README.md +94 -1
  5. package/dist/.tsbuildinfo +1 -1
  6. package/dist/contracts.d.mts +1266 -97
  7. package/dist/contracts.d.ts +1266 -97
  8. package/dist/contracts.js +225 -9
  9. package/dist/contracts.js.map +1 -1
  10. package/dist/contracts.mjs +207 -10
  11. package/dist/contracts.mjs.map +1 -1
  12. package/dist/index.d.mts +756 -96
  13. package/dist/index.d.ts +756 -96
  14. package/dist/index.js +5114 -3702
  15. package/dist/index.js.map +1 -1
  16. package/dist/index.mjs +4939 -3551
  17. package/dist/index.mjs.map +1 -1
  18. package/docs/standards/theming.md +32 -5
  19. package/package.json +2 -2
  20. package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +136 -93
  21. package/src/blocks/e-commerce/category-filters/category-filter-2.test.tsx +513 -0
  22. package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +728 -148
  23. package/src/blocks/e-commerce/checkout-forms/checkout-confirmation-fulfilment-progress.tsx +14 -8
  24. package/src/blocks/e-commerce/checkout-forms/checkout-confirmation.test.tsx +136 -0
  25. package/src/blocks/e-commerce/checkout-forms/checkout-confirmation.tsx +145 -99
  26. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +215 -102
  27. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +6 -2
  28. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-shipping-step.tsx +51 -2
  29. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.test.tsx +216 -0
  30. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +114 -11
  31. package/src/blocks/e-commerce/navigation/back-to-top.test.tsx +27 -0
  32. package/src/blocks/e-commerce/navigation/back-to-top.tsx +71 -0
  33. package/src/blocks/e-commerce/navigation/breadcrumb.test.tsx +174 -0
  34. package/src/blocks/e-commerce/navigation/breadcrumb.tsx +130 -0
  35. package/src/blocks/e-commerce/navigation/mobile-bottom-nav.test.tsx +74 -0
  36. package/src/blocks/e-commerce/navigation/mobile-bottom-nav.tsx +78 -0
  37. package/src/blocks/e-commerce/navigation/mobile-category-drawer.test.tsx +42 -2
  38. package/src/blocks/e-commerce/navigation/mobile-category-drawer.tsx +10 -8
  39. package/src/blocks/e-commerce/navigation/skip-to-content.test.tsx +35 -0
  40. package/src/blocks/e-commerce/navigation/skip-to-content.tsx +37 -0
  41. package/src/blocks/e-commerce/product-categories/product-category-2.test.tsx +51 -0
  42. package/src/blocks/e-commerce/product-categories/product-category-2.tsx +56 -36
  43. package/src/blocks/e-commerce/product-list/product-card-1.test.tsx +88 -0
  44. package/src/blocks/e-commerce/product-list/product-card-1.tsx +141 -80
  45. package/src/blocks/e-commerce/product-list/product-showcase-1.test.tsx +78 -0
  46. package/src/blocks/e-commerce/product-list/product-showcase-1.tsx +277 -0
  47. package/src/blocks/e-commerce/product-overview/product-overview-1.test.tsx +182 -0
  48. package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +298 -129
  49. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.test.tsx +229 -0
  50. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.tsx +313 -110
  51. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx +72 -0
  52. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +20 -1
  53. package/src/blocks/marketing/footers/footer-section-1.test.tsx +117 -0
  54. package/src/blocks/marketing/footers/footer-section-1.tsx +46 -9
  55. package/src/blocks/marketing/navbars/navbar-4-account-menu.test.tsx +38 -0
  56. package/src/blocks/marketing/navbars/navbar-4-account-menu.tsx +30 -14
  57. package/src/blocks/marketing/navbars/navbar-4-cart-sheet.test.tsx +204 -0
  58. package/src/blocks/marketing/navbars/navbar-4-cart-sheet.tsx +144 -21
  59. package/src/blocks/marketing/navbars/navbar-4.test.tsx +233 -0
  60. package/src/blocks/marketing/navbars/navbar-4.tsx +235 -60
  61. package/src/index.ts +56 -0
  62. package/src/lib/cart.test.ts +15 -0
  63. package/src/lib/cart.ts +11 -0
  64. package/src/lib/contracts-curated.test.ts +207 -0
  65. package/src/lib/contracts-server-boundary.test.ts +52 -0
  66. package/src/lib/contracts.ts +234 -6
  67. package/src/lib/money.test.ts +64 -0
  68. package/src/lib/money.ts +65 -0
  69. package/tsup.config.ts +0 -1
  70. package/vitest.config.ts +9 -1
@@ -2,19 +2,20 @@
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'
16
16
  import { MobileCategoryDrawer } from '../../e-commerce/navigation/mobile-category-drawer'
17
17
  import type { MenuItem } from '../../../lib/types/menu'
18
+ import type { Money } from '@thorprovider/types'
18
19
  import type { StoreCategory } from '../../../lib/contracts'
19
20
 
20
21
  function toMenuItem(category: StoreCategory): MenuItem {
@@ -41,10 +42,21 @@ function uniqueById<T extends { id: string }>(items: T[]): T[] {
41
42
  })
42
43
  }
43
44
 
44
- function Logo({ text, href }: { text: string; href?: string }) {
45
+ export interface LogoAccent {
46
+ /** Trailing segment of `logoText` to render with the accent styling. */
47
+ text: string
48
+ /** Consumer-supplied classes for the accent span (e.g. a brand color token). */
49
+ className?: string
50
+ }
51
+
52
+ function Logo({ text, href, accent }: { text: string; href?: string; accent?: LogoAccent }) {
45
53
  const className =
46
54
  'focus-visible:ring-ring flex items-center gap-2 rounded-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none'
47
55
 
56
+ const accentText = accent?.text ?? ''
57
+ const hasAccent = accentText.length > 0 && text.endsWith(accentText)
58
+ const baseText = hasAccent ? text.slice(0, text.length - accentText.length) : text
59
+
48
60
  const content = (
49
61
  <>
50
62
  <svg viewBox='0 0 32 32' aria-hidden='true' className='text-primary size-7' fill='none' xmlns='http://www.w3.org/2000/svg'>
@@ -73,7 +85,10 @@ function Logo({ text, href }: { text: string; href?: string }) {
73
85
  <circle cx='13.1' cy='26.1' r='1.7' stroke='currentColor' strokeWidth='1.2' />
74
86
  <circle cx='22.1' cy='26.1' r='1.7' stroke='currentColor' strokeWidth='1.2' />
75
87
  </svg>
76
- <span className='text-base font-semibold tracking-tight'>{text}</span>
88
+ <span className='text-base font-semibold tracking-tight'>
89
+ {baseText}
90
+ {hasAccent ? <span className={accent?.className}>{accentText}</span> : null}
91
+ </span>
77
92
  </>
78
93
  )
79
94
 
@@ -148,43 +163,138 @@ function CartBadge({ count }: CartBadgeProps) {
148
163
 
149
164
  export interface Navbar4Props {
150
165
  storeCategories: StoreCategory[]
151
- 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
152
172
  utilityLinks: UtilityLink[]
153
173
  logoText: string
154
174
  logoHref?: string
155
- /** When provided, a mobile-only "Account" link is appended to the utility links. */
175
+ /**
176
+ * Optional accent for a trailing segment of `logoText` (e.g. the "Cuba" in
177
+ * "ElectroCuba"). When omitted, or when `text` is not a trailing segment of
178
+ * `logoText`, the wordmark renders unchanged.
179
+ */
180
+ logoAccent?: LogoAccent
181
+ /** Account destination for anonymous shoppers on every viewport. */
156
182
  accountHref?: string
183
+ /**
184
+ * When `false`, no search control is rendered at all — neither the desktop
185
+ * input, the mobile trigger, nor the mobile panel. Defaults to `true` so
186
+ * legacy consumers keep the existing self-contained search. The generated
187
+ * storefront passes `false` while no verified global search contract exists
188
+ * (E2 task 5.4), so no global search is presented as working.
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
195
+ showSearch?: boolean
157
196
  searchQuery?: string
158
197
  onSearchChange?: (value: string) => void
159
198
  items?: CartItem[]
199
+ /**
200
+ * Quantity DELTA for a backend cart line-item id. The boundary converts the
201
+ * delta into an absolute quantity against the authoritative current line
202
+ * quantity before mutating the cart.
203
+ */
160
204
  onQtyChange?: (id: string, delta: number) => void
205
+ /** Remove the backend cart line-item id. */
161
206
  onRemove?: (id: string) => void
207
+ /** Authoritative backend subtotal for the mini-cart. */
208
+ subtotal?: Money
209
+ /** Shown when an authoritative subtotal is supplied but absent/malformed. */
210
+ subtotalUnavailableLabel?: string
211
+ /** Override money formatting (locale, currency). Defaults to `Money.currencyCode`. */
212
+ formatMoney?: (money: Money) => string
162
213
  user?: StoreUser
163
214
  accountMenuItems?: AccountMenuItem[]
164
215
  onHelp?: () => void
165
216
  onSignOut?: () => void
217
+ accountPending?: boolean
218
+ accountUnavailable?: boolean
219
+ onAccountRetry?: () => void
166
220
  onCheckout?: () => void
167
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
251
+ /**
252
+ * Consumer-supplied classes merged onto the outer `<header>` as the final
253
+ * argument, so a semantic surface such as `bg-brand-nav
254
+ * text-brand-nav-foreground` overrides the default `bg-background`. Purely
255
+ * presentational: no layout, structure, or behavior changes.
256
+ */
257
+ className?: string
168
258
  }
169
259
 
170
260
  export function Navbar4({
171
261
  storeCategories,
172
- shippingNote,
262
+ shippingNote = '',
173
263
  utilityLinks,
174
264
  logoText,
175
265
  logoHref,
266
+ logoAccent,
176
267
  accountHref,
268
+ currentPath,
269
+ showSearch = true,
177
270
  searchQuery: searchQueryProp,
178
271
  onSearchChange,
179
272
  items: itemsProp,
180
273
  onQtyChange: onQtyChangeProp,
181
274
  onRemove: onRemoveProp,
275
+ subtotal,
276
+ subtotalUnavailableLabel,
277
+ formatMoney,
182
278
  user,
183
279
  accountMenuItems,
184
280
  onHelp,
185
281
  onSignOut,
282
+ accountPending = false,
283
+ accountUnavailable = false,
284
+ onAccountRetry,
186
285
  onCheckout,
187
286
  onViewCart,
287
+ cartOpen,
288
+ onCartOpenChange,
289
+ canonicalCart,
290
+ cartState,
291
+ cartErrorLabel,
292
+ cartRetryLabel,
293
+ onCartRetry,
294
+ checkoutUnavailable,
295
+ checkoutDisabled,
296
+ viewCartUnavailable,
297
+ className,
188
298
  }: Navbar4Props) {
189
299
  /**
190
300
  * Integration seam
@@ -228,37 +338,58 @@ export function Navbar4({
228
338
  const count = useMemo(() => items.reduce((total, item) => total + item.qty, 0), [items])
229
339
 
230
340
  useEffect(() => {
231
- if (!searchOpen) {
341
+ if (!showSearch || !searchOpen) {
232
342
  return
233
343
  }
234
344
 
235
345
  document.getElementById('store-search-mobile')?.focus()
236
- }, [searchOpen])
346
+ }, [searchOpen, showSearch])
237
347
 
238
348
  const dismissSearch = () => {
239
349
  setSearchOpen(false)
240
350
  searchTriggerRef.current?.focus()
241
351
  }
242
352
 
353
+ // When the consumer overrides the header surface via `className`, the nav
354
+ // links inherit its text color and need an explicit hover foreground so
355
+ // `hover:bg-muted` never paints white-on-light; otherwise they keep the
356
+ // default foreground (and the default hover surface).
357
+ const navLinkColor =
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
366
+
243
367
  return (
244
- <header className='border-border bg-background/95 sticky top-0 z-40 w-full border-b backdrop-blur-md supports-backdrop-filter:bg-background/80'>
245
- <div className='bg-foreground text-background hidden lg:block'>
246
- <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'>
247
- <p className='text-background/80'>{shippingNote}</p>
248
- <ul className='flex items-center gap-5'>
249
- {utilityLinks.map(link => (
250
- <li key={`${link.href}:${link.label}`}>
251
- <a
252
- href={link.href}
253
- className='text-background/80 hover:text-background transition-colors duration-150'
254
- >
255
- {link.label}
256
- </a>
257
- </li>
258
- ))}
259
- </ul>
368
+ <header
369
+ className={cn(
370
+ 'border-border sticky top-0 z-40 w-full border-b backdrop-blur-md',
371
+ className ?? 'bg-background/95 supports-backdrop-filter:bg-background/80',
372
+ )}
373
+ >
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>
260
391
  </div>
261
- </div>
392
+ ) : null}
262
393
 
263
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'>
264
395
  <Button
@@ -285,7 +416,7 @@ export function Navbar4({
285
416
  />
286
417
 
287
418
  <div className='flex-1 lg:flex-none'>
288
- <Logo text={logoText} href={logoHref} />
419
+ <Logo text={logoText} href={logoHref} accent={logoAccent} />
289
420
  </div>
290
421
 
291
422
  <nav aria-label='Store' className='hidden flex-1 lg:block'>
@@ -295,9 +426,11 @@ export function Navbar4({
295
426
  <li key={category.href} className='group relative'>
296
427
  <a
297
428
  href={category.href}
429
+ aria-current={isCurrent(category.href) ? 'page' : undefined}
298
430
  className={cn(
299
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',
300
- category.emphasis ? 'text-destructive' : 'text-foreground',
432
+ category.emphasis ? 'text-destructive' : navLinkColor,
433
+ isCurrent(category.href) && 'font-semibold underline underline-offset-4',
301
434
  )}
302
435
  >
303
436
  {category.label}
@@ -315,9 +448,11 @@ export function Navbar4({
315
448
  <li key={category.href}>
316
449
  <a
317
450
  href={category.href}
451
+ aria-current={isCurrent(category.href) ? 'page' : undefined}
318
452
  className={cn(
319
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',
320
- category.emphasis ? 'text-destructive' : 'text-foreground',
454
+ category.emphasis ? 'text-destructive' : navLinkColor,
455
+ isCurrent(category.href) && 'font-semibold underline underline-offset-4',
321
456
  )}
322
457
  >
323
458
  {category.label}
@@ -329,38 +464,67 @@ export function Navbar4({
329
464
  </nav>
330
465
 
331
466
  <div className='flex items-center gap-1.5'>
332
- <div className='hidden lg:block lg:w-64'>
333
- <Navbar4SearchBar
334
- id='store-search'
335
- value={searchQuery}
336
- onValueChange={setSearchQuery}
337
- />
338
- </div>
467
+ {showSearch ? (
468
+ <>
469
+ <div className='hidden lg:block lg:w-64'>
470
+ <Navbar4SearchBar
471
+ id='store-search'
472
+ value={searchQuery}
473
+ onValueChange={setSearchQuery}
474
+ />
475
+ </div>
339
476
 
340
- <Button
341
- ref={searchTriggerRef}
342
- variant='ghost'
343
- size='icon'
344
- className='lg:hidden'
345
- aria-label='Search products'
346
- aria-expanded={searchOpen}
347
- onClick={() => setSearchOpen(open => !open)}
348
- >
349
- {searchOpen ? <X /> : <Search />}
350
- </Button>
351
-
352
- {user ? (
353
- <div className='hidden lg:block'>
354
- <Navbar4AccountMenu
355
- user={user}
356
- items={accountMenuItems}
357
- onHelp={onHelp}
358
- onSignOut={onSignOut}
359
- />
360
- </div>
477
+ <Button
478
+ ref={searchTriggerRef}
479
+ variant='ghost'
480
+ size='icon'
481
+ className='lg:hidden'
482
+ aria-label='Search products'
483
+ aria-expanded={searchOpen}
484
+ onClick={() => setSearchOpen(open => !open)}
485
+ >
486
+ {searchOpen ? <X /> : <Search />}
487
+ </Button>
488
+ </>
361
489
  ) : null}
362
490
 
363
- <Sheet>
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
+ )
523
+ ) : null}
524
+
525
+ <Sheet
526
+ {...(cartOpen !== undefined ? { open: cartOpen, onOpenChange: onCartOpenChange } : {})}
527
+ >
364
528
  <SheetTrigger asChild>
365
529
  <Button
366
530
  variant='ghost'
@@ -382,8 +546,19 @@ export function Navbar4({
382
546
  items={items}
383
547
  onQtyChange={changeQty}
384
548
  onRemove={removeItem}
549
+ subtotal={subtotal}
550
+ subtotalUnavailableLabel={subtotalUnavailableLabel}
551
+ formatMoney={formatMoney}
385
552
  onCheckout={onCheckout}
386
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}
387
562
  />
388
563
  </SheetContent>
389
564
  </Sheet>
@@ -394,7 +569,7 @@ export function Navbar4({
394
569
  {`Cart contains ${count} ${count === 1 ? 'item' : 'items'}`}
395
570
  </p>
396
571
 
397
- {searchOpen ? (
572
+ {showSearch && searchOpen ? (
398
573
  <div className='border-border flex flex-col gap-0 border-t p-4 lg:hidden'>
399
574
  <Navbar4SearchBar
400
575
  id='store-search-mobile'
@@ -408,4 +583,4 @@ export function Navbar4({
408
583
  )
409
584
  }
410
585
 
411
- export default Navbar4
586
+ export default Navbar4
package/src/index.ts CHANGED
@@ -1,3 +1,5 @@
1
+ 'use client'
2
+
1
3
  export { cn } from './lib/utils'
2
4
  export type { CTA } from './lib/types'
3
5
 
@@ -54,7 +56,19 @@ export { default as ProductCard1 } from './blocks/e-commerce/product-list/produc
54
56
  export { default as ProductCard2 } from './blocks/e-commerce/product-list/product-card-2'
55
57
  export { default as ProductCard3 } from './blocks/e-commerce/product-list/product-card-3'
56
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'
57
65
  export { default as ProductOverview1 } from './blocks/e-commerce/product-overview/product-overview-1'
66
+ export type {
67
+ ProductOverview1Props,
68
+ ProductOverview1Rating,
69
+ ProductDetailProduct,
70
+ ProductDetailVariant,
71
+ } from './blocks/e-commerce/product-overview/product-overview-1'
58
72
  export { default as ProductOverview2 } from './blocks/e-commerce/product-overview/product-overview-2'
59
73
  export { default as ProductOverview5 } from './blocks/e-commerce/product-overview/product-overview-5'
60
74
  export type { ProductOverview2Props, ProductOverview2Product } from './blocks/e-commerce/product-overview/product-overview-2'
@@ -79,18 +93,38 @@ export { MobileCategoryDrawer, mobileCategoryDrawerPropsSchema } from './blocks/
79
93
  export type { MobileCategoryDrawerProps } from './blocks/e-commerce/navigation/mobile-category-drawer'
80
94
  export { CategoryIcon, categoryIconNameSchema, categoryIconPropsSchema } from './blocks/e-commerce/navigation/category-icon'
81
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'
82
104
  export { menuItemSchema, collectionItemSchema } from './lib/types/menu'
83
105
  export type { MenuItem, CollectionItem } from './lib/types/menu'
84
106
  export { default as CategoryFilter1 } from './blocks/e-commerce/category-filters/category-filter-1'
107
+ export type {
108
+ CategoryFilter1Category,
109
+ CategoryFilter1PriceRange,
110
+ CategoryFilter1SortOption,
111
+ } from './blocks/e-commerce/category-filters/category-filter-1'
85
112
  export { default as CategoryFilter2 } from './blocks/e-commerce/category-filters/category-filter-2'
113
+ export { CategoryFilter2StatusView } from './blocks/e-commerce/category-filters/category-filter-2'
86
114
  export type {
87
115
  FilterProduct,
88
116
  CategoryFilter2Props,
89
117
  CategoryFilter2Category,
118
+ CategoryFilter2Collection,
90
119
  CategoryFilter2Color,
91
120
  CategoryFilter2PriceRange,
92
121
  CategoryFilter2SortOption,
93
122
  CategoryFilter2Filters,
123
+ CategoryFilter2Status,
124
+ CategoryFilter2StatusViewProps,
125
+ CategoryFilter2QueryError,
126
+ CategoryFilter2ActiveFilter,
127
+ CategoryFilter2ActiveFilterKind,
94
128
  } from './blocks/e-commerce/category-filters/category-filter-2'
95
129
  export { default as CategoryFilter4 } from './blocks/e-commerce/category-filters/category-filter-4'
96
130
  export type { CategoryFilter4Props, CategoryFilter4Product, CategoryFilter4Facets } from './blocks/e-commerce/category-filters/category-filter-4'
@@ -116,6 +150,9 @@ export type {
116
150
  CheckoutForm1OrderSummary,
117
151
  CheckoutForm1OrderSummaryItem,
118
152
  CheckoutForm1CountryOption,
153
+ CheckoutForm1ShippingOption,
154
+ CheckoutForm1Totals,
155
+ CheckoutForm1TotalsState,
119
156
  } from './blocks/e-commerce/checkout-forms/checkout-form-1'
120
157
  export type {
121
158
  CheckoutForm2Props,
@@ -168,9 +205,22 @@ export type { Verify1Props } from './blocks/application/verification/verify-1'
168
205
  export type { CanonMoney, CanonProduct } from './lib/contracts'
169
206
  export { PRODUCT_IMAGE_FALLBACK } from './lib/contracts'
170
207
  export {
208
+ isCompleteFiniteDecimal,
209
+ isAvailableMoney,
210
+ formatMoney,
211
+ } from './lib/money'
212
+ export { resolveAbsoluteQuantity } from './lib/cart'
213
+ export {
214
+ moneySchema,
215
+ cartTotalsSchema,
216
+ productDetailProductSchema,
217
+ productDetailVariantSchema,
218
+ productOverview1PropsSchema,
171
219
  buttonPropsSchema,
172
220
  storeContainer1PropsSchema,
173
221
  productCard1PropsSchema,
222
+ productPresentationSchema,
223
+ productShowcase1PropsSchema,
174
224
  shoppingCart4PropsSchema,
175
225
  navbar4PropsSchema,
176
226
  paymentMethodSchema,
@@ -184,9 +234,15 @@ export {
184
234
  accountMenuItemSchema,
185
235
  } from './lib/contracts'
186
236
  export type {
237
+ ProductDetailProductSpec,
238
+ ProductDetailVariantSpec,
239
+ ProductOverview1SpecProps,
240
+ CartTotals,
187
241
  ButtonSpecProps,
188
242
  StoreContainer1SpecProps,
189
243
  ProductCard1SpecProps,
244
+ ProductPresentation,
245
+ ProductShowcase1SpecProps,
190
246
  ShoppingCart4SpecProps,
191
247
  Navbar4SpecProps,
192
248
  CardProduct,
@@ -0,0 +1,15 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { resolveAbsoluteQuantity } from './cart'
3
+
4
+ describe('resolveAbsoluteQuantity', () => {
5
+ it('converts a Navbar DELTA into an ABSOLUTE quantity against the authoritative current quantity', () => {
6
+ expect(resolveAbsoluteQuantity(3, -1)).toBe(2)
7
+ expect(resolveAbsoluteQuantity(3, 1)).toBe(4)
8
+ expect(resolveAbsoluteQuantity(1, 1)).toBe(2)
9
+ })
10
+
11
+ it('never drops below one — removal is a distinct callback, not a zero quantity', () => {
12
+ expect(resolveAbsoluteQuantity(1, -1)).toBe(1)
13
+ expect(resolveAbsoluteQuantity(0, -1)).toBe(1)
14
+ })
15
+ })
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Cart line quantity normalization shared by the curated cart surfaces.
3
+ *
4
+ * The Navbar mini-cart expresses a quantity DELTA while the cart page reports
5
+ * an ABSOLUTE quantity. The boundary normalizes the delta against the
6
+ * authoritative current line quantity (from the backend cart) before mutating;
7
+ * it must never reinterpret one as the other.
8
+ */
9
+ export function resolveAbsoluteQuantity(currentQuantity: number, delta: number): number {
10
+ return Math.max(1, currentQuantity + delta)
11
+ }