improve loading state

This commit is contained in:
jeffvli
2026-02-09 12:43:19 -08:00
parent b4c45f0956
commit 9a2540f954
2 changed files with 123 additions and 38 deletions
@@ -104,6 +104,12 @@
padding: 0 var(--theme-spacing-md) var(--theme-spacing-xl) var(--theme-spacing-md); padding: 0 var(--theme-spacing-md) var(--theme-spacing-xl) var(--theme-spacing-md);
} }
/* Height constraint for skeleton columns; row grid gets two direct children (left, right) */
.skeleton-column-wrapper {
box-sizing: border-box;
min-width: 0;
}
.image-wrapper { .image-wrapper {
position: relative; position: relative;
display: block; display: block;
@@ -395,17 +401,29 @@
opacity: 0.7; opacity: 0.7;
} }
.skeleton-image-container {
justify-content: center;
}
.skeleton-image { .skeleton-image {
width: 100%; width: 100%;
aspect-ratio: 1; aspect-ratio: 1;
border-radius: var(--theme-radius-md); border-radius: var(--theme-radius-md);
} }
.skeleton-title-container {
justify-content: center;
}
.skeleton-title { .skeleton-title {
width: 75%; width: 75%;
height: 1.25rem; height: 1.25rem;
} }
.skeleton-artist-container {
justify-content: center;
}
.skeleton-artist { .skeleton-artist {
width: 50%; width: 50%;
height: 1rem; height: 1rem;
@@ -58,6 +58,10 @@ import { Text } from '/@/shared/components/text/text';
import { Album, LibraryItem, Song } from '/@/shared/types/domain-types'; import { Album, LibraryItem, Song } from '/@/shared/types/domain-types';
import { ItemListKey, TableColumn } from '/@/shared/types/types'; import { ItemListKey, TableColumn } from '/@/shared/types/types';
const DEFAULT_ROW_HEIGHT = 300;
const SKELETON_TRACK_ROW_COUNT = 6;
interface ItemDetailListProps { interface ItemDetailListProps {
currentPage?: number; currentPage?: number;
data?: unknown[]; data?: unknown[];
@@ -74,6 +78,7 @@ interface RowData {
columnWidthPercents: number[]; columnWidthPercents: number[];
controls?: ItemControls; controls?: ItemControls;
data: unknown[]; data: unknown[];
defaultRowHeight: number;
enableAlternateRowColors: boolean; enableAlternateRowColors: boolean;
enableHorizontalBorders: boolean; enableHorizontalBorders: boolean;
enableRowHoverHighlight: boolean; enableRowHoverHighlight: boolean;
@@ -96,7 +101,7 @@ interface TrackRowProps {
enableVerticalBorders: boolean; enableVerticalBorders: boolean;
internalState: ItemListStateActions; internalState: ItemListStateActions;
isMutatingFavorite: boolean; isMutatingFavorite: boolean;
onFavoriteClick: (song: Song) => void; isSongsLoading?: boolean;
rowIndex: number; rowIndex: number;
size: 'compact' | 'default' | 'large'; size: 'compact' | 'default' | 'large';
song: Song; song: Song;
@@ -116,7 +121,7 @@ const TrackRow = memo(
enableVerticalBorders, enableVerticalBorders,
internalState, internalState,
isMutatingFavorite, isMutatingFavorite,
onFavoriteClick, isSongsLoading,
rowIndex, rowIndex,
size, size,
song, song,
@@ -260,14 +265,13 @@ const TrackRow = memo(
song, song,
); );
const content = showHoverContent ? ( const content = isSongsLoading ? null : showHoverContent ? (
<CellComponent <CellComponent
columnId={col.id} columnId={col.id}
controls={controls} controls={controls}
internalState={internalState} internalState={internalState}
isMutatingFavorite={isMutatingFavorite} isMutatingFavorite={isMutatingFavorite}
isRowHovered={isRowHovered} isRowHovered={isRowHovered}
onFavoriteClick={onFavoriteClick}
rowIndex={rowIndex} rowIndex={rowIndex}
size={size} size={size}
song={song} song={song}
@@ -447,6 +451,88 @@ const MetadataSection = memo(
MetadataSection.displayName = 'MetadataSection'; MetadataSection.displayName = 'MetadataSection';
interface ItemDetailSkeletonRowProps {
defaultRowHeight: number;
enableAlternateRowColors: boolean;
enableHorizontalBorders: boolean;
enableVerticalBorders: boolean;
trackTableSize: 'compact' | 'default' | 'large';
}
const ItemDetailSkeletonRow = memo(
({
defaultRowHeight,
enableAlternateRowColors,
enableHorizontalBorders,
enableVerticalBorders,
trackTableSize,
}: ItemDetailSkeletonRowProps) => {
const heightStyle = {
height: defaultRowHeight,
minHeight: defaultRowHeight,
overflow: 'hidden' as const,
};
return (
<>
<div className={styles.skeletonColumnWrapper} style={heightStyle}>
<div className={styles.left}>
<div className={styles.metadata}>
<Skeleton
className={styles.skeletonImage}
containerClassName={styles.skeletonImageContainer}
/>
<Skeleton
className={styles.skeletonTitle}
containerClassName={styles.skeletonTitleContainer}
/>
<Skeleton
className={styles.skeletonArtist}
containerClassName={styles.skeletonArtistContainer}
/>
</div>
</div>
</div>
<div className={styles.skeletonColumnWrapper} style={heightStyle}>
<div className={styles.right}>
<div className={styles.tracksTable} role="table">
{Array.from({ length: SKELETON_TRACK_ROW_COUNT }).map((_, i) => (
<div
className={clsx(styles.trackRow, {
[styles.trackRowAlternateEven]:
enableAlternateRowColors && i % 2 === 0,
[styles.trackRowAlternateOdd]:
enableAlternateRowColors && i % 2 === 1,
[styles.trackRowHorizontalBorderVisible]:
enableHorizontalBorders && i > 0,
[styles.trackRowSizeCompact]: trackTableSize === 'compact',
[styles.trackRowSizeDefault]: trackTableSize === 'default',
[styles.trackRowSizeLarge]: trackTableSize === 'large',
[styles.trackRowWithHorizontalBorder]: i > 0,
})}
key={i}
role="row"
>
<div
className={clsx(styles.trackCell, {
[styles.trackCellVerticalBorderVisible]:
enableVerticalBorders,
[styles.trackCellWithVerticalBorder]: true,
})}
role="cell"
style={{ flex: 1, minWidth: 0 }}
/>
</div>
))}
</div>
</div>
</div>
</>
);
},
);
ItemDetailSkeletonRow.displayName = 'ItemDetailSkeletonRow';
type RowContentProps = Omit<RowComponentProps<RowData>, 'style'>; type RowContentProps = Omit<RowComponentProps<RowData>, 'style'>;
const RowContent = memo( const RowContent = memo(
@@ -454,6 +540,7 @@ const RowContent = memo(
columnWidthPercents, columnWidthPercents,
controls, controls,
data, data,
defaultRowHeight,
enableAlternateRowColors, enableAlternateRowColors,
enableHorizontalBorders, enableHorizontalBorders,
enableRowHoverHighlight, enableRowHoverHighlight,
@@ -474,7 +561,7 @@ const RowContent = memo(
return (data?.[index] as Album | undefined) || undefined; return (data?.[index] as Album | undefined) || undefined;
}, [data, getItem, index]); }, [data, getItem, index]);
const { data: songData } = useQuery({ const { data: songData, isLoading: isSongsQueryLoading } = useQuery({
enabled: !!item && !!item.id, enabled: !!item && !!item.id,
...albumQueries.detail({ ...albumQueries.detail({
query: { query: {
@@ -484,6 +571,8 @@ const RowContent = memo(
}), }),
}); });
const isSongsLoading = !!item && isSongsQueryLoading && !songData;
const songs = useMemo(() => { const songs = useMemo(() => {
return ( return (
songData?.songs || songData?.songs ||
@@ -502,39 +591,15 @@ const RowContent = memo(
} }
}, [item?.id, registerSongs, songData?.songs]); }, [item?.id, registerSongs, songData?.songs]);
const onFavoriteClick = useCallback((song: Song) => {
// TODO: toggle favorite for song
void song;
}, []);
if (!item) { if (!item) {
return ( return (
<> <ItemDetailSkeletonRow
<div className={styles.left}> defaultRowHeight={defaultRowHeight}
<div className={styles.metadata}> enableAlternateRowColors={enableAlternateRowColors}
<Skeleton className={styles.skeletonImage} /> enableHorizontalBorders={enableHorizontalBorders}
<Skeleton className={styles.skeletonTitle} /> enableVerticalBorders={enableVerticalBorders}
<Skeleton className={styles.skeletonArtist} /> trackTableSize={trackTableSize}
</div> />
</div>
<div className={styles.right}>
<div
className={clsx(styles.skeletonTracks, {
[styles.skeletonTracksSizeCompact]: trackTableSize === 'compact',
[styles.skeletonTracksSizeDefault]: trackTableSize === 'default',
[styles.skeletonTracksSizeLarge]: trackTableSize === 'large',
})}
>
{Array.from({ length: 10 }).map((_, i) => (
<div className={styles.skeletonTrackRow} key={i}>
<Skeleton className={styles.skeletonTrackCell} />
<Skeleton className={styles.skeletonTrackCellTitle} />
<Skeleton className={styles.skeletonTrackCell} />
</div>
))}
</div>
</div>
</>
); );
} }
@@ -561,8 +626,8 @@ const RowContent = memo(
enableVerticalBorders={enableVerticalBorders} enableVerticalBorders={enableVerticalBorders}
internalState={internalState} internalState={internalState}
isMutatingFavorite={isMutatingFavorite} isMutatingFavorite={isMutatingFavorite}
isSongsLoading={isSongsLoading}
key={song.id} key={song.id}
onFavoriteClick={onFavoriteClick}
rowIndex={rowIndex} rowIndex={rowIndex}
size={trackTableSize} size={trackTableSize}
song={song as Song} song={song as Song}
@@ -577,6 +642,7 @@ const RowContent = memo(
prev.index === next.index && prev.index === next.index &&
prev.data === next.data && prev.data === next.data &&
prev.columnWidthPercents === next.columnWidthPercents && prev.columnWidthPercents === next.columnWidthPercents &&
prev.defaultRowHeight === next.defaultRowHeight &&
prev.enableAlternateRowColors === next.enableAlternateRowColors && prev.enableAlternateRowColors === next.enableAlternateRowColors &&
prev.enableHorizontalBorders === next.enableHorizontalBorders && prev.enableHorizontalBorders === next.enableHorizontalBorders &&
prev.enableRowHoverHighlight === next.enableRowHoverHighlight && prev.enableRowHoverHighlight === next.enableRowHoverHighlight &&
@@ -706,7 +772,7 @@ export const ItemDetailList = ({
const isMutatingFavorite = isMutatingCreateFavorite || isMutatingDeleteFavorite; const isMutatingFavorite = isMutatingCreateFavorite || isMutatingDeleteFavorite;
const rowHeight = useDynamicRowHeight({ const rowHeight = useDynamicRowHeight({
defaultRowHeight: 300, defaultRowHeight: DEFAULT_ROW_HEIGHT,
}); });
const isInfinite = data !== undefined || getItem !== undefined; const isInfinite = data !== undefined || getItem !== undefined;
@@ -817,6 +883,7 @@ export const ItemDetailList = ({
columnWidthPercents, columnWidthPercents,
controls, controls,
data: dataSource, data: dataSource,
defaultRowHeight: DEFAULT_ROW_HEIGHT,
enableAlternateRowColors, enableAlternateRowColors,
enableHorizontalBorders, enableHorizontalBorders,
enableRowHoverHighlight, enableRowHoverHighlight,