返回 DeepSeek-Reasonix
layout.ts
根目录 / desktop / frontend / src / store / layout.ts
1 // layout owns the desktop shell's geometry state — sidebar + right-dock widths
2 // and the sidebar collapse flag — as a selectable store rather than App-local
3 // useState. Components read a single slice via selector (only that slice
4 // re-renders), with no prop drilling. The geometry constants, clamps, and the
5 // localStorage-backed load/save helpers live here too: they are layout-domain
6 // knowledge that belongs with the store, and keeping them here lets the store
7 // initialize itself from persisted state at module load without depending on App.
8 //
9 // Persistence behavior is intentionally unchanged from the previous App-local
10 // implementation: the store's setters are pure (state only), and callers keep
11 // invoking the exported save* helpers exactly where they did before, so the
12 // on-disk localStorage schema and write timing are byte-identical.
13
14 import type { Dispatch, SetStateAction } from "react";
15 import { create } from "zustand";
16
17 import { loadLayoutSize, loadOptionalLayoutSize, saveLayoutSize } from "../lib/layoutPreferences";
18
19 import { applySetState } from "./setState";
20
21 const SIDEBAR_COLLAPSED_KEY = "reasonix.sidebar.collapsed";
22 const SIDEBAR_DEFAULT_WIDTH = 264;
23 export const SIDEBAR_MIN_WIDTH = 264;
24 export const CREATION_SIDEBAR_MIN_WIDTH = 236;
25 // Creation keeps the expanded rail at the narrow floor by default.
26 export const CREATION_SIDEBAR_DEFAULT_WIDTH = CREATION_SIDEBAR_MIN_WIDTH;
27 export const SIDEBAR_MAX_WIDTH = 300;
28 const SIDEBAR_VIEWPORT_RATIO = 0.18;
29
30 const RIGHT_DOCK_TREE_DEFAULT_WIDTH = 300;
31 export const RIGHT_DOCK_TREE_MIN_WIDTH = 300;
32 // Creation file-tree dock stays tighter than classic 300. With Creation's
33 // narrower Windows caption strip (~108px), 252 is enough for icon+label tabs.
34 export const CREATION_RIGHT_DOCK_TREE_MIN_WIDTH = 252;
35 export const CREATION_RIGHT_DOCK_TREE_DEFAULT_WIDTH = CREATION_RIGHT_DOCK_TREE_MIN_WIDTH;
36 export const RIGHT_DOCK_TREE_MAX_WIDTH = 560;
37 export const RIGHT_DOCK_PREVIEW_DEFAULT_WIDTH = 660;
38 export const RIGHT_DOCK_PREVIEW_MIN_WIDTH = 420;
39 export const RIGHT_DOCK_MIN_RENDER_WIDTH = 280;
40 // Creation tree mode may render below the classic 280 floor when the viewport squeezes.
41 export const CREATION_RIGHT_DOCK_MIN_RENDER_WIDTH = 236;
42 export const RIGHT_DOCK_MAX_WIDTH = 860;
43 const WORKSPACE_PANEL_OPEN_KEY = "reasonix.workspacePanel.open";
44 // First-launch default when no preference is stored (matches post-#6371 UX).
45 const WORKSPACE_PANEL_DEFAULT_OPEN = true;
46
47 export function clampSidebarWidth(width: number): number {
48 return Math.min(SIDEBAR_MAX_WIDTH, Math.max(SIDEBAR_MIN_WIDTH, Math.round(width)));
49 }
50
51 export function clampCreationSidebarWidth(width: number): number {
52 return Math.min(SIDEBAR_MAX_WIDTH, Math.max(CREATION_SIDEBAR_MIN_WIDTH, Math.round(width)));
53 }
54
55 function clampStoredSidebarWidth(width: number): number {
56 return Math.min(SIDEBAR_MAX_WIDTH, Math.max(CREATION_SIDEBAR_MIN_WIDTH, Math.round(width)));
57 }
58
59 export function clampRightDockPreviewWidth(width: number): number {
60 return Math.min(RIGHT_DOCK_MAX_WIDTH, Math.max(RIGHT_DOCK_PREVIEW_MIN_WIDTH, Math.round(width)));
61 }
62
63 export function clampRightDockTreeWidth(width: number): number {
64 return Math.min(RIGHT_DOCK_TREE_MAX_WIDTH, Math.max(RIGHT_DOCK_TREE_MIN_WIDTH, Math.round(width)));
65 }
66
67 export function clampCreationRightDockTreeWidth(width: number): number {
68 return Math.min(RIGHT_DOCK_TREE_MAX_WIDTH, Math.max(CREATION_RIGHT_DOCK_TREE_MIN_WIDTH, Math.round(width)));
69 }
70
71 function clampStoredRightDockTreeWidth(width: number): number {
72 return Math.min(RIGHT_DOCK_TREE_MAX_WIDTH, Math.max(CREATION_RIGHT_DOCK_TREE_MIN_WIDTH, Math.round(width)));
73 }
74
75 export function defaultSidebarWidth(): number {
76 if (typeof window !== "undefined") {
77 return clampSidebarWidth(window.innerWidth * SIDEBAR_VIEWPORT_RATIO);
78 }
79 return SIDEBAR_DEFAULT_WIDTH;
80 }
81
82 export function defaultCreationSidebarWidth(): number {
83 return CREATION_SIDEBAR_DEFAULT_WIDTH;
84 }
85
86 export function defaultRightDockTreeWidth(): number {
87 return RIGHT_DOCK_TREE_DEFAULT_WIDTH;
88 }
89
90 export function defaultCreationRightDockTreeWidth(): number {
91 return CREATION_RIGHT_DOCK_TREE_DEFAULT_WIDTH;
92 }
93
94 function loadSidebarCollapsed(): boolean {
95 if (typeof window === "undefined") return false;
96 try {
97 return window.localStorage.getItem(SIDEBAR_COLLAPSED_KEY) === "1";
98 } catch {
99 return false;
100 }
101 }
102
103 export function saveSidebarCollapsed(collapsed: boolean): void {
104 if (typeof window === "undefined") return;
105 try {
106 window.localStorage.setItem(SIDEBAR_COLLAPSED_KEY, collapsed ? "1" : "0");
107 } catch {
108 /* ignore storage failures */
109 }
110 }
111
112 function loadSidebarWidth(): number {
113 return loadLayoutSize("sidebarWidthGraphite", defaultSidebarWidth(), clampStoredSidebarWidth);
114 }
115
116 export function saveSidebarWidth(width: number): void {
117 saveLayoutSize("sidebarWidthGraphite", width, clampStoredSidebarWidth);
118 }
119
120 function loadRightDockTreeWidth(): number {
121 return loadLayoutSize("rightDockTreeWidth", defaultRightDockTreeWidth(), clampStoredRightDockTreeWidth);
122 }
123
124 export function saveRightDockTreeWidth(width: number): void {
125 saveLayoutSize("rightDockTreeWidth", width, clampStoredRightDockTreeWidth);
126 }
127
128 function loadRightDockPreviewWidth(): number {
129 return loadLayoutSize("rightDockPreviewWidth", RIGHT_DOCK_PREVIEW_DEFAULT_WIDTH, clampRightDockPreviewWidth);
130 }
131
132 export function saveRightDockPreviewWidth(width: number): void {
133 saveLayoutSize("rightDockPreviewWidth", width, clampRightDockPreviewWidth);
134 }
135
136 // rightDockMode selects what the right dock shows. workspacePanelOpen is
137 // restored from localStorage (same pattern as sidebarCollapsed) so a collapsed
138 // dock survives restart. maximized/preview stay session-local — they are view
139 // layout, not a durable preference. (Resize drag flags, button-press animation
140 // flags, measured footer height, and viewport width stay as useState in App.tsx.)
141 export type RightDockMode = "context" | "files" | "changed" | "remote";
142
143 // terminalPanelOpen is independent from rightDockMode — the terminal is a
144 // bottom drawer that coexists with the workspace panel, not a mode of it.
145 // Persisted to localStorage so it survives restart.
146 const TERMINAL_PANEL_OPEN_KEY = "reasonix.terminalPanel.open";
147 const TERMINAL_PANEL_DEFAULT_OPEN = false;
148
149 function loadTerminalPanelOpen(): boolean {
150 if (typeof window === "undefined") return TERMINAL_PANEL_DEFAULT_OPEN;
151 try {
152 return window.localStorage.getItem(TERMINAL_PANEL_OPEN_KEY) === "1";
153 } catch {
154 return TERMINAL_PANEL_DEFAULT_OPEN;
155 }
156 }
157
158 export function saveTerminalPanelOpen(open: boolean): void {
159 if (typeof window === "undefined") return;
160 try {
161 window.localStorage.setItem(TERMINAL_PANEL_OPEN_KEY, open ? "1" : "0");
162 } catch {
163 /* ignore storage failures */
164 }
165 }
166
167 // Terminal height defaults and clamps for the bottom drawer.
168 export const TERMINAL_DEFAULT_HEIGHT = 280;
169 export const TERMINAL_MIN_HEIGHT = 120;
170 export const TERMINAL_MAX_HEIGHT_RATIO = 0.5; // max 50% of viewport height
171
172 const TERMINAL_HEIGHT_KEY = "reasonix.terminalPanel.height";
173
174 function loadTerminalHeight(): number {
175 if (typeof window === "undefined") return TERMINAL_DEFAULT_HEIGHT;
176 try {
177 const raw = window.localStorage.getItem(TERMINAL_HEIGHT_KEY);
178 if (raw === null) return TERMINAL_DEFAULT_HEIGHT;
179 const parsed = Number(raw);
180 if (Number.isFinite(parsed) && parsed >= TERMINAL_MIN_HEIGHT) return parsed;
181 return TERMINAL_DEFAULT_HEIGHT;
182 } catch {
183 return TERMINAL_DEFAULT_HEIGHT;
184 }
185 }
186
187 export function saveTerminalHeight(height: number): void {
188 if (typeof window === "undefined") return;
189 try {
190 window.localStorage.setItem(TERMINAL_HEIGHT_KEY, String(Math.round(height)));
191 } catch {
192 /* ignore storage failures */
193 }
194 }
195
196 export function terminalMaxHeight(viewportHeight: number): number {
197 return Math.max(TERMINAL_MIN_HEIGHT, Math.floor(Math.max(0, viewportHeight) * TERMINAL_MAX_HEIGHT_RATIO));
198 }
199
200 export function clampTerminalHeight(height: number, viewportHeight: number): number {
201 const max = terminalMaxHeight(viewportHeight);
202 return Math.min(max, Math.max(TERMINAL_MIN_HEIGHT, Math.round(height)));
203 }
204
205 function loadWorkspacePanelOpen(): boolean {
206 if (typeof window === "undefined") return WORKSPACE_PANEL_DEFAULT_OPEN;
207 try {
208 const raw = window.localStorage.getItem(WORKSPACE_PANEL_OPEN_KEY);
209 if (raw === null) return WORKSPACE_PANEL_DEFAULT_OPEN;
210 return raw !== "0";
211 } catch {
212 return WORKSPACE_PANEL_DEFAULT_OPEN;
213 }
214 }
215
216 export function saveWorkspacePanelOpen(open: boolean): void {
217 if (typeof window === "undefined") return;
218 try {
219 window.localStorage.setItem(WORKSPACE_PANEL_OPEN_KEY, open ? "1" : "0");
220 } catch {
221 /* ignore storage failures */
222 }
223 }
224
225 export type LayoutState = {
226 sidebarCollapsed: boolean;
227 sidebarWidth: number;
228 rightDockTreeWidth: number;
229 rightDockPreviewWidth: number;
230 workspacePanelOpen: boolean;
231 workspacePanelMaximized: boolean;
232 workspacePreviewActive: boolean;
233 rightDockMode: RightDockMode;
234 terminalPanelOpen: boolean;
235 terminalHeight: number;
236 setSidebarCollapsed: (collapsed: boolean) => void;
237 setSidebarWidth: (width: number) => void;
238 setRightDockTreeWidth: (width: number) => void;
239 setRightDockPreviewWidth: (width: number) => void;
240 setWorkspacePanelOpen: Dispatch<SetStateAction<boolean>>;
241 setWorkspacePanelMaximized: Dispatch<SetStateAction<boolean>>;
242 setWorkspacePreviewActive: Dispatch<SetStateAction<boolean>>;
243 setRightDockMode: Dispatch<SetStateAction<RightDockMode>>;
244 setTerminalPanelOpen: Dispatch<SetStateAction<boolean>>;
245 setTerminalHeight: (height: number) => void;
246 };
247
248 export const useLayoutStore = create<LayoutState>((set) => ({
249 sidebarCollapsed: loadSidebarCollapsed(),
250 sidebarWidth: loadSidebarWidth(),
251 rightDockTreeWidth: loadRightDockTreeWidth(),
252 rightDockPreviewWidth: loadRightDockPreviewWidth(),
253 workspacePanelOpen: loadWorkspacePanelOpen(),
254 workspacePanelMaximized: false,
255 workspacePreviewActive: false,
256 rightDockMode: "context",
257 terminalPanelOpen: loadTerminalPanelOpen(),
258 terminalHeight: loadTerminalHeight(),
259 setSidebarCollapsed: (collapsed) => set({ sidebarCollapsed: collapsed }),
260 setSidebarWidth: (width) => set({ sidebarWidth: width }),
261 setRightDockTreeWidth: (width) => set({ rightDockTreeWidth: width }),
262 setRightDockPreviewWidth: (width) => set({ rightDockPreviewWidth: width }),
263 setWorkspacePanelOpen: (update) => set((s) => ({ workspacePanelOpen: applySetState(s.workspacePanelOpen, update) })),
264 setWorkspacePanelMaximized: (update) => set((s) => ({ workspacePanelMaximized: applySetState(s.workspacePanelMaximized, update) })),
265 setWorkspacePreviewActive: (update) => set((s) => ({ workspacePreviewActive: applySetState(s.workspacePreviewActive, update) })),
266 setRightDockMode: (update) => set((s) => ({ rightDockMode: applySetState(s.rightDockMode, update) })),
267 setTerminalPanelOpen: (update) => set((s) => ({ terminalPanelOpen: applySetState(s.terminalPanelOpen, update) })),
268 setTerminalHeight: (height) => set({ terminalHeight: height }),
269 }));
270
271 export function applyLayoutStyleDefaults(style: "classic" | "workbench" | "creation"): void {
272 const state = useLayoutStore.getState();
273 if (loadOptionalLayoutSize("sidebarWidthGraphite", clampStoredSidebarWidth) === null) {
274 state.setSidebarWidth(style === "creation" ? defaultCreationSidebarWidth() : defaultSidebarWidth());
275 }
276 if (loadOptionalLayoutSize("rightDockTreeWidth", clampStoredRightDockTreeWidth) === null) {
277 state.setRightDockTreeWidth(style === "creation" ? defaultCreationRightDockTreeWidth() : defaultRightDockTreeWidth());
278 }
279 }
280
280 lines TYPESCRIPT