diff --git a/.gitignore b/.gitignore index 83539cc..151872e 100755 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ node_modules/ # runtime/local artifacts server/data/ test-results/ +.impeccable/review/ .claude/worktrees/ server/public diff --git a/.impeccable/config.json b/.impeccable/config.json new file mode 100644 index 0000000..747d7b3 --- /dev/null +++ b/.impeccable/config.json @@ -0,0 +1,3 @@ +{ + "buildPath": "code" +} diff --git a/.impeccable/design.json b/.impeccable/design.json new file mode 100644 index 0000000..6c453df --- /dev/null +++ b/.impeccable/design.json @@ -0,0 +1,328 @@ +{ + "schemaVersion": 2, + "generatedAt": "2026-10-02T21:37:12.817947+00:00", + "title": "Design System: YT Player — Classic", + "extensions": { + "colorMeta": { + "accent": { + "role": "primary", + "displayName": "Accent" + }, + "accent-bright": { + "role": "primary", + "displayName": "Accent Bright" + }, + "accent-deep": { + "role": "primary", + "displayName": "Accent Deep" + }, + "bg": { + "role": "neutral", + "displayName": "Bg" + }, + "bg-1": { + "role": "neutral", + "displayName": "Bg 1" + }, + "bg-2": { + "role": "neutral", + "displayName": "Bg 2" + }, + "bg-3": { + "role": "neutral", + "displayName": "Bg 3" + }, + "line": { + "role": "neutral", + "displayName": "Line" + }, + "line-soft": { + "role": "neutral", + "displayName": "Line Soft" + }, + "text": { + "role": "neutral", + "displayName": "Text" + }, + "text-2": { + "role": "neutral", + "displayName": "Text 2" + }, + "text-dim": { + "role": "neutral", + "displayName": "Text Dim" + }, + "on-primary": { + "role": "neutral", + "displayName": "On Primary" + }, + "on-play": { + "role": "neutral", + "displayName": "On Play" + }, + "light-bg": { + "role": "neutral", + "displayName": "Light Bg" + }, + "light-bg-1": { + "role": "neutral", + "displayName": "Light Bg 1" + }, + "light-bg-2": { + "role": "neutral", + "displayName": "Light Bg 2" + }, + "light-bg-3": { + "role": "neutral", + "displayName": "Light Bg 3" + }, + "light-line": { + "role": "neutral", + "displayName": "Light Line" + }, + "light-line-soft": { + "role": "neutral", + "displayName": "Light Line Soft" + }, + "light-text": { + "role": "neutral", + "displayName": "Light Text" + }, + "light-text-2": { + "role": "neutral", + "displayName": "Light Text 2" + }, + "light-text-dim": { + "role": "neutral", + "displayName": "Light Text Dim" + }, + "light-accent": { + "role": "primary", + "displayName": "Light Accent" + }, + "light-accent-bright": { + "role": "primary", + "displayName": "Light Accent Bright" + }, + "light-accent-deep": { + "role": "primary", + "displayName": "Light Accent Deep" + }, + "contrast-bg": { + "role": "neutral", + "displayName": "Contrast Bg" + }, + "contrast-bg-2": { + "role": "neutral", + "displayName": "Contrast Bg 2" + }, + "contrast-bg-3": { + "role": "neutral", + "displayName": "Contrast Bg 3" + }, + "contrast-line": { + "role": "neutral", + "displayName": "Contrast Line" + }, + "contrast-line-soft": { + "role": "neutral", + "displayName": "Contrast Line Soft" + }, + "contrast-text-dim": { + "role": "neutral", + "displayName": "Contrast Text Dim" + }, + "contrast-accent": { + "role": "primary", + "displayName": "Contrast Accent" + }, + "contrast-accent-bright": { + "role": "primary", + "displayName": "Contrast Accent Bright" + } + }, + "typographyMeta": { + "display": { + "displayName": "Display", + "purpose": "Empty-home heading." + }, + "headline": { + "displayName": "Headline", + "purpose": "Now-playing media title." + }, + "title": { + "displayName": "Title", + "purpose": "Browsing heading." + }, + "body": { + "displayName": "Body", + "purpose": "Interface text." + }, + "label": { + "displayName": "Label", + "purpose": "Suggestion chips." + }, + "media-title": { + "displayName": "Media Title", + "purpose": "Browsing media titles." + }, + "time": { + "displayName": "Time", + "purpose": "Tabular transport timestamps." + } + }, + "shadows": [ + { + "name": "dialog-dark", + "value": "0 18px 50px -12px rgba(0, 0, 0, 0.7)", + "purpose": "Inherited dialog and sheet elevation in dark mode." + }, + { + "name": "dialog-light", + "value": "0 18px 50px -18px rgba(0, 0, 0, 0.25)", + "purpose": "Inherited dialog and sheet elevation in light mode." + }, + { + "name": "playing-edge", + "value": "inset 2px 0 var(--accent)", + "purpose": "Selected media row indicator, not ambient elevation." + } + ], + "motion": [ + { + "name": "ease", + "value": "cubic-bezier(0.22, 1, 0.36, 1)", + "purpose": "Inherited interface easing." + }, + { + "name": "color-state", + "value": "background 0.16s, color 0.16s", + "purpose": "Recurring inherited color state transition." + }, + { + "name": "reduced-duration", + "value": "0.001ms", + "purpose": "Explicit preference and operating-system reduced motion." + } + ], + "breakpoints": [ + { + "name": "stacked-max", + "value": "1200px" + }, + { + "name": "mobile-max", + "value": "860px" + }, + { + "name": "xl-navigation-max", + "value": "420px" + }, + { + "name": "narrow-thumbnail-max", + "value": "380px" + }, + { + "name": "transport-wide-container-min", + "value": "620px" + }, + { + "name": "transport-narrow-container-max", + "value": "300px" + } + ] + }, + "components": [ + { + "name": "Filled action", + "kind": "button", + "refersTo": "button-primary", + "description": "Classic list action.", + "html": "", + "css": ".ds-primary { background:var(--accent-deep,#c6442f);color:#fff;border:0;border-radius:8px;min-height:44px;padding:8px 12px;font:800 14px var(--ui,\"Hanken Grotesk\",system-ui,sans-serif);cursor:pointer;transition:background .16s,color .16s; }.ds-primary:focus-visible { outline:2px solid var(--accent,#f06b53);outline-offset:3px; }" + }, + { + "name": "Search action", + "kind": "button", + "refersTo": "button-search", + "description": "Quiet search submission, accented on hover.", + "html": "", + "css": ".ds-search { background:var(--bg-3,#2b2e33);color:var(--text,#f3f1ed);border:0;border-radius:8px;min-height:46px;padding:0 22px;font:600 14px var(--ui,\"Hanken Grotesk\",system-ui,sans-serif);cursor:pointer;transition:background .16s,color .16s; }.ds-search:hover { background:var(--accent-deep,#c6442f);color:#fff; }.ds-search:focus-visible { outline:2px solid var(--accent,#f06b53);outline-offset:3px; }" + }, + { + "name": "Play control", + "kind": "button", + "refersTo": "button-play", + "description": "Circular primary transport.", + "html": "", + "css": ".ds-play { display:grid;place-items:center;width:64px;height:64px;border:0;border-radius:50%;background:var(--accent,#f06b53);color:#171719;cursor:pointer;transition:background .16s; }.ds-play:hover { background:var(--accent-bright,#ff947e); }.ds-play:focus-visible { outline:2px solid var(--accent,#f06b53);outline-offset:3px; }" + }, + { + "name": "Search field", + "kind": "input", + "refersTo": "input-search", + "description": "Full-width media query field.", + "html": "", + "css": ".ds-input { background:var(--bg-1,#18191c);color:var(--text,#f3f1ed);border:1px solid var(--line,#42464d);border-radius:8px;min-height:46px;padding:11px 14px;font:16px var(--ui,\"Hanken Grotesk\",system-ui,sans-serif);caret-color:var(--accent,#f06b53);transition:border-color .2s,background .2s; }.ds-input::placeholder { color:var(--text-dim,#a1a5ac);opacity:1; }.ds-input:focus-visible { outline:2px solid var(--accent,#f06b53);outline-offset:3px; }" + }, + { + "name": "Suggestion chip", + "kind": "chip", + "refersTo": "chip", + "description": "Media search suggestion.", + "html": "", + "css": ".ds-chip { min-height:44px;padding:10px 14px;background:var(--bg-2,#202226);border:1px solid var(--line-soft,#2b2e33);border-radius:8px;color:var(--text,#f3f1ed);font:500 14px var(--ui,\"Hanken Grotesk\",system-ui,sans-serif);cursor:pointer;transition:background .16s,border-color .16s; }.ds-chip:hover { border-color:var(--accent,#f06b53); }.ds-chip:focus-visible { outline:2px solid var(--accent,#f06b53);outline-offset:3px; }" + }, + { + "name": "Active navigation", + "kind": "nav", + "refersTo": "nav-active", + "description": "Sidebar selection with stroked SVG.", + "html": "", + "css": ".ds-nav { display:flex;align-items:center;gap:12px;min-height:44px;padding:10px 12px;border:0;border-radius:var(--radius-sm,8px);background:var(--bg-3,#2b2e33);color:var(--text,#f3f1ed);font:14px var(--ui,\"Hanken Grotesk\",system-ui,sans-serif);cursor:pointer;transition:color .18s,background .18s; }.ds-nav svg { fill:none;stroke:var(--accent,#f06b53);stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round; }.ds-nav:focus-visible { outline:2px solid var(--accent,#f06b53);outline-offset:3px; }" + }, + { + "name": "Playing media row", + "kind": "card", + "refersTo": "media-row", + "description": "Divider-separated selected media without a raster fixture.", + "html": "
Media title
Channel
", + "css": ".ds-row { display:grid;grid-template-columns:88px minmax(0,1fr) 44px;gap:12px;padding:12px 8px;border-bottom:1px solid var(--line-soft,#2b2e33);background:var(--bg-3,#2b2e33);box-shadow:inset 2px 0 var(--accent,#f06b53);color:var(--text,#f3f1ed);font-family:var(--ui,\"Hanken Grotesk\",system-ui,sans-serif); }.ds-thumb { width:88px;aspect-ratio:16/9;border-radius:6px;background:var(--bg-2,#202226); }.ds-title { font-size:14px;font-weight:600;line-height:1.45; }.ds-channel { font-size:12px;color:var(--text-dim,#a1a5ac);margin-top:5px; }.ds-row-menu { min-width:44px;min-height:44px;background:transparent;color:var(--text,#f3f1ed);border:0;cursor:pointer; }.ds-row-menu svg { fill:currentColor; }.ds-row:focus-visible { outline:2px solid var(--accent,#f06b53);outline-offset:3px; }.ds-row-menu:focus-visible { outline:2px solid var(--accent,#f06b53);outline-offset:3px; }" + } + ], + "narrative": { + "northStar": "Quiet Media Studio", + "overview": "Classic is a matte charcoal media workspace with warm white type and restrained vermilion. Watching and listening share the same visual priority; playback, browsing, and library controls remain legible together. This system applies only when the Classic layout is selected. Other theme families retain their own visual authority.\n\nThe implemented world uses tonal surfaces, fine dividers, bundled typography, and generous control targets. Light, high contrast, text scale, compact density, and reduced motion are supported appearance preferences. Source evidence is frontend/theme-classic.css, with inherited typography and preference values from frontend/styles.css and frontend/refresh.css; the approved direction lives in .impeccable/surfaces/frontend-theme-classic-css.md.", + "keyCharacteristics": [ + "Matte tonal layers and fine dividers.", + "Restrained vermilion for playback, actions, and selection.", + "Bricolage headings, Hanken interface text, JetBrains timing.", + "Responsive workspace with persistent access to playback." + ], + "rules": [ + { + "name": "The Classic Scope Rule", + "body": "Apply these rules only to Classic; shared markup does not make them rules for other layout families.", + "section": "colors" + }, + { + "name": "The Tonal Surface Rule", + "body": "Use surface tone and fine dividers for resting depth; reserve the inherited ambient shadow for dialogs and sheets.", + "section": "elevation" + } + ], + "dos": [ + "Do keep Classic overrides scoped to the selected Classic layout.", + "Do use tonal surfaces and fine dividers for workspace hierarchy.", + "Do preserve light, high contrast, text scale, density, safe areas, and reduced motion.", + "Do use inline SVG for navigation icons and retain visible keyboard focus.", + "Do use media thumbnails as user content." + ], + "donts": [ + "Don't add ambient glow or grain to Classic.", + "Don't force empty home into the video aspect ratio.", + "Don't hide row actions until hover or reduce established control targets to fit a crowded layout.", + "Don't transfer this visual world to other layout families." + ] + } +} diff --git a/.impeccable/surfaces/frontend-theme-classic-css.md b/.impeccable/surfaces/frontend-theme-classic-css.md new file mode 100644 index 0000000..3741bcf --- /dev/null +++ b/.impeccable/surfaces/frontend-theme-classic-css.md @@ -0,0 +1,26 @@ +--- +version: 1 +slug: "frontend-theme-classic-css" +primary_target: "frontend/theme-classic.css" +related_targets: ["frontend/index.html","frontend/app.js"] +--- + +# Classic surface + +Mode: Operate. Scope: Classic in the shared web frontend, mobile browser/PWA and desktop. Preserve features and all other layout themes. User approved code-first implementation of the full redesign plan. + +## Direction contract + +THESIS: A quiet media workspace gives watching and listening equal attention. Empty home is useful content, never content trapped in a video frame. + +OWN-WORLD: Matte charcoal, warm white type, restrained vermilion, 12px corners, fine dividers. Bundled Hanken UI, Bricolage headings, JetBrains timing. No ambient glow or grain. Respect light and high-contrast preferences. + +STORY: Search or choose a playlist, play, then browse without losing playback. Existing features and factual copy survive. + +FIRST VIEWPORT: Desktop has a 232px library sidebar, flexible video/workspace and 340–420px browsing pane under search. Phones have search above content, centered transport under video, and existing navigation at the bottom. Empty home replaces the video proportions with a fully visible introduction or playlist collection. + +FORM: User-pinned quiet studio workspace; concept seed 6fcc153a was run, but its assignment and catalog alternatives do not override the explicitly approved plan. Signature interaction: scrolling from playback into browsing reveals the existing mini-player above navigation; selecting it returns to playback without moving the document viewport. + +FINISH: unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance + +Constraints: no generated artwork needed; existing media thumbnails are user content. Existing untracked mockups remain untouched. No deployment. diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..ee790d2 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,243 @@ +--- +name: "YT Player — Classic" +description: "Quiet matte studio for watching and listening; applies only to the Classic layout." +colors: + accent: "#f06b53" + accent-bright: "#ff947e" + accent-deep: "#c6442f" + bg: "#121315" + bg-1: "#18191c" + bg-2: "#202226" + bg-3: "#2b2e33" + line: "#42464d" + line-soft: "#2b2e33" + text: "#f3f1ed" + text-2: "#c3c4c7" + text-dim: "#a1a5ac" + on-primary: "#ffffff" + on-play: "#171719" + light-bg: "#f4f2ee" + light-bg-1: "#ffffff" + light-bg-2: "#efece6" + light-bg-3: "#e5e0d8" + light-line: "#d8d2c8" + light-line-soft: "#e6e1d9" + light-text: "#1b1a18" + light-text-2: "#4a4843" + light-text-dim: "#625e59" + light-accent: "#ff4b32" + light-accent-bright: "#ff6a52" + light-accent-deep: "#b43623" + contrast-bg: "#000000" + contrast-bg-2: "#0a0a0a" + contrast-bg-3: "#161616" + contrast-line: "#ffffff" + contrast-line-soft: "#9a9a9a" + contrast-text-dim: "#d8d8d8" + contrast-accent: "#ff5a3c" + contrast-accent-bright: "#ff8a6c" +typography: + display: + fontFamily: "\"Bricolage Grotesque\", Georgia, serif" + fontSize: "clamp(36px, 4vw, 60px)" + fontWeight: 800 + lineHeight: 1.05 + letterSpacing: "-0.035em" + headline: + fontFamily: "\"Bricolage Grotesque\", Georgia, serif" + fontSize: "clamp(22px, 2vw, 30px)" + fontWeight: 700 + lineHeight: 1.25 + letterSpacing: "-0.02em" + title: + fontFamily: "\"Bricolage Grotesque\", Georgia, serif" + fontSize: "26px" + fontWeight: 700 + lineHeight: 1.2 + body: + fontFamily: "\"Hanken Grotesk\", system-ui, sans-serif" + fontSize: "14px" + lineHeight: 1.45 + label: + fontFamily: "\"Hanken Grotesk\", system-ui, sans-serif" + fontSize: "14px" + fontWeight: 500 + media-title: + fontFamily: "\"Hanken Grotesk\", system-ui, sans-serif" + fontSize: "14px" + fontWeight: 600 + lineHeight: 1.45 + time: + fontFamily: "\"JetBrains Mono\", ui-monospace, monospace" + fontSize: "12px" +rounded: + flat: "0px" + thumbnail: "6px" + sm: "8px" + md: "12px" + circle: "50%" +spacing: + small: "8px" + control-gap: "14px" + compact: "16px" + standard: "24px" +components: + button-primary: + backgroundColor: "{colors.accent-deep}" + textColor: "{colors.on-primary}" + rounded: "{rounded.sm}" + padding: "8px 12px" + button-search: + backgroundColor: "{colors.bg-3}" + textColor: "{colors.text}" + rounded: "{rounded.sm}" + padding: "0 22px" + button-play: + backgroundColor: "{colors.accent}" + textColor: "{colors.on-play}" + rounded: "{rounded.circle}" + width: "64px" + height: "64px" + input-search: + backgroundColor: "{colors.bg-1}" + textColor: "{colors.text}" + rounded: "{rounded.sm}" + padding: "11px 14px" + chip: + backgroundColor: "{colors.bg-2}" + textColor: "{colors.text}" + typography: "{typography.label}" + rounded: "{rounded.sm}" + padding: "10px 14px" + nav-active: + backgroundColor: "{colors.bg-3}" + textColor: "{colors.text}" + padding: "10px 12px" + media-row: + textColor: "{colors.text}" + rounded: "{rounded.flat}" + padding: "12px 8px" + panel: + backgroundColor: "{colors.bg-1}" + rounded: "{rounded.md}" +--- + +# Design System: YT Player — Classic + +## Overview + +**Creative North Star: "Quiet Media Studio"** + +Classic is a matte charcoal media workspace with warm white type and restrained vermilion. Watching and listening share the same visual priority; playback, browsing, and library controls remain legible together. This system applies only when the Classic layout is selected. Other theme families retain their own visual authority. + +The implemented world uses tonal surfaces, fine dividers, bundled typography, and generous control targets. Light, high contrast, text scale, compact density, and reduced motion are supported appearance preferences. Source evidence is frontend/theme-classic.css, with inherited typography and preference values from frontend/styles.css and frontend/refresh.css; the approved direction lives in .impeccable/surfaces/frontend-theme-classic-css.md. + +**Key Characteristics:** +- Matte tonal layers and fine dividers. +- Restrained vermilion for playback, actions, and selection. +- Bricolage headings, Hanken interface text, JetBrains timing. +- Responsive workspace with persistent access to playback. + +## Colors + +The default palette is matte charcoal with warm white text and a restrained vermilion accent. Frontmatter values are normative; unprefixed tokens describe Classic dark mode, while light and contrast tokens record implemented preference variants. + +### Primary + +- **Vermilion:** accent marks playback, focus, active navigation icons, selection, and the playing row edge. Accent bright is the play hover and active lyric emphasis; accent deep is the filled action and brand surface. +- **Action white / play charcoal:** the two implemented foregrounds for filled actions and circular transport. + +### Neutral + +- **Studio charcoal:** bg is the workspace; bg-1 is sidebar, browsing, fields, and support panels; bg-2 is chips, secondary actions, and mini-player; bg-3 carries active navigation, tabs, and playing rows. +- **Fine dividers:** line-soft separates rows and regions; line marks stronger edges, including the mini-player boundary. +- **Warm white / secondary / muted:** text, text-2, and text-dim distinguish titles, supporting copy, and metadata. +- **Light preference:** warm paper workspace, white panels, pale neutral layers, and dark text. Classic overrides muted text and deep accent for this preference; the other light accent values are inherited. +- **High contrast preference:** black primary surfaces, white foregrounds and strong lines; bg-1 inherits the same black as bg, text and text-2 inherit white, and accent-deep shares contrast-accent. These aliases are not separate color primitives. + +**The Classic Scope Rule.** Apply these rules only to Classic; shared markup does not make them rules for other layout families. + +## Typography + +**Display Font:** bundled Bricolage Grotesque, with Georgia and serif fallbacks. +**Body Font:** bundled Hanken Grotesk, with system-ui and sans-serif fallbacks. +**Label/Mono Font:** bundled JetBrains Mono, with ui-monospace and monospace fallbacks, for timing and data. + +Headings provide character; interface text stays compact and readable. Timing uses tabular numerals. + +### Hierarchy + +- **Display:** empty-home heading; the frontmatter clamp becomes a fixed (42px) on the mobile layout. +- **Headline:** now-playing title, with wrapping permitted for long media names. +- **Title:** browsing heading; playlist home uses (28px), reducing to (26px) on mobile. +- **Body:** interface baseline; empty-home supporting copy uses (17px), (1.65) leading, and (45ch) maximum width, reducing to (16px) on mobile. +- **Label:** chips and related compact actions; navigation captions stay at 11px, including narrow screens. +- **Media title:** semibold titles in browsing rows; supporting channel metadata uses (12px). +- **Time:** transport timestamps in the mono family with tabular numbers. + +Font preferences scale the whole UI to (0.9), (1), (1.12), or (1.26). Classic compensates the application height for that zoom. + +## Layout + +Desktop uses a (232px) library sidebar, a flexible playback workspace, and a browsing pane sized with `clamp(340px, 29%, 420px)`. Search spans up to (860px). Workspace padding is the standard spacing token; compact density uses the compact token and halves row block padding from (12px) to (6px). + +At viewport widths of (1200px) and below, playback and browsing stack in one scrolling body. At (860px) and below, the workspace becomes one column with mobile navigation and safe-area-aware padding. Home playlists use a two-column grid on mobile; desktop uses automatically filled columns with a (138px) minimum. Empty home has natural height rather than video proportions. Above (1200px), the playing video width is capped at `min(100%, calc(42dvh * 16 / 9))`. + +Transport responds to its own container: at (620px) and above it uses two columns with seek and quick actions spanning both; at (300px) and below it uses five equal transport columns and (44px) controls. At (420px) and below with extra-large text, bottom navigation becomes a three-column grid. Below (380px), the thumbnail width remains (88px); navigation captions retain (11px). + +Browsing rows use an (88px) thumbnail column, flexible text, and a (44px) action column. Most actions have at least (44px) targets; search uses (46px), quick transport actions (48px), and mobile navigation at least (58px) height. The main play control uses the frontmatter size, reduces to (56px) in the wide transport container, and (44px) in the narrowest container. + +The mini-player sits above mobile navigation and below the desktop workspace. Navigation, mini-player, and section-rail heights feed a combined clearance used by content and toasts. Returning from the mini-player brings playback into view without changing the document viewport. + +## Elevation & Depth + +Resting workspace surfaces are flat. Classic disables ambient bleed, grain, blur on navigation and mini-player, hero animation, card entrance animation, and thumbnail hover zoom. The playing row uses a narrow inset accent edge rather than a glow. Dialogs, shortcut panels, and more sheets retain the inherited soft ambient shadow; dark and light shadow values are recorded in the sidecar. + +**The Tonal Surface Rule.** Use surface tone and fine dividers for resting depth; reserve the inherited ambient shadow for dialogs and sheets. + +## Shapes + +Panels and the video stage use the medium radius; fields, chips, action buttons, tabs, and navigation details use the small radius. Thumbnails use the thumbnail radius. Browsing rows have flat corners and a bottom divider. The primary transport remains circular. The section rail rounds only its upper corners; mini-player surfaces remain flat across their width. + +## Components + +### Buttons + +Quiet surface buttons use bg-2 or bg-3 with primary text. The search action changes from bg-3 to accent-deep with white text on hover. Filled list and empty-state actions use accent-deep and white. Circular play uses accent with its dark foreground and changes to accent-bright on hover. Controls keep their established target sizes. Keyboard focus uses an accent outline (2px) with (3px) offset. + +### Chips + +Suggestion chips use bg-2, primary text, a line-soft border, the small radius, and label typography. Home capability descriptors remain unboxed text rather than duplicating action-chip surfaces. + +### Cards / Containers + +Media results are divider-separated rows rather than floating cards. Actions stay visible. The playing row uses bg-3 and an inset accent line (2px). Notes, up-next, and related containers use bg-1 and the medium radius; playlist collections keep natural height with no fading overflow mask. + +### Inputs / Fields + +Search uses bg-1, primary text, the small radius, and a (46px) minimum height. Placeholder text uses text-dim at full opacity and the caret uses accent. Settings fields retain at least (44px) height. Focus receives the shared visible outline. + +### Navigation + +Sidebar entries have (44px) minimum height, restrained spacing, inline stroked SVG icons (20px), and no decorative leading active marker. Active entries use bg-3 with primary text and an accent icon. Mobile navigation uses a matte bg-1 surface, muted inactive text, and an accent icon in the active entry; safe-area padding and text-scale layout remain part of the component. + +### Playback and Mini-player + +Playback combines a video stage, centered compact transport, timing, wrapped quick actions, and metadata. The mini-player uses bg-2 with a top line, no blur or shadow, and usable (44px) targets. The section rail and toast placement share the measured bottom clearance. Classic section-rail actions use the same inline stroked SVG vocabulary as navigation; other layouts retain their existing icons. + +State transitions inherit the existing short interface timings; the sidecar records the inherited easing and recurring color transition. Explicit reduced motion and the operating-system preference reduce animation and transition durations to (0.001ms), with one animation iteration. The explicit preference also sets automatic scrolling. + +## Do's and Don'ts + +### Do: +- **Do** keep Classic overrides scoped to the selected Classic layout. +- **Do** use tonal surfaces and fine dividers for workspace hierarchy. +- **Do** preserve light, high contrast, text scale, density, safe areas, and reduced motion. +- **Do** use inline SVG for navigation icons and retain visible keyboard focus. +- **Do** use media thumbnails as user content. + +### Don't: +- **Don't** add ambient glow or grain to Classic. +- **Don't** force empty home into the video aspect ratio. +- **Don't** hide row actions until hover or reduce established control targets to fit a crowded layout. +- **Don't** transfer this visual world to other layout families. diff --git a/PRODUCT.md b/PRODUCT.md new file mode 100644 index 0000000..5a697bc --- /dev/null +++ b/PRODUCT.md @@ -0,0 +1,34 @@ +# YT Player + + + +## Platform + +web + +## Users + +People watching videos and listening to music on desktop and mobile. The confirmed Classic brief gives watching and listening equal priority. + +## Product Purpose + +Search and play YouTube media, organize playlists, and keep saved media available offline. The web PWA and desktop wrappers share the same frontend. + +## Capabilities and Constraints + +Preserve playback, playlists, queue, lyrics, notes, A–B loops, audio-only mode, offline storage, sharing, and settings. The frontend is plain HTML, CSS, and JavaScript with a Bun/Hono web backend. Classic is the default layout; other layouts share markup and must retain their behavior. Appearance settings include dark, light, contrast, font scale, density, performance, and reduced motion. + +## Brand Commitments + +Keep the YT Player name and existing factual copy. The approved Classic redesign uses a quiet dark studio direction. Other layout themes remain independent. + +## Evidence on Hand + +Existing frontend, bundled fonts, local icons, and browser tests. No invented customer claims or production data. + +## Product Principles + +- Playback and browsing must remain reachable together. +- Mobile browser and installed PWA should offer consistent Classic navigation. +- Empty, populated, and playing states each need an appropriate layout. +- User data and appearance preferences survive theme changes. diff --git a/docs/classic-redesign-verification.md b/docs/classic-redesign-verification.md new file mode 100644 index 0000000..292dd6e --- /dev/null +++ b/docs/classic-redesign-verification.md @@ -0,0 +1,22 @@ +# Classic redesign verification + +## Scope + +Classic's responsive web UI, including desktop, mobile browser and simulated installed-PWA navigation. Synthetic playlist/media metadata was used for browser checks and screenshots; no production profiles or playlists were changed. No deployment was performed. + +## Automated checks + +- `npx playwright test --config playwright.classic.config.js`: 11 passed. Covers empty home at 320/390/768/1024/1280/1440px, navigation during a populated player state, mini-player return, advanced controls, theme switching, rotation, extra-large text at 320px, and cached Classic CSS on offline reload. +- `node --test --test-isolation=none frontend/*.test.js`: 72 passed after rebasing onto the latest remote changes (65 passed before the rebase). Explicitly disabling process isolation exposes individual test results in this environment; the default isolated runner initially reported only nine file-level passes. The latest suite ran outside the sandbox because the WebAssembly fallback test spawns Node. +- Existing portrait-PWA, viewport-anchor and fullscreen smoke tests run in Chromium: 9 passed, 4 failed. The same tests against unchanged HEAD frontend assets produced the identical 9/4 result. The four baseline failures are the fullscreen fallback/audio-only expectations and the portrait rotation/wide layout checks that expect the empty-home list pane to be visible. +- `node --check frontend/app.js` and `git diff --check` passed. + +## Visual evidence + +Local browser captures live under `.impeccable/review/` (ignored build evidence): desktop/mobile player and empty home, mobile playlists and settings, desktop light appearance, and a 320px phone with extra-large text. They use labeled synthetic preview artwork. The final capture pass measured no main-pane or control-deck horizontal overflow and an app height equal to the viewport for every captured size. + +Impeccable's detector ran once against the changed UI targets. Its findings refer to the shared index and inherited styling, including other themes; the report is not proof of active Classic contrast or full accessibility conformance. The independent reviewer requested SVG section shortcuts and readable footer captions. Both fixes were confirmed resolved with a `ship` verdict covering the scored fixes, recorded in `.impeccable/review/finish-review.md`. The replacement reviewer inspected local captures directly after the external vision tool stalled; the original reviewer had stopped at a usage limit. + +## Limits + +No real iOS/Android device or WebKit browser run was available. Media fixture checks validate layout/navigation, not a live YouTube stream, audio synchronization, or native fullscreen. Other layout themes retain their styles; regression checks cover switching and data preservation, not exhaustive visual audits of every theme. diff --git a/frontend/app.js b/frontend/app.js index 51f2ff0..a106c9f 100755 --- a/frontend/app.js +++ b/frontend/app.js @@ -6533,6 +6533,13 @@ const Gestures = (() => { * and reopen. Fold state is per device and rides in settings. * ========================================================================== */ const SectionRail = (() => { + const CLASSIC_ICONS = { + player: '', + meta: '', + notes: '', + related: '', + list: '', + }; const SECTIONS = [ { id: 'player', sel: '.player-stage', icon: '📺', label: 'Player' }, { id: 'meta', sel: '#nowPlayingMeta', icon: '🎵', label: 'Now playing', fold: true }, @@ -6617,7 +6624,7 @@ const SectionRail = (() => { btn.className = 'sec-rail-btn'; btn.dataset.sec = sec.id; btn.setAttribute('aria-label', sec.label); - btn.innerHTML = `${escapeHtml(sec.label)}`; + btn.innerHTML = `${escapeHtml(sec.label)}`; btn.addEventListener('click', () => onTap(sec, btn)); r.appendChild(btn); st.btns.set(sec.id, btn); @@ -7170,7 +7177,11 @@ const Party = (() => { function render() { const panel = $('partyPanel'); const chip = $('navPartyBtn'); - if (chip) chip.textContent = inParty() ? `🎉 Party · ${st.members.length}` : '🎉 Watch party'; + if (chip) { + const label = chip.querySelector('.nav-label'); + if (label) label.textContent = inParty() ? `Party · ${st.members.length}` : 'Watch party'; + else chip.textContent = inParty() ? `🎉 Party · ${st.members.length}` : '🎉 Watch party'; + } if (!panel || panel.classList.contains('hidden')) return; const nameOk = !!myName(); if (!inParty()) { @@ -10200,6 +10211,63 @@ function isPortraitPWA() { return _portraitPwaMQ.matches; } +// Classic uses one scroll container at phone and intermediate widths in both +// browser and installed mode. Read CSS so navigation follows the actual layout. +function isStackedPlayerLayout() { + if (document.documentElement.dataset.layout === 'classic') { + return getComputedStyle(document.documentElement).getPropertyValue('--classic-stacked').trim() === '1'; + } + return isPortraitPWA(); +} + +function playerScrollBehavior() { + return data.settings.reduceMotion || window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'instant' : 'smooth'; +} + +function setupClassicLayout() { + const root = document.documentElement; + const search = $('searchInput'); + const originalPlaceholder = search.getAttribute('placeholder'); + const bars = [$('bottomNav'), $('miniBar'), $('sectionRail')].filter(Boolean); + const heightVars = ['--classic-nav-height', '--classic-mini-height', '--classic-rail-height']; + let pending = 0; + function measure() { + pending = 0; + const classic = root.dataset.layout === 'classic'; + search.placeholder = classic ? 'Search YouTube…' : originalPlaceholder; + if (!classic) return; + for (const [index, el] of bars.entries()) { + const height = el.getClientRects().length ? el.offsetHeight : 0; + root.style.setProperty(heightVars[index], height + 'px'); + } + } + function schedule() { + if (!pending) pending = requestAnimationFrame(measure); + } + const observer = new MutationObserver(schedule); + observer.observe(root, { attributes: true, attributeFilter: ['data-layout', 'data-font', 'data-density'] }); + bars.forEach(el => observer.observe(el, { attributes: true, attributeFilter: ['class', 'style'] })); + if (typeof ResizeObserver !== 'undefined') { + const resize = new ResizeObserver(schedule); + bars.forEach(el => resize.observe(el)); + } + window.addEventListener('resize', schedule); + if (window.visualViewport) window.visualViewport.addEventListener('resize', schedule); + // Use the actual clipped control deck, not document scroll position: desktop + // panes and the mobile body have different scroll owners. + if (typeof IntersectionObserver !== 'undefined') { + const mini = $('miniBar'); + const visibility = new IntersectionObserver(([entry]) => { + if (root.dataset.layout !== 'classic' || !current || !current.meta) return; + if (entry.isIntersecting) { + if (!mini.classList.contains('hidden')) hideMiniBar(); + } else if (mini.classList.contains('hidden')) showMiniBar(); + }, { threshold: 0 }); + visibility.observe($('controls')); + } + measure(); +} + function applyPortraitPwaClass() { const app = document.querySelector('.app'); if (!app) return; @@ -10213,10 +10281,10 @@ function applyPortraitPwaClass() { // In portrait mode, .body is the single scroll container (player pane on top, // list pane below). Scrolling it to 0 brings the player fully into view. function scrollPlayerIntoViewPortrait() { - if (!isPortraitPWA()) return; + if (!isStackedPlayerLayout()) return; const body = document.querySelector('.body'); if (body) { - body.scrollTo({ top: 0, behavior: 'smooth' }); + body.scrollTo({ top: 0, behavior: playerScrollBehavior() }); } } @@ -10226,14 +10294,14 @@ function scrollPlayerIntoViewPortrait() { // fresh view sits invisibly below the fold and every nav tap looks dead // (the mobile "can't navigate while playing" bug). function scrollListIntoViewPortrait() { - if (!isPortraitPWA()) return; + if (!isStackedPlayerLayout()) return; const body = document.querySelector('.body'); const listPane = document.querySelector('.list-pane'); // offsetParent is null while the pane is hidden (pre-first-search) — its // rect would be all zeros and scroll the body to a bogus offset. if (!body || !listPane || !listPane.offsetParent) return; const top = body.scrollTop + listPane.getBoundingClientRect().top - body.getBoundingClientRect().top; - body.scrollTo({ top, behavior: 'smooth' }); + body.scrollTo({ top, behavior: playerScrollBehavior() }); } function setupPortraitPwaWatcher() { @@ -10855,7 +10923,7 @@ document.querySelectorAll('.chip').forEach((c) => { // .body pane (the designated scroll container) — scrollIntoView() also // scrolls overflow:hidden ancestors up to on iOS, leaving the // document offset and fixed-element hit testing broken. - if (isPortraitPWA()) scrollPlayerIntoViewPortrait(); + if (isStackedPlayerLayout()) scrollPlayerIntoViewPortrait(); else els.playerPane.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }); // Opens the set-list view for whatever is already playing. No playlist arg, @@ -11774,6 +11842,7 @@ async function boot() { wireUI(); wireShortcutHelp(); setupPortraitPwaWatcher(); + setupClassicLayout(); setupViewportAnchorGuard(); try { const loaded = await API.loadData(); diff --git a/frontend/index.html b/frontend/index.html index cbbc42a..6ed8cc2 100755 --- a/frontend/index.html +++ b/frontend/index.html @@ -25,6 +25,7 @@ +
@@ -35,17 +36,17 @@
@@ -96,6 +97,7 @@
- +