From 7fb0dffc409dcd94906604585b90f4420a71bade Mon Sep 17 00:00:00 2001 From: jeffvli Date: Thu, 20 Nov 2025 03:47:56 -0800 Subject: [PATCH] redesign album detail page --- src/i18n/locales/en.json | 1 + .../album-detail-content.module.css | 57 ++- .../components/album-detail-content.tsx | 411 +++++++++++++----- .../albums/components/album-detail-header.tsx | 219 +++------- .../albums/components/album-list-header.tsx | 8 +- .../albums/routes/album-detail-route.tsx | 1 + .../shared/components/library-header-bar.tsx | 11 +- .../components/library-header.module.css | 46 +- .../shared/components/library-header.tsx | 61 ++- .../shared/components/play-button.module.css | 77 ++++ .../shared/components/play-button.tsx | 52 ++- src/shared/components/pill/pill.module.css | 5 + src/shared/components/pill/pill.tsx | 2 +- .../components/rating/rating.module.css | 12 +- 14 files changed, 662 insertions(+), 301 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 9386609f9..19df25db7 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -70,6 +70,7 @@ "edit": "edit", "enable": "enable", "expand": "expand", + "externalLinks": "external links", "favorite": "favorite", "filter_one": "filter", "filter_other": "filters", diff --git a/src/renderer/features/albums/components/album-detail-content.module.css b/src/renderer/features/albums/components/album-detail-content.module.css index 42db7c562..5fbb63e92 100644 --- a/src/renderer/features/albums/components/album-detail-content.module.css +++ b/src/renderer/features/albums/components/album-detail-content.module.css @@ -1,6 +1,7 @@ .content-container { position: relative; z-index: 0; + container-type: inline-size; } .detail-container { @@ -8,5 +9,59 @@ flex-direction: column; gap: var(--theme-spacing-lg); padding: 1rem 2rem 5rem; - overflow: hidden; +} + +.content-layout { + display: grid; + grid-template-rows: auto auto; + grid-template-columns: 1fr; + gap: var(--theme-spacing-lg); + align-items: start; + width: 100%; + min-width: 0; +} + +.metadata-column { + display: flex; + flex-direction: column; +} + +.songs-column { + display: flex; + flex-direction: column; + min-width: 0; + overflow-x: hidden; +} + +@container (min-width: $mantine-breakpoint-lg) { + .content-layout { + grid-template-rows: 1fr; + grid-template-columns: minmax(0, 1fr) 300px; + gap: var(--theme-spacing-xl); + width: 100%; + max-width: 100%; + } + + .songs-column { + grid-row: 1; + grid-column: 1; + } + + /* Prevent sticky headers from extending into the right margins */ + .songs-column :global(.fs-item-table-list-module-sticky-header), + .songs-column :global(.fs-item-table-list-module-sticky-group-row) { + padding-right: 0; + margin-right: 0; + } + + .metadata-column { + position: sticky; + top: calc(var(--theme-spacing-lg) + 4rem); + grid-row: 1; + grid-column: 2; + align-self: start; + width: 300px; + max-height: calc(100vh - 90px - var(--theme-spacing-lg) - 4rem); + overflow-y: auto; + } } diff --git a/src/renderer/features/albums/components/album-detail-content.tsx b/src/renderer/features/albums/components/album-detail-content.tsx index dd40f8115..44b40738e 100644 --- a/src/renderer/features/albums/components/album-detail-content.tsx +++ b/src/renderer/features/albums/components/album-detail-content.tsx @@ -1,7 +1,7 @@ import { useQuery } from '@tanstack/react-query'; -import { Suspense, useMemo, useState } from 'react'; +import { ReactNode, Suspense, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useParams } from 'react-router'; +import { generatePath, useParams } from 'react-router'; import styles from './album-detail-content.module.css'; @@ -12,36 +12,310 @@ import { ItemTableList } from '/@/renderer/components/item-list/item-table-list/ import { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column'; import { albumQueries } from '/@/renderer/features/albums/api/album-api'; import { AlbumInfiniteCarousel } from '/@/renderer/features/albums/components/album-infinite-carousel'; -import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller'; -import { usePlayer } from '/@/renderer/features/player/context/player-context'; import { LibraryBackgroundOverlay } from '/@/renderer/features/shared/components/library-background-overlay'; import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu'; -import { PlayButton } from '/@/renderer/features/shared/components/play-button'; import { searchLibraryItems } from '/@/renderer/features/shared/utils'; import { useContainerQuery } from '/@/renderer/hooks'; +import { useGenreRoute } from '/@/renderer/hooks/use-genre-route'; +import { AppRoute } from '/@/renderer/router/routes'; import { useCurrentServer } from '/@/renderer/store'; +import { useGeneralSettings, useSettingsStore } from '/@/renderer/store/settings.store'; import { - useGeneralSettings, - usePlayButtonBehavior, - useSettingsStore, -} from '/@/renderer/store/settings.store'; + formatDateAbsoluteUTC, + formatDurationString, + formatSizeString, + titleCase, +} from '/@/renderer/utils'; import { replaceURLWithHTMLLinks } from '/@/renderer/utils/linkify'; +import { normalizeReleaseTypes } from '/@/renderer/utils/normalize-release-types'; import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; import { Checkbox } from '/@/shared/components/checkbox/checkbox'; +import { Flex } from '/@/shared/components/flex/flex'; import { Group } from '/@/shared/components/group/group'; import { Icon } from '/@/shared/components/icon/icon'; +import { Pill, PillLink } from '/@/shared/components/pill/pill'; import { Spinner } from '/@/shared/components/spinner/spinner'; import { Spoiler } from '/@/shared/components/spoiler/spoiler'; import { Stack } from '/@/shared/components/stack/stack'; import { TextInput } from '/@/shared/components/text-input/text-input'; import { Text } from '/@/shared/components/text/text'; -import { AlbumListSort, LibraryItem, Song, SortOrder } from '/@/shared/types/domain-types'; +import { + Album, + AlbumListSort, + ExplicitStatus, + LibraryItem, + Song, + SortOrder, +} from '/@/shared/types/domain-types'; import { ItemListKey, ListDisplayType } from '/@/shared/types/types'; interface AlbumDetailContentProps { background?: string; } +interface AlbumMetadataTagsProps { + album: Album | undefined; +} + +const AlbumMetadataTags = ({ album }: AlbumMetadataTagsProps) => { + const { t } = useTranslation(); + + const metadataItems = useMemo(() => { + if (!album) return []; + + const originalDifferentFromRelease = + album.originalDate && album.originalDate !== album.releaseDate; + + const releasePrefix = originalDifferentFromRelease + ? t('page.albumDetail.released', { postProcess: 'sentenceCase' }) + : '♫'; + + const releaseTypes = normalizeReleaseTypes(album.releaseTypes ?? [], t).map((type) => ({ + id: type, + value: titleCase(type), + })); + + const items: Array<{ id: string; value: ReactNode | string | undefined }> = []; + + if (originalDifferentFromRelease && album.originalDate) { + items.push({ + id: 'originalDate', + value: `♫ ${formatDateAbsoluteUTC(album.originalDate)}`, + }); + } + + items.push(...releaseTypes); + + items.push( + { + id: 'releaseDate', + value: album.releaseDate + ? `${releasePrefix} ${formatDateAbsoluteUTC(album.releaseDate)}` + : undefined, + }, + { + id: 'releaseYear', + value: album.releaseYear?.toString(), + }, + { + id: 'songCount', + value: album.songCount + ? t('entity.trackWithCount', { + count: album.songCount, + }) + : undefined, + }, + { + id: 'duration', + value: album.duration ? ( + + {formatDurationString(album.duration)} + + ) : undefined, + }, + { + id: 'size', + value: album.size ? formatSizeString(album.size) : undefined, + }, + { + id: 'playCount', + value: + typeof album.playCount === 'number' + ? t('entity.play', { + count: album.playCount, + }) + : undefined, + }, + { + id: 'explicitStatus', + value: + album.explicitStatus === ExplicitStatus.EXPLICIT + ? t('common.explicit', { postProcess: 'sentenceCase' }) + : album.explicitStatus === ExplicitStatus.CLEAN + ? t('common.clean', { postProcess: 'sentenceCase' }) + : undefined, + }, + { + id: 'isCompilation', + value: + album.isCompilation !== null + ? t('filter.isCompilation', { postProcess: 'sentenceCase' }) + : undefined, + }, + { + id: 'recordLabels', + value: + album.recordLabels && album.recordLabels.length > 0 + ? album.recordLabels.join(', ') + : undefined, + }, + { + id: 'version', + value: album.version || undefined, + }, + ); + + return items.filter((item) => item.value); + }, [album, t]); + + if (metadataItems.length === 0) return null; + + return ( + + + {t('common.tags', { postProcess: 'sentenceCase' })} + + + {metadataItems.map((item, index) => ( + + {item.value} + + ))} + + + ); +}; + +interface AlbumMetadataGenresProps { + genres?: Array<{ id: string; name: string }>; +} + +const AlbumMetadataGenres = ({ genres }: AlbumMetadataGenresProps) => { + const { t } = useTranslation(); + const genreRoute = useGenreRoute(); + + if (!genres || genres.length === 0) return null; + + return ( + + + {t('entity.genre', { + count: genres.length, + })} + + + {genres.map((genre) => ( + + {genre.name} + + ))} + + + ); +}; + +interface AlbumMetadataArtistsProps { + artists?: Array<{ id: string; name: string }>; +} + +const AlbumMetadataArtists = ({ artists }: AlbumMetadataArtistsProps) => { + const { t } = useTranslation(); + + if (!artists || artists.length === 0) return null; + + return ( + + + {t('entity.albumArtist', { + count: artists.length, + })} + + + {artists.map((artist) => ( + + {artist.name} + + ))} + + + ); +}; + +interface AlbumMetadataExternalLinksProps { + albumArtist?: string; + albumName?: string; + externalLinks: boolean; + lastFM: boolean; + mbzId?: null | string; + musicBrainz: boolean; +} + +const AlbumMetadataExternalLinks = ({ + albumArtist, + albumName, + externalLinks, + lastFM, + mbzId, + musicBrainz, +}: AlbumMetadataExternalLinksProps) => { + const { t } = useTranslation(); + + if (!externalLinks || (!lastFM && !musicBrainz)) return null; + + return ( + + + {t('common.externalLinks', { + postProcess: 'sentenceCase', + })} + + + {lastFM && ( + + )} + {mbzId && musicBrainz ? ( + + ) : null} + + + ); +}; + export const AlbumDetailContent = ({ background }: AlbumDetailContentProps) => { const { t } = useTranslation(); const { albumId } = useParams() as { albumId: string }; @@ -89,22 +363,6 @@ export const AlbumDetailContent = ({ background }: AlbumDetailContentProps) => { uniqueId: 'relatedGenres', }, ]; - const playButtonBehavior = usePlayButtonBehavior(); - - const { addToQueueByFetch } = usePlayer(); - - const handlePlay = () => { - if (!server?.id) return; - addToQueueByFetch(server.id, [albumId], LibraryItem.ALBUM, playButtonBehavior); - }; - - const handleMoreOptions = (e: React.MouseEvent) => { - if (!detailQuery?.data) return; - ContextMenuController.call({ - cmd: { items: [detailQuery.data], type: LibraryItem.ALBUM }, - event: e, - }); - }; const comment = detailQuery?.data?.comment; @@ -114,75 +372,29 @@ export const AlbumDetailContent = ({ background }: AlbumDetailContentProps) => {
-
- - - - {replaceURLWithHTMLLinks(comment)}} +
+
+ + + + + - - -
- {externalLinks && (lastFM || musicBrainz) ? ( -
- - {lastFM && ( - - )} - {mbzId && musicBrainz ? ( - - ) : null} - -
- ) : null} - {comment && ( -
- {replaceURLWithHTMLLinks(comment)} -
- )} - - {detailQuery?.data?.songs && detailQuery.data.songs.length > 0 && ( -
- -
- )} + +
+ {detailQuery?.data?.songs && detailQuery.data.songs.length > 0 && ( +
+ +
+ )} +
{cq.height || cq.width ? ( @@ -338,18 +550,13 @@ const AlbumDetailSongsTable = ({ songs }: AlbumDetailSongsTableProps) => { }; return ( - + + {t('common.disc', { postProcess: 'sentenceCase' })}{' '} {discGroup.discNumber} {discGroup.discSubtitle && ` - ${discGroup.discSubtitle}`} diff --git a/src/renderer/features/albums/components/album-detail-header.tsx b/src/renderer/features/albums/components/album-detail-header.tsx index 67107207d..b0a527f90 100644 --- a/src/renderer/features/albums/components/album-detail-header.tsx +++ b/src/renderer/features/albums/components/album-detail-header.tsx @@ -1,23 +1,20 @@ import { useQuery } from '@tanstack/react-query'; -import { forwardRef, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { generatePath, Link, useParams } from 'react-router'; +import { forwardRef } from 'react'; +import { useParams } from 'react-router'; import { albumQueries } from '/@/renderer/features/albums/api/album-api'; +import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller'; import { usePlayer } from '/@/renderer/features/player/context/player-context'; -import { LibraryHeader } from '/@/renderer/features/shared/components/library-header'; -import { useGenreRoute } from '/@/renderer/hooks/use-genre-route'; +import { + LibraryHeader, + LibraryHeaderMenu, +} from '/@/renderer/features/shared/components/library-header'; import { AppRoute } from '/@/renderer/router/routes'; import { useCurrentServer } from '/@/renderer/store'; -import { formatDateAbsoluteUTC, formatDurationString, titleCase } from '/@/renderer/utils'; -import { normalizeReleaseTypes } from '/@/renderer/utils/normalize-release-types'; -import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; -import { Group } from '/@/shared/components/group/group'; -import { Pill, PillLink } from '/@/shared/components/pill/pill'; -import { Rating } from '/@/shared/components/rating/rating'; +import { usePlayButtonBehavior } from '/@/renderer/store/settings.store'; import { Stack } from '/@/shared/components/stack/stack'; -import { Text } from '/@/shared/components/text/text'; import { LibraryItem, ServerType } from '/@/shared/types/domain-types'; +import { Play } from '/@/shared/types/types'; interface AlbumDetailHeaderProps { background: { @@ -34,75 +31,13 @@ export const AlbumDetailHeader = forwardRef - normalizeReleaseTypes(detailQuery.data?.releaseTypes ?? [], t).map((type) => ({ - id: type, - value: titleCase(type), - })) || [], - [detailQuery.data?.releaseTypes, t], - ); - - const metadataItems = releaseTypes.concat([ - { - id: 'releaseDate', - value: - detailQuery?.data?.releaseDate && - `${releasePrefix} ${formatDateAbsoluteUTC(detailQuery?.data?.releaseDate)}`, - }, - { - id: 'songCount', - value: t('entity.trackWithCount', { - count: detailQuery?.data?.songCount as number, - }), - }, - { - id: 'duration', - value: - detailQuery?.data?.duration && formatDurationString(detailQuery.data.duration), - }, - { - id: 'playCount', - value: - typeof detailQuery?.data?.playCount === 'number' && - t('entity.play', { - count: detailQuery?.data?.playCount, - }), - }, - { - id: 'version', - value: detailQuery.data?.version, - }, - ]); - - if (originalDifferentFromRelease) { - const formatted = `♫ ${formatDateAbsoluteUTC(detailQuery!.data!.originalDate)}`; - metadataItems.splice(0, 0, { - id: 'originalDate', - value: formatted, - }); - } - - const { setFavorite, setRating } = usePlayer(); + const { addToQueueByFetch, setFavorite, setRating } = usePlayer(); + const playButtonBehavior = usePlayButtonBehavior(); const handleFavorite = () => { if (!detailQuery?.data) return; @@ -114,24 +49,39 @@ export const AlbumDetailHeader = forwardRef { + const handleUpdateRating = showRating + ? (rating: number) => { + if (!detailQuery?.data) return; + + if (detailQuery.data.userRating === rating) { + return setRating( + detailQuery.data._serverId, + [detailQuery.data.id], + LibraryItem.ALBUM, + 0, + ); + } + + return setRating( + detailQuery.data._serverId, + [detailQuery.data.id], + LibraryItem.ALBUM, + rating, + ); + } + : undefined; + + const handlePlay = (type?: Play) => { + if (!server?.id || !albumId) return; + addToQueueByFetch(server.id, [albumId], LibraryItem.ALBUM, type || playButtonBehavior); + }; + + const handleMoreOptions = (e: React.MouseEvent) => { if (!detailQuery?.data) return; - - if (detailQuery.data.userRating === rating) { - return setRating( - detailQuery.data._serverId, - [detailQuery.data.id], - LibraryItem.ALBUM, - 0, - ); - } - - return setRating( - detailQuery.data._serverId, - [detailQuery.data.id], - LibraryItem.ALBUM, - rating, - ); + ContextMenuController.call({ + cmd: { items: [detailQuery.data], type: LibraryItem.ALBUM }, + event: e, + }); }; return ( @@ -142,80 +92,15 @@ export const AlbumDetailHeader = forwardRef - - {metadataItems.map( - (item, index) => - item.value && ( - - {item.value} - - ), - )} - - {showGenres && ( - - {detailQuery?.data?.genres?.map((genre) => ( - - {genre.name} - - ))} - - )} - - - {showRating && ( - - )} - - - {detailQuery?.data?.albumArtists.map((artist) => ( - - {artist.name} - - ))} - + handlePlay(Play.NOW)} + onRating={handleUpdateRating} + onShuffle={() => handlePlay(Play.SHUFFLE)} + rating={detailQuery?.data?.userRating || 0} + /> ); diff --git a/src/renderer/features/albums/components/album-list-header.tsx b/src/renderer/features/albums/components/album-list-header.tsx index 6a40f41fd..ceabcb2b3 100644 --- a/src/renderer/features/albums/components/album-list-header.tsx +++ b/src/renderer/features/albums/components/album-list-header.tsx @@ -45,5 +45,11 @@ export const AlbumListHeader = ({ title }: AlbumListHeaderProps) => { const PlayButton = () => { const { query } = useAlbumListFilters(); - return ; + return ( + + ); }; diff --git a/src/renderer/features/albums/routes/album-detail-route.tsx b/src/renderer/features/albums/routes/album-detail-route.tsx index 34bcf115b..d4e3ee35f 100644 --- a/src/renderer/features/albums/routes/album-detail-route.tsx +++ b/src/renderer/features/albums/routes/album-detail-route.tsx @@ -47,6 +47,7 @@ const AlbumDetailRoute = () => { {detailQuery?.data?.name} diff --git a/src/renderer/features/shared/components/library-header-bar.tsx b/src/renderer/features/shared/components/library-header-bar.tsx index fc8aa392a..fd5271db6 100644 --- a/src/renderer/features/shared/components/library-header-bar.tsx +++ b/src/renderer/features/shared/components/library-header-bar.tsx @@ -28,6 +28,7 @@ interface HeaderPlayButtonProps { itemType: LibraryItem; listQuery?: Record; songs?: Song[]; + variant?: 'default' | 'filled'; } interface TitleProps { @@ -40,6 +41,7 @@ const HeaderPlayButton = ({ itemType, listQuery, songs, + variant = 'filled', ...props }: HeaderPlayButtonProps) => { const serverId = useCurrentServerId(); @@ -73,14 +75,19 @@ const HeaderPlayButton = ({ return (
- +
); }; const Title = ({ children }: TitleProps) => { return ( - + {children} ); diff --git a/src/renderer/features/shared/components/library-header.module.css b/src/renderer/features/shared/components/library-header.module.css index 28ce74d93..a8b3128e4 100644 --- a/src/renderer/features/shared/components/library-header.module.css +++ b/src/renderer/features/shared/components/library-header.module.css @@ -22,10 +22,8 @@ width: 250px !important; height: 250px; } -} -@container (min-width: 768px) { - .library-header { + @container (min-width: $mantine-breakpoint-sm) { grid-template-areas: 'image info'; grid-template-rows: auto; grid-template-columns: 225px minmax(0, 1fr); @@ -45,10 +43,8 @@ height: 225px; } } -} -@container (min-width: 1200px) { - .library-header { + @container (min-width: $mantine-breakpoint-lg) { grid-template-columns: 250px minmax(0, 1fr); .image { @@ -70,6 +66,10 @@ align-items: center; justify-content: center; filter: drop-shadow(0 0 8px rgb(0 0 0 / 50%)); + + @container (min-width: $mantine-breakpoint-sm) { + align-items: flex-end; + } } .metadata-section { @@ -81,25 +81,12 @@ align-items: center; justify-content: center; width: 100%; + height: 100%; text-align: center; - & > div { - align-items: center; - justify-content: center; - text-align: center; - } -} - -@container (min-width: 768px) { - .image-section { - align-items: flex-end; - } - - .metadata-section, - .metadata-section > div:first-of-type, - .metadata-section > div:last-of-type { + @container (min-width: $mantine-breakpoint-sm) { align-items: flex-start; - justify-content: flex-start; + justify-content: flex-end; text-align: left; } } @@ -146,3 +133,18 @@ color: var(--theme-colors-foreground); -webkit-box-orient: vertical; } + +.library-header-menu { + display: flex; + flex-direction: column; + gap: var(--theme-spacing-sm); + align-items: center; + justify-content: center; + width: 100%; + + @container (min-width: $mantine-breakpoint-sm) { + display: flex; + flex-direction: row; + justify-content: space-between; + } +} diff --git a/src/renderer/features/shared/components/library-header.tsx b/src/renderer/features/shared/components/library-header.tsx index 29224fb60..facfeb66d 100644 --- a/src/renderer/features/shared/components/library-header.tsx +++ b/src/renderer/features/shared/components/library-header.tsx @@ -7,9 +7,16 @@ import { Link } from 'react-router'; import styles from './library-header.module.css'; +import { + WidePlayButton, + WideShuffleButton, +} from '/@/renderer/features/shared/components/play-button'; import { useGeneralSettings } from '/@/renderer/store'; +import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; import { Center } from '/@/shared/components/center/center'; +import { Group } from '/@/shared/components/group/group'; import { Image } from '/@/shared/components/image/image'; +import { Rating } from '/@/shared/components/rating/rating'; import { Text } from '/@/shared/components/text/text'; import { LibraryItem } from '/@/shared/types/domain-types'; @@ -117,6 +124,7 @@ export const LibraryHeader = forwardRef( {title && (

- + {title}

@@ -138,3 +146,54 @@ export const LibraryHeader = forwardRef( ); }, ); + +interface LibraryHeaderMenuProps { + favorite?: boolean; + onFavorite?: (e: React.MouseEvent) => void; + onMore?: (e: React.MouseEvent) => void; + onPlay?: (e: React.MouseEvent) => void; + onRating?: (rating: number) => void; + onShuffle?: (e: React.MouseEvent) => void; + rating?: number; +} + +export const LibraryHeaderMenu = ({ + favorite, + onFavorite, + onMore, + onPlay, + onRating, + onShuffle, + rating, +}: LibraryHeaderMenuProps) => { + return ( +
+ + {onPlay && } + {onShuffle && } + + + {onRating && } + {onFavorite && ( + + )} + {onMore && ( + + )} + +
+ ); +}; diff --git a/src/renderer/features/shared/components/play-button.module.css b/src/renderer/features/shared/components/play-button.module.css index 54a4a1cdf..4b8112a5d 100644 --- a/src/renderer/features/shared/components/play-button.module.css +++ b/src/renderer/features/shared/components/play-button.module.css @@ -14,3 +14,80 @@ opacity: 0.6; } } + +.button.unthemed { + @mixin light { + color: white; + background: black; + + svg { + color: white; + fill: white; + } + + &:hover { + background: lighten(black, 10%); + } + } + + @mixin dark { + color: black; + background: white; + + svg { + color: black; + fill: black; + } + + &:hover { + background: darken(white, 20%); + } + } +} + +.wide-button { + padding-right: var(--theme-spacing-xl); + padding-left: var(--theme-spacing-xl); + background: white; + border-radius: var(--theme-radius-xl); + transition: background-color 0.2s ease-in-out; +} + +.wide-button.unthemed { + @mixin light { + background: black; + + svg { + color: white; + fill: white; + } + + &:hover { + background: lighten(black, 10%); + } + } + + @mixin dark { + background: white; + + svg { + color: black; + fill: black; + } + + &:hover { + background: darken(white, 20%); + } + } +} + +.wide-button-label { + font-size: var(--theme-font-size-md); + font-weight: 600; + color: black; + + svg { + color: black; + fill: black; + } +} diff --git a/src/renderer/features/shared/components/play-button.tsx b/src/renderer/features/shared/components/play-button.tsx index b3c90471f..17e6a24a5 100644 --- a/src/renderer/features/shared/components/play-button.tsx +++ b/src/renderer/features/shared/components/play-button.tsx @@ -1,23 +1,69 @@ import clsx from 'clsx'; +import { t } from 'i18next'; import styles from './play-button.module.css'; import { ActionIcon, ActionIconProps } from '/@/shared/components/action-icon/action-icon'; +import { Button, ButtonProps } from '/@/shared/components/button/button'; +import { Group } from '/@/shared/components/group/group'; +import { Icon } from '/@/shared/components/icon/icon'; export interface PlayButtonProps extends ActionIconProps { size?: number | string; } -export const PlayButton = ({ className, ...props }: PlayButtonProps) => { +export const PlayButton = ({ className, variant = 'filled', ...props }: PlayButtonProps) => { return ( ); }; + +interface WidePlayButtonProps extends ButtonProps {} + +export const WidePlayButton = ({ + className, + variant = 'default', + ...props +}: WidePlayButtonProps) => { + return ( + + ); +}; + +export const WideShuffleButton = ({ ...props }: WidePlayButtonProps) => { + return ( + + + + {t('action.shuffle', { postProcess: 'sentenceCase' })} + + + ); +}; diff --git a/src/shared/components/pill/pill.module.css b/src/shared/components/pill/pill.module.css index d89d62b48..dff3d568f 100644 --- a/src/shared/components/pill/pill.module.css +++ b/src/shared/components/pill/pill.module.css @@ -3,6 +3,7 @@ user-select: auto; background: alpha(var(--theme-colors-background), 0.5); border: 1px solid var(--theme-colors-border); + transition: background-color 0.2s ease-in-out; &[data-variant='outline'] { background: transparent; @@ -10,6 +11,10 @@ } } +.root.link:hover { + background: alpha(var(--theme-colors-surface), 0.5); +} + .label { font-family: var(--theme-content-font-family); } diff --git a/src/shared/components/pill/pill.tsx b/src/shared/components/pill/pill.tsx index a947cff15..20d353752 100644 --- a/src/shared/components/pill/pill.tsx +++ b/src/shared/components/pill/pill.tsx @@ -80,7 +80,7 @@ export const PillLink = forwardRef(({ children, . [styles.xs]: size === 'xs', }), remove: styles.remove, - root: styles.root, + root: clsx(styles.root, styles.link), ...classNames, }} component={Link} diff --git a/src/shared/components/rating/rating.module.css b/src/shared/components/rating/rating.module.css index ab508bc54..3fca3f698 100644 --- a/src/shared/components/rating/rating.module.css +++ b/src/shared/components/rating/rating.module.css @@ -1,26 +1,36 @@ .root.xs { --rating-size: var(--theme-font-size-xs) !important; + + padding: 0 var(--theme-spacing-xs); } .root.sm { --rating-size: var(--theme-font-size-sm) !important; + + padding: 0 var(--theme-spacing-sm); } .root.md { --rating-size: var(--theme-font-size-md) !important; + + padding: 0 var(--theme-spacing-md); } .root.lg { --rating-size: var(--theme-font-size-lg) !important; + + padding: 0 var(--theme-spacing-md); } .root.xl { --rating-size: var(--theme-font-size-xl) !important; + + padding: 0 var(--theme-spacing-md); } .symbol-body { svg { - stroke: alpha(var(--theme-colors-foreground), 0.7); + stroke: var(--theme-colors-foreground); stroke-width: 2px; &:not([data-filled='true']) {