mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
feat(dashboard): add containedHeight prop for flexible layout adjustments
- Introduced a new `containedHeight` prop in `PageLayout` to manage height behavior. - Updated `SidebarLayout` and `PageClient` components to utilize `containedHeight` for improved layout control. - Enhanced styling to ensure proper flex behavior based on the new prop, allowing for better content overflow management.
This commit is contained in:
parent
22e17c9dd4
commit
d5c3ae3f30
@ -9,6 +9,7 @@ export function PageLayout(props: {
|
||||
fillWidth?: boolean,
|
||||
noPadding?: boolean,
|
||||
allowContentOverflow?: boolean,
|
||||
containedHeight?: boolean,
|
||||
fullBleed?: boolean,
|
||||
wrapHeaderInCard?: boolean,
|
||||
} & ({
|
||||
@ -19,6 +20,7 @@ export function PageLayout(props: {
|
||||
return (
|
||||
<div
|
||||
className={cn("flex flex-1 min-h-0 flex-col", !props.noPadding && "py-4 px-4 sm:py-6 sm:px-6")}
|
||||
data-contained-height={props.containedHeight ? "true" : undefined}
|
||||
data-full-bleed={props.fullBleed ? "true" : undefined}
|
||||
>
|
||||
<div
|
||||
|
||||
@ -45,6 +45,8 @@ const BACKGROUND_CHUNK_BATCH = 50;
|
||||
const EXTRA_TABS_TO_SHOW = 2;
|
||||
const REPLAY_SETTINGS_STORAGE_KEY = "stack.session-replay.settings";
|
||||
const LEGACY_PLAYER_SPEED_STORAGE_KEY = "stack.session-replay.speed";
|
||||
// TEMPORARY: multiply the rendered list for scroll verification. Set back to 1 after manual verification.
|
||||
const SESSION_REPLAY_LIST_DEBUG_MULTIPLIER: number = 100;
|
||||
|
||||
type RrwebEventWithTime = import("rrweb/typings/types").eventWithTime;
|
||||
type RrwebReplayer = InstanceType<typeof import("rrweb").Replayer>;
|
||||
@ -62,6 +64,11 @@ type RecordingRow = {
|
||||
eventCount: number,
|
||||
};
|
||||
|
||||
type RecordingListRow = {
|
||||
replay: RecordingRow,
|
||||
key: string,
|
||||
};
|
||||
|
||||
type ChunkRow = {
|
||||
id: string,
|
||||
batchId: string,
|
||||
@ -505,6 +512,19 @@ export default function PageClient({ initialReplayId, lockedUserId }: PageClient
|
||||
?? (standaloneReplay?.id === selectedRecordingId ? standaloneReplay : null),
|
||||
[recordings, selectedRecordingId, standaloneReplay],
|
||||
);
|
||||
const recordingListRows = useMemo<RecordingListRow[]>(() => {
|
||||
if (SESSION_REPLAY_LIST_DEBUG_MULTIPLIER === 1) {
|
||||
return recordings.map((replay) => ({ replay, key: replay.id }));
|
||||
}
|
||||
|
||||
const rows: RecordingListRow[] = [];
|
||||
for (let copyIndex = 0; copyIndex < SESSION_REPLAY_LIST_DEBUG_MULTIPLIER; copyIndex++) {
|
||||
for (const replay of recordings) {
|
||||
rows.push({ replay, key: `${replay.id}:${copyIndex}` });
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}, [recordings]);
|
||||
|
||||
const hasAutoSelectedRef = useRef(false);
|
||||
const loadingMoreRef = useRef(false);
|
||||
@ -1464,17 +1484,18 @@ export default function PageClient({ initialReplayId, lockedUserId }: PageClient
|
||||
) : undefined}
|
||||
fillWidth
|
||||
noPadding={isEmbedded}
|
||||
containedHeight
|
||||
>
|
||||
<SessionReplayLimitBanner />
|
||||
<PanelGroup data-walkthrough="analytics-replays" direction="horizontal" className="flex-1 min-h-[520px] rounded-xl border border-border/40 overflow-hidden bg-background">
|
||||
{!isStandaloneReplayPage && (
|
||||
<>
|
||||
<Panel defaultSize={25} minSize={16}>
|
||||
<div className="h-full flex flex-col">
|
||||
<Panel defaultSize={25} minSize={16} className="min-h-0">
|
||||
<div className="h-full min-h-0 flex flex-col overflow-hidden">
|
||||
<div className="shrink-0 px-3 py-2 border-b border-border/30 space-y-2">
|
||||
<div className="flex items-center justify-between gap-2 h-8">
|
||||
<Typography className="text-sm font-medium">
|
||||
Sessions{!loadingInitial && recordings.length > 0 ? ` (${recordings.length}${nextCursor ? "+" : ""})` : ""}
|
||||
Sessions{!loadingInitial && recordingListRows.length > 0 ? ` (${recordingListRows.length}${nextCursor ? "+" : ""})` : ""}
|
||||
</Typography>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@ -1750,7 +1771,7 @@ export default function PageClient({ initialReplayId, lockedUserId }: PageClient
|
||||
<div
|
||||
ref={listBoxRef}
|
||||
onScroll={onListScroll}
|
||||
className="flex-1 overflow-y-auto"
|
||||
className="flex-1 min-h-0 overflow-y-auto"
|
||||
>
|
||||
{loadingInitial ? (
|
||||
<div className="p-2 space-y-1">
|
||||
@ -1769,13 +1790,13 @@ export default function PageClient({ initialReplayId, lockedUserId }: PageClient
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-1.5 space-y-0.5">
|
||||
{recordings.map((r) => {
|
||||
{recordingListRows.map(({ replay: r, key }) => {
|
||||
const isSelected = r.id === selectedRecordingId;
|
||||
const durationMs = r.lastEventAt.getTime() - r.startedAt.getTime();
|
||||
const duration = formatDurationMs(durationMs);
|
||||
return (
|
||||
<div
|
||||
key={r.id}
|
||||
key={key}
|
||||
className={cn(
|
||||
"rounded-lg",
|
||||
isSelected ? "bg-muted/60 ring-1 ring-border/40" : "hover:bg-muted/20",
|
||||
@ -1826,8 +1847,8 @@ export default function PageClient({ initialReplayId, lockedUserId }: PageClient
|
||||
</>
|
||||
)}
|
||||
|
||||
<Panel defaultSize={isStandaloneReplayPage ? 100 : 75} minSize={35}>
|
||||
<div className="h-full flex flex-col">
|
||||
<Panel defaultSize={isStandaloneReplayPage ? 100 : 75} minSize={35} className="min-h-0">
|
||||
<div className="h-full min-h-0 flex flex-col overflow-hidden">
|
||||
{(standaloneReplayError || ms.downloadError || ms.playerError) && (
|
||||
<div className="p-3 space-y-2">
|
||||
{standaloneReplayError && <Alert variant="destructive">{standaloneReplayError}</Alert>}
|
||||
@ -1881,10 +1902,10 @@ export default function PageClient({ initialReplayId, lockedUserId }: PageClient
|
||||
</div>
|
||||
|
||||
{selectedRecordingId ? (
|
||||
<div className="flex-1 overflow-hidden flex flex-col">
|
||||
<div className="flex-1 overflow-hidden flex flex-col">
|
||||
<div className="flex-1 min-h-0 overflow-hidden flex flex-col">
|
||||
<div className="flex-1 min-h-0 overflow-hidden flex flex-col">
|
||||
<div
|
||||
className="flex-1 overflow-hidden grid grid-cols-[minmax(0,1fr)_260px] gap-px bg-border/40"
|
||||
className="flex-1 min-h-0 overflow-hidden grid grid-cols-[minmax(0,1fr)_260px] gap-px bg-border/40"
|
||||
style={{
|
||||
gridTemplateColumns: showRightColumn ? "minmax(0, 1fr) 260px" : "minmax(0, 1fr) 0px",
|
||||
gridTemplateRows: showRightColumn ? `repeat(${EXTRA_TABS_TO_SHOW}, auto) 1fr` : "1fr",
|
||||
|
||||
@ -737,7 +737,7 @@ export default function SidebarLayout(props: { children?: React.ReactNode }) {
|
||||
<SpotlightSearchWrapper projectId={projectId} />
|
||||
|
||||
{/* Body Layout (Left Sidebar + Content + Right Companion) */}
|
||||
<div className="relative flex flex-1 items-start w-full">
|
||||
<div className="relative flex flex-1 items-start w-full has-[[data-contained-height]]:min-h-0 has-[[data-contained-height]]:items-stretch">
|
||||
{/* Left Sidebar - Sticky */}
|
||||
<aside
|
||||
className={cn(
|
||||
@ -753,13 +753,15 @@ export default function SidebarLayout(props: { children?: React.ReactNode }) {
|
||||
</aside>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<main className="flex-1 min-w-0 pt-1 pb-3 px-3 lg:pl-0 dark:py-0 dark:px-2 dark:pb-3">
|
||||
<main className="flex-1 min-w-0 pt-1 pb-3 px-3 lg:pl-0 dark:py-0 dark:px-2 dark:pb-3 has-[[data-contained-height]]:flex has-[[data-contained-height]]:min-h-0 has-[[data-contained-height]]:flex-col">
|
||||
<div className={cn(
|
||||
"relative flex min-w-0 flex-col overflow-visible has-[[data-full-bleed]]:h-full",
|
||||
// Light mode card styling
|
||||
"min-h-[calc(100vh-4.5rem)] bg-white/80 backdrop-blur-xl shadow-[0_4px_24px_rgba(0,0,0,0.06),0_1px_4px_rgba(0,0,0,0.04)] rounded-2xl border border-black/[0.06] lg:pr-20",
|
||||
// Dark mode: remove card styling
|
||||
"dark:bg-transparent dark:backdrop-blur-none dark:shadow-none dark:rounded-none dark:border-0 dark:lg:pr-20",
|
||||
// Contained pages own their internal scroll regions, so the shell must pass down a finite flex height instead of sizing to content.
|
||||
"has-[[data-contained-height]]:flex-1 has-[[data-contained-height]]:min-h-0 has-[[data-contained-height]]:overflow-hidden",
|
||||
// Full-bleed pages (email editors etc.): remove card styling in light mode too (keep lg:pr-20 for companion space)
|
||||
"has-[[data-full-bleed]]:min-h-0 has-[[data-full-bleed]]:bg-transparent has-[[data-full-bleed]]:backdrop-blur-none has-[[data-full-bleed]]:shadow-none has-[[data-full-bleed]]:rounded-none has-[[data-full-bleed]]:border-0",
|
||||
)}>
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { cn, Spinner, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@hexclave/ui";
|
||||
import { TooltipPortal } from "@radix-ui/react-tooltip";
|
||||
import { cn, Spinner, Tooltip, TooltipContent, TooltipPortal, TooltipProvider, TooltipTrigger } from "@hexclave/ui";
|
||||
import { runAsynchronouslyWithAlert } from "@hexclave/shared/dist/utils/promises";
|
||||
import { useGlassmorphicDefault } from "./card";
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user