feat(workspace): add localStorage helper for split-view layout state
This commit is contained in:
@@ -0,0 +1,38 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from "vitest";
|
||||||
|
import {
|
||||||
|
readSplitViewState,
|
||||||
|
writeSplitViewState,
|
||||||
|
defaultSplitViewState,
|
||||||
|
} from "../splitViewStorage";
|
||||||
|
|
||||||
|
describe("splitViewStorage", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorage.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the default state when nothing is stored", () => {
|
||||||
|
expect(readSplitViewState()).toEqual(defaultSplitViewState());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("defaults to a single unselected pane", () => {
|
||||||
|
expect(defaultSplitViewState()).toEqual({ layout: 1, paneLaneIds: [null] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("round-trips a written state", () => {
|
||||||
|
writeSplitViewState({ layout: 4, paneLaneIds: [1, 2, null, null] });
|
||||||
|
expect(readSplitViewState()).toEqual({ layout: 4, paneLaneIds: [1, 2, null, null] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to the default when stored JSON is malformed", () => {
|
||||||
|
localStorage.setItem("ccam.workspace.splitView", "{not json");
|
||||||
|
expect(readSplitViewState()).toEqual(defaultSplitViewState());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to the default when the stored layout is not 1, 2, or 4", () => {
|
||||||
|
localStorage.setItem(
|
||||||
|
"ccam.workspace.splitView",
|
||||||
|
JSON.stringify({ layout: 3, paneLaneIds: [] })
|
||||||
|
);
|
||||||
|
expect(readSplitViewState()).toEqual(defaultSplitViewState());
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* @file splitViewStorage.ts
|
||||||
|
* @description Persists the Workspace page's split-terminal layout (1/2/4
|
||||||
|
* panes) and each pane's chosen lane id to localStorage, so the layout
|
||||||
|
* survives a page reload. Follows the same read/write-with-fallback
|
||||||
|
* convention as useTheme.ts.
|
||||||
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type SplitLayout = 1 | 2 | 4;
|
||||||
|
|
||||||
|
export interface SplitViewState {
|
||||||
|
layout: SplitLayout;
|
||||||
|
paneLaneIds: (number | null)[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const STORAGE_KEY = "ccam.workspace.splitView";
|
||||||
|
|
||||||
|
export function defaultSplitViewState(): SplitViewState {
|
||||||
|
return { layout: 1, paneLaneIds: [null] };
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidLayout(value: unknown): value is SplitLayout {
|
||||||
|
return value === 1 || value === 2 || value === 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidState(value: unknown): value is SplitViewState {
|
||||||
|
if (!value || typeof value !== "object") return false;
|
||||||
|
const v = value as Record<string, unknown>;
|
||||||
|
return (
|
||||||
|
isValidLayout(v.layout) &&
|
||||||
|
Array.isArray(v.paneLaneIds) &&
|
||||||
|
v.paneLaneIds.every((id) => id === null || typeof id === "number")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readSplitViewState(): SplitViewState {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (!raw) return defaultSplitViewState();
|
||||||
|
const parsed: unknown = JSON.parse(raw);
|
||||||
|
return isValidState(parsed) ? parsed : defaultSplitViewState();
|
||||||
|
} catch {
|
||||||
|
return defaultSplitViewState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeSplitViewState(state: SplitViewState): void {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
||||||
|
} catch {
|
||||||
|
/* ignore quota / disabled storage */
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user