// HTML Menu sandbox // http://localhost:5173/html-menu/ import '@app/styles.css'; import '@videojs/html/ui/menu'; // ── Class constants ─────────────────────────────────────────────────────────── const menuContentSurfaceClass = 'bg-white border-none ring-1 ring-black/10 shadow-sm rounded-md p-1 min-w-[10rem] overflow-hidden outline-none'; const menuNavSurfaceClass = 'bg-white border-none ring-1 ring-black/10 shadow-sm rounded-md min-w-[10rem] overflow-hidden outline-none'; const menuContentPlacementClass = [ 'data-side=bottom:origin-top data-side=top:origin-bottom', 'data-side=left:origin-right data-side=right:origin-left', 'data-starting-style:opacity-0 data-starting-style:scale-95 data-starting-style:-translate-y-1 data-starting-style:blur-sm', 'data-ending-style:opacity-0 data-ending-style:scale-95 data-ending-style:-translate-y-1 data-ending-style:blur-sm', ].join(' '); const menuContentClass = [ menuContentSurfaceClass, 'transition-[opacity,scale,translate,filter] duration-150 ease-in-out', menuContentPlacementClass, ].join(' '); const menuLabelClass = 'block px-2 pt-1.5 pb-0.5 text-xs font-semibold text-slate-500 select-none'; const menuSeparatorClass = 'block h-px bg-slate-200 -mx-1 my-1'; const menuItemClass = [ 'relative flex items-center gap-2 rounded-[calc(0.375rem-2px)] px-2 py-1.5', 'text-sm text-slate-900 cursor-default select-none outline-none transition-colors', 'data-[highlighted]:bg-slate-100', 'aria-disabled:opacity-50 aria-disabled:pointer-events-none', ].join(' '); const radioItemClass = [menuItemClass, 'pl-8'].join(' '); const checkboxItemClass = [menuItemClass, 'pl-8'].join(' '); // Submenu trigger — same as a regular item but with space-between layout const subMenuTriggerClass = [menuItemClass, 'justify-between'].join(' '); const subMenuContentClass = [ 'absolute inset-0 z-10 bg-white rounded-[inherit] p-1 outline-none overflow-hidden translate-x-0', 'transition-transform duration-300 ease-in-out will-change-transform', '[&[data-starting-style][data-direction=forward]]:translate-x-full', '[&[data-ending-style][data-direction=forward]]:-translate-x-full', '[&[data-starting-style][data-direction=back]]:-translate-x-full', '[&[data-ending-style][data-direction=back]]:translate-x-full', ].join(' '); // Root and submenu views share the same viewport so they can slide over each other. const rootViewClass = [ 'absolute inset-0 p-1 translate-x-0', 'transition-transform duration-300 ease-in-out will-change-transform', 'data-[menu-view-state=inactive]:-translate-x-full', ].join(' '); const backButtonClass = [ 'flex items-center gap-1.5 w-full rounded-[calc(0.375rem-2px)] px-2 py-1.5 mb-0.5', 'text-sm font-medium text-slate-500 cursor-default select-none outline-none transition-colors', 'hover:bg-slate-100 hover:text-slate-900', ].join(' '); const menuNavPopupClass = [ 'group relative', menuNavSurfaceClass, 'w-(--media-menu-width) h-(--media-menu-height)', 'transition-[opacity,scale,translate,filter,width,height] duration-150 ease-in-out', menuContentPlacementClass, ].join(' '); // ── Render ──────────────────────────────────────────────────────────────────── const root = document.getElementById('root')!; root.innerHTML = `

HTML Menu

Custom elements — <media-menu> and friends

Radio group Resolution Auto 1080p 720p 480p 360p (unavailable)

Selected: auto

Mixed items Playback Loop Autoplay Copy link Report issue

Loop: off

Submenu Quality auto Speed Normal Copy link Quality Auto 1080p 720p 480p Speed 0.5x 0.75x Normal 1.25x 1.5x 2x

Quality: auto  ·  Speed: Normal

`; // ── Event listeners ─────────────────────────────────────────────────────────── document.getElementById('quality-group')!.addEventListener('value-change', (e) => { const { value } = (e as CustomEvent).detail; (e.target as HTMLElement).setAttribute('value', value); document.getElementById('quality-output')!.textContent = value; }); document.getElementById('loop-item')!.addEventListener('checked-change', (e) => { const { checked } = (e as CustomEvent).detail; (e.target as HTMLElement).toggleAttribute('checked', checked); document.getElementById('loop-output')!.textContent = checked ? 'on' : 'off'; }); document.getElementById('autoplay-item')!.addEventListener('checked-change', (e) => { const checked = (e as CustomEvent).detail.checked; (e.target as HTMLElement).toggleAttribute('checked', checked); }); document.getElementById('copy-item')!.addEventListener('select', () => { console.log('copy link'); }); document.getElementById('report-item')!.addEventListener('select', () => { console.log('report issue'); }); document.getElementById('nav-quality-group')!.addEventListener('value-change', (event) => { const { value } = (event as CustomEvent).detail; (event.target as HTMLElement).setAttribute('value', value); document.getElementById('nav-quality-output')!.textContent = value; document.getElementById('nav-quality-hint')!.textContent = value; }); document.getElementById('nav-speed-group')!.addEventListener('value-change', (event) => { const { value } = (event as CustomEvent).detail; (event.target as HTMLElement).setAttribute('value', value); const label = value === '1' ? 'Normal' : `${value}x`; document.getElementById('nav-speed-output')!.textContent = label; document.getElementById('nav-speed-hint')!.textContent = label; }); document.getElementById('nav-copy-item')!.addEventListener('select', () => { console.log('nav: copy link'); });