@graphcommerce/next-ui 4.28.0 → 4.29.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.
@@ -12,7 +12,7 @@ function isButtonProps(props: ButtonProps<'div'> | BoxProps<'div'>): props is Bu
12
12
  return props.onClick !== undefined
13
13
  }
14
14
 
15
- const RenderComponent = (props: ButtonProps<'div'> | BoxProps<'div'>) =>
15
+ const ButtonOrBox = (props: ButtonProps<'div'> | BoxProps<'div'>) =>
16
16
  isButtonProps(props) ? <ButtonBase component='div' {...props} /> : <Box {...props} />
17
17
 
18
18
  export type ActionCardProps = {
@@ -101,26 +101,26 @@ export function ActionCard(props: ActionCardProps) {
101
101
  })
102
102
 
103
103
  return (
104
- <RenderComponent
104
+ <ButtonOrBox
105
105
  className={classes.root}
106
106
  onClick={onClick && ((event) => onClick?.(event, value))}
107
107
  disabled={disabled}
108
108
  sx={[
109
109
  (theme) => ({
110
110
  '&.sizeSmall': {
111
- padding: `5px 10px`,
111
+ py: `5px`,
112
112
  display: 'flex',
113
113
  typography: 'body2',
114
114
  },
115
115
 
116
116
  '&.sizeMedium': {
117
- padding: `10px 12px`,
117
+ py: `10px`,
118
118
  typography: 'body2',
119
119
  display: 'block',
120
120
  },
121
121
 
122
122
  '&.sizeLarge': {
123
- padding: `${theme.spacings.xxs} ${theme.spacings.xs}`,
123
+ py: theme.spacings.xxs,
124
124
  display: 'block',
125
125
  },
126
126
 
@@ -184,6 +184,10 @@ export function ActionCard(props: ActionCardProps) {
184
184
  ),
185
185
  },
186
186
 
187
+ '&.sizeSmall': { px: `10px` },
188
+ '&.sizeMedium': { px: `12px` },
189
+ '&.sizeLarge': { px: theme.spacings.xs },
190
+
187
191
  '&.selected': {
188
192
  border: `2px solid ${theme.palette[color].main}`,
189
193
  boxShadow: `0 0 0 4px ${alpha(
@@ -223,6 +227,7 @@ export function ActionCard(props: ActionCardProps) {
223
227
  flexDirection: 'row',
224
228
  width: '100%',
225
229
  justifyContent: 'space-between',
230
+ alignContent: 'stretch',
226
231
  }}
227
232
  >
228
233
  <Box
@@ -230,6 +235,7 @@ export function ActionCard(props: ActionCardProps) {
230
235
  display: 'flex',
231
236
  flexDirection: 'row',
232
237
  justifyContent: 'space-between',
238
+ alignContent: 'stretch',
233
239
  }}
234
240
  >
235
241
  {image && (
@@ -302,6 +308,6 @@ export function ActionCard(props: ActionCardProps) {
302
308
  </Box>
303
309
  </Box>
304
310
  {after && <Box className={classes.after}>{after}</Box>}
305
- </RenderComponent>
311
+ </ButtonOrBox>
306
312
  )
307
313
  }
@@ -0,0 +1,57 @@
1
+ import { Box, BoxProps } from '@mui/material'
2
+ import React from 'react'
3
+ import { extendableComponent } from '../Styles'
4
+ import { ActionCardProps } from './ActionCard'
5
+
6
+ type ActionCardLayoutProps = {
7
+ children?: React.ReactNode
8
+ } & Pick<ActionCardProps, 'layout'> &
9
+ BoxProps
10
+
11
+ const parts = ['root'] as const
12
+ const name = 'ActionCardLayout'
13
+ const { withState } = extendableComponent<
14
+ Pick<ActionCardProps, 'layout'>,
15
+ typeof name,
16
+ typeof parts
17
+ >(name, parts)
18
+
19
+ export const ActionCardLayout = React.forwardRef<HTMLDivElement, ActionCardLayoutProps>(
20
+ (props, ref) => {
21
+ const { layout = 'list' } = props
22
+
23
+ const classes = withState({ layout })
24
+
25
+ return (
26
+ <Box
27
+ ref={ref}
28
+ {...props}
29
+ className={classes.root}
30
+ sx={[
31
+ (theme) => ({
32
+ '&.layoutStack': {
33
+ display: 'grid',
34
+ height: 'min-content',
35
+ gap: theme.spacings.xxs,
36
+ },
37
+ '&.layoutList': {
38
+ display: 'grid',
39
+ height: 'min-content',
40
+ },
41
+ '&.layoutGrid': {
42
+ display: 'grid',
43
+ gridTemplateColumns: 'repeat(2, 1fr)',
44
+ gap: theme.spacings.xxs,
45
+ },
46
+ '&.layoutInline': {
47
+ display: 'flex',
48
+ flexWrap: 'wrap',
49
+ gap: theme.spacings.xxs,
50
+ },
51
+ }),
52
+ ...(Array.isArray(props.sx) ? props.sx : [props.sx]),
53
+ ]}
54
+ />
55
+ )
56
+ },
57
+ )
@@ -3,6 +3,7 @@ import React from 'react'
3
3
  import { isFragment } from 'react-is'
4
4
  import { extendableComponent } from '../Styles'
5
5
  import { ActionCardProps } from './ActionCard'
6
+ import { ActionCardLayout } from './ActionCardLayout'
6
7
 
7
8
  type MultiSelect = {
8
9
  multiple: true
@@ -132,52 +133,25 @@ export const ActionCardList = React.forwardRef<HTMLDivElement, ActionCardListPro
132
133
 
133
134
  return (
134
135
  <div>
135
- <Box
136
- className={classes.root}
137
- ref={ref}
138
- sx={[
139
- (theme) => ({
140
- '&.layoutStack': {
141
- display: 'grid',
142
- height: 'min-content',
143
- gap: theme.spacings.xxs,
144
- },
145
- '&.layoutList': {
146
- display: 'grid',
147
- height: 'min-content',
148
- },
149
- '&.layoutGrid': {
150
- display: 'grid',
151
- gridTemplateColumns: 'repeat(2, 1fr)',
152
- gap: theme.spacings.xxs,
153
- },
154
- '&.layoutInline': {
155
- display: 'flex',
156
- flexWrap: 'wrap',
157
- gap: theme.spacings.xxs,
158
- },
159
- }),
160
-
161
- ...(Array.isArray(sx) ? sx : [sx]),
162
- ]}
163
- >
136
+ <ActionCardLayout sx={sx} className={classes.root} layout={layout}>
164
137
  {childReactNodes.map((child) => {
165
138
  if (collapse && Boolean(value) && !isValueSelected(child.props.value, value))
166
139
  return null
167
140
  return React.cloneElement(child, {
168
141
  onClick: handleChange,
169
- error: child.props.error ?? error,
170
- color: child.props.color ?? color,
171
- variant: child.props.variant ?? variant,
172
- size: child.props.size ?? size,
173
- layout: child.props.layout ?? layout,
142
+ error,
143
+ color,
144
+ variant,
145
+ size,
146
+ layout,
147
+ ...child.props,
174
148
  selected:
175
149
  child.props.selected === undefined
176
150
  ? isValueSelected(child.props.value, value)
177
151
  : child.props.selected,
178
152
  })
179
153
  })}
180
- </Box>
154
+ </ActionCardLayout>
181
155
  {error && errorMessage && (
182
156
  <Alert
183
157
  severity='error'
@@ -0,0 +1,4 @@
1
+ export * from './ActionCard'
2
+ export * from './ActionCardLayout'
3
+ export * from './ActionCardList'
4
+ export * from './ActionCardListForm'
package/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Change Log
2
2
 
3
+ ## 4.29.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#1679](https://github.com/graphcommerce-org/graphcommerce/pull/1679) [`0bd9ea582`](https://github.com/graphcommerce-org/graphcommerce/commit/0bd9ea58230dde79c5fe2cdb07e9860151460270) Thanks [@paales](https://github.com/paales)! - Added a new Fab component which adds a loading state
8
+
9
+ ### Patch Changes
10
+
11
+ - [#1679](https://github.com/graphcommerce-org/graphcommerce/pull/1679) [`e76df6dc3`](https://github.com/graphcommerce-org/graphcommerce/commit/e76df6dc37c11c793a5d008ba36932d17dc23855) Thanks [@paales](https://github.com/paales)! - Added AddProductsToCartFab for a smaller add to cart button
12
+
13
+ - Updated dependencies []:
14
+ - @graphcommerce/framer-scroller@2.1.42
15
+
16
+ ## 4.28.1
17
+
18
+ ### Patch Changes
19
+
20
+ - [#1675](https://github.com/graphcommerce-org/graphcommerce/pull/1675) [`9e630670f`](https://github.com/graphcommerce-org/graphcommerce/commit/9e630670ff6c952ab7b938d890b5509804985cf3) Thanks [@paales](https://github.com/paales)! - Added a new ItemScroller component to be able to make horizontal product scrollers
21
+
22
+ - [#1675](https://github.com/graphcommerce-org/graphcommerce/pull/1675) [`2e9fa5984`](https://github.com/graphcommerce-org/graphcommerce/commit/2e9fa5984a07ff14fc1b3a4f62189a26e8e3ecdd) Thanks [@paales](https://github.com/paales)! - Measure the size of children of the overlay to determine the size of children
23
+
24
+ - [#1675](https://github.com/graphcommerce-org/graphcommerce/pull/1675) [`adf13069a`](https://github.com/graphcommerce-org/graphcommerce/commit/adf13069af6460c960276b402237371c12fc6dec) Thanks [@paales](https://github.com/paales)! - Use realtime measurements for useOverlayPosition instead of computed values, to improve flickering issues
25
+
26
+ - [#1675](https://github.com/graphcommerce-org/graphcommerce/pull/1675) [`1b1504c9b`](https://github.com/graphcommerce-org/graphcommerce/commit/1b1504c9b0e51f2787bce91e1ff1940f540411d6) Thanks [@paales](https://github.com/paales)! - Added crosssel functionality
27
+
28
+ - Updated dependencies [[`81f31d1e5`](https://github.com/graphcommerce-org/graphcommerce/commit/81f31d1e54397368088a4289aaddd29facfceeef), [`a8905d263`](https://github.com/graphcommerce-org/graphcommerce/commit/a8905d263273cb9322583d5759a5fdc66eceb8e4), [`1b1504c9b`](https://github.com/graphcommerce-org/graphcommerce/commit/1b1504c9b0e51f2787bce91e1ff1940f540411d6), [`6c2e27b1b`](https://github.com/graphcommerce-org/graphcommerce/commit/6c2e27b1be4aaa888e65a2bd69eaeb467a54a023)]:
29
+ - @graphcommerce/framer-scroller@2.1.41
30
+ - @graphcommerce/framer-utils@3.2.1
31
+ - @graphcommerce/framer-next-pages@3.3.2
32
+ - @graphcommerce/image@3.1.10
33
+
3
34
  ## 4.28.0
4
35
 
5
36
  ### Minor Changes
package/Fab/Fab.tsx ADDED
@@ -0,0 +1,57 @@
1
+ import {
2
+ Box,
3
+ CircularProgress,
4
+ CircularProgressProps,
5
+ Fab as FabBase,
6
+ FabProps as FabPropsBase,
7
+ } from '@mui/material'
8
+ import { IconSvg, IconSvgProps } from '../IconSvg'
9
+ import { useFabSize } from '../Theme/MuiFab'
10
+
11
+ export type FabProps = Omit<FabPropsBase<'button'>, 'variant' | 'children'> & {
12
+ loading?: boolean
13
+ icon: IconSvgProps['src']
14
+ circularProgress?: Omit<CircularProgressProps, 'size'>
15
+ }
16
+
17
+ /** Adds loading functionality to the Fab component. */
18
+ export function Fab(props: FabProps) {
19
+ const {
20
+ size = 'responsive',
21
+ disabled,
22
+ loading,
23
+ sx = [],
24
+ icon,
25
+ circularProgress,
26
+ ...fabProps
27
+ } = props
28
+
29
+ const fabSize = useFabSize(size)
30
+
31
+ const circSx = circularProgress?.sx ?? []
32
+
33
+ return (
34
+ <FabBase
35
+ type='submit'
36
+ color='primary'
37
+ size={size}
38
+ {...fabProps}
39
+ disabled={disabled}
40
+ sx={[{ display: 'grid' }, ...(Array.isArray(sx) ? sx : [sx])]}
41
+ >
42
+ <Box sx={{ display: 'flex', placeContent: 'center', gridArea: '1/1' }}>
43
+ <IconSvg src={icon} size='medium' />
44
+ </Box>
45
+ {loading && (
46
+ <CircularProgress
47
+ size={fabSize}
48
+ {...circularProgress}
49
+ sx={[
50
+ { display: 'flex', placeContent: 'center', gridArea: '1/1' },
51
+ ...(Array.isArray(circSx) ? circSx : [circSx]),
52
+ ]}
53
+ />
54
+ )}
55
+ </FabBase>
56
+ )
57
+ }
package/Fab/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './Fab'
@@ -0,0 +1,95 @@
1
+ import {
2
+ ScrollerProvider,
3
+ Scroller,
4
+ ScrollerButton,
5
+ ScrollerButtonProps,
6
+ } from '@graphcommerce/framer-scroller'
7
+ import { Box, SxProps, Theme } from '@mui/material'
8
+ import { IconSvg } from '../IconSvg'
9
+ import { extendableComponent, responsiveVal } from '../Styles'
10
+ import { useFabSize } from '../Theme'
11
+ import { iconChevronLeft, iconChevronRight } from '../icons'
12
+
13
+ const { classes } = extendableComponent('SidebarSlider', [
14
+ 'root',
15
+ 'grid',
16
+ 'sidebar',
17
+ 'scrollerContainer',
18
+ 'scroller',
19
+ 'sliderButtons',
20
+ 'centerLeft',
21
+ 'centerRight',
22
+ ] as const)
23
+
24
+ type SliderProps = {
25
+ children: React.ReactNode
26
+ sx?: SxProps<Theme>
27
+ buttonSize?: ScrollerButtonProps['size']
28
+ }
29
+
30
+ export function ItemScroller(props: SliderProps) {
31
+ const { children, sx, buttonSize = 'responsive' } = props
32
+
33
+ const size = useFabSize(buttonSize)
34
+
35
+ return (
36
+ <Box sx={sx} className={classes.root}>
37
+ <Box sx={{ position: 'relative', minWidth: 1 }} className={classes.scrollerContainer}>
38
+ <ScrollerProvider scrollSnapAlign='start'>
39
+ <Scroller
40
+ className={classes.scroller}
41
+ hideScrollbar
42
+ sx={(theme) => ({
43
+ gridColumnGap: theme.spacings.md,
44
+ gridRowGap: theme.spacings.lg,
45
+ // paddingRight: theme.page.horizontal,
46
+ px: theme.page.horizontal,
47
+ scrollPaddingLeft: theme.page.horizontal,
48
+ scrollPaddingRight: theme.page.horizontal,
49
+ gridAutoColumns: responsiveVal(200, 300),
50
+ })}
51
+ >
52
+ {children}
53
+ </Scroller>
54
+ <Box
55
+ className={classes.centerLeft}
56
+ sx={(theme) => ({
57
+ display: 'grid',
58
+ gridAutoFlow: 'row',
59
+ gap: theme.spacings.xxs,
60
+ position: 'absolute',
61
+ left: theme.spacings.sm,
62
+ top: `calc(50% - 28px)`,
63
+ })}
64
+ >
65
+ <ScrollerButton
66
+ direction='left'
67
+ sx={{ display: { xs: 'none', md: 'flex' } }}
68
+ size='responsive'
69
+ >
70
+ <IconSvg src={iconChevronLeft} />
71
+ </ScrollerButton>
72
+ </Box>
73
+ <Box
74
+ className={classes.centerRight}
75
+ sx={(theme) => ({
76
+ display: 'grid',
77
+ gap: theme.spacings.xxs,
78
+ position: 'absolute',
79
+ right: theme.spacings.sm,
80
+ top: `calc(50% - (${size}/2))`,
81
+ })}
82
+ >
83
+ <ScrollerButton
84
+ direction='right'
85
+ sx={{ display: { xs: 'none', md: 'flex' } }}
86
+ size='responsive'
87
+ >
88
+ <IconSvg src={iconChevronRight} />
89
+ </ScrollerButton>
90
+ </Box>
91
+ </ScrollerProvider>
92
+ </Box>
93
+ </Box>
94
+ )
95
+ }
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  Scroller,
3
3
  ScrollerButton,
4
- ScrollerButtonProps as ScrollerButtonPropsType,
4
+ ScrollerButtonProps,
5
5
  ScrollerPageCounter,
6
6
  ScrollerProvider,
7
7
  } from '@graphcommerce/framer-scroller'
@@ -28,7 +28,7 @@ export type SidebarSliderProps = {
28
28
  children: ReactNode
29
29
  sidebar: ReactNode
30
30
  sx?: SxProps<Theme>
31
- buttonSize?: ScrollerButtonPropsType['size']
31
+ buttonSize?: ScrollerButtonProps['size']
32
32
  }
33
33
 
34
34
  export function SidebarSlider(props: SidebarSliderProps) {
@@ -1,2 +1,3 @@
1
1
  export * from './SidebarGallery'
2
2
  export * from './SidebarSlider'
3
+ export * from './ItemScroller'
@@ -158,6 +158,7 @@ export function LayoutHeaderContent(props: LayoutHeaderContentProps) {
158
158
  gridArea: 'center',
159
159
  justifyContent: 'start',
160
160
  overflow: 'hidden',
161
+ justifySelf: 'center',
161
162
 
162
163
  transition: `opacity 150ms`,
163
164
  opacity: 0,
@@ -25,7 +25,11 @@ export function LayoutOverlay(props: LayoutOverlayProps) {
25
25
  const [isPresent, safeToRemove] = usePresence()
26
26
 
27
27
  return (
28
- <ScrollerProvider scrollSnapTypeSm={scrollSnapTypeSm} scrollSnapTypeMd={scrollSnapTypeMd}>
28
+ <ScrollerProvider
29
+ scrollSnapTypeSm={scrollSnapTypeSm}
30
+ scrollSnapTypeMd={scrollSnapTypeMd}
31
+ _inititalSnap={false}
32
+ >
29
33
  <OverlayBase
30
34
  active={active}
31
35
  direction={direction}
@@ -116,7 +116,7 @@ export const NavigationOverlay = React.memo<NavigationOverlayProps>((props) => {
116
116
  sx={{
117
117
  zIndex: 'drawer',
118
118
  '& .LayoutOverlayBase-overlayPane': {
119
- minWidth: 'auto !important',
119
+ minWidth: itemWidthMd,
120
120
  width: 'max-content',
121
121
  overflow: 'hidden',
122
122
  display: 'grid',
@@ -99,7 +99,7 @@ export function OverlayBase(incommingProps: LayoutOverlayBaseProps) {
99
99
  )(th)
100
100
 
101
101
  const { scrollerRef, snap } = useScrollerContext()
102
- const positions = useOverlayPosition()
102
+ const positions = useOverlayPosition(variantSm, variantMd)
103
103
  const scrollTo = useScrollTo()
104
104
  const beforeRef = useRef<HTMLDivElement>(null)
105
105
 
@@ -148,7 +148,6 @@ export function OverlayBase(incommingProps: LayoutOverlayBaseProps) {
148
148
  scroller.scrollLeft = positions.open.x.get()
149
149
  scroller.scrollTop = positions.open.y.get()
150
150
  }
151
-
152
151
  if (positions.open.visible.get() === 0) {
153
152
  scroller.scrollLeft = positions.closed.x.get()
154
153
  scroller.scrollTop = positions.closed.y.get()
@@ -157,8 +156,6 @@ export function OverlayBase(incommingProps: LayoutOverlayBaseProps) {
157
156
 
158
157
  window.addEventListener('resize', resize)
159
158
  return () => window.removeEventListener('resize', resize)
160
- // We're not checking for all deps, because that will cause rerenders.
161
- // The scroller context shouldn't be changing, but at the moment it is.
162
159
  }, [positions, scrollerRef])
163
160
 
164
161
  // When the overlay is closed by navigating away, we're closing the overlay.
@@ -197,7 +194,13 @@ export function OverlayBase(incommingProps: LayoutOverlayBaseProps) {
197
194
  useDomEvent(windowRef, 'keyup', handleEscape, { passive: true })
198
195
 
199
196
  // When the overlay isn't visible anymore, we navigate back.
200
- useEffect(() => positions.open.visible.onChange((o) => o === 0 && closeOverlay()))
197
+ useEffect(
198
+ () =>
199
+ positions.open.visible.onChange(
200
+ (o) => position.get() !== OverlayPosition.OPENED && o === 0 && closeOverlay(),
201
+ ),
202
+ [closeOverlay, position, positions.open.visible],
203
+ )
201
204
 
202
205
  // Measure the offset of the overlay in the scroller.
203
206
  const offsetY = useMotionValue(0)
@@ -345,7 +348,6 @@ export function OverlayBase(incommingProps: LayoutOverlayBaseProps) {
345
348
  gridArea: 'overlay',
346
349
  scrollSnapAlign: 'start',
347
350
  scrollSnapStop: 'always',
348
-
349
351
  [theme.breakpoints.down('md')]: {
350
352
  justifyContent: justifySm,
351
353
  alignItems: justifySm,
@@ -370,6 +372,9 @@ export function OverlayBase(incommingProps: LayoutOverlayBaseProps) {
370
372
  '&.sizeMdFloating': {
371
373
  padding: `${theme.page.vertical} ${theme.page.horizontal}`,
372
374
  },
375
+ '&.sizeMdFloating.variantMdBottom': {
376
+ marginTop: `calc(${theme.page.vertical} * -1)`,
377
+ },
373
378
  },
374
379
  })}
375
380
  >
@@ -380,7 +385,7 @@ export function OverlayBase(incommingProps: LayoutOverlayBaseProps) {
380
385
  pointerEvents: 'all',
381
386
  backgroundColor: theme.palette.background.paper,
382
387
  boxShadow: theme.shadows[24],
383
- scrollSnapAlign: 'end',
388
+
384
389
  [theme.breakpoints.down('md')]: {
385
390
  minWidth: '80vw',
386
391
  '&:not(.sizeMdFull)': {
@@ -394,6 +399,7 @@ export function OverlayBase(incommingProps: LayoutOverlayBaseProps) {
394
399
  '&.variantSmBottom': {
395
400
  borderTopLeftRadius: `${theme.shape.borderRadius * 3}px`,
396
401
  borderTopRightRadius: `${theme.shape.borderRadius * 3}px`,
402
+ scrollSnapAlign: 'end',
397
403
  },
398
404
  '&.sizeSmFloating': {
399
405
  borderRadius: `${theme.shape.borderRadius * 3}px`,
@@ -409,15 +415,11 @@ export function OverlayBase(incommingProps: LayoutOverlayBaseProps) {
409
415
  },
410
416
  },
411
417
  [theme.breakpoints.up('md')]: {
412
- '&.sizeMdFull': {
413
- minWidth: 'max(600px, 50vw)',
414
- },
415
- '&:not(.sizeMdFull)': {
416
- width: 'max-content',
417
- },
418
+ minWidth: '1px',
418
419
 
419
420
  '&.sizeMdFull.variantMdBottom': {
420
421
  minHeight: `calc(${clientSizeCssVar.y} - ${mdSpacingTop})`,
422
+ scrollSnapAlign: 'end',
421
423
  },
422
424
  '&.sizeMdFull.variantMdLeft': {
423
425
  paddingBottom: '1px',
@@ -433,6 +435,10 @@ export function OverlayBase(incommingProps: LayoutOverlayBaseProps) {
433
435
  borderTopLeftRadius: `${theme.shape.borderRadius * 4}px`,
434
436
  borderTopRightRadius: `${theme.shape.borderRadius * 4}px`,
435
437
  },
438
+ '&.variantMdLeft, &.variantMdRight': {
439
+ width: 'max-content',
440
+ },
441
+
436
442
  '&.sizeMdFloating': {
437
443
  borderRadius: `${theme.shape.borderRadius * 4}px`,
438
444
  },
@@ -5,71 +5,101 @@ import {
5
5
  useIsomorphicLayoutEffect,
6
6
  } from '@graphcommerce/framer-utils'
7
7
  import { motionValue } from 'framer-motion'
8
- import { useEffect } from 'react'
8
+ import { useCallback, useEffect } from 'react'
9
+ import { useMatchMedia } from '../../hooks'
9
10
 
10
- export function useOverlayPosition() {
11
- const { getScrollSnapPositions, scrollerRef } = useScrollerContext()
11
+ const clampRound = (value: number) => Math.round(Math.max(0, Math.min(1, value)) * 100) / 100
12
12
 
13
+ export function useOverlayPosition(
14
+ variantSm: 'left' | 'bottom' | 'right',
15
+ variantMd: 'left' | 'bottom' | 'right',
16
+ ) {
17
+ const match = useMatchMedia()
18
+ const { getScrollSnapPositions, scrollerRef } = useScrollerContext()
13
19
  const state = useConstant(() => ({
14
20
  open: {
15
21
  x: motionValue(0),
16
22
  y: motionValue(0),
17
23
  visible: motionValue(0),
18
24
  },
19
- closed: { x: motionValue(0), y: motionValue(0) },
25
+ closed: {
26
+ x: motionValue(0),
27
+ y: motionValue(0),
28
+ },
20
29
  }))
21
30
 
22
31
  const scroll = useElementScroll(scrollerRef)
23
32
 
33
+ const variant = useCallback(
34
+ () => (match.up('md') ? variantMd : variantSm),
35
+ [match, variantMd, variantSm],
36
+ )
37
+
24
38
  useIsomorphicLayoutEffect(() => {
25
39
  if (!scrollerRef.current) return () => {}
26
40
 
27
41
  const measure = () => {
28
42
  const positions = getScrollSnapPositions()
29
43
 
30
- state.open.x.set(positions.x[1] ?? 0)
31
- state.closed.x.set(positions.x[0])
32
- state.open.y.set(positions.y[1] ?? 0)
33
- state.closed.y.set(positions.y[0])
44
+ if (variant() === 'left') {
45
+ state.open.x.set(0)
46
+ state.closed.x.set(positions.x[positions.x.length - 1] ?? 0)
47
+ }
48
+ if (variant() === 'right') {
49
+ state.open.x.set(positions.x[positions.x.length - 1] ?? 0)
50
+ state.closed.x.set(0)
51
+ }
52
+ if (variant() === 'bottom') {
53
+ state.open.y.set(positions.y[positions.y.length - 1] ?? 0)
54
+ state.closed.y.set(0)
55
+ }
34
56
  }
35
- const ro = new ResizeObserver(measure)
36
57
  measure()
37
58
 
59
+ const ro = new ResizeObserver(measure)
38
60
  ro.observe(scrollerRef.current)
61
+ ;[...scrollerRef.current.children].forEach((child) => ro.observe(child))
62
+
39
63
  return () => ro.disconnect()
40
- })
64
+ }, [getScrollSnapPositions, scrollerRef, state, variant])
41
65
 
42
66
  // sets a float between 0 and 1 for the visibility of the overlay
43
67
  useEffect(() => {
68
+ if (!scrollerRef.current) return () => {}
44
69
  const calc = () => {
45
- const x = scroll.x.get()
46
- const y = scroll.y.get()
47
-
48
- const yC = state.closed.y.get()
49
- const yO = state.open.y.get()
50
- const visY = yC === yO ? 1 : Math.max(0, Math.min(1, (y - yC) / (yO - yC)))
51
-
52
- const xC = state.closed.x.get()
53
- const xO = state.open.x.get()
70
+ const x = scrollerRef.current?.scrollLeft ?? scroll.x.get()
71
+ const y = scrollerRef.current?.scrollTop ?? scroll.y.get()
54
72
 
55
- const visX = xO === xC ? 1 : Math.max(0, Math.min(1, (x - xC) / (xO - xC)))
56
-
57
- let vis = visY * visX
58
- if (xC === 0 && xO === 0 && yC === 0 && yO === 0) vis = 0
73
+ const positions = getScrollSnapPositions()
59
74
 
60
- // todo: visibility sometimes flickers
61
- state.open.visible.set(vis)
75
+ if (variant() === 'left') {
76
+ const closedX = positions.x[1] ?? 0
77
+ state.open.visible.set(closedX === 0 ? 0 : clampRound((x - closedX) / -closedX))
78
+ }
79
+ if (variant() === 'right') {
80
+ const openedX = positions.x[1] ?? 0
81
+ state.open.visible.set(openedX === 0 ? 0 : clampRound(x / openedX))
82
+ }
83
+ if (variant() === 'bottom') {
84
+ const openedY = positions.y[1] ?? 0
85
+ state.open.visible.set(openedY === 0 ? 0 : clampRound(y / openedY))
86
+ }
62
87
  }
63
88
 
64
89
  const cancelY = scroll.y.onChange(calc)
65
90
  const cancelX = scroll.x.onChange(calc)
66
91
  calc()
67
92
 
93
+ const ro = new ResizeObserver(calc)
94
+ ro.observe(scrollerRef.current)
95
+ ;[...scrollerRef.current.children].forEach((child) => ro.observe(child))
96
+
68
97
  return () => {
69
98
  cancelY()
70
99
  cancelX()
100
+ ro.disconnect()
71
101
  }
72
- }, [state, scroll])
102
+ }, [getScrollSnapPositions, scroll, scrollerRef, state, variant])
73
103
 
74
104
  return state
75
105
  }
@@ -1,20 +1,23 @@
1
1
  import type { OptionalKeysOf, Simplify } from 'type-fest'
2
2
 
3
+ export type RequiredKeys<
4
+ T extends Record<string, unknown>,
5
+ Keys extends OptionalKeysOf<T>,
6
+ > = Simplify<
7
+ Omit<T, Keys> & {
8
+ [K in Keys]: NonNullable<T[K]>
9
+ }
10
+ >
11
+
3
12
  export function filterNonNullableKeys<
4
13
  T extends Record<string, unknown>,
5
14
  Keys extends OptionalKeysOf<T>,
6
15
  >(items: (T | null | undefined)[] | null | undefined, values: Keys[] = []) {
7
16
  if (!items) return []
8
17
 
9
- type ResultWithRequired = Simplify<
10
- Omit<T, Keys> & {
11
- [K in Keys]: NonNullable<T[K]>
12
- }
13
- >
14
-
15
18
  const result = items.filter(
16
19
  (item) => item !== null && typeof item !== 'undefined' && values.every((v) => item?.[v]),
17
20
  )
18
21
 
19
- return result as ResultWithRequired[]
22
+ return result as RequiredKeys<T, Keys>[]
20
23
  }
@@ -1,9 +1,9 @@
1
1
  import { Trans } from '@lingui/react'
2
2
  import { Button } from '@mui/material'
3
3
  import { MessageSnackbar } from './MessageSnackbar'
4
- import { MessageSnackbarImplProps } from './MessageSnackbarImpl'
4
+ import { MessageSnackbarProps } from './MessageSnackbarImpl'
5
5
 
6
- export type ErrorSnackbarProps = MessageSnackbarImplProps
6
+ export type ErrorSnackbarProps = MessageSnackbarProps
7
7
 
8
8
  export function ErrorSnackbar(props: ErrorSnackbarProps) {
9
9
  const { action, ...passedProps } = props
@@ -1,4 +1,4 @@
1
- import { breakpointVal } from '@graphcommerce/next-ui'
1
+ import { i18n } from '@lingui/core'
2
2
  import {
3
3
  Fab,
4
4
  Snackbar,
@@ -12,13 +12,13 @@ import {
12
12
  } from '@mui/material'
13
13
  import React, { useEffect, useState } from 'react'
14
14
  import { IconSvg } from '../IconSvg'
15
- import { extendableComponent } from '../Styles'
15
+ import { extendableComponent, breakpointVal } from '../Styles'
16
16
  import { iconClose, iconCheckmark, iconSadFace } from '../icons'
17
17
 
18
18
  type Size = 'normal' | 'wide'
19
19
  type Variant = 'contained' | 'pill'
20
20
 
21
- export type MessageSnackbarImplProps = Omit<
21
+ export type MessageSnackbarProps = Omit<
22
22
  SnackbarProps,
23
23
  'autoHideDuration' | 'anchorOrigin' | 'color'
24
24
  > & {
@@ -41,7 +41,7 @@ const parts = ['root', 'content', 'children', 'actionButton', 'close'] as const
41
41
  const { withState } = extendableComponent<OwnerState, typeof name, typeof parts>(name, parts)
42
42
 
43
43
  // eslint-disable-next-line import/no-default-export
44
- export default function MessageSnackbarImpl(props: MessageSnackbarImplProps) {
44
+ export default function MessageSnackbarImpl(props: MessageSnackbarProps) {
45
45
  const [showSnackbar, setShowSnackbar] = useState<boolean>(false)
46
46
 
47
47
  const {
@@ -114,7 +114,8 @@ export default function MessageSnackbarImpl(props: MessageSnackbarImplProps) {
114
114
  alignItems: 'center',
115
115
  gap: theme.spacings.xxs,
116
116
  gridTemplate: {
117
- xs: `"icon children close" "action action action"`,
117
+ xs: `"icon children close"
118
+ "action action action"`,
118
119
  md: '"icon children action close"',
119
120
  },
120
121
  gridTemplateColumns: {
@@ -139,7 +140,7 @@ export default function MessageSnackbarImpl(props: MessageSnackbarImplProps) {
139
140
  )}
140
141
  <Fab
141
142
  className={classes.close}
142
- aria-label='Close'
143
+ aria-label={i18n._(/* i18n */ 'Close')}
143
144
  size='small'
144
145
  onClick={hideSnackbar}
145
146
  onMouseDown={preventAnimationBubble}
package/icons/index.ts CHANGED
@@ -1,42 +1,41 @@
1
- export { default as iconSearch } from './search.svg'
2
- export { default as iconPerson } from './person-alt.svg'
1
+ export { default as iconArrowBack } from './arrow-left.svg'
2
+ export { default as iconArrowForward } from './arrow-right.svg'
3
+ export { default as iconShoppingBag } from './bag.svg'
4
+ export { default as iconInvoice } from './box-alt.svg'
5
+ export { default as iconBox } from './box.svg'
6
+ export { default as iconOrderBefore } from './calendar.svg'
7
+ export { default as iconCancelAlt } from './cancel-alt.svg'
8
+ export { default as iconCartAdd } from './cart-add.svg'
9
+ export { default as iconCart } from './cart.svg'
10
+ export { default as iconChat } from './chat-alt.svg'
11
+ export { default as iconCustomerService } from './chat.svg'
3
12
  export { default as iconChevronDown } from './chevron-down.svg'
4
- export { default as iconChevronLeft } from './chevron-left.svg'
13
+ export { default as iconChevronBack, default as iconChevronLeft } from './chevron-left.svg'
5
14
  export { default as iconChevronRight } from './chevron-right.svg'
6
15
  export { default as iconChevronUp } from './chevron-up.svg'
7
- export { default as iconAddresses } from './home-alt.svg'
16
+ export { default as iconClose } from './close.svg'
17
+ export { default as iconId } from './credit-card.svg'
18
+ export { default as iconEllypsis } from './ellypsis.svg'
19
+ export { default as iconEmail, default as iconEmailOutline } from './envelope-alt.svg'
20
+ export { default as icon404 } from './explore.svg'
8
21
  export { default as iconHeart } from './favourite.svg'
22
+ export { default as iconMenu } from './hamburger.svg'
23
+ export { default as iconParty } from './happy-face.svg'
24
+ export { default as iconAddresses, default as iconHome } from './home-alt.svg'
9
25
  export { default as iconLocation } from './location.svg'
10
- export { default as iconInvoice } from './box-alt.svg'
11
- export { default as iconCustomerService } from './chat.svg'
12
- export { default as iconShoppingBag } from './bag.svg'
26
+ export { default as iconLock } from './lock.svg'
27
+ export { default as iconFullscreen } from './maximise.svg'
13
28
  export { default as iconFullscreenExit } from './minimise.svg'
14
- export { default as iconChat } from './chat-alt.svg'
15
- export { default as iconChevronBack } from './chevron-left.svg'
16
- export { default as iconCancelAlt } from './cancel-alt.svg'
17
- export { default as iconEmail } from './envelope-alt.svg'
18
- export { default as iconCheckmark } from './ok.svg'
19
- export { default as iconArrowBack } from './arrow-left.svg'
20
- export { default as iconArrowForward } from './arrow-right.svg'
21
- export { default as iconMenu } from './hamburger.svg'
22
29
  export { default as iconMin } from './minus.svg'
23
- export { default as iconPhone } from './smartphone.svg'
24
- export { default as iconPlus } from './plus.svg'
25
- export { default as iconClose } from './close.svg'
26
- export { default as iconFullscreen } from './maximise.svg'
27
- export { default as iconOrderBefore } from './calendar.svg'
28
- export { default as iconBox } from './box.svg'
29
- export { default as iconHome } from './home-alt.svg'
30
- export { default as iconId } from './credit-card.svg'
31
- export { default as iconLock } from './lock.svg'
30
+ export { default as iconMoon } from './moon.svg'
32
31
  export { default as iconNewspaper } from './news.svg'
33
- export { default as iconSadFace } from './sad-face.svg'
32
+ export { default as iconCheckmark } from './ok.svg'
33
+ export { default as iconPerson } from './person-alt.svg'
34
+ export { default as iconPlay } from './play.svg'
35
+ export { default as iconPlus } from './plus.svg'
34
36
  export { default as iconShutdown } from './power.svg'
35
- export { default as iconParty } from './happy-face.svg'
37
+ export { default as iconSadFace } from './sad-face.svg'
38
+ export { default as iconSearch } from './search.svg'
39
+ export { default as iconPhone } from './smartphone.svg'
36
40
  export { default as iconStar } from './star.svg'
37
- export { default as iconEmailOutline } from './envelope-alt.svg'
38
- export { default as icon404 } from './explore.svg'
39
41
  export { default as iconSun } from './sun.svg'
40
- export { default as iconMoon } from './moon.svg'
41
- export { default as iconPlay } from './play.svg'
42
- export { default as iconEllypsis } from './ellypsis.svg'
package/index.ts CHANGED
@@ -1,6 +1,4 @@
1
- export * from './ActionCard/ActionCard'
2
- export * from './ActionCard/ActionCardList'
3
- export * from './ActionCard/ActionCardListForm'
1
+ export * from './ActionCard'
4
2
  export * from './AnimatedRow/AnimatedRow'
5
3
  export * from './Blog/BlogAuthor/BlogAuthor'
6
4
  export * from './Blog/BlogContent/BlogContent'
@@ -12,6 +10,7 @@ export * from './Blog/BlogTitle/BlogTitle'
12
10
  export * from './Button'
13
11
  export * from './ChipMenu/ChipMenu'
14
12
  export * from './ContainerWithHeader/ContainerWithHeader'
13
+ export * from './Fab'
15
14
  export * from './FlagAvatar/FlagAvatar'
16
15
  export * from './Footer'
17
16
  export * from './Form/Form'
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@graphcommerce/next-ui",
3
3
  "homepage": "https://www.graphcommerce.org/",
4
4
  "repository": "github:graphcommerce-org/graphcommerce",
5
- "version": "4.28.0",
5
+ "version": "4.29.0",
6
6
  "author": "",
7
7
  "license": "MIT",
8
8
  "sideEffects": false,
@@ -19,10 +19,10 @@
19
19
  "@emotion/react": "^11.9.3",
20
20
  "@emotion/server": "^11.4.0",
21
21
  "@emotion/styled": "^11.9.3",
22
- "@graphcommerce/framer-next-pages": "3.3.1",
23
- "@graphcommerce/framer-scroller": "2.1.40",
24
- "@graphcommerce/framer-utils": "3.2.0",
25
- "@graphcommerce/image": "3.1.9",
22
+ "@graphcommerce/framer-next-pages": "3.3.2",
23
+ "@graphcommerce/framer-scroller": "2.1.42",
24
+ "@graphcommerce/framer-utils": "3.2.1",
25
+ "@graphcommerce/image": "3.1.10",
26
26
  "cookie": "^0.5.0",
27
27
  "react-is": "^18.2.0",
28
28
  "schema-dts": "^1.1.0"