// Ejected HTML menu layout sandbox with Tailwind browser CDN CSS. // http://localhost:5173/html-ejected-menu-layout/ import '@videojs/html/icons/element'; import '@videojs/html/video/skin.css'; import '@videojs/html/video/ui'; interface LayoutCall { target: 'menu' | 'root view' | 'submenu view' | 'other'; time: number; duration: number; } const SETTLE_TIME = 700; const root = document.getElementById('root')!; const panelCount = Number(new URLSearchParams(location.search).get('panels')); const PANEL_COUNT = Number.isInteger(panelCount) && panelCount > 0 && panelCount <= 13 ? panelCount : 13; const pageContent = Array.from( { length: 24 }, (_, index) => `
${index + 1}

Tailwind content card

Host-page content to exercise selector matching during layout.

` ).join(''); const panels = Array.from({ length: PANEL_COUNT }, (_, index) => { const panelNumber = index + 1; return { panelNumber, trigger: ` Test panel ${panelNumber} `, content: ` Test panel ${panelNumber}
Option one Option two
`, }; }); root.innerHTML = `

Ejected HTML menu layout

The default video skin in light DOM with ${PANEL_COUNT} mounted settings submenus, Tailwind 4 browser CDN CSS, and host-page content. Use this to compare layout-read counts and duration before and after a menu change.

Close the settings menu, then start a recording.
${pageContent}
${panels.map((panel) => panel.trigger).join('')}
${panels.map((panel) => panel.content).join('')}
`; const settingsMenu = document.getElementById('settings-menu')!; const recordButton = document.getElementById('record-open')!; const result = document.getElementById('result')!; const status = document.getElementById('status')!; const settingsTrigger = document.getElementById('settings-trigger')!; const calls: LayoutCall[] = []; const originalGetBoundingClientRect = Element.prototype.getBoundingClientRect; let recording = false; function getTarget(element: Element): LayoutCall['target'] { if (element === settingsMenu) return 'menu'; if (element.matches('[data-menu-root-view]')) return 'root view'; if (element.matches('[data-menu-view]')) return 'submenu view'; return 'other'; } function getSummary(): Record { return { menu: calls.filter((call) => call.target === 'menu').length, 'root view': calls.filter((call) => call.target === 'root view').length, 'submenu view': calls.filter((call) => call.target === 'submenu view').length, other: calls.filter((call) => call.target === 'other').length, }; } function getDuration(): number { return calls.reduce((duration, call) => duration + call.duration, 0); } Element.prototype.getBoundingClientRect = function (...args): DOMRect { const track = recording && settingsMenu.contains(this); const time = track ? performance.now() : 0; const rect = originalGetBoundingClientRect.apply(this, args); if (track) { calls.push({ target: getTarget(this), time, duration: performance.now() - time, }); } return rect; }; recordButton.addEventListener('click', () => { calls.length = 0; recording = true; result.textContent = ''; status.textContent = 'Recording. Open the settings menu once.'; }); settingsTrigger.addEventListener('click', () => { if (!recording) return; window.setTimeout(() => { recording = false; const summary = getSummary(); const total = Object.values(summary).reduce((count, value) => count + value, 0); result.textContent = `${total} reads · ${getDuration().toFixed(1)} ms · root ${summary['root view']} · submenu ${summary['submenu view']}`; status.textContent = `Recorded an open with ${settingsMenu.querySelectorAll(':scope > media-menu').length} mounted submenus.`; }, SETTLE_TIME); }); Object.assign(window, { menuLayoutProbe: { calls, getSummary, start() { calls.length = 0; recording = true; }, stop() { recording = false; }, }, }); window.addEventListener( 'pagehide', () => { Element.prototype.getBoundingClientRect = originalGetBoundingClientRect; }, { once: true } );