1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
|
/**
* Workspace is the right-hand panel: the file tree next to the Preview /
* Editor / Terminal tabs. Panes are hidden rather than unmounted when
* another tab is active — the terminal's shell session and the editor's
* unsaved state survive tab switches (and the panel being collapsed).
* The file tree column is resizable: its width lives in the workspace
* store and reaches the CSS through the `--filetree-width` custom property.
*
* @example
* <Workspace /> // visibility is driven by the workspace store
*/
import { type CSSProperties, type ReactNode, useEffect, useState } from "react";
import {
MAX_FILE_TREE_WIDTH,
MIN_FILE_TREE_WIDTH,
type WorkspaceTab,
resetFileTreeWidth,
setFileTreeWidth,
showTab,
useWorkspace,
} from "../workspace";
import { EditorPane } from "./EditorPane";
import { FileTree } from "./FileTree";
import { PreviewPane } from "./PreviewPane";
import { ResizeHandle } from "./ResizeHandle";
import { TerminalPane } from "./TerminalPane";
const tabs: { id: WorkspaceTab; label: string }[] = [
{ id: "preview", label: "Preview" },
{ id: "editor", label: "Editor" },
{ id: "terminal", label: "Terminal" },
];
export function Workspace() {
const workspace = useWorkspace();
// The terminal (and its shell) starts on first activation only.
const [terminalStarted, setTerminalStarted] = useState(false);
useEffect(() => {
if (workspace.open && workspace.tab === "terminal") {
setTerminalStarted(true);
}
}, [workspace.open, workspace.tab]);
const style = {
"--filetree-width": `${workspace.fileTreeWidth}px`,
} as CSSProperties;
return (
<aside
className={`workspace${workspace.open ? "" : " workspace-hidden"}`}
aria-label="workspace"
style={style}
>
<FileTree />
<ResizeHandle
label="Resize the file tree"
width={workspace.fileTreeWidth}
min={MIN_FILE_TREE_WIDTH}
max={MAX_FILE_TREE_WIDTH}
onResize={setFileTreeWidth}
onReset={resetFileTreeWidth}
/>
<div className="workspace-main">
<nav className="workspace-tabs" aria-label="workspace tabs">
{tabs.map((tab) => (
<button
key={tab.id}
type="button"
className={`workspace-tab${workspace.tab === tab.id ? " workspace-tab-active" : ""}`}
onClick={() => showTab(tab.id)}
>
{tab.label}
</button>
))}
</nav>
<div className="workspace-content">
<Pane visible={workspace.tab === "preview"}>
<PreviewPane path={workspace.previewPath} />
</Pane>
<Pane visible={workspace.tab === "editor"}>
<EditorPane path={workspace.editorPath} />
</Pane>
<Pane visible={workspace.tab === "terminal"}>
{terminalStarted ? (
<TerminalPane />
) : (
<p className="pane-empty">starting…</p>
)}
</Pane>
</div>
</div>
</aside>
);
}
/** Pane hides its children without unmounting them. */
function Pane({
visible,
children,
}: { visible: boolean; children: ReactNode }) {
return (
<div className={`workspace-pane${visible ? "" : " workspace-pane-hidden"}`}>
{children}
</div>
);
}
|