1import { useSyncExternalStore, type ReactNode } from "react";
2import { Split, Stack } from "react-native-screens";
3
4import { RenderErrorBoundary, RenderFailureView } from "../../components/RenderErrorBoundary";
5import { PaneContext } from "../../components/ui/Pane";
6import { GLASS_HEADER_OPTIONS } from "../../native/glass-header-options";
7import { NativeColumnContent } from "../../native/NativeColumnContent.ios";
8import { V5StackHeader } from "../../native/V5StackHeader.ios";
9import { WorkspaceSlotOptionsContext } from "../../native/v5-workspace-context";
10import {
11 InspectorCloseContext,
12 useInspectorBarSlot,
13 type InspectorBarStore,
14} from "./inspector-bar-store";
15import {
16 useMountedInspectorPanes,
17 type InspectorContent,
18 type InspectorPane,
19} from "./inspector-panes";
20
21// A pane without an AppBar has no bar; one with it gets the glass the workspace's screens get.
22const PANE_HEADER_OPTIONS = { ...GLASS_HEADER_OPTIONS, headerShown: false };
23
24function PaneHeader(props: { readonly store: InspectorBarStore }) {
25 const options = useSyncExternalStore(props.store.subscribe, props.store.getSnapshot);
26 // Its own search identity, so opening the inspector doesn't morph the sidebar's search into it.
27 return (
28 <V5StackHeader
29 options={{ ...PANE_HEADER_OPTIONS, ...options }}
30 canGoBack={false}
31 searchIdentifier="inspector-search"
32 />
33 );
34}
35
36function PaneScreenContent(props: {
37 readonly pane: InspectorPane;
38 readonly resetKeys: ReadonlyArray<unknown> | undefined;
39 readonly onClose: () => void;
40}) {
41 const { store, slot } = useInspectorBarSlot();
42 const content = (
43 <WorkspaceSlotOptionsContext value={slot}>
44 <RenderErrorBoundary
45 resetKeys={props.resetKeys}
46 renderFallback={(fallback) => (
47 <RenderFailureView {...fallback} title="The inspector couldn't be displayed" />
48 )}
49 >
50 {props.pane.render()}
51 </RenderErrorBoundary>
52 </WorkspaceSlotOptionsContext>
53 );
54 return (
55 <InspectorCloseContext value={props.onClose}>
56 <PaneContext value="inspector">
57 <PaneHeader store={store} />
58 <NativeColumnContent>{content}</NativeColumnContent>
59 </PaneContext>
60 </InspectorCloseContext>
61 );
62}
63
64/**
65 * The inspector column's panes, each a screen of the column like the detail column's routes.
66 * Only the active one is attached, so UIKit sees one bar, one toolbar and one scroll view.
67 */
68export function InspectorColumnScreens(props: {
69 readonly content: InspectorContent | undefined;
70 readonly onClose: () => void;
71 /** Drawn over the active pane. */
72 readonly overlay?: ReactNode;
73}) {
74 const panes = useMountedInspectorPanes(props.content);
75 return panes.map((pane) => {
76 const active = pane.key === props.content?.active;
77 return (
78 <Split.Inspector
79 key={pane.key}
80 screenKey={`inspector:${pane.key}`}
81 activityMode={active ? "attached" : "detached"}
82 >
83 <PaneScreenContent
84 pane={pane}
85 resetKeys={props.content?.resetKeys}
86 onClose={props.onClose}
87 />
88 {active ? props.overlay : null}
89 </Split.Inspector>
90 );
91 });
92}
93
94/** The inspector's panes where it has no column: a native stack with the active one attached. */
95export function InspectorPanes(props: {
96 readonly content: InspectorContent | undefined;
97 readonly onClose: () => void;
98}) {
99 const panes = useMountedInspectorPanes(props.content);
100 if (panes.length === 0) return null;
101 return (
102 <Stack.Host>
103 {panes.map((pane) => (
104 <Stack.Screen
105 key={pane.key}
106 screenKey={`inspector:${pane.key}`}
107 activityMode={pane.key === props.content?.active ? "attached" : "detached"}
108 >
109 <PaneScreenContent
110 pane={pane}
111 resetKeys={props.content?.resetKeys}
112 onClose={props.onClose}
113 />
114 </Stack.Screen>
115 ))}
116 </Stack.Host>
117 );
118}