@graphcommerce/next-ui 10.1.0-canary.7 → 11.0.0-canary.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,124 @@
1
1
  # Change Log
2
2
 
3
+ ## 11.0.0-canary.45
4
+
5
+ ## 10.1.0-canary.44
6
+
7
+ ## 10.1.0-canary.43
8
+
9
+ ## 10.1.0-canary.42
10
+
11
+ ## 10.1.0-canary.41
12
+
13
+ ## 10.1.0-canary.40
14
+
15
+ ## 10.1.0-canary.39
16
+
17
+ ### Minor Changes
18
+
19
+ - [#2652](https://github.com/graphcommerce-org/graphcommerce/pull/2652) [`132d1e1`](https://github.com/graphcommerce-org/graphcommerce/commit/132d1e1075b22843ed5934314c723df06ea13a6a) - `HeroBanner` gains an `asset?: React.ReactNode` prop, and `videoSrc` is deprecated.
20
+
21
+ The banner rendered a `<video>` itself, from a raw URL — so it could only ever hold a video, and left nowhere to hang a poster, the one place a poster matters most (a full-bleed autoplaying video above the fold). It now takes a node and renders it, the way its sibling `SpecialBanner` already does; positioning stays with the banner, stretching whatever is passed to fill via `& img, & video`.
22
+
23
+ ```diff
24
+ - <HeroBanner videoSrc={asset.filename} … />
25
+ + <HeroBanner asset={<Asset asset={asset} poster={poster} />} … />
26
+ ```
27
+
28
+ Taking a node rather than a source also keeps `next-ui` free of any CMS: the Storyblok and Hygraph examples each pass their own `<Asset>`, both of which already render images and video.
29
+
30
+ `videoSrc` still works but is deprecated: when set (and `asset` is not) it renders a bare autoplaying video, keeping the `HeroBanner-video` class. The only behavioural change on that path is that the scroll parallax is gone, along with the `framer-motion`, `useScrollY` and `clientSize` machinery it needed. Migrate to `asset` to regain images, posters, and control over how the media is rendered. ([@bramvanderholst](https://github.com/bramvanderholst))
31
+
32
+ ## 10.1.0-canary.38
33
+
34
+ ## 10.1.0-canary.37
35
+
36
+ ## 10.1.0-canary.36
37
+
38
+ ## 10.1.0-canary.35
39
+
40
+ ## 10.1.0-canary.34
41
+
42
+ ## 10.1.0-canary.33
43
+
44
+ ## 10.1.0-canary.32
45
+
46
+ ## 10.1.0-canary.31
47
+
48
+ ## 10.1.0-canary.30
49
+
50
+ ### Patch Changes
51
+
52
+ - [#2643](https://github.com/graphcommerce-org/graphcommerce/pull/2643) [`04ba9be`](https://github.com/graphcommerce-org/graphcommerce/commit/04ba9be6f050cc686133bec57f40da9f8eecb4c8) - Only render disabled pagination items as links when enabled. Disabled `PaginationItem`s no longer receive a `component`/`href`, preventing crawlers from following links to an infinite number of list pages and inflating the static page cache. ([@Giovanni-Schroevers](https://github.com/Giovanni-Schroevers))
53
+
54
+ ## 10.1.0-canary.29
55
+
56
+ ## 10.1.0-canary.28
57
+
58
+ ## 10.1.0-canary.27
59
+
60
+ ## 10.1.0-canary.26
61
+
62
+ ## 10.1.0-canary.25
63
+
64
+ ### Minor Changes
65
+
66
+ - [#2637](https://github.com/graphcommerce-org/graphcommerce/pull/2637) [`bdaa6ec`](https://github.com/graphcommerce-org/graphcommerce/commit/bdaa6ec6aa2669b74fc6702ad46336db4c154b7c) - Refactored `LayoutNavigation` into composable pieces (`Header`, `HeaderContainer`, `MenuOverlay`, project-local `LayoutDefault`). `LayoutDefault` / `LayoutDefaultProps` in `@graphcommerce/next-ui` are marked `@deprecated`. If you are upgrading and do not want these changes, you can just discard them. This is just a structural change for more ease of use. No visually change. ([@bramvanderholst](https://github.com/bramvanderholst))
67
+
68
+ ### Patch Changes
69
+
70
+ - [#2637](https://github.com/graphcommerce-org/graphcommerce/pull/2637) [`9f5e765`](https://github.com/graphcommerce-org/graphcommerce/commit/9f5e76575e3932f8fcd8689d8ef42e4c44b923d1) - Added disableScrollEffects prop to CartFab & NavigationFab for easier customization of the header ([@bramvanderholst](https://github.com/bramvanderholst))
71
+
72
+ - [#2637](https://github.com/graphcommerce-org/graphcommerce/pull/2637) [`21a676b`](https://github.com/graphcommerce-org/graphcommerce/commit/21a676bb50888278d437b88a6e49e6f3d0c4d4bb) - Changed Footer props type to allow setting footer container props ([@bramvanderholst](https://github.com/bramvanderholst))
73
+
74
+ ## 10.1.0-canary.24
75
+
76
+ ## 10.1.0-canary.23
77
+
78
+ ## 10.1.0-canary.22
79
+
80
+ ### Minor Changes
81
+
82
+ - [#2627](https://github.com/graphcommerce-org/graphcommerce/pull/2627) [`95c188f`](https://github.com/graphcommerce-org/graphcommerce/commit/95c188fcd0dc6cb4ca3edc9877d203d45fe18bb0) - Add `YoutubeEmbed` component — a lightweight lazy-loading YouTube player that defers iframe creation until the user clicks the poster. Uses preconnect on hover for fast playback start and is styled with MUI sx, so no external CSS is required. Supports playlists, no-cookie mode, custom aspect ratios and ad-network preconnect hints.
83
+
84
+ `ProductVideo` (used by `ProductPageGallery`) now delegates YouTube playback to `YoutubeEmbed`, so any product whose Magento `media_gallery` contains a YouTube video entry gets the new lazy-loading player on its product page. Vimeo and self-hosted video paths are unchanged. The Magento preview image is passed as the YoutubeEmbed `thumbnail` so the visible poster stays consistent with the rest of the gallery.
85
+
86
+ Fix `SidebarGallery` so it forwards the `Additional` and `slotProps` from each image to `MotionImageAspect`. Before this fix the gallery silently dropped both props, which meant any `Additional` overlay configured by `ProductPageGallery` (the `<ProductVideo>` overlay with its `PlayCircle` and the new `YoutubeEmbed`) never reached the DOM. That was a latent regression that made all video-gallery entries render as static images, with or without this PR's YouTube changes.
87
+
88
+ Fix `playwright.config.ts` so `npx playwright test` actually loads. The config previously imported `examples/magento-graphcms/next.config.ts`, which transitively pulled `@graphcommerce/next-config`'s ESM build into a CJS context and crashed with `ReferenceError: exports is not defined`. Replaced with an opt-in `PLAYWRIGHT_LOCALES` env var for the multi-locale projects that the next.config import was meant to drive. ([@paales](https://github.com/paales))
89
+
90
+ ## 10.1.0-canary.21
91
+
92
+ ### Patch Changes
93
+
94
+ - [#2633](https://github.com/graphcommerce-org/graphcommerce/pull/2633) [`7554ad4`](https://github.com/graphcommerce-org/graphcommerce/commit/7554ad479cf78d790eeedd3f862c5d57403bda51) - Fix: `<Fab variant="extended">` no longer gets a fixed `width` from `MuiFabSizes`. The size-based width/height variants are now scoped to `variant: 'circular'` only, so extended Fabs can grow with their label (controlled via `min-width` instead) as MUI intends. Previously every extended Fab without an explicit `size` matched the default `large` rule and was forced to 54px wide. ([@paales](https://github.com/paales))
95
+
96
+ ## 10.1.0-canary.20
97
+
98
+ ## 10.1.0-canary.19
99
+
100
+ ## 10.1.0-canary.18
101
+
102
+ ## 10.1.0-canary.17
103
+
104
+ ## 10.1.0-canary.16
105
+
106
+ ## 10.1.0-canary.15
107
+
108
+ ## 10.1.0-canary.14
109
+
110
+ ## 10.1.0-canary.13
111
+
112
+ ## 10.1.0-canary.12
113
+
114
+ ## 10.1.0-canary.11
115
+
116
+ ## 10.1.0-canary.10
117
+
118
+ ## 10.1.0-canary.9
119
+
120
+ ## 10.1.0-canary.8
121
+
3
122
  ## 10.1.0-canary.7
4
123
 
5
124
  ### Patch Changes
package/Fab/Fab.tsx CHANGED
@@ -43,6 +43,7 @@ export function Fab(props: FabProps) {
43
43
  </Box>
44
44
  {loading && (
45
45
  <CircularProgress
46
+ aria-hidden='true'
46
47
  size={fabSize}
47
48
  color={color !== 'primary' ? 'primary' : 'inherit'}
48
49
  {...slotProps?.circularProgress}
package/Footer/Footer.tsx CHANGED
@@ -1,7 +1,7 @@
1
1
  import { sxx } from '@graphcommerce/next-ui'
2
- import type { ContainerProps } from '@mui/material'
3
2
  import { Box } from '@mui/material'
4
- import React from 'react'
3
+ import type React from 'react'
4
+ import type { ContainerSizingProps } from '../Container/Container'
5
5
  import { Container } from '../Container/Container'
6
6
  import { LazyHydrate } from '../LazyHydrate'
7
7
  import { extendableComponent } from '../Styles'
@@ -12,7 +12,7 @@ export type FooterProps = {
12
12
  customerService?: React.ReactNode
13
13
  copyright?: React.ReactNode
14
14
  children?: React.ReactNode
15
- } & ContainerProps
15
+ } & ContainerSizingProps
16
16
 
17
17
  const { classes, selectors } = extendableComponent('Footer', [
18
18
  'root',
@@ -254,13 +254,11 @@ export function SidebarGallery(props: SidebarGalleryProps) {
254
254
  <MotionImageAspect
255
255
  // eslint-disable-next-line react/no-array-index-key
256
256
  key={idx}
257
+ {...image}
257
258
  layout
258
259
  layoutDependency={zoomed}
259
- src={image.src}
260
- width={image.width}
261
- height={image.height}
262
260
  loading={idx === 0 ? 'eager' : 'lazy'}
263
- sx={{ display: 'block', objectFit: 'contain' }}
261
+ sx={sxx({ display: 'block', objectFit: 'contain' }, image.sx)}
264
262
  sizes={{
265
263
  0: '100vw',
266
264
  [theme.breakpoints.values.md]: zoomed ? '100vw' : '60vw',
@@ -52,7 +52,7 @@ export function FullPageMessage(props: FullPageMessageProps) {
52
52
  justifyItems: 'center',
53
53
  }}
54
54
  >
55
- <Box className={classes.iconWrapper}>{icon}</Box>
55
+ <Box className={classes.iconWrapper} aria-hidden='true'>{icon}</Box>
56
56
 
57
57
  <Box
58
58
  className={classes.subject}
@@ -10,6 +10,10 @@ import { SkipLink } from '../../SkipLink/SkipLink'
10
10
  import { extendableComponent } from '../../Styles'
11
11
  import { useFabSize } from '../../Theme'
12
12
 
13
+ /**
14
+ * @deprecated Import `LayoutDefaultProps` from your project's
15
+ * `components/Layout/LayoutDefault.tsx` instead.
16
+ */
13
17
  export type LayoutDefaultProps = {
14
18
  className?: string
15
19
  beforeHeader?: React.ReactNode
@@ -31,6 +35,10 @@ const { withState } = extendableComponent<OwnerState, 'LayoutDefault', typeof pa
31
35
  parts,
32
36
  )
33
37
 
38
+ /**
39
+ * @deprecated Import `LayoutDefault` from your project's
40
+ * `components/Layout/LayoutDefault.tsx` instead.
41
+ */
34
42
  export function LayoutDefault(props: LayoutDefaultProps) {
35
43
  const {
36
44
  children,
@@ -17,6 +17,8 @@ const MotionDiv = styled(m.div)({})
17
17
  export type NavigationFabProps = {
18
18
  menuIcon?: React.ReactNode
19
19
  closeIcon?: React.ReactNode
20
+ disableScrollEffects?: boolean
21
+ disableElevation?: boolean
20
22
  sx?: SxProps<Theme>
21
23
  } & Pick<FabProps, 'color' | 'size' | 'variant' | 'onClick'>
22
24
 
@@ -29,7 +31,14 @@ type OwnerState = {
29
31
  const { withState } = extendableComponent<OwnerState, typeof name, typeof parts>(name, parts)
30
32
 
31
33
  export function NavigationFab(props: NavigationFabProps) {
32
- const { menuIcon, closeIcon, sx = [], ...fabProps } = props
34
+ const {
35
+ menuIcon,
36
+ closeIcon,
37
+ disableScrollEffects,
38
+ disableElevation,
39
+ sx = [],
40
+ ...fabProps
41
+ } = props
33
42
  const router = useRouter()
34
43
  const [openEl, setOpenEl] = React.useState<null | HTMLElement>(null)
35
44
 
@@ -53,28 +62,41 @@ export function NavigationFab(props: NavigationFabProps) {
53
62
  <Box sx={sxx({ position: 'relative', width: fabIconSize, height: fabIconSize }, sx)}>
54
63
  <MotionDiv
55
64
  className={classes.wrapper}
56
- sx={{
57
- [theme.breakpoints.down('md')]: {
58
- opacity: '1 !important',
59
- transform: 'none !important',
60
- },
61
- }}
65
+ sx={
66
+ disableScrollEffects
67
+ ? {
68
+ opacity: '1 !important',
69
+ transform: 'none !important',
70
+ }
71
+ : {
72
+ [theme.breakpoints.down('md')]: {
73
+ opacity: '1 !important',
74
+ transform: 'none !important',
75
+ },
76
+ }
77
+ }
62
78
  style={{ opacity }}
63
79
  >
64
80
  <Fab
65
81
  color='inherit'
66
82
  aria-label='Open Menu'
67
83
  size='responsive'
68
- sx={{
69
- boxShadow: 'none',
70
- '&:hover, &:focus': {
84
+ sx={sxx(
85
+ {
71
86
  boxShadow: 'none',
87
+ pointerEvents: 'all',
88
+ '&:hover, &:focus': {
89
+ boxShadow: 'none',
90
+ },
91
+ },
92
+ !fabProps.color && {
93
+ '&:hover, &:focus': {
94
+ background: theme.vars.palette.text.primary,
95
+ },
72
96
  background: theme.vars.palette.text.primary,
97
+ color: theme.vars.palette.background.paper,
73
98
  },
74
- background: theme.vars.palette.text.primary,
75
- pointerEvents: 'all',
76
- color: theme.vars.palette.background.paper,
77
- }}
99
+ )}
78
100
  className={classes.fab}
79
101
  {...fabProps}
80
102
  >
@@ -109,20 +131,22 @@ export function NavigationFab(props: NavigationFabProps) {
109
131
  />
110
132
  )}
111
133
  </Fab>
112
- <MotionDiv
113
- sx={{
114
- pointerEvents: 'none',
115
- borderRadius: '99em',
116
- position: 'absolute',
117
- height: '100%',
118
- width: '100%',
119
- boxShadow: theme.shadows[6],
120
- top: 0,
121
- [theme.breakpoints.down('md')]: { opacity: '1 !important' },
122
- }}
123
- className={classes.shadow}
124
- style={{ opacity: shadowOpacity }}
125
- />
134
+ {!disableElevation && (
135
+ <MotionDiv
136
+ sx={{
137
+ pointerEvents: 'none',
138
+ borderRadius: '99em',
139
+ position: 'absolute',
140
+ height: '100%',
141
+ width: '100%',
142
+ boxShadow: theme.shadows[6],
143
+ top: 0,
144
+ [theme.breakpoints.down('md')]: { opacity: '1 !important' },
145
+ }}
146
+ className={classes.shadow}
147
+ style={disableScrollEffects ? undefined : { opacity: shadowOpacity }}
148
+ />
149
+ )}
126
150
  </MotionDiv>
127
151
  </Box>
128
152
  )
@@ -38,6 +38,7 @@ export function PageLoadIndicator() {
38
38
 
39
39
  return (
40
40
  <MLinearProgress
41
+ aria-hidden='true'
41
42
  style={{ opacity }}
42
43
  sx={{
43
44
  position: 'fixed',
@@ -84,8 +84,7 @@ export function PaginationExtended(props: PaginationExtendedProps) {
84
84
  renderItem ??
85
85
  ((item) => (
86
86
  <PaginationItem
87
- component={NextLink}
88
- href={paginationHref(item)}
87
+ {...(!item.disabled && { component: NextLink, href: paginationHref(item) })}
89
88
  slots={{ previous: Prev, next: Next }}
90
89
  {...item}
91
90
  />
@@ -1,32 +1,37 @@
1
- import { clientSize } from '@graphcommerce/framer-utils'
2
- import { sxx } from '@graphcommerce/next-ui'
3
1
  import type { ContainerProps, SxProps, Theme } from '@mui/material'
4
- import { Box, styled } from '@mui/material'
5
- import { m, useTransform } from 'framer-motion'
2
+ import { Box } from '@mui/material'
6
3
  import React from 'react'
7
- import { useScrollY } from '../../Layout/hooks/useScrollY'
8
4
  import { extendableComponent, responsiveVal } from '../../Styles'
9
5
  import { Row } from '../Row'
10
6
 
11
7
  export type HeroBannerProps = ContainerProps & {
12
8
  pageLinks: React.ReactNode
13
- videoSrc: string
9
+ /**
10
+ * Rendered full-bleed behind the copy. Takes a node rather than a source, so
11
+ * the banner is agnostic about what fills it — an image, a video, a CMS
12
+ * component. See `SpecialBanner`, which takes its asset the same way.
13
+ */
14
+ asset?: React.ReactNode
15
+ /**
16
+ * @deprecated Pass an `asset` node instead — e.g. `<Asset asset={…} />`,
17
+ * which also renders images and can carry a poster. When set (and `asset`
18
+ * is not), it renders a bare autoplaying video, without the scroll parallax
19
+ * earlier versions applied.
20
+ */
21
+ videoSrc?: string
14
22
  children: React.ReactNode
15
23
  sx?: SxProps<Theme>
16
24
  }
17
25
 
18
26
  const compName = 'HeroBanner'
27
+ // `video` and `animated` are unused since the banner stopped rendering its own
28
+ // motion video, but stay in the parts list so the generated class names remain
29
+ // available to consumers targeting them in global CSS.
19
30
  const parts = ['root', 'wrapper', 'copy', 'asset', 'animated', 'video'] as const
20
31
  const { classes } = extendableComponent(compName, parts)
21
32
 
22
- const MotionVideo = styled(m.video)({})
23
-
24
33
  export function HeroBanner(props: HeroBannerProps) {
25
- const { pageLinks, videoSrc, children, sx = [], ...containerProps } = props
26
- const scrollY = useScrollY()
27
- const scale = useTransform([scrollY, clientSize.y], ([scrollYCurr, clientSizeYCurr]: number[]) =>
28
- clientSizeYCurr ? (scrollYCurr / clientSizeYCurr) * 1.7 + 1 : 1,
29
- )
34
+ const { pageLinks, asset, videoSrc, children, sx = [], ...containerProps } = props
30
35
 
31
36
  return (
32
37
  <Row maxWidth={false} {...containerProps} className={classes.root} sx={sx}>
@@ -60,25 +65,28 @@ export function HeroBanner(props: HeroBannerProps) {
60
65
  sx={{
61
66
  gridArea: '1 / 1',
62
67
  position: 'relative',
63
- }}
64
- >
65
- <MotionVideo
66
- src={videoSrc}
67
- autoPlay
68
- muted
69
- loop
70
- playsInline
71
- disableRemotePlayback
72
- className={classes.video}
73
- style={{ scale }}
74
- sx={{
68
+ // The banner sizes itself from its copy, so whatever is passed has
69
+ // to fill the box rather than dictate it.
70
+ '& img, & video': {
75
71
  position: 'absolute',
76
- transition: 'transform 0.5s cubic-bezier(0.33, 1, 0.68, 1)',
77
72
  objectFit: 'cover',
78
73
  width: '100%',
79
74
  height: '100%',
80
- }}
81
- />
75
+ },
76
+ }}
77
+ >
78
+ {asset ??
79
+ (videoSrc ? (
80
+ <video
81
+ src={videoSrc}
82
+ autoPlay
83
+ muted
84
+ loop
85
+ playsInline
86
+ disableRemotePlayback
87
+ className={classes.video}
88
+ />
89
+ ) : null)}
82
90
  </Box>
83
91
  </Box>
84
92
  </Row>
package/Theme/MuiFab.ts CHANGED
@@ -80,11 +80,14 @@ const sizes: FabSize[] = [
80
80
  /**
81
81
  * This defines the sizes for the added responsive variant.
82
82
  *
83
+ * Only applied to circular (default) Fabs — extended Fabs must keep `width: auto` and rely on their
84
+ * content + `min-width`, otherwise they collapse to a fixed square regardless of label.
85
+ *
83
86
  * To override the sizes, please do not add variant declarations direcly, but modify
84
87
  * `yourTheme.components.MuiFabExtra.sizes` instead.
85
88
  */
86
89
  export const MuiFabSizes: FabVariants = sizes.map((size) => ({
87
- props: { size },
90
+ props: { size, variant: 'circular' },
88
91
  style: ({ theme }) => fabWidthHeight(size, theme),
89
92
  }))
90
93
 
@@ -0,0 +1,201 @@
1
+ import { Box, type SxProps, type Theme } from '@mui/material'
2
+ import * as React from 'react'
3
+ import { extendableComponent } from '../Styles/extendableComponent'
4
+ import { sxx } from '../utils/sxx'
5
+
6
+ export type YoutubePosterResolution =
7
+ | 'default'
8
+ | 'mqdefault'
9
+ | 'hqdefault'
10
+ | 'sddefault'
11
+ | 'maxresdefault'
12
+
13
+ export type YoutubeEmbedProps = {
14
+ id: string
15
+ title: string
16
+ announce?: string
17
+ adNetwork?: boolean
18
+ aspectHeight?: number
19
+ aspectWidth?: number
20
+ noCookie?: boolean
21
+ cookie?: boolean
22
+ params?: string
23
+ playlist?: boolean
24
+ playlistCoverId?: string
25
+ poster?: YoutubePosterResolution
26
+ webp?: boolean
27
+ muted?: boolean
28
+ thumbnail?: string
29
+ rel?: 'preload' | 'prefetch'
30
+ onIframeAdded?: () => void
31
+ sx?: SxProps<Theme>
32
+ ref?: React.Ref<HTMLIFrameElement>
33
+ }
34
+
35
+ const componentName = 'YoutubeEmbed' as const
36
+ const parts = ['root', 'poster', 'playButton', 'iframe'] as const
37
+ const { classes } = extendableComponent(componentName, parts)
38
+
39
+ export function YoutubeEmbed(props: YoutubeEmbedProps) {
40
+ const {
41
+ id,
42
+ title,
43
+ announce = 'Watch',
44
+ adNetwork = false,
45
+ aspectHeight = 9,
46
+ aspectWidth = 16,
47
+ noCookie = true,
48
+ cookie = false,
49
+ params = '',
50
+ playlist = false,
51
+ playlistCoverId,
52
+ poster = 'hqdefault',
53
+ webp = false,
54
+ muted = false,
55
+ thumbnail,
56
+ rel = 'preload',
57
+ onIframeAdded,
58
+ sx,
59
+ ref,
60
+ } = props
61
+
62
+ const [preconnected, setPreconnected] = React.useState(false)
63
+ const [iframeReady, setIframeReady] = React.useState(false)
64
+
65
+ const videoId = encodeURIComponent(id)
66
+ const videoPlaylistCoverId =
67
+ typeof playlistCoverId === 'string' ? encodeURIComponent(playlistCoverId) : null
68
+
69
+ const format = webp ? 'webp' : 'jpg'
70
+ const vi = webp ? 'vi_webp' : 'vi'
71
+ const posterUrl =
72
+ thumbnail ||
73
+ (!playlist
74
+ ? `https://i.ytimg.com/${vi}/${videoId}/${poster}.${format}`
75
+ : `https://i.ytimg.com/${vi}/${videoPlaylistCoverId}/${poster}.${format}`)
76
+
77
+ const ytUrl = cookie || !noCookie ? 'https://www.youtube.com' : 'https://www.youtube-nocookie.com'
78
+ const paramsSuffix = params ? `&${params}` : ''
79
+ const mutedSuffix = muted ? '&mute=1' : ''
80
+
81
+ const iframeSrc = !playlist
82
+ ? `${ytUrl}/embed/${videoId}?autoplay=1&state=1${mutedSuffix}${paramsSuffix}`
83
+ : `${ytUrl}/embed/videoseries?autoplay=1${mutedSuffix}&list=${videoId}${paramsSuffix}`
84
+
85
+ const warmConnections = () => {
86
+ if (!preconnected) setPreconnected(true)
87
+ }
88
+ const addIframe = () => {
89
+ if (!iframeReady) setIframeReady(true)
90
+ }
91
+
92
+ React.useEffect(() => {
93
+ if (iframeReady) onIframeAdded?.()
94
+ }, [iframeReady, onIframeAdded])
95
+
96
+ return (
97
+ <>
98
+ <link rel={rel} href={posterUrl} as='image' />
99
+ {preconnected && (
100
+ <>
101
+ <link rel='preconnect' href={ytUrl} />
102
+ <link rel='preconnect' href='https://www.google.com' />
103
+ {adNetwork && (
104
+ <>
105
+ <link rel='preconnect' href='https://static.doubleclick.net' />
106
+ <link rel='preconnect' href='https://googleads.g.doubleclick.net' />
107
+ </>
108
+ )}
109
+ </>
110
+ )}
111
+ <Box
112
+ className={classes.root}
113
+ onPointerOver={warmConnections}
114
+ onClick={addIframe}
115
+ data-title={title}
116
+ sx={sxx(
117
+ {
118
+ position: 'relative',
119
+ display: 'block',
120
+ contain: 'content',
121
+ backgroundPosition: 'center center',
122
+ backgroundSize: 'cover',
123
+ cursor: 'pointer',
124
+ maxWidth: '100%',
125
+ aspectRatio: `${aspectWidth} / ${aspectHeight}`,
126
+ backgroundImage: `url(${posterUrl})`,
127
+ '&::before': {
128
+ content: '""',
129
+ display: 'block',
130
+ position: 'absolute',
131
+ top: 0,
132
+ right: 0,
133
+ left: 0,
134
+ height: '60px',
135
+ background:
136
+ 'linear-gradient(180deg, rgba(0,0,0,0.247) 0%, rgba(0,0,0,0) 100%)',
137
+ pointerEvents: 'none',
138
+ },
139
+ },
140
+ sx,
141
+ )}
142
+ >
143
+ {!iframeReady && (
144
+ <Box
145
+ component='button'
146
+ type='button'
147
+ className={classes.playButton}
148
+ aria-label={`${announce} ${title}`}
149
+ sx={{
150
+ border: 0,
151
+ padding: 0,
152
+ cursor: 'pointer',
153
+ width: '68px',
154
+ height: '48px',
155
+ position: 'absolute',
156
+ top: '50%',
157
+ left: '50%',
158
+ marginLeft: '-34px',
159
+ marginTop: '-24px',
160
+ borderRadius: '14%',
161
+ transition: 'background-color 100ms cubic-bezier(0, 0, 0.2, 1)',
162
+ backgroundColor: '#212121',
163
+ opacity: 0.8,
164
+ '&:hover, &:focus': { backgroundColor: 'red', opacity: 1 },
165
+ '&::before': {
166
+ content: '""',
167
+ borderStyle: 'solid',
168
+ borderWidth: '11px 0 11px 19px',
169
+ borderColor: 'transparent transparent transparent #fff',
170
+ display: 'block',
171
+ position: 'absolute',
172
+ top: '50%',
173
+ left: '50%',
174
+ transform: 'translate3d(-50%, -50%, 0)',
175
+ },
176
+ }}
177
+ />
178
+ )}
179
+ {iframeReady && (
180
+ <Box
181
+ component='iframe'
182
+ ref={ref}
183
+ className={classes.iframe}
184
+ title={title}
185
+ allow='accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture'
186
+ allowFullScreen
187
+ src={iframeSrc}
188
+ sx={{
189
+ border: 0,
190
+ position: 'absolute',
191
+ top: 0,
192
+ left: 0,
193
+ width: '100%',
194
+ height: '100%',
195
+ }}
196
+ />
197
+ )}
198
+ </Box>
199
+ </>
200
+ )
201
+ }
@@ -0,0 +1 @@
1
+ export * from './YoutubeEmbed'
package/index.ts CHANGED
@@ -66,6 +66,7 @@ export * from './ToggleButton/ToggleButton'
66
66
  export * from './ToggleButtonGroup/ToggleButtonGroup'
67
67
  export * from './UspList/UspList'
68
68
  export * from './UspList/UspListItem'
69
+ export * from './YoutubeEmbed'
69
70
  export * from './utils/cookie'
70
71
  export * from './utils/cssFlags'
71
72
  export * from './utils/normalizeLocale'
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": "10.1.0-canary.7",
5
+ "version": "11.0.0-canary.45",
6
6
  "sideEffects": false,
7
7
  "prettier": "@graphcommerce/prettier-config-pwa",
8
8
  "eslintConfig": {
@@ -43,13 +43,13 @@
43
43
  "@emotion/react": "^11.14.0",
44
44
  "@emotion/server": "^11.11.0",
45
45
  "@emotion/styled": "^11.14.1",
46
- "@graphcommerce/eslint-config-pwa": "^10.1.0-canary.7",
47
- "@graphcommerce/framer-next-pages": "^10.1.0-canary.7",
48
- "@graphcommerce/framer-scroller": "^10.1.0-canary.7",
49
- "@graphcommerce/framer-utils": "^10.1.0-canary.7",
50
- "@graphcommerce/image": "^10.1.0-canary.7",
51
- "@graphcommerce/prettier-config-pwa": "^10.1.0-canary.7",
52
- "@graphcommerce/typescript-config-pwa": "^10.1.0-canary.7",
46
+ "@graphcommerce/eslint-config-pwa": "^11.0.0-canary.45",
47
+ "@graphcommerce/framer-next-pages": "^11.0.0-canary.45",
48
+ "@graphcommerce/framer-scroller": "^11.0.0-canary.45",
49
+ "@graphcommerce/framer-utils": "^11.0.0-canary.45",
50
+ "@graphcommerce/image": "^11.0.0-canary.45",
51
+ "@graphcommerce/prettier-config-pwa": "^11.0.0-canary.45",
52
+ "@graphcommerce/typescript-config-pwa": "^11.0.0-canary.45",
53
53
  "@lingui/core": "^5",
54
54
  "@lingui/macro": "^5",
55
55
  "@lingui/react": "^5",