Skip to content
Source
Sign inSign up

inspector-panes.ts

49 lines1.9 KB

1import { useEffect, useState, type ReactNode } from "react";
2​
3export interface InspectorPane {
4 readonly key: string;
5 readonly render: () => ReactNode;
6 /** Opening one of these panes soon mounts the others too, so switching between them is instant. */
7 readonly prewarm?: boolean;
8}
9​
10/** What a route shows in the workspace's inspector: its panes and the one on screen. */
11export interface InspectorContent {
12 readonly panes: ReadonlyArray<InspectorPane>;
13 readonly active: string;
14 /** A failed pane renders again when one of these changes. */
15 readonly resetKeys?: ReadonlyArray<unknown>;
17​
18const PREWARM_DELAY_MS = 350;
19​
20/**
21 * The panes to keep mounted: every pane opened since the inspector appeared, plus prewarmed ones.
22 * A mounted pane keeps its state and scroll position while another is on screen.
23 */
24export function useMountedInspectorPanes(content: InspectorContent | undefined) {
25 const [opened, setOpened] = useState<ReadonlySet<string>>(() => new Set());
26 const active = content?.active;
27 const prewarms = content?.panes.filter((pane) => pane.prewarm) ?? [];
28 const prewarmSignature = prewarms.some((pane) => pane.key === active)
29 ? prewarms.map((pane) => pane.key).join("\n")
30 : "";
31​
32 if (active !== undefined && !opened.has(active)) {
33 setOpened(new Set([...opened, active]));
34 }
35​
36 // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- prewarms the other panes on a timer after one opens
37 useEffect(() => {
38 if (prewarmSignature === "") return;
39 const timeout = setTimeout(() => {
40 setOpened((current) => {
41 const keys = prewarmSignature.split("\n");
42 return keys.every((key) => current.has(key)) ? current : new Set([...current, ...keys]);
43 });
44 }, PREWARM_DELAY_MS);
45 return () => clearTimeout(timeout);
46 }, [prewarmSignature]);
47​
48 return content?.panes.filter((pane) => opened.has(pane.key)) ?? [];