From 18bf3adb1f29387a10ff3ae55b8e7fdeac573424 Mon Sep 17 00:00:00 2001 From: Frank Stellmacher <171614930+Psychotoxical@users.noreply.github.com> Date: Wed, 13 May 2026 19:15:40 +0200 Subject: [PATCH] refactor(styles): split layout.css into per-section files (#658) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit layout.css (3209 LOC) → 29 per-section files in src/styles/layout/ + an index.css that imports them in original cascade order. Same mechanic as theme.css + components.css splits: top-level sections detected by single-dash or 3+ dash header decoration. Concatenating in @import order reproduces the original byte stream (+1 trailing newline, cosmetic). Generated via /tmp/split-layout-css.mjs. --- src/main.tsx | 2 +- src/styles/layout.css | 3209 ----------------- src/styles/layout/_intro.css | 42 + .../layout/app-updater-toast-mobile.css | 5 + src/styles/layout/bottomnav-component.css | 80 + src/styles/layout/content-header-mobile.css | 16 + src/styles/layout/cover-art.css | 26 + ...title-bar-linux-only-decorations-false.css | 139 + src/styles/layout/empty-state.css | 12 + src/styles/layout/eq-popup.css | 59 + src/styles/layout/grid-overrides.css | 26 + src/styles/layout/header.css | 36 + src/styles/layout/index.css | 29 + src/styles/layout/main-content.css | 122 + src/styles/layout/mobile-more-sheet.css | 87 + src/styles/layout/mobile-player-takeover.css | 41 + src/styles/layout/offline-banner.css | 41 + .../layout/offline-storage-full-banner.css | 34 + ...llarea-shared-queue-mini-sidebar-menus.css | 191 + src/styles/layout/player-bar-mobile.css | 41 + src/styles/layout/player-bar.css | 428 +++ src/styles/layout/queue-panel.css | 338 ++ src/styles/layout/resizer-handles.css | 54 + src/styles/layout/scrubber.css | 58 + .../layout/sidebar-device-sync-queue.css | 16 + .../sidebar-expandable-playlists-section.css | 140 + .../layout/sidebar-offline-download-queue.css | 42 + src/styles/layout/sidebar.css | 444 +++ src/styles/layout/track-metadata.css | 57 + src/styles/layout/transport-controls.css | 50 + src/styles/layout/update-toast.css | 394 ++ src/styles/layout/utility-footer.css | 191 + 32 files changed, 3240 insertions(+), 3210 deletions(-) delete mode 100644 src/styles/layout.css create mode 100644 src/styles/layout/_intro.css create mode 100644 src/styles/layout/app-updater-toast-mobile.css create mode 100644 src/styles/layout/bottomnav-component.css create mode 100644 src/styles/layout/content-header-mobile.css create mode 100644 src/styles/layout/cover-art.css create mode 100644 src/styles/layout/custom-title-bar-linux-only-decorations-false.css create mode 100644 src/styles/layout/empty-state.css create mode 100644 src/styles/layout/eq-popup.css create mode 100644 src/styles/layout/grid-overrides.css create mode 100644 src/styles/layout/header.css create mode 100644 src/styles/layout/index.css create mode 100644 src/styles/layout/main-content.css create mode 100644 src/styles/layout/mobile-more-sheet.css create mode 100644 src/styles/layout/mobile-player-takeover.css create mode 100644 src/styles/layout/offline-banner.css create mode 100644 src/styles/layout/offline-storage-full-banner.css create mode 100644 src/styles/layout/overlayscrollarea-shared-queue-mini-sidebar-menus.css create mode 100644 src/styles/layout/player-bar-mobile.css create mode 100644 src/styles/layout/player-bar.css create mode 100644 src/styles/layout/queue-panel.css create mode 100644 src/styles/layout/resizer-handles.css create mode 100644 src/styles/layout/scrubber.css create mode 100644 src/styles/layout/sidebar-device-sync-queue.css create mode 100644 src/styles/layout/sidebar-expandable-playlists-section.css create mode 100644 src/styles/layout/sidebar-offline-download-queue.css create mode 100644 src/styles/layout/sidebar.css create mode 100644 src/styles/layout/track-metadata.css create mode 100644 src/styles/layout/transport-controls.css create mode 100644 src/styles/layout/update-toast.css create mode 100644 src/styles/layout/utility-footer.css diff --git a/src/main.tsx b/src/main.tsx index d3c454de..2da1189e 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,7 +4,7 @@ import App from './App'; import { runPreReactBootstrap } from './app/bootstrap'; import './i18n'; import './styles/themes/index.css'; -import './styles/layout.css'; +import './styles/layout/index.css'; import './styles/components/index.css'; import './styles/tracks.css'; diff --git a/src/styles/layout.css b/src/styles/layout.css deleted file mode 100644 index 9754a1dc..00000000 --- a/src/styles/layout.css +++ /dev/null @@ -1,3209 +0,0 @@ -/* ─── Psysonic App Shell ─── */ - -*, *::before, *::after { - user-select: none; -} - -/* Allow text selection only where the user needs to copy content */ -.artist-bio, [data-selectable] { - user-select: text; -} - -.app-shell { - position: relative; - display: grid; - grid-template-columns: var(--sidebar-width) 1fr var(--queue-width); - /* minmax(0, 1fr) instead of plain 1fr: CSS Grid spec treats 1fr as - minmax(auto, 1fr), meaning the row won't shrink below its min-content - size. With minmax(0, 1fr) the row can collapse to zero, keeping the - player bar row always visible regardless of content size or zoom level. */ - grid-template-rows: minmax(0, 1fr) var(--player-height); - grid-template-areas: - "sidebar main queue" - "player player player"; - /* height: 100% inherits from html/body/#root (all 100%) so the grid - tracks actual window resize events on every platform. - 100vh was used previously but WebView2 (Windows) does not update the - CSS viewport synchronously on resize, causing the player-bar grid row - to drift off-screen. overflow: hidden still prevents the 1fr row from - pushing the grid taller than the window on Linux WMs that ignore - the OS minHeight constraint. */ - height: 100%; - overflow: hidden; - background: var(--bg-app); -} - -/* When player bar is floating, content extends to bottom */ -.app-shell.floating-player { - grid-template-rows: minmax(0, 1fr); - grid-template-areas: - "sidebar main queue"; -} - -/* ─── Custom title bar (Linux only — decorations: false) ─── */ -:root { - --titlebar-height: 32px; -} - -.app-shell[data-titlebar] { - grid-template-rows: var(--titlebar-height) minmax(0, 1fr) var(--player-height); - grid-template-areas: - "titlebar titlebar titlebar" - "sidebar main queue" - "player player player"; -} - -/* When player bar is floating with titlebar, content extends to bottom */ -.app-shell[data-titlebar].floating-player { - grid-template-rows: var(--titlebar-height) minmax(0, 1fr); - grid-template-areas: - "titlebar titlebar titlebar" - "sidebar main queue"; -} - -/* Resize grips — replace the native GTK grips lost when decorations: false */ -.app-shell[data-titlebar]::before, -.app-shell[data-titlebar]::after { - content: ''; - position: fixed; - bottom: 0; - width: 14px; - height: 14px; - pointer-events: none; - background-image: radial-gradient(circle, var(--text-muted) 1.5px, transparent 1.5px); - background-size: 4px 4px; - opacity: 0.35; - z-index: 9999; -} -.app-shell[data-titlebar]::before { left: 0; } -.app-shell[data-titlebar]::after { right: 0; } - -.titlebar { - grid-area: titlebar; - display: flex; - align-items: center; - justify-content: flex-end; - background: var(--bg-sidebar); - border-bottom: 1px solid var(--border-subtle); - padding: 0 6px 0 12px; - height: var(--titlebar-height); - user-select: none; -} - -.titlebar-track { - position: absolute; - left: 50%; - transform: translateX(-50%); - display: flex; - align-items: center; - gap: 6px; - max-width: 40%; - pointer-events: none; - padding: 3px 10px; - background: rgba(0, 0, 0, 0.45); - border-radius: 999px; - box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08); -} - -.titlebar-track:empty { - display: none; -} - -.titlebar-track-state { - font-size: 9px; - color: var(--accent); - flex-shrink: 0; - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.6); -} - -.titlebar-track-text { - font-size: 11px; - color: #ececec; - max-width: 100%; - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.5); -} - -.titlebar-controls { - display: flex; - align-items: center; - gap: 8px; - padding: 0 8px; - flex: 0 0 auto; -} - -.titlebar-btn { - display: flex; - align-items: center; - justify-content: center; - width: 14px; - height: 14px; - padding: 0; - border: none; - border-radius: 50%; - cursor: pointer; - box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.18); - transition: filter var(--transition-fast), box-shadow var(--transition-fast); -} - -.titlebar-btn-close { - background: #ff5f57; -} - -.titlebar-btn-minimize { - background: #febc2e; -} - -.titlebar-btn-maximize { - background: #28c840; -} - -.titlebar-btn:hover { - filter: brightness(1.1); -} - -.titlebar-btn-close:hover { box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.18), 0 0 6px rgba(255, 95, 87, 0.75); } -.titlebar-btn-minimize:hover { box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.18), 0 0 6px rgba(254, 188, 46, 0.75); } -.titlebar-btn-maximize:hover { box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.18), 0 0 6px rgba(40, 200, 64, 0.75); } - -.titlebar-btn:active { - filter: brightness(0.92); -} - -.titlebar-btn:focus-visible { - outline: none; - box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.18), 0 0 0 2px rgba(255, 255, 255, 0.5); -} - -/* Resizer handles must start below the titlebar */ -.app-shell[data-titlebar] .resizer { - top: var(--titlebar-height); -} - -/* ─── Resizer Handles ─── */ -.resizer { - position: absolute; - top: 0; - bottom: var(--player-height); - width: 6px; - cursor: col-resize; - z-index: 50; - background: transparent; - transition: background var(--transition-fast); -} - -.resizer:hover, .resizer:active { - background: var(--accent); -} - -.resizer-queue { right: calc(var(--queue-width) - 3px); } -.resizer-queue { overflow: visible; } -/* Queue handle has its own visual affordance; don't paint seam stripe on hover. */ -.resizer-queue:hover, .resizer-queue:active { background: transparent; } - -.resizer-queue-handle { - z-index: 101; - width: 22px; - height: 22px; - border-radius: 50%; - border: 1px solid var(--border-subtle); - background: var(--bg-card); - color: var(--text-muted); - cursor: col-resize; - display: flex; - align-items: center; - justify-content: center; - transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease; - box-shadow: var(--shadow-sm); -} - -.resizer-queue-handle:hover { - background: var(--bg-hover); - color: var(--text-primary); - box-shadow: var(--shadow-md); -} - -/* While dragging an overlay scrollbar thumb, hide queue resizer so the - pointer does not pick up col-resize / queue resize (thumb sits under the - resizer hit box near the main/queue edge). */ -body.is-overlay-scrollbar-thumb-drag .resizer { - pointer-events: none !important; -} - -/* In native fullscreen the resizer sits flush against the screen edge and - would interfere with OS edge-swipe gestures — hide it entirely. */ -.app-shell[data-fullscreen] .resizer { display: none; } - -/* ─── Sidebar ─── */ -.sidebar { - grid-area: sidebar; - display: flex; - flex-direction: column; - background: var(--bg-sidebar); - border-right: 1px solid var(--border-subtle); - position: relative; - z-index: 2; - min-height: 0; /* allow 1fr row to shrink freely */ -} - -.sidebar-brand { - display: flex; - align-items: center; - gap: var(--space-3); - padding: 0 var(--space-5); - height: 64px; - box-sizing: border-box; - border-bottom: 1px solid var(--border-subtle); -} - -.sidebar-brand .logo-icon { - width: 32px; - height: 32px; - flex-shrink: 0; -} - -.sidebar-brand .brand-name { - font-family: var(--font-display); - font-size: 20px; - font-weight: 700; - letter-spacing: -0.02em; - background: linear-gradient(135deg, var(--accent), var(--ctp-blue)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.sidebar-nav { - flex: 1; - min-height: 0; - display: flex; - flex-direction: column; - overflow: hidden; -} - -.sidebar-nav-scroll.overlay-scroll { - flex: 1; - min-height: 0; -} - -.sidebar-nav-scroll .overlay-scroll__viewport.sidebar-nav-viewport { - padding: var(--space-1) var(--space-3) var(--space-4); - display: flex; - flex-direction: column; - gap: var(--space-1); -} - -/* Library scope: Navidrome-style dropdown trigger + fixed panel (portal) */ -.nav-library-scope-trigger { - display: flex; - align-items: flex-start; - gap: var(--space-2); - width: calc(100% - 2 * var(--space-3)); - margin: var(--space-3) var(--space-3) var(--space-2); - padding: var(--space-2) var(--space-2); - border: 1px solid var(--border-subtle, rgba(255, 255, 255, 0.08)); - border-radius: var(--radius-md); - background: var(--bg-tertiary, rgba(0, 0, 0, 0.2)); - color: var(--text-secondary); - cursor: pointer; - text-align: left; - transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast); -} - -.nav-library-scope-trigger:hover { - background: var(--bg-hover); - color: var(--text-primary); - border-color: var(--border-default, rgba(255, 255, 255, 0.12)); -} - -/* «Все библиотеки»: как секция навигации, без рамки и подложки */ -.nav-library-scope-trigger--plain { - border-color: transparent; - background: transparent; - align-items: center; -} - -.nav-library-scope-trigger--plain:hover { - border-color: transparent; -} - -.nav-library-scope-trigger--plain .nav-library-scope-chevron { - margin-top: 0; -} - -.nav-library-scope-trigger--open:not(.nav-library-scope-trigger--plain) { - border-color: var(--accent, var(--border-default)); -} - -.nav-library-scope-trigger--open.nav-library-scope-trigger--plain { - border-color: var(--border-subtle, rgba(255, 255, 255, 0.08)); - background: var(--bg-tertiary, rgba(0, 0, 0, 0.2)); -} - -.nav-library-scope-icon { - flex-shrink: 0; - margin-top: 2px; - opacity: 0.85; -} - -.nav-library-scope-text { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - gap: 2px; -} - -.nav-library-scope-title { - font-size: 10px; - font-weight: 600; - letter-spacing: 0.1em; - text-transform: uppercase; - color: var(--text-muted); -} - -.nav-library-scope-subtitle { - font-size: 12px; - font-weight: 500; - color: var(--text-primary); - line-height: 1.25; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.nav-library-scope-chevron { - flex-shrink: 0; - margin-top: 2px; - opacity: 0.7; - transition: transform var(--transition-fast); -} - -.nav-library-scope-trigger--open .nav-library-scope-chevron { - transform: rotate(180deg); -} - -.nav-library-dropdown-panel { - z-index: 10050; - display: flex; - flex-direction: column; - padding: var(--space-1); - border-radius: var(--radius-md); - border: 1px solid var(--border-dropdown, rgba(255, 255, 255, 0.12)); - background: var(--bg-card); - box-shadow: 0 8px 24px var(--shadow-dropdown, rgba(0, 0, 0, 0.45)); - box-sizing: border-box; - overflow-y: visible; -} - -/* >10 папок: «Все библиотеки» + до 10 строк папок, дальше — прокрутка */ -.nav-library-dropdown-panel--many-libraries { - --nav-lib-dropdown-row-h: calc(2 * var(--space-2) + 1.35rem); - max-height: min( - calc(11 * var(--nav-lib-dropdown-row-h) + 2 * var(--space-1)), - 70vh - ); - overflow-y: auto; -} - -.nav-library-dropdown-item { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--space-3); - width: 100%; - margin: 0; - padding: var(--space-2) var(--space-3); - border: none; - border-radius: var(--radius-sm); - background: transparent; - color: var(--text-primary); - font-size: 13px; - font-weight: 500; - text-align: left; - cursor: pointer; - transition: background var(--transition-fast); -} - -.nav-library-dropdown-item:hover { - background: var(--bg-hover); -} - -.nav-library-dropdown-item--selected { - color: var(--accent); -} - -.nav-library-dropdown-item-label { - flex: 1; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.nav-library-dropdown-check { - flex-shrink: 0; - color: var(--accent); -} - -.nav-library-dropdown-check-spacer { - flex-shrink: 0; - width: 16px; - height: 16px; -} - -.nav-section-label { - font-size: 10px; - font-weight: 600; - letter-spacing: 0.1em; - text-transform: uppercase; - color: var(--text-muted); - padding: var(--space-4) var(--space-3) var(--space-2); -} - -/* Long-press reorder targets (library / system blocks in Sidebar) */ -.sidebar-nav-dnd-row { - position: relative; - border-radius: var(--radius-md); -} - -.sidebar-nav-dnd-row--dragging { - opacity: 0.45; -} - -.sidebar-nav-dnd-row--drop-before::before, -.sidebar-nav-dnd-row--drop-after::after { - content: ''; - position: absolute; - left: 6px; - right: 6px; - height: 2px; - background: var(--accent); - border-radius: 1px; - pointer-events: none; - z-index: 3; -} - -.sidebar-nav-dnd-row--drop-before::before { - top: 0; -} - -.sidebar-nav-dnd-row--drop-after::after { - bottom: 0; -} - -/* Drag-out-of-sidebar: hide item — trash follows pointer */ -.sidebar-nav-dnd-trash-hint { - display: flex; - align-items: center; - justify-content: center; - width: 48px; - height: 48px; - border-radius: 50%; - pointer-events: none; - z-index: 100002; - color: var(--danger, #e05555); - background: color-mix(in srgb, var(--danger, #e05555) 18%, var(--bg-card, #1e1e2e)); - border: 1px solid color-mix(in srgb, var(--danger, #e05555) 45%, transparent); - box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45); -} - -.nav-link { - display: flex; - align-items: center; - gap: var(--space-3); - padding: var(--space-3) var(--space-3); - border-radius: var(--radius-md); - color: var(--text-secondary); - font-size: 14px; - font-weight: 500; - transition: all var(--transition-fast); - cursor: pointer; - text-decoration: none; - position: relative; -} - -.nav-link:hover { - background: var(--bg-hover); - color: var(--text-primary); -} - -.nav-link.active { - background: var(--accent-dim); - color: var(--accent); -} - -.nav-link.active::before { - content: ''; - position: absolute; - left: 0; - top: 50%; - transform: translateY(-50%); - width: 3px; - height: 60%; - background: var(--accent); - border-radius: 0 var(--radius-full) var(--radius-full) 0; -} - -.nav-link svg { - flex-shrink: 0; - opacity: 0.7; - transition: opacity var(--transition-fast); -} - -.nav-link.active svg, -.nav-link:hover svg { - opacity: 1; -} - -/* Now Playing nav link — subtly accented but not permanently "active"-looking */ -.nav-link-nowplaying { - color: var(--accent); - font-weight: 600; -} -.nav-link-nowplaying:hover { - background: var(--bg-hover); - color: var(--accent); -} -.nav-link-nowplaying.active { - background: var(--accent-dim); - color: var(--accent); -} -.nav-link-nowplaying svg { - opacity: 1; -} - -.nav-np-icon-wrap { - position: relative; - display: flex; - align-items: center; - flex-shrink: 0; -} -.nav-np-dot { - position: absolute; - top: -3px; - right: -4px; - width: 7px; - height: 7px; - border-radius: 50%; - background: var(--accent); - animation: np-dot-pulse 1.8s ease-in-out infinite; -} -@keyframes np-dot-pulse { - 0%, 100% { opacity: 1; transform: scale(1); } - 50% { opacity: 0.4; transform: scale(0.7); } -} - -.sidebar-nav-unread-badge { - margin-left: auto; - min-width: 18px; - height: 18px; - padding: 0 5px; - border-radius: 999px; - background: var(--accent); - color: var(--ctp-crust, #11111b); - font-size: 10px; - font-weight: 700; - line-height: 18px; - text-align: center; - box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 38%, transparent); -} - -/* Collapsed Sidebar Styles */ -.sidebar.collapsed .sidebar-brand { - justify-content: center; - padding: 0 var(--space-3); -} - -.sidebar.collapsed .nav-link { - justify-content: center; - padding: var(--space-3) 0; -} - -.sidebar.collapsed .sidebar-nav-unread-badge { - position: absolute; - top: 3px; - right: 7px; - margin-left: 0; - min-width: 16px; - height: 16px; - padding: 0 4px; - line-height: 16px; - font-size: 9px; -} - -.collapse-btn { - position: absolute; - top: calc(50% - var(--player-height) / 2); - left: calc(var(--sidebar-width) - 11px); - transform: translateY(-50%); - z-index: 101; - width: 22px; - height: 22px; - border-radius: 50%; - border: 1px solid var(--border-subtle); - background: var(--bg-card); - color: var(--text-muted); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease; - box-shadow: var(--shadow-sm); -} - -.collapse-btn:hover { - background: var(--bg-hover); - color: var(--text-primary); - box-shadow: var(--shadow-md); -} - -.queue-toggle-btn { - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border-radius: var(--radius-md); - border: none; - background: transparent; - color: var(--accent); - cursor: pointer; - transition: background 0.15s ease, color 0.15s ease; - flex-shrink: 0; -} - -.queue-toggle-btn:hover { - background: var(--accent-dim); - color: var(--accent); -} - -/* ─── Update Toast ─── */ -@keyframes update-toast-in { - from { opacity: 0; transform: translateY(6px); } - to { opacity: 1; transform: translateY(0); } -} - -/* ── AppUpdater floating toast ── */ -.app-updater-toast { - position: fixed; - bottom: calc(var(--player-height, 80px) + 12px); - left: 12px; - width: 210px; - padding: var(--space-3); - border-radius: var(--radius-md); - background: var(--bg-sidebar); - border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent); - box-shadow: 0 4px 20px rgba(0,0,0,0.35); - display: flex; - flex-direction: column; - gap: 4px; - z-index: 9000; - animation: update-toast-in 0.35s ease both; -} -.app-updater-header { - display: flex; - align-items: center; - gap: var(--space-2); - color: var(--accent); -} -.app-updater-label { - font-size: 11px; - font-weight: 700; - letter-spacing: 0.04em; - text-transform: uppercase; - flex: 1; -} -.app-updater-dismiss { - background: none; - border: none; - cursor: pointer; - color: var(--text-muted); - padding: 0; - display: flex; - align-items: center; - opacity: 0.6; - transition: opacity var(--transition-fast); -} -.app-updater-dismiss:hover { opacity: 1; } -.app-updater-version { - font-size: 12px; - color: var(--text-secondary); - padding-left: 19px; -} -.app-updater-actions { - padding-left: 19px; - margin-top: 2px; - display: flex; - flex-direction: column; - gap: 4px; -} -.app-updater-hint { - font-size: 10px; - color: var(--text-muted); - font-style: italic; - margin: 0 0 2px; -} -.app-updater-error { - font-size: 10px; - color: var(--danger); - background: color-mix(in srgb, var(--danger) 12%, transparent); - border-radius: 4px; - padding: 4px 6px; - margin-bottom: 4px; - word-break: break-all; -} -.app-updater-btn-primary { - display: inline-flex; - align-items: center; - gap: 5px; - font-size: 11px; - font-weight: 600; - color: var(--accent); - background: none; - border: none; - cursor: pointer; - padding: 0; - opacity: 0.85; - transition: opacity var(--transition-fast); -} -.app-updater-btn-primary:hover { opacity: 1; } -.app-updater-btn-secondary { - display: inline-flex; - align-items: center; - gap: 5px; - font-size: 11px; - font-weight: 400; - color: var(--text-secondary); - background: none; - border: none; - cursor: pointer; - padding: 0; - opacity: 0.75; - transition: opacity var(--transition-fast); -} -.app-updater-btn-secondary:hover { opacity: 1; color: var(--text-primary); } -.app-updater-progress-wrap { - display: flex; - align-items: center; - gap: var(--space-2); - padding-left: 19px; - margin-top: 2px; -} -.app-updater-progress-bar { - flex: 1; - height: 3px; - background: var(--bg-glass); - border-radius: 2px; - overflow: hidden; -} -.app-updater-progress-fill { - height: 100%; - background: var(--accent); - border-radius: 2px; - transition: width 0.15s ease; -} -.app-updater-pct { - font-size: 10px; - color: var(--text-muted); - min-width: 26px; - text-align: right; -} -.app-updater-status { - font-size: 11px; - color: var(--text-secondary); - padding-left: 19px; - font-style: italic; -} - -/* ── Update Modal ────────────────────────────────────────────────────────── */ -.update-modal { - width: min(520px, 94vw); - gap: 0; - padding: 0; - max-height: 85vh; - display: flex; - flex-direction: column; -} -.update-modal-header { - padding: 14px 16px 14px 18px; -} -.update-modal-versions { - font-size: 12px; - color: var(--text-muted); - display: block; - margin-top: 2px; -} -/* Scrollable middle section (changelog + download area combined) */ -.update-modal-body { - flex: 1; - overflow-y: auto; - min-height: 0; -} -/* Changelog accordion */ -.update-modal-changelog { - border-top: 1px solid var(--border-subtle); - border-bottom: 1px solid var(--border-subtle); -} -.update-modal-changelog-toggle { - width: 100%; - display: flex; - align-items: center; - gap: var(--space-2); - padding: 9px 18px; - background: none; - border: none; - cursor: pointer; - color: var(--text-secondary); - font-size: 12px; - font-weight: 600; - text-align: left; - letter-spacing: 0.03em; - transition: color var(--transition-fast), background var(--transition-fast); -} -.update-modal-changelog-toggle:hover { - color: var(--text-primary); - background: var(--bg-glass); -} -.update-modal-changelog-body { - padding: 4px 18px 12px; -} -/* Download area */ -.update-modal-download-area { - padding: 14px 18px; -} -.update-modal-asset { - display: flex; - align-items: center; - gap: var(--space-2); - background: var(--bg-glass); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-md); - padding: 8px 12px; -} -.update-modal-asset-name { - font-size: 12px; - color: var(--text-secondary); - flex: 1; - word-break: break-all; -} -.update-modal-asset-size { - font-size: 11px; - color: var(--text-muted); - flex-shrink: 0; -} -.update-modal-progress { - display: flex; - align-items: center; - gap: var(--space-2); -} -.update-modal-progress .app-updater-progress-bar { flex: 1; height: 4px; } -.update-modal-dl-bytes { - font-size: 11px; - color: var(--text-muted); - white-space: nowrap; -} -.update-modal-done { - display: flex; - flex-direction: column; - gap: var(--space-2); -} -.update-modal-done-title { - font-size: 13px; - font-weight: 600; - color: var(--accent); -} -.update-modal-done-hint { - font-size: 12px; - color: var(--text-secondary); -} -.update-modal-folder-btn { - align-self: flex-start; - display: inline-flex; - align-items: center; - gap: var(--space-2); - font-size: 12px; -} -/* macOS Tauri Updater — idle state info block */ -.update-modal-mac-info { - display: flex; - flex-direction: column; - gap: var(--space-2); -} -.update-modal-mac-info-main { - font-size: 13px; - font-weight: 600; - color: var(--text-primary); -} -.update-modal-mac-info-sub { - font-size: 12px; - color: var(--text-secondary); - line-height: 1.45; -} -.update-modal-trust-badges { - display: flex; - flex-wrap: wrap; - gap: var(--space-2); - margin-top: var(--space-1); -} -.update-modal-trust-badge { - display: inline-flex; - align-items: center; - gap: 4px; - font-size: 11px; - color: var(--positive, var(--accent)); - background: var(--bg-glass); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-sm); - padding: 3px 8px; -} -.update-modal-trust-badge svg { - flex-shrink: 0; -} -/* macOS Tauri Updater — done state (after install, before/during restart) */ -.update-modal-done-icon { - align-self: center; - color: var(--positive, var(--accent)); - margin-bottom: var(--space-1); -} -.update-modal-done-countdown { - font-size: 12px; - color: var(--text-secondary); - font-variant-numeric: tabular-nums; -} -/* AUR hint */ -.update-modal-aur { - display: flex; - flex-direction: column; - gap: var(--space-2); -} -.update-modal-aur-title { - font-size: 13px; - color: var(--text-secondary); -} -.update-modal-aur-cmd { - display: block; - background: var(--bg-glass); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-sm); - padding: 6px 10px; - font-size: 12px; - color: var(--accent); - font-family: monospace; - user-select: all; -} -.update-modal-aur-alt { - color: var(--text-muted); -} -.update-modal-asset-none { - padding: 4px 0; -} -/* Footer */ -.update-modal-footer { - display: flex; - align-items: center; - gap: var(--space-2); - padding: 12px 18px; - border-top: 1px solid var(--border-subtle); - flex-shrink: 0; -} -.update-modal-skip { - font-size: 12px; - color: var(--text-muted); - padding: var(--space-1) var(--space-2); -} -.update-modal-skip:hover { color: var(--text-secondary); } - -.update-toast { - margin: 0 var(--space-1) var(--space-2); - padding: var(--space-3) var(--space-3); - border-radius: var(--radius-md); - background: var(--accent-dim); - border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent); - display: flex; - flex-direction: column; - gap: 3px; - animation: update-toast-in 0.35s ease both; -} - -.update-toast-header { - display: flex; - align-items: center; - gap: var(--space-2); - color: var(--accent); -} - -.update-toast-label { - font-size: 11px; - font-weight: 700; - letter-spacing: 0.04em; - text-transform: uppercase; - color: var(--accent); -} - -.update-toast-version { - font-size: 12px; - color: var(--text-secondary); - padding-left: 22px; -} - -.update-toast-link { - font-size: 11px; - font-weight: 600; - color: var(--accent); - text-decoration: none; - padding-left: 22px; - opacity: 0.8; - transition: opacity var(--transition-fast); -} - -.update-toast-link:hover { - opacity: 1; - text-decoration: underline; -} - -.update-toast-icon { - display: flex; - align-items: center; - justify-content: center; - padding: var(--space-2) 0; - color: var(--accent); - animation: update-toast-in 0.35s ease both; - cursor: default; -} - -/* ─── Sidebar offline download queue ─── */ -.sidebar-offline-queue { - margin: 4px var(--space-1) 0; - padding: 6px 10px; - border-radius: var(--radius-md); - background: var(--accent-dim); - border: 1px solid color-mix(in srgb, var(--accent) 25%, transparent); - display: flex; - align-items: center; - gap: 7px; - font-size: 11px; - color: var(--accent); - overflow: hidden; -} - -.sidebar-offline-queue--collapsed { - justify-content: center; - padding: 6px; -} - -.sidebar-offline-cancel { - margin-left: auto; - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - width: 16px; - height: 16px; - border-radius: 50%; - border: none; - background: transparent; - color: var(--accent); - cursor: pointer; - opacity: 0.6; - padding: 0; - transition: opacity 0.15s, background 0.15s; -} -.sidebar-offline-cancel:hover { - opacity: 1; - background: color-mix(in srgb, var(--accent) 20%, transparent); -} - -/* ─── Sidebar device-sync queue ─── */ -.sidebar-sync-queue { - background: color-mix(in srgb, var(--success, #4ade80) 10%, var(--bg-sidebar)); - border-color: color-mix(in srgb, var(--success, #4ade80) 25%, transparent); - color: var(--success, #4ade80); -} - -@keyframes spin-slow { - to { transform: rotate(360deg); } -} - -.spin-slow { - animation: spin-slow 2s linear infinite; - flex-shrink: 0; -} - -/* ─── Main Content ─── */ -.main-content { - grid-area: main; - display: flex; - flex-direction: column; - overflow: hidden; - min-width: 0; - min-height: 0; /* allow 1fr row to shrink freely */ - background: var(--bg-app); - z-index: 1; -} - -/* Scoped UI scaling target. Sidebar / queue / player / titlebar live in - sibling grid cells of .app-shell and stay at 1:1. The wrapper inherits - .main-content's flex column layout so .content-header (fixed height) and - .content-body (flex: 1, scroll) keep working unchanged. - Zoom is applied via inline style only when uiScale !== 1, so the default - case has no extra layer or layout cost. */ -.main-content-zoom { - display: flex; - flex-direction: column; - flex: 1; - min-height: 0; - min-width: 0; -} - -.content-header { - display: flex; - align-items: center; - gap: var(--space-4); - padding: 0 var(--space-6); - height: 64px; - box-sizing: border-box; - border-bottom: 1px solid var(--border-subtle); - background: var(--bg-app); - position: relative; - z-index: 10; -} - -.content-header h1 { - font-family: var(--font-display); - font-size: 18px; - font-weight: 600; - color: var(--text-primary); -} - -.content-header .spacer { flex: 1; } - -.content-header > * { - transition: opacity 0.14s ease, visibility 0.14s ease; -} - -.content-header[data-live-search-overlay] > :not(.live-search):not(.spacer) { - opacity: 0; - visibility: hidden; - pointer-events: none; -} - -.content-body { - flex: 1; - overflow-y: auto; - overflow-x: hidden; - padding: var(--space-6); - contain: paint; -} - -/* When floating player is active, add extra bottom padding so content doesn't - get hidden behind the fixed floating bar (player height + bottom offset + minimal spacing). - !important needed to override inline padding:0 on App.tsx content-body */ -.app-shell.floating-player .content-body { - padding-bottom: calc(var(--player-height) + 12px) !important; -} - -/* Every page re-uses .content-body as its outer wrapper, but App.tsx already - renders one around as the scroll container. The inner one must - not establish its own scroll ancestor — otherwise `position: sticky` - children anchor to a container that never scrolls, and the header just - scrolls along with the content. Padding stays: the outer App.tsx - content-body has `padding: 0` inline, the inner one is what gives each - page its breathing room. */ -.content-body .content-body { - overflow: visible; -} - -/* Main route stack: scroll + overlay thumb live in the inner viewport, not the - outer .content-body, so sticky headers still anchor to the scrolling element. */ -.content-body.app-shell-route-host { - flex: 1; - min-height: 0; - overflow: hidden; - display: flex; - flex-direction: column; - padding: 0; - position: relative; - contain: none; -} - -.app-shell-route-scroll.overlay-scroll { - flex: 1; - min-height: 0; -} - -.app-shell-route-scroll .app-shell-route-scroll__viewport { - contain: paint; -} - -/* Sticky page header: keeps page title + filter/search bar visible while the - body scrolls. Negative horizontal margins (+ matching padding) make the - background flush with the scroll container edges so the sticky block looks - like a real bar, not a floating island. `top: 0` clamps below the content- - body's own padding-top; the residual 24px gap above the sticky block while - scrolling is intentional — gives the bar visual breathing room. */ -.page-sticky-header { - position: sticky; - top: 0; - z-index: 5; - background: var(--bg-app); - margin: 0 calc(-1 * var(--space-6)) var(--space-5); - padding: var(--space-4) var(--space-6); - border-bottom: 1px solid var(--border-subtle); -} - -/* ─── Offline Banner ─── */ -.offline-banner { - display: flex; - align-items: center; - gap: 8px; - padding: 6px 16px; - background: color-mix(in srgb, var(--accent) 12%, var(--bg-sidebar)); - border-bottom: 1px solid color-mix(in srgb, var(--accent) 30%, transparent); - color: var(--accent); - font-size: 12px; - font-weight: 500; - flex-shrink: 0; -} - -.offline-banner span { - flex: 1; -} - -.offline-banner-retry { - display: flex; - align-items: center; - gap: 4px; - background: none; - border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent); - border-radius: var(--radius-sm); - color: var(--accent); - font-size: 11px; - padding: 2px 8px; - cursor: pointer; - transition: background var(--transition-fast); -} - -.offline-banner-retry:hover { - background: color-mix(in srgb, var(--accent) 15%, transparent); -} - -.offline-banner-retry:disabled { - opacity: 0.5; - cursor: default; -} - -/* ─── Offline Storage Full Banner ─── */ -.offline-storage-full-banner { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 10px 16px; - margin: 0 0 1rem 0; - background: color-mix(in srgb, var(--color-danger, #e53935) 12%, var(--bg-card)); - border: 1px solid color-mix(in srgb, var(--color-danger, #e53935) 40%, transparent); - border-radius: var(--radius-md); - font-size: 13px; - color: var(--text-primary); - line-height: 1.4; -} - -.offline-storage-full-banner span { - flex: 1; -} - -.offline-storage-full-dismiss { - background: none; - border: none; - cursor: pointer; - color: var(--text-muted); - font-size: 18px; - line-height: 1; - padding: 0 2px; - flex-shrink: 0; -} - -.offline-storage-full-dismiss:hover { - color: var(--text-primary); -} - -/* ─── Player Bar ─── */ -.player-bar { - grid-area: player; - display: flex; - align-items: center; - gap: var(--space-6); - padding: 0 var(--space-5); - background: var(--bg-player); - border-top: 1px solid var(--border-subtle); - height: var(--player-height); - position: relative; - z-index: 100; - /* WebKitGTK (software compositing) occasionally paints the bar fully - black for one frame when an unrelated layer in the page invalidates. - `contain` isolates layout/paint so the bar's region cannot - participate in the surrounding dirty rect. */ - contain: layout paint; -} - -/* Floating player bar styles - positioning handled by ResizeObserver */ -.player-bar.floating { - position: fixed; - bottom: 12px; - /* left/right handled dynamically by JS */ - z-index: 1000; - border-radius: 50px; - width: auto; - min-width: 100px; - max-width: none; - grid-area: unset; - padding: 0 24px 0 8px; - gap: 16px; - background: var(--bg-player); - border: 1px solid rgba(255, 255, 255, 0.12); - box-shadow: - 0 8px 32px rgba(0, 0, 0, 0.8), - 0 0 0 1px rgba(255, 255, 255, 0.05), - inset 0 1px 0 rgba(255, 255, 255, 0.08); -} - -/* Internal element adjustments for floating mode */ -.player-bar.floating .player-track-info { - flex: 0 0 auto; - min-width: 240px; - max-width: 320px; -} - -.player-bar.floating .player-album-art-wrap { - border-radius: 50%; -} - -.player-bar.floating .player-album-art, -.player-bar.floating .player-album-art-placeholder { - border-radius: 0; -} - -.player-bar.floating .player-buttons { - flex-shrink: 0; - gap: 16px; -} - -.player-bar.floating .player-waveform-section { - flex: 1; - min-width: 200px; -} - -.player-bar.floating .player-volume-section { - flex: 0 0 auto; - min-width: 120px; - display: flex; - align-items: center; - gap: 8px; -} - -/* Liquid-glass floating bar — macOS + Windows only. - Inspired by PR #211 (kveld9), adapted to theme tokens and reduced - blur so it stays within WebView2 / WKWebView GPU budgets. Linux - always keeps the solid look from PR #216, regardless of compositor. */ -html[data-platform="macos"] .player-bar.floating, -html[data-platform="windows"] .player-bar.floating { - background: color-mix(in srgb, var(--bg-player) 72%, transparent); - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); - border: 1px solid color-mix(in srgb, var(--text-primary) 10%, transparent); - box-shadow: - 0 12px 40px rgba(0, 0, 0, 0.45), - inset 0 1px 0 color-mix(in srgb, var(--text-primary) 12%, transparent); -} - -.player-track-info { - display: flex; - align-items: center; - gap: var(--space-3); - flex: 0 0 320px; - min-width: 0; - overflow: hidden; -} - -.player-album-art { - width: 52px; - height: 52px; - border-radius: var(--radius-md); - object-fit: cover; - background: var(--bg-card); - flex-shrink: 0; - box-shadow: var(--shadow-md); -} - -.player-album-art-placeholder { - width: 52px; - height: 52px; - border-radius: var(--radius-md); - background: var(--bg-card); - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - color: var(--text-muted); -} - -.player-album-art-wrap { - position: relative; - flex-shrink: 0; - border-radius: var(--radius-sm); - overflow: hidden; - background: var(--bg-card); -} -.player-album-art-wrap::after { - content: ''; - position: absolute; - inset: 0; - background: rgba(0, 0, 0, 0.3); - opacity: 0; - transition: opacity var(--transition-fast); - pointer-events: none; -} -.player-album-art-wrap.clickable { cursor: pointer; } -.player-album-art-wrap:hover::after { opacity: 1; } - -/* Expand icon hint that appears on cover hover */ -.player-art-expand-hint { - position: absolute; - inset: 0; - display: flex; - align-items: center; - justify-content: center; - color: white; - opacity: 0; - transition: opacity var(--transition-fast); - pointer-events: none; - z-index: 1; -} -.player-album-art-wrap.clickable:hover .player-art-expand-hint { opacity: 1; } - -.player-track-meta { - flex: 1 1 0; - min-width: 0; - overflow: hidden; -} - -.player-track-name { - font-size: 14px; - font-weight: 600; - color: var(--text-primary); -} - -.player-track-artist { - font-size: 12px; - color: var(--text-secondary); - margin-top: 2px; -} - -.player-track-rating { - margin-top: 4px; - width: auto; - justify-content: flex-start; -} - -.player-track-rating .star { - font-size: 11px; - padding: 0 1px; -} - -/* ── Marquee (PlayerBar track name / artist) ── */ -.marquee-wrap { - overflow: hidden; - white-space: nowrap; - width: 100%; -} - -.marquee-scroll { - display: inline-block; - animation: player-marquee 12s linear infinite; - animation-delay: 2s; -} - -@keyframes player-marquee { - 0%, 12% { transform: translateX(0); } - 78%, 88% { transform: translateX(var(--marquee-amount)); } - 88.1%, 100% { transform: translateX(0); } -} - -.player-buttons { - display: flex; - align-items: center; - gap: var(--space-2); - flex-shrink: 0; -} - -.player-btn { - display: flex; - align-items: center; - justify-content: center; - width: 36px; - height: 36px; - border-radius: 50%; - color: var(--text-muted); - transition: all var(--transition-fast); - flex-shrink: 0; -} - -.player-btn:hover { - background: var(--bg-hover); - color: var(--text-primary); - transform: scale(1.12); -} - -.player-btn-sm { - width: 28px; - height: 28px; -} - -/* Star + Last.fm heart — visually separated from track title */ -.player-star-btn { - margin: var(--space-1); - color: var(--text-muted); - transition: color var(--transition-fast); -} - -.player-star-btn:hover, -.player-star-btn.is-starred { - color: var(--color-star-active, var(--accent)); -} - -.player-btn-primary { - width: 46px; - height: 46px; - background: var(--accent); - color: var(--ctp-crust); - border-radius: 50%; - box-shadow: 0 4px 18px rgba(0, 0, 0, 0.4); - flex-shrink: 0; -} - -.player-btn-primary:hover { - background: var(--accent); - color: var(--ctp-crust); - transform: scale(1.06); - box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4), var(--shadow-glow); - filter: brightness(1.12); -} - -/* Waveform seekbar section */ -.player-waveform-section { - flex: 1; - min-width: 0; - display: flex; - align-items: center; - gap: var(--space-2); - margin-left: 24px; - margin-right: 24px; -} - -.player-waveform-wrap { - flex: 1; - min-width: 0; -} - -.player-time { - font-size: 11px; - color: var(--text-muted); - font-variant-numeric: tabular-nums; - white-space: nowrap; - flex-shrink: 0; - min-width: 2.5rem; -} - -.player-time:first-child { - text-align: right; -} - -.player-time:last-child { - text-align: left; -} - -/* Clickable time toggle with swap indicator */ -.player-time-toggle { - cursor: pointer; - display: inline-flex; - align-items: center; - padding: 2px 4px; - border-radius: 4px; - transition: background-color 0.15s, color 0.15s; -} - -.player-time-toggle:hover { - background-color: var(--surface-hover, rgba(128, 128, 128, 0.2)); - color: var(--text-primary); -} - -.player-time-toggle:active { - background-color: var(--surface-active, rgba(128, 128, 128, 0.3)); -} - -/* Volume section */ -.player-volume-section { - display: flex; - align-items: center; - gap: var(--space-2); - flex-shrink: 0; - width: 155px; -} - -.player-volume-slider-wrap { - flex: 1; - min-width: 0; - position: relative; -} - -.player-volume-slider { - width: 100%; - display: block; - margin: 0; - vertical-align: middle; -} - -.player-volume-pct { - position: absolute; - bottom: calc(100% + 6px); - transform: translateX(-50%); - font-size: 10px; - font-weight: 600; - background: var(--bg-card); - color: var(--text-primary); - border: 1px solid var(--border-subtle); - padding: 2px 6px; - border-radius: var(--radius-sm); - pointer-events: none; - white-space: nowrap; - box-shadow: var(--shadow-sm); -} - -.player-eq-btn { - color: var(--text-muted); - flex-shrink: 0; - transition: color 0.15s; -} -.player-eq-btn:hover, -.player-eq-btn.active { - color: var(--accent); -} - -.player-overflow-wrap { - position: relative; - flex-shrink: 0; -} - -.player-overflow-menu { - position: absolute; - right: 0; - bottom: calc(100% + 8px); - width: 238px; - padding: 10px; - border-radius: var(--radius-md); - border: 1px solid var(--border); - background: var(--bg-card); - box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35); - z-index: 240; - display: flex; - flex-direction: column; - gap: 8px; -} - -.player-overflow-menu-row { - display: flex; - gap: 6px; -} - -.player-overflow-menu-btn { - flex: 1; - display: inline-flex; - align-items: center; - justify-content: center; - gap: 6px; - height: 30px; - border-radius: var(--radius-md); - border: 1px solid var(--border-subtle); - background: var(--bg-hover); - color: var(--text-secondary); - font-size: 12px; - font-weight: 600; - cursor: pointer; - transition: background var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast); -} - -.player-overflow-menu-btn:hover { - background: var(--border); - color: var(--text-primary); -} - -.player-overflow-menu-btn.active { - color: var(--accent); - border-color: color-mix(in srgb, var(--accent) 45%, transparent); -} - -.player-volume-section.player-volume-section--menu { - width: 100%; -} - -.player-overflow-menu--volume-only { - padding: 10px; -} - -.player-volume-slider-wrap.player-volume-slider-wrap--menu-only { - width: 100%; - min-width: 0; -} - -/* ─── EQ Popup ─── */ -.eq-popup-backdrop { - position: fixed; - inset: 0; - z-index: 200; - background: rgba(0, 0, 0, 0.35); - backdrop-filter: blur(2px); -} - -.eq-popup { - position: fixed; - z-index: 201; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: min(700px, 92vw); - background: var(--bg-sidebar); - border: 1px solid var(--border); - border-radius: 14px; - box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5); - padding: 20px; - display: flex; - flex-direction: column; - gap: 16px; -} -.eq-popup .eq-wrap { - max-width: none; -} - -.eq-popup-header { - display: flex; - align-items: center; - justify-content: space-between; -} - -.eq-popup-title { - font-size: 15px; - font-weight: 600; - color: var(--text-primary); -} - -.eq-popup-close { - display: flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - border-radius: 6px; - background: var(--bg-hover); - border: 1px solid var(--border); - color: var(--text-muted); - cursor: pointer; - transition: background 0.15s, color 0.15s; -} -.eq-popup-close:hover { - background: var(--accent); - color: var(--bg-app); -} - -/* ─── Queue Panel ─── */ -.queue-panel { - grid-area: queue; - background: var(--bg-sidebar); - border-left: 1px solid var(--border-subtle); - display: flex; - flex-direction: column; - overflow: hidden; - min-width: 0; - min-height: 0; /* allow 1fr row to shrink freely */ -} - -.queue-panel.queue-drop-active { - box-shadow: inset 0 0 0 2px var(--accent); - background: linear-gradient(var(--accent-dim), var(--accent-dim)), var(--bg-sidebar); -} - -.queue-header { - height: 64px; - box-sizing: border-box; - padding: 0 var(--space-4); - background: var(--bg-app); - border-bottom: 1px solid var(--border-subtle); - display: flex; - justify-content: space-between; - align-items: center; - flex-shrink: 0; -} - -.queue-action-btn { - display: flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - border-radius: var(--radius-sm); - color: var(--text-muted); - cursor: pointer; - transition: background var(--transition-fast), color var(--transition-fast); - flex-shrink: 0; -} -.queue-action-btn:hover:not(:disabled) { - background: var(--bg-hover); - color: var(--text-primary); -} -.queue-action-btn:disabled { - opacity: 0.35; - cursor: default; -} - -.queue-toolbar { - display: flex; - align-items: center; - justify-content: center; - gap: 6px; - padding: 0 var(--space-3); - height: 44px; - flex-shrink: 0; - border-bottom: 1px solid var(--border-subtle); - background: var(--bg-app); -} - -.crossfade-popover { - position: absolute; - top: calc(100% + 10px); - right: 0; - width: 170px; - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius-md); - padding: 10px 12px; - box-shadow: 0 4px 16px rgba(0,0,0,0.3); - z-index: 200; - display: flex; - flex-direction: column; - gap: 8px; -} - -.crossfade-popover-label { - display: flex; - align-items: center; - gap: 5px; - font-size: 11px; - font-weight: 600; - color: var(--accent); - letter-spacing: 0.03em; -} - -.crossfade-popover-value { - margin-left: auto; - font-size: 12px; - font-weight: 700; - font-family: monospace; - color: var(--text-primary); -} - -.crossfade-popover-slider { - width: 100%; - accent-color: var(--accent); - cursor: pointer; -} - -.crossfade-popover-range { - display: flex; - justify-content: space-between; - font-size: 10px; - color: var(--text-muted); -} - -.queue-toolbar-sep { - width: 1px; - height: 18px; - background: var(--border-subtle); - margin: 0 2px; - flex-shrink: 0; -} - -.queue-round-btn { - display: flex; - align-items: center; - justify-content: center; - width: 30px; - height: 30px; - border-radius: 50%; - color: var(--text-primary); - background: var(--bg-hover); - border: none; - cursor: pointer; - transition: background var(--transition-fast), color var(--transition-fast); - flex-shrink: 0; -} -.queue-round-btn:hover:not(:disabled) { - background: var(--border); - color: var(--accent); -} -.queue-round-btn:disabled { - opacity: 0.35; - cursor: default; -} -.queue-round-btn.active { - color: var(--ctp-base); - background: var(--accent); -} - -.queue-current-track { - display: flex; - flex-direction: column; - flex-shrink: 0; - border-bottom: 1px solid var(--border-subtle); -} - -.queue-current-track-body { - padding: var(--space-3) var(--space-4); - display: flex; - flex-direction: row; - align-items: flex-start; - gap: var(--space-3); -} - -.queue-current-cover { - width: 90px; - height: 90px; - flex-shrink: 0; - border-radius: var(--radius-md); - overflow: hidden; - background: var(--bg-surface); - display: flex; - align-items: center; - justify-content: center; - box-shadow: 0 2px 8px rgba(0,0,0,0.2); - position: relative; -} - -.queue-current-cover img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.queue-current-cover .fallback { - color: var(--text-muted); -} - -.queue-current-info { - display: flex; - flex-direction: column; - gap: 2px; - min-width: 0; - flex: 1; -} - -.queue-current-info h3 { - font-size: 13px; - font-weight: 700; - margin: 0; - color: var(--text-primary); -} - -.queue-current-sub { - font-size: 12px; - color: var(--text-secondary); - transition: color var(--transition-fast); -} - -.queue-current-sub.is-link { - cursor: pointer; -} -.queue-current-sub.is-link:hover { - color: var(--accent); -} - -.queue-current-tech { - width: 100%; - font-size: 9px; - font-family: monospace; - letter-spacing: 0.05em; - background: var(--ctp-surface1); - color: var(--accent); - padding: 3px var(--space-4); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - text-align: center; - backdrop-filter: blur(8px); - display: flex; - align-items: center; - justify-content: center; - gap: 6px; -} - -.queue-current-tech-source { - flex-shrink: 0; - display: inline-flex; - align-items: center; - line-height: 0; - opacity: 0.92; -} - -.queue-current-tech-main { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; -} - -/* Two-line layout: base info + RG badge on top, optional ReplayGain - values underneath when the badge is expanded. The stack wraps both - rows so the source icon stays vertically centered next to the whole - text block. */ -.queue-current-tech-stack { - min-width: 0; - display: flex; - flex-direction: column; - align-items: center; - gap: 1px; -} - -.queue-current-tech-row { - min-width: 0; - display: flex; - align-items: center; - justify-content: center; - gap: 6px; -} - -/* Pill-shaped toggle that signals "this track has ReplayGain metadata" - without committing screen space to the numbers — values appear in the - tooltip and on the second line when expanded. */ -.queue-current-tech-rg-badge { - flex-shrink: 0; - display: inline-flex; - align-items: center; - gap: 2px; - padding: 1px 5px 1px 6px; - border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent); - background: color-mix(in srgb, var(--accent) 12%, transparent); - color: var(--accent); - border-radius: 999px; - font: inherit; - font-size: 8px; - letter-spacing: 0.06em; - cursor: pointer; - transition: background 0.12s, border-color 0.12s, transform 0.12s; -} -.queue-current-tech-rg-badge:hover { - background: color-mix(in srgb, var(--accent) 22%, transparent); - border-color: color-mix(in srgb, var(--accent) 55%, transparent); -} -.queue-current-tech-rg-badge svg { - transition: transform 0.18s ease; -} -.queue-current-tech-rg-badge--open svg { - transform: rotate(180deg); -} - -.queue-current-tech-rg { - min-width: 0; - max-width: 100%; - font-size: 8px; - opacity: 0.72; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.queue-current-tech-metric { - margin: 0; - padding: 0; - border: none; - background: transparent; - font: inherit; - letter-spacing: inherit; - line-height: inherit; - color: inherit; - cursor: pointer; - text-decoration: none; - border-radius: 2px; -} - -.queue-current-tech-metric:hover { - background: color-mix(in srgb, var(--accent) 14%, transparent); -} - -.queue-current-tech-metric--lufs-reanalyze { - display: inline-flex; - align-items: baseline; - gap: 0; - padding: 2px 4px; - margin: -2px -4px; - max-width: 100%; - min-width: 0; - font-variant-numeric: tabular-nums; -} - -.queue-divider { - padding: var(--space-3) var(--space-4) 0; - flex-shrink: 0; -} - -/* ─── OverlayScrollArea (shared: queue, mini, sidebar, menus) ─────────────── */ -.overlay-scroll { - --overlay-rail-top: 0px; - --overlay-rail-bottom: 0px; - position: relative; - overflow: hidden; - min-height: 0; - display: flex; - flex-direction: column; -} - -.overlay-scroll--rail-inset-none { - --overlay-rail-top: 0px; - --overlay-rail-bottom: 0px; -} - -.overlay-scroll--rail-inset-mini { - --overlay-rail-top: 4px; - --overlay-rail-bottom: 4px; -} - -.overlay-scroll--rail-inset-panel { - --overlay-rail-top: var(--space-2); - --overlay-rail-bottom: var(--space-2); -} - -.overlay-scroll--viewport-scroll-auto .overlay-scroll__viewport { - scroll-behavior: auto; -} - -.overlay-scroll__viewport { - flex: 1 1 auto; - min-height: 0; - overflow-y: auto; - overscroll-behavior: contain; - box-sizing: border-box; - scrollbar-width: none; -} - -.overlay-scroll__viewport::-webkit-scrollbar { - display: none; -} - -.overlay-scroll__rail { - position: absolute; - top: var(--overlay-rail-top); - right: 0; - bottom: var(--overlay-rail-bottom); - width: 14px; - z-index: 1; - pointer-events: auto; -} - -.overlay-scroll__thumb { - position: absolute; - top: 0; - right: 2px; - width: 3px; - border-radius: 2px; - background: var(--text-muted); - opacity: 0; - transition: - opacity 0.18s ease, - width 0.2s ease, - border-radius 0.2s ease; - pointer-events: none; - will-change: transform; - cursor: grab; - touch-action: none; -} - -.overlay-scroll__viewport.is-scrolling ~ .overlay-scroll__rail .overlay-scroll__thumb, -.overlay-scroll:hover .overlay-scroll__rail .overlay-scroll__thumb { - opacity: 0.55; - pointer-events: auto; -} - -.overlay-scroll__rail:hover .overlay-scroll__thumb, -.overlay-scroll__rail:has(.overlay-scroll__thumb.is-thumb-dragging) .overlay-scroll__thumb { - width: 9px; - border-radius: 4px; -} - -.overlay-scroll__rail .overlay-scroll__thumb.is-thumb-dragging { - opacity: 0.9; - pointer-events: auto; - cursor: grabbing; - transition: opacity 0.18s ease; -} - -.queue-list-wrap.overlay-scroll { - flex: 1 1 auto; -} - -.queue-list.overlay-scroll__viewport { - padding: var(--space-2); -} - -.queue-empty { - padding: var(--space-4); - color: var(--text-muted); - text-align: center; - font-size: 14px; -} - -.queue-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: var(--space-2) var(--space-3); - border-radius: var(--radius-sm); - cursor: pointer; - transition: background var(--transition-fast); - color: var(--text-secondary); -} -/* Prevent child elements from stealing dragenter/dragleave events */ -.queue-item > * { pointer-events: none; } - -.queue-item:hover, .queue-item.context-active { - background: var(--bg-hover); - color: var(--text-primary); -} - -.queue-item.active { - background: var(--accent-dim); - color: var(--accent); -} - -.queue-item-info { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - gap: 2px; -} - -.queue-item-title { - font-size: 12px; - font-weight: 500; - color: inherit; -} - -.queue-item-artist { - font-size: 11px; - color: var(--text-muted); - transition: color var(--transition-fast); -} - -.queue-item:hover .queue-item-artist, -.queue-item.context-active .queue-item-artist { - color: var(--text-secondary); -} - -.queue-item-duration { - font-size: 11px; - color: var(--text-muted); - flex-shrink: 0; - margin-left: var(--space-3); -} - -.queue-item.active .queue-item-artist, -.queue-item.active .queue-item-duration { - color: inherit; - opacity: 0.8; -} - -.queue-item-attribution, -.queue-current-attribution { - font-size: 10px; - color: var(--text-muted); - opacity: 0.75; - margin-top: 1px; - font-style: italic; -} - -.queue-item:hover .queue-item-attribution, -.queue-item.context-active .queue-item-attribution { - color: var(--text-secondary); - opacity: 0.9; -} - -.queue-item.active .queue-item-attribution { - color: inherit; - opacity: 0.7; -} - -/* ═══════════════════════════════════════════════════════════════════════════ - MOBILE LAYOUT (< 800px) - Controller: data-mobile attribute set by useIsMobile hook - ═══════════════════════════════════════════════════════════════════════════ */ - -/* ─── BottomNav Component ─── */ -.bottom-nav { - grid-area: bottomnav; - display: flex; - align-items: stretch; - justify-content: space-around; - background: var(--bg-sidebar); - border-top: 1px solid var(--border-subtle); - height: 56px; - z-index: 100; - flex-shrink: 0; -} - -.bottom-nav-item { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 3px; - flex: 1; - color: var(--text-muted); - text-decoration: none; - font-size: 10px; - font-weight: 500; - letter-spacing: 0.02em; - transition: color var(--transition-fast); - position: relative; -} - -.bottom-nav-item:hover { - color: var(--text-secondary); -} - -.bottom-nav-item.active { - color: var(--accent); -} - -.bottom-nav-item.active::before { - content: ''; - position: absolute; - top: 0; - left: 50%; - transform: translateX(-50%); - width: 32px; - height: 2px; - background: var(--accent); - border-radius: 0 0 var(--radius-full) var(--radius-full); -} - -.bottom-nav-item svg { - opacity: 0.65; - transition: opacity var(--transition-fast); -} - -.bottom-nav-item.active svg, -.bottom-nav-item:hover svg { - opacity: 1; -} - -.bottom-nav-icon-wrap { - position: relative; - display: flex; - align-items: center; -} - -.bottom-nav-np-dot { - position: absolute; - top: -2px; - right: -5px; - width: 6px; - height: 6px; - border-radius: 50%; - background: var(--accent); - animation: np-dot-pulse 1.8s ease-in-out infinite; -} - -.bottom-nav-label { - line-height: 1; -} - -/* ─── Mobile More Sheet ─── */ -.mobile-more-backdrop { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.45); - z-index: 200; -} - -.mobile-more-sheet { - position: fixed; - bottom: 0; - left: 0; - right: 0; - background: var(--bg-app); - border-radius: 16px 16px 0 0; - border-top: 1px solid var(--border); - z-index: 201; - padding: 0.5rem 0.75rem calc(0.75rem + var(--bottom-nav-height, 62px)); - animation: more-sheet-up 0.22s ease; -} - -@keyframes more-sheet-up { - from { transform: translateY(100%); } - to { transform: translateY(0); } -} - -.mobile-more-handle { - width: 36px; - height: 4px; - background: var(--border); - border-radius: 2px; - margin: 0 auto 0.75rem; -} - -.mobile-more-grid { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 0.25rem 0; -} - -.mobile-more-item { - display: flex; - flex-direction: column; - align-items: center; - gap: 0.3rem; - padding: 0.65rem 0.25rem; - border-radius: var(--radius); - color: var(--text-secondary); - text-decoration: none; - font-size: 11px; - font-weight: 500; - cursor: pointer; - background: none; - border: none; - transition: color var(--transition-fast), background var(--transition-fast); -} - -.mobile-more-item:hover { - color: var(--text-primary); - background: var(--bg-hover); -} - -.mobile-more-item.active { - color: var(--accent); -} - -.mobile-more-icon { - display: flex; - align-items: center; - justify-content: center; - opacity: 0.75; -} - -.mobile-more-item.active .mobile-more-icon, -.mobile-more-item:hover .mobile-more-icon { - opacity: 1; -} - -.mobile-more-label { - line-height: 1.2; - text-align: center; - max-width: 72px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -/* ─── Grid Overrides ─── */ -.app-shell[data-mobile] { - grid-template-columns: 1fr; - grid-template-rows: minmax(0, 1fr) auto auto; - grid-template-areas: - "main" - "bottomnav" - "player"; - width: 100vw; - max-width: 100%; -} - -.app-shell[data-mobile][data-titlebar] { - grid-template-rows: var(--titlebar-height) minmax(0, 1fr) auto auto; - grid-template-areas: - "titlebar" - "main" - "bottomnav" - "player"; -} - -/* Hide the sidebar collapse button on mobile */ -.app-shell[data-mobile] .collapse-btn { - display: none; -} - -/* ─── Content Header — Mobile ─── */ -.app-shell[data-mobile] .content-header { - padding: 0 var(--space-4); - height: 52px; - gap: var(--space-3); -} - -/* Hide queue toggle on mobile */ -.app-shell[data-mobile] .queue-toggle-btn { - display: none; -} - -.app-shell[data-mobile] .resizer-queue-handle { - display: none; -} - -/* ─── Player Bar — Mobile ─── */ -.app-shell[data-mobile] .player-bar { - gap: var(--space-3); - padding: 0 var(--space-3); - height: 64px; -} - -.app-shell[data-mobile] .player-track-info { - flex: 1 1 0; - min-width: 0; -} - -.app-shell[data-mobile] .player-waveform-section, -.app-shell[data-mobile] .player-volume-section, -.app-shell[data-mobile] .player-eq-btn, -.app-shell[data-mobile] .player-bar > .player-btn.player-btn-sm:not(.player-star-btn):not(.player-love-btn) { - display: none; -} - -.app-shell[data-mobile] .player-buttons { - gap: var(--space-1); - flex-shrink: 0; -} - -.app-shell[data-mobile] .player-btn-primary { - width: 40px; - height: 40px; -} - -.app-shell[data-mobile] .player-album-art, -.app-shell[data-mobile] .player-album-art-placeholder { - width: 44px; - height: 44px; -} - -/* Hide star + last.fm love buttons in mobile player to save space */ -.app-shell[data-mobile] .player-star-btn, -.app-shell[data-mobile] .player-love-btn { - display: none; -} - -/* ─── App Updater Toast — Mobile ─── */ -.app-shell[data-mobile] .app-updater-toast { - bottom: calc(64px + 56px + 12px); -} - -/* ─── Mobile Player Takeover ─── */ -/* When the mobile player is active, the app-shell becomes a single full-screen - area with no BottomNav or PlayerBar — just the main content. */ -.app-shell[data-mobile-player] { - grid-template-rows: minmax(0, 1fr); - grid-template-areas: "main"; -} - -.app-shell[data-mobile-player] .content-header { - display: none; -} - -.app-shell[data-mobile-player] .content-body { - padding: 0 !important; - overflow: hidden !important; -} - -/* ═══════════════════════════════════════════════════════════════════════════ - MOBILE PLAYER VIEW (MobilePlayerView.tsx) - ═══════════════════════════════════════════════════════════════════════════ */ - -.mp-view { - position: fixed; - inset: 0; - display: flex; - flex-direction: column; - justify-content: space-evenly; - background: var(--bg-app); /* fallback; overridden by dynamic gradient via inline style */ - overflow: hidden !important; - scrollbar-width: none; /* Firefox */ - -ms-overflow-style: none; /* IE/Edge */ - padding: 0 20px env(safe-area-inset-bottom, 0); - box-sizing: border-box; - transition: background 0.8s ease; - z-index: 10; -} - -.mp-view::-webkit-scrollbar { - display: none; /* Chrome, Safari, Tauri/WebKitGTK */ -} - -/* ─── Header ─── */ -.mp-header { - display: flex; - align-items: center; - justify-content: space-between; - height: 52px; - flex-shrink: 0; -} - -.mp-header-title { - font-size: 13px; - font-weight: 600; - letter-spacing: 0.04em; - text-transform: uppercase; - color: var(--text-muted); -} - -.mp-back { - display: flex; - align-items: center; - justify-content: center; - width: 44px; - height: 44px; - border-radius: 50%; - background: none; - border: none; - color: var(--text-secondary); - cursor: pointer; - transition: color var(--transition-fast), background var(--transition-fast); -} - -.mp-back:hover { - background: var(--bg-hover); - color: var(--text-primary); -} - -/* ─── Cover Art ─── */ -.mp-cover-wrap { - flex: 0 1 auto; - min-height: 0; - display: flex; - align-items: center; - justify-content: center; -} - -.mp-cover { - width: min(calc(100vw - 28px), 55vh); - max-width: 440px; - aspect-ratio: 1; - object-fit: cover; - border-radius: 14px; - box-shadow: 0 16px 52px rgba(0, 0, 0, 0.7), 0 4px 16px rgba(0, 0, 0, 0.4); -} - -.mp-cover-fallback { - display: flex; - align-items: center; - justify-content: center; - background: var(--bg-card); - color: var(--text-muted); -} - -/* ─── Track Metadata ─── */ -.mp-meta { - display: flex; - align-items: center; - gap: var(--space-3); - padding: 8px 0 4px; - flex-shrink: 0; -} - -.mp-meta-text { - flex: 1; - min-width: 0; -} - -.mp-title { - font-size: 22px; - font-weight: 700; - color: var(--text-primary); - line-height: 1.3; -} - -.mp-artist { - font-size: 15px; - color: var(--accent); - margin-top: 2px; -} - -.mp-track-info { - font-size: 12px; - color: var(--text-muted); - margin-top: 4px; - letter-spacing: 0.01em; -} - -.mp-heart { - display: flex; - align-items: center; - justify-content: center; - width: 44px; - height: 44px; - border-radius: 50%; - background: none; - border: none; - color: var(--text-muted); - cursor: pointer; - flex-shrink: 0; - transition: color var(--transition-fast), transform var(--transition-fast); -} - -.mp-heart:hover { - transform: scale(1.12); -} - -.mp-heart.active { - color: var(--color-star-active, var(--accent)); -} - -/* ─── Scrubber ─── */ -.mp-scrubber-wrap { - padding: 12px 0 2px; - flex-shrink: 0; -} - -.mp-scrubber { - position: relative; - height: 28px; - display: flex; - align-items: center; - cursor: pointer; - touch-action: none; -} - -.mp-scrubber-bg { - position: absolute; - left: 0; - right: 0; - height: 4px; - border-radius: 2px; - background: var(--ctp-surface2, rgba(255,255,255,0.12)); -} - -.mp-scrubber-fill { - position: absolute; - left: 0; - height: 4px; - border-radius: 2px; - background: var(--accent); - transition: width 0.08s linear; -} - -.mp-scrubber-thumb { - position: absolute; - top: 50%; - width: 14px; - height: 14px; - border-radius: 50%; - background: var(--accent); - transform: translate(-50%, -50%); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); - transition: transform 0.1s ease; -} - -.mp-scrubber:active .mp-scrubber-thumb { - transform: translate(-50%, -50%) scale(1.3); -} - -.mp-scrubber-times { - display: flex; - justify-content: space-between; - padding-top: 4px; - font-size: 11px; - color: var(--text-muted); - font-variant-numeric: tabular-nums; -} - -/* ─── Transport Controls ─── */ -.mp-controls { - display: flex; - align-items: center; - justify-content: center; - gap: var(--space-4); - padding: 8px 0 4px; - flex-shrink: 0; -} - -.mp-ctrl-btn { - display: flex; - align-items: center; - justify-content: center; - width: 52px; - height: 52px; - border-radius: 50%; - background: none; - border: none; - color: var(--text-secondary); - cursor: pointer; - transition: color var(--transition-fast), transform var(--transition-fast); -} - -.mp-ctrl-btn:hover { - color: var(--text-primary); - transform: scale(1.08); -} - -.mp-ctrl-sm { - width: 44px; - height: 44px; - color: var(--text-muted); -} - -.mp-ctrl-play { - width: 64px; - height: 64px; - background: var(--accent); - color: var(--ctp-crust); - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); -} - -.mp-ctrl-play:hover { - color: var(--ctp-crust); - filter: brightness(1.1); - transform: scale(1.06); - box-shadow: 0 6px 28px rgba(0, 0, 0, 0.4), var(--shadow-glow); -} - -/* ─── Empty state ─── */ -.mp-empty { - flex: 1; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: var(--space-4); - color: var(--text-muted); - font-size: 15px; -} - -/* ─── Utility Footer ─── */ -.mp-footer { - display: flex; - align-items: center; - justify-content: space-evenly; - padding: 8px 0 max(12px, env(safe-area-inset-bottom, 12px)); - flex-shrink: 0; -} - -.mp-footer-btn { - display: flex; - align-items: center; - gap: 6px; - background: none; - border: none; - color: var(--text-muted); - font-size: 13px; - font-weight: 500; - cursor: pointer; - padding: 10px 18px; - border-radius: var(--radius-full); - transition: color var(--transition-fast), background var(--transition-fast); - min-height: 44px; -} - -.mp-footer-btn:hover { - background: var(--bg-hover); - color: var(--text-primary); -} - -/* ═══════════════════════════════════════════════════════════════════════════ - MOBILE QUEUE / LYRICS DRAWER - ═══════════════════════════════════════════════════════════════════════════ */ - -@keyframes mq-slide-up { - from { transform: translateY(100%); } - to { transform: translateY(0); } -} - -@keyframes mq-fade-in { - from { opacity: 0; } - to { opacity: 1; } -} - -.mq-drawer-backdrop { - position: fixed; - inset: 0; - z-index: 500; - background: rgba(0, 0, 0, 0.5); - backdrop-filter: blur(4px); - animation: mq-fade-in 0.2s ease both; -} - -.mq-drawer { - position: fixed; - bottom: 0; - left: 0; - right: 0; - height: 90vh; - z-index: 501; - background: var(--bg-sidebar); - border-radius: 18px 18px 0 0; - display: flex; - flex-direction: column; - overflow: hidden; - animation: mq-slide-up 0.3s cubic-bezier(0.22, 1, 0.36, 1) both; - box-shadow: 0 -8px 40px rgba(0, 0, 0, 0.4); -} - -.mq-drawer-header { - display: flex; - align-items: center; - gap: var(--space-3); - padding: 16px 20px 12px; - border-bottom: 1px solid var(--border-subtle); - flex-shrink: 0; -} - -.mq-drawer-header h3 { - font-size: 17px; - font-weight: 700; - color: var(--text-primary); - margin: 0; -} - -.mq-drawer-count { - font-size: 13px; - color: var(--accent); - flex: 1; -} - -.mq-drawer-close { - display: flex; - align-items: center; - justify-content: center; - width: 36px; - height: 36px; - border-radius: 50%; - background: var(--bg-hover); - border: none; - color: var(--text-muted); - cursor: pointer; - flex-shrink: 0; - transition: background var(--transition-fast), color var(--transition-fast); -} - -.mq-drawer-close:hover { - background: var(--border); - color: var(--text-primary); -} - -.mq-drawer-list { - flex: 1; - overflow-y: auto; - padding: var(--space-2); -} - -.mq-drawer-empty { - padding: var(--space-6); - text-align: center; - color: var(--text-muted); - font-size: 14px; -} - -/* Lyrics drawer — fill entire drawer */ -.mq-drawer-lyrics .mq-drawer-list { - padding: 0; -} - -/* ── Queue Item ── */ -.mq-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: 10px 14px; - border-radius: var(--radius-md); - cursor: pointer; - color: var(--text-secondary); - transition: background var(--transition-fast); - min-height: 44px; -} - -.mq-item:hover { - background: var(--bg-hover); - color: var(--text-primary); -} - -.mq-item.active { - background: var(--accent-dim); - color: var(--accent); -} - -.mq-item-info { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - gap: 2px; -} - -.mq-item-title { - font-size: 13px; - font-weight: 500; - color: inherit; - display: flex; - align-items: center; - gap: 6px; -} - -.mq-item-artist { - font-size: 11px; - color: var(--text-muted); -} - -.mq-item.active .mq-item-artist { - color: inherit; - opacity: 0.7; -} - -.mq-item-dur { - font-size: 11px; - color: var(--text-muted); - flex-shrink: 0; - margin-left: var(--space-3); -} - -.mq-item.active .mq-item-dur { - color: inherit; - opacity: 0.7; -} - -/* ─── Sidebar Expandable Playlists Section ─── */ -.sidebar-playlists-wrapper { - display: flex; - flex-direction: column; -} - -.sidebar-playlists-header-row { - display: flex; - align-items: center; - gap: var(--space-1); - padding-right: var(--space-2); -} - -.sidebar-playlists-main-link { - flex: 1; - min-width: 0; -} - -.sidebar-playlists-toggle { - display: flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - padding: 0; - border-radius: var(--radius-md); - background: transparent; - border: none; - color: var(--text-muted); - cursor: pointer; - transition: all var(--transition-fast); - flex-shrink: 0; -} - -.sidebar-playlists-toggle:hover { - background: var(--bg-hover); - color: var(--text-primary); -} - -.sidebar-playlists-toggle.expanded { - transform: rotate(90deg); - color: var(--accent); -} - -.sidebar-playlists-section { - display: flex; - flex-direction: column; -} - -.sidebar-playlists-header { - display: flex; - align-items: center; - gap: var(--space-3); - width: 100%; - padding: var(--space-2) var(--space-3); - border-radius: var(--radius-md); - color: var(--text-secondary); - font-size: 14px; - font-weight: 500; - transition: background var(--transition-fast), color var(--transition-fast); - cursor: pointer; - background: transparent; - border: none; - text-align: left; -} - -.sidebar-playlists-header:hover { - background: var(--bg-hover); - color: var(--text-primary); -} - -.sidebar-playlists-header.active { - background: var(--accent-dim); - color: var(--accent); -} - -.sidebar-playlists-chevron { - margin-left: auto; - transition: transform 0.2s ease; - flex-shrink: 0; -} - -.sidebar-playlists-chevron.expanded { - transform: rotate(90deg); -} - -.sidebar-playlists-list { - display: flex; - flex-direction: column; - padding-left: var(--space-4); - padding-top: var(--space-1); - padding-bottom: var(--space-1); - gap: var(--space-1); -} - -.sidebar-playlist-item { - display: flex; - align-items: center; - gap: var(--space-2); - padding: var(--space-1) var(--space-2); - border-radius: var(--radius-md); - color: var(--text-secondary); - font-size: 13px; - font-weight: 400; - transition: background var(--transition-fast), color var(--transition-fast); - overflow: hidden; -} - -.sidebar-playlist-item span { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - flex: 1; - min-width: 0; -} - -.sidebar-playlist-item:hover { - background: var(--bg-hover); - color: var(--text-primary); -} - -.sidebar-playlist-item.active { - background: var(--accent-dim); - color: var(--accent); -} - -.sidebar-playlists-loading { - display: flex; - align-items: center; - justify-content: center; - padding: var(--space-2); -} - -.sidebar-playlists-empty { - padding: var(--space-2); - font-size: 12px; - color: var(--text-muted); - font-style: italic; -} diff --git a/src/styles/layout/_intro.css b/src/styles/layout/_intro.css new file mode 100644 index 00000000..322ec64f --- /dev/null +++ b/src/styles/layout/_intro.css @@ -0,0 +1,42 @@ +/* ─── Psysonic App Shell ─── */ + +*, *::before, *::after { + user-select: none; +} + +/* Allow text selection only where the user needs to copy content */ +.artist-bio, [data-selectable] { + user-select: text; +} + +.app-shell { + position: relative; + display: grid; + grid-template-columns: var(--sidebar-width) 1fr var(--queue-width); + /* minmax(0, 1fr) instead of plain 1fr: CSS Grid spec treats 1fr as + minmax(auto, 1fr), meaning the row won't shrink below its min-content + size. With minmax(0, 1fr) the row can collapse to zero, keeping the + player bar row always visible regardless of content size or zoom level. */ + grid-template-rows: minmax(0, 1fr) var(--player-height); + grid-template-areas: + "sidebar main queue" + "player player player"; + /* height: 100% inherits from html/body/#root (all 100%) so the grid + tracks actual window resize events on every platform. + 100vh was used previously but WebView2 (Windows) does not update the + CSS viewport synchronously on resize, causing the player-bar grid row + to drift off-screen. overflow: hidden still prevents the 1fr row from + pushing the grid taller than the window on Linux WMs that ignore + the OS minHeight constraint. */ + height: 100%; + overflow: hidden; + background: var(--bg-app); +} + +/* When player bar is floating, content extends to bottom */ +.app-shell.floating-player { + grid-template-rows: minmax(0, 1fr); + grid-template-areas: + "sidebar main queue"; +} + diff --git a/src/styles/layout/app-updater-toast-mobile.css b/src/styles/layout/app-updater-toast-mobile.css new file mode 100644 index 00000000..808c05f9 --- /dev/null +++ b/src/styles/layout/app-updater-toast-mobile.css @@ -0,0 +1,5 @@ +/* ─── App Updater Toast — Mobile ─── */ +.app-shell[data-mobile] .app-updater-toast { + bottom: calc(64px + 56px + 12px); +} + diff --git a/src/styles/layout/bottomnav-component.css b/src/styles/layout/bottomnav-component.css new file mode 100644 index 00000000..4d92a0ff --- /dev/null +++ b/src/styles/layout/bottomnav-component.css @@ -0,0 +1,80 @@ +/* ─── BottomNav Component ─── */ +.bottom-nav { + grid-area: bottomnav; + display: flex; + align-items: stretch; + justify-content: space-around; + background: var(--bg-sidebar); + border-top: 1px solid var(--border-subtle); + height: 56px; + z-index: 100; + flex-shrink: 0; +} + +.bottom-nav-item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 3px; + flex: 1; + color: var(--text-muted); + text-decoration: none; + font-size: 10px; + font-weight: 500; + letter-spacing: 0.02em; + transition: color var(--transition-fast); + position: relative; +} + +.bottom-nav-item:hover { + color: var(--text-secondary); +} + +.bottom-nav-item.active { + color: var(--accent); +} + +.bottom-nav-item.active::before { + content: ''; + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); + width: 32px; + height: 2px; + background: var(--accent); + border-radius: 0 0 var(--radius-full) var(--radius-full); +} + +.bottom-nav-item svg { + opacity: 0.65; + transition: opacity var(--transition-fast); +} + +.bottom-nav-item.active svg, +.bottom-nav-item:hover svg { + opacity: 1; +} + +.bottom-nav-icon-wrap { + position: relative; + display: flex; + align-items: center; +} + +.bottom-nav-np-dot { + position: absolute; + top: -2px; + right: -5px; + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--accent); + animation: np-dot-pulse 1.8s ease-in-out infinite; +} + +.bottom-nav-label { + line-height: 1; +} + diff --git a/src/styles/layout/content-header-mobile.css b/src/styles/layout/content-header-mobile.css new file mode 100644 index 00000000..070658b4 --- /dev/null +++ b/src/styles/layout/content-header-mobile.css @@ -0,0 +1,16 @@ +/* ─── Content Header — Mobile ─── */ +.app-shell[data-mobile] .content-header { + padding: 0 var(--space-4); + height: 52px; + gap: var(--space-3); +} + +/* Hide queue toggle on mobile */ +.app-shell[data-mobile] .queue-toggle-btn { + display: none; +} + +.app-shell[data-mobile] .resizer-queue-handle { + display: none; +} + diff --git a/src/styles/layout/cover-art.css b/src/styles/layout/cover-art.css new file mode 100644 index 00000000..a1c2ef82 --- /dev/null +++ b/src/styles/layout/cover-art.css @@ -0,0 +1,26 @@ +/* ─── Cover Art ─── */ +.mp-cover-wrap { + flex: 0 1 auto; + min-height: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.mp-cover { + width: min(calc(100vw - 28px), 55vh); + max-width: 440px; + aspect-ratio: 1; + object-fit: cover; + border-radius: 14px; + box-shadow: 0 16px 52px rgba(0, 0, 0, 0.7), 0 4px 16px rgba(0, 0, 0, 0.4); +} + +.mp-cover-fallback { + display: flex; + align-items: center; + justify-content: center; + background: var(--bg-card); + color: var(--text-muted); +} + diff --git a/src/styles/layout/custom-title-bar-linux-only-decorations-false.css b/src/styles/layout/custom-title-bar-linux-only-decorations-false.css new file mode 100644 index 00000000..73c25502 --- /dev/null +++ b/src/styles/layout/custom-title-bar-linux-only-decorations-false.css @@ -0,0 +1,139 @@ +/* ─── Custom title bar (Linux only — decorations: false) ─── */ +:root { + --titlebar-height: 32px; +} + +.app-shell[data-titlebar] { + grid-template-rows: var(--titlebar-height) minmax(0, 1fr) var(--player-height); + grid-template-areas: + "titlebar titlebar titlebar" + "sidebar main queue" + "player player player"; +} + +/* When player bar is floating with titlebar, content extends to bottom */ +.app-shell[data-titlebar].floating-player { + grid-template-rows: var(--titlebar-height) minmax(0, 1fr); + grid-template-areas: + "titlebar titlebar titlebar" + "sidebar main queue"; +} + +/* Resize grips — replace the native GTK grips lost when decorations: false */ +.app-shell[data-titlebar]::before, +.app-shell[data-titlebar]::after { + content: ''; + position: fixed; + bottom: 0; + width: 14px; + height: 14px; + pointer-events: none; + background-image: radial-gradient(circle, var(--text-muted) 1.5px, transparent 1.5px); + background-size: 4px 4px; + opacity: 0.35; + z-index: 9999; +} +.app-shell[data-titlebar]::before { left: 0; } +.app-shell[data-titlebar]::after { right: 0; } + +.titlebar { + grid-area: titlebar; + display: flex; + align-items: center; + justify-content: flex-end; + background: var(--bg-sidebar); + border-bottom: 1px solid var(--border-subtle); + padding: 0 6px 0 12px; + height: var(--titlebar-height); + user-select: none; +} + +.titlebar-track { + position: absolute; + left: 50%; + transform: translateX(-50%); + display: flex; + align-items: center; + gap: 6px; + max-width: 40%; + pointer-events: none; + padding: 3px 10px; + background: rgba(0, 0, 0, 0.45); + border-radius: 999px; + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08); +} + +.titlebar-track:empty { + display: none; +} + +.titlebar-track-state { + font-size: 9px; + color: var(--accent); + flex-shrink: 0; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.6); +} + +.titlebar-track-text { + font-size: 11px; + color: #ececec; + max-width: 100%; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.5); +} + +.titlebar-controls { + display: flex; + align-items: center; + gap: 8px; + padding: 0 8px; + flex: 0 0 auto; +} + +.titlebar-btn { + display: flex; + align-items: center; + justify-content: center; + width: 14px; + height: 14px; + padding: 0; + border: none; + border-radius: 50%; + cursor: pointer; + box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.18); + transition: filter var(--transition-fast), box-shadow var(--transition-fast); +} + +.titlebar-btn-close { + background: #ff5f57; +} + +.titlebar-btn-minimize { + background: #febc2e; +} + +.titlebar-btn-maximize { + background: #28c840; +} + +.titlebar-btn:hover { + filter: brightness(1.1); +} + +.titlebar-btn-close:hover { box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.18), 0 0 6px rgba(255, 95, 87, 0.75); } +.titlebar-btn-minimize:hover { box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.18), 0 0 6px rgba(254, 188, 46, 0.75); } +.titlebar-btn-maximize:hover { box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.18), 0 0 6px rgba(40, 200, 64, 0.75); } + +.titlebar-btn:active { + filter: brightness(0.92); +} + +.titlebar-btn:focus-visible { + outline: none; + box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.18), 0 0 0 2px rgba(255, 255, 255, 0.5); +} + +/* Resizer handles must start below the titlebar */ +.app-shell[data-titlebar] .resizer { + top: var(--titlebar-height); +} + diff --git a/src/styles/layout/empty-state.css b/src/styles/layout/empty-state.css new file mode 100644 index 00000000..9d32908c --- /dev/null +++ b/src/styles/layout/empty-state.css @@ -0,0 +1,12 @@ +/* ─── Empty state ─── */ +.mp-empty { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--space-4); + color: var(--text-muted); + font-size: 15px; +} + diff --git a/src/styles/layout/eq-popup.css b/src/styles/layout/eq-popup.css new file mode 100644 index 00000000..98d7e1f5 --- /dev/null +++ b/src/styles/layout/eq-popup.css @@ -0,0 +1,59 @@ +/* ─── EQ Popup ─── */ +.eq-popup-backdrop { + position: fixed; + inset: 0; + z-index: 200; + background: rgba(0, 0, 0, 0.35); + backdrop-filter: blur(2px); +} + +.eq-popup { + position: fixed; + z-index: 201; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: min(700px, 92vw); + background: var(--bg-sidebar); + border: 1px solid var(--border); + border-radius: 14px; + box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5); + padding: 20px; + display: flex; + flex-direction: column; + gap: 16px; +} +.eq-popup .eq-wrap { + max-width: none; +} + +.eq-popup-header { + display: flex; + align-items: center; + justify-content: space-between; +} + +.eq-popup-title { + font-size: 15px; + font-weight: 600; + color: var(--text-primary); +} + +.eq-popup-close { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 6px; + background: var(--bg-hover); + border: 1px solid var(--border); + color: var(--text-muted); + cursor: pointer; + transition: background 0.15s, color 0.15s; +} +.eq-popup-close:hover { + background: var(--accent); + color: var(--bg-app); +} + diff --git a/src/styles/layout/grid-overrides.css b/src/styles/layout/grid-overrides.css new file mode 100644 index 00000000..285da165 --- /dev/null +++ b/src/styles/layout/grid-overrides.css @@ -0,0 +1,26 @@ +/* ─── Grid Overrides ─── */ +.app-shell[data-mobile] { + grid-template-columns: 1fr; + grid-template-rows: minmax(0, 1fr) auto auto; + grid-template-areas: + "main" + "bottomnav" + "player"; + width: 100vw; + max-width: 100%; +} + +.app-shell[data-mobile][data-titlebar] { + grid-template-rows: var(--titlebar-height) minmax(0, 1fr) auto auto; + grid-template-areas: + "titlebar" + "main" + "bottomnav" + "player"; +} + +/* Hide the sidebar collapse button on mobile */ +.app-shell[data-mobile] .collapse-btn { + display: none; +} + diff --git a/src/styles/layout/header.css b/src/styles/layout/header.css new file mode 100644 index 00000000..7595bdf3 --- /dev/null +++ b/src/styles/layout/header.css @@ -0,0 +1,36 @@ +/* ─── Header ─── */ +.mp-header { + display: flex; + align-items: center; + justify-content: space-between; + height: 52px; + flex-shrink: 0; +} + +.mp-header-title { + font-size: 13px; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--text-muted); +} + +.mp-back { + display: flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + border-radius: 50%; + background: none; + border: none; + color: var(--text-secondary); + cursor: pointer; + transition: color var(--transition-fast), background var(--transition-fast); +} + +.mp-back:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + diff --git a/src/styles/layout/index.css b/src/styles/layout/index.css new file mode 100644 index 00000000..61303c3a --- /dev/null +++ b/src/styles/layout/index.css @@ -0,0 +1,29 @@ +@import './_intro.css'; +@import './custom-title-bar-linux-only-decorations-false.css'; +@import './resizer-handles.css'; +@import './sidebar.css'; +@import './update-toast.css'; +@import './sidebar-offline-download-queue.css'; +@import './sidebar-device-sync-queue.css'; +@import './main-content.css'; +@import './offline-banner.css'; +@import './offline-storage-full-banner.css'; +@import './player-bar.css'; +@import './eq-popup.css'; +@import './queue-panel.css'; +@import './overlayscrollarea-shared-queue-mini-sidebar-menus.css'; +@import './bottomnav-component.css'; +@import './mobile-more-sheet.css'; +@import './grid-overrides.css'; +@import './content-header-mobile.css'; +@import './player-bar-mobile.css'; +@import './app-updater-toast-mobile.css'; +@import './mobile-player-takeover.css'; +@import './header.css'; +@import './cover-art.css'; +@import './track-metadata.css'; +@import './scrubber.css'; +@import './transport-controls.css'; +@import './empty-state.css'; +@import './utility-footer.css'; +@import './sidebar-expandable-playlists-section.css'; diff --git a/src/styles/layout/main-content.css b/src/styles/layout/main-content.css new file mode 100644 index 00000000..fc0fc3a8 --- /dev/null +++ b/src/styles/layout/main-content.css @@ -0,0 +1,122 @@ +/* ─── Main Content ─── */ +.main-content { + grid-area: main; + display: flex; + flex-direction: column; + overflow: hidden; + min-width: 0; + min-height: 0; /* allow 1fr row to shrink freely */ + background: var(--bg-app); + z-index: 1; +} + +/* Scoped UI scaling target. Sidebar / queue / player / titlebar live in + sibling grid cells of .app-shell and stay at 1:1. The wrapper inherits + .main-content's flex column layout so .content-header (fixed height) and + .content-body (flex: 1, scroll) keep working unchanged. + Zoom is applied via inline style only when uiScale !== 1, so the default + case has no extra layer or layout cost. */ +.main-content-zoom { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + min-width: 0; +} + +.content-header { + display: flex; + align-items: center; + gap: var(--space-4); + padding: 0 var(--space-6); + height: 64px; + box-sizing: border-box; + border-bottom: 1px solid var(--border-subtle); + background: var(--bg-app); + position: relative; + z-index: 10; +} + +.content-header h1 { + font-family: var(--font-display); + font-size: 18px; + font-weight: 600; + color: var(--text-primary); +} + +.content-header .spacer { flex: 1; } + +.content-header > * { + transition: opacity 0.14s ease, visibility 0.14s ease; +} + +.content-header[data-live-search-overlay] > :not(.live-search):not(.spacer) { + opacity: 0; + visibility: hidden; + pointer-events: none; +} + +.content-body { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding: var(--space-6); + contain: paint; +} + +/* When floating player is active, add extra bottom padding so content doesn't + get hidden behind the fixed floating bar (player height + bottom offset + minimal spacing). + !important needed to override inline padding:0 on App.tsx content-body */ +.app-shell.floating-player .content-body { + padding-bottom: calc(var(--player-height) + 12px) !important; +} + +/* Every page re-uses .content-body as its outer wrapper, but App.tsx already + renders one around as the scroll container. The inner one must + not establish its own scroll ancestor — otherwise `position: sticky` + children anchor to a container that never scrolls, and the header just + scrolls along with the content. Padding stays: the outer App.tsx + content-body has `padding: 0` inline, the inner one is what gives each + page its breathing room. */ +.content-body .content-body { + overflow: visible; +} + +/* Main route stack: scroll + overlay thumb live in the inner viewport, not the + outer .content-body, so sticky headers still anchor to the scrolling element. */ +.content-body.app-shell-route-host { + flex: 1; + min-height: 0; + overflow: hidden; + display: flex; + flex-direction: column; + padding: 0; + position: relative; + contain: none; +} + +.app-shell-route-scroll.overlay-scroll { + flex: 1; + min-height: 0; +} + +.app-shell-route-scroll .app-shell-route-scroll__viewport { + contain: paint; +} + +/* Sticky page header: keeps page title + filter/search bar visible while the + body scrolls. Negative horizontal margins (+ matching padding) make the + background flush with the scroll container edges so the sticky block looks + like a real bar, not a floating island. `top: 0` clamps below the content- + body's own padding-top; the residual 24px gap above the sticky block while + scrolling is intentional — gives the bar visual breathing room. */ +.page-sticky-header { + position: sticky; + top: 0; + z-index: 5; + background: var(--bg-app); + margin: 0 calc(-1 * var(--space-6)) var(--space-5); + padding: var(--space-4) var(--space-6); + border-bottom: 1px solid var(--border-subtle); +} + diff --git a/src/styles/layout/mobile-more-sheet.css b/src/styles/layout/mobile-more-sheet.css new file mode 100644 index 00000000..dfeefaf2 --- /dev/null +++ b/src/styles/layout/mobile-more-sheet.css @@ -0,0 +1,87 @@ +/* ─── Mobile More Sheet ─── */ +.mobile-more-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.45); + z-index: 200; +} + +.mobile-more-sheet { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: var(--bg-app); + border-radius: 16px 16px 0 0; + border-top: 1px solid var(--border); + z-index: 201; + padding: 0.5rem 0.75rem calc(0.75rem + var(--bottom-nav-height, 62px)); + animation: more-sheet-up 0.22s ease; +} + +@keyframes more-sheet-up { + from { transform: translateY(100%); } + to { transform: translateY(0); } +} + +.mobile-more-handle { + width: 36px; + height: 4px; + background: var(--border); + border-radius: 2px; + margin: 0 auto 0.75rem; +} + +.mobile-more-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 0.25rem 0; +} + +.mobile-more-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.3rem; + padding: 0.65rem 0.25rem; + border-radius: var(--radius); + color: var(--text-secondary); + text-decoration: none; + font-size: 11px; + font-weight: 500; + cursor: pointer; + background: none; + border: none; + transition: color var(--transition-fast), background var(--transition-fast); +} + +.mobile-more-item:hover { + color: var(--text-primary); + background: var(--bg-hover); +} + +.mobile-more-item.active { + color: var(--accent); +} + +.mobile-more-icon { + display: flex; + align-items: center; + justify-content: center; + opacity: 0.75; +} + +.mobile-more-item.active .mobile-more-icon, +.mobile-more-item:hover .mobile-more-icon { + opacity: 1; +} + +.mobile-more-label { + line-height: 1.2; + text-align: center; + max-width: 72px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + diff --git a/src/styles/layout/mobile-player-takeover.css b/src/styles/layout/mobile-player-takeover.css new file mode 100644 index 00000000..73f14d2d --- /dev/null +++ b/src/styles/layout/mobile-player-takeover.css @@ -0,0 +1,41 @@ +/* ─── Mobile Player Takeover ─── */ +/* When the mobile player is active, the app-shell becomes a single full-screen + area with no BottomNav or PlayerBar — just the main content. */ +.app-shell[data-mobile-player] { + grid-template-rows: minmax(0, 1fr); + grid-template-areas: "main"; +} + +.app-shell[data-mobile-player] .content-header { + display: none; +} + +.app-shell[data-mobile-player] .content-body { + padding: 0 !important; + overflow: hidden !important; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + MOBILE PLAYER VIEW (MobilePlayerView.tsx) + ═══════════════════════════════════════════════════════════════════════════ */ + +.mp-view { + position: fixed; + inset: 0; + display: flex; + flex-direction: column; + justify-content: space-evenly; + background: var(--bg-app); /* fallback; overridden by dynamic gradient via inline style */ + overflow: hidden !important; + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE/Edge */ + padding: 0 20px env(safe-area-inset-bottom, 0); + box-sizing: border-box; + transition: background 0.8s ease; + z-index: 10; +} + +.mp-view::-webkit-scrollbar { + display: none; /* Chrome, Safari, Tauri/WebKitGTK */ +} + diff --git a/src/styles/layout/offline-banner.css b/src/styles/layout/offline-banner.css new file mode 100644 index 00000000..05b6d845 --- /dev/null +++ b/src/styles/layout/offline-banner.css @@ -0,0 +1,41 @@ +/* ─── Offline Banner ─── */ +.offline-banner { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 16px; + background: color-mix(in srgb, var(--accent) 12%, var(--bg-sidebar)); + border-bottom: 1px solid color-mix(in srgb, var(--accent) 30%, transparent); + color: var(--accent); + font-size: 12px; + font-weight: 500; + flex-shrink: 0; +} + +.offline-banner span { + flex: 1; +} + +.offline-banner-retry { + display: flex; + align-items: center; + gap: 4px; + background: none; + border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent); + border-radius: var(--radius-sm); + color: var(--accent); + font-size: 11px; + padding: 2px 8px; + cursor: pointer; + transition: background var(--transition-fast); +} + +.offline-banner-retry:hover { + background: color-mix(in srgb, var(--accent) 15%, transparent); +} + +.offline-banner-retry:disabled { + opacity: 0.5; + cursor: default; +} + diff --git a/src/styles/layout/offline-storage-full-banner.css b/src/styles/layout/offline-storage-full-banner.css new file mode 100644 index 00000000..8b465f60 --- /dev/null +++ b/src/styles/layout/offline-storage-full-banner.css @@ -0,0 +1,34 @@ +/* ─── Offline Storage Full Banner ─── */ +.offline-storage-full-banner { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 10px 16px; + margin: 0 0 1rem 0; + background: color-mix(in srgb, var(--color-danger, #e53935) 12%, var(--bg-card)); + border: 1px solid color-mix(in srgb, var(--color-danger, #e53935) 40%, transparent); + border-radius: var(--radius-md); + font-size: 13px; + color: var(--text-primary); + line-height: 1.4; +} + +.offline-storage-full-banner span { + flex: 1; +} + +.offline-storage-full-dismiss { + background: none; + border: none; + cursor: pointer; + color: var(--text-muted); + font-size: 18px; + line-height: 1; + padding: 0 2px; + flex-shrink: 0; +} + +.offline-storage-full-dismiss:hover { + color: var(--text-primary); +} + diff --git a/src/styles/layout/overlayscrollarea-shared-queue-mini-sidebar-menus.css b/src/styles/layout/overlayscrollarea-shared-queue-mini-sidebar-menus.css new file mode 100644 index 00000000..46db9e87 --- /dev/null +++ b/src/styles/layout/overlayscrollarea-shared-queue-mini-sidebar-menus.css @@ -0,0 +1,191 @@ +/* ─── OverlayScrollArea (shared: queue, mini, sidebar, menus) ─────────────── */ +.overlay-scroll { + --overlay-rail-top: 0px; + --overlay-rail-bottom: 0px; + position: relative; + overflow: hidden; + min-height: 0; + display: flex; + flex-direction: column; +} + +.overlay-scroll--rail-inset-none { + --overlay-rail-top: 0px; + --overlay-rail-bottom: 0px; +} + +.overlay-scroll--rail-inset-mini { + --overlay-rail-top: 4px; + --overlay-rail-bottom: 4px; +} + +.overlay-scroll--rail-inset-panel { + --overlay-rail-top: var(--space-2); + --overlay-rail-bottom: var(--space-2); +} + +.overlay-scroll--viewport-scroll-auto .overlay-scroll__viewport { + scroll-behavior: auto; +} + +.overlay-scroll__viewport { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + overscroll-behavior: contain; + box-sizing: border-box; + scrollbar-width: none; +} + +.overlay-scroll__viewport::-webkit-scrollbar { + display: none; +} + +.overlay-scroll__rail { + position: absolute; + top: var(--overlay-rail-top); + right: 0; + bottom: var(--overlay-rail-bottom); + width: 14px; + z-index: 1; + pointer-events: auto; +} + +.overlay-scroll__thumb { + position: absolute; + top: 0; + right: 2px; + width: 3px; + border-radius: 2px; + background: var(--text-muted); + opacity: 0; + transition: + opacity 0.18s ease, + width 0.2s ease, + border-radius 0.2s ease; + pointer-events: none; + will-change: transform; + cursor: grab; + touch-action: none; +} + +.overlay-scroll__viewport.is-scrolling ~ .overlay-scroll__rail .overlay-scroll__thumb, +.overlay-scroll:hover .overlay-scroll__rail .overlay-scroll__thumb { + opacity: 0.55; + pointer-events: auto; +} + +.overlay-scroll__rail:hover .overlay-scroll__thumb, +.overlay-scroll__rail:has(.overlay-scroll__thumb.is-thumb-dragging) .overlay-scroll__thumb { + width: 9px; + border-radius: 4px; +} + +.overlay-scroll__rail .overlay-scroll__thumb.is-thumb-dragging { + opacity: 0.9; + pointer-events: auto; + cursor: grabbing; + transition: opacity 0.18s ease; +} + +.queue-list-wrap.overlay-scroll { + flex: 1 1 auto; +} + +.queue-list.overlay-scroll__viewport { + padding: var(--space-2); +} + +.queue-empty { + padding: var(--space-4); + color: var(--text-muted); + text-align: center; + font-size: 14px; +} + +.queue-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-2) var(--space-3); + border-radius: var(--radius-sm); + cursor: pointer; + transition: background var(--transition-fast); + color: var(--text-secondary); +} +/* Prevent child elements from stealing dragenter/dragleave events */ +.queue-item > * { pointer-events: none; } + +.queue-item:hover, .queue-item.context-active { + background: var(--bg-hover); + color: var(--text-primary); +} + +.queue-item.active { + background: var(--accent-dim); + color: var(--accent); +} + +.queue-item-info { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; +} + +.queue-item-title { + font-size: 12px; + font-weight: 500; + color: inherit; +} + +.queue-item-artist { + font-size: 11px; + color: var(--text-muted); + transition: color var(--transition-fast); +} + +.queue-item:hover .queue-item-artist, +.queue-item.context-active .queue-item-artist { + color: var(--text-secondary); +} + +.queue-item-duration { + font-size: 11px; + color: var(--text-muted); + flex-shrink: 0; + margin-left: var(--space-3); +} + +.queue-item.active .queue-item-artist, +.queue-item.active .queue-item-duration { + color: inherit; + opacity: 0.8; +} + +.queue-item-attribution, +.queue-current-attribution { + font-size: 10px; + color: var(--text-muted); + opacity: 0.75; + margin-top: 1px; + font-style: italic; +} + +.queue-item:hover .queue-item-attribution, +.queue-item.context-active .queue-item-attribution { + color: var(--text-secondary); + opacity: 0.9; +} + +.queue-item.active .queue-item-attribution { + color: inherit; + opacity: 0.7; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + MOBILE LAYOUT (< 800px) + Controller: data-mobile attribute set by useIsMobile hook + ═══════════════════════════════════════════════════════════════════════════ */ + diff --git a/src/styles/layout/player-bar-mobile.css b/src/styles/layout/player-bar-mobile.css new file mode 100644 index 00000000..1d2a5574 --- /dev/null +++ b/src/styles/layout/player-bar-mobile.css @@ -0,0 +1,41 @@ +/* ─── Player Bar — Mobile ─── */ +.app-shell[data-mobile] .player-bar { + gap: var(--space-3); + padding: 0 var(--space-3); + height: 64px; +} + +.app-shell[data-mobile] .player-track-info { + flex: 1 1 0; + min-width: 0; +} + +.app-shell[data-mobile] .player-waveform-section, +.app-shell[data-mobile] .player-volume-section, +.app-shell[data-mobile] .player-eq-btn, +.app-shell[data-mobile] .player-bar > .player-btn.player-btn-sm:not(.player-star-btn):not(.player-love-btn) { + display: none; +} + +.app-shell[data-mobile] .player-buttons { + gap: var(--space-1); + flex-shrink: 0; +} + +.app-shell[data-mobile] .player-btn-primary { + width: 40px; + height: 40px; +} + +.app-shell[data-mobile] .player-album-art, +.app-shell[data-mobile] .player-album-art-placeholder { + width: 44px; + height: 44px; +} + +/* Hide star + last.fm love buttons in mobile player to save space */ +.app-shell[data-mobile] .player-star-btn, +.app-shell[data-mobile] .player-love-btn { + display: none; +} + diff --git a/src/styles/layout/player-bar.css b/src/styles/layout/player-bar.css new file mode 100644 index 00000000..394b654c --- /dev/null +++ b/src/styles/layout/player-bar.css @@ -0,0 +1,428 @@ +/* ─── Player Bar ─── */ +.player-bar { + grid-area: player; + display: flex; + align-items: center; + gap: var(--space-6); + padding: 0 var(--space-5); + background: var(--bg-player); + border-top: 1px solid var(--border-subtle); + height: var(--player-height); + position: relative; + z-index: 100; + /* WebKitGTK (software compositing) occasionally paints the bar fully + black for one frame when an unrelated layer in the page invalidates. + `contain` isolates layout/paint so the bar's region cannot + participate in the surrounding dirty rect. */ + contain: layout paint; +} + +/* Floating player bar styles - positioning handled by ResizeObserver */ +.player-bar.floating { + position: fixed; + bottom: 12px; + /* left/right handled dynamically by JS */ + z-index: 1000; + border-radius: 50px; + width: auto; + min-width: 100px; + max-width: none; + grid-area: unset; + padding: 0 24px 0 8px; + gap: 16px; + background: var(--bg-player); + border: 1px solid rgba(255, 255, 255, 0.12); + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.8), + 0 0 0 1px rgba(255, 255, 255, 0.05), + inset 0 1px 0 rgba(255, 255, 255, 0.08); +} + +/* Internal element adjustments for floating mode */ +.player-bar.floating .player-track-info { + flex: 0 0 auto; + min-width: 240px; + max-width: 320px; +} + +.player-bar.floating .player-album-art-wrap { + border-radius: 50%; +} + +.player-bar.floating .player-album-art, +.player-bar.floating .player-album-art-placeholder { + border-radius: 0; +} + +.player-bar.floating .player-buttons { + flex-shrink: 0; + gap: 16px; +} + +.player-bar.floating .player-waveform-section { + flex: 1; + min-width: 200px; +} + +.player-bar.floating .player-volume-section { + flex: 0 0 auto; + min-width: 120px; + display: flex; + align-items: center; + gap: 8px; +} + +/* Liquid-glass floating bar — macOS + Windows only. + Inspired by PR #211 (kveld9), adapted to theme tokens and reduced + blur so it stays within WebView2 / WKWebView GPU budgets. Linux + always keeps the solid look from PR #216, regardless of compositor. */ +html[data-platform="macos"] .player-bar.floating, +html[data-platform="windows"] .player-bar.floating { + background: color-mix(in srgb, var(--bg-player) 72%, transparent); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + border: 1px solid color-mix(in srgb, var(--text-primary) 10%, transparent); + box-shadow: + 0 12px 40px rgba(0, 0, 0, 0.45), + inset 0 1px 0 color-mix(in srgb, var(--text-primary) 12%, transparent); +} + +.player-track-info { + display: flex; + align-items: center; + gap: var(--space-3); + flex: 0 0 320px; + min-width: 0; + overflow: hidden; +} + +.player-album-art { + width: 52px; + height: 52px; + border-radius: var(--radius-md); + object-fit: cover; + background: var(--bg-card); + flex-shrink: 0; + box-shadow: var(--shadow-md); +} + +.player-album-art-placeholder { + width: 52px; + height: 52px; + border-radius: var(--radius-md); + background: var(--bg-card); + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-muted); +} + +.player-album-art-wrap { + position: relative; + flex-shrink: 0; + border-radius: var(--radius-sm); + overflow: hidden; + background: var(--bg-card); +} +.player-album-art-wrap::after { + content: ''; + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.3); + opacity: 0; + transition: opacity var(--transition-fast); + pointer-events: none; +} +.player-album-art-wrap.clickable { cursor: pointer; } +.player-album-art-wrap:hover::after { opacity: 1; } + +/* Expand icon hint that appears on cover hover */ +.player-art-expand-hint { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + color: white; + opacity: 0; + transition: opacity var(--transition-fast); + pointer-events: none; + z-index: 1; +} +.player-album-art-wrap.clickable:hover .player-art-expand-hint { opacity: 1; } + +.player-track-meta { + flex: 1 1 0; + min-width: 0; + overflow: hidden; +} + +.player-track-name { + font-size: 14px; + font-weight: 600; + color: var(--text-primary); +} + +.player-track-artist { + font-size: 12px; + color: var(--text-secondary); + margin-top: 2px; +} + +.player-track-rating { + margin-top: 4px; + width: auto; + justify-content: flex-start; +} + +.player-track-rating .star { + font-size: 11px; + padding: 0 1px; +} + +/* ── Marquee (PlayerBar track name / artist) ── */ +.marquee-wrap { + overflow: hidden; + white-space: nowrap; + width: 100%; +} + +.marquee-scroll { + display: inline-block; + animation: player-marquee 12s linear infinite; + animation-delay: 2s; +} + +@keyframes player-marquee { + 0%, 12% { transform: translateX(0); } + 78%, 88% { transform: translateX(var(--marquee-amount)); } + 88.1%, 100% { transform: translateX(0); } +} + +.player-buttons { + display: flex; + align-items: center; + gap: var(--space-2); + flex-shrink: 0; +} + +.player-btn { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border-radius: 50%; + color: var(--text-muted); + transition: all var(--transition-fast); + flex-shrink: 0; +} + +.player-btn:hover { + background: var(--bg-hover); + color: var(--text-primary); + transform: scale(1.12); +} + +.player-btn-sm { + width: 28px; + height: 28px; +} + +/* Star + Last.fm heart — visually separated from track title */ +.player-star-btn { + margin: var(--space-1); + color: var(--text-muted); + transition: color var(--transition-fast); +} + +.player-star-btn:hover, +.player-star-btn.is-starred { + color: var(--color-star-active, var(--accent)); +} + +.player-btn-primary { + width: 46px; + height: 46px; + background: var(--accent); + color: var(--ctp-crust); + border-radius: 50%; + box-shadow: 0 4px 18px rgba(0, 0, 0, 0.4); + flex-shrink: 0; +} + +.player-btn-primary:hover { + background: var(--accent); + color: var(--ctp-crust); + transform: scale(1.06); + box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4), var(--shadow-glow); + filter: brightness(1.12); +} + +/* Waveform seekbar section */ +.player-waveform-section { + flex: 1; + min-width: 0; + display: flex; + align-items: center; + gap: var(--space-2); + margin-left: 24px; + margin-right: 24px; +} + +.player-waveform-wrap { + flex: 1; + min-width: 0; +} + +.player-time { + font-size: 11px; + color: var(--text-muted); + font-variant-numeric: tabular-nums; + white-space: nowrap; + flex-shrink: 0; + min-width: 2.5rem; +} + +.player-time:first-child { + text-align: right; +} + +.player-time:last-child { + text-align: left; +} + +/* Clickable time toggle with swap indicator */ +.player-time-toggle { + cursor: pointer; + display: inline-flex; + align-items: center; + padding: 2px 4px; + border-radius: 4px; + transition: background-color 0.15s, color 0.15s; +} + +.player-time-toggle:hover { + background-color: var(--surface-hover, rgba(128, 128, 128, 0.2)); + color: var(--text-primary); +} + +.player-time-toggle:active { + background-color: var(--surface-active, rgba(128, 128, 128, 0.3)); +} + +/* Volume section */ +.player-volume-section { + display: flex; + align-items: center; + gap: var(--space-2); + flex-shrink: 0; + width: 155px; +} + +.player-volume-slider-wrap { + flex: 1; + min-width: 0; + position: relative; +} + +.player-volume-slider { + width: 100%; + display: block; + margin: 0; + vertical-align: middle; +} + +.player-volume-pct { + position: absolute; + bottom: calc(100% + 6px); + transform: translateX(-50%); + font-size: 10px; + font-weight: 600; + background: var(--bg-card); + color: var(--text-primary); + border: 1px solid var(--border-subtle); + padding: 2px 6px; + border-radius: var(--radius-sm); + pointer-events: none; + white-space: nowrap; + box-shadow: var(--shadow-sm); +} + +.player-eq-btn { + color: var(--text-muted); + flex-shrink: 0; + transition: color 0.15s; +} +.player-eq-btn:hover, +.player-eq-btn.active { + color: var(--accent); +} + +.player-overflow-wrap { + position: relative; + flex-shrink: 0; +} + +.player-overflow-menu { + position: absolute; + right: 0; + bottom: calc(100% + 8px); + width: 238px; + padding: 10px; + border-radius: var(--radius-md); + border: 1px solid var(--border); + background: var(--bg-card); + box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35); + z-index: 240; + display: flex; + flex-direction: column; + gap: 8px; +} + +.player-overflow-menu-row { + display: flex; + gap: 6px; +} + +.player-overflow-menu-btn { + flex: 1; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + height: 30px; + border-radius: var(--radius-md); + border: 1px solid var(--border-subtle); + background: var(--bg-hover); + color: var(--text-secondary); + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: background var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast); +} + +.player-overflow-menu-btn:hover { + background: var(--border); + color: var(--text-primary); +} + +.player-overflow-menu-btn.active { + color: var(--accent); + border-color: color-mix(in srgb, var(--accent) 45%, transparent); +} + +.player-volume-section.player-volume-section--menu { + width: 100%; +} + +.player-overflow-menu--volume-only { + padding: 10px; +} + +.player-volume-slider-wrap.player-volume-slider-wrap--menu-only { + width: 100%; + min-width: 0; +} + diff --git a/src/styles/layout/queue-panel.css b/src/styles/layout/queue-panel.css new file mode 100644 index 00000000..0c519a26 --- /dev/null +++ b/src/styles/layout/queue-panel.css @@ -0,0 +1,338 @@ +/* ─── Queue Panel ─── */ +.queue-panel { + grid-area: queue; + background: var(--bg-sidebar); + border-left: 1px solid var(--border-subtle); + display: flex; + flex-direction: column; + overflow: hidden; + min-width: 0; + min-height: 0; /* allow 1fr row to shrink freely */ +} + +.queue-panel.queue-drop-active { + box-shadow: inset 0 0 0 2px var(--accent); + background: linear-gradient(var(--accent-dim), var(--accent-dim)), var(--bg-sidebar); +} + +.queue-header { + height: 64px; + box-sizing: border-box; + padding: 0 var(--space-4); + background: var(--bg-app); + border-bottom: 1px solid var(--border-subtle); + display: flex; + justify-content: space-between; + align-items: center; + flex-shrink: 0; +} + +.queue-action-btn { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: var(--radius-sm); + color: var(--text-muted); + cursor: pointer; + transition: background var(--transition-fast), color var(--transition-fast); + flex-shrink: 0; +} +.queue-action-btn:hover:not(:disabled) { + background: var(--bg-hover); + color: var(--text-primary); +} +.queue-action-btn:disabled { + opacity: 0.35; + cursor: default; +} + +.queue-toolbar { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 0 var(--space-3); + height: 44px; + flex-shrink: 0; + border-bottom: 1px solid var(--border-subtle); + background: var(--bg-app); +} + +.crossfade-popover { + position: absolute; + top: calc(100% + 10px); + right: 0; + width: 170px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: 10px 12px; + box-shadow: 0 4px 16px rgba(0,0,0,0.3); + z-index: 200; + display: flex; + flex-direction: column; + gap: 8px; +} + +.crossfade-popover-label { + display: flex; + align-items: center; + gap: 5px; + font-size: 11px; + font-weight: 600; + color: var(--accent); + letter-spacing: 0.03em; +} + +.crossfade-popover-value { + margin-left: auto; + font-size: 12px; + font-weight: 700; + font-family: monospace; + color: var(--text-primary); +} + +.crossfade-popover-slider { + width: 100%; + accent-color: var(--accent); + cursor: pointer; +} + +.crossfade-popover-range { + display: flex; + justify-content: space-between; + font-size: 10px; + color: var(--text-muted); +} + +.queue-toolbar-sep { + width: 1px; + height: 18px; + background: var(--border-subtle); + margin: 0 2px; + flex-shrink: 0; +} + +.queue-round-btn { + display: flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + border-radius: 50%; + color: var(--text-primary); + background: var(--bg-hover); + border: none; + cursor: pointer; + transition: background var(--transition-fast), color var(--transition-fast); + flex-shrink: 0; +} +.queue-round-btn:hover:not(:disabled) { + background: var(--border); + color: var(--accent); +} +.queue-round-btn:disabled { + opacity: 0.35; + cursor: default; +} +.queue-round-btn.active { + color: var(--ctp-base); + background: var(--accent); +} + +.queue-current-track { + display: flex; + flex-direction: column; + flex-shrink: 0; + border-bottom: 1px solid var(--border-subtle); +} + +.queue-current-track-body { + padding: var(--space-3) var(--space-4); + display: flex; + flex-direction: row; + align-items: flex-start; + gap: var(--space-3); +} + +.queue-current-cover { + width: 90px; + height: 90px; + flex-shrink: 0; + border-radius: var(--radius-md); + overflow: hidden; + background: var(--bg-surface); + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 8px rgba(0,0,0,0.2); + position: relative; +} + +.queue-current-cover img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.queue-current-cover .fallback { + color: var(--text-muted); +} + +.queue-current-info { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; + flex: 1; +} + +.queue-current-info h3 { + font-size: 13px; + font-weight: 700; + margin: 0; + color: var(--text-primary); +} + +.queue-current-sub { + font-size: 12px; + color: var(--text-secondary); + transition: color var(--transition-fast); +} + +.queue-current-sub.is-link { + cursor: pointer; +} +.queue-current-sub.is-link:hover { + color: var(--accent); +} + +.queue-current-tech { + width: 100%; + font-size: 9px; + font-family: monospace; + letter-spacing: 0.05em; + background: var(--ctp-surface1); + color: var(--accent); + padding: 3px var(--space-4); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-align: center; + backdrop-filter: blur(8px); + display: flex; + align-items: center; + justify-content: center; + gap: 6px; +} + +.queue-current-tech-source { + flex-shrink: 0; + display: inline-flex; + align-items: center; + line-height: 0; + opacity: 0.92; +} + +.queue-current-tech-main { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Two-line layout: base info + RG badge on top, optional ReplayGain + values underneath when the badge is expanded. The stack wraps both + rows so the source icon stays vertically centered next to the whole + text block. */ +.queue-current-tech-stack { + min-width: 0; + display: flex; + flex-direction: column; + align-items: center; + gap: 1px; +} + +.queue-current-tech-row { + min-width: 0; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; +} + +/* Pill-shaped toggle that signals "this track has ReplayGain metadata" + without committing screen space to the numbers — values appear in the + tooltip and on the second line when expanded. */ +.queue-current-tech-rg-badge { + flex-shrink: 0; + display: inline-flex; + align-items: center; + gap: 2px; + padding: 1px 5px 1px 6px; + border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent); + background: color-mix(in srgb, var(--accent) 12%, transparent); + color: var(--accent); + border-radius: 999px; + font: inherit; + font-size: 8px; + letter-spacing: 0.06em; + cursor: pointer; + transition: background 0.12s, border-color 0.12s, transform 0.12s; +} +.queue-current-tech-rg-badge:hover { + background: color-mix(in srgb, var(--accent) 22%, transparent); + border-color: color-mix(in srgb, var(--accent) 55%, transparent); +} +.queue-current-tech-rg-badge svg { + transition: transform 0.18s ease; +} +.queue-current-tech-rg-badge--open svg { + transform: rotate(180deg); +} + +.queue-current-tech-rg { + min-width: 0; + max-width: 100%; + font-size: 8px; + opacity: 0.72; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.queue-current-tech-metric { + margin: 0; + padding: 0; + border: none; + background: transparent; + font: inherit; + letter-spacing: inherit; + line-height: inherit; + color: inherit; + cursor: pointer; + text-decoration: none; + border-radius: 2px; +} + +.queue-current-tech-metric:hover { + background: color-mix(in srgb, var(--accent) 14%, transparent); +} + +.queue-current-tech-metric--lufs-reanalyze { + display: inline-flex; + align-items: baseline; + gap: 0; + padding: 2px 4px; + margin: -2px -4px; + max-width: 100%; + min-width: 0; + font-variant-numeric: tabular-nums; +} + +.queue-divider { + padding: var(--space-3) var(--space-4) 0; + flex-shrink: 0; +} + diff --git a/src/styles/layout/resizer-handles.css b/src/styles/layout/resizer-handles.css new file mode 100644 index 00000000..8e7ec13a --- /dev/null +++ b/src/styles/layout/resizer-handles.css @@ -0,0 +1,54 @@ +/* ─── Resizer Handles ─── */ +.resizer { + position: absolute; + top: 0; + bottom: var(--player-height); + width: 6px; + cursor: col-resize; + z-index: 50; + background: transparent; + transition: background var(--transition-fast); +} + +.resizer:hover, .resizer:active { + background: var(--accent); +} + +.resizer-queue { right: calc(var(--queue-width) - 3px); } +.resizer-queue { overflow: visible; } +/* Queue handle has its own visual affordance; don't paint seam stripe on hover. */ +.resizer-queue:hover, .resizer-queue:active { background: transparent; } + +.resizer-queue-handle { + z-index: 101; + width: 22px; + height: 22px; + border-radius: 50%; + border: 1px solid var(--border-subtle); + background: var(--bg-card); + color: var(--text-muted); + cursor: col-resize; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease; + box-shadow: var(--shadow-sm); +} + +.resizer-queue-handle:hover { + background: var(--bg-hover); + color: var(--text-primary); + box-shadow: var(--shadow-md); +} + +/* While dragging an overlay scrollbar thumb, hide queue resizer so the + pointer does not pick up col-resize / queue resize (thumb sits under the + resizer hit box near the main/queue edge). */ +body.is-overlay-scrollbar-thumb-drag .resizer { + pointer-events: none !important; +} + +/* In native fullscreen the resizer sits flush against the screen edge and + would interfere with OS edge-swipe gestures — hide it entirely. */ +.app-shell[data-fullscreen] .resizer { display: none; } + diff --git a/src/styles/layout/scrubber.css b/src/styles/layout/scrubber.css new file mode 100644 index 00000000..541a6506 --- /dev/null +++ b/src/styles/layout/scrubber.css @@ -0,0 +1,58 @@ +/* ─── Scrubber ─── */ +.mp-scrubber-wrap { + padding: 12px 0 2px; + flex-shrink: 0; +} + +.mp-scrubber { + position: relative; + height: 28px; + display: flex; + align-items: center; + cursor: pointer; + touch-action: none; +} + +.mp-scrubber-bg { + position: absolute; + left: 0; + right: 0; + height: 4px; + border-radius: 2px; + background: var(--ctp-surface2, rgba(255,255,255,0.12)); +} + +.mp-scrubber-fill { + position: absolute; + left: 0; + height: 4px; + border-radius: 2px; + background: var(--accent); + transition: width 0.08s linear; +} + +.mp-scrubber-thumb { + position: absolute; + top: 50%; + width: 14px; + height: 14px; + border-radius: 50%; + background: var(--accent); + transform: translate(-50%, -50%); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); + transition: transform 0.1s ease; +} + +.mp-scrubber:active .mp-scrubber-thumb { + transform: translate(-50%, -50%) scale(1.3); +} + +.mp-scrubber-times { + display: flex; + justify-content: space-between; + padding-top: 4px; + font-size: 11px; + color: var(--text-muted); + font-variant-numeric: tabular-nums; +} + diff --git a/src/styles/layout/sidebar-device-sync-queue.css b/src/styles/layout/sidebar-device-sync-queue.css new file mode 100644 index 00000000..71c7fda3 --- /dev/null +++ b/src/styles/layout/sidebar-device-sync-queue.css @@ -0,0 +1,16 @@ +/* ─── Sidebar device-sync queue ─── */ +.sidebar-sync-queue { + background: color-mix(in srgb, var(--success, #4ade80) 10%, var(--bg-sidebar)); + border-color: color-mix(in srgb, var(--success, #4ade80) 25%, transparent); + color: var(--success, #4ade80); +} + +@keyframes spin-slow { + to { transform: rotate(360deg); } +} + +.spin-slow { + animation: spin-slow 2s linear infinite; + flex-shrink: 0; +} + diff --git a/src/styles/layout/sidebar-expandable-playlists-section.css b/src/styles/layout/sidebar-expandable-playlists-section.css new file mode 100644 index 00000000..f67a2aa2 --- /dev/null +++ b/src/styles/layout/sidebar-expandable-playlists-section.css @@ -0,0 +1,140 @@ +/* ─── Sidebar Expandable Playlists Section ─── */ +.sidebar-playlists-wrapper { + display: flex; + flex-direction: column; +} + +.sidebar-playlists-header-row { + display: flex; + align-items: center; + gap: var(--space-1); + padding-right: var(--space-2); +} + +.sidebar-playlists-main-link { + flex: 1; + min-width: 0; +} + +.sidebar-playlists-toggle { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + padding: 0; + border-radius: var(--radius-md); + background: transparent; + border: none; + color: var(--text-muted); + cursor: pointer; + transition: all var(--transition-fast); + flex-shrink: 0; +} + +.sidebar-playlists-toggle:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.sidebar-playlists-toggle.expanded { + transform: rotate(90deg); + color: var(--accent); +} + +.sidebar-playlists-section { + display: flex; + flex-direction: column; +} + +.sidebar-playlists-header { + display: flex; + align-items: center; + gap: var(--space-3); + width: 100%; + padding: var(--space-2) var(--space-3); + border-radius: var(--radius-md); + color: var(--text-secondary); + font-size: 14px; + font-weight: 500; + transition: background var(--transition-fast), color var(--transition-fast); + cursor: pointer; + background: transparent; + border: none; + text-align: left; +} + +.sidebar-playlists-header:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.sidebar-playlists-header.active { + background: var(--accent-dim); + color: var(--accent); +} + +.sidebar-playlists-chevron { + margin-left: auto; + transition: transform 0.2s ease; + flex-shrink: 0; +} + +.sidebar-playlists-chevron.expanded { + transform: rotate(90deg); +} + +.sidebar-playlists-list { + display: flex; + flex-direction: column; + padding-left: var(--space-4); + padding-top: var(--space-1); + padding-bottom: var(--space-1); + gap: var(--space-1); +} + +.sidebar-playlist-item { + display: flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-1) var(--space-2); + border-radius: var(--radius-md); + color: var(--text-secondary); + font-size: 13px; + font-weight: 400; + transition: background var(--transition-fast), color var(--transition-fast); + overflow: hidden; +} + +.sidebar-playlist-item span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; + min-width: 0; +} + +.sidebar-playlist-item:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.sidebar-playlist-item.active { + background: var(--accent-dim); + color: var(--accent); +} + +.sidebar-playlists-loading { + display: flex; + align-items: center; + justify-content: center; + padding: var(--space-2); +} + +.sidebar-playlists-empty { + padding: var(--space-2); + font-size: 12px; + color: var(--text-muted); + font-style: italic; +} + diff --git a/src/styles/layout/sidebar-offline-download-queue.css b/src/styles/layout/sidebar-offline-download-queue.css new file mode 100644 index 00000000..be7129af --- /dev/null +++ b/src/styles/layout/sidebar-offline-download-queue.css @@ -0,0 +1,42 @@ +/* ─── Sidebar offline download queue ─── */ +.sidebar-offline-queue { + margin: 4px var(--space-1) 0; + padding: 6px 10px; + border-radius: var(--radius-md); + background: var(--accent-dim); + border: 1px solid color-mix(in srgb, var(--accent) 25%, transparent); + display: flex; + align-items: center; + gap: 7px; + font-size: 11px; + color: var(--accent); + overflow: hidden; +} + +.sidebar-offline-queue--collapsed { + justify-content: center; + padding: 6px; +} + +.sidebar-offline-cancel { + margin-left: auto; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + border-radius: 50%; + border: none; + background: transparent; + color: var(--accent); + cursor: pointer; + opacity: 0.6; + padding: 0; + transition: opacity 0.15s, background 0.15s; +} +.sidebar-offline-cancel:hover { + opacity: 1; + background: color-mix(in srgb, var(--accent) 20%, transparent); +} + diff --git a/src/styles/layout/sidebar.css b/src/styles/layout/sidebar.css new file mode 100644 index 00000000..fec6e23b --- /dev/null +++ b/src/styles/layout/sidebar.css @@ -0,0 +1,444 @@ +/* ─── Sidebar ─── */ +.sidebar { + grid-area: sidebar; + display: flex; + flex-direction: column; + background: var(--bg-sidebar); + border-right: 1px solid var(--border-subtle); + position: relative; + z-index: 2; + min-height: 0; /* allow 1fr row to shrink freely */ +} + +.sidebar-brand { + display: flex; + align-items: center; + gap: var(--space-3); + padding: 0 var(--space-5); + height: 64px; + box-sizing: border-box; + border-bottom: 1px solid var(--border-subtle); +} + +.sidebar-brand .logo-icon { + width: 32px; + height: 32px; + flex-shrink: 0; +} + +.sidebar-brand .brand-name { + font-family: var(--font-display); + font-size: 20px; + font-weight: 700; + letter-spacing: -0.02em; + background: linear-gradient(135deg, var(--accent), var(--ctp-blue)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.sidebar-nav { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.sidebar-nav-scroll.overlay-scroll { + flex: 1; + min-height: 0; +} + +.sidebar-nav-scroll .overlay-scroll__viewport.sidebar-nav-viewport { + padding: var(--space-1) var(--space-3) var(--space-4); + display: flex; + flex-direction: column; + gap: var(--space-1); +} + +/* Library scope: Navidrome-style dropdown trigger + fixed panel (portal) */ +.nav-library-scope-trigger { + display: flex; + align-items: flex-start; + gap: var(--space-2); + width: calc(100% - 2 * var(--space-3)); + margin: var(--space-3) var(--space-3) var(--space-2); + padding: var(--space-2) var(--space-2); + border: 1px solid var(--border-subtle, rgba(255, 255, 255, 0.08)); + border-radius: var(--radius-md); + background: var(--bg-tertiary, rgba(0, 0, 0, 0.2)); + color: var(--text-secondary); + cursor: pointer; + text-align: left; + transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast); +} + +.nav-library-scope-trigger:hover { + background: var(--bg-hover); + color: var(--text-primary); + border-color: var(--border-default, rgba(255, 255, 255, 0.12)); +} + +/* «Все библиотеки»: как секция навигации, без рамки и подложки */ +.nav-library-scope-trigger--plain { + border-color: transparent; + background: transparent; + align-items: center; +} + +.nav-library-scope-trigger--plain:hover { + border-color: transparent; +} + +.nav-library-scope-trigger--plain .nav-library-scope-chevron { + margin-top: 0; +} + +.nav-library-scope-trigger--open:not(.nav-library-scope-trigger--plain) { + border-color: var(--accent, var(--border-default)); +} + +.nav-library-scope-trigger--open.nav-library-scope-trigger--plain { + border-color: var(--border-subtle, rgba(255, 255, 255, 0.08)); + background: var(--bg-tertiary, rgba(0, 0, 0, 0.2)); +} + +.nav-library-scope-icon { + flex-shrink: 0; + margin-top: 2px; + opacity: 0.85; +} + +.nav-library-scope-text { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; +} + +.nav-library-scope-title { + font-size: 10px; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--text-muted); +} + +.nav-library-scope-subtitle { + font-size: 12px; + font-weight: 500; + color: var(--text-primary); + line-height: 1.25; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.nav-library-scope-chevron { + flex-shrink: 0; + margin-top: 2px; + opacity: 0.7; + transition: transform var(--transition-fast); +} + +.nav-library-scope-trigger--open .nav-library-scope-chevron { + transform: rotate(180deg); +} + +.nav-library-dropdown-panel { + z-index: 10050; + display: flex; + flex-direction: column; + padding: var(--space-1); + border-radius: var(--radius-md); + border: 1px solid var(--border-dropdown, rgba(255, 255, 255, 0.12)); + background: var(--bg-card); + box-shadow: 0 8px 24px var(--shadow-dropdown, rgba(0, 0, 0, 0.45)); + box-sizing: border-box; + overflow-y: visible; +} + +/* >10 папок: «Все библиотеки» + до 10 строк папок, дальше — прокрутка */ +.nav-library-dropdown-panel--many-libraries { + --nav-lib-dropdown-row-h: calc(2 * var(--space-2) + 1.35rem); + max-height: min( + calc(11 * var(--nav-lib-dropdown-row-h) + 2 * var(--space-1)), + 70vh + ); + overflow-y: auto; +} + +.nav-library-dropdown-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-3); + width: 100%; + margin: 0; + padding: var(--space-2) var(--space-3); + border: none; + border-radius: var(--radius-sm); + background: transparent; + color: var(--text-primary); + font-size: 13px; + font-weight: 500; + text-align: left; + cursor: pointer; + transition: background var(--transition-fast); +} + +.nav-library-dropdown-item:hover { + background: var(--bg-hover); +} + +.nav-library-dropdown-item--selected { + color: var(--accent); +} + +.nav-library-dropdown-item-label { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.nav-library-dropdown-check { + flex-shrink: 0; + color: var(--accent); +} + +.nav-library-dropdown-check-spacer { + flex-shrink: 0; + width: 16px; + height: 16px; +} + +.nav-section-label { + font-size: 10px; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--text-muted); + padding: var(--space-4) var(--space-3) var(--space-2); +} + +/* Long-press reorder targets (library / system blocks in Sidebar) */ +.sidebar-nav-dnd-row { + position: relative; + border-radius: var(--radius-md); +} + +.sidebar-nav-dnd-row--dragging { + opacity: 0.45; +} + +.sidebar-nav-dnd-row--drop-before::before, +.sidebar-nav-dnd-row--drop-after::after { + content: ''; + position: absolute; + left: 6px; + right: 6px; + height: 2px; + background: var(--accent); + border-radius: 1px; + pointer-events: none; + z-index: 3; +} + +.sidebar-nav-dnd-row--drop-before::before { + top: 0; +} + +.sidebar-nav-dnd-row--drop-after::after { + bottom: 0; +} + +/* Drag-out-of-sidebar: hide item — trash follows pointer */ +.sidebar-nav-dnd-trash-hint { + display: flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + border-radius: 50%; + pointer-events: none; + z-index: 100002; + color: var(--danger, #e05555); + background: color-mix(in srgb, var(--danger, #e05555) 18%, var(--bg-card, #1e1e2e)); + border: 1px solid color-mix(in srgb, var(--danger, #e05555) 45%, transparent); + box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45); +} + +.nav-link { + display: flex; + align-items: center; + gap: var(--space-3); + padding: var(--space-3) var(--space-3); + border-radius: var(--radius-md); + color: var(--text-secondary); + font-size: 14px; + font-weight: 500; + transition: all var(--transition-fast); + cursor: pointer; + text-decoration: none; + position: relative; +} + +.nav-link:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.nav-link.active { + background: var(--accent-dim); + color: var(--accent); +} + +.nav-link.active::before { + content: ''; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 3px; + height: 60%; + background: var(--accent); + border-radius: 0 var(--radius-full) var(--radius-full) 0; +} + +.nav-link svg { + flex-shrink: 0; + opacity: 0.7; + transition: opacity var(--transition-fast); +} + +.nav-link.active svg, +.nav-link:hover svg { + opacity: 1; +} + +/* Now Playing nav link — subtly accented but not permanently "active"-looking */ +.nav-link-nowplaying { + color: var(--accent); + font-weight: 600; +} +.nav-link-nowplaying:hover { + background: var(--bg-hover); + color: var(--accent); +} +.nav-link-nowplaying.active { + background: var(--accent-dim); + color: var(--accent); +} +.nav-link-nowplaying svg { + opacity: 1; +} + +.nav-np-icon-wrap { + position: relative; + display: flex; + align-items: center; + flex-shrink: 0; +} +.nav-np-dot { + position: absolute; + top: -3px; + right: -4px; + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--accent); + animation: np-dot-pulse 1.8s ease-in-out infinite; +} +@keyframes np-dot-pulse { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.4; transform: scale(0.7); } +} + +.sidebar-nav-unread-badge { + margin-left: auto; + min-width: 18px; + height: 18px; + padding: 0 5px; + border-radius: 999px; + background: var(--accent); + color: var(--ctp-crust, #11111b); + font-size: 10px; + font-weight: 700; + line-height: 18px; + text-align: center; + box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 38%, transparent); +} + +/* Collapsed Sidebar Styles */ +.sidebar.collapsed .sidebar-brand { + justify-content: center; + padding: 0 var(--space-3); +} + +.sidebar.collapsed .nav-link { + justify-content: center; + padding: var(--space-3) 0; +} + +.sidebar.collapsed .sidebar-nav-unread-badge { + position: absolute; + top: 3px; + right: 7px; + margin-left: 0; + min-width: 16px; + height: 16px; + padding: 0 4px; + line-height: 16px; + font-size: 9px; +} + +.collapse-btn { + position: absolute; + top: calc(50% - var(--player-height) / 2); + left: calc(var(--sidebar-width) - 11px); + transform: translateY(-50%); + z-index: 101; + width: 22px; + height: 22px; + border-radius: 50%; + border: 1px solid var(--border-subtle); + background: var(--bg-card); + color: var(--text-muted); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease; + box-shadow: var(--shadow-sm); +} + +.collapse-btn:hover { + background: var(--bg-hover); + color: var(--text-primary); + box-shadow: var(--shadow-md); +} + +.queue-toggle-btn { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: var(--radius-md); + border: none; + background: transparent; + color: var(--accent); + cursor: pointer; + transition: background 0.15s ease, color 0.15s ease; + flex-shrink: 0; +} + +.queue-toggle-btn:hover { + background: var(--accent-dim); + color: var(--accent); +} + diff --git a/src/styles/layout/track-metadata.css b/src/styles/layout/track-metadata.css new file mode 100644 index 00000000..26719c9f --- /dev/null +++ b/src/styles/layout/track-metadata.css @@ -0,0 +1,57 @@ +/* ─── Track Metadata ─── */ +.mp-meta { + display: flex; + align-items: center; + gap: var(--space-3); + padding: 8px 0 4px; + flex-shrink: 0; +} + +.mp-meta-text { + flex: 1; + min-width: 0; +} + +.mp-title { + font-size: 22px; + font-weight: 700; + color: var(--text-primary); + line-height: 1.3; +} + +.mp-artist { + font-size: 15px; + color: var(--accent); + margin-top: 2px; +} + +.mp-track-info { + font-size: 12px; + color: var(--text-muted); + margin-top: 4px; + letter-spacing: 0.01em; +} + +.mp-heart { + display: flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + border-radius: 50%; + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + flex-shrink: 0; + transition: color var(--transition-fast), transform var(--transition-fast); +} + +.mp-heart:hover { + transform: scale(1.12); +} + +.mp-heart.active { + color: var(--color-star-active, var(--accent)); +} + diff --git a/src/styles/layout/transport-controls.css b/src/styles/layout/transport-controls.css new file mode 100644 index 00000000..eac11fb7 --- /dev/null +++ b/src/styles/layout/transport-controls.css @@ -0,0 +1,50 @@ +/* ─── Transport Controls ─── */ +.mp-controls { + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-4); + padding: 8px 0 4px; + flex-shrink: 0; +} + +.mp-ctrl-btn { + display: flex; + align-items: center; + justify-content: center; + width: 52px; + height: 52px; + border-radius: 50%; + background: none; + border: none; + color: var(--text-secondary); + cursor: pointer; + transition: color var(--transition-fast), transform var(--transition-fast); +} + +.mp-ctrl-btn:hover { + color: var(--text-primary); + transform: scale(1.08); +} + +.mp-ctrl-sm { + width: 44px; + height: 44px; + color: var(--text-muted); +} + +.mp-ctrl-play { + width: 64px; + height: 64px; + background: var(--accent); + color: var(--ctp-crust); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); +} + +.mp-ctrl-play:hover { + color: var(--ctp-crust); + filter: brightness(1.1); + transform: scale(1.06); + box-shadow: 0 6px 28px rgba(0, 0, 0, 0.4), var(--shadow-glow); +} + diff --git a/src/styles/layout/update-toast.css b/src/styles/layout/update-toast.css new file mode 100644 index 00000000..e66403ac --- /dev/null +++ b/src/styles/layout/update-toast.css @@ -0,0 +1,394 @@ +/* ─── Update Toast ─── */ +@keyframes update-toast-in { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: translateY(0); } +} + +/* ── AppUpdater floating toast ── */ +.app-updater-toast { + position: fixed; + bottom: calc(var(--player-height, 80px) + 12px); + left: 12px; + width: 210px; + padding: var(--space-3); + border-radius: var(--radius-md); + background: var(--bg-sidebar); + border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent); + box-shadow: 0 4px 20px rgba(0,0,0,0.35); + display: flex; + flex-direction: column; + gap: 4px; + z-index: 9000; + animation: update-toast-in 0.35s ease both; +} +.app-updater-header { + display: flex; + align-items: center; + gap: var(--space-2); + color: var(--accent); +} +.app-updater-label { + font-size: 11px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + flex: 1; +} +.app-updater-dismiss { + background: none; + border: none; + cursor: pointer; + color: var(--text-muted); + padding: 0; + display: flex; + align-items: center; + opacity: 0.6; + transition: opacity var(--transition-fast); +} +.app-updater-dismiss:hover { opacity: 1; } +.app-updater-version { + font-size: 12px; + color: var(--text-secondary); + padding-left: 19px; +} +.app-updater-actions { + padding-left: 19px; + margin-top: 2px; + display: flex; + flex-direction: column; + gap: 4px; +} +.app-updater-hint { + font-size: 10px; + color: var(--text-muted); + font-style: italic; + margin: 0 0 2px; +} +.app-updater-error { + font-size: 10px; + color: var(--danger); + background: color-mix(in srgb, var(--danger) 12%, transparent); + border-radius: 4px; + padding: 4px 6px; + margin-bottom: 4px; + word-break: break-all; +} +.app-updater-btn-primary { + display: inline-flex; + align-items: center; + gap: 5px; + font-size: 11px; + font-weight: 600; + color: var(--accent); + background: none; + border: none; + cursor: pointer; + padding: 0; + opacity: 0.85; + transition: opacity var(--transition-fast); +} +.app-updater-btn-primary:hover { opacity: 1; } +.app-updater-btn-secondary { + display: inline-flex; + align-items: center; + gap: 5px; + font-size: 11px; + font-weight: 400; + color: var(--text-secondary); + background: none; + border: none; + cursor: pointer; + padding: 0; + opacity: 0.75; + transition: opacity var(--transition-fast); +} +.app-updater-btn-secondary:hover { opacity: 1; color: var(--text-primary); } +.app-updater-progress-wrap { + display: flex; + align-items: center; + gap: var(--space-2); + padding-left: 19px; + margin-top: 2px; +} +.app-updater-progress-bar { + flex: 1; + height: 3px; + background: var(--bg-glass); + border-radius: 2px; + overflow: hidden; +} +.app-updater-progress-fill { + height: 100%; + background: var(--accent); + border-radius: 2px; + transition: width 0.15s ease; +} +.app-updater-pct { + font-size: 10px; + color: var(--text-muted); + min-width: 26px; + text-align: right; +} +.app-updater-status { + font-size: 11px; + color: var(--text-secondary); + padding-left: 19px; + font-style: italic; +} + +/* ── Update Modal ────────────────────────────────────────────────────────── */ +.update-modal { + width: min(520px, 94vw); + gap: 0; + padding: 0; + max-height: 85vh; + display: flex; + flex-direction: column; +} +.update-modal-header { + padding: 14px 16px 14px 18px; +} +.update-modal-versions { + font-size: 12px; + color: var(--text-muted); + display: block; + margin-top: 2px; +} +/* Scrollable middle section (changelog + download area combined) */ +.update-modal-body { + flex: 1; + overflow-y: auto; + min-height: 0; +} +/* Changelog accordion */ +.update-modal-changelog { + border-top: 1px solid var(--border-subtle); + border-bottom: 1px solid var(--border-subtle); +} +.update-modal-changelog-toggle { + width: 100%; + display: flex; + align-items: center; + gap: var(--space-2); + padding: 9px 18px; + background: none; + border: none; + cursor: pointer; + color: var(--text-secondary); + font-size: 12px; + font-weight: 600; + text-align: left; + letter-spacing: 0.03em; + transition: color var(--transition-fast), background var(--transition-fast); +} +.update-modal-changelog-toggle:hover { + color: var(--text-primary); + background: var(--bg-glass); +} +.update-modal-changelog-body { + padding: 4px 18px 12px; +} +/* Download area */ +.update-modal-download-area { + padding: 14px 18px; +} +.update-modal-asset { + display: flex; + align-items: center; + gap: var(--space-2); + background: var(--bg-glass); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-md); + padding: 8px 12px; +} +.update-modal-asset-name { + font-size: 12px; + color: var(--text-secondary); + flex: 1; + word-break: break-all; +} +.update-modal-asset-size { + font-size: 11px; + color: var(--text-muted); + flex-shrink: 0; +} +.update-modal-progress { + display: flex; + align-items: center; + gap: var(--space-2); +} +.update-modal-progress .app-updater-progress-bar { flex: 1; height: 4px; } +.update-modal-dl-bytes { + font-size: 11px; + color: var(--text-muted); + white-space: nowrap; +} +.update-modal-done { + display: flex; + flex-direction: column; + gap: var(--space-2); +} +.update-modal-done-title { + font-size: 13px; + font-weight: 600; + color: var(--accent); +} +.update-modal-done-hint { + font-size: 12px; + color: var(--text-secondary); +} +.update-modal-folder-btn { + align-self: flex-start; + display: inline-flex; + align-items: center; + gap: var(--space-2); + font-size: 12px; +} +/* macOS Tauri Updater — idle state info block */ +.update-modal-mac-info { + display: flex; + flex-direction: column; + gap: var(--space-2); +} +.update-modal-mac-info-main { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); +} +.update-modal-mac-info-sub { + font-size: 12px; + color: var(--text-secondary); + line-height: 1.45; +} +.update-modal-trust-badges { + display: flex; + flex-wrap: wrap; + gap: var(--space-2); + margin-top: var(--space-1); +} +.update-modal-trust-badge { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 11px; + color: var(--positive, var(--accent)); + background: var(--bg-glass); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); + padding: 3px 8px; +} +.update-modal-trust-badge svg { + flex-shrink: 0; +} +/* macOS Tauri Updater — done state (after install, before/during restart) */ +.update-modal-done-icon { + align-self: center; + color: var(--positive, var(--accent)); + margin-bottom: var(--space-1); +} +.update-modal-done-countdown { + font-size: 12px; + color: var(--text-secondary); + font-variant-numeric: tabular-nums; +} +/* AUR hint */ +.update-modal-aur { + display: flex; + flex-direction: column; + gap: var(--space-2); +} +.update-modal-aur-title { + font-size: 13px; + color: var(--text-secondary); +} +.update-modal-aur-cmd { + display: block; + background: var(--bg-glass); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); + padding: 6px 10px; + font-size: 12px; + color: var(--accent); + font-family: monospace; + user-select: all; +} +.update-modal-aur-alt { + color: var(--text-muted); +} +.update-modal-asset-none { + padding: 4px 0; +} +/* Footer */ +.update-modal-footer { + display: flex; + align-items: center; + gap: var(--space-2); + padding: 12px 18px; + border-top: 1px solid var(--border-subtle); + flex-shrink: 0; +} +.update-modal-skip { + font-size: 12px; + color: var(--text-muted); + padding: var(--space-1) var(--space-2); +} +.update-modal-skip:hover { color: var(--text-secondary); } + +.update-toast { + margin: 0 var(--space-1) var(--space-2); + padding: var(--space-3) var(--space-3); + border-radius: var(--radius-md); + background: var(--accent-dim); + border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent); + display: flex; + flex-direction: column; + gap: 3px; + animation: update-toast-in 0.35s ease both; +} + +.update-toast-header { + display: flex; + align-items: center; + gap: var(--space-2); + color: var(--accent); +} + +.update-toast-label { + font-size: 11px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--accent); +} + +.update-toast-version { + font-size: 12px; + color: var(--text-secondary); + padding-left: 22px; +} + +.update-toast-link { + font-size: 11px; + font-weight: 600; + color: var(--accent); + text-decoration: none; + padding-left: 22px; + opacity: 0.8; + transition: opacity var(--transition-fast); +} + +.update-toast-link:hover { + opacity: 1; + text-decoration: underline; +} + +.update-toast-icon { + display: flex; + align-items: center; + justify-content: center; + padding: var(--space-2) 0; + color: var(--accent); + animation: update-toast-in 0.35s ease both; + cursor: default; +} + diff --git a/src/styles/layout/utility-footer.css b/src/styles/layout/utility-footer.css new file mode 100644 index 00000000..02e81921 --- /dev/null +++ b/src/styles/layout/utility-footer.css @@ -0,0 +1,191 @@ +/* ─── Utility Footer ─── */ +.mp-footer { + display: flex; + align-items: center; + justify-content: space-evenly; + padding: 8px 0 max(12px, env(safe-area-inset-bottom, 12px)); + flex-shrink: 0; +} + +.mp-footer-btn { + display: flex; + align-items: center; + gap: 6px; + background: none; + border: none; + color: var(--text-muted); + font-size: 13px; + font-weight: 500; + cursor: pointer; + padding: 10px 18px; + border-radius: var(--radius-full); + transition: color var(--transition-fast), background var(--transition-fast); + min-height: 44px; +} + +.mp-footer-btn:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + MOBILE QUEUE / LYRICS DRAWER + ═══════════════════════════════════════════════════════════════════════════ */ + +@keyframes mq-slide-up { + from { transform: translateY(100%); } + to { transform: translateY(0); } +} + +@keyframes mq-fade-in { + from { opacity: 0; } + to { opacity: 1; } +} + +.mq-drawer-backdrop { + position: fixed; + inset: 0; + z-index: 500; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); + animation: mq-fade-in 0.2s ease both; +} + +.mq-drawer { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: 90vh; + z-index: 501; + background: var(--bg-sidebar); + border-radius: 18px 18px 0 0; + display: flex; + flex-direction: column; + overflow: hidden; + animation: mq-slide-up 0.3s cubic-bezier(0.22, 1, 0.36, 1) both; + box-shadow: 0 -8px 40px rgba(0, 0, 0, 0.4); +} + +.mq-drawer-header { + display: flex; + align-items: center; + gap: var(--space-3); + padding: 16px 20px 12px; + border-bottom: 1px solid var(--border-subtle); + flex-shrink: 0; +} + +.mq-drawer-header h3 { + font-size: 17px; + font-weight: 700; + color: var(--text-primary); + margin: 0; +} + +.mq-drawer-count { + font-size: 13px; + color: var(--accent); + flex: 1; +} + +.mq-drawer-close { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border-radius: 50%; + background: var(--bg-hover); + border: none; + color: var(--text-muted); + cursor: pointer; + flex-shrink: 0; + transition: background var(--transition-fast), color var(--transition-fast); +} + +.mq-drawer-close:hover { + background: var(--border); + color: var(--text-primary); +} + +.mq-drawer-list { + flex: 1; + overflow-y: auto; + padding: var(--space-2); +} + +.mq-drawer-empty { + padding: var(--space-6); + text-align: center; + color: var(--text-muted); + font-size: 14px; +} + +/* Lyrics drawer — fill entire drawer */ +.mq-drawer-lyrics .mq-drawer-list { + padding: 0; +} + +/* ── Queue Item ── */ +.mq-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 14px; + border-radius: var(--radius-md); + cursor: pointer; + color: var(--text-secondary); + transition: background var(--transition-fast); + min-height: 44px; +} + +.mq-item:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.mq-item.active { + background: var(--accent-dim); + color: var(--accent); +} + +.mq-item-info { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; +} + +.mq-item-title { + font-size: 13px; + font-weight: 500; + color: inherit; + display: flex; + align-items: center; + gap: 6px; +} + +.mq-item-artist { + font-size: 11px; + color: var(--text-muted); +} + +.mq-item.active .mq-item-artist { + color: inherit; + opacity: 0.7; +} + +.mq-item-dur { + font-size: 11px; + color: var(--text-muted); + flex-shrink: 0; + margin-left: var(--space-3); +} + +.mq-item.active .mq-item-dur { + color: inherit; + opacity: 0.7; +} +