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

FileExplorerPane.tsx

114 lines3.6 KB

1import { type CSSProperties, type ReactNode, useState } from "react";
2​
3import { RightPanelResizeHandle } from "~/components/preview/RightPanelResizeHandle";
4import { RESIZABLE_WIDTH_PROPERTY, useResizableWidth } from "~/hooks/useResizableWidth";
5import { observeResize } from "~/lib/observeResize";
6import { cn } from "~/lib/utils";
7​
8const MIN_WIDTH = 160;
9const MAX_WIDTH = 560;
10const FILE_CONTENT_MIN_WIDTH = 256;
11​
12/** A persisted, resizable file-tree column; fills the row when resizing is disabled.
13 * Callers can retain a responsive initial width via defaultWidthFraction (a fraction of the row),
14 * and use a separate saved width, minimum and resize label. A chosen width overrides the default.
15 */
16export function FileExplorerPane({
17 resizable,
18 children,
19 storageKey = "t3code.fileExplorerWidth",
20 defaultWidth = 256,
21 defaultWidthFraction,
22 minWidth = MIN_WIDTH,
23 resizeLabel = "Resize file explorer",
24}: {
25 resizable: boolean;
26 children: ReactNode;
27 storageKey?: string;
28 defaultWidth?: number;
29 defaultWidthFraction?: number;
30 minWidth?: number;
31 resizeLabel?: string;
32}) {
33 const [maxWidth, setMaxWidth] = useState(MAX_WIDTH);
34 const [responsiveDefaultWidth, setResponsiveDefaultWidth] = useState(defaultWidth);
35 const { width, setWidth, handlers } = useResizableWidth({
36 storageKey,
37 defaultWidth: responsiveDefaultWidth,
38 minWidth,
39 maxWidth,
40 edge: "left",
41 });
42​
43 return (
44 <aside
45 ref={(host) => {
46 const row = host?.parentElement;
47 if (!row || !resizable) return;
48 const measure = () => {
49 const initialWidth = Math.max(
50 minWidth,
51 Math.min(defaultWidth, row.clientWidth * (defaultWidthFraction ?? 1)),
52 );
53 // A responsive default must not shrink to make room for the content reserve.
54 const availableWidth = Math.max(
55 row.clientWidth - FILE_CONTENT_MIN_WIDTH,
56 defaultWidthFraction === undefined ? minWidth : initialWidth,
57 );
58 setMaxWidth(Math.max(minWidth, Math.min(MAX_WIDTH, availableWidth)));
59 setResponsiveDefaultWidth(initialWidth);
60 };
61 measure();
62 return observeResize(row, measure);
63 }}
64 aria-label="File explorer"
65 className={cn(
66 "relative flex min-h-0 min-w-0 shrink-0 bg-background",
67 resizable ? "border-l border-border/60" : "flex-1",
68 )}
69 style={
70 resizable
71 ? ({
72 [RESIZABLE_WIDTH_PROPERTY]: `${width}px`,
73 width: `var(${RESIZABLE_WIDTH_PROPERTY})`,
74 } as CSSProperties)
75 : undefined
76 }
77 >
78 {resizable ? (
79 <RightPanelResizeHandle
80 handlers={handlers}
81 label={resizeLabel}
82 tabIndex={0}
83 aria-valuenow={width}
84 aria-valuemin={minWidth}
85 aria-valuemax={maxWidth}
86 onKeyDown={(event) => {
87 if (event.altKey || event.ctrlKey || event.metaKey) return;
88 let nextWidth: number;
89 switch (event.key) {
90 case "ArrowLeft":
91 nextWidth = width + 16;
92 break;
93 case "ArrowRight":
94 nextWidth = width - 16;
95 break;
96 case "Home":
97 nextWidth = minWidth;
98 break;
99 case "End":
100 nextWidth = maxWidth;
101 break;
102 default:
103 return;
104 }
105 event.preventDefault();
106 event.stopPropagation();
107 setWidth(nextWidth);
108 }}
109 />
110 ) : null}
111 {children}
112 </aside>
113 );