Skip to content
Source
Sign inSign up
Open History to see the last commit

PanelTabBreadcrumb.tsx

82 lines2.9 KB

1import { ChevronRightIcon } from "lucide-react";
2import { createContext, useContext, useLayoutEffect, useState, type ReactNode } from "react";
3import { createPortal } from "react-dom";
4​
5/**
6 * Where the active panel tab hosts its surface's header (pen JiaZk): the
7 * breadcrumb after the tab's title and the actions at the row's end.
8 * RightPanelTabs provides it; the elements are null until the row mounts.
9 */
10export interface PanelTabSlots {
11 readonly breadcrumb: HTMLElement | null;
12 readonly actions: HTMLElement | null;
13 readonly registerActions: () => () => void;
14 /** The breadcrumb sits on the pane's bar beside the tabs, not in the tab. */
15 readonly breadcrumbOnBar?: boolean;
17​
18export const PanelTabSlotsContext = createContext<PanelTabSlots | null>(null);
19​
20/**
21 * Whether the surface renders inside a panel tab bar, which then carries its
22 * breadcrumb and actions. A surface drops its own header row when it does.
23 */
24export function useInPanelTabBar(): boolean {
25 return useContext(PanelTabSlotsContext) !== null;
27​
28/** The breadcrumb shares the pane's bar with its tabs, so it gives up room first. */
29export function usePanelTabBreadcrumbOnBar(): boolean {
30 return useContext(PanelTabSlotsContext)?.breadcrumbOnBar === true;
32​
33export function usePanelTabActionsHost(): {
34 hasActions: boolean;
35 registerActions: () => () => void;
36} {
37 const [count, setCount] = useState(0);
38 const registerActions = () => {
39 setCount((current) => current + 1);
40 return () => setCount((current) => current - 1);
41 };
42 return { hasActions: count > 0, registerActions };
44​
45function PanelTabPortal({
46 slot,
47 children,
48}: {
49 slot: "breadcrumb" | "actions";
50 children: ReactNode;
51}) {
52 const slots = useContext(PanelTabSlotsContext);
53 if (!slots) return children;
54 const element = slots[slot];
55 return element ? createPortal(children, element) : null;
57​
58/**
59 * A surface's breadcrumb, shown in its tab after the title (pen JiaZk:
60 * "Diff › Latest turn"). Outside a panel tab bar it renders in place.
61 */
62export function PanelTabBreadcrumb({ children }: { children: ReactNode }) {
63 if (!useInPanelTabBar()) return children;
64 return (
65 <PanelTabPortal slot="breadcrumb">
66 <ChevronRightIcon aria-hidden className="size-3 shrink-0 text-dim-foreground" />
67 {children}
68 </PanelTabPortal>
69 );
71​
72/**
73 * A surface's header actions, shown at the end of the panel tab row (pen
74 * JiaZk: collapse, layout, wrap, whitespace, file tree). Outside a panel tab
75 * bar they render in place.
76 */
77export function PanelTabActions({ children }: { children: ReactNode }) {
78 const registerActions = useContext(PanelTabSlotsContext)?.registerActions;
79 // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- Tell the tab bar before paint, so its actions notch never draws empty.
80 useLayoutEffect(() => registerActions?.(), [registerActions]);
81 return <PanelTabPortal slot="actions">{children}</PanelTabPortal>;