nandi/oripublic Fork 0
b6cd929ab1425a4ef6073ea9c189917cca02cf3c
Commits
Clone
git clone https://git.rickub.com/nandi/ori.git
git clone ssh://git@rickub.com/nandi/ori.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

forked from bots-garden/ori

workspace.ts · 136 lines · 4.1 KBTypeScript Blame HistoryRaw
✨ Workspace panel, selectors, previews, desktop app, sandbox template, resizable file tree, light/dark theme 76d62ac k33g yesterday1/**
2 * State of the workspace panel (file tree + Preview / Editor / Terminal
3 * tabs), kept apart from the chat state: the two evolve independently.
4 *
5 * @example
6 * openPreview("README.md"); // opens the panel on the preview tab
7 * useWorkspace((w) => w.tab); // inside a component
8 */
9
10import { create } from "zustand";
11
12export type WorkspaceTab = "preview" | "editor" | "terminal";
13
14/** Default width of the file tree column, in pixels (14rem at 16px). */
15export const DEFAULT_FILE_TREE_WIDTH = 224;
16/** Narrowest width the file tree column can be dragged to, in pixels. */
17export const MIN_FILE_TREE_WIDTH = 120;
18/** Widest width the file tree column can be dragged to, in pixels. */
19export const MAX_FILE_TREE_WIDTH = 800;
20/** localStorage key under which the chosen width is remembered. */
21export const FILE_TREE_WIDTH_KEY = "ori.fileTreeWidth";
22
23export interface WorkspaceState {
24 /** Whether the right-hand panel is visible. */
25 open: boolean;
26 /** The active tab. */
27 tab: WorkspaceTab;
28 /** File shown in the preview tab, if any. */
29 previewPath: string | null;
30 /** File opened in the editor tab, if any. */
31 editorPath: string | null;
32 /** Width of the file tree column, in pixels, always within the bounds. */
33 fileTreeWidth: number;
34}
35
36const initialWorkspace: WorkspaceState = {
37 open: false,
38 tab: "preview",
39 previewPath: null,
40 editorPath: null,
41 fileTreeWidth: loadFileTreeWidth(),
42};
43
44/** React hook over the workspace store. */
45export const useWorkspace = create<WorkspaceState>(() => initialWorkspace);
46
47/** toggleWorkspace shows or hides the panel. */
48export function toggleWorkspace(): void {
49 useWorkspace.setState((state) => ({ open: !state.open }));
50}
51
52/** showTab activates a tab (and opens the panel if needed). */
53export function showTab(tab: WorkspaceTab): void {
54 useWorkspace.setState({ open: true, tab });
55}
56
57/** openPreview shows a file in the preview tab. */
58export function openPreview(path: string): void {
59 useWorkspace.setState({ open: true, tab: "preview", previewPath: path });
60}
61
62/** openEditor opens a file in the editor tab. */
63export function openEditor(path: string): void {
64 useWorkspace.setState({ open: true, tab: "editor", editorPath: path });
65}
66
67/**
68 * clampFileTreeWidth brings a width back within the allowed bounds; a
69 * non-finite value (NaN, Infinity) falls back to the default width.
70 *
71 * @example
72 * clampFileTreeWidth(50); // 120
73 * clampFileTreeWidth(300); // 300
74 * clampFileTreeWidth(NaN); // 224
75 */
76export function clampFileTreeWidth(width: number): number {
77 if (!Number.isFinite(width)) {
78 return DEFAULT_FILE_TREE_WIDTH;
79 }
80 return Math.min(
81 MAX_FILE_TREE_WIDTH,
82 Math.max(MIN_FILE_TREE_WIDTH, Math.round(width)),
83 );
84}
85
86/**
87 * setFileTreeWidth resizes the file tree column and remembers the width in
88 * localStorage so it survives a page reload.
89 *
90 * @example
91 * setFileTreeWidth(320);
92 * useWorkspace.getState().fileTreeWidth; // 320
93 */
94export function setFileTreeWidth(width: number): void {
95 const clamped = clampFileTreeWidth(width);
96 useWorkspace.setState({ fileTreeWidth: clamped });
97 saveFileTreeWidth(clamped);
98}
99
100/** resetFileTreeWidth restores the default width (and forgets the stored one). */
101export function resetFileTreeWidth(): void {
102 setFileTreeWidth(DEFAULT_FILE_TREE_WIDTH);
103}
104
105/**
106 * loadFileTreeWidth reads the remembered width. Storage can be missing or
107 * throw (private browsing, blocked site data, non-browser test runs), so any
108 * failure or unusable value yields the default.
109 */
110function loadFileTreeWidth(): number {
111 try {
112 const stored = globalThis.localStorage?.getItem(FILE_TREE_WIDTH_KEY);
113 if (stored === null || stored === undefined) {
114 return DEFAULT_FILE_TREE_WIDTH;
115 }
116 return clampFileTreeWidth(Number(stored));
117 } catch {
118 return DEFAULT_FILE_TREE_WIDTH;
119 }
120}
121
122function saveFileTreeWidth(width: number): void {
123 try {
124 globalThis.localStorage?.setItem(FILE_TREE_WIDTH_KEY, String(width));
125 } catch {
126 // Storage unavailable: the width still applies for this page.
127 }
128}
129
130/** resetWorkspace restores the initial state; meant for tests. */
131export function resetWorkspace(): void {
132 useWorkspace.setState(
133 { ...initialWorkspace, fileTreeWidth: DEFAULT_FILE_TREE_WIDTH },
134 true,
135 );
136}