Merge branch 'match-video' into master

Match Highlights mobile UX: windowed bottom sheet (video + list on screen,
top/bottom bars hidden, flush to player, non-blocking coach caption) and a
fullscreen landscape drawer that squeezes the video beside a solid side panel.
This commit is contained in:
ghassan 2026-07-28 23:26:45 +03:00
commit eb5cea8630

View File

@ -1882,6 +1882,133 @@
font-size: 0.75rem; font-size: 0.75rem;
} }
} }
/* ============================================================
MATCH HIGHLIGHTS mobile bottom sheet (Option A)
Desktop keeps its side pane untouched. Below 991px the pane
becomes a bottom sheet so the player stays visible up top
while points / coach reviews scroll in the lower half.
============================================================ */
.hl-sheet-backdrop { display: none; }
.hl-sheet-grab { display: none; }
@media (max-width: 991px) {
.events-sidebar {
display: flex !important; /* override desktop display:none so it can slide */
position: fixed;
left: 0; right: 0; bottom: 0;
/* Top sits exactly at the player's bottom edge so nothing (title,
actions, channel row) shows between the video and the sheet. JS
sets the precise px; the 56.25vw fallback = a full-width 16:9 player. */
top: var(--hl-sheet-top, 56.25vw);
width: 100% !important;
height: auto; max-height: none;
margin: 0 !important;
border-radius: 18px 18px 0 0;
box-shadow: 0 -14px 44px rgba(0, 0, 0, 0.55);
transform: translateY(100%);
transition: transform 0.34s cubic-bezier(0.32, 0.72, 0, 1);
pointer-events: none;
z-index: 1400; /* above the fixed bottom nav (z-index 1000) */
}
.events-sidebar.show {
display: flex !important;
transform: translateY(0);
pointer-events: auto;
}
/* Drag affordance at the top of the sheet */
.hl-sheet-grab {
display: block; flex-shrink: 0;
width: 40px; height: 4px; border-radius: 3px;
background: rgba(255, 255, 255, 0.3);
margin: 10px auto 4px; cursor: pointer;
}
/* Transparent tap-to-dismiss catcher no dim, so the match video keeps
full brightness and detail while the sheet is open. */
.hl-sheet-backdrop {
display: block;
position: fixed; inset: 0;
background: transparent;
visibility: hidden;
z-index: 1399;
}
.hl-sheet-backdrop.show { visibility: visible; }
/* Reclaim the top bar's space while the sheet is open: slide the header
up and float the scroll area to the very top so the player gets more
room. Both revert the moment the sheet closes. */
.yt-header { transition: transform 0.3s ease; }
body.hl-sheet-open .yt-header { transform: translateY(-100%); }
#main.yt-main { transition: top 0.3s ease; }
body.hl-sheet-open #main.yt-main { top: 0 !important; }
/* Hide the bottom nav too so the sheet owns the whole lower area.
!important overrides the base `.yt-bottom-nav { transform: none !important }`. */
.yt-bottom-nav { transition: transform 0.3s ease; }
body.hl-sheet-open .yt-bottom-nav { transform: translateY(100%) !important; }
/* Coach-note overlay on the small mobile player: a slim caption pinned to
the very bottom edge (max 2 lines) so it never blocks the view. The full
note stays readable in the sheet. */
.coach-note-overlay {
bottom: 6px;
font-size: 12px;
font-weight: 600;
line-height: 1.3;
padding: 5px 10px;
max-width: 94%;
}
.coach-note-overlay.show {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
@media (max-width: 991px) and (prefers-reduced-motion: reduce) {
.events-sidebar,
.yt-header,
.yt-bottom-nav,
#main.yt-main { transition: none; }
}
/* ============================================================
MATCH HIGHLIGHTS fullscreen landscape drawer
Native fullscreen renders only #ytpWrap + descendants, so JS
relocates the pane inside the player. It becomes a narrow solid
right-side drawer, and the video (inside .ytp) is squeezed into
the remaining left area so nothing overlaps both are fully
visible side by side. Only while .ytp-fullscreen is on.
============================================================ */
.ytp-wrap.ytp-fullscreen { --hl-drawer-w: clamp(230px, 30vw, 360px); }
.ytp-wrap.ytp-fullscreen .events-sidebar {
display: flex !important;
position: absolute; top: 0; right: 0; bottom: 0; left: auto;
/* !important is required: on a <=991px landscape phone the mobile sheet
rule sets width:100% !important, which would otherwise stretch the
drawer across the whole video. */
width: var(--hl-drawer-w) !important;
height: auto !important; max-height: none !important;
margin: 0 !important; border-radius: 0 !important;
background: var(--bg-secondary, #17171b); /* solid: the video is squeezed aside, not behind it */
border-left: 1px solid rgba(255, 255, 255, 0.10);
box-shadow: none;
transform: translateX(100%);
transition: transform 0.34s cubic-bezier(0.32, 0.72, 0, 1);
pointer-events: none; z-index: 40;
}
.ytp-wrap.ytp-fullscreen .events-sidebar.show { transform: translateX(0); pointer-events: auto; }
.ytp-wrap.ytp-fullscreen .events-sidebar .hl-sheet-grab { display: none; }
/* Squeeze the video into the remaining left area when the drawer is open.
Everything else (controls, coach caption, the Highlights button) lives
inside .ytp, so it all moves with the video nothing sits under the drawer. */
.ytp-wrap.ytp-fullscreen .ytp { transition: width 0.34s cubic-bezier(0.32, 0.72, 0, 1); }
.ytp-wrap.ytp-fullscreen.hl-drawer-open .ytp { width: calc(100% - var(--hl-drawer-w)); }
@media (prefers-reduced-motion: reduce) {
.ytp-wrap.ytp-fullscreen .events-sidebar { transition: none; }
}
</style> </style>
@endsection @endsection
@ -2222,26 +2349,86 @@
const videoId = @json(isset($video) ? $video->getRouteKey() : ''); const videoId = @json(isset($video) ? $video->getRouteKey() : '');
if (toggleBtn && sidebar) { if (toggleBtn && sidebar) {
// Load saved state on page load const backdrop = document.getElementById('hlSheetBackdrop');
const savedState = localStorage.getItem(`highlights_${videoId}`); const grab = sidebar.querySelector('.hl-sheet-grab');
if (savedState === 'open') { const mainScroll = document.getElementById('main');
sidebar.classList.add('show'); const player = document.getElementById('ytpWrap');
toggleBtn.classList.add('expanded'); const isMobile = () => window.matchMedia('(max-width: 991px)').matches;
toggleBtn.textContent = 'Highlights';
}
toggleBtn.addEventListener('click', function() { // Pin the sheet's top edge to the player's exact bottom so the
const isOpen = sidebar.classList.toggle('show'); // gap between them is zero on any device/orientation.
toggleBtn.classList.toggle('expanded'); const positionSheet = () => {
if (!isMobile()) { sidebar.style.removeProperty('--hl-sheet-top'); return; }
const wrap = document.getElementById('ytpWrap');
if (wrap) {
const h = Math.round(wrap.getBoundingClientRect().height);
if (h > 0) sidebar.style.setProperty('--hl-sheet-top', h + 'px');
}
};
// Save state to localStorage const setOpen = (open) => {
if (isOpen) { sidebar.classList.toggle('show', open);
toggleBtn.classList.toggle('expanded', open);
// Fullscreen drawer state — harmless when not fullscreen (CSS is gated).
if (player) player.classList.toggle('hl-drawer-open', open);
if (backdrop) backdrop.classList.toggle('show', open && isMobile());
// Hide the top bar (mobile only) to give the player more room.
document.body.classList.toggle('hl-sheet-open', open && isMobile());
if (open) {
localStorage.setItem(`highlights_${videoId}`, 'open'); localStorage.setItem(`highlights_${videoId}`, 'open');
toggleBtn.textContent = 'Highlights'; // Mobile: the pane is a bottom sheet — bring the player back
// to the top so the user can watch while reading highlights.
if (isMobile()) {
if (mainScroll) mainScroll.scrollTo({ top: 0, behavior: 'smooth' });
positionSheet();
}
} else { } else {
localStorage.removeItem(`highlights_${videoId}`); localStorage.removeItem(`highlights_${videoId}`);
} }
};
// Restore last state on DESKTOP only — a sheet auto-popping open on
// every mobile page load would be jarring.
if (localStorage.getItem(`highlights_${videoId}`) === 'open' && !isMobile()) {
setOpen(true);
}
toggleBtn.addEventListener('click', () => setOpen(!sidebar.classList.contains('show')));
if (backdrop) backdrop.addEventListener('click', () => setOpen(false));
if (grab) grab.addEventListener('click', () => setOpen(false));
// Keep the scrim + header state honest if the viewport crosses
// the mobile boundary while the pane is open.
window.addEventListener('resize', () => {
const openMobile = sidebar.classList.contains('show') && isMobile();
if (backdrop) backdrop.classList.toggle('show', openMobile);
document.body.classList.toggle('hl-sheet-open', openMobile);
positionSheet();
}); });
// Fullscreen: native fullscreen paints only #ytpWrap and its
// descendants, so the pane must live INSIDE the player to be
// visible. Relocate it there on enter, move it home on exit; CSS
// reshapes it into the transparent right-side drawer.
if (player) {
let fsHome = null;
const onFsChange = () => {
const fsEl = document.fullscreenElement || document.webkitFullscreenElement;
const inPlayerFs = fsEl === player;
if (inPlayerFs && !fsHome) {
fsHome = document.createComment('events-sidebar-home');
sidebar.parentNode.insertBefore(fsHome, sidebar);
player.appendChild(sidebar);
player.classList.toggle('hl-drawer-open', sidebar.classList.contains('show'));
} else if (!inPlayerFs && fsHome) {
fsHome.parentNode.insertBefore(sidebar, fsHome);
fsHome.parentNode.removeChild(fsHome);
fsHome = null;
}
};
document.addEventListener('fullscreenchange', onFsChange);
document.addEventListener('webkitfullscreenchange', onFsChange);
}
} }
}); });
</script> </script>
@ -2253,7 +2440,11 @@
<!-- Sidebar - Match Highlights --> <!-- Sidebar - Match Highlights -->
<div class="yt-sidebar-container" style="display: flex; flex-direction: column; gap: 16px;"> <div class="yt-sidebar-container" style="display: flex; flex-direction: column; gap: 16px;">
{{-- Mobile-only scrim behind the bottom sheet (tap to dismiss) --}}
<div class="hl-sheet-backdrop" id="hlSheetBackdrop"></div>
<aside class="events-sidebar"> <aside class="events-sidebar">
{{-- Mobile-only drag handle at the top of the sheet --}}
<div class="hl-sheet-grab"></div>
<div class="events-sidebar-header" style="justify-content: center;"> <div class="events-sidebar-header" style="justify-content: center;">
<h3>Match Highlights</h3> <h3>Match Highlights</h3>
</div> </div>