feat(tv): zona-like leanback navigation, hidden header in tv mode, left-edge drawer summon without collapse toggle to 0.0.5

This commit is contained in:
Nikita 2026-10-27 12:07:20 +05:00
parent 6750d7d437
commit f380fc2add
5 changed files with 237 additions and 124 deletions

View file

@ -1,7 +1,7 @@
{ {
"name": "has", "name": "has",
"private": true, "private": true,
"version": "0.0.4", "version": "0.0.5",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View file

@ -1,6 +1,6 @@
[package] [package]
name = "has" name = "has"
version = "0.0.4" version = "0.0.5"
description = "has" description = "has"
authors = ["autli"] authors = ["autli"]
edition = "2024" edition = "2024"

View file

@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "has", "productName": "has",
"version": "0.0.4", "version": "0.0.5",
"identifier": "com.has.tauri-app", "identifier": "com.has.tauri-app",
"build": { "build": {
"beforeDevCommand": "npm run dev", "beforeDevCommand": "npm run dev",

View file

@ -2017,21 +2017,22 @@
} }
/* Android TV Leanback Navigation Drawer (Slide-in Overlay) */ /* Android TV Leanback Navigation Drawer (Slide-in Overlay) */
/* Android TV Leanback Navigation Drawer (Zona / Android TV 10-Foot UI) */
.has-cinema-tv-nav-drawer { .has-cinema-tv-nav-drawer {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
bottom: 0; bottom: 0;
width: 270px; width: 280px;
background: rgba(8, 12, 24, 0.97); background: rgba(8, 12, 24, 0.98);
backdrop-filter: blur(28px); backdrop-filter: blur(28px);
border-right: 1px solid rgba(129, 140, 248, 0.35); border-right: 1px solid rgba(129, 140, 248, 0.35);
box-shadow: 15px 0 50px rgba(0, 0, 0, 0.9); box-shadow: 20px 0 60px rgba(0, 0, 0, 0.95);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
z-index: 2000; z-index: 2000;
transform: translateX(-100%); transform: translateX(-100%);
transition: transform 0.26s cubic-bezier(0.16, 1, 0.3, 1); transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1);
pointer-events: none; pointer-events: none;
visibility: hidden; visibility: hidden;
} }
@ -2045,63 +2046,40 @@
.has-cinema-tv-nav-header { .has-cinema-tv-nav-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: flex-start;
padding: 18px 16px 14px; padding: 24px 20px 18px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06); border-bottom: 1px solid rgba(255, 255, 255, 0.08);
} }
.has-cinema-tv-nav-logo { .has-cinema-tv-nav-logo {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 12px;
overflow: hidden;
} }
.has-cinema-tv-nav-logo-badge { .has-cinema-tv-nav-logo-badge {
background: linear-gradient(135deg, #6366f1, #a855f7); background: linear-gradient(135deg, #6366f1, #a855f7);
color: #fff; color: #fff;
font-weight: 800; font-weight: 900;
font-size: 13px; font-size: 14px;
padding: 4px 8px; padding: 5px 10px;
border-radius: 8px; border-radius: 8px;
letter-spacing: 1px; letter-spacing: 1.5px;
box-shadow: 0 0 16px rgba(99, 102, 241, 0.5);
} }
.has-cinema-tv-nav-logo-text { .has-cinema-tv-nav-logo-text {
font-size: 16px; font-size: 18px;
font-weight: 700; font-weight: 800;
color: #f8fafc; color: #f8fafc;
white-space: nowrap; letter-spacing: 0.5px;
}
.has-cinema-tv-nav-toggle-btn {
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 8px;
color: #cbd5e1;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
font-size: 12px;
}
.has-cinema-tv-nav-toggle-btn:focus,
.has-cinema-tv-nav-toggle-btn:hover {
background: #6366f1;
color: #ffffff;
outline: 3px solid #818cf8 !important;
box-shadow: 0 0 16px rgba(129, 140, 248, 0.7) !important;
} }
.has-cinema-tv-nav-list { .has-cinema-tv-nav-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
padding: 12px 10px; padding: 16px 12px;
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
} }
@ -2111,12 +2089,12 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 14px; gap: 14px;
padding: 12px 14px; padding: 12px 16px;
background: transparent; background: transparent;
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 12px; border-radius: 12px;
color: #94a3b8; color: #94a3b8;
font-size: 15px; font-size: 15.5px;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1); transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
@ -2125,18 +2103,19 @@
.has-cinema-tv-nav-item:hover, .has-cinema-tv-nav-item:hover,
.has-cinema-tv-nav-item:focus { .has-cinema-tv-nav-item:focus {
background: rgba(99, 102, 241, 0.25) !important; background: rgba(99, 102, 241, 0.3) !important;
border-color: #818cf8 !important; border-color: #818cf8 !important;
color: #ffffff !important; color: #ffffff !important;
outline: 3px solid #818cf8 !important; outline: 3px solid #818cf8 !important;
box-shadow: 0 0 24px rgba(129, 140, 248, 0.8) !important; box-shadow: 0 0 24px rgba(129, 140, 248, 0.85) !important;
transform: scale(1.04) !important; transform: scale(1.03) !important;
} }
.has-cinema-tv-nav-item.active { .has-cinema-tv-nav-item.active {
background: linear-gradient(135deg, rgba(99, 102, 241, 0.35), rgba(139, 92, 246, 0.35)); background: linear-gradient(135deg, rgba(99, 102, 241, 0.4), rgba(139, 92, 246, 0.4));
border-color: rgba(129, 140, 248, 0.6); border-color: rgba(129, 140, 248, 0.6);
color: #ffffff; color: #ffffff;
font-weight: 700;
} }
.has-cinema-tv-nav-item.logout { .has-cinema-tv-nav-item.logout {
@ -2168,36 +2147,100 @@
} }
.has-cinema-tv-nav-footer { .has-cinema-tv-nav-footer {
padding: 12px; padding: 14px 16px;
font-size: 11.5px; font-size: 12px;
color: #64748b; color: #64748b;
text-align: center; text-align: center;
border-top: 1px solid rgba(255, 255, 255, 0.08); border-top: 1px solid rgba(255, 255, 255, 0.08);
} }
/* TV Mode Root Overrides (Safe Overscan Area & Compact Proportions) */ /* TV Mode Root Overrides (Zona TV & Android TV Experience) */
.has-tv-mode { .has-tv-mode {
font-size: 14.5px; font-size: 14.5px;
} }
/* Navigation is completely invisible on cinema screen (summoned by pressing Left from edge) */
.has-tv-mode .has-cinema-header { .has-tv-mode .has-cinema-header {
padding: 16px 54px !important; display: none !important;
}
.has-tv-mode .has-cinema-nav-links {
gap: 20px;
}
.has-tv-mode .has-cinema-nav-item {
font-size: 15px;
padding: 6px 10px;
} }
.has-tv-mode .has-cinema-hero { .has-tv-mode .has-cinema-hero {
padding: 85px 54px 30px !important; padding: 40px 54px 30px !important;
min-height: 360px; min-height: 360px;
} }
/* Dedicated TV Search Section when activeTab === "search" */
.has-cinema-tv-search-page {
padding: 40px 54px 20px;
display: flex;
flex-direction: column;
gap: 16px;
}
.has-cinema-tv-search-box-wrap {
display: flex;
align-items: center;
gap: 14px;
background: rgba(15, 23, 42, 0.85);
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 16px;
padding: 14px 22px;
backdrop-filter: blur(16px);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.has-cinema-tv-search-box-wrap:focus-within {
border-color: #818cf8;
box-shadow: 0 0 25px rgba(129, 140, 248, 0.6);
}
.has-cinema-tv-search-lens {
font-size: 22px;
}
.has-cinema-tv-search-input-field {
flex: 1;
background: transparent;
border: none;
outline: none;
color: #ffffff;
font-size: 18px;
font-weight: 600;
}
.has-cinema-tv-search-input-field::placeholder {
color: #64748b;
}
.has-cinema-tv-search-clear-btn {
background: rgba(255, 255, 255, 0.1);
border: none;
color: #94a3b8;
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
}
.has-cinema-tv-search-clear-btn:hover,
.has-cinema-tv-search-clear-btn:focus {
background: #ef4444;
color: #ffffff;
outline: 2px solid #ffffff;
}
.has-cinema-tv-search-hint {
font-size: 13.5px;
color: #94a3b8;
padding-left: 6px;
}
.has-tv-mode .has-cinema-hero-title { .has-tv-mode .has-cinema-hero-title {
font-size: 30px; font-size: 30px;
margin-bottom: 8px; margin-bottom: 8px;

View file

@ -594,7 +594,7 @@ export default function Cinema({
const [, setConfig] = useState<CinemaConfig>({ tmdb_api_key: null }); const [, setConfig] = useState<CinemaConfig>({ tmdb_api_key: null });
// Navigation Filter // Navigation Filter
const [activeTab, setActiveTab] = useState<"home" | "movies" | "series" | "anime" | "bookmarks">("home"); const [activeTab, setActiveTab] = useState<"home" | "movies" | "series" | "anime" | "bookmarks" | "search">("home");
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [searchResults, setSearchResults] = useState<CinemaMediaItem[]>([]); const [searchResults, setSearchResults] = useState<CinemaMediaItem[]>([]);
const [isSearching, setIsSearching] = useState(false); const [isSearching, setIsSearching] = useState(false);
@ -1764,7 +1764,19 @@ export default function Cinema({
if (isTvNavOpen) { if (isTvNavOpen) {
e.preventDefault(); e.preventDefault();
setIsTvNavOpen(false); setIsTvNavOpen(false);
focusFirstContentCard(); setTimeout(() => {
if (lastFocusedCardRef.current && document.contains(lastFocusedCardRef.current)) {
lastFocusedCardRef.current.focus();
lastFocusedCardRef.current.scrollIntoView({
behavior: "smooth",
block: "center",
inline: "center",
});
playTvBeep(640, 0.03);
} else {
focusFirstContentCard();
}
}, 40);
return; return;
} }
if (showTrailerModal) { if (showTrailerModal) {
@ -1807,6 +1819,22 @@ export default function Cinema({
focusFirstContentCard(); focusFirstContentCard();
return; return;
} }
// At home root on TV, Back button summons the navigation drawer
if (isTvMode && !isTvNavOpen) {
e.preventDefault();
lastFocusedCardRef.current = document.activeElement as HTMLElement | null;
setIsTvNavOpen(true);
setTimeout(() => {
const targetNav =
document.querySelector<HTMLElement>(".has-cinema-tv-nav-item.active") ||
document.querySelector<HTMLElement>(".has-cinema-tv-nav-item");
if (targetNav) {
targetNav.focus();
playTvBeep(640, 0.03);
}
}, 40);
return;
}
} }
// 3. D-Pad Directional Arrows Navigation // 3. D-Pad Directional Arrows Navigation
@ -1853,14 +1881,26 @@ export default function Cinema({
if (direction === "right") { if (direction === "right") {
e.preventDefault(); e.preventDefault();
setIsTvNavOpen(false); setIsTvNavOpen(false);
focusFirstContentCard(); setTimeout(() => {
if (lastFocusedCardRef.current && document.contains(lastFocusedCardRef.current)) {
lastFocusedCardRef.current.focus();
lastFocusedCardRef.current.scrollIntoView({
behavior: "smooth",
block: "center",
inline: "center",
});
playTvBeep(640, 0.03);
} else {
focusFirstContentCard();
}
}, 40);
return; return;
} }
} }
let focusables = Array.from( let focusables = Array.from(
document.querySelectorAll<HTMLElement>( document.querySelectorAll<HTMLElement>(
'[data-tv-focusable="true"], .has-cinema-nav-item, .has-cinema-tv-nav-item, .has-cinema-tv-menu-pill, .has-cinema-btn-play, .has-cinema-btn-bookmark, .has-cinema-search-input, .has-cinema-media-card, .has-cinema-grid-card, .has-cinema-episode-card, .has-cinema-player-hud-btn, .has-cinema-player-btn-back, .has-cinema-drawer-option-btn, .has-cinema-drawer-chip, .has-cinema-drawer-ep-btn, .has-cinema-drawer-close-btn, .has-cinema-drawer-btn-primary, .has-cinema-drawer-btn-secondary, .has-cinema-player-pill-nav, .has-cinema-season-tab, .has-cinema-details-btn-play, .has-cinema-details-close-btn, .has-cinema-details-btn-bookmark, .has-cinema-details-btn-trailer, .has-cinema-actor-card, .has-cinema-btn-info-wide' '[data-tv-focusable="true"], .has-cinema-nav-item, .has-cinema-tv-nav-item, .has-cinema-btn-play, .has-cinema-btn-bookmark, .has-cinema-media-card, .has-cinema-grid-card, .has-cinema-episode-card, .has-cinema-player-hud-btn, .has-cinema-player-btn-back, .has-cinema-drawer-option-btn, .has-cinema-drawer-chip, .has-cinema-drawer-ep-btn, .has-cinema-drawer-close-btn, .has-cinema-drawer-btn-primary, .has-cinema-drawer-btn-secondary, .has-cinema-player-pill-nav, .has-cinema-season-tab, .has-cinema-details-btn-play, .has-cinema-details-close-btn, .has-cinema-details-btn-bookmark, .has-cinema-details-btn-trailer, .has-cinema-actor-card, .has-cinema-btn-info-wide'
) )
).filter((el) => { ).filter((el) => {
const r = el.getBoundingClientRect(); const r = el.getBoundingClientRect();
@ -1873,6 +1913,22 @@ export default function Cinema({
); );
}); });
// TV navigation drawer focus isolation
if (isTvNavOpen) {
const drawerEl = document.querySelector<HTMLElement>(".has-cinema-tv-nav-drawer");
if (drawerEl) {
focusables = focusables.filter((el) => drawerEl.contains(el));
}
} else {
// When drawer is closed, NEVER include drawer items in focusable pool
focusables = focusables.filter((el) => !el.closest(".has-cinema-tv-nav-drawer"));
}
// In TV mode, top header is hidden, so never include header elements
if (isTvMode) {
focusables = focusables.filter((el) => !el.closest(".has-cinema-header"));
}
// Trap focus inside modal or drawer if open // Trap focus inside modal or drawer if open
if (settingsSheet) { if (settingsSheet) {
focusables = focusables.filter((el) => settingsSheet.contains(el)); focusables = focusables.filter((el) => settingsSheet.contains(el));
@ -1952,10 +2008,11 @@ export default function Cinema({
playTvBeep(640, 0.025); playTvBeep(640, 0.025);
} else { } else {
// If no element found in this direction outside modals: // If no element found in this direction outside modals:
// Trigger the TV Navigation Drawer on Left (from leftmost card) or Right (from rightmost edge) // When moving LEFT from the leftmost card, summon the TV Navigation Drawer!
if (!detailsEl && !playerEl && !trailerEl && !settingsSheet && !episodesSheet) { if (!detailsEl && !playerEl && !trailerEl && !settingsSheet && !episodesSheet) {
if (direction === "left" || direction === "right") { if (direction === "left") {
e.preventDefault(); e.preventDefault();
lastFocusedCardRef.current = current;
setIsTvNavOpen(true); setIsTvNavOpen(true);
setTimeout(() => { setTimeout(() => {
const targetNav = const targetNav =
@ -2276,46 +2333,38 @@ export default function Cinema({
)} )}
{/* ======================================================== {/* ========================================================
ANDROID TV LEANBACK NAVIGATION DRAWER ANDROID TV LEANBACK NAVIGATION DRAWER (Zona TV Experience)
(Opens when pressing Left from cards, remote Menu, or Menu pill) (Summoned only by moving Left from the leftmost card)
======================================================== */} ======================================================== */}
{isTvMode && ( {isTvMode && (
<aside <aside
className={`has-cinema-tv-nav-drawer ${isTvNavOpen ? "open" : "collapsed"}`} className={`has-cinema-tv-nav-drawer ${isTvNavOpen ? "open" : ""}`}
aria-label="Навигация для ТВ" aria-label="Навигация для ТВ"
> >
<div className="has-cinema-tv-nav-header"> <div className="has-cinema-tv-nav-header">
<div className="has-cinema-tv-nav-logo"> <div className="has-cinema-tv-nav-logo">
<span className="has-cinema-tv-nav-logo-badge">HAS</span> <span className="has-cinema-tv-nav-logo-badge">HAS</span>
{isTvNavOpen && <span className="has-cinema-tv-nav-logo-text">Кинотеатр</span>} <span className="has-cinema-tv-nav-logo-text">Кинотеатр</span>
</div> </div>
<button
type="button"
className="has-cinema-tv-nav-toggle-btn"
onClick={() => setIsTvNavOpen((prev) => !prev)}
title={isTvNavOpen ? "Свернуть меню (Вправо)" : "Развернуть меню"}
data-tv-focusable="true"
>
{isTvNavOpen ? "◀" : "▶"}
</button>
</div> </div>
<div className="has-cinema-tv-nav-list"> <div className="has-cinema-tv-nav-list">
<button <button
type="button" type="button"
className={`has-cinema-tv-nav-item ${searchQuery ? "active" : ""}`} className={`has-cinema-tv-nav-item ${activeTab === "search" || searchQuery ? "active" : ""}`}
onClick={() => { onClick={() => {
setActiveTab("search");
setIsTvNavOpen(false); setIsTvNavOpen(false);
const searchInput = document.querySelector<HTMLInputElement>(".has-cinema-search-input"); setTimeout(() => {
if (searchInput) { const s = document.querySelector<HTMLInputElement>(".has-cinema-tv-search-input-field") || document.querySelector<HTMLInputElement>(".has-cinema-search-input");
searchInput.focus(); if (s) s.focus();
} }, 60);
}} }}
data-tv-focusable="true" data-tv-focusable="true"
title="Поиск" title="Поиск"
> >
<span className="has-cinema-tv-nav-icon">🔍</span> <span className="has-cinema-tv-nav-icon">🔍</span>
{isTvNavOpen && <span className="has-cinema-tv-nav-label">Поиск</span>} <span className="has-cinema-tv-nav-label">Поиск</span>
</button> </button>
<button <button
@ -2325,13 +2374,13 @@ export default function Cinema({
setActiveTab("home"); setActiveTab("home");
setSearchQuery(""); setSearchQuery("");
setIsTvNavOpen(false); setIsTvNavOpen(false);
focusFirstContentCard(); setTimeout(() => focusFirstContentCard(), 60);
}} }}
data-tv-focusable="true" data-tv-focusable="true"
title="Каталог" title="Каталог"
> >
<span className="has-cinema-tv-nav-icon">🎬</span> <span className="has-cinema-tv-nav-icon">🏠</span>
{isTvNavOpen && <span className="has-cinema-tv-nav-label">Каталог</span>} <span className="has-cinema-tv-nav-label">Каталог</span>
</button> </button>
<button <button
@ -2341,13 +2390,13 @@ export default function Cinema({
setActiveTab("movies"); setActiveTab("movies");
setSearchQuery(""); setSearchQuery("");
setIsTvNavOpen(false); setIsTvNavOpen(false);
focusFirstContentCard(); setTimeout(() => focusFirstContentCard(), 60);
}} }}
data-tv-focusable="true" data-tv-focusable="true"
title="Фильмы" title="Фильмы"
> >
<span className="has-cinema-tv-nav-icon">🍿</span> <span className="has-cinema-tv-nav-icon">🎬</span>
{isTvNavOpen && <span className="has-cinema-tv-nav-label">Фильмы</span>} <span className="has-cinema-tv-nav-label">Фильмы</span>
</button> </button>
<button <button
@ -2357,13 +2406,13 @@ export default function Cinema({
setActiveTab("series"); setActiveTab("series");
setSearchQuery(""); setSearchQuery("");
setIsTvNavOpen(false); setIsTvNavOpen(false);
focusFirstContentCard(); setTimeout(() => focusFirstContentCard(), 60);
}} }}
data-tv-focusable="true" data-tv-focusable="true"
title="Сериалы" title="Сериалы"
> >
<span className="has-cinema-tv-nav-icon">📺</span> <span className="has-cinema-tv-nav-icon">📺</span>
{isTvNavOpen && <span className="has-cinema-tv-nav-label">Сериалы</span>} <span className="has-cinema-tv-nav-label">Сериалы</span>
</button> </button>
<button <button
@ -2373,13 +2422,13 @@ export default function Cinema({
setActiveTab("anime"); setActiveTab("anime");
setSearchQuery(""); setSearchQuery("");
setIsTvNavOpen(false); setIsTvNavOpen(false);
focusFirstContentCard(); setTimeout(() => focusFirstContentCard(), 60);
}} }}
data-tv-focusable="true" data-tv-focusable="true"
title="Аниме" title="Аниме"
> >
<span className="has-cinema-tv-nav-icon">⛩️</span> <span className="has-cinema-tv-nav-icon">🌸</span>
{isTvNavOpen && <span className="has-cinema-tv-nav-label">Аниме</span>} <span className="has-cinema-tv-nav-label">Аниме</span>
</button> </button>
<button <button
@ -2389,17 +2438,15 @@ export default function Cinema({
setActiveTab("bookmarks"); setActiveTab("bookmarks");
setSearchQuery(""); setSearchQuery("");
setIsTvNavOpen(false); setIsTvNavOpen(false);
focusFirstContentCard(); setTimeout(() => focusFirstContentCard(), 60);
}} }}
data-tv-focusable="true" data-tv-focusable="true"
title="Мой список" title="Мой список"
> >
<span className="has-cinema-tv-nav-icon">⭐</span> <span className="has-cinema-tv-nav-icon">⭐</span>
{isTvNavOpen && ( <span className="has-cinema-tv-nav-label">
<span className="has-cinema-tv-nav-label"> Мой список {cinemaState.bookmarks.length > 0 && `(${cinemaState.bookmarks.length})`}
Мой список {cinemaState.bookmarks.length > 0 && `(${cinemaState.bookmarks.length})`} </span>
</span>
)}
</button> </button>
{onNavigateTab && ( {onNavigateTab && (
@ -2414,7 +2461,7 @@ export default function Cinema({
title="Панель сервера" title="Панель сервера"
> >
<span className="has-cinema-tv-nav-icon">🖥️</span> <span className="has-cinema-tv-nav-icon">🖥️</span>
{isTvNavOpen && <span className="has-cinema-tv-nav-label">Панель сервера</span>} <span className="has-cinema-tv-nav-label">Панель сервера</span>
</button> </button>
)} )}
@ -2430,21 +2477,19 @@ export default function Cinema({
title="Выйти" title="Выйти"
> >
<span className="has-cinema-tv-nav-icon">🚪</span> <span className="has-cinema-tv-nav-icon">🚪</span>
{isTvNavOpen && <span className="has-cinema-tv-nav-label">Выйти</span>} <span className="has-cinema-tv-nav-label">Выйти</span>
</button> </button>
)} )}
</div> </div>
{isTvNavOpen && ( <div className="has-cinema-tv-nav-footer">
<div className="has-cinema-tv-nav-footer"> <span>▶ Вправо или Назад — закрыть меню</span>
<span>▶ Нажмите Вправо для перехода к фильмам</span> </div>
</div>
)}
</aside> </aside>
)} )}
{/* ======================================================== {/* ========================================================
1. TOP NAVIGATION BAR (All categories & search) 1. TOP NAVIGATION BAR (For Desktop / Mobile only; hidden in TV Mode)
======================================================== */} ======================================================== */}
<header className="has-cinema-header"> <header className="has-cinema-header">
<nav className="has-cinema-nav-links"> <nav className="has-cinema-nav-links">
@ -2506,17 +2551,6 @@ export default function Cinema({
</nav> </nav>
<div className="has-cinema-header-right"> <div className="has-cinema-header-right">
{isTvMode && (
<button
type="button"
className={`has-cinema-tv-menu-pill ${isTvNavOpen ? "active" : ""}`}
onClick={() => setIsTvNavOpen((prev) => !prev)}
title="Меню навигации (Пульт: ◀ или Влево)"
data-tv-focusable="true"
>
☰ Разделы
</button>
)}
{/* Universal Search Box */} {/* Universal Search Box */}
<div className="has-cinema-search-wrapper"> <div className="has-cinema-search-wrapper">
@ -2560,6 +2594,42 @@ export default function Cinema({
</div> </div>
</header> </header>
{/* ========================================================
TV SEARCH VIEW (When activeTab === "search" in TV Mode)
======================================================== */}
{isTvMode && activeTab === "search" && (
<section className="has-cinema-tv-search-page">
<div className="has-cinema-tv-search-box-wrap">
<span className="has-cinema-tv-search-lens">🔍</span>
<input
type="text"
className="has-cinema-tv-search-input-field"
placeholder="Введите название фильма, сериала или аниме..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
data-tv-focusable="true"
autoFocus
/>
{searchQuery && (
<button
type="button"
className="has-cinema-tv-search-clear-btn"
onClick={() => setSearchQuery("")}
data-tv-focusable="true"
title="Очистить"
>
✕
</button>
)}
</div>
{searchQuery.trim().length === 0 && (
<div className="has-cinema-tv-search-hint">
<span>💡 Введите название с клавиатуры или пульта. Нажмите Влево с края для вызова меню.</span>
</div>
)}
</section>
)}
{/* ======================================================== {/* ========================================================
SEARCH RESULTS FULL GRID VIEW (When Searching) SEARCH RESULTS FULL GRID VIEW (When Searching)
======================================================== */} ======================================================== */}