feat: align homepage with youtube layout
This commit is contained in:
1 parent
01b1114909
commit
e29fb4c859
5 files changed
+139
-10
No files matched your search
+1
-1
@@ -5,5 +5,5 @@ import { applySeo } from '../lib/seo';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const StoreContext = createContext(null);
|
||||
export function StoreProvider({ children }) { const initial = new URLSearchParams(location.search).get('lang'); const [lang, setLangState] = useState(initial || 'id'); const [query, setQuery] = useState(''); const [sidebarOpen, setSidebarOpen] = useState(false); const setLang = (next) => { setLangState(next); const url = new URL(location.href); url.searchParams.set('lang', next); history.replaceState(null, '', url); }; const locale = getLocale(lang); useEffect(() => applySeo(locale, location.pathname), [lang, locale]); const value = useMemo(() => ({ lang, locale, setLang, query, setQuery, sidebarOpen, setSidebarOpen }), [lang, locale, query, sidebarOpen]); return <StoreContext.Provider value={value}>{children}</StoreContext.Provider>; }
|
||||
export function StoreProvider({ children }) { const initial = new URLSearchParams(location.search).get('lang'); const [lang, setLangState] = useState(initial || 'id'); const [query, setQuery] = useState(''); const [sidebarOpen, setSidebarOpen] = useState(() => window.innerWidth > 760); const setLang = (next) => { setLangState(next); const url = new URL(location.href); url.searchParams.set('lang', next); history.replaceState(null, '', url); }; const locale = getLocale(lang); useEffect(() => applySeo(locale, location.pathname), [lang, locale]); const value = useMemo(() => ({ lang, locale, setLang, query, setQuery, sidebarOpen, setSidebarOpen }), [lang, locale, query, sidebarOpen]); return <StoreContext.Provider value={value}>{children}</StoreContext.Provider>; }
|
||||
export function useAppStore() { return useContext(StoreContext); }
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
// Author: fruit.com.cn
|
||||
export const locales = {
|
||||
id: { code: 'id', htmlLang: 'id', title: 'YMB · Cerita pendek, emosi besar', description: 'Temukan drama pendek terbaik dan episode baru setiap hari.', search: 'Cari drama, aktor, atau genre', home: 'Beranda', shorts: 'Shorts', subscriptions: 'Langganan', library: 'Perpustakaan', history: 'Riwayat', liked: 'Drama disukai', hero: 'Cerita pendek,', accent: 'emosi besar.', watch: 'Mulai menonton', picks: 'Drama pilihan minggu ini', shortTitle: 'Cerita dalam satu tarikan napas', channels: 'Ikuti cerita favoritmu', genres: ['Semua', 'Romansa', 'Balas dendam', 'Keluarga', 'Rahasia', 'Kisah kedua'], follow: 'Ikuti', following: 'Mengikuti', empty: 'Belum ada drama yang cocok.', loading: 'Memuat dari server…', apiFallback: 'Server belum tersedia, menampilkan katalog demo sementara.', playbackUnavailable: 'Pilih episode untuk mulai menonton.', swipeHint: 'Geser ke atas untuk menemukan cerita berikutnya' },
|
||||
en: { code: 'en', htmlLang: 'en', title: 'YMB · Short stories, big emotions', description: 'Discover the best short dramas and new episodes every day.', search: 'Search dramas, actors, or genres', home: 'Home', shorts: 'Shorts', subscriptions: 'Subscriptions', library: 'Library', history: 'History', liked: 'Liked dramas', hero: 'Short stories,', accent: 'big emotions.', watch: 'Watch now', picks: 'Drama picks this week', shortTitle: 'Stories in one breath', channels: 'Follow your favorite stories', genres: ['All', 'Romance', 'Revenge', 'Family', 'Secrets', 'Second chance'], follow: 'Follow', following: 'Following', empty: 'No dramas match your search.', loading: 'Loading from server…', apiFallback: 'Server unavailable, showing the demo catalog temporarily.', playbackUnavailable: 'Select an episode to start watching.', swipeHint: 'Swipe up to discover the next story' },
|
||||
zh: { code: 'zh', htmlLang: 'zh-CN', title: 'YMB · 短故事,大情绪', description: '发现精彩短剧,每天更新新剧集。', search: '搜索短剧、演员或题材', home: '首页', shorts: 'Shorts', subscriptions: '订阅', library: '媒体库', history: '观看记录', liked: '喜欢的短剧', hero: '短故事,', accent: '大情绪。', watch: '立即观看', picks: '本周精选短剧', shortTitle: '一口气看完的故事', channels: '关注你喜欢的故事', genres: ['全部', '爱情', '复仇', '家庭', '秘密', '重生'], follow: '关注', following: '已关注', empty: '没有找到匹配的短剧。', loading: '正在从服务端加载…', apiFallback: '服务端暂不可用,暂时展示演示目录。', playbackUnavailable: '请选择剧集开始播放。', swipeHint: '向上滑动发现下一个故事' }
|
||||
id: { code: 'id', htmlLang: 'id', title: 'YMB · Cerita pendek, emosi besar', description: 'Temukan drama pendek terbaik dan episode baru setiap hari.', search: 'Cari drama, aktor, atau genre', home: 'Beranda', shorts: 'Shorts', subscriptions: 'Langganan', library: 'Perpustakaan', history: 'Riwayat', liked: 'Drama disukai', hero: 'Cerita pendek,', accent: 'emosi besar.', watch: 'Mulai menonton', picks: 'Drama pilihan minggu ini', shortTitle: 'Cerita dalam satu tarikan napas', viewAll: 'Lihat semua →', explore: 'Jelajahi →', videoCount: 'video', channels: 'Ikuti cerita favoritmu', genres: ['Semua', 'Romansa', 'Balas dendam', 'Keluarga', 'Rahasia', 'Kisah kedua'], follow: 'Ikuti', following: 'Mengikuti', empty: 'Belum ada drama yang cocok.', loading: 'Memuat dari server…', apiFallback: 'Server belum tersedia, menampilkan katalog demo sementara.', playbackUnavailable: 'Pilih episode untuk mulai menonton.', swipeHint: 'Geser ke atas untuk menemukan cerita berikutnya' },
|
||||
en: { code: 'en', htmlLang: 'en', title: 'YMB · Short stories, big emotions', description: 'Discover the best short dramas and new episodes every day.', search: 'Search dramas, actors, or genres', home: 'Home', shorts: 'Shorts', subscriptions: 'Subscriptions', library: 'Library', history: 'History', liked: 'Liked dramas', hero: 'Short stories,', accent: 'big emotions.', watch: 'Watch now', picks: 'Drama picks this week', shortTitle: 'Stories in one breath', viewAll: 'View all →', explore: 'Explore →', videoCount: 'videos', channels: 'Follow your favorite stories', genres: ['All', 'Romance', 'Revenge', 'Family', 'Secrets', 'Second chance'], follow: 'Follow', following: 'Following', empty: 'No dramas match your search.', loading: 'Loading from server…', apiFallback: 'Server unavailable, showing the demo catalog temporarily.', playbackUnavailable: 'Select an episode to start watching.', swipeHint: 'Swipe up to discover the next story' },
|
||||
zh: { code: 'zh', htmlLang: 'zh-CN', title: 'YMB · 短故事,大情绪', description: '发现精彩短剧,每天更新新剧集。', search: '搜索短剧、演员或题材', home: '首页', shorts: 'Shorts', subscriptions: '订阅', library: '媒体库', history: '观看记录', liked: '喜欢的短剧', hero: '短故事,', accent: '大情绪。', watch: '立即观看', picks: '本周精选短剧', shortTitle: '一口气看完的故事', viewAll: '查看全部 →', explore: '探索 →', videoCount: '个视频', channels: '关注你喜欢的故事', genres: ['全部', '爱情', '复仇', '家庭', '秘密', '重生'], follow: '关注', following: '已关注', empty: '没有找到匹配的短剧。', loading: '正在从服务端加载…', apiFallback: '服务端暂不可用,暂时展示演示目录。', playbackUnavailable: '请选择剧集开始播放。', swipeHint: '向上滑动发现下一个故事' }
|
||||
};
|
||||
export function getLocale(code) { return locales[code] || locales.id; }
|
||||
@@ -1,12 +1,64 @@
|
||||
// Author: fruit.com.cn
|
||||
import { Bell, Bookmark, Compass, History, Home, Menu, Search, UserRound, X } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { NavLink, Outlet, useNavigate } from 'react-router-dom';
|
||||
import { useAppStore } from '../app/store';
|
||||
import { LanguageSwitcher } from '../components/LanguageSwitcher';
|
||||
|
||||
export function AppShell() {
|
||||
const { locale, lang, setLang, query, setQuery, sidebarOpen, setSidebarOpen } = useAppStore(); const navigate = useNavigate();
|
||||
const { locale, query, setQuery, sidebarOpen, setSidebarOpen } = useAppStore();
|
||||
const navigate = useNavigate();
|
||||
const [searchOpen, setSearchOpen] = useState(false);
|
||||
const navItems = [[Home, locale.home, '/'], [Compass, locale.shorts, '/shorts'], [UserRound, locale.subscriptions, '/subscriptions'], [Bookmark, locale.library, '/library'], [History, locale.history, '/history']];
|
||||
const submitSearch = (event) => { event.preventDefault(); navigate(`/search?q=${encodeURIComponent(query)}`); };
|
||||
return <><header className="topbar"><button className="mobile-only icon-button" onClick={() => setSidebarOpen(!sidebarOpen)} aria-label="Menu"><Menu /></button><NavLink className="brand" to="/"><img src="/brand/ymb-logo-blue.png" alt="YMB" /></NavLink><form className="search-form" onSubmit={submitSearch}><input value={query} onChange={(event) => setQuery(event.target.value)} placeholder={locale.search} aria-label={locale.search} /><button aria-label="Search"><Search size={19} /></button></form><div className="top-actions"><LanguageSwitcher /><Bell size={19} /><span className="profile-chip">N</span></div></header><aside className={`sidebar ${sidebarOpen ? 'open' : ''}`}><nav>{navItems.map(([Icon, label, to]) => <NavLink to={to} end={to === '/'} key={to}><Icon size={19} />{label}</NavLink>)}</nav><div className="nav-rule" /><small>GENRE POPULER</small>{locale.genres.slice(1).map((genre) => <button className="genre-link" key={genre} onClick={() => navigate(`/search?genre=${encodeURIComponent(genre)}`)}>{genre}</button>)}</aside><main className="content"><Outlet /></main><nav className="bottom-nav">{navItems.slice(0, 4).map(([Icon, label, to]) => <NavLink to={to} key={to}><Icon size={19} />{label}</NavLink>)}</nav>{sidebarOpen && <button className="scrim mobile-only" onClick={() => setSidebarOpen(false)} aria-label="Close"><X /></button>}</>;
|
||||
const submitSearch = (event) => {
|
||||
event.preventDefault();
|
||||
navigate(`/search?q=${encodeURIComponent(query)}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="topbar">
|
||||
<button className="icon-button topbar-menu" onClick={() => setSidebarOpen(!sidebarOpen)} aria-label="Menu" type="button">
|
||||
<Menu size={22} />
|
||||
</button>
|
||||
<NavLink className="brand" to="/" aria-label="YMB">
|
||||
<img src="/brand/ymb-logo-blue.png" alt="YMB" />
|
||||
</NavLink>
|
||||
<form className={`search-form ${searchOpen ? 'open' : ''}`} onSubmit={submitSearch}>
|
||||
<input value={query} onChange={(event) => setQuery(event.target.value)} placeholder={locale.search} aria-label={locale.search} />
|
||||
<button aria-label="Search" type="submit"><Search size={19} /></button>
|
||||
</form>
|
||||
<div className="top-actions">
|
||||
<button className="icon-button mobile-search" onClick={() => setSearchOpen(!searchOpen)} aria-label={locale.search} type="button"><Search size={20} /></button>
|
||||
<LanguageSwitcher />
|
||||
<Bell size={19} aria-hidden="true" />
|
||||
<span className="profile-chip" aria-label="Profile">N</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<aside className={`sidebar ${sidebarOpen ? 'open' : 'closed'}`}>
|
||||
<nav>
|
||||
{navItems.map(([Icon, label, to]) => (
|
||||
<NavLink to={to} end={to === '/'} key={to}>
|
||||
<Icon size={19} />
|
||||
<span className="sidebar-label">{label}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
<div className="nav-rule" />
|
||||
<small>GENRE POPULER</small>
|
||||
{locale.genres.slice(1).map((genre) => (
|
||||
<button className="genre-link" key={genre} onClick={() => navigate(`/search?genre=${encodeURIComponent(genre)}`)} type="button">
|
||||
<span className="sidebar-label">{genre}</span>
|
||||
</button>
|
||||
))}
|
||||
</aside>
|
||||
|
||||
<main className={`content ${sidebarOpen ? '' : 'content-wide'}`}><Outlet /></main>
|
||||
<nav className="bottom-nav">
|
||||
{navItems.slice(0, 4).map(([Icon, label, to]) => <NavLink to={to} key={to}><Icon size={19} />{label}</NavLink>)}
|
||||
</nav>
|
||||
{sidebarOpen && <button className="scrim mobile-only" onClick={() => setSidebarOpen(false)} aria-label="Close" type="button"><X /></button>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
+76
-3
@@ -1,13 +1,86 @@
|
||||
// Author: fruit.com.cn
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { MediaCard } from '../components/MediaCard';
|
||||
import { SectionHeading } from '../components/SectionHeading';
|
||||
import { videos } from '../data/catalog';
|
||||
import { useAppStore } from '../app/store';
|
||||
import { searchCatalog } from '../services/catalogService';
|
||||
import { appApi } from '../services/appApi';
|
||||
import { normalizeDrama, normalizePage, uniqueDramas } from '../services/normalizers';
|
||||
import { normalizeDrama, uniqueDramas } from '../services/normalizers';
|
||||
import { useApi } from '../hooks/useApi';
|
||||
|
||||
export function HomePage() { const { locale } = useAppStore(); const [genre, setGenre] = useState(locale.genres[0]); const [toast, setToast] = useState(''); const api = useApi(() => appApi.homeFeed().then((payload) => uniqueDramas(payload?.sections?.flatMap((section) => section.dramas || []).map((item) => normalizeDrama(item, locale.code)) || [])), [locale.code]); const remoteItems = api.data || []; const items = remoteItems.length ? remoteItems : searchCatalog('', genre, locale.genres.indexOf(genre)); const play = () => { setToast(locale.watch); setTimeout(() => setToast(''), 1400); }; return <><section className="hero-card"><div className="hero-copy"><small>YMB ORIGINAL</small><h1>{locale.hero}<br /><em>{locale.accent}</em></h1><p>{locale.description}</p><button className="primary-button" onClick={play}>▶ {locale.watch}</button></div><div className="hero-art"><img src="/assets/hero.jpg" alt="YMB drama" /></div></section><section><SectionHeading title={locale.picks} /><div className="chip-row">{locale.genres.map((item) => <button className={`chip ${genre === item ? 'active' : ''}`} key={item} onClick={() => setGenre(item)}>{item}</button>)}</div>{api.loading && <div className="api-note">{locale.loading}</div>}{api.error && <div className="api-note warning">{locale.apiFallback}</div>}<div className="video-grid">{items.map((video) => <MediaCard key={video.id} video={video} />)}</div></section><section><SectionHeading title={locale.shortTitle} action="Explore →" /><div className="shorts-row">{(remoteItems.length ? remoteItems.slice(0, 6) : videos.slice(1, 7)).map((video) => <MediaCard key={`short-${video.id}`} video={video} compact />)}</div></section>{toast && <div className="toast">{toast}</div>}</>;
|
||||
const SHORTS_LIMIT = 6;
|
||||
const ALL_GENRES_INDEX = 0;
|
||||
|
||||
function normalizeHomeFeed(payload, localeCode) {
|
||||
const sections = payload?.sections || [];
|
||||
const dramas = sections.flatMap((section) => section.dramas || []);
|
||||
return uniqueDramas(dramas.map((item) => normalizeDrama(item, localeCode)));
|
||||
}
|
||||
|
||||
function filterByGenre(items, genre, localeGenres) {
|
||||
if (!genre || genre === localeGenres[ALL_GENRES_INDEX]) return items;
|
||||
const filtered = items.filter((item) => item.genre === genre);
|
||||
return filtered.length ? filtered : items;
|
||||
}
|
||||
|
||||
export function HomePage() {
|
||||
const { locale } = useAppStore();
|
||||
const [genre, setGenre] = useState(locale.genres[ALL_GENRES_INDEX]);
|
||||
const api = useApi(
|
||||
() => appApi.homeFeed().then((payload) => normalizeHomeFeed(payload, locale.code)),
|
||||
[locale.code],
|
||||
);
|
||||
const remoteItems = api.data || [];
|
||||
const fallbackItems = searchCatalog('', genre, locale.genres.indexOf(genre));
|
||||
const items = useMemo(
|
||||
() => filterByGenre(remoteItems.length ? remoteItems : fallbackItems, genre, locale.genres),
|
||||
[fallbackItems, genre, locale.genres, remoteItems],
|
||||
);
|
||||
const shortsItems = (remoteItems.length ? remoteItems : videos).slice(0, SHORTS_LIMIT);
|
||||
|
||||
useEffect(() => {
|
||||
setGenre(locale.genres[ALL_GENRES_INDEX]);
|
||||
}, [locale.code, locale.genres]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="youtube-home-header" aria-labelledby="home-feed-title">
|
||||
<div>
|
||||
<span className="eyebrow">YMB HOME</span>
|
||||
<h1 id="home-feed-title">{locale.picks}</h1>
|
||||
<p>{locale.description}</p>
|
||||
</div>
|
||||
<span className="feed-count">{items.length} {locale.videoCount}</span>
|
||||
</section>
|
||||
|
||||
<section className="youtube-home-feed" aria-labelledby="recommended-title">
|
||||
<SectionHeading title={locale.picks} action={locale.viewAll} />
|
||||
<div className="chip-row" aria-label="Genre filters">
|
||||
{locale.genres.map((item) => (
|
||||
<button
|
||||
className={`chip ${genre === item ? 'active' : ''}`}
|
||||
key={item}
|
||||
type="button"
|
||||
onClick={() => setGenre(item)}
|
||||
>
|
||||
{item}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{api.loading && <div className="api-note">{locale.loading}</div>}
|
||||
{api.error && <div className="api-note warning">{locale.apiFallback}</div>}
|
||||
<div className="video-grid" id="recommended-title">
|
||||
{items.map((video) => <MediaCard key={video.id} video={video} />)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="youtube-shorts-shelf" aria-labelledby="shorts-title">
|
||||
<SectionHeading title={locale.shortTitle} action={locale.explore} />
|
||||
<div className="shorts-row" id="shorts-title">
|
||||
{shortsItems.map((video) => <MediaCard key={`short-${video.id}`} video={video} compact />)}
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -3,3 +3,7 @@
|
||||
:root{--bg:#0f0f0f;--surface:#181818;--line:#303030;--text:#f1f1f1;--muted:#aaa;--accent:#ff4b3e;--sidebar:230px}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:14px Arial,sans-serif}a{color:inherit;text-decoration:none}button,input{font:inherit}.topbar{height:64px;position:sticky;top:0;z-index:3;display:flex;align-items:center;gap:24px;padding:0 28px;background:#0f0f0ff2;border-bottom:1px solid #222}.brand{display:flex;align-items:center;gap:8px;font-size:26px;font-weight:700;min-width:150px}.brand-symbol{display:grid;place-items:center;width:28px;height:28px;border-radius:8px;background:var(--accent);font-size:12px}.search-form{display:flex;max-width:620px;flex:1;margin:auto}.search-form input{width:100%;height:40px;padding:0 16px;border:1px solid #3b3b3b;border-radius:22px 0 0 22px;background:#121212;color:#fff}.search-form button{width:54px;border:1px solid #3b3b3b;border-left:0;border-radius:0 22px 22px 0;background:#282828;color:#fff;font-size:24px}.top-actions{display:flex;align-items:center;gap:10px}.icon-button{border:0;background:transparent;color:#fff;font-size:22px;cursor:pointer}.profile-chip{border:0;border-radius:50%;width:32px;height:32px;background:#d45a68;color:#fff}.sidebar{position:fixed;top:64px;bottom:0;width:var(--sidebar);padding:20px 12px;background:var(--bg);border-right:1px solid #222}.main-nav,.genre-nav{display:grid;gap:4px}.nav-item,.genre-nav a{display:flex;gap:18px;align-items:center;padding:11px 14px;border-radius:10px;color:#ccc}.nav-item:hover,.nav-item.active,.genre-nav a:hover{background:#272727;color:#fff}.nav-item span{width:18px;text-align:center;font-size:20px}.nav-item b{font-weight:400}.nav-rule{height:1px;background:var(--line);margin:18px 10px}.nav-label{padding:0 14px;color:#888;font-size:11px;text-transform:uppercase;letter-spacing:.1em}.sidebar-footer{position:absolute;bottom:24px;padding:0 14px;color:#666;font-size:11px;line-height:1.8}.content{max-width:1500px;padding:28px 34px 80px calc(var(--sidebar) + 34px);margin:auto}.hero-card{position:relative;min-height:350px;overflow:hidden;border-radius:18px;background:#27191a;margin-bottom:44px}.hero-copy{position:relative;z-index:1;width:52%;padding:54px}.eyebrow{color:#ff786f;font-size:11px;font-weight:700;letter-spacing:.16em}.hero-copy h1{margin:12px 0;font-size:56px;line-height:.98}.hero-copy h1 em{color:#ff786f;font-style:normal}.hero-copy p{max-width:390px;color:#ccc;line-height:1.7}.hero-art{position:absolute;inset:0 0 0 40%}.hero-art img{width:100%;height:100%;object-fit:cover}.hero-gradient{position:absolute;inset:0;background:linear-gradient(90deg,#27191a,rgba(39,25,26,.75) 26%,transparent 80%)}.hero-actions{display:flex;gap:10px}.primary-button,.ghost-button,.chip{border:0;border-radius:22px;padding:11px 17px;cursor:pointer;font-weight:600}.primary-button{background:#fff;color:#111}.ghost-button,.chip{background:#2b2b2b;color:#fff}.section-block{margin-bottom:46px}.section-heading{display:flex;justify-content:space-between;align-items:end;margin-bottom:17px}.section-heading h2{margin:7px 0 0;font-size:24px}.see-all{color:#ff8a82;font-size:13px}.chip-row{display:flex;gap:8px;overflow:auto;margin-bottom:18px}.chip.active,.chip:hover{background:#fff;color:#111}.video-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:24px 16px}.thumb{position:relative;aspect-ratio:16/9;overflow:hidden;border-radius:10px;background:#222}.thumb img,.short-card img{width:100%;height:100%;object-fit:cover}.duration{position:absolute;right:7px;bottom:7px;padding:3px 6px;background:#000c;border-radius:4px;font-size:11px}.video-meta{display:flex;gap:9px;padding-top:9px}.channel-avatar{display:grid;place-items:center;flex:none;width:32px;height:32px;border-radius:50%;background:#734c67;font-weight:700}.video-title{margin:0;line-height:1.4;font-weight:600}.video-sub{margin:4px 0 0;color:#999;font-size:11px;line-height:1.45}.shorts-row{display:grid;grid-template-columns:repeat(6,1fr);gap:14px}.short-card{position:relative;aspect-ratio:9/14;overflow:hidden;border-radius:11px}.short-card:after{content:'';position:absolute;inset:45% 0 0;background:linear-gradient(transparent,#000c)}.short-title{position:absolute;z-index:1;bottom:10px;left:9px;right:9px;font-size:12px;font-weight:600}.channel-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.channel-card{display:flex;align-items:center;gap:10px;padding:14px;border:1px solid var(--line);border-radius:11px}.channel-card p{margin:0 0 4px;font-weight:600}.channel-card span{color:#888;font-size:11px}.follow-button{margin-left:auto;border:0;border-radius:18px;padding:8px 11px;background:#fff;color:#111}.bottom-nav,.mobile-only{display:none}.toast{position:fixed;left:50%;bottom:24px;transform:translate(-50%,20px);opacity:0;background:#fff;color:#111;padding:11px 16px;border-radius:7px;transition:.2s}.toast.show{opacity:1;transform:translate(-50%,0)}
|
||||
@media(max-width:1100px){.video-grid{grid-template-columns:repeat(3,1fr)}.shorts-row{grid-template-columns:repeat(5,1fr)}.channel-grid{grid-template-columns:repeat(2,1fr)}}
|
||||
@media(max-width:760px){.topbar{height:58px;padding:0 14px;gap:12px}.mobile-only{display:block}.desktop-only,.sidebar{display:none}.brand{min-width:auto;font-size:23px}.search-form{position:absolute;top:65px;left:14px;right:14px;display:none}.search-form.open{display:flex}.content{padding:20px 14px 90px}.hero-card{min-height:420px;margin:0 -14px 36px;border-radius:0;display:flex;align-items:flex-end}.hero-copy{width:100%;padding:24px}.hero-copy h1{font-size:42px}.hero-art{inset:0}.hero-gradient{background:linear-gradient(0deg,#27191a,rgba(39,25,26,.45) 55%,transparent)}.section-heading h2{font-size:21px}.video-grid{grid-template-columns:repeat(2,1fr);gap:22px 12px}.shorts-row{display:flex;overflow:auto;margin-right:-14px}.short-card{flex:0 0 130px}.channel-grid{grid-template-columns:1fr}.bottom-nav{position:fixed;display:flex;justify-content:space-around;align-items:center;left:0;right:0;bottom:0;height:66px;z-index:3;background:#151515;border-top:1px solid #292929}.bottom-nav a{display:grid;place-items:center;gap:4px;color:#aaa;font-size:10px}.bottom-nav span{font-size:20px}.bottom-nav .active{color:#fff}.bottom-nav .active span{color:var(--accent)}}
|
||||
|
||||
/* Author: fruit.com.cn */
|
||||
.topbar-menu{display:grid;place-items:center;flex:none;width:40px;height:40px;border-radius:50%;cursor:pointer}.topbar-menu:hover{background:#272727}.mobile-search{display:none}.sidebar{transition:width .2s,padding .2s}.sidebar.closed{width:84px;padding-left:10px;padding-right:10px}.sidebar.closed nav a,.sidebar.closed .genre-link{justify-content:center;padding-left:10px;padding-right:10px}.sidebar.closed .sidebar-label,.sidebar.closed small,.sidebar.closed .nav-rule{display:none}.content{transition:padding-left .2s}.content-wide{padding-left:112px}.youtube-home-header{display:flex;align-items:end;justify-content:space-between;gap:20px;padding:4px 0 22px;border-bottom:1px solid var(--line);margin-bottom:22px}.youtube-home-header h1{margin:7px 0 5px;font:700 30px Outfit}.youtube-home-header p{margin:0;color:var(--muted)}.youtube-home-header .feed-count{color:#888;font-size:12px;white-space:nowrap}.youtube-home-feed,.youtube-shorts-shelf{margin-bottom:46px}.youtube-home-feed .section-heading button,.youtube-shorts-shelf .section-heading button{border:0;background:transparent;color:#aaa;cursor:pointer}.youtube-home-feed .section-heading button:hover,.youtube-shorts-shelf .section-heading button:hover{color:#fff}.youtube-home-feed .video-grid{margin-top:2px}
|
||||
@media(max-width:760px){.topbar-menu{width:34px;height:34px}.mobile-search{display:grid}.top-actions>svg{display:none}.top-actions{margin-left:auto}.sidebar{display:none;top:58px;bottom:66px;width:246px;padding:16px 12px;box-shadow:16px 0 30px #0008}.sidebar.open{display:block}.sidebar.closed{display:none}.content,.content-wide{padding:18px 14px 90px}.search-form{z-index:8}.search-form.open{display:flex}.youtube-home-header{align-items:flex-start;padding:0 0 18px}.youtube-home-header h1{font-size:24px}.youtube-home-header .feed-count{padding-top:30px}.youtube-home-feed,.youtube-shorts-shelf{margin-bottom:34px}}
|
||||
Reference in new issue
Block a user