@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
@@ -1,13 +1,7 @@
1
1
  import { usePageContext } from '@graphcommerce/framer-next-pages'
2
- import { addBasePath } from 'next/dist/client/add-base-path'
3
- import { addLocale } from 'next/dist/client/add-locale'
4
- import { getDomainLocale } from 'next/dist/client/get-domain-locale'
5
- import { resolveHref } from 'next/dist/client/resolve-href'
6
- import { NextRouter } from 'next/dist/shared/lib/router/router'
7
2
  import Head from 'next/head'
8
- import { useRouter } from 'next/router'
9
3
  import type {} from '@graphcommerce/next-config'
10
- import { storefrontConfig } from '../hooks'
4
+ import { Canonical, useCanonical } from './canonicalize'
11
5
 
12
6
  // https://developers.google.com/search/docs/advanced/robots/robots_meta_tag#directives
13
7
  export type MetaRobots =
@@ -23,8 +17,6 @@ export type MetaRobots =
23
17
  | `max-video-preview:${number}`
24
18
  type MetaRobotsAll = ['all' | 'none']
25
19
 
26
- type Canonical = `http://${string}` | `https://${string}` | `/${string}` | string
27
-
28
20
  export type PageMetaProps = {
29
21
  title: string
30
22
  canonical?: Canonical
@@ -36,68 +28,6 @@ export type PageMetaProps = {
36
28
  ogType?: string | null
37
29
  }
38
30
 
39
- type PartialNextRouter = Pick<
40
- NextRouter,
41
- 'pathname' | 'locale' | 'locales' | 'isLocaleDomain' | 'domainLocales' | 'defaultLocale'
42
- >
43
- export function canonicalize(router: PartialNextRouter, incoming?: Canonical) {
44
- let canonical = incoming
45
-
46
- if (!canonical) return canonical
47
-
48
- if (!canonical.startsWith('http') && !canonical.startsWith('/')) {
49
- if (process.env.NODE_ENV !== 'production') {
50
- console.warn(
51
- `canonical is relative (${canonical}), a canonical must start with '/', 'http://' or 'https://'`,
52
- )
53
- }
54
- canonical = `/${canonical}`
55
- }
56
-
57
- if (canonical.startsWith('/')) {
58
- let [href, as = href] = resolveHref(router as NextRouter, canonical, true)
59
-
60
- const curLocale = router.locale
61
-
62
- // Copied from here https://github.com/vercel/next.js/blob/213c42f446874d29d07fa2cca6e6b11fc9c3b711/packages/next/client/link.tsx#L512
63
- const localeDomain = getDomainLocale(
64
- as,
65
- curLocale,
66
- router && router.locales,
67
- router.domainLocales,
68
- )
69
-
70
- if (localeDomain) {
71
- canonical = localeDomain
72
- } else {
73
- href = addBasePath(addLocale(as, curLocale, router.defaultLocale))
74
-
75
- let siteUrl =
76
- storefrontConfig(router.locale)?.canonicalBaseUrl ||
77
- import.meta.graphCommerce.canonicalBaseUrl
78
- if (siteUrl.endsWith('/')) siteUrl = siteUrl.slice(0, -1)
79
-
80
- canonical = `${siteUrl}${href}`
81
- }
82
- }
83
-
84
- if (!canonical.startsWith('http')) {
85
- if (process.env.NODE_ENV !== 'production') {
86
- throw new Error(
87
- `canonical must start with '/', 'http://' or 'https://', '${canonical}' given`,
88
- )
89
- }
90
- canonical = undefined
91
- }
92
-
93
- return canonical
94
- }
95
-
96
- export function useCanonical(incoming?: Canonical) {
97
- const router = useRouter()
98
- return canonicalize(router, incoming)
99
- }
100
-
101
31
  export function PageMeta(props: PageMetaProps) {
102
32
  const { active } = usePageContext()
103
33
  const {
@@ -0,0 +1,78 @@
1
+ import { addBasePath } from 'next/dist/client/add-base-path'
2
+ import { addLocale } from 'next/dist/client/add-locale'
3
+ import { getDomainLocale } from 'next/dist/client/get-domain-locale'
4
+ import { resolveHref } from 'next/dist/client/resolve-href'
5
+ import { NextRouter } from 'next/dist/shared/lib/router/router'
6
+ import type {} from '@graphcommerce/next-config'
7
+ import { useRouter } from 'next/router'
8
+ import { storefrontConfig } from '../utils/storefrontConfig'
9
+
10
+ type PartialNextRouter = Pick<
11
+ NextRouter,
12
+ 'pathname' | 'locale' | 'locales' | 'isLocaleDomain' | 'domainLocales' | 'defaultLocale'
13
+ >
14
+
15
+ export function canonicalize(router: PartialNextRouter, incoming?: Canonical) {
16
+ let canonical = incoming
17
+
18
+ if (!canonical) return canonical
19
+
20
+ if (!canonical.startsWith('http') && !canonical.startsWith('/')) {
21
+ if (process.env.NODE_ENV !== 'production') {
22
+ console.warn(
23
+ `canonical is relative (${canonical}), a canonical must start with '/', 'http://' or 'https://'`,
24
+ )
25
+ }
26
+ canonical = `/${canonical}`
27
+ }
28
+
29
+ if (canonical.startsWith('/')) {
30
+ let [href, as = href] = resolveHref(router as NextRouter, canonical, true)
31
+
32
+ const curLocale = router.locale
33
+
34
+ // Copied from here https://github.com/vercel/next.js/blob/213c42f446874d29d07fa2cca6e6b11fc9c3b711/packages/next/client/link.tsx#L512
35
+ const localeDomain = getDomainLocale(
36
+ as,
37
+ curLocale,
38
+ router && router.locales,
39
+ router.domainLocales,
40
+ )
41
+
42
+ if (localeDomain) {
43
+ canonical = localeDomain
44
+ } else {
45
+ const conf = storefrontConfig(router.locale)
46
+
47
+ href = addBasePath(
48
+ addLocale(as, curLocale, conf?.domain ? conf.locale : router.defaultLocale),
49
+ )
50
+
51
+ let siteUrl = conf?.canonicalBaseUrl || import.meta.graphCommerce.canonicalBaseUrl
52
+
53
+ if (conf?.domain && !conf?.canonicalBaseUrl) siteUrl = `https://${conf.domain}`
54
+
55
+ if (siteUrl.endsWith('/')) siteUrl = siteUrl.slice(0, -1)
56
+
57
+ canonical = `${siteUrl}${href}`
58
+ }
59
+ }
60
+
61
+ if (!canonical.startsWith('http')) {
62
+ if (process.env.NODE_ENV !== 'production') {
63
+ throw new Error(
64
+ `canonical must start with '/', 'http://' or 'https://', '${canonical}' given`,
65
+ )
66
+ }
67
+ canonical = undefined
68
+ }
69
+
70
+ return canonical
71
+ }
72
+
73
+ export type Canonical = `http://${string}` | `https://${string}` | `/${string}` | string
74
+
75
+ export function useCanonical(incoming?: Canonical) {
76
+ const router = useRouter()
77
+ return canonicalize(router, incoming)
78
+ }
@@ -0,0 +1,103 @@
1
+ import {
2
+ PaginationProps,
3
+ Box,
4
+ SxProps,
5
+ Theme,
6
+ Pagination,
7
+ PaginationItem,
8
+ PaginationRenderItemParams,
9
+ } from '@mui/material'
10
+ import { IconSvg } from '../IconSvg'
11
+ import { extendableComponent } from '../Styles'
12
+ import { NextLink } from '../Theme'
13
+ import { iconChevronLeft, iconChevronRight } from '../icons'
14
+
15
+ export type PaginationExtendedProps = {
16
+ count: number
17
+ page: number
18
+ sx?: SxProps<Theme>
19
+ size?: 'small' | 'medium' | 'large'
20
+ paginationHref: (params: PaginationRenderItemParams) => string
21
+ siblingCount?: number
22
+ boundaryCount?: number
23
+ } & Omit<PaginationProps, 'count' | 'url' | 'defaultPage' | 'page'>
24
+
25
+ const parts = ['root', 'button', 'icon'] as const
26
+ const { classes } = extendableComponent('Pagination', parts)
27
+
28
+ function Prev() {
29
+ return <IconSvg src={iconChevronLeft} className={classes.icon} size='medium' />
30
+ }
31
+ function Next() {
32
+ return <IconSvg src={iconChevronRight} className={classes.icon} size='medium' />
33
+ }
34
+
35
+ /**
36
+ * Rel="prev" and rel="next" are deprecated by Google.
37
+ *
38
+ * Read more: https://ahrefs.com/blog/rel-prev-next-pagination/
39
+ */
40
+ export function PaginationExtended(props: PaginationExtendedProps) {
41
+ const {
42
+ count,
43
+ page,
44
+ sx = [],
45
+ size,
46
+ paginationHref,
47
+ renderItem,
48
+ siblingCount,
49
+ boundaryCount,
50
+ } = props
51
+
52
+ return (
53
+ <Box
54
+ className={classes.root}
55
+ sx={[
56
+ (theme) => ({
57
+ margin: '0 auto',
58
+ marginTop: theme.spacings.lg,
59
+ marginBottom: theme.spacings.lg,
60
+ display: 'flex',
61
+ alignItems: 'center',
62
+ justifyContent: 'center',
63
+ gap: '6px',
64
+ '& .Mui-disabled': {
65
+ background: 'none',
66
+ },
67
+ '& .MuiPagination-ul': {
68
+ justifyContent: 'center',
69
+ },
70
+ '& .MuiPaginationItem-root': {
71
+ minWidth: '35px',
72
+ height: '35px',
73
+ padding: 0,
74
+ '&.MuiPaginationItem-previousNext, &.MuiPaginationItem-ellipsis': {
75
+ margin: 0,
76
+ },
77
+ },
78
+ }),
79
+ ...(Array.isArray(sx) ? sx : [sx]),
80
+ ]}
81
+ >
82
+ <Pagination
83
+ count={count}
84
+ defaultPage={page}
85
+ page={page}
86
+ siblingCount={siblingCount ?? 1}
87
+ boundaryCount={boundaryCount ?? 1}
88
+ size={size ?? 'large'}
89
+ renderItem={
90
+ renderItem ??
91
+ ((item) => (
92
+ <PaginationItem
93
+ component={NextLink}
94
+ href={paginationHref(item)}
95
+ slots={{ previous: Prev, next: Next }}
96
+ {...item}
97
+ />
98
+ ))
99
+ }
100
+ />
101
+ </Box>
102
+ )
103
+ }
@@ -9,9 +9,10 @@ import {
9
9
  SxProps,
10
10
  Theme,
11
11
  Portal,
12
+ SvgIconProps,
12
13
  } from '@mui/material'
13
14
  import React, { useEffect, useState } from 'react'
14
- import { IconSvg } from '../IconSvg'
15
+ import { IconSvg, IconSvgProps } from '../IconSvg'
15
16
  import { extendableComponent, breakpointVal } from '../Styles'
16
17
  import { iconClose, iconCheckmark, iconSadFace } from '../icons'
17
18
  import iconInfo from '../icons/info.svg'
@@ -28,6 +29,8 @@ export type MessageSnackbarProps = Omit<
28
29
  children?: React.ReactNode
29
30
  onClose?: () => void
30
31
  sx?: SxProps<Theme>
32
+ disableClose?: boolean
33
+ icon?: IconSvgProps['src']
31
34
  } & OwnerState
32
35
 
33
36
  type OwnerState = {
@@ -39,6 +42,8 @@ type OwnerState = {
39
42
  * Setting this to true allows interaction with the rest of the page without closing the Snackbar
40
43
  */
41
44
  disableBackdropClick?: boolean
45
+ disableClose?: boolean
46
+ disableIcon?: boolean
42
47
  }
43
48
 
44
49
  const name = 'MessageSnackbarImpl' as const
@@ -62,10 +67,21 @@ export default function MessageSnackbarImpl(props: MessageSnackbarProps) {
62
67
  severity = 'info',
63
68
  sx,
64
69
  disableBackdropClick,
70
+ disableClose,
71
+ disableIcon,
72
+ icon,
65
73
  ...snackbarProps
66
74
  } = props
67
75
 
68
- const classes = withState({ sticky, size, severity, variant })
76
+ const classes = withState({
77
+ sticky,
78
+ size,
79
+ severity,
80
+ variant,
81
+ disableBackdropClick,
82
+ disableClose,
83
+ disableIcon,
84
+ })
69
85
 
70
86
  useEffect(() => {
71
87
  setShowSnackbar(!!open)
@@ -89,9 +105,10 @@ export default function MessageSnackbarImpl(props: MessageSnackbarProps) {
89
105
  }
90
106
  }
91
107
 
92
- let icon = iconCheckmark
93
- if (severity === 'info') icon = iconInfo
94
- if (severity === 'error') icon = iconSadFace
108
+ let icon2 = iconCheckmark
109
+ if (severity === 'info') icon2 = iconInfo
110
+ if (severity === 'error') icon2 = iconSadFace
111
+ if (icon) icon2 = icon
95
112
 
96
113
  return (
97
114
  <Portal>
@@ -148,10 +165,32 @@ export default function MessageSnackbarImpl(props: MessageSnackbarProps) {
148
165
  gridArea: 'children',
149
166
  },
150
167
  },
168
+
169
+ '&.disableIcon .MuiSnackbarContent-message': {
170
+ gridTemplate: {
171
+ xs: `"children close"
172
+ "action action"`,
173
+ md: '"children action close"',
174
+ },
175
+ },
176
+ '&.disableClose .MuiSnackbarContent-message': {
177
+ gridTemplate: {
178
+ xs: `"icon children"
179
+ "action action"`,
180
+ md: '"icon children action"',
181
+ },
182
+ },
183
+ '&.disableIcon.disableClose .MuiSnackbarContent-message': {
184
+ gridTemplate: {
185
+ xs: `"children"
186
+ "action"`,
187
+ md: '"children action"',
188
+ },
189
+ },
151
190
  })}
152
191
  message={
153
192
  <>
154
- <IconSvg src={icon} size='large' />
193
+ {!disableIcon && <IconSvg src={icon2} size='large' />}
155
194
  <Box gridArea='children'>{children}</Box>
156
195
  {/* </Box> */}
157
196
  {action && (
@@ -159,19 +198,21 @@ export default function MessageSnackbarImpl(props: MessageSnackbarProps) {
159
198
  {action}
160
199
  </Box>
161
200
  )}
162
- <Fab
163
- className={classes.close}
164
- aria-label={i18n._(/* i18n */ 'Close')}
165
- size='small'
166
- onClick={hideSnackbar}
167
- onMouseDown={preventAnimationBubble}
168
- onTouchStart={preventAnimationBubble}
169
- sx={(theme) => ({
170
- backgroundColor: lighten(theme.palette.background.paper, 0.1),
171
- })}
172
- >
173
- <IconSvg src={iconClose} />
174
- </Fab>
201
+ {!disableClose && (
202
+ <Fab
203
+ className={classes.close}
204
+ aria-label={i18n._(/* i18n */ 'Close')}
205
+ size='small'
206
+ onClick={hideSnackbar}
207
+ onMouseDown={preventAnimationBubble}
208
+ onTouchStart={preventAnimationBubble}
209
+ sx={(theme) => ({
210
+ backgroundColor: lighten(theme.palette.background.paper, 0.1),
211
+ })}
212
+ >
213
+ <IconSvg src={iconClose} />
214
+ </Fab>
215
+ )}
175
216
  </>
176
217
  }
177
218
  />
@@ -6,15 +6,14 @@
6
6
  export function responsiveVal(
7
7
  min: number,
8
8
  max: number,
9
- maxBreakpoint = 1280,
10
- ): `max(${number}px, min(${string}, ${number}px))` {
9
+ minBreakpoint = 320,
10
+ maxBreakpoint = 2560,
11
+ ): `clamp(${number}px, ${string}, ${number}px)` {
11
12
  const round = (x: number, n: number): number => Math.round(x * 10 ** n) / 10 ** n
12
13
 
13
- const minBreakpoint = 320
14
14
  const growth = (max - min) / (maxBreakpoint - minBreakpoint)
15
15
  const base = round(min - growth * minBreakpoint, 2)
16
16
  const vsize = round(growth * 100, 2)
17
17
 
18
- const calc = `(${base}px + ${vsize}vw)`
19
- return `max(${min}px, min(${calc}, ${max}px))`
18
+ return `clamp(${min}px, (${base}px + ${vsize}vw), ${max}px)`
20
19
  }
@@ -1,10 +1,10 @@
1
1
  import type { EmotionJSX } from '@emotion/react/types/jsx-namespace'
2
2
  import createEmotionServer from '@emotion/server/create-instance'
3
- // eslint-disable-next-line @next/next/no-document-import-in-page
4
3
  import { AppType } from 'next/app'
4
+ // eslint-disable-next-line @next/next/no-document-import-in-page
5
5
  import type NextDocument from 'next/document'
6
6
  // eslint-disable-next-line @next/next/no-document-import-in-page
7
- import type { DocumentContext, DocumentInitialProps } from 'next/document'
7
+ import type { DocumentContext } from 'next/document'
8
8
  import { EmotionProviderProps } from './EmotionProvider'
9
9
  import { createEmotionCache } from './createEmotionCache'
10
10
 
@@ -14,6 +14,7 @@ import { useRouter } from 'next/router'
14
14
  import { createContext, useContext, useEffect, useMemo, useState } from 'react'
15
15
  import { IconSvg } from '../IconSvg'
16
16
  import { iconMoon, iconSun } from '../icons'
17
+ import { getCssFlag, setCssFlag } from '../utils/cssFlags'
17
18
 
18
19
  type Mode = 'dark' | 'light'
19
20
  type UserMode = 'auto' | Mode
@@ -22,6 +23,7 @@ type ColorModeContext = {
22
23
  userMode: UserMode
23
24
  browserMode: Mode
24
25
  currentMode: Mode
26
+ isSingleMode: boolean
25
27
  toggle: () => void
26
28
  }
27
29
 
@@ -29,48 +31,59 @@ export const colorModeContext = createContext(undefined as unknown as ColorModeC
29
31
  colorModeContext.displayName = 'ColorModeContext'
30
32
 
31
33
  type ThemeProviderProps = {
32
- // eslint-disable-next-line react/no-unused-prop-types
33
- light: Theme
34
- // eslint-disable-next-line react/no-unused-prop-types
35
- dark: Theme
36
-
37
34
  children: React.ReactNode
38
- }
35
+ ssrMode?: Mode
36
+ listenToBrowser?: boolean
37
+ } & (
38
+ | { light: Theme; dark?: undefined }
39
+ | { light?: undefined; dark: Theme }
40
+ | { light: Theme; dark: Theme }
41
+ )
39
42
 
40
43
  /**
41
44
  * Wrapper around `import { ThemeProvider } from '@mui/material'`
42
45
  *
43
46
  * The multi DarkLightModeThemeProvider allows switching between light and dark mode based on URL
44
47
  * and on user input.
45
- *
46
- * If you _just_ wan't a single theme, use the import { ThemeProvider } from '@mui/material' instead.
47
48
  */
48
49
  export function DarkLightModeThemeProvider(props: ThemeProviderProps) {
49
- const { children, light, dark } = props
50
+ const { children, light, dark, ssrMode = 'light', listenToBrowser = true } = props
51
+ const [configuredMode, setConfiguredMode] = useState<UserMode>(listenToBrowser ? 'auto' : ssrMode)
52
+ const setThemeMode = (mode: UserMode) => {
53
+ setConfiguredMode(mode)
54
+ setCssFlag('color-scheme', mode)
55
+ }
50
56
 
51
- // todo: Save this in local storage
52
- const [userMode, setUserMode] = useState<UserMode>('auto')
53
57
  const browserMode: Mode = useMediaQuery('(prefers-color-scheme: dark)') ? 'dark' : 'light'
54
58
 
55
59
  // If the user has set a mode, use that. Otherwise, use the browser mode.
56
- const currentMode = userMode === 'auto' ? browserMode : userMode
57
- const theme = currentMode === 'light' ? light : dark
60
+ const currentMode = configuredMode === 'auto' ? browserMode : configuredMode
61
+
62
+ let theme: Theme = light || dark // Default
63
+ if (light && currentMode === 'light') theme = light
64
+ else if (dark) theme = dark
65
+
66
+ useEffect(() => {
67
+ const flag = getCssFlag('color-scheme') as Mode
68
+ if (flag) setConfiguredMode(flag)
69
+ }, [setConfiguredMode])
58
70
 
59
71
  // If a URL parameter is present, switch from auto to light or dark mode
60
72
  const { asPath } = useRouter()
61
73
  useEffect(() => {
62
- if (asPath.includes('darkmode')) setUserMode('dark')
74
+ if (asPath.includes('darkmode')) setConfiguredMode('dark')
63
75
  }, [asPath])
64
76
 
65
77
  // Create the context
66
78
  const colorContext: ColorModeContext = useMemo(
67
79
  () => ({
68
80
  browserMode,
69
- userMode,
81
+ userMode: configuredMode,
70
82
  currentMode,
71
- toggle: () => setUserMode(currentMode === 'light' ? 'dark' : 'light'),
83
+ isSingleMode: !light || !dark,
84
+ toggle: () => setThemeMode(currentMode === 'light' ? 'dark' : 'light'),
72
85
  }),
73
- [browserMode, currentMode, userMode],
86
+ [browserMode, configuredMode, currentMode, light, dark],
74
87
  )
75
88
 
76
89
  return (
@@ -85,7 +98,12 @@ export function useColorMode() {
85
98
  }
86
99
 
87
100
  export function DarkLightModeToggleFab(props: Omit<FabProps, 'onClick'>) {
88
- const { currentMode, toggle } = useColorMode()
101
+ const { currentMode, isSingleMode, toggle } = useColorMode()
102
+
103
+ if (isSingleMode) {
104
+ return null
105
+ }
106
+
89
107
  return (
90
108
  <Fab size='large' color='inherit' onClick={toggle} {...props}>
91
109
  <IconSvg src={currentMode === 'light' ? iconMoon : iconSun} size='large' />
@@ -100,7 +118,11 @@ export function DarkLightModeToggleFab(props: Omit<FabProps, 'onClick'>) {
100
118
  */
101
119
  export function DarkLightModeMenuSecondaryItem(props: ListItemButtonProps) {
102
120
  const { sx = [] } = props
103
- const { currentMode, toggle } = useColorMode()
121
+ const { currentMode, isSingleMode, toggle } = useColorMode()
122
+
123
+ if (isSingleMode) {
124
+ return null
125
+ }
104
126
 
105
127
  return (
106
128
  <ListItemButton {...props} sx={[{}, ...(Array.isArray(sx) ? sx : [sx])]} dense onClick={toggle}>
@@ -1,4 +1,4 @@
1
- import { ComponentsVariants, Theme } from '@mui/material'
1
+ import { ComponentsVariants, Theme, alpha, darken, lighten } from '@mui/material'
2
2
  import { responsiveVal } from '../Styles/responsiveVal'
3
3
 
4
4
  declare module '@mui/material/Button/Button' {
@@ -126,7 +126,15 @@ export const MuiButtonPill: ButtonVariants = [
126
126
  },
127
127
  {
128
128
  props: { variant: 'pill', color: 'inherit' },
129
- style: ({ theme }) => ({ backgroundColor: theme.palette.background.paper }),
129
+ style: ({ theme }) => ({
130
+ backgroundColor: theme.palette.background.paper,
131
+ '&:hover:not(.Mui-disabled)': {
132
+ backgroundColor:
133
+ theme.palette.mode === 'light'
134
+ ? darken(theme.palette.background.default, 0.05)
135
+ : lighten(theme.palette.background.default, 0.2),
136
+ },
137
+ }),
130
138
  },
131
139
  {
132
140
  props: { variant: 'pill', disabled: true },
@@ -142,7 +150,7 @@ export const MuiButtonInline: ButtonVariants = [
142
150
  props: { variant: 'inline', color: 'primary' },
143
151
  style: ({ theme }) => ({
144
152
  color: theme.palette.primary.main,
145
- '&:hover:not(.Mui-disabled)': { backgroundColor: `${theme.palette.primary.main}30` },
153
+ '&:hover:not(.Mui-disabled)': { backgroundColor: alpha(theme.palette.primary.main, 0.19) },
146
154
  }),
147
155
  },
148
156
  {
@@ -8,6 +8,9 @@ export type TimeAgoProps = {
8
8
  locale?: string
9
9
  }
10
10
 
11
+ /**
12
+ * @deprecated Use <RelativeToTimeFormat /> instead.
13
+ */
11
14
  export function TimeAgo(props: TimeAgoProps) {
12
15
  const { date } = props
13
16
  const msPerMinute = 60 * 1000
package/hooks/index.ts CHANGED
@@ -5,5 +5,6 @@ export * from './useNumberFormat'
5
5
  export * from './useMemoObject'
6
6
  export * from './useStorefrontConfig'
7
7
  export * from './useUrlQuery'
8
- export * from './useSsr'
8
+ export * from './useIsSsr'
9
9
  export * from './useLocale'
10
+ export * from './memoDeep'
@@ -0,0 +1,38 @@
1
+ // eslint-disable-next-line import/no-extraneous-dependencies
2
+ import { equal } from '@wry/equality'
3
+ import { FunctionComponent, memo, NamedExoticComponent } from 'react'
4
+
5
+ /**
6
+ * This is a deep comparison version of React's `memo` function.
7
+ *
8
+ * This method isn't too expensive to run, but will be rerun every time a parent component is rendered.
9
+ *
10
+ * This should probably only be used as the result of a performance profiling session.
11
+ */
12
+ export function memoDeep<P extends object>(
13
+ Component: FunctionComponent<P>,
14
+ measure: boolean = false,
15
+ ): NamedExoticComponent<P> {
16
+ return memo(
17
+ Component,
18
+ process.env.NODE_ENV === 'development' && measure
19
+ ? (prevProps, nextProps) => {
20
+ const start = performance.now()
21
+ const result = equal(prevProps, nextProps)
22
+ const ms = performance.now() - start
23
+
24
+ if (ms < 0.2) return result
25
+
26
+ console.log(`memoDeep took more than 0.2ms`, {
27
+ result,
28
+ ms,
29
+ Component,
30
+ prevProps,
31
+ nextProps,
32
+ })
33
+
34
+ return result
35
+ }
36
+ : equal,
37
+ )
38
+ }
@@ -1,11 +1,11 @@
1
1
  import { useMemo } from 'react'
2
2
  import { useLocale } from './useLocale'
3
3
 
4
- export type DateTimeFormatProps = Intl.DateTimeFormatOptions
5
-
6
- export function useDateTimeFormat(props?: DateTimeFormatProps) {
4
+ /**
5
+ * @deprecated use <DateFormat/>, <TimeFormat/> or <DateTimeFormat/> instead
6
+ */
7
+ export function useDateTimeFormat(props?: Intl.DateTimeFormatOptions) {
7
8
  const locale = useLocale()
8
-
9
9
  const formatter = useMemo(() => new Intl.DateTimeFormat(locale, props), [locale, props])
10
10
  return formatter
11
11
  }
@@ -2,6 +2,9 @@ import { useSyncExternalStore } from 'react'
2
2
 
3
3
  const emptySubscribe = () => () => {}
4
4
 
5
+ /**
6
+ * This method will return true on the server and during hydration and false after that.
7
+ */
5
8
  export function useIsSSR() {
6
9
  return useSyncExternalStore(
7
10
  emptySubscribe,