refactor(web): share screen fade-up animation with the monitoring column
Extract the primary pane's per-screen enter/exit transition into a shared `ScreenStage` element (the `fade-up` Mantine Transition: incoming screen slides up + fades in while the outgoing fades down + out, both absolutely positioned). InspectorView now imports it (behavior unchanged), and the pinned monitoring column uses it too — via a new `fill` variant that stretches the stage to the column's height — so switching Logs/Protocol/Network animates the same way as switching the main screens. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
This commit is contained in:
parent
5d024ab54f
commit
13a1d774de
4 changed files with 105 additions and 35 deletions
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
|
||||||
|
import { ScreenStage } from "./ScreenStage";
|
||||||
|
|
||||||
|
describe("ScreenStage", () => {
|
||||||
|
it("renders its screen when active", () => {
|
||||||
|
renderWithMantine(
|
||||||
|
<ScreenStage active>
|
||||||
|
<div>active screen</div>
|
||||||
|
</ScreenStage>,
|
||||||
|
);
|
||||||
|
expect(screen.getByText("active screen")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders nothing when inactive (outgoing screen unmounts)", () => {
|
||||||
|
renderWithMantine(
|
||||||
|
<ScreenStage active={false}>
|
||||||
|
<div>inactive screen</div>
|
||||||
|
</ScreenStage>,
|
||||||
|
);
|
||||||
|
expect(screen.queryByText("inactive screen")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still renders its screen in the fill variant", () => {
|
||||||
|
renderWithMantine(
|
||||||
|
<ScreenStage active fill>
|
||||||
|
<div>filled screen</div>
|
||||||
|
</ScreenStage>,
|
||||||
|
);
|
||||||
|
expect(screen.getByText("filled screen")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { Box, Transition } from "@mantine/core";
|
||||||
|
|
||||||
|
/** Screen enter / exit durations for the shared `fade-up` stage transition. */
|
||||||
|
export const SCREEN_ENTER_MS = 350;
|
||||||
|
export const SCREEN_EXIT_MS = 250;
|
||||||
|
|
||||||
|
export interface ScreenStageProps {
|
||||||
|
/** True when this stage's screen is the active one. */
|
||||||
|
active: boolean;
|
||||||
|
children: ReactNode;
|
||||||
|
/**
|
||||||
|
* Stretch the stage to fill its relative-positioned parent (adds `bottom: 0`).
|
||||||
|
* Needed where the screen relies on the parent for height (e.g. an inner
|
||||||
|
* ScrollArea in the monitoring column). Off by default so callers whose
|
||||||
|
* screens size themselves keep the top/left/right anchoring.
|
||||||
|
*/
|
||||||
|
fill?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps a screen in a Mantine `fade-up` Transition so that, on switch, the
|
||||||
|
* incoming screen slides up and fades in while the outgoing one fades down and
|
||||||
|
* out — both mounted at once via absolute positioning. With Transition's default
|
||||||
|
* (`keepMounted={false}`) the outgoing screen unmounts after its exit animation,
|
||||||
|
* resetting any local screen state (search filters, scroll, expanded sections).
|
||||||
|
*
|
||||||
|
* Shared by the primary InspectorView pane and the pinned monitoring column so
|
||||||
|
* both use identical enter/exit motion (#1639-follow-up). Must be rendered
|
||||||
|
* inside a `position: relative` container.
|
||||||
|
*/
|
||||||
|
export function ScreenStage({
|
||||||
|
active,
|
||||||
|
children,
|
||||||
|
fill = false,
|
||||||
|
}: ScreenStageProps) {
|
||||||
|
return (
|
||||||
|
<Transition
|
||||||
|
mounted={active}
|
||||||
|
transition="fade-up"
|
||||||
|
duration={SCREEN_ENTER_MS}
|
||||||
|
exitDuration={SCREEN_EXIT_MS}
|
||||||
|
timingFunction="ease"
|
||||||
|
>
|
||||||
|
{(styles) => (
|
||||||
|
// `style={styles}` is the runtime transition state from Mantine's
|
||||||
|
// Transition API — interpolated values, not static styling.
|
||||||
|
<Box
|
||||||
|
style={styles}
|
||||||
|
pos="absolute"
|
||||||
|
top={0}
|
||||||
|
left={0}
|
||||||
|
right={0}
|
||||||
|
bottom={fill ? 0 : undefined}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Transition>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { Divider, Stack } from "@mantine/core";
|
import { Divider, Stack } from "@mantine/core";
|
||||||
import { MonitoringControls } from "../MonitoringControls/MonitoringControls";
|
import { MonitoringControls } from "../MonitoringControls/MonitoringControls";
|
||||||
|
import { ScreenStage } from "../../elements/ScreenStage/ScreenStage";
|
||||||
|
|
||||||
export interface MonitoringScreenProps {
|
export interface MonitoringScreenProps {
|
||||||
/** Available monitor tabs (Logs/Protocol/Network, filtered by capability). */
|
/** Available monitor tabs (Logs/Protocol/Network, filtered by capability). */
|
||||||
|
|
@ -26,7 +27,10 @@ const ColumnLayout = Stack.withProps({
|
||||||
gap: 0,
|
gap: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Relative-positioned host so each tab's `ScreenStage` (absolutely positioned)
|
||||||
|
// can cross-fade over it. `mih: 0` lets the inner ScrollArea bound its height.
|
||||||
const ScreenSlot = Stack.withProps({
|
const ScreenSlot = Stack.withProps({
|
||||||
|
pos: "relative",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
mih: 0,
|
mih: 0,
|
||||||
gap: 0,
|
gap: 0,
|
||||||
|
|
@ -57,7 +61,13 @@ export function MonitoringScreen({
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
<Divider />
|
<Divider />
|
||||||
<ScreenSlot>{screens[value]}</ScreenSlot>
|
<ScreenSlot>
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<ScreenStage key={tab} active={tab === value} fill>
|
||||||
|
{screens[tab]}
|
||||||
|
</ScreenStage>
|
||||||
|
))}
|
||||||
|
</ScreenSlot>
|
||||||
</ColumnLayout>
|
</ColumnLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import {
|
||||||
} from "react";
|
} from "react";
|
||||||
import {
|
import {
|
||||||
AppShell,
|
AppShell,
|
||||||
Box,
|
|
||||||
Flex,
|
Flex,
|
||||||
Group,
|
Group,
|
||||||
Stack,
|
Stack,
|
||||||
|
|
@ -84,6 +83,7 @@ import {
|
||||||
type ConsoleUiState,
|
type ConsoleUiState,
|
||||||
} from "../../screens/ConsoleScreen/ConsoleScreen";
|
} from "../../screens/ConsoleScreen/ConsoleScreen";
|
||||||
import type { SortDirection } from "../../elements/SortToggle/SortToggle";
|
import type { SortDirection } from "../../elements/SortToggle/SortToggle";
|
||||||
|
import { ScreenStage } from "../../elements/ScreenStage/ScreenStage";
|
||||||
import { MonitoringScreen } from "../../groups/MonitoringScreen/MonitoringScreen";
|
import { MonitoringScreen } from "../../groups/MonitoringScreen/MonitoringScreen";
|
||||||
import { ResizeHandle } from "../../elements/ResizeHandle/ResizeHandle";
|
import { ResizeHandle } from "../../elements/ResizeHandle/ResizeHandle";
|
||||||
import { getServerType } from "@inspector/core/mcp/config.js";
|
import { getServerType } from "@inspector/core/mcp/config.js";
|
||||||
|
|
@ -241,9 +241,6 @@ function serializeMonitorWidth(value: number): string {
|
||||||
return String(value);
|
return String(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const SCREEN_ENTER_MS = 350;
|
|
||||||
const SCREEN_EXIT_MS = 250;
|
|
||||||
|
|
||||||
// Relative-positioned wrapper for the absolutely-positioned `ScreenStage`
|
// Relative-positioned wrapper for the absolutely-positioned `ScreenStage`
|
||||||
// children. `mih: "100%"` requires `AppShell.Main` to provide a definite
|
// children. `mih: "100%"` requires `AppShell.Main` to provide a definite
|
||||||
// height for the stack itself to fill — the absolute children render
|
// height for the stack itself to fill — the absolute children render
|
||||||
|
|
@ -306,36 +303,6 @@ const MonitorColumnGroup = Group.withProps({
|
||||||
flex: "0 0 auto",
|
flex: "0 0 auto",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wraps each screen in a Mantine Transition. With Transition's default
|
|
||||||
// (`keepMounted={false}`), the outgoing screen unmounts after its exit
|
|
||||||
// animation — any local screen state (search filters, scroll position,
|
|
||||||
// expanded sections) is reset on tab switch.
|
|
||||||
function ScreenStage({
|
|
||||||
active,
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
active: boolean;
|
|
||||||
children: ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Transition
|
|
||||||
mounted={active}
|
|
||||||
transition="fade-up"
|
|
||||||
duration={SCREEN_ENTER_MS}
|
|
||||||
exitDuration={SCREEN_EXIT_MS}
|
|
||||||
timingFunction="ease"
|
|
||||||
>
|
|
||||||
{(styles) => (
|
|
||||||
// `style={styles}` is the runtime transition state from Mantine's
|
|
||||||
// Transition API — these are interpolated values, not static styling.
|
|
||||||
<Box style={styles} pos="absolute" top={0} left={0} right={0}>
|
|
||||||
{children}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Transition>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InspectorViewProps {
|
export interface InspectorViewProps {
|
||||||
// Server list (static config; runtime connection state comes from the
|
// Server list (static config; runtime connection state comes from the
|
||||||
// separate fields below and is merged into each card by this component).
|
// separate fields below and is merged into each card by this component).
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue