@@ -251,6 +251,7 @@ function openModal(card) {
251251 modalIcon . textContent = icon ;
252252 modalTitle . textContent = title ;
253253 modalLaunch . href = href ;
254+ mountModalVideo ( card , title ) ;
254255 if ( readmeCache . has ( path ) ) {
255256 modalBody . innerHTML = readmeCache . get ( path ) ;
256257 renderMathAndDiagrams ( modalBody ) ;
@@ -282,6 +283,60 @@ function closeModal() {
282283 overlay . classList . remove ( 'open' ) ;
283284 overlay . setAttribute ( 'aria-hidden' , 'true' ) ;
284285 document . body . classList . remove ( 'modal-open' ) ;
286+ clearModalVideo ( ) ;
287+ }
288+ /* ── Modal video helpers ───────────────────────────────────── */
289+ const modalMedia = document . getElementById ( 'modalMedia' ) ;
290+ function clearModalVideo ( ) {
291+ if ( ! modalMedia ) return ;
292+ const v = modalMedia . querySelector ( 'video' ) ;
293+ if ( v ) {
294+ v . pause ( ) ;
295+ }
296+ modalMedia . innerHTML = '' ;
297+ modalMedia . classList . remove ( 'is-playing' ) ;
298+ }
299+ function mountModalVideo ( card , title ) {
300+ if ( ! modalMedia ) return ;
301+ clearModalVideo ( ) ;
302+ const src = card . dataset . video ;
303+ if ( ! src ) return ;
304+ const video = document . createElement ( 'video' ) ;
305+ video . className = 'featured-card-video' ;
306+ video . muted = true ;
307+ video . loop = true ;
308+ video . playsInline = true ;
309+ video . controls = true ;
310+ video . preload = 'metadata' ;
311+ video . setAttribute ( 'aria-label' , ( title || 'demo' ) + ' — demonstration video' ) ;
312+ const source = document . createElement ( 'source' ) ;
313+ source . src = src ;
314+ source . type = 'video/mp4' ;
315+ video . appendChild ( source ) ;
316+ modalMedia . appendChild ( video ) ;
317+ const reduceMotion =
318+ window . matchMedia && window . matchMedia ( '(prefers-reduced-motion: reduce)' ) . matches ;
319+ // Touch-friendly play button overlay
320+ const playBtn = document . createElement ( 'button' ) ;
321+ playBtn . className = 'featured-card-play' ;
322+ playBtn . type = 'button' ;
323+ playBtn . setAttribute ( 'aria-label' , 'Play ' + ( title || 'demo' ) + ' video' ) ;
324+ modalMedia . appendChild ( playBtn ) ;
325+ const togglePlay = ( ) => {
326+ if ( video . paused ) {
327+ const p = video . play ( ) ;
328+ if ( p && typeof p . catch === 'function' ) p . catch ( ( ) => { } ) ;
329+ } else {
330+ video . pause ( ) ;
331+ }
332+ } ;
333+ playBtn . addEventListener ( 'click' , togglePlay ) ;
334+ video . addEventListener ( 'play' , ( ) => modalMedia . classList . add ( 'is-playing' ) ) ;
335+ video . addEventListener ( 'pause' , ( ) => modalMedia . classList . remove ( 'is-playing' ) ) ;
336+ if ( ! reduceMotion ) {
337+ const p = video . play ( ) ;
338+ if ( p && typeof p . catch === 'function' ) p . catch ( ( ) => { } ) ;
339+ }
285340}
286341
287342document . querySelectorAll ( '.featured-card[data-readme]' ) . forEach ( ( card ) => {
@@ -334,6 +389,25 @@ document.addEventListener('keydown', (e) => {
334389 source . type = 'video/mp4' ;
335390 video . appendChild ( source ) ;
336391 mount . appendChild ( video ) ;
392+ // Track play state so the overlay button can hide itself.
393+ video . addEventListener ( 'play' , ( ) => mount . classList . add ( 'is-playing' ) ) ;
394+ video . addEventListener ( 'pause' , ( ) => mount . classList . remove ( 'is-playing' ) ) ;
395+ // Touch-friendly play button overlay (works without hover).
396+ const playBtn = document . createElement ( 'button' ) ;
397+ playBtn . className = 'featured-card-play' ;
398+ playBtn . type = 'button' ;
399+ playBtn . setAttribute ( 'aria-label' , 'Play ' + title + ' video' ) ;
400+ playBtn . addEventListener ( 'click' , ( e ) => {
401+ // Don't trigger the card's modal-open click handler.
402+ e . stopPropagation ( ) ;
403+ e . preventDefault ( ) ;
404+ if ( video . paused ) {
405+ safePlay ( video ) ;
406+ } else {
407+ video . pause ( ) ;
408+ }
409+ } ) ;
410+ mount . appendChild ( playBtn ) ;
337411 mount . dataset . built = 'true' ;
338412 return video ;
339413 }
0 commit comments