Skip to content

feat(web): dense TrackRow on Trending behind the track_rows flag - #14684

Merged
dylanjeffers merged 5 commits into
mainfrom
design/track-row
Oct 21, 2026
Merged

dylanjeffers merged 5 commits into
mainfrom
design/track-row

Conversation

@dylanjeffers

Copy link
Copy Markdown
Contributor

What

Step two of the redesign, stacked on #14683 (tokens v2). A dense track row replaces the tile on Trending when the track_rows flag is on. No change when the flag is off, which is the default.

tiles rows
Tracks visible at 1440×900 3 full + part of a 4th 11
Row height 144px + 12px gap 56px

Harmony TrackRow

packages/harmony/src/components/track-row. Grid: 28px rank · 40px art · title/artist · plays · favorite · duration · menu. Presentational: the app passes title and subtitle nodes (links), artwork, pre-formatted playCount and duration, and a menu node. States: hover/focus-within (surface-1 fill, play button replaces the rank, favorite and menu fade in), isActive (accent title, aria-current), isPlaying (3-bar equalizer, pause icon), isFavorited (heart stays visible in accent), isLoading (skeleton cells). The rank uses the display face. Reduced motion stops the equalizer.

Clicking anywhere on the row that is not a link, button or the menu toggles playback, matching the tile.

Web

  • TrackRowTile (components/track/desktop) has the same contract as the desktop TrackTile: reads the track from the tanquery cache, drives play/pause through the lineup's togglePlay, favorites through tracksSocialActions, gated tracks open the locked-content modal, and the overflow menu is the same TrackMenu config as the tile. Artwork comes from useTrackCoverArt at 150×150. Draggable like the tile.
  • TrackLineup takes tileVariant?: 'tile' | 'row'. Rows render with no gap between list items; narrow containers and mobile still get the tile.
  • TrendingPageContent reads useFeatureFlag(FeatureFlags.TRACK_ROWS) and passes the variant to both the tracks and underground lineups.
  • FeatureFlags.TRACK_ROWS = 'track_rows', default false. The flag still needs to be created in Optimizely before rollout; locally it can be forced with localStorage.setItem('FeatureFlagOverride:track_rows', 'enabled').

Verified

  • tsc clean for harmony and web; eslint clean on all touched files
  • Dev server against prod data, flag forced on: rows render with artwork, badges, counts and durations; hover reveals play/favorite/menu; clicking play starts playback and the row shows the equalizer, pause label and aria-current; light and dark.

Not in this PR

  • Rows on Feed, profiles, search and collections (next, once Trending reads well)
  • Rank movement (▲/▼) needs week-over-week data the API does not expose yet
  • Repost and share stay in the overflow menu, per the spec

🤖 Generated with Claude Code

Neue light/dark palettes rebuilt around neutral chrome and one accent:
- every text pair passes WCAG AA (subdued was 3.5:1 dark / 2.6:1 light)
- borders visible in dark mode (1.6:1, was 1.27:1)
- primary purple #6A52E0 so white button text passes (5.4:1, was 4.26)
- premium is green again (the Neue migration had mapped it to purple)
- headings are plain text instead of a gradient
- selected nav item sits on a neutral surface, not the accent

Type: four weights (400/500/600/700). Body defaults to regular, title to
demi-bold, heading/display to bold. Heavy is no longer used by variants.

Web fonts: Inter Variable for UI text and Bricolage Grotesque Variable for
display/heading, both self-hosted via @fontsource-variable, replacing the
242 KB base64 Avenir in the blocking CSS. Harmony now also emits
harmony-no-fonts.css for apps that load fonts themselves (from #14623).
Text sets --harmony-font-family on display elements so web's global
font-family rule keeps the display face.

Mobile picks up the palette and weights through harmony-native; its font
files are unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 4a2fc37

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

…ir fallback

- Gradient headings and the purple selected nav stay on Classic and Matrix;
  only Neue gets plain headings and the neutral selected state.
- Neue dark: text on the #6A52E0 accent (primary button, secondary hover,
  pill hover) uses staticWhite. static.white is dark there and gave 3.4:1.
- Font stacks list Avenir after Inter, so embed, protocol-dashboard and
  distro (harmony.css, no Inter) keep rendering in Avenir.
- Confetti canvas text uses Inter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dylanjeffers and others added 3 commits October 19, 2026 17:17
The Neue dark text fix applied to every primary button, so the light green
Buy button got near-white text. Only the accent background uses light text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Harmony gains `TrackRow`, a 56px single-line row: rank · 40px artwork ·
title and artist · plays · favorite · duration · overflow menu. Hover or
keyboard focus swaps the rank for a play button and reveals the favorite
and menu controls; the playing row shows an equalizer and an accent title.
The row is presentational: the app passes links, artwork, formatted
numbers and the menu.

Web adds `TrackRowTile`, a connected row with the same props as the
desktop `TrackTile`, so `TrackLineup` can swap between them with a new
`tileVariant` prop. Narrow containers and mobile keep the tile.

Trending renders rows when `FeatureFlags.TRACK_ROWS` (`track_rows`,
default off) is enabled. At 1440x900 the page shows 11 tracks instead
of 3 and a half.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Locked tracks show the price/unlock pill in place of plays, and gated or
  hidden tracks show their access label next to plays, like the tile.
- Owners get no favorite button, matching the tile and the track menu.
- Zero play counts render empty instead of "0".
- Podcasts and audiobooks show "X left" and "Played", like the tile.
- Rank column is 32px and ranks from 100 use a smaller size, so three
  digits fit.
- track_rows defaults on, so pages don't swap tiles for rows once the
  Optimizely datafile loads. The remote flag is still the kill switch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from design/tokens-v2 to main October 21, 2026 23:03
@dylanjeffers
dylanjeffers merged commit 922b2d2 into main Oct 21, 2026
18 checks passed
@dylanjeffers
dylanjeffers deleted the design/track-row branch October 21, 2026 23:03
dylanjeffers added a commit that referenced this pull request Oct 21, 2026
…14685)

## What

Stacked on #14684. The profile page's Tracks and Reposts lineups render
Harmony `TrackRow` when `track_rows` is on. Default off, no change
otherwise.

- `TrackRow` gets an optional `context` node: one 12px line above the
row, aligned with the title column, used here for "Artist Pick" with the
pin icon. It adds 16px to that row only.
- `TrackRowTile` takes `showArtistPick` and fills `context` when the
track is the owner's pick, matching the tile's label.
- `TrackLineup` no longer applies the two-column grid container in row
mode (profile Tracks uses `LineupVariant.GRID` for small tiles; rows
span the column).
- `ProfilePage` (desktop) reads the flag and passes `tileVariant` to the
artist Tracks lineup, the artist Reposts lineup and the user Reposts
lineup.

## Feed is intentionally left on tiles

A feed item is a post: who reposted it, inline playlists, comment
activity. The tile carries that; a 56px row would strip it. Rows are for
catalog surfaces (Trending, profiles, search, collections). Keeping Feed
on tiles also leaves a control surface for reading the row experiment.

## Verified

- `tsc` clean for harmony and web; eslint clean
- Dev server, flag forced on: Ljazz profile Tracks shows the pinned
track with the "Artist Pick" line, then rows at full column width;
Reposts tab renders rows. Radio Juicy (no pick) renders plain rows.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
dylanjeffers added a commit that referenced this pull request Oct 22, 2026
…14700)

Phase 1 of the redesign on mobile: tracks in catalog lineups render as
64px rows instead of bordered tiles, matching web (#14684-#14690). Gated
by the existing `track_rows` flag, which is on in production, so this
reaches RC users through the RC OTA on merge. Feed keeps tiles.

**Where:** Trending, Underground, profile Tracks and Reposts, search
track results. Collections in mixed lineups (Reposts) stay tiles.
Library already uses `TrackList` rows and is unchanged.

**Row:** optional rank, 48px art, title, artist with badges, duration
(or "12m left" / "Played" for podcasts and audiobooks), and a
price/unlock button in place of the duration on locked tracks.
- Tap plays (gated tracks open the purchase modal or locked-content
drawer, same as the tile).
- Long-press opens the track's actions, with a haptic. No kebab: the
title gets the space. VoiceOver gets the same menu as a "More options"
action.
- The playing track gets an accent title and an equalizer on its art.

**Code:**
- `harmony-native/components/TrackRow`: presentational row.
- `lineup-tile/TrackRowTile`: connected row.
- `lineup-tile/useOpenTrackOverflow`: overflow actions now shared by
`TrackTile` and the row (tile menu unchanged). Rows add share, favorite
and repost since they have no inline buttons.
- `TrackLineup` gets `tileVariant: 'tile' | 'row'`.

**Tested** on the iOS 26.5 simulator (1.5.190 Release build with this
bundle swapped in), signed in: Trending rows and ranks, tap to play and
switch tracks, equalizer, long-press menu without starting playback,
pagination to 50, profile Tracks rows, loading skeletons. Not tested:
Android, search results, a locked track's price button, VoiceOver.

Mobile `tsc` and eslint are clean locally (mobile CI only runs security
checks).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant