@graphcommerce/next-ui 9.1.0-canary.54 → 9.1.0-canary.55
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/CHANGELOG.md +16 -0
- package/FramerScroller/SidebarGallery.tsx +3 -2
- package/Intl/DateTimeFormat/DateTimeFormat.tsx +3 -2
- package/Layout/components/LayoutHeaderClose.tsx +9 -6
- package/LayoutOverlay/components/LayoutOverlayHeader2.tsx +213 -0
- package/LayoutOverlay/index.ts +1 -0
- package/Overlay/components/OverlayCloseButton.tsx +25 -0
- package/Overlay/components/OverlayHeader2.tsx +26 -0
- package/Overlay/components/index.ts +4 -2
- package/Tabs/TabItem.tsx +134 -0
- package/Tabs/Tabs.tsx +124 -0
- package/index.ts +2 -0
- package/package.json +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## 9.1.0-canary.55
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#2539](https://github.com/graphcommerce-org/graphcommerce/pull/2539) [`87fc3c2`](https://github.com/graphcommerce-org/graphcommerce/commit/87fc3c28165c7c66b48882b0f044bbc9b63b9846) - Created new Tabs and TabItem component to be used for MultiCart setup ([@paales](https://github.com/paales))
|
|
8
|
+
|
|
9
|
+
- [#2539](https://github.com/graphcommerce-org/graphcommerce/pull/2539) [`286a20e`](https://github.com/graphcommerce-org/graphcommerce/commit/286a20e01f2a565f058415fa1c8dfbb2eeb3163b) - Added an OverlayCloseButton and implemented it for various locations. ([@paales](https://github.com/paales))
|
|
10
|
+
|
|
11
|
+
- [#2539](https://github.com/graphcommerce-org/graphcommerce/pull/2539) [`65dcefb`](https://github.com/graphcommerce-org/graphcommerce/commit/65dcefb8740166fd5df662e0e895c65d70273393) - Solve hydration error because multiple literals could be in a DateTimeFormat ([@paales](https://github.com/paales))
|
|
12
|
+
|
|
13
|
+
- [#2539](https://github.com/graphcommerce-org/graphcommerce/pull/2539) [`88fd114`](https://github.com/graphcommerce-org/graphcommerce/commit/88fd11485f9368e79d277fa45942e58214f794a6) - Created a LayoutOverlayHeader2 that does not support any floating modes or something and thus is simpler to customize. ([@paales](https://github.com/paales))
|
|
14
|
+
|
|
15
|
+
- [#2539](https://github.com/graphcommerce-org/graphcommerce/pull/2539) [`23793aa`](https://github.com/graphcommerce-org/graphcommerce/commit/23793aab26455b1bea0d1b3b37c96a228b656bc4) - Prevent excessive rerender when multiple images with the same url are in a product ([@paales](https://github.com/paales))
|
|
16
|
+
|
|
17
|
+
- [#2539](https://github.com/graphcommerce-org/graphcommerce/pull/2539) [`a419257`](https://github.com/graphcommerce-org/graphcommerce/commit/a4192571eb2332630ba3d103f61ff69dac8b2e5c) - Solve issue where the sidebar wasn't 100% width on the PDP on mobile ([@paales](https://github.com/paales))
|
|
18
|
+
|
|
3
19
|
## 9.1.0-canary.54
|
|
4
20
|
|
|
5
21
|
## 9.1.0-canary.53
|
|
@@ -242,7 +242,8 @@ export function SidebarGallery(props: SidebarGalleryProps) {
|
|
|
242
242
|
>
|
|
243
243
|
{images.map((image, idx) => (
|
|
244
244
|
<MotionImageAspect
|
|
245
|
-
|
|
245
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
246
|
+
key={idx}
|
|
246
247
|
layout
|
|
247
248
|
layoutDependency={zoomed}
|
|
248
249
|
src={image.src}
|
|
@@ -362,7 +363,7 @@ export function SidebarGallery(props: SidebarGalleryProps) {
|
|
|
362
363
|
className={classes.sidebarWrapper}
|
|
363
364
|
sx={[
|
|
364
365
|
{
|
|
365
|
-
width: sidebarSize,
|
|
366
|
+
width: { xs: '100%', md: sidebarSize },
|
|
366
367
|
gridArea: 'right',
|
|
367
368
|
boxSizing: 'content-box',
|
|
368
369
|
display: 'grid',
|
|
@@ -19,8 +19,9 @@ export function DateTimeFormat(props: DateTimeFormatProps) {
|
|
|
19
19
|
return (
|
|
20
20
|
<Box component='span' className='DateTimeFormat' suppressHydrationWarning sx={sx}>
|
|
21
21
|
{dateValue &&
|
|
22
|
-
formatter.formatToParts(dateValue).map((part) => (
|
|
23
|
-
|
|
22
|
+
formatter.formatToParts(dateValue).map((part, index) => (
|
|
23
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
24
|
+
<span className={part.type} key={`${part.type}-${index}`} suppressHydrationWarning>
|
|
24
25
|
{part.value}
|
|
25
26
|
</span>
|
|
26
27
|
))}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { useGo, usePageContext } from '@graphcommerce/framer-next-pages'
|
|
2
2
|
import { i18n } from '@lingui/core'
|
|
3
|
-
import { Fab } from '@mui/material'
|
|
3
|
+
import { Fab, type ButtonProps } from '@mui/material'
|
|
4
4
|
import { useState } from 'react'
|
|
5
|
+
import { Button } from '../../Button'
|
|
6
|
+
import type { FabProps } from '../../Fab'
|
|
5
7
|
import { iconClose } from '../../icons'
|
|
6
8
|
import { IconSvg, useIconSvgSize } from '../../IconSvg'
|
|
7
9
|
import { useFabSize } from '../../Theme'
|
|
8
10
|
|
|
9
11
|
export type LayoutHeaderCloseProps = {
|
|
10
12
|
onClose?: () => void
|
|
13
|
+
size?: FabProps['size']
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
export function useShowClose() {
|
|
@@ -16,29 +19,29 @@ export function useShowClose() {
|
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
export function LayoutHeaderClose(props: LayoutHeaderCloseProps) {
|
|
19
|
-
const { onClose } = props
|
|
22
|
+
const { onClose, size = 'responsive' } = props
|
|
20
23
|
const { closeSteps } = usePageContext()
|
|
21
24
|
const [disabled, setDisabled] = useState(false)
|
|
22
25
|
const go = useGo(closeSteps * -1)
|
|
23
26
|
const onClick = () => {
|
|
24
27
|
setDisabled(true)
|
|
25
|
-
|
|
26
28
|
return onClose ? onClose() : go()
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
const fabSize = useFabSize(
|
|
30
|
-
const svgSize = useIconSvgSize('large')
|
|
31
|
+
const fabSize = useFabSize(size)
|
|
32
|
+
const svgSize = useIconSvgSize(size === 'large' ? 'large' : 'medium')
|
|
31
33
|
|
|
32
34
|
return (
|
|
33
35
|
<Fab
|
|
34
36
|
onClick={onClick}
|
|
37
|
+
className='LayoutHeaderClose-root'
|
|
35
38
|
sx={{
|
|
36
39
|
boxShadow: 'none',
|
|
37
40
|
marginLeft: `calc((${fabSize} - ${svgSize}) * -0.5)`,
|
|
38
41
|
marginRight: `calc((${fabSize} - ${svgSize}) * -0.5)`,
|
|
39
42
|
background: 'none',
|
|
40
43
|
}}
|
|
41
|
-
size=
|
|
44
|
+
size={size}
|
|
42
45
|
disabled={disabled}
|
|
43
46
|
aria-label={i18n._(/* i18n */ 'Close')}
|
|
44
47
|
>
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import type { SxProps, Theme } from '@mui/material'
|
|
2
|
+
import { Box } from '@mui/material'
|
|
3
|
+
import React, { useRef } from 'react'
|
|
4
|
+
import type { BackProps } from '../../Layout/components/LayoutHeaderBack'
|
|
5
|
+
import { LayoutHeaderBack, useShowBack } from '../../Layout/components/LayoutHeaderBack'
|
|
6
|
+
import { LayoutHeaderClose, useShowClose } from '../../Layout/components/LayoutHeaderClose'
|
|
7
|
+
import { extendableComponent } from '../../Styles'
|
|
8
|
+
import { sxx } from '../../utils/sxx'
|
|
9
|
+
|
|
10
|
+
export type LayoutOverlayHeader2Props = Pick<BackProps, 'disableBackNavigation'> & {
|
|
11
|
+
/** Main content to display in the center */
|
|
12
|
+
children: React.ReactNode
|
|
13
|
+
|
|
14
|
+
disableChildrenPadding?: boolean
|
|
15
|
+
|
|
16
|
+
/** Button to display on the left side of the title */
|
|
17
|
+
primary?: React.ReactNode
|
|
18
|
+
|
|
19
|
+
/** Button to display on the right side of the title */
|
|
20
|
+
secondary?: React.ReactNode
|
|
21
|
+
|
|
22
|
+
/** Hide the back button */
|
|
23
|
+
hideBackButton?: boolean
|
|
24
|
+
|
|
25
|
+
/** Justify alignment for center content */
|
|
26
|
+
justify?: 'start' | 'center' | 'end'
|
|
27
|
+
|
|
28
|
+
/** Custom styles for the root element */
|
|
29
|
+
sx?: SxProps<Theme>
|
|
30
|
+
|
|
31
|
+
closeLocation?: 'left' | 'right'
|
|
32
|
+
|
|
33
|
+
size?: 'small' | 'responsive'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const name = 'LayoutOverlayHeader2'
|
|
37
|
+
const parts = ['root', 'bg', 'content', 'left', 'center', 'right'] as const
|
|
38
|
+
|
|
39
|
+
type State = {
|
|
40
|
+
left: boolean
|
|
41
|
+
right: boolean
|
|
42
|
+
childrenPadding: boolean
|
|
43
|
+
size: 'small' | 'responsive'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const { withState } = extendableComponent<State, typeof name, typeof parts>(name, parts)
|
|
47
|
+
|
|
48
|
+
export const LayoutOverlayHeader2 = React.memo<LayoutOverlayHeader2Props>((props) => {
|
|
49
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
50
|
+
|
|
51
|
+
const {
|
|
52
|
+
children,
|
|
53
|
+
hideBackButton = false,
|
|
54
|
+
primary,
|
|
55
|
+
secondary,
|
|
56
|
+
justify = 'center',
|
|
57
|
+
closeLocation = 'right',
|
|
58
|
+
sx = [],
|
|
59
|
+
disableBackNavigation,
|
|
60
|
+
disableChildrenPadding = false,
|
|
61
|
+
size = 'responsive',
|
|
62
|
+
} = props
|
|
63
|
+
|
|
64
|
+
const showBack = useShowBack() && !hideBackButton
|
|
65
|
+
const showClose = useShowClose()
|
|
66
|
+
|
|
67
|
+
const close = showClose && <LayoutHeaderClose size={size === 'small' ? 'small' : 'responsive'} />
|
|
68
|
+
const back = showBack && <LayoutHeaderBack disableBackNavigation={disableBackNavigation} />
|
|
69
|
+
|
|
70
|
+
let left = secondary
|
|
71
|
+
let right = primary
|
|
72
|
+
|
|
73
|
+
if (back && !secondary) left = back
|
|
74
|
+
|
|
75
|
+
// Handle close button positioning based on closeLocation
|
|
76
|
+
if (close && closeLocation === 'left' && !left) left = close
|
|
77
|
+
if (close && closeLocation === 'right' && !right) right = close
|
|
78
|
+
if (close && closeLocation === 'right' && !left && right) left = close
|
|
79
|
+
if (!left && !right && !children) return null
|
|
80
|
+
|
|
81
|
+
const classes = withState({
|
|
82
|
+
left: !!left,
|
|
83
|
+
right: !!right,
|
|
84
|
+
childrenPadding: !disableChildrenPadding,
|
|
85
|
+
size,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<Box
|
|
90
|
+
className={classes.root}
|
|
91
|
+
sx={sxx(
|
|
92
|
+
(theme) => ({
|
|
93
|
+
zIndex: children ? theme.zIndex.appBar : theme.zIndex.appBar - 2,
|
|
94
|
+
position: 'sticky',
|
|
95
|
+
left: 0,
|
|
96
|
+
right: 0,
|
|
97
|
+
top: 0,
|
|
98
|
+
marginTop: 0,
|
|
99
|
+
|
|
100
|
+
// Special positioning for bottom overlays
|
|
101
|
+
[theme.breakpoints.down('md')]: {
|
|
102
|
+
height: theme.appShell.headerHeightSm,
|
|
103
|
+
'.variantSmBottom.sizeSmFull &, .variantSmBottom.sizeSmMinimal &': {
|
|
104
|
+
top: `calc(${theme.appShell.headerHeightSm} * 0.5 * -1)`,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
[theme.breakpoints.up('md')]: {
|
|
108
|
+
height: theme.appShell.appBarHeightMd,
|
|
109
|
+
'.variantMdBottom.sizeMdFull &, .variantMdBottom.sizeMdMinimal &': {
|
|
110
|
+
top: `calc(${theme.appShell.appBarHeightMd} * 0.5 * -1)`,
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
'&.sizeSmall': {
|
|
114
|
+
height: theme.appShell.headerHeightSm,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
sx,
|
|
119
|
+
)}
|
|
120
|
+
>
|
|
121
|
+
<Box
|
|
122
|
+
className={classes.content}
|
|
123
|
+
ref={ref}
|
|
124
|
+
sx={sxx((theme) => ({
|
|
125
|
+
position: 'absolute',
|
|
126
|
+
inset: 0,
|
|
127
|
+
width: '100%',
|
|
128
|
+
display: 'grid',
|
|
129
|
+
alignItems: 'center',
|
|
130
|
+
gap: theme.spacings.xs,
|
|
131
|
+
height: theme.appShell.headerHeightSm,
|
|
132
|
+
[theme.breakpoints.up('md')]: {
|
|
133
|
+
height: theme.appShell.appBarHeightMd,
|
|
134
|
+
'&.sizeSmall': {
|
|
135
|
+
height: theme.appShell.headerHeightSm,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
// Default: center only
|
|
140
|
+
gridTemplateAreas: '"center"',
|
|
141
|
+
gridTemplateColumns: '1fr',
|
|
142
|
+
|
|
143
|
+
// Left only
|
|
144
|
+
'&.left:not(.right)': {
|
|
145
|
+
gridTemplateAreas: '"left center"',
|
|
146
|
+
gridTemplateColumns: justify === 'center' ? '1fr max-content' : 'auto 1fr',
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
// Right only
|
|
150
|
+
'&.right:not(.left)': {
|
|
151
|
+
gridTemplateAreas: '"center right"',
|
|
152
|
+
gridTemplateColumns: justify === 'center' ? 'max-content 1fr' : '1fr auto',
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
// Both left and right
|
|
156
|
+
'&.left.right': {
|
|
157
|
+
gridTemplateAreas: '"left center right"',
|
|
158
|
+
gridTemplateColumns: justify === 'center' ? '1fr max-content 1fr' : 'auto 1fr auto',
|
|
159
|
+
},
|
|
160
|
+
}))}
|
|
161
|
+
>
|
|
162
|
+
{left && (
|
|
163
|
+
<Box
|
|
164
|
+
className={classes.left}
|
|
165
|
+
sx={(theme) => ({
|
|
166
|
+
pl: theme.spacings.sm,
|
|
167
|
+
display: 'grid',
|
|
168
|
+
gridAutoFlow: 'column',
|
|
169
|
+
gap: theme.spacings.sm,
|
|
170
|
+
gridArea: 'left',
|
|
171
|
+
justifyContent: 'start',
|
|
172
|
+
})}
|
|
173
|
+
>
|
|
174
|
+
{left}
|
|
175
|
+
</Box>
|
|
176
|
+
)}
|
|
177
|
+
|
|
178
|
+
<Box
|
|
179
|
+
className={classes.center}
|
|
180
|
+
sx={(theme) => ({
|
|
181
|
+
display: 'grid',
|
|
182
|
+
gridAutoFlow: 'column',
|
|
183
|
+
gap: theme.spacings.sm,
|
|
184
|
+
gridArea: 'center',
|
|
185
|
+
overflow: 'hidden',
|
|
186
|
+
height: '100%',
|
|
187
|
+
'&.childrenPadding:not(.right)': { pr: theme.spacings.sm },
|
|
188
|
+
'&.childrenPadding:not(.left)': { pl: theme.spacings.sm },
|
|
189
|
+
})}
|
|
190
|
+
>
|
|
191
|
+
{children}
|
|
192
|
+
</Box>
|
|
193
|
+
|
|
194
|
+
{right && (
|
|
195
|
+
<Box
|
|
196
|
+
className={classes.right}
|
|
197
|
+
sx={(theme) => ({
|
|
198
|
+
'& > *': { width: 'min-content' },
|
|
199
|
+
display: 'grid',
|
|
200
|
+
gridAutoFlow: 'column',
|
|
201
|
+
gap: theme.spacings.sm,
|
|
202
|
+
gridArea: 'right',
|
|
203
|
+
justifyContent: 'end',
|
|
204
|
+
pr: theme.spacings.sm,
|
|
205
|
+
})}
|
|
206
|
+
>
|
|
207
|
+
{right}
|
|
208
|
+
</Box>
|
|
209
|
+
)}
|
|
210
|
+
</Box>
|
|
211
|
+
</Box>
|
|
212
|
+
)
|
|
213
|
+
})
|
package/LayoutOverlay/index.ts
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useGo, usePageContext } from '@graphcommerce/framer-next-pages'
|
|
2
|
+
import { i18n } from '@lingui/core'
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { Button, type ButtonProps } from '../../Button'
|
|
5
|
+
|
|
6
|
+
export function OverlayCloseButton(props: ButtonProps) {
|
|
7
|
+
const { disabled, ...buttonProps } = props
|
|
8
|
+
const { closeSteps } = usePageContext()
|
|
9
|
+
const [isDisabled, setIsDisabled] = useState(disabled)
|
|
10
|
+
const go = useGo(closeSteps * -1)
|
|
11
|
+
|
|
12
|
+
const onClick = () => {
|
|
13
|
+
setIsDisabled(true)
|
|
14
|
+
go()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Button
|
|
19
|
+
onClick={onClick}
|
|
20
|
+
disabled={isDisabled}
|
|
21
|
+
aria-label={i18n._(/* i18n */ 'Close overlay')}
|
|
22
|
+
{...buttonProps}
|
|
23
|
+
/>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { LayoutTitle, type TitleProps } from '../../Layout'
|
|
2
|
+
import { LayoutHeaderClose } from '../../Layout/components/LayoutHeaderClose'
|
|
3
|
+
import { LayoutOverlayHeader2, type LayoutOverlayHeader2Props } from '../../LayoutOverlay'
|
|
4
|
+
|
|
5
|
+
export type OverlayHeader2Props = Omit<LayoutOverlayHeader2Props, 'hideBackButton'> & {
|
|
6
|
+
onClose: () => void
|
|
7
|
+
icon?: TitleProps['icon']
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function OverlayHeader2(props: OverlayHeader2Props) {
|
|
11
|
+
const { children, onClose, primary, secondary, icon, ...rest } = props
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<LayoutOverlayHeader2
|
|
15
|
+
hideBackButton
|
|
16
|
+
size='small'
|
|
17
|
+
primary={primary ?? <LayoutHeaderClose onClose={onClose} size='small' />}
|
|
18
|
+
secondary={primary ? <LayoutHeaderClose onClose={onClose} size='small' /> : secondary}
|
|
19
|
+
{...rest}
|
|
20
|
+
>
|
|
21
|
+
<LayoutTitle size='small' component='span' icon={icon}>
|
|
22
|
+
{children}
|
|
23
|
+
</LayoutTitle>
|
|
24
|
+
</LayoutOverlayHeader2>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
export * from './OverlayBase'
|
|
2
1
|
export * from './Overlay'
|
|
3
|
-
export * from './
|
|
2
|
+
export * from './OverlayBase'
|
|
3
|
+
export * from './OverlayCloseButton'
|
|
4
4
|
export * from './OverlayHeader'
|
|
5
|
+
export * from './OverlayHeader2'
|
|
6
|
+
export * from './OverlayStickyBottom'
|
package/Tabs/TabItem.tsx
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { Box, ButtonBase, type SxProps, type Theme } from '@mui/material'
|
|
2
|
+
import { alpha, useTheme } from '@mui/material/styles'
|
|
3
|
+
import { forwardRef } from 'react'
|
|
4
|
+
import { extendableComponent } from '../Styles/extendableComponent'
|
|
5
|
+
import { responsiveVal } from '../Styles/responsiveVal'
|
|
6
|
+
import { sxx } from '../utils/sxx'
|
|
7
|
+
|
|
8
|
+
export type TabItemVariant = 'chrome'
|
|
9
|
+
|
|
10
|
+
export type TabItemProps = {
|
|
11
|
+
/** Whether this tab is selected */
|
|
12
|
+
selected: boolean
|
|
13
|
+
/** Content to display inside the tab */
|
|
14
|
+
children: React.ReactNode
|
|
15
|
+
/** Click handler */
|
|
16
|
+
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void
|
|
17
|
+
|
|
18
|
+
/** Spacing for the tab */
|
|
19
|
+
spacing?: string
|
|
20
|
+
/** Custom styling */
|
|
21
|
+
sx?: SxProps<Theme>
|
|
22
|
+
/** Whether to disable ripple effect */
|
|
23
|
+
disableRipple?: boolean
|
|
24
|
+
/** Additional className */
|
|
25
|
+
className?: string
|
|
26
|
+
|
|
27
|
+
color: (theme: Theme) => string
|
|
28
|
+
variant?: 'chrome'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type State = {
|
|
32
|
+
selected: boolean
|
|
33
|
+
variant: 'chrome'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const name = 'TabItem'
|
|
37
|
+
const parts = ['root', 'content'] as const
|
|
38
|
+
const { withState } = extendableComponent<State, typeof name, typeof parts>(name, parts)
|
|
39
|
+
|
|
40
|
+
export const TabItem = forwardRef<HTMLButtonElement, TabItemProps>((props, ref) => {
|
|
41
|
+
const {
|
|
42
|
+
selected,
|
|
43
|
+
children,
|
|
44
|
+
onClick,
|
|
45
|
+
spacing = responsiveVal(4, 20),
|
|
46
|
+
sx,
|
|
47
|
+
disableRipple = true,
|
|
48
|
+
className,
|
|
49
|
+
color = (theme) => theme.palette.background.default,
|
|
50
|
+
variant = 'chrome',
|
|
51
|
+
...other
|
|
52
|
+
} = props
|
|
53
|
+
|
|
54
|
+
const classes = withState({ selected, variant })
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<Box
|
|
58
|
+
ref={ref}
|
|
59
|
+
component={ButtonBase}
|
|
60
|
+
onClick={selected ? undefined : onClick}
|
|
61
|
+
className={`${classes.root} ${className ?? ''}`}
|
|
62
|
+
disableRipple={selected || disableRipple}
|
|
63
|
+
role='tab'
|
|
64
|
+
aria-selected={selected}
|
|
65
|
+
// tabindex={0}
|
|
66
|
+
sx={sxx(
|
|
67
|
+
(theme) => ({
|
|
68
|
+
position: 'relative',
|
|
69
|
+
textDecoration: 'none',
|
|
70
|
+
color: 'text.primary',
|
|
71
|
+
typography: 'subtitle1',
|
|
72
|
+
mt: spacing,
|
|
73
|
+
height: `calc(100% - ${spacing})`,
|
|
74
|
+
pb: spacing,
|
|
75
|
+
mx: `calc(${spacing} / 2)`,
|
|
76
|
+
transition: 'background-color 0.2s ease-in-out',
|
|
77
|
+
|
|
78
|
+
'&:hover:not(.selected) .TabItem-content': {
|
|
79
|
+
bgcolor: alpha(color(theme), 0.5),
|
|
80
|
+
},
|
|
81
|
+
'&:focus:not(.selected) .TabItem-content': {
|
|
82
|
+
bgcolor: alpha(color(theme), 0.5),
|
|
83
|
+
},
|
|
84
|
+
'&::before': {
|
|
85
|
+
opacity: 0,
|
|
86
|
+
transition: 'opacity 0.2s ease-in-out',
|
|
87
|
+
content: '""',
|
|
88
|
+
position: 'absolute',
|
|
89
|
+
bottom: 0,
|
|
90
|
+
left: '-10px',
|
|
91
|
+
width: '10px',
|
|
92
|
+
height: '10px',
|
|
93
|
+
background: `radial-gradient(circle at top left, transparent 10px, ${color(theme)} 10px)`,
|
|
94
|
+
},
|
|
95
|
+
'&::after': {
|
|
96
|
+
opacity: 0,
|
|
97
|
+
transition: 'opacity 0.2s ease-in-out',
|
|
98
|
+
content: '""',
|
|
99
|
+
position: 'absolute',
|
|
100
|
+
bottom: 0,
|
|
101
|
+
right: '-10px',
|
|
102
|
+
width: '10px',
|
|
103
|
+
height: '10px',
|
|
104
|
+
background: `radial-gradient(circle at top right, transparent 10px, ${color(theme)} 10px)`,
|
|
105
|
+
},
|
|
106
|
+
borderStartStartRadius: '0.5em',
|
|
107
|
+
borderStartEndRadius: '0.5em',
|
|
108
|
+
'&.selected': {
|
|
109
|
+
bgcolor: color(theme),
|
|
110
|
+
'&::before': { opacity: 1 },
|
|
111
|
+
'&::after': { opacity: 1 },
|
|
112
|
+
},
|
|
113
|
+
}),
|
|
114
|
+
sx,
|
|
115
|
+
)}
|
|
116
|
+
{...other}
|
|
117
|
+
>
|
|
118
|
+
<Box
|
|
119
|
+
className={classes.content}
|
|
120
|
+
sx={{
|
|
121
|
+
display: 'flex',
|
|
122
|
+
alignItems: 'center',
|
|
123
|
+
height: '100%',
|
|
124
|
+
px: spacing,
|
|
125
|
+
gap: `calc(${spacing} / 2)`,
|
|
126
|
+
borderRadius: '0.5em',
|
|
127
|
+
transition: 'background-color 0.2s ease-in-out',
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
{children}
|
|
131
|
+
</Box>
|
|
132
|
+
</Box>
|
|
133
|
+
)
|
|
134
|
+
})
|
package/Tabs/Tabs.tsx
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Scroller, ScrollerButton, ScrollerProvider } from '@graphcommerce/framer-scroller'
|
|
2
|
+
import { Box, type SxProps, type Theme } from '@mui/material'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { iconChevronLeft, iconChevronRight } from '../icons'
|
|
5
|
+
import { IconSvg } from '../IconSvg'
|
|
6
|
+
import { extendableComponent } from '../Styles/extendableComponent'
|
|
7
|
+
import { sxx } from '../utils/sxx'
|
|
8
|
+
|
|
9
|
+
export type TabsProps = {
|
|
10
|
+
children: React.ReactNode
|
|
11
|
+
sx?: SxProps<Theme>
|
|
12
|
+
/** Additional content to show before tabs */
|
|
13
|
+
startContent?: React.ReactNode
|
|
14
|
+
/** Additional content to show after tabs */
|
|
15
|
+
endContent?: React.ReactNode
|
|
16
|
+
/** Whether to show separators between tabs */
|
|
17
|
+
showSeparators?: boolean
|
|
18
|
+
/** Custom separator component */
|
|
19
|
+
separator?: React.ReactNode
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { classes } = extendableComponent('Tabs', ['root', 'scroller', 'left', 'right', 'button'])
|
|
23
|
+
|
|
24
|
+
export function Tabs(props: TabsProps) {
|
|
25
|
+
const { children, sx, startContent, endContent, showSeparators = true, separator } = props
|
|
26
|
+
|
|
27
|
+
const defaultSeparator = (
|
|
28
|
+
<Box
|
|
29
|
+
className='separator'
|
|
30
|
+
sx={{ width: '2px', height: '1em', bgcolor: 'divider', mx: '-1px' }}
|
|
31
|
+
/>
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
const separatorElement = separator ?? defaultSeparator
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<Box
|
|
38
|
+
className={classes.root}
|
|
39
|
+
sx={sxx(
|
|
40
|
+
{
|
|
41
|
+
alignItems: 'stretch',
|
|
42
|
+
position: 'relative',
|
|
43
|
+
pointerEvents: 'all',
|
|
44
|
+
gridTemplateColumns: 'auto 1fr auto',
|
|
45
|
+
display: 'grid',
|
|
46
|
+
gridAutoFlow: 'column',
|
|
47
|
+
},
|
|
48
|
+
sx,
|
|
49
|
+
)}
|
|
50
|
+
>
|
|
51
|
+
<ScrollerProvider scrollSnapAlign='none'>
|
|
52
|
+
<Scroller
|
|
53
|
+
hideScrollbar
|
|
54
|
+
sx={{
|
|
55
|
+
gridArea: '1 / 1 / 1 / 4',
|
|
56
|
+
gridAutoColumns: 'min-content',
|
|
57
|
+
alignItems: 'center',
|
|
58
|
+
// Hide the Separator that comes before or after the selected tab.
|
|
59
|
+
'& .separator': { transition: 'opacity 0.2s ease-in-out' },
|
|
60
|
+
'& .TabItem-root.selected + .separator': { opacity: 0 },
|
|
61
|
+
'& .separator:has(+ .TabItem-root.selected)': { opacity: 0 },
|
|
62
|
+
// Same for hover:
|
|
63
|
+
'& .TabItem-root:hover + .separator': { opacity: 0 },
|
|
64
|
+
'& .separator:has(+ .TabItem-root:hover)': { opacity: 0 },
|
|
65
|
+
}}
|
|
66
|
+
className={classes.scroller}
|
|
67
|
+
role='tablist'
|
|
68
|
+
>
|
|
69
|
+
{startContent}
|
|
70
|
+
|
|
71
|
+
{showSeparators && startContent && separatorElement}
|
|
72
|
+
|
|
73
|
+
{React.Children.map(children, (child, index) => (
|
|
74
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
75
|
+
<React.Fragment key={index}>
|
|
76
|
+
{showSeparators && index > 0 && separatorElement}
|
|
77
|
+
{child}
|
|
78
|
+
</React.Fragment>
|
|
79
|
+
))}
|
|
80
|
+
|
|
81
|
+
{showSeparators && endContent && separatorElement}
|
|
82
|
+
|
|
83
|
+
{endContent}
|
|
84
|
+
</Scroller>
|
|
85
|
+
|
|
86
|
+
<ScrollerButton
|
|
87
|
+
sxContainer={{
|
|
88
|
+
gridArea: '1 / 1 / 1 / 2',
|
|
89
|
+
display: 'flex',
|
|
90
|
+
alignItems: 'center',
|
|
91
|
+
justifyContent: 'center',
|
|
92
|
+
pointerEvents: 'none',
|
|
93
|
+
'& > *': { pointerEvents: 'all' },
|
|
94
|
+
}}
|
|
95
|
+
sx={{ pointerEvents: 'all' }}
|
|
96
|
+
direction='left'
|
|
97
|
+
size='small'
|
|
98
|
+
tabIndex={-1}
|
|
99
|
+
className={`${classes.left} ${classes.button}`}
|
|
100
|
+
>
|
|
101
|
+
<IconSvg src={iconChevronLeft} />
|
|
102
|
+
</ScrollerButton>
|
|
103
|
+
|
|
104
|
+
<ScrollerButton
|
|
105
|
+
sxContainer={{
|
|
106
|
+
gridArea: '1 / 3 / 1 / 4',
|
|
107
|
+
display: 'flex',
|
|
108
|
+
alignItems: 'center',
|
|
109
|
+
justifyContent: 'center',
|
|
110
|
+
pointerEvents: 'none',
|
|
111
|
+
'& > *': { pointerEvents: 'all' },
|
|
112
|
+
}}
|
|
113
|
+
sx={{ pointerEvents: 'all' }}
|
|
114
|
+
direction='right'
|
|
115
|
+
size='small'
|
|
116
|
+
tabIndex={-1}
|
|
117
|
+
className={`${classes.right} ${classes.button}`}
|
|
118
|
+
>
|
|
119
|
+
<IconSvg src={iconChevronRight} />
|
|
120
|
+
</ScrollerButton>
|
|
121
|
+
</ScrollerProvider>
|
|
122
|
+
</Box>
|
|
123
|
+
)
|
|
124
|
+
}
|
package/index.ts
CHANGED
|
@@ -59,6 +59,8 @@ export * from './Stepper/Stepper'
|
|
|
59
59
|
export * from './Styles'
|
|
60
60
|
export * from './TextInputNumber/TextInputNumber'
|
|
61
61
|
export * from './Theme'
|
|
62
|
+
export * from './Tabs/TabItem'
|
|
63
|
+
export * from './Tabs/Tabs'
|
|
62
64
|
export * from './TimeAgo/TimeAgo'
|
|
63
65
|
export * from './ToggleButton/ToggleButton'
|
|
64
66
|
export * from './ToggleButtonGroup/ToggleButtonGroup'
|
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": "9.1.0-canary.
|
|
5
|
+
"version": "9.1.0-canary.55",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"prettier": "@graphcommerce/prettier-config-pwa",
|
|
8
8
|
"eslintConfig": {
|
|
@@ -24,13 +24,13 @@
|
|
|
24
24
|
"@emotion/react": "^11",
|
|
25
25
|
"@emotion/server": "^11",
|
|
26
26
|
"@emotion/styled": "^11",
|
|
27
|
-
"@graphcommerce/eslint-config-pwa": "^9.1.0-canary.
|
|
28
|
-
"@graphcommerce/framer-next-pages": "^9.1.0-canary.
|
|
29
|
-
"@graphcommerce/framer-scroller": "^9.1.0-canary.
|
|
30
|
-
"@graphcommerce/framer-utils": "^9.1.0-canary.
|
|
31
|
-
"@graphcommerce/image": "^9.1.0-canary.
|
|
32
|
-
"@graphcommerce/prettier-config-pwa": "^9.1.0-canary.
|
|
33
|
-
"@graphcommerce/typescript-config-pwa": "^9.1.0-canary.
|
|
27
|
+
"@graphcommerce/eslint-config-pwa": "^9.1.0-canary.55",
|
|
28
|
+
"@graphcommerce/framer-next-pages": "^9.1.0-canary.55",
|
|
29
|
+
"@graphcommerce/framer-scroller": "^9.1.0-canary.55",
|
|
30
|
+
"@graphcommerce/framer-utils": "^9.1.0-canary.55",
|
|
31
|
+
"@graphcommerce/image": "^9.1.0-canary.55",
|
|
32
|
+
"@graphcommerce/prettier-config-pwa": "^9.1.0-canary.55",
|
|
33
|
+
"@graphcommerce/typescript-config-pwa": "^9.1.0-canary.55",
|
|
34
34
|
"@lingui/core": "^4.2.1",
|
|
35
35
|
"@lingui/macro": "^4.2.1",
|
|
36
36
|
"@lingui/react": "^4.2.1",
|