@graphcommerce/next-ui 8.1.0-canary.9 → 9.0.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.
Files changed (65) hide show
  1. package/ActionCard/ActionCard.tsx +60 -36
  2. package/ActionCard/ActionCardAccordion.tsx +2 -1
  3. package/ActionCard/ActionCardList.tsx +3 -2
  4. package/ActionCard/ActionCardListForm.tsx +1 -1
  5. package/Blog/BlogAuthor/BlogAuthor.tsx +2 -4
  6. package/Blog/BlogListItem/BlogListItem.tsx +2 -4
  7. package/Breadcrumbs/Breadcrumbs.tsx +195 -0
  8. package/Breadcrumbs/BreadcrumbsJsonLd.tsx +13 -0
  9. package/Breadcrumbs/BreadcrumbsList.tsx +101 -0
  10. package/Breadcrumbs/BreadcrumbsPopper.tsx +48 -0
  11. package/Breadcrumbs/index.ts +4 -0
  12. package/Breadcrumbs/jsonLdBreadcrumb.tsx +19 -0
  13. package/Breadcrumbs/types.ts +11 -0
  14. package/CHANGELOG.md +162 -0
  15. package/Config.graphqls +5 -0
  16. package/Document/DocumentBodyEnd.tsx +7 -0
  17. package/Document/DocumentBodyStart.tsx +7 -0
  18. package/Document/DocumentHeadEnd.tsx +7 -0
  19. package/Document/DocumentHeadStart.tsx +7 -0
  20. package/Document/index.ts +4 -0
  21. package/Footer/Footer.tsx +1 -1
  22. package/FramerScroller/SidebarGallery.tsx +54 -27
  23. package/Intl/DateTimeFormat/DateFormat.tsx +10 -0
  24. package/Intl/DateTimeFormat/DateTimeFormat.tsx +25 -0
  25. package/Intl/DateTimeFormat/TimeFormat.tsx +10 -0
  26. package/Intl/DateTimeFormat/index.ts +3 -0
  27. package/Intl/DisplayNames/DisplayNames.tsx +22 -0
  28. package/Intl/DisplayNames/index.ts +1 -0
  29. package/Intl/ListFormat.tsx +32 -0
  30. package/Intl/NumberFormat/CurrencyFormat.tsx +191 -0
  31. package/Intl/NumberFormat/NumberFormat.tsx +24 -0
  32. package/Intl/NumberFormat/PercentFormat.tsx +5 -0
  33. package/Intl/NumberFormat/UnitFormat.tsx +59 -0
  34. package/Intl/NumberFormat/index.ts +4 -0
  35. package/Intl/index.ts +11 -0
  36. package/JsonLd/JsonLd.tsx +3 -2
  37. package/Layout/components/LayoutHeader.tsx +17 -3
  38. package/Layout/components/LayoutHeaderBack.tsx +9 -1
  39. package/Layout/components/LayoutTitle.tsx +0 -5
  40. package/LazyHydrate/LazyHydrate.tsx +19 -3
  41. package/PageLoadIndicator/PageLoadIndicator.tsx +25 -14
  42. package/PageMeta/PageMeta.tsx +1 -76
  43. package/PageMeta/canonicalize.ts +78 -0
  44. package/Pagination/PaginationExtended.tsx +103 -0
  45. package/Snackbar/MessageSnackbarImpl.tsx +60 -19
  46. package/Styles/responsiveVal.tsx +4 -5
  47. package/Styles/withEmotionCache.tsx +2 -2
  48. package/Theme/DarkLightModeThemeProvider.tsx +41 -19
  49. package/hooks/index.ts +1 -1
  50. package/hooks/useDateTimeFormat.ts +4 -4
  51. package/hooks/{useSsr.ts → useIsSsr.ts} +3 -0
  52. package/hooks/useLocale.ts +2 -2
  53. package/hooks/useNumberFormat.ts +4 -3
  54. package/hooks/useStorefrontConfig.ts +1 -9
  55. package/hooks/useUrlQuery.ts +6 -4
  56. package/icons.ts +55 -0
  57. package/index.ts +10 -3
  58. package/package.json +8 -9
  59. package/server.ts +8 -0
  60. package/utils/cssFlags.tsx +53 -0
  61. package/utils/getCssFlagInitScript.tsx +20 -0
  62. package/utils/normalizeLocale.ts +26 -0
  63. package/utils/sitemap.ts +1 -1
  64. package/utils/storefrontConfig.ts +8 -0
  65. package/icons/index.ts +0 -48
@@ -13,7 +13,7 @@ import { extendableComponent, responsiveVal } from '../Styles'
13
13
  import { breakpointVal } from '../Styles/breakpointVal'
14
14
 
15
15
  type Variants = 'outlined' | 'default'
16
- type Size = 'large' | 'medium' | 'small'
16
+ type Size = 'large' | 'medium' | 'small' | 'responsive'
17
17
  type Color = 'primary' | 'secondary' | 'success' | 'error' | 'info' | 'warning'
18
18
  type Layout = 'inline' | 'grid' | 'list' | 'stack'
19
19
 
@@ -37,9 +37,9 @@ export type ActionCardProps = {
37
37
  price?: React.ReactNode
38
38
  after?: React.ReactNode
39
39
  secondaryAction?: React.ReactNode
40
- onClick?: (event: React.MouseEvent<HTMLElement>, value: string | number | null) => void
40
+ onClick?: (event: React.MouseEvent<HTMLElement>, value: string | number | boolean | null) => void
41
41
  selected?: boolean
42
- value: string | number | null
42
+ value: string | number | boolean | null
43
43
  reset?: React.ReactNode
44
44
  disabled?: boolean
45
45
  error?: boolean
@@ -47,6 +47,7 @@ export type ActionCardProps = {
47
47
 
48
48
  const parts = [
49
49
  'root',
50
+ 'rootInner',
50
51
  'image',
51
52
  'title',
52
53
  'action',
@@ -77,6 +78,13 @@ const { withState, selectors } = extendableComponent<StateProps, typeof name, ty
77
78
 
78
79
  export const actionCardSelectors = selectors
79
80
 
81
+ export const actionCardImageSizes = {
82
+ small: responsiveVal(60, 80),
83
+ medium: responsiveVal(60, 80),
84
+ large: responsiveVal(100, 120),
85
+ responsive: responsiveVal(60, 120),
86
+ }
87
+
80
88
  export function ActionCard(props: ActionCardProps) {
81
89
  const {
82
90
  title,
@@ -92,7 +100,7 @@ export function ActionCard(props: ActionCardProps) {
92
100
  selected = false,
93
101
  reset,
94
102
  disabled = false,
95
- size = 'medium',
103
+ size = 'responsive',
96
104
  color = 'primary',
97
105
  variant = 'outlined',
98
106
  layout = 'list',
@@ -140,18 +148,24 @@ export function ActionCard(props: ActionCardProps) {
140
148
  py: responsiveVal(12, 14),
141
149
  display: 'block',
142
150
  },
151
+ '&.sizeResponsive': {
152
+ px: responsiveVal(8, 16),
153
+ py: responsiveVal(4, 14),
154
+ display: { xs: 'flex', md: 'block', lg: 'block' },
155
+ [theme.breakpoints.down('md')]: { typography: 'body2' },
156
+ },
143
157
 
144
158
  '&.variantDefault': {
145
159
  position: 'relative',
146
- '&::after': {
147
- content: '""',
148
- position: 'absolute',
149
- width: '100%',
150
- left: 0,
151
- bottom: '-1px',
152
- borderBottom: `1px solid ${theme.palette.divider}`,
153
- display: 'block',
154
- },
160
+ // '&::after': {
161
+ // content: '""',
162
+ // position: 'absolute',
163
+ // width: '100%',
164
+ // left: 0,
165
+ // bottom: '-1px',
166
+ // borderBottom: `1px solid ${theme.palette.divider}`,
167
+ // display: 'block',
168
+ // },
155
169
  '&.selected': {
156
170
  backgroundColor:
157
171
  theme.palette.mode === 'light'
@@ -165,27 +179,34 @@ export function ActionCard(props: ActionCardProps) {
165
179
  )}`,
166
180
  },
167
181
 
168
- '&.sizeSmall': {
169
- mt: { xs: '2px', sm: '3px', md: '5px' },
170
- mb: { xs: '3px', sm: '4px', md: '6px' },
171
- '&::after': {
172
- mb: { xs: '-2px', sm: '-3px', md: '-5px' },
173
- },
174
- },
175
- '&.sizeMedium': {
176
- mt: { xs: '4px', sm: '5px', md: '6px' },
177
- mb: { xs: '5px', sm: '6px', md: '7px' },
178
- '&::after': {
179
- mb: { xs: '-4px', sm: '-5px', md: '-6px' },
180
- },
181
- },
182
- '&.sizeLarge': {
183
- mt: { xs: '5px', sm: '7px', md: '8px' },
184
- mb: { xs: '6px', sm: '8px', md: '9px' },
185
- '&::after': {
186
- mb: { xs: '-5px', sm: '-7px', md: '-8px' },
187
- },
188
- },
182
+ // '&.sizeSmall': {
183
+ // mt: { xs: '2px', sm: '3px', md: '5px' },
184
+ // mb: { xs: '3px', sm: '4px', md: '6px' },
185
+ // '&::after': {
186
+ // mb: { xs: '-2px', sm: '-3px', md: '-5px' },
187
+ // },
188
+ // },
189
+ // '&.sizeMedium': {
190
+ // mt: { xs: '4px', sm: '5px', md: '6px' },
191
+ // mb: { xs: '5px', sm: '6px', md: '7px' },
192
+ // '&::after': {
193
+ // mb: { xs: '-4px', sm: '-5px', md: '-6px' },
194
+ // },
195
+ // },
196
+ // '&.sizeLarge': {
197
+ // mt: { xs: '5px', sm: '7px', md: '8px' },
198
+ // mb: { xs: '6px', sm: '8px', md: '9px' },
199
+ // '&::after': {
200
+ // mb: { xs: '-5px', sm: '-7px', md: '-8px' },
201
+ // },
202
+ // },
203
+ // '&.sizeResponsive': {
204
+ // mt: responsiveVal(2, 8),
205
+ // mb: responsiveVal(3, 9),
206
+ // '&::after': {
207
+ // mb: responsiveVal(-2, -8),
208
+ // },
209
+ // },
189
210
  },
190
211
 
191
212
  '&.variantOutlined': {
@@ -261,6 +282,7 @@ export function ActionCard(props: ActionCardProps) {
261
282
  ]}
262
283
  >
263
284
  <Box
285
+ className={classes.rootInner}
264
286
  sx={{
265
287
  display: 'flex',
266
288
  flexDirection: 'row',
@@ -298,9 +320,10 @@ export function ActionCard(props: ActionCardProps) {
298
320
  <Box
299
321
  className={classes.title}
300
322
  sx={{
301
- '&.sizeSmall': { typography: 'body2' },
323
+ '&.sizeSmall': { typography: 'body1' },
302
324
  '&.sizeMedium': { typography: 'body1' },
303
325
  '&.sizeLarge': { typography: 'h6' },
326
+ '&.sizeResponsive': { typography: { xs: 'body1', md: 'body1', lg: 'body1' } },
304
327
  }}
305
328
  >
306
329
  {title}
@@ -341,8 +364,9 @@ export function ActionCard(props: ActionCardProps) {
341
364
  sx={{
342
365
  textAlign: 'right',
343
366
  typography: 'body1',
344
- '&.sizeMedium': { typographty: 'subtitle1' },
367
+ '&.sizeMedium': { typography: 'subtitle1' },
345
368
  '&.sizeLarge': { typography: 'h6' },
369
+ '&.sizeResponsive': { typography: { xs: 'body1', md: 'subtitle1', lg: 'h6' } },
346
370
  }}
347
371
  >
348
372
  {price}
@@ -29,6 +29,7 @@ export function ActionCardAccordion(props: ActionCardAccordionProps) {
29
29
  '&.Mui-expanded': { my: 0 },
30
30
  '::before': { display: 'none' },
31
31
  border: 'none',
32
+ borderBottom: `1px solid ${theme.palette.divider}`,
32
33
  '&:not(.Mui-expanded)': { borderBottom: `1px solid ${theme.palette.divider}` },
33
34
  }),
34
35
  ...(Array.isArray(sx) ? sx : [sx]),
@@ -39,7 +40,7 @@ export function ActionCardAccordion(props: ActionCardAccordionProps) {
39
40
  expandIcon={<IconSvg src={iconChevronDown} />}
40
41
  sx={{
41
42
  px: 0,
42
- typography: 'h6',
43
+ typography: 'subtitle1',
43
44
  minHeight: 54,
44
45
  '& .MuiAccordionSummary-content': {
45
46
  display: 'flex',
@@ -12,13 +12,13 @@ import { ActionCardLayout } from './ActionCardLayout'
12
12
  type MultiSelect = {
13
13
  multiple: true
14
14
  collapse?: false
15
- value: (string | number | null)[]
15
+ value: (string | boolean | number | null)[]
16
16
 
17
17
  onChange?: (event: React.MouseEvent<HTMLElement>, value: MultiSelect['value']) => void
18
18
  }
19
19
  type Select = {
20
20
  multiple?: boolean
21
- value: string | number | null
21
+ value: string | boolean | number | null
22
22
  collapse?: boolean
23
23
 
24
24
  /** Value is null when deselected when not required */
@@ -44,6 +44,7 @@ function isValueSelected(
44
44
  candidate?: Select['value'] | MultiSelect['value'],
45
45
  ) {
46
46
  if (candidate === undefined) return false
47
+ if (typeof value === 'boolean') return value
47
48
  if (Array.isArray(candidate)) return candidate.indexOf(value) >= 0
48
49
  return value === candidate
49
50
  }
@@ -74,7 +74,7 @@ export function ActionCardListForm<
74
74
  {items.map((item) => (
75
75
  <RenderItem
76
76
  {...item}
77
- key={item.value ?? ''}
77
+ key={`${item.value}`}
78
78
  value={item.value}
79
79
  selected={onSelect(item.value, value)}
80
80
  onReset={(e) => {
@@ -1,6 +1,6 @@
1
1
  import { Avatar, Box, Chip, SxProps, Theme } from '@mui/material'
2
2
  import { responsiveVal } from '../../Styles/responsiveVal'
3
- import { useDateTimeFormat } from '../../hooks'
3
+ import { DateFormat } from '../../Intl/DateTimeFormat'
4
4
 
5
5
  export type BlogAuthorProps = {
6
6
  author: string
@@ -11,8 +11,6 @@ export type BlogAuthorProps = {
11
11
  export function BlogAuthor(props: BlogAuthorProps) {
12
12
  const { author, date, sx = [] } = props
13
13
 
14
- const formatter = useDateTimeFormat({ month: 'long', day: 'numeric' })
15
-
16
14
  return (
17
15
  <Box
18
16
  maxWidth='md'
@@ -48,7 +46,7 @@ export function BlogAuthor(props: BlogAuthorProps) {
48
46
  {author}
49
47
  </Box>
50
48
  <Box sx={(theme) => ({ lineHeight: 1.4, color: theme.palette.text.disabled })}>
51
- {formatter.format(new Date(date))}
49
+ <DateFormat dateStyle='long'>{date}</DateFormat>
52
50
  </Box>
53
51
  </section>
54
52
  }
@@ -2,7 +2,7 @@ import { Box, Link, SxProps, Theme, Typography } from '@mui/material'
2
2
  import React from 'react'
3
3
  import { extendableComponent } from '../../Styles'
4
4
  import { breakpointVal } from '../../Styles/breakpointVal'
5
- import { useDateTimeFormat } from '../../hooks'
5
+ import { DateFormat } from '../../Intl'
6
6
 
7
7
  export type BlogListItemProps = {
8
8
  asset: React.ReactNode
@@ -19,8 +19,6 @@ const { classes } = extendableComponent(name, parts)
19
19
  export function BlogListItem(props: BlogListItemProps) {
20
20
  const { asset, url, date, title, sx = [] } = props
21
21
 
22
- const formatter = useDateTimeFormat({ dateStyle: 'long' })
23
-
24
22
  return (
25
23
  <Box
26
24
  className={classes.item}
@@ -67,7 +65,7 @@ export function BlogListItem(props: BlogListItemProps) {
67
65
  color: theme.palette.text.secondary,
68
66
  })}
69
67
  >
70
- {formatter.format(new Date(date))}
68
+ <DateFormat dateStyle='long'>{date}</DateFormat>
71
69
  </Box>
72
70
  )}
73
71
 
@@ -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/canonicalize'
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
+ }