@hanzo/commerce 6.2.4 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/buy/_to_deprecate_select-family-item-card.tsx +0 -1
- package/components/buy/_to_deprecate_select-family-item-panel.tsx +1 -1
- package/components/buy/add-to-cart-widget.tsx +9 -7
- package/components/buy/buy-card.tsx +3 -4
- package/components/buy/carousel-buy-card.tsx +120 -85
- package/components/buy/multi-family/all-variants-carousel.tsx +257 -0
- package/components/{select-family → buy/multi-family}/family-carousel/index.tsx +7 -17
- package/components/{select-family → buy/multi-family}/family-carousel/slide.tsx +3 -3
- package/components/{select-family → buy/multi-family}/family-carousel/state.ts +6 -2
- package/components/buy/multi-family/index.ts +2 -0
- package/components/buy/{foo.txt → please-keep.txt} +1 -1
- package/components/buy/single-family-selector.tsx +90 -0
- package/components/buy/title-and-byline.tsx +25 -0
- package/components/cart/cart-panel/cart-line-item.tsx +1 -1
- package/components/index.ts +1 -2
- package/components/item-selector/button.tsx +28 -18
- package/components/item-selector/carousel/index.tsx +197 -0
- package/components/item-selector/carousel/slider.tsx +40 -0
- package/components/{select-family/level-nodes-widget → node-tabs}/index.tsx +3 -3
- package/components/{select-family/level-nodes-widget → node-tabs}/node-image.tsx +1 -1
- package/package.json +2 -2
- package/service/impls/standalone/actual-line-item.ts +3 -1
- package/service/impls/standalone/standalone-service.ts +41 -4
- package/types/category-node.ts +21 -9
- package/types/commerce-service.ts +12 -13
- package/types/family.ts +7 -1
- package/types/index.ts +4 -3
- package/types/item-selector.ts +46 -9
- package/types/multi-family-selector-props.ts +20 -0
- package/types/selection-ui-specifier.ts +37 -21
- package/util/index.ts +3 -0
- package/util/item-selector-options-accessor.ts +32 -0
- package/util/multi-family-selector-options-accessor.ts +15 -0
- package/util/selection-ui-specifiers.ts +1 -5
- package/components/item-selector/carousel.tsx +0 -162
|
@@ -11,30 +11,20 @@ import {
|
|
|
11
11
|
CarouselPrevious,
|
|
12
12
|
CarouselNext,
|
|
13
13
|
} from '@hanzo/ui/primitives'
|
|
14
|
-
import type { Dimensions } from '@hanzo/ui/types'
|
|
15
14
|
|
|
16
|
-
import type {
|
|
15
|
+
import type { MultiFamilySelectorProps } from '../../../../types'
|
|
17
16
|
|
|
18
17
|
import FamilySlide from './slide'
|
|
19
|
-
import {
|
|
20
|
-
SlideState,
|
|
21
|
-
FamilyCarouselState
|
|
22
|
-
} from './state'
|
|
18
|
+
import { FamilyCarouselState } from './state'
|
|
23
19
|
|
|
24
|
-
import { useCommerce } from '
|
|
20
|
+
import { useCommerce } from '../../../..'
|
|
25
21
|
|
|
26
|
-
const FamilyCarousel: React.FC<{
|
|
27
|
-
families: Family[]
|
|
28
|
-
clx?: string
|
|
29
|
-
itemClx?: string
|
|
30
|
-
slideOptions?: FamilyCarouselSlideOptions
|
|
31
|
-
mediaConstraint?: Dimensions
|
|
32
|
-
mobile?: boolean
|
|
33
|
-
}> = ({
|
|
22
|
+
const FamilyCarousel: React.FC<MultiFamilySelectorProps> = ({
|
|
34
23
|
families,
|
|
35
24
|
clx='',
|
|
36
25
|
itemClx='',
|
|
37
|
-
|
|
26
|
+
itemOptions,
|
|
27
|
+
selectorOptions,
|
|
38
28
|
mediaConstraint={w: 250, h: 250},
|
|
39
29
|
mobile=false,
|
|
40
30
|
}) => {
|
|
@@ -70,7 +60,7 @@ const FamilyCarousel: React.FC<{
|
|
|
70
60
|
selectedItemRef={slideState}
|
|
71
61
|
selectSku={slideState.selectSku.bind(slideState)}
|
|
72
62
|
mediaConstraint={mediaConstraint}
|
|
73
|
-
options={
|
|
63
|
+
options={itemOptions}
|
|
74
64
|
mobile={mobile}
|
|
75
65
|
clx={itemClx}
|
|
76
66
|
/>
|
|
@@ -7,8 +7,8 @@ import {
|
|
|
7
7
|
} from '@hanzo/ui/primitives'
|
|
8
8
|
import type { Dimensions } from '@hanzo/ui/types'
|
|
9
9
|
|
|
10
|
-
import type {
|
|
11
|
-
import { ButtonItemSelector } from '
|
|
10
|
+
import type { ItemSelectorOptions, Family, LineItem, ItemSelector } from '../../../../types'
|
|
11
|
+
import { ButtonItemSelector } from '../../..'
|
|
12
12
|
import { cn } from '@hanzo/ui/util'
|
|
13
13
|
|
|
14
14
|
const FamilySlide: React.FC<Omit<ItemSelector, 'items'> & {
|
|
@@ -16,7 +16,7 @@ const FamilySlide: React.FC<Omit<ItemSelector, 'items'> & {
|
|
|
16
16
|
mediaConstraint: Dimensions
|
|
17
17
|
clx?: string
|
|
18
18
|
mobile?: boolean,
|
|
19
|
-
options?:
|
|
19
|
+
options?: ItemSelectorOptions
|
|
20
20
|
}> = ({
|
|
21
21
|
family,
|
|
22
22
|
selectedItemRef,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Family, LineItem, ObsLineItemRef, Product } from '
|
|
1
|
+
import type { Family, LineItem, ObsLineItemRef, Product } from '../../../../types'
|
|
2
2
|
import { observable, action, computed, makeObservable } from 'mobx'
|
|
3
3
|
|
|
4
4
|
class SlideState implements ObsLineItemRef {
|
|
@@ -49,7 +49,11 @@ class FamilyCarouselState {
|
|
|
49
49
|
_map = new Map<string, SlideState>()
|
|
50
50
|
_syncSku: (sku: string) => void
|
|
51
51
|
|
|
52
|
-
constructor(
|
|
52
|
+
constructor(
|
|
53
|
+
fams: Family[],
|
|
54
|
+
// initialFamilyId: string,
|
|
55
|
+
syncSkuToCurrentSlide: (sku: string) => void
|
|
56
|
+
) {
|
|
53
57
|
|
|
54
58
|
fams.forEach((f, index) => {this._map.set(f.id, new SlideState(
|
|
55
59
|
f,
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { observer } from 'mobx-react-lite'
|
|
3
|
+
|
|
4
|
+
import { MediaStack, Skeleton } from '@hanzo/ui/primitives'
|
|
5
|
+
import type { Dimensions } from '@hanzo/ui/types'
|
|
6
|
+
|
|
7
|
+
import { useCommerce } from '../..'
|
|
8
|
+
|
|
9
|
+
import type { ItemSelectorOptions, ItemSelectorProps, LineItem } from '../../types'
|
|
10
|
+
import TitleAndByline from './title-and-byline'
|
|
11
|
+
import { accessItemOptions } from '../../util'
|
|
12
|
+
import { cn } from '@hanzo/ui/util'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
const DEF_MEDIA_CONSTRAINT = {w: 200, h: 200}
|
|
16
|
+
|
|
17
|
+
const SingleFamilySelector: React.FC<{
|
|
18
|
+
items: LineItem[] // these may be sorted, so are we can't just use currentFamily.products
|
|
19
|
+
Selector: React.ComponentType<ItemSelectorProps>
|
|
20
|
+
selOptions: ItemSelectorOptions | undefined
|
|
21
|
+
scrollable?: boolean
|
|
22
|
+
showItemMedia?: boolean
|
|
23
|
+
mediaConstraint?: Dimensions
|
|
24
|
+
mobile?: boolean
|
|
25
|
+
clx?: string
|
|
26
|
+
titleAreaClx?: string
|
|
27
|
+
}> = ({
|
|
28
|
+
items,
|
|
29
|
+
Selector,
|
|
30
|
+
selOptions,
|
|
31
|
+
scrollable=false,
|
|
32
|
+
showItemMedia=true,
|
|
33
|
+
mediaConstraint=DEF_MEDIA_CONSTRAINT,
|
|
34
|
+
mobile=false,
|
|
35
|
+
clx='',
|
|
36
|
+
titleAreaClx=''
|
|
37
|
+
}) => {
|
|
38
|
+
|
|
39
|
+
const cmmc = useCommerce()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
const MyTitleAndByline: React.FC<{clx?: string}> = observer(({
|
|
43
|
+
clx=''
|
|
44
|
+
}) => {
|
|
45
|
+
const { familyTitle, showFamilyByline } = accessItemOptions(selOptions)
|
|
46
|
+
const title = (familyTitle === 'none' ?
|
|
47
|
+
undefined
|
|
48
|
+
:
|
|
49
|
+
(familyTitle === 'long' ?
|
|
50
|
+
cmmc.currentFamily?.title
|
|
51
|
+
:
|
|
52
|
+
(cmmc.currentFamily?.titleShort ?? cmmc.currentFamily?.title)
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
const byline = (familyTitle !== 'none') && showFamilyByline ? cmmc.currentFamily?.byline : undefined
|
|
56
|
+
return ( <TitleAndByline clx={clx} title={title} byline={byline}/> )
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const ItemMedia: React.FC = observer(() => (
|
|
60
|
+
cmmc.currentItem ? (
|
|
61
|
+
<MediaStack
|
|
62
|
+
media={cmmc.currentItem!}
|
|
63
|
+
constrainTo={mediaConstraint}
|
|
64
|
+
clx={'mb-2 ' + (scrollable ? 'shrink-0' : '')}
|
|
65
|
+
/>
|
|
66
|
+
) : (
|
|
67
|
+
<Skeleton className={'w-[200px] h-[200px] my-2 ' + (scrollable ? 'shrink-0' : '')}/>
|
|
68
|
+
)
|
|
69
|
+
))
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className={cn(clx, 'flex flex-col items-center', (scrollable ? 'shrink' : ''))}>
|
|
73
|
+
<MyTitleAndByline clx={cn(titleAreaClx, (scrollable ? 'shrink-0' : ''))} />
|
|
74
|
+
{showItemMedia && (<ItemMedia />)}
|
|
75
|
+
{items && (
|
|
76
|
+
<Selector
|
|
77
|
+
items={items}
|
|
78
|
+
selectedItemRef={cmmc}
|
|
79
|
+
selectSku={cmmc.setCurrentItem.bind(cmmc)}
|
|
80
|
+
scrollable={scrollable}
|
|
81
|
+
mobile={mobile}
|
|
82
|
+
options={selOptions}
|
|
83
|
+
clx={(scrollable ? 'shrink' : '')}
|
|
84
|
+
/>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export default SingleFamilySelector
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { ApplyTypography } from '@hanzo/ui/primitives'
|
|
4
|
+
import { cn } from '@hanzo/ui/util'
|
|
5
|
+
|
|
6
|
+
const TitleAndByline: React.FC<{
|
|
7
|
+
title: string | undefined
|
|
8
|
+
byline?: string | undefined
|
|
9
|
+
clx?: string
|
|
10
|
+
bylineClx?: string
|
|
11
|
+
}> = ({
|
|
12
|
+
title,
|
|
13
|
+
byline,
|
|
14
|
+
clx='',
|
|
15
|
+
bylineClx=''
|
|
16
|
+
}) => ( (title || byline) ? (
|
|
17
|
+
<ApplyTypography className={cn('flex flex-col items-center !gap-0 [&>*]:!m-0 ', clx)} >
|
|
18
|
+
<h4>{title}</h4>
|
|
19
|
+
{byline && (<h6 className={bylineClx}>{byline}</h6>)}
|
|
20
|
+
</ApplyTypography>
|
|
21
|
+
) : (null)
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
export default TitleAndByline
|
|
25
|
+
|
|
@@ -61,7 +61,7 @@ const CartLineItem: React.FC<{
|
|
|
61
61
|
</div>
|
|
62
62
|
<div className='flex flex-row items-center justify-between w-full'>
|
|
63
63
|
<div className='flex flex-row items-center'>
|
|
64
|
-
<AddToCartWidget
|
|
64
|
+
<AddToCartWidget variant='minimal' item={item} buttonClx='!h-8 md:!h-6' />
|
|
65
65
|
{item.quantity > 1 && (<span className='pl-2.5'>{'@' + formatCurrencyValue(item.price)}</span>)}
|
|
66
66
|
</div>
|
|
67
67
|
<div className='flex flex-row gap-1 items-center justify-end'>
|
package/components/index.ts
CHANGED
|
@@ -7,8 +7,7 @@ export { default as BuyDrawer } from './buy/buy-drawer'
|
|
|
7
7
|
export { default as CartAccordian } from './cart/cart-accordian'
|
|
8
8
|
export { default as CartPanel } from './cart/cart-panel'
|
|
9
9
|
|
|
10
|
-
export { default as
|
|
11
|
-
|
|
10
|
+
export { default as NodeTabs } from './node-tabs'
|
|
12
11
|
export { default as PaymentStepForm } from './checkout/payment-step-form'
|
|
13
12
|
export { default as ShippingStepForm } from './checkout/shipping-step-form'
|
|
14
13
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use client'
|
|
2
|
-
import React from 'react'
|
|
2
|
+
import React, { useEffect, useRef } from 'react'
|
|
3
3
|
import { observer } from 'mobx-react-lite'
|
|
4
4
|
|
|
5
5
|
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
|
@@ -14,7 +14,7 @@ import { cn } from '@hanzo/ui/util'
|
|
|
14
14
|
import type { Dimensions } from '@hanzo/ui/types'
|
|
15
15
|
|
|
16
16
|
import type { ItemSelectorProps, LineItem } from '../../types'
|
|
17
|
-
import { formatCurrencyValue } from '../../util'
|
|
17
|
+
import { accessItemOptions, formatCurrencyValue } from '../../util'
|
|
18
18
|
|
|
19
19
|
import QuantityIndicator from '../quantity-indicator'
|
|
20
20
|
|
|
@@ -72,15 +72,21 @@ const ButtonItemSelector: React.FC<ItemSelectorProps> = observer(({
|
|
|
72
72
|
options={}
|
|
73
73
|
}) => {
|
|
74
74
|
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
const {
|
|
76
|
+
showPrice,
|
|
77
|
+
showQuantity,
|
|
78
|
+
showFamilyInOption,
|
|
79
|
+
buttonType,
|
|
80
|
+
horizButtons,
|
|
81
|
+
} = accessItemOptions(options)
|
|
82
|
+
|
|
83
|
+
const showImage = buttonType !== 'text'
|
|
84
|
+
const showText = buttonType !== 'image'
|
|
85
|
+
const showBoth = buttonType === 'image-and-text'
|
|
80
86
|
|
|
81
87
|
const labelAndPrice = (item : LineItem) => (
|
|
82
|
-
(
|
|
83
|
-
(showPrice ? ((
|
|
88
|
+
(showFamilyInOption ? (item.familyTitle + ', ' + item.optionLabel) : item.optionLabel) +
|
|
89
|
+
(showPrice ? ((showFamilyInOption ? ': ' : ', ') + formatCurrencyValue(item.price)) : '')
|
|
84
90
|
)
|
|
85
91
|
|
|
86
92
|
const Item: React.FC<{
|
|
@@ -91,22 +97,24 @@ const ButtonItemSelector: React.FC<ItemSelectorProps> = observer(({
|
|
|
91
97
|
selected
|
|
92
98
|
}) => {
|
|
93
99
|
|
|
94
|
-
const textClx = (selected) ? 'text-accent
|
|
100
|
+
const textClx = (selected) ? 'text-accent' : 'text-muted'
|
|
95
101
|
const cursorClx = (selected) ? 'hover:cursor-default' : 'hover:cursor-pointer'
|
|
96
|
-
const justifyClx = (
|
|
97
|
-
|
|
102
|
+
const justifyClx = (showBoth) ? '' : 'justify-center'
|
|
103
|
+
// If no image, the Label must fill the entire button since it has to be
|
|
104
|
+
// clickable all the way to the border
|
|
105
|
+
const paddingClx = (showImage) ? (scrollable ? 'px-4 py-2' : 'px-2 py-2' ) : ''
|
|
98
106
|
let bgClx = ''
|
|
99
107
|
let borderClx = ''
|
|
100
108
|
if (scrollable) {
|
|
101
109
|
borderClx += (selected) ? 'border-foreground border ' : 'border-b border-muted-2 '
|
|
102
110
|
}
|
|
103
|
-
else if (!
|
|
111
|
+
else if (!showImage) {
|
|
104
112
|
borderClx += 'border rounded-lg '
|
|
105
113
|
borderClx += (selected) ? 'border-foreground ' : 'border-muted-2 '
|
|
106
114
|
bgClx += 'hover:bg-level-2'
|
|
107
115
|
}
|
|
108
116
|
|
|
109
|
-
const outerClx = ['h-10
|
|
117
|
+
const outerClx = ['h-10 flex items-center', justifyClx, paddingClx, bgClx, borderClx]
|
|
110
118
|
|
|
111
119
|
return (
|
|
112
120
|
<div className={cn(...outerClx, itemClx )} data-vaul-no-drag >
|
|
@@ -116,13 +124,15 @@ const ButtonItemSelector: React.FC<ItemSelectorProps> = observer(({
|
|
|
116
124
|
className={cn(
|
|
117
125
|
cursorClx,
|
|
118
126
|
(scrollable ? '' : 'border-transparent border-2 data-[state=checked]:border-foreground'),
|
|
119
|
-
|
|
127
|
+
showBoth ? 'mr-2' : (showImage ? '' : 'hidden')
|
|
120
128
|
)}
|
|
121
129
|
/>
|
|
122
130
|
<Label htmlFor={item.sku} className={cn(
|
|
123
131
|
showQuantity ? 'flex items-center' : 'block',
|
|
124
132
|
textClx,
|
|
125
133
|
cursorClx,
|
|
134
|
+
(showImage ? '' : 'self-stretch w-full flex items-center justify-center px-3 py-2'),
|
|
135
|
+
(showText ? '' : 'hidden')
|
|
126
136
|
)}>
|
|
127
137
|
<div className={showQuantity ? 'grow' : ''}>{labelAndPrice(item)}</div>
|
|
128
138
|
{showQuantity && (
|
|
@@ -140,10 +150,10 @@ const ButtonItemSelector: React.FC<ItemSelectorProps> = observer(({
|
|
|
140
150
|
|
|
141
151
|
return items.length > 1 ? (
|
|
142
152
|
<RadioGroup
|
|
143
|
-
className={cn(
|
|
153
|
+
className={cn(
|
|
144
154
|
(scrollable ? 'shrink min-h-0 gap-0' : (mobile ? 'gap-3' : 'gap-1')),
|
|
145
|
-
(horizButtons ?
|
|
146
|
-
(mobile &&
|
|
155
|
+
(horizButtons ? `grid grid-cols-${items.length} gap-1` : 'flex flex-col'),
|
|
156
|
+
(mobile && showText) ? 'min-w-pr-50' : '',
|
|
147
157
|
clx,
|
|
148
158
|
)}
|
|
149
159
|
onValueChange={selectSku}
|
|
@@ -0,0 +1,197 @@
|
|
|
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
|
+
import type { Dimensions } from '@hanzo/ui/types'
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
ApplyTypography,
|
|
11
|
+
type CarouselOptions,
|
|
12
|
+
type CarouselApi,
|
|
13
|
+
Carousel,
|
|
14
|
+
CarouselContent,
|
|
15
|
+
CarouselItem,
|
|
16
|
+
CarouselPrevious,
|
|
17
|
+
CarouselNext,
|
|
18
|
+
MediaStack,
|
|
19
|
+
} from '@hanzo/ui/primitives'
|
|
20
|
+
|
|
21
|
+
import type { ItemSelectorProps, LineItem } from '../../../types'
|
|
22
|
+
import { formatCurrencyValue, accessItemOptions } from '../../../util'
|
|
23
|
+
|
|
24
|
+
import QuantityIndicator from '../../quantity-indicator'
|
|
25
|
+
import ItemCarouselSlider from './slider'
|
|
26
|
+
|
|
27
|
+
const DEFAULT_CONSTRAINT = {w: 250, h: 250}
|
|
28
|
+
|
|
29
|
+
interface CarouselItemSelectorPropsExt {
|
|
30
|
+
constrainTo: Dimensions
|
|
31
|
+
options?: CarouselOptions
|
|
32
|
+
/** Do not show Family and / or Item title and Price */
|
|
33
|
+
imageOnly?: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const ItemSlide: React.FC<{
|
|
37
|
+
item: LineItem,
|
|
38
|
+
constrainTo: Dimensions
|
|
39
|
+
clx?: string
|
|
40
|
+
}> = ({
|
|
41
|
+
item,
|
|
42
|
+
constrainTo,
|
|
43
|
+
clx=''
|
|
44
|
+
}) => (
|
|
45
|
+
<CarouselItem className={cn('p-2 flex flex-col justify-center items-center', clx)}>
|
|
46
|
+
<MediaStack media={item} constrainTo={constrainTo} clx='my-4' />
|
|
47
|
+
</CarouselItem>
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
const CarouselItemSelector: React.FC<ItemSelectorProps> = observer(({
|
|
51
|
+
items,
|
|
52
|
+
selectSku,
|
|
53
|
+
selectedItemRef: itemRef,
|
|
54
|
+
clx='',
|
|
55
|
+
itemClx='',
|
|
56
|
+
options={},
|
|
57
|
+
ext={
|
|
58
|
+
options: {loop: true},
|
|
59
|
+
constrainTo: DEFAULT_CONSTRAINT,
|
|
60
|
+
imageOnly: false
|
|
61
|
+
} satisfies CarouselItemSelectorPropsExt
|
|
62
|
+
}) => {
|
|
63
|
+
|
|
64
|
+
const {
|
|
65
|
+
showPrice,
|
|
66
|
+
showQuantity,
|
|
67
|
+
showFamilyInOption,
|
|
68
|
+
showByline,
|
|
69
|
+
showSlider,
|
|
70
|
+
} = accessItemOptions(options)
|
|
71
|
+
|
|
72
|
+
const elbaApiRef = useRef<CarouselApi | undefined>(undefined)
|
|
73
|
+
const scrollToRef = useRef<((index: number) => void) | undefined>(undefined)
|
|
74
|
+
const dontRespondRef = useRef<boolean>(false)
|
|
75
|
+
|
|
76
|
+
const setApi = (api: CarouselApi) => {elbaApiRef.current = api}
|
|
77
|
+
const setScrollTo = (scrollTo: (index: number) => void) => { scrollToRef.current = scrollTo}
|
|
78
|
+
|
|
79
|
+
const carouselOptions = 'options' in ext ? ext.options : undefined
|
|
80
|
+
const constrainTo = 'constrainTo' in ext ? ext.constrainTo : DEFAULT_CONSTRAINT
|
|
81
|
+
const imageOnly = 'imageOnly' in ext ? ext.imageOnly : false
|
|
82
|
+
|
|
83
|
+
const onSelect = useCallback((emblaApi: CarouselApi) => {
|
|
84
|
+
if (dontRespondRef.current) {
|
|
85
|
+
dontRespondRef.current = false
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
const index = emblaApi.selectedScrollSnap()
|
|
89
|
+
if (index !== -1) {
|
|
90
|
+
selectSku(items[index].sku)
|
|
91
|
+
if (scrollToRef.current) {
|
|
92
|
+
scrollToRef.current(index)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
dontRespondRef.current = false
|
|
96
|
+
}, [scrollToRef.current ])
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
return reaction(
|
|
100
|
+
() => (itemRef.item),
|
|
101
|
+
(item) => {
|
|
102
|
+
if (elbaApiRef.current) {
|
|
103
|
+
const index = items.findIndex((el) => (el.sku === item?.sku))
|
|
104
|
+
if (index !== -1) {
|
|
105
|
+
dontRespondRef.current = true
|
|
106
|
+
elbaApiRef.current.scrollTo(index)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
}, [])
|
|
112
|
+
|
|
113
|
+
const optionLabel = () => (
|
|
114
|
+
showFamilyInOption ?
|
|
115
|
+
(itemRef.item?.familyTitle + ', ' + itemRef.item?.optionLabel)
|
|
116
|
+
:
|
|
117
|
+
itemRef.item?.optionLabel
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
const debugBorder = (c: 'r' | 'g' | 'b'): string => {
|
|
121
|
+
return ''
|
|
122
|
+
|
|
123
|
+
switch (c) {
|
|
124
|
+
case 'r': return ' border border-[#ffaaaa] '
|
|
125
|
+
case 'g': return ' border border-[#aaffaa] '
|
|
126
|
+
case 'b': return ' border border-[#aaaaff] '
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const onScrollIndexChange = (index: number) => {
|
|
131
|
+
dontRespondRef.current = true
|
|
132
|
+
selectSku(items![index].sku)
|
|
133
|
+
elbaApiRef.current?.scrollTo(index)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<div className={cn('w-full flex flex-col items-center', clx)}>
|
|
138
|
+
<Carousel
|
|
139
|
+
options={showSlider ? {...carouselOptions, loop: false} : carouselOptions }
|
|
140
|
+
className={'w-full px-2' + debugBorder('r')}
|
|
141
|
+
onCarouselSelect={onSelect}
|
|
142
|
+
setApi={setApi}
|
|
143
|
+
>
|
|
144
|
+
<CarouselContent>
|
|
145
|
+
{items.map((item) => (
|
|
146
|
+
<ItemSlide key={item.sku} item={item} constrainTo={constrainTo} clx={itemClx} />
|
|
147
|
+
))}
|
|
148
|
+
</CarouselContent>
|
|
149
|
+
{!showSlider && (<>
|
|
150
|
+
<CarouselPrevious className='left-1'/>
|
|
151
|
+
<CarouselNext className='right-1'/>
|
|
152
|
+
</>)}
|
|
153
|
+
</Carousel>
|
|
154
|
+
|
|
155
|
+
{(!imageOnly && itemRef.item) && (<>
|
|
156
|
+
<ApplyTypography className='flex flex-col items-center [&>*]:!m-0 !gap-1 '>
|
|
157
|
+
<div className={
|
|
158
|
+
'flex items-center gap-1 [&>*]:!m-0 ' + debugBorder('g') +
|
|
159
|
+
(showFamilyInOption ? 'flex-col' : 'flex-row')
|
|
160
|
+
}>
|
|
161
|
+
<h6 className='font-semibold'>
|
|
162
|
+
{optionLabel() + (showPrice && !showFamilyInOption ? ',' : '')}
|
|
163
|
+
</h6>
|
|
164
|
+
<div className={
|
|
165
|
+
'flex items-center gap-1 [&>*]:!m-0 flex-row ' + debugBorder('b') +
|
|
166
|
+
(showFamilyInOption ? 'w-full justify-between' : '')
|
|
167
|
+
}>
|
|
168
|
+
{showPrice && (<p>{formatCurrencyValue(itemRef.item.price)}</p>)}
|
|
169
|
+
{showQuantity && (
|
|
170
|
+
<QuantityIndicator
|
|
171
|
+
item={itemRef.item}
|
|
172
|
+
clx='h-[22px] ml-4'
|
|
173
|
+
iconClx='fill-foreground'
|
|
174
|
+
digitClx='not-typography font-semibold text-primary-fg leading-none font-sans text-xs'
|
|
175
|
+
/>
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
{showByline && itemRef.item.byline && (<p>{itemRef.item.byline}</p>)}
|
|
180
|
+
</ApplyTypography>
|
|
181
|
+
{showSlider && (
|
|
182
|
+
<ItemCarouselSlider
|
|
183
|
+
clx='mt-5 w-[320px]'
|
|
184
|
+
numStops={items.length}
|
|
185
|
+
setScrollTo={setScrollTo}
|
|
186
|
+
onIndexChange={onScrollIndexChange}
|
|
187
|
+
/>
|
|
188
|
+
)}
|
|
189
|
+
</>)}
|
|
190
|
+
</div>
|
|
191
|
+
)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
export {
|
|
195
|
+
type CarouselItemSelectorPropsExt,
|
|
196
|
+
CarouselItemSelector as default
|
|
197
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import React, { useEffect, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
import { Slider } from '@hanzo/ui/primitives'
|
|
5
|
+
|
|
6
|
+
const ItemCarouselSlider: React.FC<{
|
|
7
|
+
clx?: string
|
|
8
|
+
setScrollTo: (scrollTo: (index: number) => void) => void
|
|
9
|
+
onIndexChange: (i: number) => void
|
|
10
|
+
numStops: number
|
|
11
|
+
}> = ({
|
|
12
|
+
clx='',
|
|
13
|
+
setScrollTo,
|
|
14
|
+
onIndexChange,
|
|
15
|
+
numStops,
|
|
16
|
+
}) => {
|
|
17
|
+
|
|
18
|
+
const [index, setIndex] = useState<number>(0)
|
|
19
|
+
useEffect(() => { setScrollTo(setIndex) }, [setScrollTo])
|
|
20
|
+
|
|
21
|
+
const onValueChange = (v: number[]) => { setIndex(v[0]); onIndexChange(v[0]) }
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Slider
|
|
25
|
+
className={clx}
|
|
26
|
+
thumbClx='w-8 border-muted border-2 bg-level-1 focus-visible:ring-0 focus-visible:ring-offset-0 transition-none'
|
|
27
|
+
trackBgClx='bg-level-3'
|
|
28
|
+
rangeBgClx='bg-level-3'
|
|
29
|
+
thumbSlidingClx='bg-muted-2'
|
|
30
|
+
defaultValue={[0]}
|
|
31
|
+
min={0}
|
|
32
|
+
max={numStops - 1}
|
|
33
|
+
step={1}
|
|
34
|
+
value={[index]}
|
|
35
|
+
onValueChange={onValueChange}
|
|
36
|
+
/>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default ItemCarouselSlider
|
|
@@ -4,10 +4,10 @@ import React, { useState } from 'react'
|
|
|
4
4
|
import { ToggleGroup, ToggleGroupItem} from "@hanzo/ui/primitives"
|
|
5
5
|
import { cn } from '@hanzo/ui/util'
|
|
6
6
|
|
|
7
|
-
import type { CategoryNode, StringMutator, StringArrayMutator } from '
|
|
7
|
+
import type { CategoryNode, StringMutator, StringArrayMutator } from '../../types'
|
|
8
8
|
import NodeImage from './node-image'
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const NodeTabs: React.FC<{
|
|
11
11
|
levelNodes: CategoryNode[]
|
|
12
12
|
mutator: StringMutator | StringArrayMutator
|
|
13
13
|
multiple?: boolean
|
|
@@ -88,4 +88,4 @@ const LevelNodesWidget: React.FC<{
|
|
|
88
88
|
)
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
export default
|
|
91
|
+
export default NodeTabs
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/commerce",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.3.0",
|
|
4
4
|
"description": "e-commerce framework.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"@hanzo/auth": "^2.3.5",
|
|
44
|
-
"@hanzo/ui": "^3.
|
|
44
|
+
"@hanzo/ui": "^3.6.0",
|
|
45
45
|
"@hookform/resolvers": "^3.3.4",
|
|
46
46
|
"@radix-ui/react-radio-group": "^1.1.3",
|
|
47
47
|
"firebase": "^10.8.0",
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
observable,
|
|
6
6
|
} from 'mobx'
|
|
7
7
|
|
|
8
|
-
import type { ImageDef, VideoDef } from '@hanzo/ui/types'
|
|
8
|
+
import type { ImageDef, MediaTransform, VideoDef } from '@hanzo/ui/types'
|
|
9
9
|
|
|
10
10
|
import type { Product, LineItem, CommerceService } from '../../../types'
|
|
11
11
|
|
|
@@ -36,6 +36,7 @@ class ActualLineItem
|
|
|
36
36
|
img?: ImageDef
|
|
37
37
|
video?: VideoDef
|
|
38
38
|
animation?: string
|
|
39
|
+
mediaTransform?: MediaTransform
|
|
39
40
|
optionImg?: ImageDef
|
|
40
41
|
timeAdded: number = 0 // timeAdded of being added to cart
|
|
41
42
|
|
|
@@ -53,6 +54,7 @@ class ActualLineItem
|
|
|
53
54
|
this.video = prod.video
|
|
54
55
|
this.animation = prod.animation
|
|
55
56
|
this.optionImg = prod.optionImg
|
|
57
|
+
this.mediaTransform = prod.mediaTransform
|
|
56
58
|
|
|
57
59
|
if (snap) {
|
|
58
60
|
this.qu = snap.quantity
|