From 4dbf79f2963495dbe038b321a1bb5be3072c9f76 Mon Sep 17 00:00:00 2001 From: Dylan Audius Date: Mon, 19 Oct 2026 15:23:13 -0700 Subject: [PATCH 1/3] 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/3] 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/3] 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' + }) } : {})} />