The invisible 50pt drag-grabber overlay covered the page bar and
swallowed taps on the info/comments/related/queue tabs. Limit the
grabber hit area to the 14pt strip above the page bar and exclude
the gradient from hit testing.
Phase 1 of the player panels redesign:
- New PanelSettings model (pages, regions, layout mode, default
visibility) stored as an optional field on PlayerControlsLayout with
an effectivePanelSettings accessor; lenient Codable so future page
types and settings payloads never corrupt saved presets.
- New Views/Player/Panels content engine: PlayerPanelContentView
renders a page bar plus the selected page (info, comments, related,
chapters, queue, tracks), with runtime hiding of empty pages.
- PortraitDetailsPanel and FloatingDetailsPanel now host the shared
content instead of their duplicated info bodies; drag machinery,
pills, and fades are untouched.
- Comments list extracted into CommentsListContent shared by the
overlay and the comments page; comments pill routes to the comments
page (expanding the portrait panel) when the page is enabled, and
falls back to the overlay when it is not.
- Tracks page embeds QualitySelectorView via a new isEmbedded mode.
- Deleted the unused video info bodies in ExpandedPlayerSheet+VideoInfo
and the dead PlayerInfoTab model.
Add a Custom swatch to the appearance settings accent color grid,
backed by a hex value stored in settings (synced via iCloud like the
rest). iOS uses the native ColorPicker wheel; macOS uses a circular
swatch that opens NSColorPanel, since the SwiftUI color well looks out
of place among the circles. All accent consumers now read the resolved
color through SettingsManager.resolvedAccentColor.
The indigo preset is retired from the grid but kept in the enum, so
users who selected it keep their color until they pick another one.
Claude-Session: https://claude.ai/code/session_0154KH8RAVAvm6iVanhmoW8o
Reset isCommentsExpanded and commentsFrame on the NavigationCoordinator
directly when the portrait panel is dismissed, since PortraitDetailsPanel
owns its own @State that doesn't sync back through .onChange during dismiss.
Also track comments overlay frame via GeometryReader so the dismiss gesture
can allow swipes outside the comments area instead of blanket-blocking.