Skip to content

Material 3 Expressive design system overhaul - #7

Merged
ingoau merged 20 commits into
mainfrom
claude/desktop-player-redesign
Sep 25, 2026
Merged

ingoau merged 20 commits into
mainfrom
claude/desktop-player-redesign

Conversation

@ingoau

@ingoau ingoau commented Sep 24, 2026

Copy link
Copy Markdown
Owner

Implements a comprehensive Material 3 Expressive design system refresh across the entire application, replacing the previous design language with Material 3's expressive aesthetic including updated typography, spacing, motion, and component styling.

Summary

This is a major design system update that introduces Material 3 Expressive tokens, springs-based motion, and redesigned form controls. The changes affect the global design tokens, all major UI components, and introduce new stylesheets for different page contexts (shell, library, pages).

Key Changes

Design Tokens & Motion

  • Added comprehensive Material 3 Expressive shape scale (--shape-xs through --shape-full) replacing ad-hoc border-radius values
  • Introduced spring-based motion system with sampled linear() easings for spatial animations (FLIP transitions) and effects
  • Added motion duration tokens (--dur-short, --dur-spring, --dur-spring-long)
  • Implemented tonal surfaces (--surface-tonal, --surface-tonal-sunken) with contrast-checked accent washes
  • Added shadow tokens (--shadow-1, --shadow-2) and layout grid tokens (--page-gutter, --page-title-inset, --page-pad)
  • Added typography ramp tokens for display, section, and card titles with responsive sizing

Form Controls

  • Created new components/controls.tsx with Material 3 Expressive implementations:
    • Slider: thick rounded track with gap around handle, optional value bubble, vertical orientation support
    • Switch: outlined when off, filled with check when on, over transparent native checkbox
    • Select: select-only combobox with M3 menu replacing platform <select>
  • All controls use transparent native inputs/elements for accessibility and semantics

Fullscreen Player Redesign

  • Completely rewrote FullscreenPlayer.tsx with new layout and interaction model
  • Artwork flies in/out using FLIP animation with spring motion when switching between art and panes (Lyrics/Queue/About)
  • Added WavySeek.tsx component: Material 3's wavy progress bar with amplitude spring that responds to playback state
  • Implemented pane switching with remembered state (nowPlayingMode in store)
  • Added motion tokens passed as CSS custom properties for coordinated animations

Stylesheet Organization

  • Split styles into context-specific sheets loaded after global.css:
    • m3-shell.css: app shell, navigation, floating panes, dialogs, menus
    • m3-library.css: library views with tiles, grids, artist cookies, genre cards
    • m3-pages.css: settings, first-run setup, tool pages
    • controls.css: form control styling
    • now-playing.css: fullscreen player and queue timeline
  • All sheets follow Material 3 Expressive color rules: text only on --bg, --bg-elev, --bg-sunken, or contrast-checked accent tints

Icon System

  • Replaced lucide-react with Material Symbols (rounded, Apache-2.0)
  • Generated material-icons.ts from @iconify-json/material-symbols via new scripts/gen-icons.mjs
  • Each icon has outlined and filled variants; outlined is default
  • Supports both renderer short names and Material Symbols names from action registry

Queue Panel Updates

  • Redesigned queue timeline to rest with current item at top
  • History section above with separate Clear button
  • "Playing next" and "Continue playing — from " sections below
  • Updated string keys: "Played" → "History", "Continuing from" → "Continue playing"

Settings Redesign

  • Converted to pill-based section navigation in sidebar
  • Grouped setting rows with tonal backgrounds and rounded corners
  • Integrated new form controls (Select, Slider, Switch)

Spring Motion Library

  • New lib/spring.ts: damped spring sampled into linear() easings
  • Exports SPRING_SPATIAL, SPRING_FAST, SPRING_EFFECTS with easing and duration
  • Used for FLIP animations and CSS transitions throughout

Accessibility & Reduced Motion

  • All

https://claude.ai/code/session_01PwQBQmwS2cmw42GzN9EVyJ

…w-playing view

Lyrics, Queue and About replace the artwork in place while the artwork
springs into a thumbnail beside the title (FLIP); the choice is remembered.
Wavy seek bar that flattens when paused, morphing play button, tonal
playback options, artwork-tinted background darkening towards the bottom.
The queue timeline rests on the current item with History above, Clear
actions on History and Playing next, and follows the current item only
while the user isn't working in it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwQBQmwS2cmw42GzN9EVyJ
The section Clear buttons move out of the listbox (axe: a listbox holds
options only) and are anchored onto their sticky headers with CSS anchor
positioning. Snackbars drop in from the top while the player is open.
The e2e specs drive the Lyrics / Queue / About toggles instead of the old
fullscreen tabs, and cover the remembered pane, L/Q switching, the queue
resting on the current item and clearing history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwQBQmwS2cmw42GzN9EVyJ
…on app-wide

Shape and motion tokens; pill buttons and circular icon buttons that
squash on press; tonal filled selection for nav items, tabs, menus and the
palette; springier tiles, dialogs and snackbars; filled rounded inputs.
The player bar keeps its look. Drops the old two-column fullscreen rules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwQBQmwS2cmw42GzN9EVyJ
The seek bar is rebuilt to Material 3 Expressive's wavy progress: a thick
round-capped wave whose amplitude is a spring (it swells on play, settles
flat on pause), tapering into the start and the thumb; a gap either side
of a bar thumb that stretches while held, a value bubble while dragging or
keyboard-focused, a stop indicator on the track, remaining time. Drawn
by a rAF loop only while moving; still under reduced motion.

Wide windows get a desktop layout instead of a stretched phone column:
the artwork (or the pane replacing it) fills the left, the controls are a
centred column on the right with bigger type, full-width toggles and an
expressive volume slider. The artwork still flies between its slots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwQBQmwS2cmw42GzN9EVyJ
Wide windows: in the artwork mode the cover is much smaller and sits beside
the controls, the two centred together; with Lyrics, Queue or About open the
pane fills the left and the cover stacks, as large as the window allows,
on top of the controls column. The controls glide to their new place (FLIP)
as the cover flies between its slots. Narrower windows cap the cover and
give the pane-mode thumbnail more room.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwQBQmwS2cmw42GzN9EVyJ
Icons come from Material Symbols instead of Lucide: scripts/gen-icons.mjs
extracts the outlined and filled rounded glyphs the renderer uses from the
Iconify dataset into material-icons.ts (only those, ~50 kB), and Icon draws
them. A `filled` prop replaces the old fill-style hack: transport glyphs are
filled by default, a loved heart and set stars fill, and selected states
(the current sidebar destination, the player's pressed toggles and pane
pills) switch to the filled glyph as Material 3 does. Drops lucide-react.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwQBQmwS2cmw42GzN9EVyJ
…s them

Shuffle and repeat now flank the transport, and autoplay, device and
sleep share a row with volume, in every layout. With Lyrics, Queue or
About open on a wide window the controls condense further: title and its
actions on one line, a smaller transport, tighter spacing. That leaves
room for the stacked cover to fill the column's width (height allowing).

Toasts pop in by transform only, so they are never drawn part-transparent
(axe could catch one mid-fade below 4.5:1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwQBQmwS2cmw42GzN9EVyJ
…e panel cards, menus, dialogs, page transition)

The window background is the plain surface; the content and the side panel
float on it as rounded surface-container panes with an 8px gutter. The
sidebar is a navigation drawer/rail on the background with a springy tonal
indicator behind a filled icon; sentence-case section headings. The top bar
blends into the background around a big centred pill search bar whose results
drop as an attached sheet. Queue and lyrics are two stacked cards with a
segmented pill Queue/Recent control. Menus, palette, jobs popover, dialogs
and snackbars get larger shapes, expressive titles and spring entrances.
Router keys the view on the page so each navigation rises in on a spring
(transform only; off under reduced motion). Forced-colours and the narrow
layout are covered in m3-shell.css.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwQBQmwS2cmw42GzN9EVyJ
- Settings: pill section nav with icons and a filled tonal active item;
  display-size page title, sentence-case section headings; rows grouped
  into segmented list cards (2px gaps, big corners on the ends); M3
  switches over the native checkboxes; M3 Expressive sliders (thick
  track, gap-separated bar handle, stop dot); filled rounded fields;
  cache usage as segmented linear progress; big EQ well; order lists and
  per-category shortcut groups as grouped lists with a morphing recorder.
- Setup: expressive hero (decorative M3 shapes, display wordmark) beside
  a tonal card form with a large pill Connect button.
- Downloads: connected button-group tabs, grouped rounded rows with
  morphing artwork, M3 linear progress with a gap and stop dot.
- Filters: grouped rows with tonal shape avatars; builder rule groups as
  tonal cards, assist-chip actions, capability chip, sample side card.
- Stats: display-number hero cards, pill bars, tonal cards.
- Mini player: rounded art, filled play button that morphs while playing.
- Icons: tune, graphic_eq, swap_horiz, cast, palette, keyboard, backup,
  dashboard_customize, headphones, schedule, dns, key.

All motion is killed by reduced motion; forced-colours equivalents for
the new switches, sliders, progress and grouped rows; narrow layout kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwQBQmwS2cmw42GzN9EVyJ
Display-size page titles with counts as tonal chips and sort as a connected
button group; tiles whose art lifts and morphs into a squircle on a spring,
with a large squircle play FAB that rounds into a circle; artists clipped to
a scalloped cookie that morphs on hover; home carousels under bold headings
with tonal cookie icons; detail heroes as tonal cards with big squircle art,
a large Play FAB, Shuffle and secondary actions as tonal buttons; track
tables with a pill header, rounded rows that square off when selected and a
tonal playing row; genre cards with a per-genre coloured decorative shape;
empty states with a glyph in a tonal cookie. Short windows (200% zoom) get a
compact hero and a scrolling detail page. Reduced motion and forced colours
covered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwQBQmwS2cmw42GzN9EVyJ
Native form controls are replaced by our own, drawn to Material 3
Expressive and shared by every screen (components/controls.tsx,
styles/controls.css):
- Slider: thick rounded track split by a gap around a bar handle, stop
  dot, value bubble while dragged or focused; horizontal (volume in the
  player bar and fullscreen player, settings) and vertical (equaliser
  bands). A transparent native range input on top keeps keys, assistive
  tech and tests unchanged.
- Switch: outlined when off; filled with an always-white handle and a
  check when on (role=switch over a native checkbox).
- Select: a select-only combobox with an M3 menu (check + tonal fill on
  the chosen item, type-ahead, Home/End/Page keys), which follows its field
  on scroll instead of closing; same <option> children and onChange shape
  as <select>, so call sites only swapped the tag.
- Number inputs lose the spinners; colour input is a round swatch.

Album, playlist and artist Play buttons now work before the page's details
have loaded (they only need the id): a quick Enter used to do nothing,
which is what made the keyboard e2e test flaky.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwQBQmwS2cmw42GzN9EVyJ
One layout grid, type ramp and control-size set, shared by every page:
- tokens in global.css: --page-gutter/--page-title-inset/--page-pad,
  --title-display/--title-section/--title-card, --btn-h/-sm/-lg, --field-h
- every view header/body uses them (the tool pages had 32/34px padding and
  48px titles, the library 28px and a clamp); hero, track tables, grouped
  lists, cards and genre cards all start at the same gutter; tables no
  longer depend on inline "0 8px" padding (--table-inset)
- one section-heading style (with the tonal icon on Home, Search and
  Artist pages), settings h2s match it; one card-title for cards, the
  filter sample and the jobs popover
- text buttons 36px, small 32px, header/dialog actions 40px everywhere;
  text fields 36px (prompt dialog field was 28px); badges one size
- one segmented tab style for Downloads and the side panel
- Filters/Playlists create buttons and the builder's Play are tonal;
  Downloads usage text moved beside the tabs so actions stay on one line
- empty states all carry a glyph (Filters, Stats, Downloads, side panel)
- context menu clamps with its layout size (it overflowed the window while
  scaled by its entrance animation) and scrolls if taller than the window
- sidebar Settings pill no longer wider than the others; settings nav wide
  enough for "Keyboard shortcuts"; carousel scrollbars show on hover only;
  narrow Home rows fill the width
- removed dead/duplicated rules (per-page header/body/h1/field/button
  overrides, old detail-head action sizes, .home-section h2 that beat the
  shared heading rule, bordered pin-row/rule-group/card bases)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwQBQmwS2cmw42GzN9EVyJ
Conflicts in m3-pages.css: the controls branch's select/switch/slider
removals win; its page-scoped .input/.btn.sm heights are now covered by
the shared --field-h / --btn-h-sm tokens; .m3-select keeps its bg-elev
fill on setting rows and filter rule groups. .m3-select's height uses
--field-h so it tracks the text fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwQBQmwS2cmw42GzN9EVyJ
The Albums/genre sort group targeted the old native .select; with the
merged .m3-select it rendered as a sunken 36px field beside a tonal 40px
direction button. Target both, sized with --btn-h-lg.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwQBQmwS2cmw42GzN9EVyJ
The hover state layer (14 % of the page colour over the toast) lightened the
background under the --accent-inverse action text to about 3.1:1; axe
caught it once a toast appeared under the pointer. Hover now draws a ring in
the text colour and leaves the background alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwQBQmwS2cmw42GzN9EVyJ
…2e undo is real

The native core spells its default shortcuts with Ctrl ("Ctrl+Z", "Ctrl+K"
…) on Linux and Windows, which parsed as the literal Ctrl modifier while
chordFromEvent reports Ctrl there as the platform modifier: the keymap
never matched, so page-level Ctrl shortcuts (undo, palette, find…) did
nothing unless a focused widget handled the key. Off macOS the keymap now
folds a Ctrl binding into Mod.

native.spec's rate-then-undo passed on main only by accident: its click at
the content pane's centre moved the pointer off the rating stars, and the
undo it meant to test never reached the core. Here the taller album header
puts the album's own rating at that point, which exposed it. The test now
focuses the pane, moves the pointer away from the stars' hover preview,
and presses Ctrl+Z, which really undoes the rating.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwQBQmwS2cmw42GzN9EVyJ
@ingoau
ingoau merged commit 2305144 into main Sep 25, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants