@graphcommerce/next-ui 8.1.0-canary.8 → 9.0.0-canary.100

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 (83) hide show
  1. package/ActionCard/ActionCard.tsx +66 -39
  2. package/ActionCard/ActionCardAccordion.tsx +2 -1
  3. package/ActionCard/ActionCardLayout.tsx +1 -1
  4. package/ActionCard/ActionCardList.tsx +11 -10
  5. package/ActionCard/index.ts +0 -1
  6. package/Blog/BlogAuthor/BlogAuthor.tsx +2 -4
  7. package/Blog/BlogListItem/BlogListItem.tsx +2 -4
  8. package/Blog/BlogTags/BlogTag.tsx +4 -1
  9. package/Breadcrumbs/Breadcrumbs.tsx +195 -0
  10. package/Breadcrumbs/BreadcrumbsJsonLd.tsx +13 -0
  11. package/Breadcrumbs/BreadcrumbsList.tsx +101 -0
  12. package/Breadcrumbs/BreadcrumbsPopper.tsx +48 -0
  13. package/Breadcrumbs/index.ts +4 -0
  14. package/Breadcrumbs/jsonLdBreadcrumb.tsx +19 -0
  15. package/Breadcrumbs/types.ts +11 -0
  16. package/CHANGELOG.md +341 -98
  17. package/Config.graphqls +5 -0
  18. package/Document/DocumentBodyEnd.tsx +7 -0
  19. package/Document/DocumentBodyStart.tsx +7 -0
  20. package/Document/DocumentHeadEnd.tsx +7 -0
  21. package/Document/DocumentHeadStart.tsx +7 -0
  22. package/Document/index.ts +4 -0
  23. package/Footer/Footer.tsx +1 -1
  24. package/FramerScroller/SidebarGallery.tsx +54 -27
  25. package/FullPageMessage/FullPageMessage.tsx +1 -0
  26. package/Intl/DateTimeFormat/DateFormat.tsx +10 -0
  27. package/Intl/DateTimeFormat/DateTimeFormat.tsx +20 -0
  28. package/Intl/DateTimeFormat/TimeFormat.tsx +10 -0
  29. package/Intl/DateTimeFormat/index.ts +3 -0
  30. package/Intl/DateTimeFormat/toDate.ts +16 -0
  31. package/Intl/DisplayNames/DisplayNames.tsx +22 -0
  32. package/Intl/DisplayNames/index.ts +1 -0
  33. package/Intl/ListFormat.tsx +33 -0
  34. package/Intl/NumberFormat/CurrencyFormat.tsx +191 -0
  35. package/Intl/NumberFormat/NumberFormat.tsx +24 -0
  36. package/Intl/NumberFormat/PercentFormat.tsx +5 -0
  37. package/Intl/NumberFormat/UnitFormat.tsx +59 -0
  38. package/Intl/NumberFormat/index.ts +4 -0
  39. package/Intl/RelativeTimeFormat/RelativeTimeFormat.tsx +34 -0
  40. package/Intl/RelativeTimeFormat/RelativeToTimeFormat.tsx +35 -0
  41. package/Intl/RelativeTimeFormat/index.ts +2 -0
  42. package/Intl/RelativeTimeFormat/relativeTimeFormatAutoUnit.ts +23 -0
  43. package/Intl/index.ts +11 -0
  44. package/JsonLd/JsonLd.tsx +3 -2
  45. package/Layout/components/LayoutHeader.tsx +30 -10
  46. package/Layout/components/LayoutHeaderBack.tsx +20 -9
  47. package/Layout/components/LayoutHeaderClose.tsx +8 -2
  48. package/Layout/components/LayoutTitle.tsx +0 -5
  49. package/LayoutDefault/components/LayoutDefault.tsx +15 -13
  50. package/LazyHydrate/LazyHydrate.tsx +19 -3
  51. package/Overlay/components/OverlayBase.tsx +31 -6
  52. package/Overlay/components/OverlayHeader.tsx +27 -0
  53. package/Overlay/components/index.ts +1 -0
  54. package/PageLoadIndicator/PageLoadIndicator.tsx +25 -14
  55. package/PageMeta/PageMeta.tsx +1 -71
  56. package/PageMeta/canonicalize.ts +78 -0
  57. package/Pagination/PaginationExtended.tsx +103 -0
  58. package/Snackbar/MessageSnackbarImpl.tsx +60 -19
  59. package/Styles/responsiveVal.tsx +4 -5
  60. package/Styles/withEmotionCache.tsx +2 -2
  61. package/Theme/DarkLightModeThemeProvider.tsx +41 -19
  62. package/Theme/MuiButton.ts +11 -3
  63. package/TimeAgo/TimeAgo.tsx +3 -0
  64. package/hooks/index.ts +2 -1
  65. package/hooks/memoDeep.ts +38 -0
  66. package/hooks/useDateTimeFormat.ts +4 -4
  67. package/hooks/{useSsr.ts → useIsSsr.ts} +3 -0
  68. package/hooks/useLocale.ts +2 -2
  69. package/hooks/useNumberFormat.ts +4 -3
  70. package/hooks/useStorefrontConfig.ts +1 -9
  71. package/hooks/useUrlQuery.ts +6 -4
  72. package/icons.ts +55 -0
  73. package/index.ts +11 -2
  74. package/package.json +14 -14
  75. package/server.ts +9 -0
  76. package/utils/cssFlags.tsx +53 -0
  77. package/utils/getCssFlagInitScript.tsx +20 -0
  78. package/utils/normalizeLocale.ts +26 -0
  79. package/utils/robots.ts +41 -0
  80. package/utils/sitemap.ts +47 -0
  81. package/utils/storefrontConfig.ts +8 -0
  82. package/ActionCard/ActionCardListForm.tsx +0 -88
  83. package/icons/index.ts +0 -48
@@ -0,0 +1,34 @@
1
+ import { forwardRef, useMemo } from 'react'
2
+ import { useLocale } from '../../hooks/useLocale'
3
+ import { useMemoObject } from '../../hooks/useMemoObject'
4
+ import { relativeTimeFormatUnitAuto } from './relativeTimeFormatAutoUnit'
5
+
6
+ export function useRelativeTimeFormatter(props: Intl.RelativeTimeFormatOptions) {
7
+ const locale = useLocale()
8
+ const memoOptions = useMemoObject(props)
9
+ return useMemo(() => new Intl.RelativeTimeFormat(locale, memoOptions), [locale, memoOptions])
10
+ }
11
+
12
+ export type RelativeTimeFormatProps = {
13
+ children: number
14
+ unit?: Intl.RelativeTimeFormatUnit
15
+ styleFormat?: Intl.RelativeTimeFormatStyle
16
+ } & Omit<Intl.RelativeTimeFormatOptions, 'style'>
17
+
18
+ /**
19
+ * Alternative: {@link file://./RelativeToTimeFormat.tsx}
20
+ */
21
+ export const RelativeTimeFormat = forwardRef<HTMLSpanElement, RelativeTimeFormatProps>(
22
+ (props, ref) => {
23
+ const { children, unit, styleFormat, localeMatcher, numeric, ...rest } = props
24
+ const formatter = useRelativeTimeFormatter({ localeMatcher, numeric, style: styleFormat })
25
+
26
+ const [value, autoUnit] = relativeTimeFormatUnitAuto({ value: children, unit })
27
+
28
+ return (
29
+ <span suppressHydrationWarning ref={ref} {...rest}>
30
+ {children ? formatter.format(value, autoUnit) : null}
31
+ </span>
32
+ )
33
+ },
34
+ )
@@ -0,0 +1,35 @@
1
+ import { forwardRef, useMemo } from 'react'
2
+ import { DateValue, toDate } from '../DateTimeFormat/toDate'
3
+ import { RelativeTimeFormat, RelativeTimeFormatProps } from './RelativeTimeFormat'
4
+
5
+ type RelativeToTimeFormatProps = Omit<RelativeTimeFormatProps, 'children'> & {
6
+ /**
7
+ * Date to format a relative value for.
8
+ */
9
+ children: DateValue
10
+ /**
11
+ * If provided, the component will format a relative value to this date.
12
+ * Else, it will format a relative value to the current date.
13
+ */
14
+ to?: DateValue
15
+ }
16
+
17
+ export const RelativeToTimeFormat = forwardRef<HTMLSpanElement, RelativeToTimeFormatProps>(
18
+ (props, ref) => {
19
+ const { children, to, ...rest } = props
20
+
21
+ const relativeTo = useMemo(() => {
22
+ const date = toDate(children)
23
+ if (!date) return 0
24
+ const toDateValue = (to && toDate(to)) || new Date()
25
+
26
+ return Math.round((date.getTime() - toDateValue.getTime()) / 1000)
27
+ }, [children, to])
28
+
29
+ return (
30
+ <RelativeTimeFormat {...rest} ref={ref}>
31
+ {relativeTo}
32
+ </RelativeTimeFormat>
33
+ )
34
+ },
35
+ )
@@ -0,0 +1,2 @@
1
+ export * from './RelativeTimeFormat'
2
+ export * from './RelativeToTimeFormat'
@@ -0,0 +1,23 @@
1
+ type UseRelativeTimeFormatUnitAutoProps = {
2
+ value: number
3
+ unit?: Intl.RelativeTimeFormatUnit
4
+ }
5
+
6
+ export function relativeTimeFormatUnitAuto(
7
+ props: UseRelativeTimeFormatUnitAutoProps,
8
+ ): [number, Intl.RelativeTimeFormatUnit] {
9
+ const { value, unit } = props
10
+
11
+ if (unit) return [value, unit]
12
+
13
+ // Calculate the absolute value once
14
+ const absValue = Math.abs(value)
15
+
16
+ if (absValue >= 60 * 60 * 24 * 365) return [Math.round(value / (60 * 60 * 24 * 365)), 'year']
17
+ if (absValue >= 60 * 60 * 24 * 30) return [Math.round(value / (60 * 60 * 24 * 30)), 'month']
18
+ if (absValue >= 60 * 60 * 24 * 7) return [Math.round(value / (60 * 60 * 24 * 7)), 'week']
19
+ if (absValue >= 60 * 60 * 24) return [Math.round(value / (60 * 60 * 24)), 'day']
20
+ if (absValue >= 60 * 60) return [Math.round(value / (60 * 60)), 'hour']
21
+ if (absValue >= 60) return [Math.round(value / 60), 'minute']
22
+ return [Math.round(value), 'second']
23
+ }
package/Intl/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator
2
+ // export * from './Collator' // Intl.Collator
3
+
4
+ export * from './DisplayNames' // Intl.DisplayNames
5
+ export * from './DateTimeFormat' // Intl.DateTimeFormat
6
+ export * from './ListFormat' // Intl.ListFormat
7
+ // export * from './Locale' // Intl.Locale
8
+ export * from './NumberFormat' // Intl.NumberFormat
9
+ // export * from './PluralRules' // Intl.PluralRules
10
+ export * from './RelativeTimeFormat' // Intl.RelativeTimeFormat
11
+ // export * from './Segmenter' // Intl.Segmenter
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) }}
@@ -1,13 +1,14 @@
1
1
  import { Box, SxProps, Theme } from '@mui/material'
2
2
  import React from 'react'
3
3
  import { extendableComponent } from '../../Styles'
4
- import { LayoutHeaderBack, useShowBack } from './LayoutHeaderBack'
4
+ import { BackProps, LayoutHeaderBack, useShowBack } from './LayoutHeaderBack'
5
5
  import { LayoutHeaderClose, useShowClose } from './LayoutHeaderClose'
6
6
  import { LayoutHeaderContent, LayoutHeaderContentProps } from './LayoutHeaderContent'
7
7
  import { FloatingProps } from './LayoutHeadertypes'
8
8
 
9
9
  export type LayoutHeaderProps = FloatingProps &
10
- Omit<LayoutHeaderContentProps, 'left' | 'right'> & {
10
+ Omit<LayoutHeaderContentProps, 'left' | 'right'> &
11
+ Pick<BackProps, 'disableBackNavigation'> & {
11
12
  /**
12
13
  * Button to display on the left side of the title
13
14
  *
@@ -28,6 +29,9 @@ export type LayoutHeaderProps = FloatingProps &
28
29
  sx?: SxProps<Theme>
29
30
 
30
31
  hideBackButton?: boolean
32
+
33
+ hideSm?: boolean | null
34
+ hideMd?: boolean | null
31
35
  }
32
36
 
33
37
  type ComponentStyleProps = {
@@ -36,15 +40,16 @@ type ComponentStyleProps = {
36
40
  children: boolean
37
41
  floatingSm: boolean
38
42
  floatingMd: boolean
43
+ hideSm: boolean
44
+ hideMd: boolean
39
45
  size: 'small' | 'responsive'
40
46
  }
41
47
 
42
- const { selectors, withState } = extendableComponent<ComponentStyleProps, 'LayoutHeader'>(
43
- 'LayoutHeader',
44
- ['root'] as const,
45
- )
48
+ const { withState } = extendableComponent<ComponentStyleProps, 'LayoutHeader'>('LayoutHeader', [
49
+ 'root',
50
+ ] as const)
46
51
 
47
- export function LayoutHeader(props: LayoutHeaderProps) {
52
+ export const LayoutHeader = React.memo<LayoutHeaderProps>((props) => {
48
53
  const {
49
54
  children,
50
55
  divider,
@@ -56,6 +61,9 @@ export function LayoutHeader(props: LayoutHeaderProps) {
56
61
  size = 'responsive',
57
62
  sx = [],
58
63
  bgColor,
64
+ hideSm = false,
65
+ hideMd = false,
66
+ disableBackNavigation,
59
67
  } = props
60
68
  const showBack = useShowBack() && !hideBackButton
61
69
  const showClose = useShowClose()
@@ -69,7 +77,12 @@ export function LayoutHeader(props: LayoutHeaderProps) {
69
77
  if (divider || primary || secondary) floatingSm = false
70
78
 
71
79
  const close = showClose && <LayoutHeaderClose />
72
- const back = showBack && <LayoutHeaderBack breakpoint={floatingSm ? 'xs' : undefined} />
80
+ const back = showBack && (
81
+ <LayoutHeaderBack
82
+ breakpoint={floatingSm ? 'xs' : undefined}
83
+ disableBackNavigation={disableBackNavigation}
84
+ />
85
+ )
73
86
 
74
87
  let left = secondary
75
88
  let right = primary
@@ -88,6 +101,8 @@ export function LayoutHeader(props: LayoutHeaderProps) {
88
101
  children: !!children,
89
102
  divider: !!divider,
90
103
  size,
104
+ hideSm: !!hideSm,
105
+ hideMd: !!hideMd,
91
106
  })
92
107
 
93
108
  return (
@@ -115,6 +130,9 @@ export function LayoutHeader(props: LayoutHeaderProps) {
115
130
  '&.divider': {
116
131
  marginBottom: 0,
117
132
  },
133
+ '&.hideSm .LayoutHeaderContent-left': {
134
+ display: 'none',
135
+ },
118
136
  },
119
137
 
120
138
  [theme.breakpoints.up('md')]: {
@@ -140,6 +158,9 @@ export function LayoutHeader(props: LayoutHeaderProps) {
140
158
  '&.divider': {
141
159
  marginBottom: 0,
142
160
  },
161
+ '&.hideMd .LayoutHeaderContent-left': {
162
+ display: 'none',
163
+ },
143
164
  },
144
165
  }),
145
166
  ...(Array.isArray(sx) ? sx : [sx]),
@@ -159,5 +180,4 @@ export function LayoutHeader(props: LayoutHeaderProps) {
159
180
  </LayoutHeaderContent>
160
181
  </Box>
161
182
  )
162
- }
163
- LayoutHeader.selectors = selectors
183
+ })
@@ -1,4 +1,9 @@
1
- import { useUp, usePrevUp, usePageContext } from '@graphcommerce/framer-next-pages'
1
+ import {
2
+ useUp,
3
+ usePrevUp,
4
+ usePageContext,
5
+ usePrevPageRouter,
6
+ } from '@graphcommerce/framer-next-pages'
2
7
  import { i18n } from '@lingui/core'
3
8
  import { Box, SxProps, Theme } from '@mui/material'
4
9
  import { useRouter } from 'next/router'
@@ -7,7 +12,12 @@ import { IconSvg } from '../../IconSvg'
7
12
  import { responsiveVal } from '../../Styles'
8
13
  import { iconChevronLeft } from '../../icons'
9
14
 
10
- export type BackProps = Omit<LinkOrButtonProps, 'onClick' | 'children'>
15
+ export type BackProps = Omit<LinkOrButtonProps, 'onClick' | 'children'> & {
16
+ /**
17
+ * Will not use `router.back()` if available, and will always use the `up.href`
18
+ */
19
+ disableBackNavigation?: boolean
20
+ }
11
21
 
12
22
  export function useShowBack() {
13
23
  const path = useRouter().asPath.split('?')[0]
@@ -16,10 +26,7 @@ export function useShowBack() {
16
26
  const { backSteps } = usePageContext()
17
27
 
18
28
  const canClickBack = backSteps > 0 && path !== prevUp?.href
19
-
20
- if (canClickBack) return true
21
- if (up?.href && up.href !== path) return true
22
- return false
29
+ return canClickBack || (up?.href && up.href !== path)
23
30
  }
24
31
 
25
32
  const buttonSx: SxProps<Theme> = (theme) => ({
@@ -36,19 +43,23 @@ const buttonSx: SxProps<Theme> = (theme) => ({
36
43
  })
37
44
 
38
45
  export function LayoutHeaderBack(props: BackProps) {
46
+ const { disableBackNavigation = false, ...rest } = props
39
47
  const router = useRouter()
40
48
  const path = router.asPath.split('?')[0]
41
49
  const up = useUp()
42
50
  const prevUp = usePrevUp()
43
51
  const { backSteps } = usePageContext()
52
+ const prevPageRouter = usePrevPageRouter()
44
53
 
45
54
  const backIcon = <IconSvg src={iconChevronLeft} size='medium' />
46
- const canClickBack = backSteps > 0 && path !== prevUp?.href
55
+ const canClickBack = backSteps > 0 && path !== prevUp?.href && !disableBackNavigation
47
56
 
48
57
  let label = i18n._(/* i18n */ 'Back')
49
58
  if (up?.href === path && up?.title) label = up.title
50
59
  if (prevUp?.href === path && prevUp?.title) label = prevUp.title
51
60
 
61
+ if (up && prevPageRouter?.asPath === up?.href) label = up.title
62
+
52
63
  if (canClickBack) {
53
64
  return (
54
65
  <LinkOrButton
@@ -57,7 +68,7 @@ export function LayoutHeaderBack(props: BackProps) {
57
68
  color='inherit'
58
69
  startIcon={backIcon}
59
70
  aria-label={label}
60
- {...props}
71
+ {...rest}
61
72
  >
62
73
  <Box component='span' sx={{ display: { xs: 'none', md: 'inline' } }}>
63
74
  {label}
@@ -74,7 +85,7 @@ export function LayoutHeaderBack(props: BackProps) {
74
85
  startIcon={backIcon}
75
86
  aria-label={up.title}
76
87
  color='inherit'
77
- {...props}
88
+ {...rest}
78
89
  >
79
90
  <Box component='span' sx={{ display: { xs: 'none', md: 'inline' } }}>
80
91
  {up.title}
@@ -6,18 +6,24 @@ import { IconSvg, useIconSvgSize } from '../../IconSvg'
6
6
  import { useFabSize } from '../../Theme'
7
7
  import { iconClose } from '../../icons'
8
8
 
9
+ type LayoutHeaderCloseProps = {
10
+ onClose?: () => void
11
+ }
12
+
9
13
  export function useShowClose() {
10
14
  const { overlayGroup } = usePageContext()
11
15
  return !!overlayGroup
12
16
  }
13
17
 
14
- export function LayoutHeaderClose() {
18
+ export function LayoutHeaderClose(props: LayoutHeaderCloseProps) {
19
+ const { onClose } = props
15
20
  const { closeSteps } = usePageContext()
16
21
  const [disabled, setDisabled] = useState(false)
17
22
  const go = useGo(closeSteps * -1)
18
23
  const onClick = () => {
19
24
  setDisabled(true)
20
- go()
25
+
26
+ return onClose ? onClose() : go()
21
27
  }
22
28
 
23
29
  const fabSize = useFabSize('responsive')
@@ -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),
@@ -129,19 +129,21 @@ export function LayoutDefault(props: LayoutDefaultProps) {
129
129
  })}
130
130
  >
131
131
  {menuFab}
132
- <Box
133
- sx={(theme) => ({
134
- display: 'flex',
135
- flexDirection: 'row-reverse',
136
- gap: theme.spacings.sm,
137
- [theme.breakpoints.up('md')]: {
138
- flexDirection: 'column',
139
- alignItems: 'flex-end',
140
- },
141
- })}
142
- >
143
- {cartFab}
144
- </Box>
132
+ {cartFab && (
133
+ <Box
134
+ sx={(theme) => ({
135
+ display: 'flex',
136
+ flexDirection: 'row-reverse',
137
+ gap: theme.spacings.sm,
138
+ [theme.breakpoints.up('md')]: {
139
+ flexDirection: 'column',
140
+ alignItems: 'flex-end',
141
+ },
142
+ })}
143
+ >
144
+ {cartFab}
145
+ </Box>
146
+ )}
145
147
  </Box>
146
148
  ) : (
147
149
  <div />
@@ -1,5 +1,12 @@
1
1
  import { Box, BoxProps } from '@mui/material'
2
- import React, { useState, useRef, startTransition, useLayoutEffect, useEffect } from 'react'
2
+ import React, {
3
+ useState,
4
+ useRef,
5
+ startTransition,
6
+ useLayoutEffect,
7
+ useEffect,
8
+ CSSProperties,
9
+ } from 'react'
3
10
 
4
11
  // Make sure the server doesn't choke on the useLayoutEffect
5
12
  export const useLayoutEffect2 = typeof window !== 'undefined' ? useLayoutEffect : useEffect
@@ -18,6 +25,14 @@ export type LazyHydrateProps = BoxProps<'div'> & {
18
25
  * - Hydrate the component on some state `<LazyHydrate hydrated={someState}>` where someState initially is false and later becomes true.
19
26
  */
20
27
  hydrated?: boolean
28
+
29
+ /**
30
+ * By default LazyHydrate does not defer the rendering of components when they are rendered client
31
+ * side, because using an IntersectionObserver on an element with no height, will cause all siblings to render at once.
32
+ *
33
+ * By proving a height, we can use the IntersectionObserver on the client as well.
34
+ */
35
+ height?: CSSProperties['height']
21
36
  }
22
37
 
23
38
  /**
@@ -26,7 +41,7 @@ export type LazyHydrateProps = BoxProps<'div'> & {
26
41
  * This can be a way to improve the TBT of a page.
27
42
  */
28
43
  export function LazyHydrate(props: LazyHydrateProps) {
29
- const { hydrated, children, ...elementProps } = props
44
+ const { hydrated, children, height, ...elementProps } = props
30
45
  const rootRef = useRef<HTMLDivElement>(null)
31
46
 
32
47
  const [isHydrated, setIsHydrated] = useState(hydrated || false)
@@ -37,7 +52,7 @@ export function LazyHydrate(props: LazyHydrateProps) {
37
52
  if (isHydrated || !rootRef.current) return undefined
38
53
 
39
54
  // If the element wasn't rendered on the server, we hydrate it immediately
40
- if (!rootRef.current?.hasAttribute('data-lazy-hydrate')) {
55
+ if (!height && !rootRef.current?.hasAttribute('data-lazy-hydrate')) {
41
56
  setIsHydrated(true)
42
57
  return undefined
43
58
  }
@@ -77,6 +92,7 @@ export function LazyHydrate(props: LazyHydrateProps) {
77
92
  dangerouslySetInnerHTML={{ __html: '' }}
78
93
  suppressHydrationWarning
79
94
  {...elementProps}
95
+ style={{ ...elementProps.style, height }}
80
96
  />
81
97
  )
82
98
  }
@@ -1,3 +1,4 @@
1
+ import { Direction } from '@graphcommerce/framer-next-pages'
1
2
  import { Scroller, useScrollerContext, useScrollTo } from '@graphcommerce/framer-scroller'
2
3
  import {
3
4
  dvh,
@@ -46,7 +47,7 @@ export type LayoutOverlayBaseProps = {
46
47
  sx?: SxProps<Theme>
47
48
  sxBackdrop?: SxProps<Theme>
48
49
  active: boolean
49
- direction?: 1 | -1
50
+ direction?: Direction
50
51
  onClosed: () => void
51
52
  offsetPageY?: number
52
53
  isPresent: boolean
@@ -123,7 +124,8 @@ export function OverlayBase(incomingProps: LayoutOverlayBaseProps) {
123
124
  props.smSpacingTop ?? ((theme) => `calc(${theme.appShell.headerHeightSm} * 0.5)`)
124
125
  )(th)
125
126
 
126
- const { scrollerRef, snap, scroll, getScrollSnapPositions, disableSnap } = useScrollerContext()
127
+ const { scrollerRef, snap, scroll, getScrollSnapPositions, disableSnap, enableSnap } =
128
+ useScrollerContext()
127
129
  const scrollTo = useScrollTo()
128
130
 
129
131
  const beforeRef = useRef<HTMLDivElement>(null)
@@ -137,7 +139,7 @@ export function OverlayBase(incomingProps: LayoutOverlayBaseProps) {
137
139
 
138
140
  const match = useMatchMedia()
139
141
  const positions = useConstant(() => ({
140
- open: { x: motionValue(0), y: motionValue(0), visible: motionValue(0) },
142
+ open: { x: motionValue(0), y: motionValue(0), visible: motionValue(direction === 0 ? 1 : 0) },
141
143
  closed: { x: motionValue(0), y: motionValue(0) },
142
144
  }))
143
145
 
@@ -277,17 +279,40 @@ export function OverlayBase(incomingProps: LayoutOverlayBaseProps) {
277
279
  useIsomorphicLayoutEffect(() => {
278
280
  const scroller = scrollerRef.current
279
281
 
280
- if (!scroller || !isPresent) return
282
+ if (!scroller || !isPresent || position.get() === OverlayPosition.OPENED) return
281
283
 
282
284
  if (variant() === 'right') document.body.style.overflow = 'hidden'
283
285
 
284
- if (position.get() !== OverlayPosition.OPENED && !scroll.animating.get()) {
286
+ if (direction === 0) {
287
+ disableSnap()
288
+ scroller.scrollTop = positions.open.y.get()
289
+ scroller.scrollLeft = positions.open.x.get()
290
+ scroll.y.set(positions.open.y.get())
291
+ scroll.x.set(positions.open.x.get())
292
+ position.set(OverlayPosition.OPENED)
293
+ enableSnap()
294
+ } else if (!scroll.animating.get()) {
285
295
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
286
296
  scrollTo(openClosePositions().open, { stopAnimationOnScroll: false }).then(() =>
287
297
  position.set(OverlayPosition.OPENED),
288
298
  )
289
299
  }
290
- }, [isPresent, openClosePositions, position, scroll.animating, scrollTo, scrollerRef, variant])
300
+ }, [
301
+ direction,
302
+ disableSnap,
303
+ enableSnap,
304
+ isPresent,
305
+ openClosePositions,
306
+ position,
307
+ positions.open.x,
308
+ positions.open.y,
309
+ scroll.animating,
310
+ scroll.x,
311
+ scroll.y,
312
+ scrollTo,
313
+ scrollerRef,
314
+ variant,
315
+ ])
291
316
 
292
317
  // When the overlay is closed by navigating away, we're closing the overlay.
293
318
  useEffect(() => {
@@ -0,0 +1,27 @@
1
+ import { LayoutHeaderProps, LayoutTitle, TitleProps } from '../../Layout'
2
+ import { LayoutHeaderClose } from '../../Layout/components/LayoutHeaderClose'
3
+ import { LayoutOverlayHeader } from '../../LayoutOverlay/components/LayoutOverlayHeader'
4
+
5
+ type OverlayHeaderProps = Omit<LayoutHeaderProps, 'hideBackButton' | 'switchPoint'> &
6
+ Pick<TitleProps, 'icon'> & { onClose: () => void }
7
+
8
+ export const OverlayHeader = (props: OverlayHeaderProps) => {
9
+ const { children, onClose, sx = [], icon, primary, secondary, ...rest } = props
10
+
11
+ return (
12
+ <LayoutOverlayHeader
13
+ noAlign
14
+ sx={[{ '&.noAlign': { mb: 0 } }, ...(Array.isArray(sx) ? sx : [sx])]}
15
+ switchPoint={-10000}
16
+ size='small'
17
+ hideBackButton
18
+ primary={primary ?? <LayoutHeaderClose onClose={onClose} />}
19
+ secondary={primary ? <LayoutHeaderClose onClose={onClose} /> : secondary}
20
+ {...rest}
21
+ >
22
+ <LayoutTitle size='small' component='span' icon={icon}>
23
+ {children}
24
+ </LayoutTitle>
25
+ </LayoutOverlayHeader>
26
+ )
27
+ }
@@ -1,3 +1,4 @@
1
1
  export * from './OverlayBase'
2
2
  export * from './Overlay'
3
3
  export * from './OverlayStickyBottom'
4
+ export * from './OverlayHeader'
@@ -1,6 +1,15 @@
1
- import { LinearProgress, Fade } from '@mui/material'
1
+ import { LinearProgress, LinearProgressProps } from '@mui/material'
2
+ import { m, motionValue, useTransform } from 'framer-motion'
2
3
  import { useRouter } from 'next/router'
3
- import { useEffect, useState } from 'react'
4
+ import { forwardRef, useEffect, useState } from 'react'
5
+
6
+ export const showPageLoadIndicator = motionValue(false)
7
+
8
+ const MLinearProgress = m(
9
+ forwardRef((props: Omit<LinearProgressProps, 'style'>, ref: LinearProgressProps['ref']) => (
10
+ <LinearProgress ref={ref} {...props} />
11
+ )),
12
+ )
4
13
 
5
14
  /**
6
15
  * Creates a [LinearProgress](https://mui.com/components/progress/#linear) animation when the route
@@ -24,18 +33,20 @@ export function PageLoadIndicator() {
24
33
  }
25
34
  }, [events])
26
35
 
36
+ const opacity = useTransform(() => (showPageLoadIndicator.get() || loading ? 1 : 0))
37
+
27
38
  return (
28
- <Fade in={loading}>
29
- <LinearProgress
30
- sx={{
31
- position: 'fixed',
32
- width: '100%',
33
- top: 0,
34
- height: 3,
35
- marginBottom: '-3px',
36
- zIndex: 'tooltip',
37
- }}
38
- />
39
- </Fade>
39
+ <MLinearProgress
40
+ style={{ opacity }}
41
+ sx={{
42
+ position: 'fixed',
43
+ width: '100%',
44
+ top: 0,
45
+ height: 3,
46
+ marginBottom: '-3px',
47
+ zIndex: 'tooltip',
48
+ transition: 'opacity 0.3s',
49
+ }}
50
+ />
40
51
  )
41
52
  }