From 4dbf79f2963495dbe038b321a1bb5be3072c9f76 Mon Sep 17 00:00:00 2001 From: Dylan Audius Date: Mon, 19 Oct 2026 15:23:13 -0700 Subject: [PATCH 1/5] feat(web): TrackRow on profile Tracks and Reposts behind track_rows The profile page passes the row variant to its Tracks and Reposts lineups when the `track_rows` flag is on. `TrackRow` gains an optional `context` line above the row, which the connected row uses to label the artist pick. In row mode the lineup drops the two-column grid it uses for small tiles so rows span the full width. Feed stays on tiles on purpose: a feed item is a post with who reposted it and inline playlists, and the tile carries that better than a row. Co-Authored-By: Claude Fable 5.1 --- .../src/components/track-row/TrackRow.tsx | 30 ++++++++++++++++--- .../harmony/src/components/track-row/types.ts | 5 ++++ .../web/src/components/lineup/TrackLineup.tsx | 5 +++- .../components/track/desktop/TrackRowTile.tsx | 16 +++++++++- .../components/desktop/ProfilePage.tsx | 9 ++++++ 5 files changed, 59 insertions(+), 6 deletions(-) diff --git a/packages/harmony/src/components/track-row/TrackRow.tsx b/packages/harmony/src/components/track-row/TrackRow.tsx index e7a7c449b0b..30d71033363 100644 --- a/packages/harmony/src/components/track-row/TrackRow.tsx +++ b/packages/harmony/src/components/track-row/TrackRow.tsx @@ -84,6 +84,7 @@ export const TrackRow = forwardRef( onTogglePlay, onToggleFavorite, menu, + context, playLabel = isPlaying ? 'Pause' : 'Play', favoriteLabel = isFavorited ? 'Unfavorite' : 'Favorite', className, @@ -123,6 +124,16 @@ export const TrackRow = forwardRef( [onToggleFavorite] ) + // Title column starts after the rank and artwork cells and two gaps. + const contextCss = css({ + display: 'flex', + alignItems: 'center', + gap: spacing.xs, + height: 16, + marginTop: spacing.xs, + paddingLeft: spacing.s + RANK_SIZE + ART_SIZE + spacing.m * 2 + }) + const rowCss = css({ display: 'grid', gridTemplateColumns: `${RANK_SIZE}px ${ART_SIZE}px minmax(0, 1fr) auto 32px 48px 32px`, @@ -189,14 +200,14 @@ export const TrackRow = forwardRef( const showEqualizer = isActive && isPlaying const PlayIcon = showEqualizer ? IconPause : IconPlay - return ( + const row = (
{rank != null && !showEqualizer ? ( @@ -297,5 +308,16 @@ export const TrackRow = forwardRef(
) + + if (!context) return row + + return ( +
+ + {context} + + {row} +
+ ) } ) diff --git a/packages/harmony/src/components/track-row/types.ts b/packages/harmony/src/components/track-row/types.ts index 9669b278ff3..722bc2aebac 100644 --- a/packages/harmony/src/components/track-row/types.ts +++ b/packages/harmony/src/components/track-row/types.ts @@ -32,6 +32,11 @@ export type TrackRowProps = { onToggleFavorite?: () => void /** Overflow menu trigger. Hidden until hover or focus. */ menu?: ReactNode + /** + * One short line above the row, aligned with the title column, for + * context such as "Artist Pick". Adds 16px to the row's height. + */ + context?: ReactNode /** Accessible name for the play button. */ playLabel?: string /** Accessible name for the favorite button. */ diff --git a/packages/web/src/components/lineup/TrackLineup.tsx b/packages/web/src/components/lineup/TrackLineup.tsx index c743ad9fe3f..d7990d69ace 100644 --- a/packages/web/src/components/lineup/TrackLineup.tsx +++ b/packages/web/src/components/lineup/TrackLineup.tsx @@ -287,7 +287,10 @@ export const TrackLineup = ({ } else if (variant === LineupVariant.GRID) { tileSize = TrackTileSize.SMALL statSize = 'small' - containerClassName = styles.searchTrackTileContainer + // Rows are full width; the two-column grid is for small tiles only. + containerClassName = isRowLayout + ? undefined + : styles.searchTrackTileContainer } else if (variant === LineupVariant.CONDENSED) { tileSize = TrackTileSize.SMALL } diff --git a/packages/web/src/components/track/desktop/TrackRowTile.tsx b/packages/web/src/components/track/desktop/TrackRowTile.tsx index 77a5f3e308d..247075bd252 100644 --- a/packages/web/src/components/track/desktop/TrackRowTile.tsx +++ b/packages/web/src/components/track/desktop/TrackRowTile.tsx @@ -21,6 +21,7 @@ import { Artwork, IconButton, IconKebabHorizontal, + IconPin, Text, TrackRow } from '@audius/harmony' @@ -62,6 +63,8 @@ export type TrackRowTileProps = { isFeed?: boolean onClick?: (trackId: ID) => void containerClassName?: string + /** Mark the owner's pinned track with an "Artist Pick" line. */ + showArtistPick?: boolean } /** @@ -79,7 +82,8 @@ export const TrackRowTile = ({ hasLoaded, isFeed = false, onClick, - containerClassName + containerClassName, + showArtistPick = false }: TrackRowTileProps) => { const dispatch = useDispatch() const { data: currentUserId } = useCurrentUserId() @@ -130,6 +134,8 @@ export const TrackRowTile = ({ const trackPosition = useSelector((state: CommonState) => getTrackPosition(state, { trackId, userId: currentUserId }) ) + const isArtistPick = + showArtistPick && partialUser?.artist_pick_track_id === trackId const { isFetchingNFTAccess, hasStreamAccess, isPreviewable } = useGatedContentAccess(trackWithFallback) @@ -319,6 +325,14 @@ export const TrackRowTile = ({ } menu={overflowMenu} playLabel={playLabel} + context={ + isArtistPick ? ( + <> + + {messages.artistPick} + + ) : undefined + } data-testid='track-row' /> ) diff --git a/packages/web/src/pages/profile-page/components/desktop/ProfilePage.tsx b/packages/web/src/pages/profile-page/components/desktop/ProfilePage.tsx index 7655641af96..ac25f22a969 100644 --- a/packages/web/src/pages/profile-page/components/desktop/ProfilePage.tsx +++ b/packages/web/src/pages/profile-page/components/desktop/ProfilePage.tsx @@ -17,8 +17,10 @@ import { getProfileRepostsQueryKey } from '@audius/common/api' import { useMuteUser } from '@audius/common/context' +import { useFeatureFlag } from '@audius/common/hooks' import { commentsMessages } from '@audius/common/messages' import { Status } from '@audius/common/models' +import { FeatureFlags } from '@audius/common/services' import { ProfilePageTabs } from '@audius/common/store' import { route } from '@audius/common/utils' import { @@ -89,6 +91,10 @@ const LeftColumnSpacer = () => ( ) const ProfilePage = ({ containerRef }: ProfilePageProps) => { + const { isEnabled: isTrackRowsEnabled } = useFeatureFlag( + FeatureFlags.TRACK_ROWS + ) + const tileVariant = isTrackRowsEnabled ? 'row' : 'tile' const profileFocusRootRef = useRef(null) usePreventOffscreenFocus(profileFocusRootRef) @@ -303,6 +309,7 @@ const ProfilePage = ({ containerRef }: ProfilePageProps) => { hasNextPage={userRepostsQuery.hasNextPage} loadNextPage={userRepostsQuery.loadNextPage} variant={LineupVariant.CONDENSED} + tileVariant={tileVariant} scrollParent={containerRef?.current ?? null} /> ) @@ -337,6 +344,7 @@ const ProfilePage = ({ containerRef }: ProfilePageProps) => { hasNextPage={artistTracksQuery.hasNextPage} loadNextPage={artistTracksQuery.loadNextPage} variant={LineupVariant.GRID} + tileVariant={tileVariant} leadingElementId={profile.artist_pick_track_id ?? undefined} showArtistPick scrollParent={containerRef?.current ?? null} @@ -382,6 +390,7 @@ const ProfilePage = ({ containerRef }: ProfilePageProps) => { hasNextPage={userRepostsQuery.hasNextPage} loadNextPage={userRepostsQuery.loadNextPage} variant={LineupVariant.CONDENSED} + tileVariant={tileVariant} maxEntries={profile.repost_count} scrollParent={containerRef?.current ?? null} /> From 326e8043c02348f33b3bb8949dc0edfee916be0b Mon Sep 17 00:00:00 2001 From: Dylan Audius Date: Mon, 19 Oct 2026 17:11:44 -0700 Subject: [PATCH 2/5] fix(web): space collection cards between rows on profile Reposts Reposts mixes playlists into the track lineup. With rows the cards sat flush against the row above; give them a top margin in row layout. Co-Authored-By: Claude Opus 5.5 --- packages/web/src/components/lineup/Lineup.module.css | 5 +++++ packages/web/src/components/lineup/TrackLineup.tsx | 7 +++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/web/src/components/lineup/Lineup.module.css b/packages/web/src/components/lineup/Lineup.module.css index cec044eaf31..edeef9a84f4 100644 --- a/packages/web/src/components/lineup/Lineup.module.css +++ b/packages/web/src/components/lineup/Lineup.module.css @@ -28,3 +28,8 @@ margin: 0px !important; margin-bottom: 24px !important; } + +/* Collection cards mixed into a row lineup (profile Reposts) */ +.collectionAmongRows { + margin-top: var(--harmony-spacing-m); +} diff --git a/packages/web/src/components/lineup/TrackLineup.tsx b/packages/web/src/components/lineup/TrackLineup.tsx index d7990d69ace..9f44b9b15ce 100644 --- a/packages/web/src/components/lineup/TrackLineup.tsx +++ b/packages/web/src/components/lineup/TrackLineup.tsx @@ -439,7 +439,9 @@ export const TrackLineup = ({ playTrack: handlePlayCollectionTrack, pauseTrack, size: tileSize, - containerClassName, + containerClassName: cn(containerClassName, { + [styles.collectionAmongRows]: isRowLayout + }), id: item.id, isLoading: false, hasLoaded: () => {}, @@ -469,7 +471,8 @@ export const TrackLineup = ({ onClickTile, showArtistPick, TrackTile, - CollectionTile + CollectionTile, + isRowLayout ]) const isInitialLoad = isPending && tiles.length === 0 From 2fee8ee866c4a975be4a2e92529be86ec30a8197 Mon Sep 17 00:00:00 2001 From: Dylan Audius Date: Mon, 19 Oct 2026 15:34:07 -0700 Subject: [PATCH 3/5] feat(web): TrackRow on search track results behind track_rows Search's track results pass the row variant to TrackLineup when the `track_rows` flag is on, and drop the tile spacing styles in row mode. Collection pages are left on their tracks table: it is already a dense 48px row with sortable columns and drag reorder in edit mode, which the row does not replace. Co-Authored-By: Claude Fable 5.1 --- .../search-results/TrackResults.tsx | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/web/src/pages/search-page/search-results/TrackResults.tsx b/packages/web/src/pages/search-page/search-results/TrackResults.tsx index 32a7dcb26a9..fb64bff11d4 100644 --- a/packages/web/src/pages/search-page/search-results/TrackResults.tsx +++ b/packages/web/src/pages/search-page/search-results/TrackResults.tsx @@ -1,7 +1,9 @@ import { useCallback, useMemo } from 'react' import { SEARCH_PAGE_SIZE, useSearchTrackResults } from '@audius/common/api' +import { useFeatureFlag } from '@audius/common/hooks' import { Kind } from '@audius/common/models' +import { FeatureFlags } from '@audius/common/services' import { searchActions, SearchKind } from '@audius/common/store' import { Flex } from '@audius/harmony' import { css } from '@emotion/css' @@ -38,6 +40,10 @@ export const TrackResults = (props: TrackResultsProps) => { const mainContentRef = useMainContentRef() const isMobile = useIsMobile() + const { isEnabled: isTrackRowsEnabled } = useFeatureFlag( + FeatureFlags.TRACK_ROWS + ) + const isRowLayout = isTrackRowsEnabled && !isMobile const dispatch = useDispatch() @@ -85,6 +91,7 @@ export const TrackResults = (props: TrackResultsProps) => { hasNextPage={shouldLoadMore ? hasNextPage : false} loadNextPage={shouldLoadMore ? loadNextPage : undefined} variant={LineupVariant.MAIN} + tileVariant={isRowLayout ? 'row' : 'tile'} scrollParent={mainContentRef.current} emptyElement={} onClickTile={handleClickTrackTile} @@ -92,12 +99,15 @@ export const TrackResults = (props: TrackResultsProps) => { {...(!isMobile ? { lineupContainerStyles: css({ width: '100%' }), - tileContainerStyles: css({ - display: 'flex', - flexDirection: 'column', - gap: '4px 16px', - justifyContent: 'space-between' - }) + // Rows stack with no gap; tiles keep their spacing. + tileContainerStyles: isRowLayout + ? undefined + : css({ + display: 'flex', + flexDirection: 'column', + gap: '4px 16px', + justifyContent: 'space-between' + }) } : {})} /> From 7d741b12757a770e962a10dd61dc826e1a5e9bb8 Mon Sep 17 00:00:00 2001 From: Dylan Audius Date: Mon, 19 Oct 2026 15:39:34 -0700 Subject: [PATCH 4/5] feat(web): dense tracks table on collection pages behind track_rows Table gains an `isDense` prop that restyles rows to match Harmony's TrackRow: flat surface instead of a card, 56px rows, hover fill, accent title on the active row, sentence-case 12px header, 40px artwork and a subdued artist line. Sorting, drag reorder and edit mode are unchanged. The collection page turns it on with the `track_rows` flag and makes its outer wrapper transparent so the hover fill shows. Co-Authored-By: Claude Fable 5.1 --- .../web/src/components/table/Table.module.css | 63 +++++++++++++++++++ packages/web/src/components/table/Table.tsx | 19 +++++- .../tracks-table/TracksTable.module.css | 26 ++++++++ .../desktop/CollectionPage.module.css | 4 ++ .../components/desktop/CollectionPage.tsx | 13 +++- 5 files changed, 121 insertions(+), 4 deletions(-) diff --git a/packages/web/src/components/table/Table.module.css b/packages/web/src/components/table/Table.module.css index 8b1441c28b2..4e6ce42964c 100644 --- a/packages/web/src/components/table/Table.module.css +++ b/packages/web/src/components/table/Table.module.css @@ -382,3 +382,66 @@ .cellSectionEnd.menu { flex: 0 0 64px; } + +/* Dense rows: the TrackRow look on a sortable table. Flat surface, 56px rows, + hover fill instead of card shadows, sentence-case header. */ +.dense.tableWrapper { + background-color: transparent; + border-radius: 0; + box-shadow: none; + padding-bottom: 0; +} + +.dense .table, +.dense .tableHead, +.dense .tableHeadRow, +.dense .tableHeader, +.dense .tableBody, +.dense .tableRow, +.dense .tableRow > .tableCell { + background-color: transparent; +} + +.dense .tableHead { + padding-top: 8px; + font-size: 12px; + font-weight: 500; + text-transform: none; + color: var(--harmony-text-subdued); + border-bottom: 1px solid var(--harmony-border-default); +} + +.dense .headerContent { + min-height: 36px; +} + +.dense .tableRow { + font-size: 14px; + font-weight: 500; + border-radius: var(--harmony-border-radius-m); + transition: background-color 0.12s cubic-bezier(0.44, 0, 0.56, 1); +} + +.dense .tableRow.active { + color: var(--harmony-secondary); +} + +.dense .tableCell { + height: 56px; + padding: 4px 12px; + font-variant-numeric: tabular-nums; +} + +.dense .tableRow:hover:not(.skeletonRow), +.dense .tableRow:focus-within:not(.skeletonRow), +.dense .tableRow:hover:not(.skeletonRow) > .tableCell, +.dense .tableRow:focus-within:not(.skeletonRow) > .tableCell { + box-shadow: none; + background-color: var(--harmony-bg-surface-1); +} + +.dense .tableRow.disabled, +.dense .tableRow.disabled:hover { + background-color: transparent; + opacity: 0.5; +} diff --git a/packages/web/src/components/table/Table.tsx b/packages/web/src/components/table/Table.tsx index da41555eb40..889dc3a4736 100644 --- a/packages/web/src/components/table/Table.tsx +++ b/packages/web/src/components/table/Table.tsx @@ -110,6 +110,8 @@ export type TableProps = { isPaginated?: boolean isReorderable?: boolean isTracksTable?: boolean + /** Flat, 56px rows styled like Harmony's TrackRow: no card, hover fill, sentence-case header. */ + isDense?: boolean isVirtualized?: boolean loading?: boolean onClickRow?: ( @@ -149,6 +151,7 @@ export const Table = ({ isPaginated = false, isReorderable = false, isTracksTable = false, + isDense = false, isVirtualized = false, loading = false, onClickRow, @@ -817,7 +820,11 @@ export const Table = ({ const renderContent = useCallback(() => { return (
(
{ const mainContentRef = useMainContentRef() + const { isEnabled: isTrackRowsEnabled } = useFeatureFlag( + FeatureFlags.TRACK_ROWS + ) const { collection, user, @@ -344,12 +350,17 @@ const CollectionPage = ({ type }: CollectionPageProps) => { dataSource.length === 0 ? ( ) : ( -
+
Date: Mon, 19 Oct 2026 17:12:08 -0700 Subject: [PATCH 5/5] fix(web): make the dense TracksTable rules match Table puts its CSS-module `dense` class on the wrapper, which is hashed, so `:global(.dense)` in TracksTable.module.css never matched and the 40px art, subdued artist line and flat locked/disabled rows didn't apply. TracksTable now adds its own module class when isDense is set. Co-Authored-By: Claude Opus 5.5 --- .../components/tracks-table/TracksTable.module.css | 14 +++++++------- .../src/components/tracks-table/TracksTable.tsx | 5 +++++ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/web/src/components/tracks-table/TracksTable.module.css b/packages/web/src/components/tracks-table/TracksTable.module.css index c2f05d18e5f..08212a20ab8 100644 --- a/packages/web/src/components/tracks-table/TracksTable.module.css +++ b/packages/web/src/components/tracks-table/TracksTable.module.css @@ -292,28 +292,28 @@ opacity: 0; } -/* Dense table (see Table.module.css .dense): 40px art, subdued artist line */ -:global(.dense) .inlineArtworkContainer { +/* Dense table (isDense, see Table.module.css): 40px art, subdued artist line */ +.dense .inlineArtworkContainer { width: 40px; height: 40px; min-width: 40px; border: 0; } -:global(.dense) .trackHeaderArtworkSpacer { +.dense .trackHeaderArtworkSpacer { width: 48px; min-width: 48px; } -:global(.dense) .stackedArtistText { +.dense .stackedArtistText { color: var(--harmony-text-subdued); } -:global(.dense) .tableRow.disabled, -:global(.dense) .tableRow.disabled * { +.dense .tableRow.disabled, +.dense .tableRow.disabled * { background-color: transparent; } -:global(.dense) .tableRow.lockedRow { +.dense .tableRow.lockedRow { background-color: transparent; } diff --git a/packages/web/src/components/tracks-table/TracksTable.tsx b/packages/web/src/components/tracks-table/TracksTable.tsx index 599fa70b82f..fc504609e50 100644 --- a/packages/web/src/components/tracks-table/TracksTable.tsx +++ b/packages/web/src/components/tracks-table/TracksTable.tsx @@ -1105,6 +1105,11 @@ export const TracksTable = ({ return (