feat: paginate stream page indicator

Closes #221
This commit is contained in:
Arjun Patel
2026-05-28 10:12:41 -07:00
parent 563c91e7d5
commit e5d1349ad7
2 changed files with 169 additions and 47 deletions
@@ -7,6 +7,7 @@ import {
import type { HumanPresence } from "@/hooks/use-presence-positions";
const MAX_VISIBLE_AVATARS = 3;
const PAGE_SIZE = 10;
interface PlaybackPageIndicatorProps {
total: number;
@@ -34,47 +35,116 @@ export function PlaybackPageIndicator({
const showAvatars = layer !== "tracks";
const showTracks = layer !== "avatars";
const paginated = total > PAGE_SIZE;
const safeCurrent = current < 0 ? 0 : current;
const pageStart = paginated
? Math.floor(safeCurrent / PAGE_SIZE) * PAGE_SIZE
: 0;
const visibleCount = paginated
? Math.min(PAGE_SIZE, total - pageStart)
: total;
const hasPrevPage = paginated && pageStart > 0;
const hasNextPage = paginated && pageStart + PAGE_SIZE < total;
return (
<div className="flex w-full items-end gap-px leading-none">
{Array.from({ length: total }, (_, i) => {
const presence = presenceBySegment?.get(i);
return (
<div key={i} className="flex flex-1 flex-col items-stretch">
{showAvatars && presence && presence.length > 0 && (
<SegmentPresenceAvatars presence={presence} onlineHumanIds={onlineHumanIds} />
)}
{showTracks && (
<button
onClick={(e) => {
e.stopPropagation();
onGoTo(i);
}}
className="group relative block h-3 w-full"
>
{/* Dim track */}
<div className="absolute inset-x-0 bottom-0 h-[3px] bg-white/30 transition-all group-hover:h-1.5" />
{/* Fill */}
<div
className="absolute left-0 bottom-0 h-[3px] bg-white/90 transition-all group-hover:h-1.5"
style={{
width:
i < current
? "100%"
: i === current
? `${progress * 100}%`
: "0%",
transition: i === current ? "width 300ms linear" : "none",
}}
/>
</button>
)}
</div>
);
})}
<div className="flex w-full flex-col items-stretch leading-none">
<div className="flex w-full items-end gap-px">
{paginated && (
<GhostStub
visible={hasPrevPage}
interactive={showTracks}
onClick={() => onGoTo(pageStart - 1)}
/>
)}
<div className="flex flex-1 items-end gap-px">
{Array.from({ length: visibleCount }, (_, j) => {
const i = pageStart + j;
const presence = presenceBySegment?.get(i);
return (
<div key={i} className="flex flex-1 flex-col items-stretch">
{showAvatars && presence && presence.length > 0 && (
<SegmentPresenceAvatars
presence={presence}
onlineHumanIds={onlineHumanIds}
/>
)}
{showTracks && (
<button
onClick={(e) => {
e.stopPropagation();
onGoTo(i);
}}
className="group relative block h-3 w-full"
>
{/* Dim track */}
<div className="absolute inset-x-0 bottom-0 h-[3px] bg-white/30 transition-all group-hover:h-1.5" />
{/* Fill */}
<div
className="absolute left-0 bottom-0 h-[3px] bg-white/90 transition-all group-hover:h-1.5"
style={{
width:
i < current
? "100%"
: i === current
? `${progress * 100}%`
: "0%",
transition: i === current ? "width 300ms linear" : "none",
}}
/>
</button>
)}
</div>
);
})}
</div>
{paginated && (
<GhostStub
visible={hasNextPage}
interactive={showTracks}
onClick={() => onGoTo(pageStart + PAGE_SIZE)}
/>
)}
</div>
{paginated && showTracks && current >= 0 && (
<div className="pointer-events-none pt-1 text-center text-[10px] font-medium tabular-nums tracking-wide text-white/40">
{current + 1} / {total}
</div>
)}
</div>
);
}
function GhostStub({
visible,
interactive,
onClick,
}: {
visible: boolean;
interactive: boolean;
onClick: () => void;
}) {
// Always reserve width so segments don't shift when stubs appear/disappear.
if (!visible) {
return <div className="h-3 w-2 shrink-0" aria-hidden />;
}
if (!interactive) {
// Avatars layer: invisible spacer matching the tracks layer's stub width.
return <div className="h-3 w-2 shrink-0" aria-hidden />;
}
return (
<button
onClick={(e) => {
e.stopPropagation();
onClick();
}}
className="group relative block h-3 w-2 shrink-0"
aria-label="Jump to adjacent page"
>
<div className="absolute inset-x-0 bottom-0 h-[3px] bg-white/15 transition-all group-hover:h-1.5 group-hover:bg-white/40" />
</button>
);
}
function SegmentPresenceAvatars({
presence,
onlineHumanIds,