@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.
- package/.turbo/turbo-build.log +15 -15
- package/.turbo/turbo-test.log +37 -27
- package/README.md +42 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/contracts.d.mts +162 -14
- package/dist/contracts.d.ts +162 -14
- package/dist/contracts.js +36 -3
- package/dist/contracts.js.map +1 -1
- package/dist/contracts.mjs +34 -3
- package/dist/contracts.mjs.map +1 -1
- package/dist/index.d.mts +294 -12
- package/dist/index.d.ts +294 -12
- package/dist/index.js +3577 -2882
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3431 -2750
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/blocks/e-commerce/navigation/back-to-top.test.tsx +27 -0
- package/src/blocks/e-commerce/navigation/back-to-top.tsx +71 -0
- package/src/blocks/e-commerce/navigation/breadcrumb.test.tsx +174 -0
- package/src/blocks/e-commerce/navigation/breadcrumb.tsx +130 -0
- package/src/blocks/e-commerce/navigation/mobile-bottom-nav.test.tsx +74 -0
- package/src/blocks/e-commerce/navigation/mobile-bottom-nav.tsx +78 -0
- package/src/blocks/e-commerce/navigation/skip-to-content.test.tsx +35 -0
- package/src/blocks/e-commerce/navigation/skip-to-content.tsx +37 -0
- package/src/blocks/e-commerce/product-list/product-card-1.test.tsx +88 -0
- package/src/blocks/e-commerce/product-list/product-card-1.tsx +141 -80
- package/src/blocks/e-commerce/product-list/product-showcase-1.test.tsx +78 -0
- package/src/blocks/e-commerce/product-list/product-showcase-1.tsx +277 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.test.tsx +92 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.tsx +86 -25
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx +7 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +1 -1
- package/src/blocks/marketing/navbars/navbar-4-account-menu.test.tsx +38 -0
- package/src/blocks/marketing/navbars/navbar-4-account-menu.tsx +30 -14
- package/src/blocks/marketing/navbars/navbar-4-cart-sheet.test.tsx +150 -0
- package/src/blocks/marketing/navbars/navbar-4-cart-sheet.tsx +102 -9
- package/src/blocks/marketing/navbars/navbar-4.test.tsx +121 -0
- package/src/blocks/marketing/navbars/navbar-4.tsx +134 -32
- package/src/index.ts +18 -0
- 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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
312
|
-
<div className='
|
|
313
|
-
<
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
<
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
423
|
-
<
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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,
|
package/src/lib/contracts.ts
CHANGED
|
@@ -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'
|