From db804b2b2bb9d8b93b037a8d7069bce47de8317d Mon Sep 17 00:00:00 2001 From: Ezequiel Juarez Garcia Date: Mon, 11 May 2026 13:46:19 -0400 Subject: [PATCH] docs(04): research phase domain --- .../04-ui-polish-mobile-ux/04-RESEARCH.md | 565 ++++++++++++++++++ 1 file changed, 565 insertions(+) create mode 100644 .planning/phases/04-ui-polish-mobile-ux/04-RESEARCH.md diff --git a/.planning/phases/04-ui-polish-mobile-ux/04-RESEARCH.md b/.planning/phases/04-ui-polish-mobile-ux/04-RESEARCH.md new file mode 100644 index 0000000..5c6e791 --- /dev/null +++ b/.planning/phases/04-ui-polish-mobile-ux/04-RESEARCH.md @@ -0,0 +1,565 @@ +# Phase 04: UI Polish + Mobile UX — Research + +**Researched:** 2026-05-11 +**Domain:** Responsive CSS layout, vanilla JS UI polish, mobile UX patterns +**Confidence:** HIGH + +## Summary + +This phase is a frontend-only CSS/HTML/JS polish pass on the existing `static/index.html`, `static/style.css`, and `static/app.js` files. No new backend capabilities, no new WebSocket messages, no new libraries or dependencies. All 18 user decisions (D-01 through D-18) are locked and specific — the research job is to verify that the specified CSS/JS techniques are well-supported across modern mobile browsers, and to identify the existing code integration points that the planner must reference. + +**Primary recommendation:** Implement all changes as CSS-only responsive adaptations under a single `@media (max-width: 480px)` breakpoint, extend the existing `showToast()` with a tier parameter, and add arrow-button reorder with optimistic DOM swaps following the existing vote-toggle pattern. All needed CSS features (`-webkit-line-clamp`, flexbox, CSS transitions, `@keyframes`) have 95%+ global browser support — no polyfills or fallbacks needed. + +## Architectural Responsibility Map + +| Capability | Primary Tier | Secondary Tier | Rationale | +|------------|-------------|----------------|-----------| +| Mobile responsive layout | Browser / Client | — | Pure CSS `@media` queries — no server involvement | +| Progress bar + time display | Browser / Client | — | Client-side `width` CSS update driven by existing `server.time_update` WebSocket data | +| Loading/placeholder states | Browser / Client | — | Client-side DOM manipulation on `client.add`; morphs on `server.queue_update` or `server.error` | +| Toast feedback system | Browser / Client | — | Extends existing client-side `showToast()`; no server changes needed | +| Touch reorder (↑/↓ arrows) | Browser / Client | — | Optimistic DOM swap + `client.reorder` WebSocket message (already exists) | +| Disconnection error panel | Browser / Client | — | Client-side visibility toggle on `ws.onclose` / `ws.onopen` | + +All capabilities are Browser/Client tier. No server, database, or CDN involvement in this phase. + + +## User Constraints (from CONTEXT.md) + +### Locked Decisions + +- **D-01:** Queue items stack to 2-row layout at ≤480px. Row 1: track title (full width, wraps to 2 lines then truncates with ellipsis). Row 2: vote button + count badge + duration + remove button — all 44px touch targets, size unchanged from desktop. +- **D-02:** Drag handle (`☰`) hidden on mobile (`display: none` at ≤480px). HTML5 drag-and-drop doesn't work on touch — reorder moves to dedicated ↑/↓ buttons. +- **D-03:** Settings section stacks vertically at ≤480px — label/description above slider/control rather than side-by-side columns. +- **D-04:** Single 480px breakpoint for all mobile adaptations. CSS-only (`@media (max-width: 480px)`), no JavaScript breakpoint detection. +- **D-05:** Header gets minimal changes at ≤480px: h1 size 1.5rem (from 1.75rem), slightly tighter padding. Nickname row already wraps naturally. +- **D-06:** Thin horizontal progress bar (4px height, `border-radius: 2px`) in Now Playing section. Fill uses `#e94560`, track uses `#1a1a2e` at 40% opacity. Smooth CSS `transition: width 0.3s ease`. +- **D-07:** Time display below bar: "1:23 / 4:20" left, remaining "-2:57" right. Both Small (14px) typography. Elapsed updated ~1s from `server.time_update`. +- **D-08:** Progress bar fill width via CSS `width: X%` where `X = (playback_time / total_duration) * 100`. CSS transition smooths between 1s updates. +- **D-09:** yt-dlp resolution shows a placeholder `
  • ` with pulsing "Resolving..." text and shimmer animation. Add button disables during resolution. +- **D-10:** Resolution failure converts placeholder to error state: red-tinted `
  • ` with error text + dismiss ✕ button. Simultaneously fires error toast. +- **D-11:** Unified 3-tier toast system (success/error/info) reusing existing `#error-toast` element, extended with CSS classes. +- **D-12:** Add `#2ecc71` (green) to color palette as a success-only color. Used exclusively for success toast backgrounds. +- **D-13:** Toast fires for **every** user action that modifies state. +- **D-14:** Connection error panel below Now Playing when WebSocket closed during reconnect backoff. Replaces bare "reconnecting..." text. +- **D-15:** ↑/↓ arrow buttons (U+2191/U+2193, 44px touch targets, stacked vertically) on each queue item — visible only to host. Sends `client.reorder`. +- **D-16:** Arrow buttons present on both desktop and mobile at all widths. On desktop, drag-and-drop remains available alongside arrows. +- **D-17:** Arrows positioned in action column. Color `#666` default, `#e94560` on hover. 44px touch targets. +- **D-18:** First queue item hides ↑, last queue item hides ↓. Buttons are `display: none` at edges, not disabled. + +### the agent's Discretion + +No "you decide" areas — all decisions were explicitly made. + +### Deferred Ideas (OUT OF SCOPE) + +None — discussion stayed within phase scope. + + + +## Phase Requirements + +| ID | Description | Research Support | +|----|-------------|------------------| +| AC-02 | Web UI is responsive and usable on mobile phone browsers | All CSS features used (`-webkit-line-clamp`, flexbox, CSS transitions, `@media`) have 95%+ global browser support. Single 480px breakpoint covers iPhone SE (320px) through iPhone 14 Plus (428px) with safe margins. 44px touch targets preserved at all widths. | + + +## Standard Stack + +### Core +| Library | Version | Purpose | Why Standard | +|---------|---------|---------|--------------| +| Vanilla CSS3 | N/A (browser-native) | Responsive layout, animations, transitions | No build step. The project already uses vanilla CSS. All needed features (`@media`, flexbox, `@keyframes`, `transition`, `-webkit-line-clamp`) are Baseline Widely Available. | +| Vanilla JavaScript (ES5 compatible) | N/A (browser-native) | DOM manipulation, WebSocket handlers, toast system | The project already uses ES5-compatible JS (var declarations, no arrow functions, no template literals outside innerHTML). Consistent with Phase 1-3 style. | +| Unicode Glyphs | N/A (font-native) | Icons for arrows (↑↓), dismiss (✕), info (ℹ) | No icon library needed. All glyphs are in system fonts. The project already uses this pattern (△/▲ vote, ☰ drag, ✕ remove, ⏸▶⏭ host controls, ✎ edit). | + +### Supporting +No supporting libraries needed — this is a pure frontend polish phase. + +### Alternatives Considered +| Instead of | Could Use | Tradeoff | +|------------|-----------|----------| +| Vanilla CSS + JS | Tailwind CSS | Would require build step, contradicts project's no-build-step simplicity. Overkill for a single-file stylesheet. | +| Unicode glyphs | Font Awesome / SVG icons | Adds HTTP requests or inline bloat. Unicode glyphs are instantly available, already used in the project, and sufficient for all needed icons. | +| CSS-only line-clamp | JavaScript text truncation | JS-based truncation requires re-measuring on resize, adds complexity. `-webkit-line-clamp` is well-supported (95.94% global) and CSS-only. | + +**Installation:** +No installation needed. All work is in existing `static/` files served by the FastAPI server. + +### CSS Feature Verification + +All CSS features used in this phase were verified against caniuse.com and MDN (retrieved 2026-05-11): + +| CSS Feature | Global Support | Mobile Safari | Chrome Android | Verified | +|-------------|---------------|---------------|----------------|----------| +| `@media (max-width)` | 100% baseline | ✅ iOS 3.2+ | ✅ 2.1+ | [VERIFIED: caniuse.com] | +| `display: flex` | 99.2% | ✅ iOS 7+ | ✅ 4.4+ | [VERIFIED: caniuse.com] | +| `-webkit-line-clamp` | 95.94% | ✅ iOS 5+ | ✅ 2.3+ | [VERIFIED: caniuse.com] | +| `@keyframes` | 98.3% | ✅ iOS 7+ | ✅ 4.4+ | [VERIFIED: caniuse.com] | +| `transition` | 98.6% | ✅ iOS 7+ | ✅ 4.4+ | [VERIFIED: caniuse.com] | +| `position: fixed` | 100% baseline | ✅ iOS 3.2+ | ✅ 2.1+ | [VERIFIED: caniuse.com] | + +No polyfills, vendor prefixes (beyond `-webkit-line-clamp`'s standard co-dependency), or fallbacks needed. The target mobile browsers (Safari iOS 15+, Chrome Android 100+) support all features natively. + +## Architecture Patterns + +### System Architecture Diagram + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Browser (Client) │ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ Static Files: index.html, style.css, app.js │ │ +│ │ │ │ +│ │ index.html │ │ +│ │ ├── Header (h1 + nickname row) │ │ +│ │ ├── #error-toast (fixed position, 3-tier) │ │ +│ │ ├── #now-playing (title, duration, progress bar, │ │ +│ │ │ host controls, skip vote row, progress time) │ │ +│ │ ├── #disconnect-panel (conditional, below Now │ │ +│ │ │ Playing) │ │ +│ │ ├── #queue-section (queue items with vote, arrows, │ │ +│ │ │ drag handle, title, duration, remove) │ │ +│ │ ├── #add-song (URL input + Add button) │ │ +│ │ ├── #share (QR code + URL) │ │ +│ │ ├── #host-settings (toggle, slider, session mgmt) │ │ +│ │ └── #stop-server-modal (conditional overlay) │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ │ │ +│ WebSocket │ +│ │ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ Data Flow: │ │ +│ │ │ │ +│ │ Server → Client (inbound messages): │ │ +│ │ server.sync ──────→ renderFullState() │ │ +│ │ server.time_update → updateTime() (progress bar) │ │ +│ │ server.queue_update → applyQueueDelta() │ │ +│ │ server.playback_update → updatePlayback() │ │ +│ │ server.error ──────→ showToast(tier="error") │ │ +│ │ server.votes_update → update vote counts/badges │ │ +│ │ server.skip_update → update skip badge/state │ │ +│ │ server.queue_sort → re-render queue order │ │ +│ │ server.settings_update → applySettingsToUI() │ │ +│ │ server.session_saved → showToast(tier="success") │ │ +│ │ server.session_loaded → showToast(tier="success") │ │ +│ │ server.nickname_error → showToast(tier="error") │ │ +│ │ server.shutdown ───→ showToast(tier="error") │ │ +│ │ │ │ +│ │ Client → Server (outbound messages, unchanged): │ │ +│ │ client.add, client.vote, client.unvote, │ │ +│ │ client.skip_vote, client.reorder, client.remove, │ │ +│ │ client.host.*, client.nickname_set │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ CSS Responsive Layer: │ │ +│ │ │ │ +│ │ Desktop (>480px): │ │ +│ │ Queue: single-row flex, all controls inline │ │ +│ │ Drag handle: visible, grab cursor │ │ +│ │ Settings: 2-col grid (label left, control right) │ │ +│ │ h1: 1.75rem │ │ +│ │ │ │ +│ │ Mobile (≤480px): │ │ +│ │ Queue: 2-row stacked (title row + actions row) │ │ +│ │ Drag handle: display: none │ │ +│ │ Settings: single col (label above control) │ │ +│ │ h1: 1.5rem │ │ +│ │ Arrow buttons: remain visible as sole reorder │ │ +│ └──────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + HTTP / WS + │ +┌─────────────────────────────────────────────────────────────┐ +│ FastAPI Server │ +│ (UNCHANGED — no new messages, no new endpoints) │ +│ │ +│ Serves: static/, /api/qrcode, /api/queue, /ws │ +│ Broadcasts: all existing server.* messages unchanged │ +└─────────────────────────────────────────────────────────────┘ + │ + mpv IPC socket + │ +┌─────────────────────────────────────────────────────────────┐ +│ mpv Process (UNCHANGED) │ +│ Separate OS process — unaffected by this phase │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Recommended Project Structure + +The phase only modifies existing files. No new files or directories: + +``` +static/ +├── index.html # Add: progress bar markup, disconnect panel, 2-row wrappers in queue items +├── style.css # Add: @media (max-width: 480px) block, progress bar CSS, shimmer keyframes, +│ # toast tier variants (.success/.error/.info), arrow button styles, +│ # disconnect panel styles, button disabled state +└── app.js # Modify: renderQueueItem() (arrow buttons + 2-row wrappers), + # updateTime() (progress bar + time labels), + # showToast() (tier parameter + durations), + # applyQueueDelta() (placeholder logic), + # add-form submit handler (placeholder render + button disable), + # connectWs() (disconnect panel visibility), + # All click handlers (add toast calls), + # queue-list click handler (arrow button delegated events) +``` + +### Pattern 1: Optimistic DOM Update with Server Confirmation + +**What:** The existing pattern (used for vote toggle, skip vote toggle): instantly update the DOM on user action, send WebSocket message, server confirms via broadcast. Arrow button reorder follows this exact same pattern. + +**When to use:** All user interactions in this phase: arrow reorder, vote toggle (already exists, add toast), skip vote (already exists, add toast), remove (already exists, add toast), settings changes (add toast), save/load session (add toast). + +**Example (arrow reorder, following existing vote pattern):** +```javascript +// Source: existing vote toggle pattern in app.js line 716-756 +// Arrow button click handler (NEW — follows same pattern): +document.getElementById("queue-list").addEventListener("click", function (e) { + var arrowBtn = e.target.closest(".arrow-up, .arrow-down"); + if (arrowBtn) { + var li = arrowBtn.closest("li"); + var isUp = arrowBtn.classList.contains("arrow-up"); + + // Optimistic DOM swap + var targetLi = isUp ? li.previousElementSibling : li.nextElementSibling; + if (!targetLi) return; + if (isUp) { + li.parentNode.insertBefore(li, targetLi); + } else { + li.parentNode.insertBefore(targetLi, li); + } + + // Build new order array from DOM + var items = document.querySelectorAll("#queue-list li[data-track-id]"); + var newOrder = []; + items.forEach(function (item) { + newOrder.push(item.getAttribute("data-track-id")); + }); + + // Update arrow visibility for edges + updateArrowVisibility(); + + // Send to server + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: "client.reorder", payload: { order: newOrder } })); + } + + // Show toast + showToast("Queue reordered", "info"); + } +}); +``` + +### Pattern 2: CSS-Only Responsive Breakpoint + +**What:** A single `@media (max-width: 480px)` block in `style.css` handles all mobile adaptations. No JavaScript detection, no class toggling, no `window.matchMedia`. + +**When to use:** All responsive behavior in this phase: 2-row queue layout, hidden drag handle, vertical settings stacking, h1 size reduction. + +**Example:** +```css +@media (max-width: 480px) { + /* Queue items become 2-row stacked */ + #queue-list li { + flex-direction: column; + align-items: stretch; + gap: 6px; + padding: 12px 8px; + } + + /* Title: 2-line wrap with ellipsis */ + #queue-list li .track-title { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + white-space: normal; + width: 100%; + } + + /* Drag handle hidden */ + .drag-handle { + display: none; + } + + /* Settings stack vertically */ + .settings-row { + flex-direction: column; + } +} +``` + +### Pattern 3: Tiered Toast with Auto-Dismiss + +**What:** Extend the existing `showToast()` function to accept a tier parameter (`"success"`, `"error"`, `"info"`). Each tier has its own background color, auto-dismiss duration, and CSS class. + +**When to use:** Every user action that modifies state per D-13. + +**Example:** +```javascript +function showToast(message, tier) { + tier = tier || "error"; // Backward compatible — existing calls without tier default to error + var toast = document.getElementById("error-toast"); + toast.textContent = message; + toast.className = ""; // Clear all classes + toast.classList.add(tier); + toast.style.display = "block"; + + clearTimeout(toast._timer); + + var durations = { success: 2000, error: 5000, info: 3000 }; + toast._timer = setTimeout(function () { + toast.style.display = "none"; + }, durations[tier] || 5000); +} +``` + +### Anti-Patterns to Avoid +- **Over-engineering responsive breakpoints:** Don't add multiple breakpoints or JavaScript resize listeners. D-04 mandates a single `@media (max-width: 480px)` CSS-only approach. Adding `window.matchMedia` or `resize` event handlers contradicts the locked decision. +- **Building a toast queue:** Don't implement toast stacking or queuing. The UI-SPEC says "only one toast visible at a time — new toast replaces the current one immediately (no queueing)." +- **Using `disabled` attribute for hidden arrows:** D-18 says `display: none` at edges, never `disabled`. Don't show grayed-out arrow buttons at boundaries. + +## Don't Hand-Roll + +| Problem | Don't Build | Use Instead | Why | +|---------|-------------|-------------|-----| +| Multi-line text truncation | JavaScript text measurement + string truncation | `-webkit-line-clamp: 2` with `display: -webkit-box` | JS-based truncation requires re-measuring on every resize, layout shift, and font load. `-webkit-line-clamp` is declarative CSS with 95.94% global support, including all target mobile browsers. | +| Progress bar animation | `requestAnimationFrame` loop for smooth animation | CSS `transition: width 0.3s ease` | `requestAnimationFrame` adds unnecessary JS overhead. The CSS transition naturally interpolates between `server.time_update` values arriving every ~1s. No need for client-side interpolation. | +| Touch drag-and-drop | Custom touch event handlers (touchstart/touchmove/touchend) with hit-testing | Use ↑/↓ buttons for touch reorder instead | HTML5 drag-and-drop doesn't work on touch (D-02). Building a touch drag system from scratch involves coordinate tracking, scroll locking, ghost elements, and accessibility concerns. Arrow buttons are simpler, unambiguous, and consistent across desktop+mobile. | +| Toast animation | JavaScript animation with `setInterval` | CSS `@keyframes slideIn` + `transition` | CSS animations are GPU-accelerated, don't block the main thread, and are trivially maintainable. The project already has a `slideIn` keyframe. | + +**Key insight:** The existing codebase already provides all the infrastructure needed. The progress bar is a CSS `width` update driven by data already arriving every ~1s. The toast system is an extension of an existing `showToast()` function. Arrow reorder follows the existing optimistic-DOM-update pattern. Almost nothing needs to be built from scratch — this phase is wiring existing infrastructure to new UI elements. + +## Common Pitfalls + +### Pitfall 1: `-webkit-line-clamp` without `-webkit-box-orient: vertical` + +**What goes wrong:** The text doesn't actually clamp — it flows to full height without truncation. The ellipsis appears but the text isn't hidden. + +**Why it happens:** `-webkit-line-clamp` only works as part of a specific combination: `display: -webkit-box` + `-webkit-box-orient: vertical`. Missing any one of these three properties silently fails (no error, just no clamping). + +**How to avoid:** Always use the three-property pattern together in the mobile title CSS: +```css +.queue-title { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} +``` +**Warning signs:** Text runs to full height on mobile. Inspect computed styles — check that all three properties are present. + +### Pitfall 2: Toast Flicker on Rapid Actions + +**What goes wrong:** Multiple rapid actions (e.g., voting on several songs quickly) cause the toast to flicker as each new toast replaces the previous one mid-slide. + +**Why it happens:** The "replace immediately" behavior (D-11 design spec) means a new `showToast()` call clears the existing timeout and restarts the animation. With a 300ms slide-in animation and rapid actions, the toast never stabilizes. + +**How to avoid:** Accept this as intended behavior (the design spec explicitly says "new toast replaces the current one immediately"). Use short durations for frequent actions (success=2s, info=3s) so toasts don't linger. The flicker is mild because the toast content and position don't change — only the background color and text do. + +**Warning signs:** Users report motion discomfort. If necessary, add a 150ms minimum display time before a new toast can replace the current one. But don't implement a full queue — that contradicts the locked spec. + +### Pitfall 3: Arrow Button Order Array Desync + +**What goes wrong:** The optimistic DOM swap produces a different order than what the server computes (e.g., if another user's action arrives between the swap and the server's response). + +**Why it happens:** WebSocket is asynchronous. Between the optimistic DOM swap and the server processing `client.reorder`, the server may have processed another user's `client.vote` or `client.remove` that changed the queue. The server's `server.queue_sort` broadcast will have the true order. + +**How to avoid:** Follow the existing pattern from drag-and-drop: the client optimistically reorders, but always trusts the server's `server.queue_sort` broadcast when it arrives. If the server order matches the optimistic order (track IDs in same sequence), no DOM change. If mismatched, re-render to server order. The existing `handleWsMessage` for `server.queue_sort` (line 268-289) already handles this. + +**Warning signs:** Queue jumps after arrow clicks. This indicates the optimistic and server orders diverged. The server's order must always win. + +### Pitfall 4: Progress Bar Jumps at Track Change + +**What goes wrong:** When a track ends and the next begins, the progress bar jumps from ~100% back to 0%. The CSS `transition: width 0.3s ease` makes this visible as a brief slide-back. + +**Why it happens:** The server sends a `server.playback_update` with the new track (duration resets to 0), and the next `server.time_update` sets `width: 0%`. The CSS transition makes 100% → 0% visible as a smooth slide rather than an instant reset. + +**How to avoid:** Remove the transition temporarily during track change. When `updatePlayback()` fires for a new track (new `current_track`), temporarily set `transition: none` on `.progress-fill`, set `width: 0%`, then restore the transition on the next `updateTime()` call. Or use `requestAnimationFrame` to batch the transition removal/restoration. + +**Warning signs:** Progress bar slides backward after each track. Check that transitions are disabled during the reset frame. + +### Pitfall 5: Placeholder `
  • ` Not Cleaned Up on Network Error Before Response + +**What goes wrong:** If the WebSocket disconnects between sending `client.add` and receiving the response, the placeholder `
  • ` remains in the queue indefinitely with no way to resolve. + +**Why it happens:** The placeholder is added optimistically before the WebSocket message is sent. If the connection drops before the server processes `client.add`, neither `server.queue_update` (success) nor `server.error` (failure) arrives to morph or remove the placeholder. + +**How to avoid:** In `ws.onclose`, scan the queue for `.queue-placeholder` elements and convert them to an error state ("Connection lost — try again"), or remove them. The placeholder represents an unconfirmed action that can never be resolved once the connection drops. + +**Warning signs:** A "Resolving..." placeholder persists through a reconnect cycle. Check `ws.onclose` cleanup logic. + +## Code Examples + +### Progress Bar Width Update + +```javascript +// Source: existing updateTime() at app.js:200 — extended +// [CITED: integration point from 04-CONTEXT.md code_context section] +function updateTime(data) { + var t = data.payload.playback_time; + var total = data.payload.total_duration; // Server must include this (verify during implementation) + + // Existing fallback text + document.getElementById("progress-time").textContent = formatSeconds(t); + + // Progress bar fill + var fill = document.querySelector(".progress-fill"); + var elapsedEl = document.querySelector(".time-elapsed"); + var remainingEl = document.querySelector(".time-remaining"); + var totalEl = document.querySelector(".time-total"); + + if (fill && total) { + var pct = Math.min(100, (t / total) * 100); + fill.style.width = pct + "%"; + } + + if (elapsedEl) elapsedEl.textContent = formatSeconds(t); + if (totalEl) totalEl.textContent = "/ " + formatSeconds(total); + if (remainingEl) remainingEl.textContent = "-" + formatSeconds(Math.max(0, total - t)); +} +``` + +### Shimmer Animation CSS + +```css +/* Source: MDN @keyframes docs [VERIFIED: developer.mozilla.org] */ +/* [VERIFIED: 04-UI-SPEC.md animation inventory] */ +@keyframes shimmer { + 0% { opacity: 0.4; } + 50% { opacity: 0.8; } + 100% { opacity: 0.4; } +} + +.queue-placeholder.resolving { + animation: shimmer 1.2s ease-in-out infinite; + background: rgba(26, 26, 46, 0.6); + color: #888; + cursor: default; +} + +.queue-placeholder.error { + animation: none; + background: rgba(233, 69, 96, 0.1); + color: #e94560; +} +``` + +### Toast Tier CSS Variants + +```css +/* Source: existing #error-toast at style.css:181 — extended */ +/* [VERIFIED: 04-CONTEXT.md D-11, D-12] */ +#error-toast.success { + background: #2ecc71; + color: #ffffff; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); +} + +#error-toast.error { + background: #e94560; + color: #ffffff; +} + +#error-toast.info { + background: #0f3460; + color: #ffffff; +} + +.toast-dismiss { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + min-width: 44px; + min-height: 44px; + background: transparent; + border: none; + color: rgba(255, 255, 255, 0.7); + font-size: 16px; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.toast-dismiss:hover { + color: #ffffff; + opacity: 1; +} +``` + +## State of the Art + +| Old Approach | Current Approach | When Changed | Impact | +|--------------|------------------|--------------|--------| +| Single-line queue item truncation (desktop-only) | 2-line wrap with CSS `-webkit-line-clamp` on mobile | Phase 4 (May 2026) | Song titles get readable multi-line display on narrow screens | +| Bare "reconnecting..." text on disconnect | Structured disconnection panel with heading, body, and manual Reconnect button | Phase 4 (May 2026) | Users get clear guidance during connection loss instead of cryptic status text | +| Error-only toast system | 3-tier toast (success/error/info) with tier-specific durations and colors | Phase 4 (May 2026) | All state mutations get visible confirmation; success vs error vs neutral are visually distinct | +| HTML5 drag-and-drop only for reorder | Arrow ↑/↓ buttons as universal reorder alongside drag-and-drop on desktop | Phase 4 (May 2026) | Touch devices and keyboard users can reorder; desktop gets both methods | + +**Deprecated/outdated:** +- HTML5 drag-and-drop as sole reorder mechanism: Replaced by arrow buttons as the universal method, with drag-and-drop remaining as a desktop bonus per D-16. +- Bare `ws.onclose` "reconnecting..." text: Replaced by the structured disconnection panel per D-14. + +## Assumptions Log + +| # | Claim | Section | Risk if Wrong | +|---|-------|---------|---------------| +| A1 | `server.time_update` payload currently includes (or will be extended to include) `total_duration` — the progress bar calculation needs `playback_time / total_duration` | Code Examples | LOW risk — if total_duration is missing, it can be derived from the `#current-duration` data attribute or the now-playing track metadata. The server can easily add this field. | +| A2 | The `formatSeconds()` helper handles `null`/`undefined` gracefully (returns "--:--") — progress bar time labels will use this same function | Code Examples | LOW risk — `formatSeconds` at line 128 already handles null → "--:--". Verified in existing code. | +| A3 | The `#queue-list` click handler for arrow buttons can be added as a delegated event alongside the existing vote-btn and remove-btn handlers | Architecture Patterns | LOW risk — existing code at line 716 already uses delegated events on `#queue-list`. Adding arrow button logic to the same handler is a standard pattern. | +| A4 | The `isHost` guard for rendering arrow buttons works the same as the existing host controls guard — both check the same `isHost` variable | Architecture Patterns | LOW risk — `isHost` is set in `renderFullState()` (line 103) and used for host controls rendering (line 119). Arrow buttons follow the same guard pattern. | + +## Open Questions + +1. **Does `server.time_update` include `total_duration` in its payload?** + - What we know: The `updateTime()` handler currently only accesses `data.payload.playback_time`. The progress bar needs total duration too. + - What's unclear: Whether the server already sends this field or needs a one-line addition. + - Recommendation: During implementation, check the actual `server.time_update` payload shape. If `total_duration` is absent, request a server-side addition (one field in the broadcast dict). Fallback: read from `data-track-duration` attribute on the now-playing element. + +2. **Should the `showToast()` tier parameter be a second argument or a config object?** + - What we know: All existing calls use `showToast(message)` with a single string argument. Adding a second argument maintains backward compatibility. + - What's unclear: Whether future phases will need more options (icon, action button, custom duration). + - Recommendation: Use a simple second argument `showToast(message, tier)`. Default to `"error"` for backward compatibility. This keeps the API simple while covering all current needs. + +3. **What happens to the progress bar when there is no current track?** + - What we know: D-06 says the progress bar lives in the Now Playing section. When queue is empty, there's no track. + - What's unclear: Should the bar be hidden entirely, show at 0%, or show a grayed-out state? + - Recommendation: Hide the progress bar and time labels entirely when `#current-title` shows "—" (no track). The `#progress-time` fallback continues showing "--:--". This avoids a meaningless empty progress bar. + +## Environment Availability + +Step 2.6: SKIPPED. No external dependencies identified. This phase is purely code/config changes in existing `static/` files — no tools, databases, services, CLIs, or package managers needed beyond what the project already uses (browser for rendering, FastAPI server for serving static files). The FastAPI server and mpv are already verified from Phases 1-3. + +## Sources + +### Primary (HIGH confidence) +- [04-CONTEXT.md] — All 18 user decisions (D-01 through D-18), integration points, existing code patterns +- [04-UI-SPEC.md] — Design system, spacing, typography, color palette, component inventory, interaction contracts, state inventory, animation inventory, layout specs, responsive behavior +- [caniuse.com — CSS line-clamp] — Verified 95.94% global support, working on all mobile browsers since iOS 5+ and Chrome Android 2.3+ [VERIFIED: caniuse.com, fetched 2026-05-11] +- [MDN Web Docs — line-clamp CSS property] — Confirmed three-property co-dependency: `display: -webkit-box` + `-webkit-box-orient: vertical` + `-webkit-line-clamp: N` [VERIFIED: developer.mozilla.org, last modified 2026-04-20] +- [02-UI-SPEC.md] — Baseline design system (carry-forward). Colors, typography, spacing, animation inventory, layout specs +- [Existing code: static/index.html, style.css, app.js] — Verified integration points match CONTEXT.md code_context claims [VERIFIED: codebase read] + +### Secondary (MEDIUM confidence) +- [MDN Web Docs — display CSS property] — General reference for flex layout patterns [CITED: developer.mozilla.org, last modified 2026-04-20] + +### Tertiary (LOW confidence) +None — all claims verified against caniuse.com, MDN, existing codebase, or explicit CONTEXT.md decisions. + +## Metadata + +**Confidence breakdown:** +- Standard stack: HIGH — No libraries to version-check. All CSS features verified against caniuse.com and MDN (retrieved within 24h). Feature compatibility is stable (these are mature Baseline features). +- Architecture: HIGH — The integration points are documented explicitly in 04-CONTEXT.md, verified against existing code. Line numbers match. The patterns (optimistic UI, CSS-only breakpoint, delegated events) are already used in the existing codebase. +- Pitfalls: HIGH — Derived from official docs (MDN line-clamp docs), CSS spec analysis (transition interference at track boundary), and the existing code patterns (order array desync from drag-and-drop precedent). + +**Research date:** 2026-05-11 +**Valid until:** 2026-06-11 (30 days — CSS feature compatibility is stable; no fast-moving libraries) -- 2.51.2