98 lines
3.6 KiB
TypeScript
98 lines
3.6 KiB
TypeScript
export type WorkspaceItem = {
|
|
id: string;
|
|
label: string;
|
|
path: string;
|
|
kind: 'favorite' | 'view' | 'recent';
|
|
updatedAt: string;
|
|
};
|
|
|
|
type WorkspaceState = {
|
|
favorites: WorkspaceItem[];
|
|
views: WorkspaceItem[];
|
|
recent: WorkspaceItem[];
|
|
};
|
|
|
|
const STORAGE_KEY = 'pulse.operator.workspace.v1';
|
|
export const WORKSPACE_EVENT = 'pulse-workspace-changed';
|
|
|
|
const emptyState = (): WorkspaceState => ({ favorites: [], views: [], recent: [] });
|
|
|
|
function validItem(value: unknown): value is WorkspaceItem {
|
|
if (!value || typeof value !== 'object') return false;
|
|
const item = value as Partial<WorkspaceItem>;
|
|
return typeof item.id === 'string' && typeof item.label === 'string' && typeof item.path === 'string'
|
|
&& ['favorite', 'view', 'recent'].includes(String(item.kind)) && typeof item.updatedAt === 'string';
|
|
}
|
|
|
|
export function readWorkspaceState(): WorkspaceState {
|
|
try {
|
|
const raw = window.localStorage.getItem(STORAGE_KEY);
|
|
if (!raw) return emptyState();
|
|
const parsed = JSON.parse(raw) as Partial<WorkspaceState>;
|
|
return {
|
|
favorites: Array.isArray(parsed.favorites) ? parsed.favorites.filter(validItem).slice(0, 24) : [],
|
|
views: Array.isArray(parsed.views) ? parsed.views.filter(validItem).slice(0, 24) : [],
|
|
recent: Array.isArray(parsed.recent) ? parsed.recent.filter(validItem).slice(0, 12) : [],
|
|
};
|
|
} catch {
|
|
return emptyState();
|
|
}
|
|
}
|
|
|
|
function writeWorkspaceState(state: WorkspaceState): void {
|
|
try {
|
|
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
|
} catch {
|
|
return;
|
|
}
|
|
window.dispatchEvent(new CustomEvent(WORKSPACE_EVENT));
|
|
}
|
|
|
|
function timestamp(): string { return new Date().toISOString(); }
|
|
function cleanPath(path: string): string {
|
|
if (!path.startsWith('/')) return '/';
|
|
return path.slice(0, 2048);
|
|
}
|
|
function cleanLabel(label: string): string { return label.trim().slice(0, 160) || 'Pulse'; }
|
|
|
|
export function isFavorite(path: string): boolean {
|
|
const clean = cleanPath(path);
|
|
return readWorkspaceState().favorites.some((item) => item.path === clean);
|
|
}
|
|
|
|
export function toggleFavorite(label: string, path: string): boolean {
|
|
const state = readWorkspaceState();
|
|
const clean = cleanPath(path);
|
|
const existing = state.favorites.some((item) => item.path === clean);
|
|
const favorite: WorkspaceItem = { id: 'favorite:' + clean, label: cleanLabel(label), path: clean, kind: 'favorite', updatedAt: timestamp() };
|
|
state.favorites = existing
|
|
? state.favorites.filter((item) => item.path !== clean)
|
|
: [favorite, ...state.favorites].slice(0, 24);
|
|
writeWorkspaceState(state);
|
|
return !existing;
|
|
}
|
|
|
|
export function saveCurrentView(label: string, path: string): void {
|
|
const state = readWorkspaceState();
|
|
const clean = cleanPath(path);
|
|
const item: WorkspaceItem = { id: 'view:' + clean, label: cleanLabel(label), path: clean, kind: 'view', updatedAt: timestamp() };
|
|
state.views = [item, ...state.views.filter((value) => value.path !== clean)].slice(0, 24);
|
|
writeWorkspaceState(state);
|
|
}
|
|
|
|
export function removeSavedView(path: string): void {
|
|
const state = readWorkspaceState();
|
|
const clean = cleanPath(path);
|
|
state.views = state.views.filter((item) => item.path !== clean);
|
|
writeWorkspaceState(state);
|
|
}
|
|
|
|
export function recordRecent(label: string, path: string): void {
|
|
const state = readWorkspaceState();
|
|
const clean = cleanPath(path);
|
|
if (clean === '/404') return;
|
|
const item: WorkspaceItem = { id: 'recent:' + clean, label: cleanLabel(label), path: clean, kind: 'recent', updatedAt: timestamp() };
|
|
state.recent = [item, ...state.recent.filter((value) => value.path !== clean)].slice(0, 12);
|
|
writeWorkspaceState(state);
|
|
}
|