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/collections-table/CollectionsTable.tsx b/packages/web/src/components/collections-table/CollectionsTable.tsx index f605a36fe38..83cbff2d02a 100644 --- a/packages/web/src/components/collections-table/CollectionsTable.tsx +++ b/packages/web/src/components/collections-table/CollectionsTable.tsx @@ -56,6 +56,7 @@ type CollectionsTableProps = { totalRowCount?: number tableHeaderClassName?: string responsiveColumns?: TableProps['responsiveColumns'] + isDense?: boolean } const defaultColumns: CollectionsTableColumn[] = [ @@ -84,7 +85,8 @@ export const CollectionsTable = ({ showMoreLimit, totalRowCount, tableHeaderClassName, - responsiveColumns + responsiveColumns, + isDense }: CollectionsTableProps) => { // Cell Render Functions const renderNameCell = useCallback((cellInfo: CollectionCell) => { @@ -282,6 +284,7 @@ export const CollectionsTable = ({ totalRowCount={totalRowCount} tableHeaderClassName={tableHeaderClassName} responsiveColumns={responsiveColumns} + isDense={isDense} /> ) } 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 c743ad9fe3f..9f44b9b15ce 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 } @@ -436,7 +439,9 @@ export const TrackLineup = ({ playTrack: handlePlayCollectionTrack, pauseTrack, size: tileSize, - containerClassName, + containerClassName: cn(containerClassName, { + [styles.collectionAmongRows]: isRowLayout + }), id: item.id, isLoading: false, hasLoaded: () => {}, @@ -466,7 +471,8 @@ export const TrackLineup = ({ onClickTile, showArtistPick, TrackTile, - CollectionTile + CollectionTile, + isRowLayout ]) const isInitialLoad = isPending && tiles.length === 0 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..97c4f354dfb 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 (
(
)} @@ -944,6 +956,7 @@ export const Table = ({ fetchBatchSize, scrollRef, wrapperClassName, + isDense, tableClassName, getTableProps, tableHeaderClassName, 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/components/tracks-table/TracksTable.module.css b/packages/web/src/components/tracks-table/TracksTable.module.css index 25e78d715b2..08212a20ab8 100644 --- a/packages/web/src/components/tracks-table/TracksTable.module.css +++ b/packages/web/src/components/tracks-table/TracksTable.module.css @@ -291,3 +291,29 @@ .tableRow .overflowMenu { opacity: 0; } + +/* Dense table (isDense, see Table.module.css): 40px art, subdued artist line */ +.dense .inlineArtworkContainer { + width: 40px; + height: 40px; + min-width: 40px; + border: 0; +} + +.dense .trackHeaderArtworkSpacer { + width: 48px; + min-width: 48px; +} + +.dense .stackedArtistText { + color: var(--harmony-text-subdued); +} + +.dense .tableRow.disabled, +.dense .tableRow.disabled * { + background-color: transparent; +} + +.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 (
{ const mainContentRef = useMainContentRef() + const { isEnabled: isTrackRowsEnabled } = useFeatureFlag( + FeatureFlags.TRACK_ROWS + ) const { collection, user, @@ -344,12 +350,17 @@ const CollectionPage = ({ type }: CollectionPageProps) => { dataSource.length === 0 ? ( ) : ( -
+
{ + const { isEnabled: isTrackRowsEnabled } = useFeatureFlag( + FeatureFlags.TRACK_ROWS + ) const navigate = useNavigateToPage() const { data: accountUser } = useCurrentAccountUser() const account = accountUser @@ -69,6 +74,7 @@ export const ArtistDashboardAlbumsTab = ({ totalRowCount={account.track_count} tableHeaderClassName={styles.tableHeader} responsiveColumns={RESPONSIVE_TABLE_POLICIES.dashboardAlbums} + isDense={isTrackRowsEnabled} /> ) diff --git a/packages/web/src/pages/dashboard-page/components/ArtistDashboardTracksTab.tsx b/packages/web/src/pages/dashboard-page/components/ArtistDashboardTracksTab.tsx index c76f6cb24e8..e1ac7dd1d2c 100644 --- a/packages/web/src/pages/dashboard-page/components/ArtistDashboardTracksTab.tsx +++ b/packages/web/src/pages/dashboard-page/components/ArtistDashboardTracksTab.tsx @@ -4,6 +4,8 @@ import { useCurrentAccountUser, useUserTracksByHandle } from '@audius/common/api' +import { useFeatureFlag } from '@audius/common/hooks' +import { FeatureFlags } from '@audius/common/services' import { Nullable } from '@audius/common/utils' import { Flex } from '@audius/harmony' @@ -39,6 +41,9 @@ export const ArtistDashboardTracksTab = ({ selectedFilter, filterText }: ArtistDashboardTracksTabProps) => { + const { isEnabled: isTrackRowsEnabled } = useFeatureFlag( + FeatureFlags.TRACK_ROWS + ) const navigate = useNavigateToPage() const [page, setPage] = useState(0) @@ -98,6 +103,7 @@ export const ArtistDashboardTracksTab = ({ tableHeaderClassName={styles.tableHeader} responsiveColumns={RESPONSIVE_TABLE_POLICIES.dashboardTracks} shouldShowGatedType + isDense={isTrackRowsEnabled} /> ) diff --git a/packages/web/src/pages/library-page/components/desktop/LibraryPage.tsx b/packages/web/src/pages/library-page/components/desktop/LibraryPage.tsx index a15559daf4d..578e90e6fa7 100644 --- a/packages/web/src/pages/library-page/components/desktop/LibraryPage.tsx +++ b/packages/web/src/pages/library-page/components/desktop/LibraryPage.tsx @@ -5,7 +5,9 @@ import { useFavoriteTrack, useUnfavoriteTrack } from '@audius/common/api' +import { useFeatureFlag } from '@audius/common/hooks' import { Kind, Status, FavoriteSource, Track } from '@audius/common/models' +import { FeatureFlags } from '@audius/common/services' import { libraryPageSelectors, LibraryCategory, @@ -68,6 +70,9 @@ const tableColumns: TracksTableColumn[] = [ ] const LibraryPage = () => { + const { isEnabled: isTrackRowsEnabled } = useFeatureFlag( + FeatureFlags.TRACK_ROWS + ) const titleRowRef = useRef(null) const tabContainerRef = useRef(null) const isCondensedHeader = useIsContainerNarrow(titleRowRef, 720) @@ -220,7 +225,10 @@ const LibraryPage = () => { ( ) 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} /> 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' + }) } : {})} />