@graphcommerce/next-ui 8.1.0-canary.26 → 8.1.0-canary.28

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.
@@ -0,0 +1,195 @@
1
+ import { Trans } from '@lingui/react'
2
+ import {
3
+ Box,
4
+ Breadcrumbs as MuiBreadcrumbs,
5
+ BreadcrumbsProps as MuiBreadcrumbProps,
6
+ ClickAwayListener,
7
+ IconButton,
8
+ Link,
9
+ Typography,
10
+ useEventCallback,
11
+ useTheme,
12
+ SxProps,
13
+ Theme,
14
+ LinkProps,
15
+ } from '@mui/material'
16
+ import dynamic from 'next/dynamic'
17
+ import { useState, MouseEvent } from 'react'
18
+ import { IconSvg } from '../IconSvg'
19
+ import { iconClose, iconEllypsis } from '../icons'
20
+ import type { BreadcrumbsType } from './types'
21
+ import { i18n } from '@lingui/core'
22
+ import { Button } from '../Button'
23
+
24
+ const BreadcrumbsPopper = dynamic(
25
+ async () => (await import('./BreadcrumbsPopper')).BreadcrumbsPopper,
26
+ )
27
+
28
+ export type BreadcrumbsProps = BreadcrumbsType &
29
+ Omit<MuiBreadcrumbProps, 'children'> & {
30
+ maxItems?: number
31
+ lastIsLink?: boolean
32
+ breadcrumbsAmountDesktop?: number
33
+ breadcrumbsAmountMobile?: number
34
+ itemSx?: SxProps<Theme>
35
+ linkProps?: Omit<LinkProps, 'href'>
36
+ }
37
+
38
+ export function Breadcrumbs(props: BreadcrumbsProps) {
39
+ const {
40
+ breadcrumbs,
41
+ sx,
42
+ breadcrumbsAmountDesktop = 4,
43
+ breadcrumbsAmountMobile = 2,
44
+ lastIsLink = false,
45
+ maxItems,
46
+ itemSx = [],
47
+ linkProps,
48
+ ...rest
49
+ } = props
50
+ const [anchorElement, setAnchorElement] = useState<HTMLButtonElement | null>(null)
51
+ const theme = useTheme()
52
+
53
+ const isDefaultMobile = breadcrumbsAmountMobile === 0
54
+ const showButtonMobile = breadcrumbs.length > breadcrumbsAmountMobile && !isDefaultMobile
55
+ const isDefaultDesktop = breadcrumbsAmountDesktop === 0
56
+ const showButtonDesktop = breadcrumbs.length > breadcrumbsAmountDesktop && !isDefaultDesktop
57
+
58
+ const handleClick = useEventCallback((event: MouseEvent<HTMLButtonElement>) => {
59
+ setAnchorElement((el) => (el !== event.currentTarget ? event.currentTarget : null))
60
+ })
61
+
62
+ const handleClose = () => setAnchorElement(null)
63
+
64
+ const breadcrumbLinks = [...(lastIsLink ? breadcrumbs : breadcrumbs.slice(0, -1))]
65
+ const last = lastIsLink ? null : breadcrumbs[breadcrumbs.length - 1]
66
+
67
+ return (
68
+ <MuiBreadcrumbs
69
+ {...rest}
70
+ aria-label={i18n._(/* i18n*/ `Breadcrumbs`)}
71
+ maxItems={maxItems}
72
+ color='inherit'
73
+ sx={[
74
+ {},
75
+ !maxItems && {
76
+ '& .MuiBreadcrumbs-ol': {
77
+ flexWrap: 'nowrap',
78
+ '& .MuiBreadcrumbs-li': {
79
+ '&:nth-of-type(1)': {
80
+ display: {
81
+ xs: showButtonMobile ? 'flex' : 'none',
82
+ md: showButtonDesktop ? 'flex' : 'none',
83
+ },
84
+ },
85
+ '&:nth-last-of-type(1)': {
86
+ display: 'inline-flex',
87
+ overflowX: 'hidden',
88
+ },
89
+ },
90
+ },
91
+ '& .MuiBreadcrumbs-separator': {
92
+ '&:nth-of-type(2)': {
93
+ display: {
94
+ xs: !showButtonMobile && 'none',
95
+ md: !showButtonDesktop && 'none',
96
+ },
97
+ },
98
+ },
99
+
100
+ [theme.breakpoints.down('md')]: showButtonMobile && {
101
+ '& .MuiBreadcrumbs-li, & .MuiBreadcrumbs-separator': {
102
+ display: 'none',
103
+ [`&:nth-last-of-type(-n+${breadcrumbsAmountMobile * 2})`]: {
104
+ display: 'flex',
105
+ },
106
+ },
107
+ },
108
+
109
+ [theme.breakpoints.up('md')]: showButtonDesktop && {
110
+ '& .MuiBreadcrumbs-li, & .MuiBreadcrumbs-separator': {
111
+ display: 'none',
112
+ [`&:nth-last-of-type(-n+${breadcrumbsAmountDesktop * 2})`]: {
113
+ display: 'flex',
114
+ },
115
+ },
116
+ },
117
+ },
118
+ ...(Array.isArray(sx) ? sx : [sx]),
119
+ ]}
120
+ >
121
+ {!maxItems && (
122
+ <ClickAwayListener
123
+ mouseEvent='onMouseDown'
124
+ touchEvent='onTouchStart'
125
+ onClickAway={handleClose}
126
+ >
127
+ <Box sx={{ position: 'relative', display: 'flex' }}>
128
+ <Button
129
+ aria-describedby={anchorElement ? 'breadcrumb-list' : undefined}
130
+ color='inherit'
131
+ variant='pill'
132
+ size='small'
133
+ onClick={handleClick}
134
+ sx={{
135
+ minWidth: 0,
136
+ // borderRadius: 2,
137
+ // boxShadow: 6,
138
+ // color: 'text.primary',
139
+ // px: 1,
140
+ // py: { xs: 0.3, md: 0.5 },
141
+ // typography: 'caption',
142
+ // backgroundColor: 'background.paper',
143
+ }}
144
+ >
145
+ <IconSvg src={anchorElement ? iconClose : iconEllypsis} />
146
+ </Button>
147
+ <BreadcrumbsPopper
148
+ breadcrumbs={breadcrumbs}
149
+ anchorElement={anchorElement}
150
+ onClose={handleClose}
151
+ showDesktopAmount={breadcrumbsAmountDesktop}
152
+ showMobileAmount={breadcrumbsAmountMobile}
153
+ />
154
+ </Box>
155
+ </ClickAwayListener>
156
+ )}
157
+
158
+ <Link
159
+ href='/'
160
+ color='inherit'
161
+ underline='hover'
162
+ {...linkProps}
163
+ sx={[...(Array.isArray(itemSx) ? itemSx : [itemSx])]}
164
+ >
165
+ <Trans id='Home' />
166
+ </Link>
167
+ {breadcrumbLinks.map((breadcrumb) => (
168
+ <Link
169
+ key={breadcrumb.href}
170
+ color='inherit'
171
+ underline='hover'
172
+ {...breadcrumb}
173
+ {...linkProps}
174
+ sx={[
175
+ ...(Array.isArray(breadcrumb.sx) ? breadcrumb.sx : [breadcrumb.sx]),
176
+ ...(Array.isArray(itemSx) ? itemSx : [itemSx]),
177
+ ]}
178
+ >
179
+ {breadcrumb.name}
180
+ </Link>
181
+ ))}
182
+
183
+ {last && (
184
+ <Typography
185
+ component='span'
186
+ noWrap
187
+ color='inherit'
188
+ sx={[{ fontWeight: '600' }, ...(Array.isArray(itemSx) ? itemSx : [itemSx])]}
189
+ >
190
+ {last.name}
191
+ </Typography>
192
+ )}
193
+ </MuiBreadcrumbs>
194
+ )
195
+ }
@@ -0,0 +1,13 @@
1
+ import { JsonLd } from '../JsonLd/JsonLd'
2
+ import type { BreadcrumbsType } from './types'
3
+
4
+ type BreadcrumbsJsonLdProps<T extends { '@type': string }> = BreadcrumbsType & {
5
+ render: (breadcrumbs: BreadcrumbsType['breadcrumbs']) => T & { '@context': 'https://schema.org' }
6
+ }
7
+
8
+ export function BreadcrumbsJsonLd<T extends { '@type': string }>(props: BreadcrumbsJsonLdProps<T>) {
9
+ const { render, breadcrumbs } = props
10
+
11
+ if (!breadcrumbs.length) return null
12
+ return <JsonLd<T> item={render(breadcrumbs)} keyVal='breadcrumb-jsonld' />
13
+ }
@@ -0,0 +1,101 @@
1
+ import { Trans } from '@lingui/react'
2
+ import { Box, Link, alpha, useTheme } from '@mui/material'
3
+ import { useEffect, useRef, KeyboardEvent } from 'react'
4
+ import type { BreadcrumbsType } from './types'
5
+
6
+ type PopperBreadcrumbsListProps = {
7
+ autoFocus: boolean
8
+ breadcrumbs: BreadcrumbsType['breadcrumbs']
9
+ showDesktopAmount?: number
10
+ showMobileAmount?: number
11
+ onClose: () => void
12
+ }
13
+
14
+ export function BreadcrumbsList(props: PopperBreadcrumbsListProps) {
15
+ const { autoFocus, breadcrumbs, showDesktopAmount = 4, showMobileAmount = 3, onClose } = props
16
+ const listRef = useRef<HTMLDivElement | null>(null)
17
+ const theme = useTheme()
18
+
19
+ const handleKeyDown = (event: KeyboardEvent) => {
20
+ if (event.key === 'Escape') {
21
+ onClose()
22
+ }
23
+ }
24
+
25
+ useEffect(() => {
26
+ if (autoFocus) {
27
+ listRef.current?.focus()
28
+ }
29
+ }, [autoFocus])
30
+
31
+ return (
32
+ <Box
33
+ ref={listRef}
34
+ onKeyDown={handleKeyDown}
35
+ tabIndex={-1}
36
+ sx={{
37
+ backgroundColor: 'background.paper',
38
+ borderRadius: 3,
39
+ boxShadow: 12,
40
+ display: 'flex',
41
+ flexDirection: 'column',
42
+ overflow: 'hidden auto',
43
+ py: `calc(${theme.spacings.xxs} / 2)`,
44
+ [theme.breakpoints.up('md')]: {
45
+ '& .MuiLink-root': {
46
+ [`:nth-last-of-type(-n+${showDesktopAmount})`]: {
47
+ display: 'none',
48
+ },
49
+ },
50
+ },
51
+ [theme.breakpoints.down('md')]: {
52
+ '& .MuiLink-root': {
53
+ [`:nth-last-of-type(-n+${showMobileAmount})`]: {
54
+ display: 'none',
55
+ },
56
+ },
57
+ },
58
+ }}
59
+ >
60
+ <Link
61
+ href='/'
62
+ underline='none'
63
+ color='text.primary'
64
+ variant='body1'
65
+ noWrap
66
+ onClick={onClose}
67
+ tabIndex={0}
68
+ sx={{
69
+ flex: 1,
70
+ padding: `calc(${theme.spacings.xxs} / 2) ${theme.spacings.xs}`,
71
+ '&:hover': {
72
+ backgroundColor: alpha(theme.palette.action.hover, 0.025),
73
+ },
74
+ }}
75
+ >
76
+ <Trans id='Home' />
77
+ </Link>
78
+ {breadcrumbs.slice(0, breadcrumbs.length).map((breadcrumb) => (
79
+ <Link
80
+ {...breadcrumb}
81
+ key={breadcrumb.href}
82
+ underline='none'
83
+ color='text.primary'
84
+ variant='body1'
85
+ noWrap
86
+ onClick={onClose}
87
+ tabIndex={0}
88
+ sx={{
89
+ flex: 1,
90
+ padding: `calc(${theme.spacings.xxs} / 2) ${theme.spacings.xs}`,
91
+ '&:hover': {
92
+ backgroundColor: alpha(theme.palette.action.hover, 0.025),
93
+ },
94
+ }}
95
+ >
96
+ {breadcrumb.name}
97
+ </Link>
98
+ ))}
99
+ </Box>
100
+ )
101
+ }
@@ -0,0 +1,48 @@
1
+ import { Box, Popper } from '@mui/material'
2
+ import { BreadcrumbsList } from './BreadcrumbsList'
3
+ import type { BreadcrumbsType } from './types'
4
+
5
+ export type BreadcrumbsPopperType = BreadcrumbsType & {
6
+ anchorElement: HTMLButtonElement | null
7
+ showDesktopAmount?: number
8
+ showMobileAmount?: number
9
+ onClose: () => void
10
+ }
11
+
12
+ export function BreadcrumbsPopper(props: BreadcrumbsPopperType) {
13
+ const { anchorElement, breadcrumbs, showDesktopAmount, showMobileAmount, onClose } = props
14
+
15
+ return (
16
+ <Popper
17
+ anchorEl={anchorElement}
18
+ open={Boolean(anchorElement)}
19
+ disablePortal
20
+ placement='bottom-start'
21
+ modifiers={[
22
+ { name: 'offset', options: { offset: [0, 10] } },
23
+ {
24
+ name: 'preventOverflow',
25
+ enabled: true,
26
+ options: { altBoundary: false, padding: 10 },
27
+ },
28
+ ]}
29
+ sx={(theme) => ({
30
+ maxWidth: {
31
+ md: `calc(100vw - ${theme.spacings.xxl} * 2)`,
32
+ xs: `calc(100vw - ${theme.page.horizontal} * 2)`,
33
+ },
34
+ zIndex: 100,
35
+ })}
36
+ >
37
+ <Box>
38
+ <BreadcrumbsList
39
+ autoFocus={Boolean(anchorElement)}
40
+ breadcrumbs={breadcrumbs}
41
+ showDesktopAmount={showDesktopAmount}
42
+ showMobileAmount={showMobileAmount}
43
+ onClose={onClose}
44
+ />
45
+ </Box>
46
+ </Popper>
47
+ )
48
+ }
@@ -0,0 +1,4 @@
1
+ export * from './Breadcrumbs'
2
+ export * from './types'
3
+ export * from './BreadcrumbsJsonLd'
4
+ export * from './jsonLdBreadcrumb'
@@ -0,0 +1,19 @@
1
+ import { NextRouter } from 'next/router'
2
+ import type { BreadcrumbList } from 'schema-dts'
3
+ import { canonicalize } from '../PageMeta/PageMeta'
4
+ import type { BreadcrumbsType } from './types'
5
+
6
+ export function jsonLdBreadcrumb(
7
+ breadcrumbs: BreadcrumbsType['breadcrumbs'],
8
+ router: NextRouter,
9
+ ): BreadcrumbList {
10
+ return {
11
+ '@type': 'BreadcrumbList',
12
+ itemListElement: breadcrumbs.map(({ name, href }, index) => ({
13
+ '@type': 'ListItem',
14
+ position: index + 1,
15
+ name,
16
+ item: canonicalize(router, href),
17
+ })),
18
+ }
19
+ }
@@ -0,0 +1,11 @@
1
+ import { Theme } from '@emotion/react'
2
+ import { SxProps } from '@mui/material'
3
+
4
+ export type BreadcrumbItem = {
5
+ name: string
6
+ href: string
7
+ }
8
+
9
+ export type BreadcrumbsType = {
10
+ breadcrumbs: ({ sx?: SxProps<Theme> } & BreadcrumbItem)[]
11
+ }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Change Log
2
2
 
3
+ ## 8.1.0-canary.28
4
+
5
+ ## 8.1.0-canary.27
6
+
7
+ ### Minor Changes
8
+
9
+ - [#2273](https://github.com/graphcommerce-org/graphcommerce/pull/2273) [`77955c5`](https://github.com/graphcommerce-org/graphcommerce/commit/77955c56ac8633ab1c5e0f3ddb25e3a87236e2bb) - Improve Breadcrumbs on Category and Product pages
10
+ ([@Jessevdpoel](https://github.com/Jessevdpoel))
11
+
3
12
  ## 8.1.0-canary.26
4
13
 
5
14
  ### Minor Changes
package/Config.graphqls CHANGED
@@ -3,6 +3,11 @@ extend input GraphCommerceConfig {
3
3
  Configuration for the SidebarGallery component
4
4
  """
5
5
  sidebarGallery: SidebarGalleryConfig
6
+
7
+ """
8
+ Configuration for the SidebarGallery component
9
+ """
10
+ breadcrumbs: Boolean = false
6
11
  }
7
12
 
8
13
  """
@@ -356,7 +356,7 @@ export function SidebarGallery(props: SidebarGalleryProps) {
356
356
  'THUMBNAILS_BOTTOM' ? (
357
357
  <ScrollerThumbnails images={images} />
358
358
  ) : (
359
- <ScrollerDots />
359
+ <ScrollerDots layout />
360
360
  )}
361
361
  </Box>
362
362
  </MotionBox>
package/JsonLd/JsonLd.tsx CHANGED
@@ -2,14 +2,15 @@ import Head from 'next/head'
2
2
  import { safeJsonLdReplacer } from './safeJsonLdReplacer'
3
3
 
4
4
  export function JsonLd<T extends { '@type': string }>(props: {
5
+ keyVal?: string
5
6
  item: T & { '@context': 'https://schema.org' }
6
7
  }) {
7
- const { item } = props
8
+ const { item, keyVal } = props
8
9
 
9
10
  return (
10
11
  <Head>
11
12
  <script
12
- key='jsonld'
13
+ key={keyVal ?? 'jsonld'}
13
14
  type='application/ld+json'
14
15
  // eslint-disable-next-line react/no-danger
15
16
  dangerouslySetInnerHTML={{ __html: JSON.stringify(item, safeJsonLdReplacer) }}
@@ -140,6 +140,9 @@ export function LayoutHeader(props: LayoutHeaderProps) {
140
140
  '&.divider': {
141
141
  marginBottom: 0,
142
142
  },
143
+ '& .LayoutHeaderContent-left': import.meta.graphCommerce.breadcrumbs && {
144
+ display: 'none',
145
+ },
143
146
  },
144
147
  }),
145
148
  ...(Array.isArray(sx) ? sx : [sx]),
@@ -40,12 +40,7 @@ export const LayoutTitle = React.forwardRef<HTMLDivElement, TitleProps>((props,
40
40
  alignItems: 'center',
41
41
  justifyContent: 'center',
42
42
  gap: `6px`,
43
- flexFlow: 'unset',
44
- [theme.breakpoints.up('md')]: {
45
- flexFlow: 'column',
46
- },
47
43
  '&.sizeSmall': {
48
- flexFlow: 'unset',
49
44
  overflow: 'hidden',
50
45
  '& svg': {
51
46
  width: responsiveVal(24, 28),
package/index.ts CHANGED
@@ -8,6 +8,7 @@ export * from './Blog/BlogListItem/BlogListItem'
8
8
  export * from './Blog/BlogTags/BlogTags'
9
9
  export * from './Blog/BlogTitle/BlogTitle'
10
10
  export * from './Button'
11
+ export * from './Breadcrumbs'
11
12
  export * from './ChipMenu/ChipMenu'
12
13
  export * from './ContainerWithHeader/ContainerWithHeader'
13
14
  export * from './Fab'
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": "8.1.0-canary.26",
5
+ "version": "8.1.0-canary.28",
6
6
  "sideEffects": false,
7
7
  "prettier": "@graphcommerce/prettier-config-pwa",
8
8
  "eslintConfig": {
@@ -26,14 +26,14 @@
26
26
  "typescript": "5.3.3"
27
27
  },
28
28
  "peerDependencies": {
29
- "@graphcommerce/eslint-config-pwa": "^8.1.0-canary.26",
30
- "@graphcommerce/framer-next-pages": "^8.1.0-canary.26",
31
- "@graphcommerce/framer-scroller": "^8.1.0-canary.26",
32
- "@graphcommerce/framer-utils": "^8.1.0-canary.26",
33
- "@graphcommerce/image": "^8.1.0-canary.26",
34
- "@graphcommerce/lingui-next": "^8.1.0-canary.26",
35
- "@graphcommerce/prettier-config-pwa": "^8.1.0-canary.26",
36
- "@graphcommerce/typescript-config-pwa": "^8.1.0-canary.26",
29
+ "@graphcommerce/eslint-config-pwa": "^8.1.0-canary.28",
30
+ "@graphcommerce/framer-next-pages": "^8.1.0-canary.28",
31
+ "@graphcommerce/framer-scroller": "^8.1.0-canary.28",
32
+ "@graphcommerce/framer-utils": "^8.1.0-canary.28",
33
+ "@graphcommerce/image": "^8.1.0-canary.28",
34
+ "@graphcommerce/lingui-next": "^8.1.0-canary.28",
35
+ "@graphcommerce/prettier-config-pwa": "^8.1.0-canary.28",
36
+ "@graphcommerce/typescript-config-pwa": "^8.1.0-canary.28",
37
37
  "@lingui/core": "^4.2.1",
38
38
  "@lingui/macro": "^4.2.1",
39
39
  "@lingui/react": "^4.2.1",