@hanzo/commerce 6.2.4 → 6.3.1

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 (36) hide show
  1. package/components/buy/_to_deprecate_select-family-item-card.tsx +0 -1
  2. package/components/buy/_to_deprecate_select-family-item-panel.tsx +1 -1
  3. package/components/buy/add-to-cart-widget.tsx +9 -7
  4. package/components/buy/buy-card.tsx +3 -4
  5. package/components/buy/carousel-buy-card.tsx +120 -85
  6. package/components/buy/multi-family/all-variants-carousel.tsx +257 -0
  7. package/components/{select-family → buy/multi-family}/family-carousel/index.tsx +7 -17
  8. package/components/{select-family → buy/multi-family}/family-carousel/slide.tsx +3 -3
  9. package/components/{select-family → buy/multi-family}/family-carousel/state.ts +6 -2
  10. package/components/buy/multi-family/index.ts +2 -0
  11. package/components/buy/{foo.txt → please-keep.txt} +1 -1
  12. package/components/buy/single-family-selector.tsx +90 -0
  13. package/components/buy/title-and-byline.tsx +25 -0
  14. package/components/cart/cart-panel/cart-line-item.tsx +1 -1
  15. package/components/index.ts +1 -2
  16. package/components/item-selector/button.tsx +28 -18
  17. package/components/item-selector/carousel/index.tsx +197 -0
  18. package/components/item-selector/carousel/slider.tsx +40 -0
  19. package/components/{select-family/level-nodes-widget → node-tabs}/index.tsx +3 -3
  20. package/components/{select-family/level-nodes-widget → node-tabs}/node-image.tsx +1 -1
  21. package/package.json +3 -3
  22. package/service/impls/standalone/actual-line-item.ts +3 -1
  23. package/service/impls/standalone/standalone-service.ts +41 -4
  24. package/types/category-node.ts +21 -9
  25. package/types/commerce-service.ts +12 -13
  26. package/types/family.ts +7 -1
  27. package/types/index.ts +4 -3
  28. package/types/item-selector.ts +46 -9
  29. package/types/multi-family-selector-props.ts +20 -0
  30. package/types/selection-ui-specifier.ts +37 -21
  31. package/util/index.ts +3 -0
  32. package/util/item-selector-options-accessor.ts +32 -0
  33. package/util/multi-family-selector-options-accessor.ts +15 -0
  34. package/util/promo-codes.ts +71 -9
  35. package/util/selection-ui-specifiers.ts +1 -5
  36. package/components/item-selector/carousel.tsx +0 -162
@@ -66,7 +66,6 @@ const SelectFamilyItemCard: React.FC<React.HTMLAttributes<HTMLDivElement> & Item
66
66
  // TODO disable if nothing selected
67
67
  (selItemRef.item && !isLoading) && (
68
68
  <AddToCartWidget
69
- size='default'
70
69
  item={selItemRef.item}
71
70
  onQuantityChanged={onQuantityChanged}
72
71
  className={cn('lg:min-w-[160px] lg:mx-auto', className)}
@@ -106,7 +106,7 @@ const SelectFamilyItemPanel: React.FC<
106
106
 
107
107
  const AddToCartArea: React.FC<{ className?: string }> = observer(({ className = '' }) => (
108
108
  (selectedItemRef.item && !isLoading) ? (
109
- <AddToCartWidget size='default' item={selectedItemRef.item} className={className}/>
109
+ <AddToCartWidget item={selectedItemRef.item} className={className}/>
110
110
  ) : (
111
111
  <div className={cn('h-6 w-12 invisible', className)} />
112
112
  )
@@ -14,7 +14,6 @@ const AddToCartWidget: React.FC<{
14
14
  disabled?: boolean
15
15
  className?: string
16
16
  buttonClx?: string
17
- size?: ButtonSizes
18
17
  variant?: 'minimal' | 'primary' | 'outline'
19
18
  onQuantityChanged?: (sku: string, oldV: number, newV: number) => void
20
19
  }> = observer(({
@@ -23,7 +22,6 @@ const AddToCartWidget: React.FC<{
23
22
  disabled=false,
24
23
  className='',
25
24
  buttonClx='',
26
- size='xs',
27
25
  onQuantityChanged
28
26
  }) => {
29
27
 
@@ -100,6 +98,8 @@ const AddToCartWidget: React.FC<{
100
98
  return ( item.isInCart ? (
101
99
  <div className={cn(
102
100
  'flex flex-row items-stretch justify-between ',
101
+ // should match 'xs' and 'default' button heights
102
+ (ghost ? 'h-8' : 'h-10'),
103
103
  ROUNDED_CLX,
104
104
  (primary ? 'bg-primary' : 'bg-transparent'),
105
105
  (outline ? 'border border-muted' : ''),
@@ -107,14 +107,15 @@ const AddToCartWidget: React.FC<{
107
107
  )}>
108
108
  <Button
109
109
  aria-label={'Remove a ' + item.title + ' from the cart'}
110
- size={size}
110
+ size={ghost ? 'xs' : 'default'}
111
111
  variant={primary ? 'primary' : 'ghost'}
112
112
  rounded={ghost ? 'full' : ROUNDED_VAL}
113
113
  className={cn(
114
114
  'lg:min-w-0 lg:px-2 grow justify-start group',
115
115
  (ghost ? 'px-1' : 'px-2'),
116
116
  (outline ? 'hover:bg-transparent' : ''),
117
- buttonClx
117
+ buttonClx,
118
+ 'h-auto self-stretch' // must be smaller than normal
118
119
  )}
119
120
  key='left'
120
121
  onClick={dec}
@@ -128,14 +129,15 @@ const AddToCartWidget: React.FC<{
128
129
  <div className={'text-sm grow-0 shrink-0 flex items-center cursor-default xs:px-2 ' + digitClx} >{item.quantity}{ghost ? '' : ' in Bag'}</div>
129
130
  <Button
130
131
  aria-label={'Add another ' + item.title + ' to the cart'}
131
- size={size}
132
+ size={ghost ? 'xs' : 'default'}
132
133
  variant={primary ? 'primary' : 'ghost'}
133
134
  rounded={ghost ? 'full' : ROUNDED_VAL}
134
135
  className={cn(
135
136
  'lg:min-w-0 lg:px-2 grow justify-end group',
136
137
  (ghost ? 'px-1' : 'px-2'),
137
138
  (outline ? 'hover:bg-transparent' : ''),
138
- buttonClx
139
+ buttonClx,
140
+ 'h-auto self-stretch' // must be smaller than normal
139
141
  )}
140
142
  onClick={inc}
141
143
  key='right'
@@ -146,7 +148,7 @@ const AddToCartWidget: React.FC<{
146
148
  ) : (
147
149
  <Button
148
150
  aria-label={'Add a ' + item.title + ' to cart'}
149
- size={size}
151
+ size={ghost ? 'xs' : 'default'}
150
152
  variant={variant as ButtonVariants}
151
153
  rounded={ROUNDED_VAL}
152
154
  className={cn(buttonClx, className)}
@@ -18,7 +18,7 @@ import { useCommerce } from '../../service/context'
18
18
  import * as pathUtils from '../../service/path-utils'
19
19
  import { getFacetValuesMutator, ObsStringMutator } from '../../util'
20
20
 
21
- import LevelNodesWidget from '../select-family/level-nodes-widget'
21
+ import NodeTabs from '../node-tabs'
22
22
  import AddToCartWidget from './add-to-cart-widget'
23
23
 
24
24
  const BuyCard: React.FC<{
@@ -170,7 +170,7 @@ const BuyCard: React.FC<{
170
170
  }
171
171
  }
172
172
 
173
- const famTitle = inst.current?.requestedFamily ? inst.current.requestedFamily.title : cmmc.selectedFamilies?.[0]?.title
173
+ const famTitle = inst.current?.requestedFamily ? inst.current.requestedFamily.title : cmmc.selectedFamilies[0].title
174
174
 
175
175
  const itemsToShow = (!cmmc.hasSelection) ? undefined :
176
176
  allVariants ? cmmc.selectedItems : cmmc.selectedFamilies[0].products as LineItem[]
@@ -205,7 +205,7 @@ const BuyCard: React.FC<{
205
205
  <h6 className='!text-center font-bold text-muted mt-3'>{inst.current!.requestedNode.subNodesLabel}</h6>
206
206
  )}
207
207
  </ApplyTypography>
208
- <LevelNodesWidget
208
+ <NodeTabs
209
209
  className={cn(
210
210
  'grid gap-0 align-stretch justify-normal ' + `grid-cols-${inst.current.requestedNode.subNodes!.length}`,
211
211
  'border-b-2 rounded-lg border-level-3 mb-4 -mr-2 -ml-2 max-w-[460px] h-10', // height is needed for iPhone bug
@@ -247,7 +247,6 @@ const BuyCard: React.FC<{
247
247
  )}
248
248
  {(cmmc.currentItem) && (
249
249
  <AddToCartWidget
250
- size='default'
251
250
  item={cmmc.currentItem}
252
251
  onQuantityChanged={onQuantityChanged}
253
252
  className={cn('min-w-[160px] mx-auto mt-4', (scroll ? 'shrink-0' : ''), addWidgetClx)}
@@ -1,9 +1,14 @@
1
1
  'use client'
2
- import React, { useRef, useEffect, type ComponentType, useState } from 'react'
2
+ import React, {
3
+ useRef,
4
+ useEffect,
5
+ type ComponentType,
6
+ useState
7
+ } from 'react'
3
8
  import { observer } from 'mobx-react-lite'
4
9
 
5
10
  import { cn } from '@hanzo/ui/util'
6
- import { ApplyTypography, Button, MediaStack, Skeleton } from '@hanzo/ui/primitives'
11
+ import { Button } from '@hanzo/ui/primitives'
7
12
 
8
13
  import type {
9
14
  ItemSelectorProps,
@@ -12,14 +17,17 @@ import type {
12
17
  CategoryNode,
13
18
  CategoryNodeRole,
14
19
  SelectionUISpecifier,
15
- ItemSelectorOptions,
20
+ ItemSelectorOptions,
21
+ MultiFamilySelectorProps,
22
+ MultiFamilySelectorOptions,
16
23
  } from '../../types'
17
24
 
18
25
  import { useCommerce } from '../../service/context'
19
26
  import { getSelectionUISpecifier } from '../../util'
20
27
 
21
28
  import { CarouselItemSelector, ButtonItemSelector } from '../item-selector'
22
- import FamilyCarousel from '../select-family/family-carousel'
29
+ import SingleFamilySelector from './single-family-selector'
30
+ import { FamilyCarousel, AllVariantsCarousel } from './multi-family'
23
31
 
24
32
  import AddToCartWidget from './add-to-cart-widget'
25
33
 
@@ -30,13 +38,25 @@ const SCROLL = {
30
38
 
31
39
  const MEDIA_CONSTRAINT = {w: 200, h: 200}
32
40
 
41
+ const sortItems = (items: LineItem[], sort: 'asc' | 'desc' | 'none'): LineItem[] => (
42
+ ((sort === 'asc') ?
43
+ items.sort((a: LineItem, b: LineItem): number => (a.price - b.price))
44
+ :
45
+ ((sort === 'desc') ?
46
+ items.sort((a: LineItem, b: LineItem): number => (b.price - a.price ))
47
+ :
48
+ items
49
+ )
50
+ )
51
+ )
52
+
33
53
  const CarouselBuyCard: React.FC<{
34
54
  skuPath: string
35
55
  clx?: string
36
56
  mobile?: boolean
37
57
  handleCheckout: () => void
38
58
  onQuantityChanged?: (sku: string, oldV: number, newV: number) => void
39
- }> = observer(({
59
+ }> = ({
40
60
  skuPath,
41
61
  clx='',
42
62
  mobile=false,
@@ -59,7 +79,13 @@ const CarouselBuyCard: React.FC<{
59
79
  selOptions: ItemSelectorOptions | undefined
60
80
  scrollable: boolean
61
81
  showItemMedia: boolean
62
- }
82
+ }
83
+ multi?: {
84
+ Selector: ComponentType<MultiFamilySelectorProps>
85
+ selectorOptions: MultiFamilySelectorOptions | undefined
86
+ itemOptions: ItemSelectorOptions | undefined
87
+ initialFamilyId: string
88
+ }
63
89
  } | undefined>(undefined)
64
90
 
65
91
  const [changeMeToRerender, setChangeMeToRerender] = useState<boolean>(false)
@@ -84,124 +110,133 @@ const CarouselBuyCard: React.FC<{
84
110
 
85
111
  if (peek.role === 'single-family') {
86
112
 
113
+ const sort = (): 'none' | 'asc' | 'desc' => {
114
+ if (!uiSpec.singleFamily?.options) {
115
+ return 'none'
116
+ }
117
+ const options = uiSpec.singleFamily.options
118
+ const showSlider = 'showSlider' in options ? options.showSlider! : true
119
+ return ('sort' in options) ?
120
+ options.sort!
121
+ :
122
+ (showSlider ? 'asc' : 'none')
123
+ }
124
+
87
125
  const items = peek.family!.products as LineItem[]
88
126
  const Selector: ComponentType<ItemSelectorProps> =
89
127
  (uiSpec.singleFamily?.type === 'buttons') ? ButtonItemSelector : CarouselItemSelector
90
128
  const selOptions = uiSpec.singleFamily?.options
91
129
 
92
130
  r.current.single = {
93
- items,
131
+ items: sortItems(items, sort()),
94
132
  Selector,
95
133
  selOptions,
96
134
  scrollable: !!(items.length > SCROLL.scrollAfter),
97
135
  showItemMedia: uiSpec.singleFamily?.type !== 'carousel'
98
136
  }
99
137
 
100
- const currItem = peek.item ?? peek.family!.products[0]
138
+ const currItem = peek.item ?? r.current.single.items[0]
101
139
  cmmc.setCurrentItem(currItem.sku)
102
140
  }
103
141
  else {
104
142
  const initialFamily = peek.family ? peek.family : peek.families![0]
143
+ // TODO: Does this ever need to be sorted??
105
144
  const currItem = peek.item ?? initialFamily.products[0]
145
+
146
+ // sets currentFamily as well
106
147
  cmmc.setCurrentItem(currItem.sku)
148
+
149
+ r.current.multi = {
150
+ Selector: (uiSpec.multiFamily?.type === 'family-carousel') ?
151
+ FamilyCarousel
152
+ :
153
+ AllVariantsCarousel,
154
+ itemOptions: uiSpec.multiFamily?.itemOptions,
155
+ selectorOptions: uiSpec.multiFamily?.selectorOptions,
156
+ initialFamilyId: initialFamily.id
157
+ }
107
158
  }
108
159
 
109
160
  // Must do this since Dialog code takes this comp out of the React shadow DOM
110
161
  // (only in prod apparently.)
111
162
  setChangeMeToRerender(!changeMeToRerender)
112
-
113
163
  }, [skuPath])
114
164
 
115
- const TitleArea: React.FC<{
116
- title: string
117
- byline?: string
165
+ const MultiFamilyUI: React.FC<{
166
+ Selector: ComponentType<MultiFamilySelectorProps>
167
+ selectorOptions: MultiFamilySelectorOptions | undefined
168
+ itemOptions: ItemSelectorOptions | undefined
169
+ families: Family[]
170
+ parent: CategoryNode
118
171
  clx?: string
119
- bylineClx?: string
120
172
  }> = ({
121
- title,
122
- byline,
123
- clx='',
124
- bylineClx=''
173
+ Selector,
174
+ itemOptions,
175
+ selectorOptions,
176
+ families,
177
+ parent,
178
+ clx=''
125
179
  }) => (
126
- <ApplyTypography className={clx}>
127
- <h3>{title}</h3>
128
- {byline && (<h6 className={bylineClx}>{byline}</h6>)}
129
- </ApplyTypography>
180
+ <Selector
181
+ families={families}
182
+ parent={parent}
183
+ clx={clx}
184
+ itemOptions={itemOptions}
185
+ selectorOptions={selectorOptions}
186
+ mediaConstraint={MEDIA_CONSTRAINT}
187
+ mobile={mobile}
188
+ />
130
189
  )
131
190
 
191
+ const Buttons: React.FC<{clx?: string}> = observer(({
192
+ clx=''
193
+ }) => (cmmc.currentItem ? (
194
+ <div className={clx}>
195
+ <AddToCartWidget
196
+ item={cmmc.currentItem}
197
+ onQuantityChanged={onQuantityChanged}
198
+ variant={cmmc.cartEmpty ? 'primary' : 'outline'}
199
+ className='min-w-[160px] w-full sm:max-w-[320px]'
200
+ />
201
+ {!cmmc.cartEmpty && (
202
+ <Button
203
+ onClick={handleCheckout}
204
+ variant='primary'
205
+ rounded='lg'
206
+ className='min-w-[160px] w-full sm:max-w-[320px]'
207
+ >
208
+ Checkout
209
+ </Button>
210
+ )}
211
+ </div>
212
+ ) : null))
213
+
132
214
  return (
133
215
  <div className={cn(
134
- 'px-4 md:px-6 pt-3 pb-4 flex flex-col gap-4 items-center min-h-[40vh]',
216
+ 'px-4 md:px-6 pt-3 pb-4 flex flex-col gap-1 items-center min-h-[40vh]',
135
217
  clx,
136
218
  r.current?.single?.scrollable ? SCROLL.scrollHeightClx : 'h-auto'
137
219
  )}>
138
- {r.current?.single ? (<>
139
- <TitleArea title={r.current?.family?.title ?? ''} clx=''/>
140
- {(r.current.single.showItemMedia && cmmc.currentItem ) && (
141
- <MediaStack
142
- media={cmmc.currentItem}
143
- constrainTo={MEDIA_CONSTRAINT}
144
- clx={(r.current.single.scrollable ? 'shrink-0' : '')}
145
- />
146
- )}
147
- {(r.current.single.showItemMedia && !cmmc.currentItem ) && (
148
- <Skeleton className={'w-[200px] h-[200px] my-4 ' + (r.current.single.scrollable ? 'shrink-0' : '')}/>
149
- )}
150
- {r.current.single.items && (
151
- <r.current.single.Selector
152
- items={r.current.single.items}
153
- selectedItemRef={cmmc}
154
- selectSku={cmmc.setCurrentItem.bind(cmmc)}
155
- scrollable={r.current.single.scrollable}
156
- mobile={mobile}
157
- options={r.current.single.selOptions}
158
- />
159
- )}
160
- </>) : (<>
161
- {r.current?.uiSpec.multiFamily?.showParentTitle && (
162
- <TitleArea
163
- title={r.current?.node.label ?? ''}
164
- byline={r.current?.node.subNodesLabel}
165
- clx={'mb-2 '}
166
- bylineClx='!text-center font-bold text-muted mt-3'
167
- />
168
- )}
169
- {r.current?.families && ( // safegaurd for first render etc.
170
- <FamilyCarousel
171
- families={r.current?.families}
172
- clx='w-full'
173
- slideOptions={r.current?.uiSpec.multiFamily?.slide.options}
220
+ {r.current?.single ? (
221
+ <SingleFamilySelector
222
+ {...r.current.single}
174
223
  mediaConstraint={MEDIA_CONSTRAINT}
175
- mobile={mobile}
224
+ mobile={mobile}
225
+ />
226
+ ) : (r.current?.multi && r.current.families && /* safegaurd for first render, etc. */ (
227
+ <MultiFamilyUI
228
+ {...r.current.multi}
229
+ families={r.current.families}
230
+ parent={r.current.node}
231
+ clx='max-w-[475px]'
176
232
  />
177
- )}
178
- </>)}
179
- {(cmmc.currentItem) && (
180
- <div className={cn(
181
- 'self-stretch mt-4 flex flex-col items-center gap-3',
233
+ ))}
234
+ <Buttons clx={cn(
235
+ 'self-stretch mt-8 flex flex-col items-center gap-3',
182
236
  (r.current?.single?.scrollable ? 'shrink-0' : '')
183
- )}>
184
- <AddToCartWidget
185
- size='default'
186
- item={cmmc.currentItem}
187
- onQuantityChanged={onQuantityChanged}
188
- variant={cmmc.cartEmpty ? 'primary' : 'outline'}
189
- className='min-w-[160px] w-full sm:max-w-[320px]'
190
- />
191
- {!cmmc.cartEmpty && (
192
- <Button
193
- onClick={handleCheckout}
194
- variant='primary'
195
- rounded='lg'
196
- className='min-w-[160px] w-full sm:max-w-[320px]'
197
- >
198
- Checkout
199
- </Button>
200
- )}
201
- </div>
202
- )}
237
+ )}/>
203
238
  </div >
204
239
  )
205
- })
240
+ }
206
241
 
207
242
  export default CarouselBuyCard
@@ -0,0 +1,257 @@
1
+ 'use client'
2
+ import React, { useCallback, useEffect, useRef, useState } from 'react'
3
+ import { reaction } from 'mobx'
4
+ import { observer } from 'mobx-react-lite'
5
+
6
+ import { cn } from '@hanzo/ui/util'
7
+
8
+ import {
9
+ ApplyTypography,
10
+ type CarouselApi,
11
+ Carousel,
12
+ CarouselContent,
13
+ CarouselItem,
14
+ CarouselPrevious,
15
+ CarouselNext,
16
+ MediaStack,
17
+ } from '@hanzo/ui/primitives'
18
+
19
+ import type { MultiFamilySelectorProps, LineItem } from '../../../types'
20
+ import {
21
+ formatCurrencyValue,
22
+ accessItemOptions,
23
+ accessMultiSelectorOptions
24
+ } from '../../../util'
25
+
26
+ import QuantityIndicator from '../../quantity-indicator'
27
+ import { ButtonItemSelector, useCommerce } from '../../..'
28
+
29
+ const debugBorder = (c: 'r' | 'g' | 'b', disable: boolean = true): string => {
30
+
31
+ if (disable) {
32
+ return ''
33
+ }
34
+ switch (c) {
35
+ case 'r': return ' border border-[#ffaaaa] '
36
+ case 'g': return ' border border-[#aaffaa] '
37
+ case 'b': return ' border border-[#aaaaff] '
38
+ }
39
+ }
40
+
41
+ const DEFAULT_CONSTRAINT = {w: 250, h: 250}
42
+
43
+ const AllVariantsCarousel: React.FC<MultiFamilySelectorProps> = ({
44
+ families,
45
+ parent,
46
+ clx='',
47
+ itemClx='',
48
+ itemOptions,
49
+ selectorOptions,
50
+ mediaConstraint=DEFAULT_CONSTRAINT,
51
+ mobile=false, // not relavant to any children
52
+ }) => {
53
+
54
+ const cmmc = useCommerce()
55
+ const r = useRef<{
56
+ api: CarouselApi | undefined
57
+ items: LineItem[]
58
+ dontRespond: boolean
59
+ initialIndex: number
60
+ } | undefined>(undefined)
61
+
62
+
63
+ const [changeMeToRerender, setChangeMeToRerender] = useState<boolean>(false)
64
+ // Safe, since Carousel is only render (and this method passed), once the ref is initialized.
65
+ const setApi = (api: CarouselApi) => { r.current!.api = api }
66
+
67
+ const onSelect = useCallback((emblaApi: CarouselApi) => {
68
+ if (r.current?.dontRespond) {
69
+ r.current.dontRespond = false
70
+ return
71
+ }
72
+ const index = emblaApi.selectedScrollSnap()
73
+ if (index !== -1) {
74
+ const item = r.current?.items[index]
75
+ cmmc.setCurrentItem(item?.sku) // (sets currentFamily as well)
76
+ }
77
+ if (r.current) {
78
+ r.current.dontRespond = false
79
+ }
80
+ }, [])
81
+
82
+ useEffect(() => {
83
+
84
+ const items = families.map((fam) => (fam.products as LineItem[])).flat()
85
+ const foundIndex = cmmc.currentItem ? items.findIndex((item) => (cmmc.currentItem!.sku == item.sku)) : -1
86
+ r.current = {
87
+ items,
88
+ initialIndex: foundIndex === -1 ? 0 : foundIndex,
89
+ dontRespond: false,
90
+ api: undefined
91
+ }
92
+
93
+ setChangeMeToRerender(!changeMeToRerender)
94
+
95
+ // This responds to the swatch clicks
96
+ return reaction(
97
+ () => (cmmc.currentItem),
98
+ (item) => {
99
+ if (r.current && r.current.api) {
100
+ const index = r.current.items.findIndex((_item: LineItem) => (_item.sku === item?.sku))
101
+ if (index && index !== -1) {
102
+ // no need to sync family, since ui only allows selecting within a family
103
+ r.current.dontRespond = true
104
+ r.current.api.scrollTo(index)
105
+ }
106
+ }
107
+ }
108
+ )
109
+ }, [])
110
+
111
+ const Header: React.FC<{clx?: string}> = observer(({
112
+ clx=''
113
+ }) => {
114
+
115
+ const { showParentTitle, parentByline: parentBylineMode } = accessMultiSelectorOptions(selectorOptions)
116
+ const { familyTitle, showFamilyByline } = accessItemOptions(itemOptions)
117
+
118
+ const parentTitleDisplay = showParentTitle ? parent.label : undefined
119
+ const parentBylineDisplay = (parentBylineMode === 'own-line') ? parent.subNodesLabel : ''
120
+
121
+ const title = (familyTitle === 'none' ?
122
+ undefined
123
+ :
124
+ (familyTitle === 'long' ?
125
+ cmmc.currentFamily?.title
126
+ :
127
+ (cmmc.currentFamily?.titleShort ?? cmmc.currentFamily?.title)
128
+ )
129
+ )
130
+
131
+ let titleLinePrefix = (parentBylineMode === 'none' || parentBylineMode === 'own-line') ? '' : (parent.subNodesLabel ?? '')
132
+ if (titleLinePrefix.length > 0 && title && parentBylineMode === 'comma-sep') {
133
+ titleLinePrefix += ', '
134
+ }
135
+ else if (titleLinePrefix.length > 0 && title && parentBylineMode === 'colon-sep') {
136
+ titleLinePrefix += ': '
137
+ }
138
+ const titleDisplay = title ? (titleLinePrefix + title) : undefined
139
+ const bylineDisplay = (familyTitle !== 'none') && showFamilyByline ? cmmc.currentFamily?.byline : undefined
140
+
141
+ return (
142
+ <ApplyTypography className={cn('flex flex-col items-center !gap-0 [&>*]:!m-0 ', clx)} >
143
+ {parentTitleDisplay && <h4>{parentTitleDisplay}</h4>}
144
+ {parentBylineDisplay && <h4>{parentBylineDisplay}</h4>}
145
+ {titleDisplay && <h4>{titleDisplay}</h4>}
146
+ {bylineDisplay && (<h6 >{bylineDisplay}</h6>)}
147
+ </ApplyTypography>
148
+ )
149
+ })
150
+
151
+ const ItemInfo: React.FC<{
152
+ clx?: string
153
+ labelClx?: string
154
+ }> = observer(({
155
+ clx='',
156
+ labelClx=''
157
+ }) => {
158
+
159
+ const {
160
+ showPrice,
161
+ showQuantity,
162
+ showFamilyInOption,
163
+ showByline,
164
+ } = accessItemOptions(itemOptions)
165
+
166
+ const optionLabel = () => (
167
+ showFamilyInOption ?
168
+ (cmmc.currentItem!.familyTitle + ', ' + cmmc.currentItem!.optionLabel)
169
+ :
170
+ cmmc.currentItem!.optionLabel
171
+ )
172
+
173
+ return (cmmc.currentItem && (
174
+ <ApplyTypography className={cn('flex flex-col items-center [&>*]:!m-0 !gap-1 ', clx)}>
175
+ <div className={
176
+ 'flex items-center gap-1 [&>*]:!m-0 ' +
177
+ debugBorder('g') +
178
+ (showFamilyInOption ? 'flex-col' : 'flex-row')
179
+ }>
180
+ <h6 className={cn('font-semibold', labelClx)}>
181
+ {optionLabel() + (showPrice && !showFamilyInOption ? ',' : '')}
182
+ </h6>
183
+ <div className={
184
+ 'flex items-center gap-1 [&>*]:!m-0 flex-row ' + debugBorder('b') +
185
+ (showFamilyInOption ? 'w-full justify-between' : '')
186
+ }>
187
+ {showPrice && (<p>{formatCurrencyValue(cmmc.currentItem.price)}</p>)}
188
+ {showQuantity && (
189
+ <QuantityIndicator
190
+ item={cmmc.currentItem}
191
+ clx='h-[22px] ml-4'
192
+ iconClx='fill-foreground'
193
+ digitClx='not-typography font-semibold text-primary-fg leading-none font-sans text-xs'
194
+ />
195
+ )}
196
+ </div>
197
+ </div>
198
+ {showByline && cmmc.currentItem.byline && (<p>{cmmc.currentItem.byline}</p>)}
199
+ </ApplyTypography>
200
+ ))
201
+ })
202
+
203
+ const Swatches: React.FC<{
204
+ clx?: string
205
+ }> = observer(({
206
+ clx=''
207
+ }) => {
208
+ const { showItemSwatches } = accessMultiSelectorOptions(selectorOptions)
209
+ if (
210
+ !showItemSwatches ||
211
+ !cmmc.currentFamily ||
212
+ cmmc.currentFamily.products.length === 1
213
+ ) {
214
+ return null
215
+ }
216
+ return <ButtonItemSelector
217
+ items={cmmc.currentFamily.products as LineItem[]}
218
+ selectedItemRef={cmmc}
219
+ selectSku={cmmc.setCurrentItem.bind(cmmc)}
220
+ clx={clx}
221
+ options={{
222
+ buttonType: 'image',
223
+ horizButtons: true
224
+ }}
225
+ />
226
+ })
227
+
228
+ return (
229
+ <div className={cn('w-full flex flex-col items-center', clx)}>
230
+ <Header />
231
+ {r.current && /* Only render once we've set the ref fields, or else bad things! */ (
232
+ <Carousel
233
+ options={{loop: true, startIndex: r.current.initialIndex}}
234
+ className={'w-full px-2' + debugBorder('r')}
235
+ onCarouselSelect={onSelect}
236
+ setApi={setApi}
237
+ >
238
+ <CarouselContent>
239
+ {r.current.items.map((item) => (
240
+ <CarouselItem key={item.sku} className={cn('p-2 flex flex-col justify-center items-center', itemClx)}>
241
+ <MediaStack media={item} constrainTo={mediaConstraint} clx='my-4'/>
242
+ </CarouselItem>
243
+ ))}
244
+ </CarouselContent>
245
+ <CarouselPrevious className='left-1'/>
246
+ <CarouselNext className='right-1'/>
247
+ </Carousel>
248
+ )}
249
+ <ItemInfo labelClx='!text-base font-medium'/>
250
+ <Swatches clx='mt-2'/>
251
+ </div>
252
+ )
253
+ }
254
+
255
+ export {
256
+ AllVariantsCarousel as default
257
+ }