nandi/oripublic Fork 0
ca7e020cbb6442f047e0002eb9831ba29f8a17af
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
  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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
/**
 * State of the workspace panel (file tree + Preview / Editor / Terminal
 * tabs), kept apart from the chat state: the two evolve independently.
 *
 * @example
 * openPreview("README.md");   // opens the panel on the preview tab
 * useWorkspace((w) => w.tab); // inside a component
 */

import { create } from "zustand";

export type WorkspaceTab = "preview" | "editor" | "terminal";

/** Default width of the file tree column, in pixels (14rem at 16px). */
export const DEFAULT_FILE_TREE_WIDTH = 224;
/** Narrowest width the file tree column can be dragged to, in pixels. */
export const MIN_FILE_TREE_WIDTH = 120;
/** Widest width the file tree column can be dragged to, in pixels. */
export const MAX_FILE_TREE_WIDTH = 800;
/** localStorage key under which the chosen width is remembered. */
export const FILE_TREE_WIDTH_KEY = "ori.fileTreeWidth";

export interface WorkspaceState {
	/** Whether the right-hand panel is visible. */
	open: boolean;
	/** The active tab. */
	tab: WorkspaceTab;
	/** File shown in the preview tab, if any. */
	previewPath: string | null;
	/** File opened in the editor tab, if any. */
	editorPath: string | null;
	/** Width of the file tree column, in pixels, always within the bounds. */
	fileTreeWidth: number;
}

const initialWorkspace: WorkspaceState = {
	open: false,
	tab: "preview",
	previewPath: null,
	editorPath: null,
	fileTreeWidth: loadFileTreeWidth(),
};

/** React hook over the workspace store. */
export const useWorkspace = create<WorkspaceState>(() => initialWorkspace);

/** toggleWorkspace shows or hides the panel. */
export function toggleWorkspace(): void {
	useWorkspace.setState((state) => ({ open: !state.open }));
}

/** showTab activates a tab (and opens the panel if needed). */
export function showTab(tab: WorkspaceTab): void {
	useWorkspace.setState({ open: true, tab });
}

/** openPreview shows a file in the preview tab. */
export function openPreview(path: string): void {
	useWorkspace.setState({ open: true, tab: "preview", previewPath: path });
}

/** openEditor opens a file in the editor tab. */
export function openEditor(path: string): void {
	useWorkspace.setState({ open: true, tab: "editor", editorPath: path });
}

/**
 * clampFileTreeWidth brings a width back within the allowed bounds; a
 * non-finite value (NaN, Infinity) falls back to the default width.
 *
 * @example
 * clampFileTreeWidth(50);   // 120
 * clampFileTreeWidth(300);  // 300
 * clampFileTreeWidth(NaN);  // 224
 */
export function clampFileTreeWidth(width: number): number {
	if (!Number.isFinite(width)) {
		return DEFAULT_FILE_TREE_WIDTH;
	}
	return Math.min(
		MAX_FILE_TREE_WIDTH,
		Math.max(MIN_FILE_TREE_WIDTH, Math.round(width)),
	);
}

/**
 * setFileTreeWidth resizes the file tree column and remembers the width in
 * localStorage so it survives a page reload.
 *
 * @example
 * setFileTreeWidth(320);
 * useWorkspace.getState().fileTreeWidth; // 320
 */
export function setFileTreeWidth(width: number): void {
	const clamped = clampFileTreeWidth(width);
	useWorkspace.setState({ fileTreeWidth: clamped });
	saveFileTreeWidth(clamped);
}

/** resetFileTreeWidth restores the default width (and forgets the stored one). */
export function resetFileTreeWidth(): void {
	setFileTreeWidth(DEFAULT_FILE_TREE_WIDTH);
}

/**
 * loadFileTreeWidth reads the remembered width. Storage can be missing or
 * throw (private browsing, blocked site data, non-browser test runs), so any
 * failure or unusable value yields the default.
 */
function loadFileTreeWidth(): number {
	try {
		const stored = globalThis.localStorage?.getItem(FILE_TREE_WIDTH_KEY);
		if (stored === null || stored === undefined) {
			return DEFAULT_FILE_TREE_WIDTH;
		}
		return clampFileTreeWidth(Number(stored));
	} catch {
		return DEFAULT_FILE_TREE_WIDTH;
	}
}

function saveFileTreeWidth(width: number): void {
	try {
		globalThis.localStorage?.setItem(FILE_TREE_WIDTH_KEY, String(width));
	} catch {
		// Storage unavailable: the width still applies for this page.
	}
}

/** resetWorkspace restores the initial state; meant for tests. */
export function resetWorkspace(): void {
	useWorkspace.setState(
		{ ...initialWorkspace, fileTreeWidth: DEFAULT_FILE_TREE_WIDTH },
		true,
	);
}