@graphcommerce/next-ui 10.1.0-canary.21 → 10.1.0-canary.22

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,17 @@
1
1
  # Change Log
2
2
 
3
+ ## 10.1.0-canary.22
4
+
5
+ ### Minor Changes
6
+
7
+ - [#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.
8
+
9
+ `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.
10
+
11
+ 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.
12
+
13
+ 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))
14
+
3
15
  ## 10.1.0-canary.21
4
16
 
5
17
  ### Patch Changes
@@ -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',
@@ -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.21",
5
+ "version": "10.1.0-canary.22",
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.21",
47
- "@graphcommerce/framer-next-pages": "^10.1.0-canary.21",
48
- "@graphcommerce/framer-scroller": "^10.1.0-canary.21",
49
- "@graphcommerce/framer-utils": "^10.1.0-canary.21",
50
- "@graphcommerce/image": "^10.1.0-canary.21",
51
- "@graphcommerce/prettier-config-pwa": "^10.1.0-canary.21",
52
- "@graphcommerce/typescript-config-pwa": "^10.1.0-canary.21",
46
+ "@graphcommerce/eslint-config-pwa": "^10.1.0-canary.22",
47
+ "@graphcommerce/framer-next-pages": "^10.1.0-canary.22",
48
+ "@graphcommerce/framer-scroller": "^10.1.0-canary.22",
49
+ "@graphcommerce/framer-utils": "^10.1.0-canary.22",
50
+ "@graphcommerce/image": "^10.1.0-canary.22",
51
+ "@graphcommerce/prettier-config-pwa": "^10.1.0-canary.22",
52
+ "@graphcommerce/typescript-config-pwa": "^10.1.0-canary.22",
53
53
  "@lingui/core": "^5",
54
54
  "@lingui/macro": "^5",
55
55
  "@lingui/react": "^5",