@luxfi/core 5.0.5 → 5.0.6

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.
@@ -13,6 +13,6 @@ export const getBullionFamilies = (videoMap: Map<string, VideoDef>) => (
13
13
  )
14
14
  )
15
15
 
16
- export { default as serviceOptions } from './lux-service-options'
16
+ export { default as serviceOptions } from '../../conf/lux-commerce-options'
17
17
  export { default as bullionPrice1oz } from './bullion-price-1oz'
18
18
 
@@ -0,0 +1,118 @@
1
+ import {
2
+ action,
3
+ computed,
4
+ makeObservable,
5
+ observable,
6
+ } from 'mobx'
7
+ import type { CommerceService, LineItem, ObsLineItemRef } from '@hanzo/commerce/types'
8
+
9
+
10
+ interface CommerceUI extends ObsLineItemRef {
11
+ showBuyOptions: (skuPath: string) => void
12
+ hideBuyOptions: () => void
13
+ get buyOptionsSkuPath(): string | undefined
14
+
15
+ itemQuantityChanged(sku: string, val: number, prevVal: number): void
16
+
17
+ get closed(): boolean
18
+ setClosed(b: boolean): void
19
+
20
+ }
21
+
22
+ class CommerceUIStore implements CommerceUI {
23
+
24
+ static readonly TIMEOUT = 1500
25
+ _buyOptionsSkuPath: string | undefined = undefined
26
+ _closed: boolean = false
27
+ _paused: boolean = false
28
+ _activeItem: LineItem | undefined = undefined
29
+ _lastActivity: number | undefined = undefined
30
+ _service: CommerceService
31
+
32
+ constructor(s: CommerceService) {
33
+ this._service = s
34
+ makeObservable(this, {
35
+ _buyOptionsSkuPath: observable,
36
+ _activeItem: observable.shallow,
37
+ _closed: observable,
38
+ showBuyOptions: action,
39
+ hideBuyOptions: action,
40
+ buyOptionsSkuPath: computed,
41
+ itemQuantityChanged: action,
42
+ setClosed: action,
43
+ closed: computed,
44
+ tick: action,
45
+ item: computed
46
+ })
47
+ }
48
+
49
+ showBuyOptions = (skuPath: string): void => {
50
+ this._service.setCurrentItem(undefined)
51
+ this._buyOptionsSkuPath = skuPath
52
+ this._paused = true
53
+ this._closed = false
54
+ }
55
+
56
+ hideBuyOptions = (): void => {
57
+ this._buyOptionsSkuPath = undefined
58
+ this._paused = false
59
+ if (this._lastActivity) {
60
+ this._lastActivity = Date.now()
61
+ }
62
+ }
63
+
64
+ get buyOptionsSkuPath(): string | undefined {
65
+ return this._buyOptionsSkuPath
66
+ }
67
+
68
+ tick = () => {
69
+ if (
70
+ !this._paused
71
+ &&
72
+ this._lastActivity
73
+ &&
74
+ (Date.now() - this._lastActivity >= CommerceUIStore.TIMEOUT)
75
+ ) {
76
+ this._activeItem = undefined
77
+ this._lastActivity = undefined
78
+ }
79
+ }
80
+
81
+ itemQuantityChanged = (sku: string, val: number, oldVal: number): void => {
82
+
83
+ if (val === 0) {
84
+ if (this._activeItem?.sku === sku) {
85
+ this._activeItem = undefined
86
+ this._lastActivity = undefined
87
+ }
88
+ // otherwise ignore
89
+ }
90
+ else if (val < oldVal) {
91
+ if (this._activeItem?.sku === sku) {
92
+ this._lastActivity = Date.now()
93
+ }
94
+ // otherwise ignore
95
+ }
96
+ else {
97
+ this._activeItem = this._service.getItemBySku(sku)
98
+ this._lastActivity = Date.now()
99
+ }
100
+ }
101
+
102
+ get item(): LineItem | undefined {
103
+ return this._activeItem
104
+ }
105
+
106
+ get closed(): boolean {
107
+ return this._closed
108
+ }
109
+
110
+ setClosed = (b: boolean): void => { this._closed = b}
111
+
112
+ dispose = () => {}
113
+ }
114
+
115
+ export {
116
+ CommerceUIStore,
117
+ type CommerceUI
118
+ }
@@ -0,0 +1,50 @@
1
+ 'use client'
2
+ import React, {
3
+ createContext,
4
+ useContext,
5
+ useRef,
6
+ type PropsWithChildren,
7
+ useEffect
8
+ } from 'react'
9
+
10
+ // https://dev.to/ivandotv/mobx-server-side-rendering-with-next-js-4m18
11
+ import { enableStaticRendering } from 'mobx-react-lite'
12
+ enableStaticRendering(typeof window === "undefined")
13
+
14
+ import { type CommerceUI, CommerceUIStore } from './commerce-ui'
15
+ import { useCommerce } from '@hanzo/commerce'
16
+
17
+ const CommerceUIContext = createContext<CommerceUIStore | undefined>(undefined)
18
+
19
+ const useCommerceUI = (): CommerceUI => {
20
+ return useContext(CommerceUIContext) as CommerceUIStore
21
+ }
22
+
23
+ const CommerceUIProvider: React.FC<PropsWithChildren & {
24
+ DEBUG_NO_TICK?: boolean
25
+ }> = ({
26
+ children,
27
+ DEBUG_NO_TICK=false
28
+ }) => {
29
+
30
+ const cmmc = useCommerce()
31
+ const valueRef = useRef<CommerceUIStore>(new CommerceUIStore(cmmc))
32
+
33
+ useEffect(() => {
34
+
35
+ //valueRef.current = new CommerceUIStore(cmmc)
36
+ return () => { valueRef.current?.dispose() }
37
+ }, [])
38
+
39
+ return (
40
+ <CommerceUIContext.Provider value={valueRef.current}>
41
+ {children}
42
+ </CommerceUIContext.Provider>
43
+ )
44
+ }
45
+
46
+ export {
47
+ useCommerceUI,
48
+ CommerceUIProvider
49
+ }
50
+
@@ -0,0 +1,20 @@
1
+ 'use client'
2
+ import React from 'react'
3
+
4
+ import type { LineItem } from '@hanzo/commerce/types'
5
+ import { AddToCartWidget } from '@hanzo/commerce'
6
+
7
+ import { useCommerceUI } from '../../commerce/ui-context'
8
+
9
+ const AddWidget: React.FC<{
10
+ item: LineItem
11
+ disabled?: boolean
12
+ className?: string
13
+ buttonClx?: string
14
+ variant?: 'minimal' | 'primary' | 'outline'
15
+ }> = (props) => {
16
+ const ui = useCommerceUI()
17
+ return <AddToCartWidget {...props} onQuantityChanged={ui.itemQuantityChanged}/>
18
+ }
19
+
20
+ export default AddWidget
@@ -0,0 +1,34 @@
1
+ 'use client'
2
+ import React, {type PropsWithChildren} from 'react'
3
+
4
+ import { Button, buttonVariants } from '@hanzo/ui/primitives'
5
+ import { type VariantProps } from '@hanzo/ui/util'
6
+
7
+ import { cn } from '@hanzo/ui/util'
8
+ import { useCommerceUI } from '../../commerce/ui-context'
9
+
10
+ const BuyButton: React.FC<
11
+ PropsWithChildren &
12
+ VariantProps<typeof buttonVariants> &
13
+ {
14
+ skuPath: string
15
+ className?: string
16
+ }
17
+ > = ({
18
+ skuPath,
19
+ children,
20
+ className='',
21
+ ...rest
22
+ }) => {
23
+
24
+ const ui = useCommerceUI()
25
+ const handleClick = () => { ui.showBuyOptions(skuPath) }
26
+
27
+ return (
28
+ <Button onClick={handleClick} {...rest} className={cn(className, '')}>
29
+ {children}
30
+ </Button>
31
+ )
32
+ }
33
+
34
+ export default BuyButton
@@ -3,29 +3,70 @@ import React, {type PropsWithChildren } from 'react'
3
3
 
4
4
  import { X as LucideX} from 'lucide-react'
5
5
 
6
- import { Button, Drawer, DrawerContent, type DrawerProps } from '@hanzo/ui/primitives'
6
+ import {
7
+ Button,
8
+ Drawer,
9
+ DrawerContent,
10
+ DrawerHandle,
11
+ type DrawerProps,
12
+ useDrawerContext
13
+ } from '@hanzo/ui/primitives'
7
14
  import { cn } from '@hanzo/ui/util'
8
15
 
16
+ import '../../../style/drawer-handle-overrides.css'
17
+
9
18
  const CommerceDrawer: React.FC<PropsWithChildren &
10
19
  Omit<DrawerProps, 'onOpenChange'> &
11
20
  {
12
21
  setOpen: (b: boolean) => void
22
+ handleHandleClicked: () => void
13
23
  drawerClx?: string
24
+ setActiveSPIndexSetter?: (fn: (snapPoint: number | string | null) => void) => void
14
25
  }
15
26
  > = ({
16
27
  children,
17
28
  open,
18
29
  setOpen,
19
30
  modal,
31
+ snapPoints,
32
+ setActiveSnapPoint,
33
+ activeSnapPoint,
34
+ handleHandleClicked,
35
+ setActiveSPIndexSetter,
20
36
  drawerClx='',
21
37
  ...rest
22
- }) => (
38
+ }) => {
39
+
40
+
41
+ return (
23
42
  // @ts-ignore
24
- <Drawer open={open} onOpenChange={setOpen} modal={modal} {...rest}>
25
- <DrawerContent modal={modal} className={cn(
43
+ <Drawer
44
+ open={open}
45
+ onOpenChange={setOpen}
46
+ modal={modal}
47
+ snapPoints={snapPoints}
48
+ setActiveSnapPoint={setActiveSnapPoint}
49
+ activeSnapPoint={activeSnapPoint}
50
+ fastDragSkipsToEnd={false}
51
+ handleOnly={true}
52
+ setActiveSPIndexSetter={setActiveSPIndexSetter}
53
+
54
+
55
+ {...rest}
56
+ >
57
+ <DrawerContent defaultHandle={false} className={cn(
26
58
  'rounded-t-xl mt-6 pt-6',
27
59
  drawerClx
28
60
  )}>
61
+
62
+ <DrawerHandle
63
+ className={
64
+ 'absolute left-0 right-0 mx-auto top-2 ' +
65
+ 'w-[100px] h-3 rounded-full bg-level-3 hover:bg-level-2 shrink-0'
66
+ }
67
+ handleClick={handleHandleClicked}
68
+ />
69
+
29
70
  {children}
30
71
  <Button
31
72
  variant='ghost'
@@ -38,6 +79,7 @@ const CommerceDrawer: React.FC<PropsWithChildren &
38
79
  </DrawerContent>
39
80
  </Drawer>
40
81
  )
82
+ }
41
83
 
42
84
 
43
85
  export default CommerceDrawer
@@ -1,44 +1,242 @@
1
1
  'use client'
2
- import React from 'react'
3
- import { useRouter } from 'next/navigation'
2
+ import React, { useEffect, useRef, useState } from 'react'
3
+ import { usePathname, useRouter } from 'next/navigation'
4
+ import { action, computed, makeObservable, observable, reaction, type IReactionDisposer } from 'mobx'
4
5
  import { observer } from 'mobx-react-lite'
5
6
 
6
- import { useCommerceUI, CarouselBuyCard } from '@hanzo/commerce'
7
+ import { CarouselBuyCard, useCommerce } from '@hanzo/commerce'
8
+
9
+ import { useCommerceUI } from '../../../commerce/ui-context'
7
10
 
8
11
  import CommerceDrawer from './drawer'
9
12
  import CheckoutButton from '../checkout-button'
10
13
 
14
+ const BUY = '700px'
15
+ const MICRO = '120px'
16
+ const BOTH = [MICRO, BUY]
17
+ const BUY_ONLY = [BUY]
18
+ const MICRO_ONLY = [MICRO]
19
+
20
+ type DrawerMode = 'checkout' | 'added' | 'buy' | 'buy-added' | 'buy-checkout' | 'none' | 'closed' // manually
21
+ type DrawerState = 'micro' | 'buy' | 'closed'
22
+
23
+ const MODE_TO_STATE = {
24
+ checkout: 'micro',
25
+ added: 'micro',
26
+ buy: 'buy',
27
+ 'buy-checkout': 'buy',
28
+ 'buy-added': 'buy',
29
+ none: 'closed',
30
+ closed: 'closed'
31
+ } satisfies Record<DrawerMode, DrawerState>
32
+
33
+ const MODE_TO_POINTS = {
34
+ checkout: MICRO_ONLY,
35
+ added: BOTH,
36
+ buy: BUY_ONLY,
37
+ 'buy-checkout': BOTH,
38
+ 'buy-added': BOTH,
39
+ none: BOTH,
40
+ closed: BOTH
41
+ }
42
+
43
+
44
+ class ObsDrawerState {
45
+
46
+ _mode: DrawerMode = 'none'
47
+
48
+ constructor() {
49
+ makeObservable(this, {
50
+ _mode: observable,
51
+ setMode: action,
52
+ mode: computed,
53
+ state: computed,
54
+ points: computed,
55
+ modal: computed,
56
+ activePoint: computed
57
+ })
58
+ }
59
+
60
+ get mode(): DrawerMode {return this._mode}
61
+ get state(): DrawerState { return MODE_TO_STATE[this._mode] }
62
+ get points(): (number | string)[] { return MODE_TO_POINTS[this._mode] }
63
+ get modal(): boolean { return this.state !== 'micro' }
64
+ get activePoint(): number | string | null {
65
+ if (this.state === 'buy') return BUY
66
+ if (this.state === 'micro') return MICRO
67
+ return null
68
+ }
69
+
70
+ setMode = (m: DrawerMode) => {this._mode = m}
71
+ }
72
+
11
73
  const CommerceUIComponent: React.FC = observer(() => {
12
74
 
75
+ const cmmc = useCommerce()
13
76
  const ui = useCommerceUI()
14
77
  const router = useRouter()
78
+ const isCheckout = usePathname() === '/checkout'
79
+
80
+ const stateRef = useRef<ObsDrawerState>(new ObsDrawerState())
81
+ const reactionDisposers = useRef<IReactionDisposer[]>([])
82
+
83
+ const [activeSnapPoint, setActiveSnapPoint] = useState<string | number | null>(null)
84
+ const setterRef = useRef<((index: number ) => void) | undefined>(undefined)
85
+
86
+ useEffect(() => {
87
+
88
+ reactionDisposers.current.push(reaction(
89
+
90
+ () => ({
91
+ buy: !!ui.buyOptionsSkuPath,
92
+ added: !isCheckout && ui.item,
93
+ checkout: !isCheckout && !cmmc.cartEmpty,
94
+ closed: ui.closed
95
+ }),
96
+ ({buy, added, checkout, closed}) => {
97
+ let mode: DrawerMode = 'none' // TODO: 'closed'
98
+ if (buy) {
99
+ if (added) {
100
+ mode = 'buy-added'
101
+ }
102
+ else if (checkout) {
103
+ mode = 'buy-checkout'
104
+ }
105
+ else {
106
+ mode = 'buy'
107
+ }
108
+ }
109
+ else {
110
+ if (closed) {
111
+ mode = 'closed'
112
+ }
113
+ else if (added) {
114
+ mode = 'added'
115
+ }
116
+ else if (checkout) {
117
+ mode = 'checkout'
118
+ }
119
+ }
120
+ stateRef.current.setMode(mode)
121
+ },
122
+ {equals: (val, prev) => (
123
+ val.buy === prev.buy
124
+ &&
125
+ val.added === prev.added
126
+ &&
127
+ val.checkout === prev.checkout
128
+ &&
129
+ val.closed === prev.closed
130
+ )}
131
+ )),
132
+ reactionDisposers.current.push(reaction(
133
+ () => ( stateRef.current.state ),
134
+ (s) => {
135
+ if (s === 'buy') {
136
+ //setterRef.current?.(stateRef.current.points.length - 1)
137
+ setActiveSnapPoint(BUY)
138
+ }
139
+ else if (s === 'micro') {
140
+ //setterRef.current?.(0)
141
+ setActiveSnapPoint(MICRO)
142
+ }
143
+ }
144
+ ))
145
+ return () => {
146
+ reactionDisposers.current?.forEach((d) => {d()})
147
+ }
148
+ }, [isCheckout])
149
+
150
+ const _setActiveSnapPoint = (pt: string | number | null): void => {
151
+ console.log("ON CHANGE: ", pt)
152
+ setActiveSnapPoint(pt)
153
+ }
15
154
 
16
155
  const handleCheckout = () => {
17
156
  router.push('/checkout')
18
157
  }
19
158
 
20
- // Should only ever be called internally to close
21
159
  const reallyOnlyCloseDrawer = (b: boolean) => {
22
- if (!b ) {
23
- ui.hideBuyOptions()
160
+ // Should only ever be called internally to close
161
+ // Using handleCloseGesture()
162
+ }
163
+
164
+ const handleHandleClicked = () => {
165
+ console.log("HANDLE CLICKED")
166
+
167
+ if (stateRef.current.state === 'buy') {
168
+ const toks = stateRef.current.mode.split('-')
169
+ if (toks.length <= 1) {
170
+ console.log("CLOSING 'BUY' ... ")
171
+ ui.hideBuyOptions()
172
+ }
173
+ else {
174
+ console.log("CLOSING 'BUY' to ", toks[1])
175
+ ui.hideBuyOptions()
176
+ }
177
+ }
178
+ else if (stateRef.current.state === 'micro') {
179
+ if (stateRef.current.mode === 'checkout') {
180
+ console.log(" CLOSING 'CHECKOUT' ... ")
181
+ ui.setClosed(true)
182
+ }
183
+ else if (stateRef.current.mode === 'added') {
184
+ console.log(" OPENING 'ADDED' ... ")
185
+ ui.showBuyOptions(ui.item?.sku ?? '')
186
+ }
24
187
  }
25
188
  }
26
189
 
190
+ const handleCloseGesture = () => {
191
+ if (stateRef.current.state === 'buy') {
192
+ console.log(" CLOSING 'BUY' ... ")
193
+ const toks = stateRef.current.mode.split('-')
194
+ if (toks.length <= 1) {
195
+ stateRef.current.setMode('none')
196
+ }
197
+ else {
198
+ stateRef.current.setMode(toks[1] as DrawerMode) // 'checkout' or 'added'
199
+ }
200
+ return true // "handled!"
201
+ }
202
+ console.log("DEFAULT CLOSE ACTION")
203
+ return false
204
+ }
205
+
206
+
207
+ const setActiveSPIndexSetter = (fn: (index: number ) => void): void => {
208
+ setterRef.current = fn
209
+ }
210
+
211
+
27
212
  return (
28
213
  <CommerceDrawer
29
- open={!!ui.buyOptionsSkuPath}
214
+ open={!(stateRef.current.state === 'closed')}
30
215
  setOpen={reallyOnlyCloseDrawer}
31
- drawerClx={'w-full md:max-w-[550px] md:mx-auto lg:max-w-[50vw]'}
216
+ drawerClx={'w-full h-full'}
217
+ snapPoints={stateRef.current.points}
218
+ modal={stateRef.current.modal}
219
+ activeSnapPoint={activeSnapPoint}
220
+ setActiveSnapPoint={_setActiveSnapPoint}
221
+ handleHandleClicked={handleHandleClicked}
222
+ setActiveSPIndexSetter={setActiveSPIndexSetter}
223
+ handleCloseGesture={handleCloseGesture}
32
224
  >
33
- <CarouselBuyCard
34
- skuPath={ui.buyOptionsSkuPath!}
35
- checkoutButton={
36
- <CheckoutButton handleCheckout={handleCheckout} className='w-full min-w-[160px] sm:max-w-[320px]'/>
37
- }
38
- clx='w-full'
39
- addBtnClx='w-full min-w-[160px] sm:max-w-[320px]'
40
- selectorClx='max-w-[475px]'
41
- />
225
+ {stateRef.current.state === 'buy' && (
226
+ <CarouselBuyCard
227
+ skuPath={ui.buyOptionsSkuPath!}
228
+ checkoutButton={
229
+ <CheckoutButton handleCheckout={handleCheckout} className='w-full min-w-[160px] sm:max-w-[320px]'/>
230
+ }
231
+ onQuantityChanged={ui.itemQuantityChanged}
232
+ clx='w-full'
233
+ addBtnClx='w-full min-w-[160px] sm:max-w-[320px]'
234
+ selectorClx='max-w-[475px]'
235
+ />
236
+ )}
237
+ {stateRef.current.state === 'micro' && (
238
+ <p>Mode: {stateRef.current.mode}</p>
239
+ )}
42
240
  </CommerceDrawer>
43
241
  )
44
242
  })
@@ -2,7 +2,6 @@
2
2
  import React, { useEffect, useRef } from 'react'
3
3
  import { observable, type IObservableValue, reaction } from 'mobx'
4
4
  import { observer } from 'mobx-react-lite'
5
- import { type LucideProps } from 'lucide-react'
6
5
 
7
6
  import { Button, type ButtonProps } from '@hanzo/ui/primitives'
8
7
  import { cn } from '@hanzo/ui/util'
@@ -12,11 +11,15 @@ import * as Icons from '../icons'
12
11
 
13
12
  const IconAndQuantity: React.FC<{
14
13
  animateOnQuantityChange?: boolean
14
+ showArrow?: boolean
15
+ showQuantity?: boolean
15
16
  clx?: string
16
17
  iconClx?: string
17
18
  digitClx?: string
18
19
  }> = observer(({
19
- animateOnQuantityChange=true,
20
+ animateOnQuantityChange=false,
21
+ showArrow=true,
22
+ showQuantity=true,
20
23
  clx='',
21
24
  iconClx='',
22
25
  digitClx=''
@@ -47,6 +50,7 @@ const IconAndQuantity: React.FC<{
47
50
 
48
51
  return (
49
52
  <div className={cn('flex items-center justify-center', clx)}>
53
+ {showQuantity && (
50
54
  <div className={cn(
51
55
  'relative flex items-center justify-center mr-1',
52
56
  ((wiggleRef.current.get() === 'more') ?
@@ -60,12 +64,13 @@ const IconAndQuantity: React.FC<{
60
64
  'absolute left-0 right-0 top-0 bottom-0',
61
65
  digitClx
62
66
  )}>
63
- <div style={{color: 'white' /* tailwind bug? */, fontSize: '11px', position: 'relative', top: '1px' }}>{cmmc.cartQuantity}</div>
67
+ <div style={{/* color: 'white' tailwind bug? ,*/ fontSize: '11px', position: 'relative', top: '1px' }}>{cmmc.cartQuantity}</div>
64
68
  </div>
65
69
  )}
66
70
  <Icons.bag width='19' height='24' className={cn('relative -top-[3px] opacity-70' , iconClx)} aria-hidden="true" />
67
71
  </div>
68
- <span style={{fontSize: '17px',}}>&rsaquo;</span>
72
+ )}
73
+ {showArrow && (<span style={{fontSize: '17px',}}>&rsaquo;</span>)}
69
74
  </div>
70
75
  )
71
76
  })
@@ -73,6 +78,7 @@ const IconAndQuantity: React.FC<{
73
78
  const CheckoutButton: React.FC<ButtonProps & {
74
79
  handleCheckout: () => void
75
80
  showQuantity?: boolean
81
+ showArrow?: boolean
76
82
  animateOnQuantityChange?: boolean
77
83
  centerText?: boolean
78
84
  }> = ({
@@ -81,8 +87,10 @@ const CheckoutButton: React.FC<ButtonProps & {
81
87
  rounded='lg',
82
88
  className,
83
89
  showQuantity=true,
90
+ showArrow=true,
84
91
  animateOnQuantityChange=true,
85
92
  centerText=true,
93
+ children,
86
94
  ...rest
87
95
  }) => {
88
96
 
@@ -93,22 +101,27 @@ const CheckoutButton: React.FC<ButtonProps & {
93
101
  variant={variant}
94
102
  rounded={rounded}
95
103
  className={cn(
104
+ 'flex justify-between items-stretch group',
105
+ showQuantity ? (centerText ? 'px-1.5' : 'pl-2.5 pr-1.5') : '',
96
106
  className,
97
- 'flex justify-between items-stretch',
98
- showQuantity ? (centerText ? 'px-1.5' : 'pl-2.5 pr-1.5') : ''
99
107
  )}
100
108
  >
101
- {showQuantity && centerText && (
102
- <IconAndQuantity clx='invisible' />
103
- )}
104
- <div className='flex justify-center items-center'>Checkout</div>
105
- {showQuantity && (
109
+ {centerText && ( // must scale this one too, as it effects layout
106
110
  <IconAndQuantity
107
- animateOnQuantityChange={animateOnQuantityChange}
108
- iconClx='fill-fg-foreground'
109
- digitClx='text-primary-fg leading-none font-bold font-sans'
111
+ showArrow={showArrow}
112
+ showQuantity={showQuantity}
113
+ clx='invisible group-hover:scale-105 transition-scale transition-duration-300'
110
114
  />
111
115
  )}
116
+ {children ?? (<div className='flex justify-center items-center'>Checkout</div>)}
117
+ <IconAndQuantity
118
+ clx='group-hover:scale-105 transition-scale transition-duration-300'
119
+ animateOnQuantityChange={animateOnQuantityChange}
120
+ showArrow={showArrow}
121
+ showQuantity={showQuantity}
122
+ iconClx='fill-background'
123
+ digitClx='text-foreground group-hover:opacity-80 leading-none font-bold font-sans'
124
+ />
112
125
  </Button>
113
126
  )
114
127
  }
@@ -1,18 +1,104 @@
1
1
  'use client'
2
- import React from 'react'
2
+ import React, { useRef } from 'react'
3
3
  import { createPortal } from 'react-dom'
4
4
  import { usePathname, useRouter } from 'next/navigation'
5
5
  import { observer } from 'mobx-react-lite'
6
6
 
7
7
  import { cn } from '@hanzo/ui/util'
8
+ import { useStepAnimation } from '@hanzo/ui/util-client'
9
+
8
10
  import { Image } from '@hanzo/ui/primitives'
9
11
 
10
- import { useCommerceUI } from '@hanzo/commerce'
12
+ import { useCommerceUI } from '../../../commerce/ui-context'
11
13
 
12
14
  import CheckoutButton from '../checkout-button'
13
15
  import useAnimationClxSet from './use-anim-clx-set'
14
- import useLaggingItemRef from './use-lagging-item-ref'
15
16
  import CONST from './const'
17
+ import type { LineItem } from '@hanzo/commerce/types'
18
+
19
+ const transStyle = (t: { transition: string, from : string, to: string } | undefined) : any => (
20
+ t ? {
21
+ transitionProperty: t.transition,
22
+ transitionTimingFunction: CONST.animTimingFn,
23
+ transitionDuration: `${CONST.animDurationMs}ms`
24
+ } : {}
25
+ )
26
+
27
+ const transClx = (on: boolean, t: { transition: string, from : string, to: string } | undefined) : string => (
28
+ on ? (t?.from ?? '') : (t?.to ?? '')
29
+ )
30
+
31
+ const VARS: any = {
32
+ BR: {
33
+ pos: 'bottom-[24px] right-[66px]',
34
+ width: 'w-initial',
35
+ centerText: false,
36
+ coClx: 'w-auto',
37
+ infoClx: 'w-auto',
38
+ activeItemAnim: {
39
+ co: {
40
+ transition: 'none',
41
+ from : 'px-3 gap-2.5',
42
+ to: ''
43
+ },
44
+ coText: {
45
+ transition: 'max-width',
46
+ from : 'max-w-[100px]',
47
+ to: 'max-w-[0px]'
48
+ },
49
+ info: {
50
+ transition: 'transform, opacity',
51
+ from : 'scale-x-100 opacity-100 origin-right',
52
+ to: 'scale-x-0 opacity-0 origin-right'
53
+ }
54
+ },
55
+ showArrow: true
56
+ },
57
+ TR: {
58
+ pos: 'top-[48px] md:top-[80px] right-[28px]',
59
+ width: 'w-initial',
60
+ centerText: false,
61
+ showQuantity: false,
62
+ showArrow: true,
63
+ coClx: 'w-auto px-3 gap-1',
64
+ infoClx: 'w-auto',
65
+ activeItemAnim: {
66
+ co: {
67
+ transition: 'none',
68
+ from : 'px-3 gap-2.5',
69
+ to: ''
70
+ },
71
+ coText: {
72
+ transition: 'max-width',
73
+ from : 'max-w-[100px]',
74
+ to: 'max-w-[0px]'
75
+ },
76
+ info: {
77
+ transition: 'transform',
78
+ from : 'scale-x-100 origin-right',
79
+ to: 'scale-x-0 origin-right'
80
+ }
81
+ },
82
+ },
83
+ TRIO: {
84
+ pos: 'top-[48px] md:top-[70px] right-[28px]',
85
+ centerText: false,
86
+ showQuantity: true,
87
+ showArrow: true,
88
+ width: 'w-initial',
89
+ coClx: 'hidden',
90
+ infoClx: 'w-auto',
91
+ activeItemAnim: {
92
+ info: {
93
+ transition: 'transform, opacity',
94
+ from : 'scale-x-100 opacity-100',
95
+ to: 'scale-x-50 opacity-0'
96
+ }
97
+ },
98
+ }
99
+ }
100
+
101
+ const v = 'TR'
16
102
 
17
103
  const CheckoutWidget: React.FC<{
18
104
  clx?: string
@@ -26,58 +112,78 @@ const CheckoutWidget: React.FC<{
26
112
  const clxSet = useAnimationClxSet(isCheckout)
27
113
 
28
114
  const itemRef = useCommerceUI()
29
- const laggingRef = useLaggingItemRef(itemRef, CONST.animDurationMs)
115
+
116
+ // for rendering content after itemRef.item() would return false
117
+ const persistentRef = useRef<LineItem | undefined>(undefined)
118
+
119
+ // Doing double duty of being initial step fn for StepAnimation,
120
+ // and also capturing the item for persistentRef :)
121
+ const initialStepFn = (): boolean => {
122
+ if (!!itemRef.item && !persistentRef.current) {
123
+ persistentRef.current = itemRef.item
124
+ }
125
+ return !!itemRef.item
126
+ }
127
+ const steps = useStepAnimation(initialStepFn, [CONST.animDurationMs, CONST.animDurationMs, CONST.animDurationMs])
30
128
 
31
129
  const handleCheckout = () => { router.push('/checkout')}
32
130
 
33
131
  return globalThis?.document?.body && createPortal(
34
132
  (<div
35
133
  className={cn(
36
- 'min-w-[160px] sm:max-w-[320px] w-[calc(100%-72px)] ml-2 !h-10',
37
- 'z-below-modal-2 fixed bottom-[20px] left-0 right-0',
38
- 'rounded-lg bg-background',
134
+ VARS[v].width,
135
+ 'z-below-modal-2 fixed ',
136
+ VARS[v].pos,
137
+ 'rounded-lg',
39
138
  'flex',
40
- itemRef.item ? 'gap-2' : '',
139
+ steps.notPast(0) ? 'bg-background' : '',
140
+ steps.notPast(1) ? 'gap-2' : '',
41
141
  clxSet.asArray.join(' ')
42
142
  )}
43
- style={laggingRef.item ? {} : CONST.shadowStyle}
143
+ style={steps.notPast(1) ? {} : VARS[v].coClx?.includes('hidden') ? {} : CONST.shadowStyle}
44
144
  >
45
145
  <div
46
146
  className={cn(
47
147
  'flex flex-row justify-between items-center',
48
- itemRef.item ? CONST.compWidthClx.itemInfo : 'w-0',
49
- laggingRef.item ? 'px-3 border rounded-lg border-muted-3' : ''
148
+ transClx(steps.notPast(0), VARS[v].activeItemAnim.info),
149
+ VARS[v].itemClx,
150
+ steps.notPast(1) ? 'px-3 border rounded-lg bg-level-1 border-muted-3' : ''
50
151
  )}
51
- style={{
52
- transitionProperty: 'width',
53
- transitionTimingFunction: CONST.animTimingFn,
54
- transitionDuration: `${CONST.animDurationMs}ms`
55
- }}
152
+ style={transStyle(VARS[v].activeItemAnim.info)}
56
153
  >
57
- {laggingRef.item?.img ? (
58
- <Image def={laggingRef.item.img} constrainTo={CONST.itemImgConstraint} preload className='grow-0 shrink-0'/>
59
- ) : ( // placeholder so things align
60
- <div style={{height: CONST.itemImgConstraint.h, width: CONST.itemImgConstraint.w}} className='bg-level-3 grow-0 shrink-0'/>
61
- )}
62
-
63
- <div className='text-muted grow ml-1'>
64
- {laggingRef.item && (<>
65
- <p className='whitespace-nowrap text-sm'>{laggingRef.item.title}</p>
66
- <p className='whitespace-nowrap text-xxs' >recently added...</p>
67
- </>)}
68
- </div>
154
+ {steps.notPast(1) && persistentRef.current?.img && (
155
+ <Image def={persistentRef.current.img} constrainTo={CONST.itemImgConstraint} preload className='grow-0 shrink-0'/>
156
+ )}
157
+ {steps.notPast(1) && persistentRef.current && (<div className='text-foreground grow ml-1'>
158
+ <p className='whitespace-nowrap text-ellipsis text-sm'>{persistentRef.current.title}</p>
159
+ <p className='whitespace-nowrap text-clip text-xxs' >recently added...</p>
160
+ </div>)}
69
161
  </div>
70
162
  <CheckoutButton
71
163
  handleCheckout={handleCheckout}
72
- centerText={!!!itemRef.item}
73
- variant='primary' rounded='lg'
74
- className={cn(itemRef.item ? CONST.compWidthClx.checkout : 'w-full')}
75
- style={{
76
- transitionProperty: 'width',
77
- transitionTimingFunction: CONST.animTimingFn,
78
- transitionDuration: `${CONST.animDurationMs}ms`
79
- }}
80
- />
164
+ centerText={VARS[v].centerText ?? !steps.notPast(0)}
165
+ variant='primary'
166
+ rounded='lg'
167
+ showQuantity={VARS[v].showQuantity ?? true}
168
+ showArrow={VARS[v].showArrow ?? true}
169
+ className={cn(
170
+ // for setting and unsetting 'gap'
171
+ transClx((VARS[v].activeItemAnim.coText ? steps.notPast(3) : true), VARS[v].activeItemAnim.co),
172
+ VARS[v].coClx
173
+ )}
174
+ style={transStyle(VARS[v].activeItemAnim.co)}
175
+ >
176
+ <div
177
+ className={cn(
178
+ 'overflow-hidden',
179
+ 'flex justify-center items-center',
180
+ transClx(steps.notPast(2), VARS[v].activeItemAnim.coText),
181
+ )}
182
+ style={transStyle(VARS[v].activeItemAnim.coText)}
183
+ >
184
+ Checkout
185
+ </div>
186
+ </CheckoutButton>
81
187
  </div>),
82
188
  globalThis?.document?.body
83
189
  )
@@ -1,8 +1,10 @@
1
1
  import { useEffect, useRef } from 'react'
2
2
  import { reaction, runInAction} from 'mobx'
3
3
 
4
+ import { useCommerce } from '@hanzo/commerce'
5
+
4
6
  import ObsStringSet from './obs-string-set'
5
- import { useCommerce, useCommerceUI } from '@hanzo/commerce'
7
+ import { useCommerceUI } from '../../../commerce/ui-context'
6
8
 
7
9
  export default (isCheckout: boolean): ObsStringSet => {
8
10
 
@@ -10,18 +10,12 @@ export { default as MiniChart } from './mini-chart'
10
10
  export { default as NotFound } from './not-found'
11
11
 
12
12
  export { default as AuthListener } from './auth/auth-listener'
13
+ export { default as AddWidget } from './commerce/add-widget'
13
14
  export { default as BuyDrawer } from './commerce/buy-drawer'
15
+ export { default as BuyButton } from './commerce/buy-button'
14
16
  export { default as CheckoutButton } from './commerce/checkout-button'
15
17
  export { default as CheckoutPanel } from './commerce/checkout-panel'
16
18
  export { default as CheckoutWidget } from './commerce/checkout-widget'
17
19
  export { default as LoginPanel } from './auth/login-panel'
18
20
  export { default as Scripts } from './scripts'
19
21
 
20
-
21
- /* PLEASE KEEP
22
- export {
23
- default as HeadMetadata,
24
- getTitleFromTemplateString,
25
- TwitterComponent
26
- } from './head-metadata'
27
- */
package/conf/index.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import type { SelectionUISpecifier } from '@hanzo/commerce/types'
2
2
 
3
+ export { default as commerceServiceOptions } from './lux-commerce-options'
4
+
3
5
  export const selectionUISpecifiers = {
4
6
 
5
7
  'LXM-CN': {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luxfi/core",
3
- "version": "5.0.5",
3
+ "version": "5.0.6",
4
4
  "description": "Library that contains shared UI primitives, support for a common design system, and other boilerplate support.",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/",
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "exports": {
29
29
  ".": "./components/index.ts",
30
- "./commerce": "./commerce/index.ts",
30
+ "./commerce-data": "./commerce/data/index.ts",
31
31
  "./root-layout": "./root-layout/index.tsx",
32
32
  "./server-actions": "./server-actions/index.ts",
33
33
  "./next": "./next/index.ts",
@@ -38,8 +38,8 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@hanzo/auth": "2.4.6",
41
- "@hanzo/commerce": "7.0.2",
42
- "@hanzo/ui": "3.7.0",
41
+ "@hanzo/commerce": "7.0.3",
42
+ "@hanzo/ui": "3.7.22",
43
43
  "@next/third-parties": "^14.1.0",
44
44
  "cookies-next": "^4.1.1",
45
45
  "date-fns": "^3.6.0",
@@ -50,13 +50,15 @@
50
50
  "peerDependencies": {
51
51
  "@hookform/resolvers": "^3.3.2",
52
52
  "lucide-react": "^0.344.0",
53
+ "mobx": "^6.12.0",
54
+ "mobx-react-lite": "^4.0.5",
53
55
  "next": "14.1.3",
54
56
  "next-themes": "^0.2.1",
55
- "react": "^18.2.0",
56
- "react-dom": "^18.2.0",
57
- "react-hook-form": "^7.51.3",
57
+ "react": "^18.3.1",
58
+ "react-dom": "^18.3.1",
59
+ "react-hook-form": "^7.51.4",
58
60
  "validator": "^13.11.0",
59
- "zod": "3.21.4"
61
+ "zod": "3.23.8"
60
62
  },
61
63
  "devDependencies": {
62
64
  "@mdx-js/loader": "^3.0.0",
@@ -64,8 +66,8 @@
64
66
  "@types/facebook-pixel": "^0.0.30",
65
67
  "@types/gtag.js": "^0.0.19",
66
68
  "@types/mdx": "^2.0.9",
67
- "@types/react": "^18.2.64",
68
- "@types/react-dom": "^18.2.18",
69
+ "@types/react": "^18.3.2",
70
+ "@types/react-dom": "^18.3.0",
69
71
  "tailwindcss": "^3.4.2",
70
72
  "typescript": "5.3.3"
71
73
  }
@@ -10,9 +10,10 @@ import { CommerceProvider } from '@hanzo/commerce'
10
10
  import getAppRouterBodyFontClasses from '../next/font/get-app-router-font-classes'
11
11
  import { FacebookPixelHead } from '../next/analytics/pixel-analytics'
12
12
 
13
+ import { CommerceUIProvider } from '../commerce/ui-context'
13
14
  import { AuthListener, ChatWidget, Header, Scripts } from '../components'
15
+
14
16
  import BuyDrawer from '../components/commerce/buy-drawer'
15
- import CheckoutWidget from '../components/commerce/checkout-widget'
16
17
 
17
18
  import { selectionUISpecifiers } from '../conf'
18
19
  import type SiteDef from '../types/site-def'
@@ -99,9 +100,10 @@ const RootLayout: React.FC<PropsWithChildren & {
99
100
  options={siteDef.commerce!.options}
100
101
  uiSpecs={selectionUISpecifiers}
101
102
  >
102
- <Guts />
103
- <BuyDrawer />
104
- <CheckoutWidget />
103
+ <CommerceUIProvider >
104
+ <Guts />
105
+ <BuyDrawer />
106
+ </CommerceUIProvider>
105
107
  </CommerceProvider>
106
108
  ) : (
107
109
  <Guts />
@@ -0,0 +1,154 @@
1
+ [vaul-drawer] {
2
+ touch-action: none;
3
+ transition: transform 0.5s cubic-bezier(0.32, 0.72, 0, 1);
4
+ }
5
+
6
+ [vaul-drawer][vaul-drawer-direction='bottom'] {
7
+ transform: translate3d(0, 100%, 0);
8
+ }
9
+
10
+ [vaul-drawer][vaul-drawer-direction='top'] {
11
+ transform: translate3d(0, -100%, 0);
12
+ }
13
+
14
+ [vaul-drawer][vaul-drawer-direction='left'] {
15
+ transform: translate3d(-100%, 0, 0);
16
+ }
17
+
18
+ [vaul-drawer][vaul-drawer-direction='right'] {
19
+ transform: translate3d(100%, 0, 0);
20
+ }
21
+
22
+ .vaul-dragging .vaul-scrollable [vault-drawer-direction='top'] {
23
+ overflow-y: hidden !important;
24
+ }
25
+ .vaul-dragging .vaul-scrollable [vault-drawer-direction='bottom'] {
26
+ overflow-y: hidden !important;
27
+ }
28
+
29
+ .vaul-dragging .vaul-scrollable [vault-drawer-direction='left'] {
30
+ overflow-x: hidden !important;
31
+ }
32
+
33
+ .vaul-dragging .vaul-scrollable [vault-drawer-direction='right'] {
34
+ overflow-x: hidden !important;
35
+ }
36
+
37
+ [vaul-drawer][vaul-drawer-visible='true'][vaul-drawer-direction='top'] {
38
+ transform: translate3d(0, var(--snap-point-height, 0), 0);
39
+ }
40
+
41
+ [vaul-drawer][vaul-drawer-visible='true'][vaul-drawer-direction='bottom'] {
42
+ transform: translate3d(0, var(--snap-point-height, 0), 0);
43
+ }
44
+
45
+ [vaul-drawer][vaul-drawer-visible='true'][vaul-drawer-direction='left'] {
46
+ transform: translate3d(var(--snap-point-height, 0), 0, 0);
47
+ }
48
+
49
+ [vaul-drawer][vaul-drawer-visible='true'][vaul-drawer-direction='right'] {
50
+ transform: translate3d(var(--snap-point-height, 0), 0, 0);
51
+ }
52
+
53
+ [vaul-overlay] {
54
+ opacity: 0;
55
+ transition: opacity 0.5s cubic-bezier(0.32, 0.72, 0, 1);
56
+ }
57
+
58
+ [vaul-overlay][vaul-drawer-visible='true'] {
59
+ opacity: 1;
60
+ }
61
+
62
+ [vaul-drawer]::after {
63
+ content: '';
64
+ position: absolute;
65
+ background: inherit;
66
+ background-color: inherit;
67
+ }
68
+
69
+ [vaul-drawer][vaul-drawer-direction='top']::after {
70
+ top: initial;
71
+ bottom: 100%;
72
+ left: 0;
73
+ right: 0;
74
+ height: 200%;
75
+ }
76
+
77
+ [vaul-drawer][vaul-drawer-direction='bottom']::after {
78
+ top: 100%;
79
+ bottom: initial;
80
+ left: 0;
81
+ right: 0;
82
+ height: 200%;
83
+ }
84
+
85
+ [vaul-drawer][vaul-drawer-direction='left']::after {
86
+ left: initial;
87
+ right: 100%;
88
+ top: 0;
89
+ bottom: 0;
90
+ width: 200%;
91
+ }
92
+
93
+ [vaul-drawer][vaul-drawer-direction='right']::after {
94
+ left: 100%;
95
+ right: initial;
96
+ top: 0;
97
+ bottom: 0;
98
+ width: 200%;
99
+ }
100
+
101
+ [vaul-handle] {
102
+ /* opacity: 0.8; */
103
+ touch-action: pan-y;
104
+ cursor: grab;
105
+ }
106
+
107
+ /* [vaul-handle]:hover, */
108
+ [vaul-handle]:active {
109
+ opacity: 1;
110
+ }
111
+
112
+ [vaul-handle]:active {
113
+ cursor: grabbing;
114
+ }
115
+
116
+ [vaul-handle-hitarea] {
117
+ position: absolute;
118
+ left: 50%;
119
+ top: 50%;
120
+ transform: translate(-50%, -50%);
121
+ width: max(100%, 2.75rem); /* 44px */
122
+ height: max(100%, 2.75rem); /* 44px */
123
+ touch-action: inherit;
124
+ }
125
+
126
+ [vaul-overlay][vaul-snap-points='true']:not([vaul-snap-points-overlay='true']):not([data-state='closed']) {
127
+ opacity: 0;
128
+ }
129
+
130
+ [vaul-overlay][vaul-snap-points-overlay='true']:not([vaul-drawer-visible='false']) {
131
+ opacity: 1;
132
+ }
133
+
134
+ /* This will allow us to not animate via animation, but still benefit from delaying unmount via Radix. */
135
+ @keyframes fake-animation {
136
+ from {
137
+ }
138
+ to {
139
+ }
140
+ }
141
+
142
+ @media (pointer: fine) {
143
+ [vaul-handle-hitarea] {
144
+ width: 100%;
145
+ height: 100%;
146
+ }
147
+ }
148
+
149
+ @media (hover: hover) and (pointer: fine) {
150
+ [vaul-drawer] {
151
+ user-select: none;
152
+ }
153
+ }
154
+
package/tsconfig.json CHANGED
@@ -2,9 +2,9 @@
2
2
  "extends": "../tsconfig.modules.base.json",
3
3
  "include": [
4
4
  "**/*.ts",
5
- "**/*.tsx",
5
+ "**/*.tsx"
6
6
  ],
7
7
  "exclude": [
8
- "node_modules",
9
- ],
8
+ "node_modules"
9
+ ]
10
10
  }
@@ -1,30 +0,0 @@
1
- import { useEffect, useRef } from 'react'
2
- import { reaction } from 'mobx'
3
-
4
- import type { LineItem, ObsLineItemRef } from "@hanzo/commerce/types"
5
- import { LineItemRef } from '@hanzo/commerce'
6
-
7
- export default (orig: ObsLineItemRef, lagMs: number): ObsLineItemRef => {
8
-
9
- // a ref that is synced to 'orig', but persists for lagMs longer
10
- // so ui does not jump while animating out.
11
- // (Fascilitates for start and end states in animation)
12
- const laggingRef = useRef<LineItemRef>(new LineItemRef())
13
-
14
- useEffect(() => (
15
- reaction(
16
- () => (orig.item),
17
- (item: LineItem | undefined) => {
18
- if (item) {
19
- laggingRef.current.set(item)
20
- }
21
- else {
22
- setTimeout(() => { laggingRef.current.set(undefined) }, lagMs)
23
- }
24
- },
25
- {equals: (val, prev) => (val?.sku === prev?.sku)}
26
- )
27
- ), [])
28
-
29
- return laggingRef.current
30
- }