import AsyncStorage from "@react-native-async-storage/async-storage"; import { create } from "zustand"; import { createJSONStorage, persist } from "zustand/middleware"; import { buildWorkspaceTabPersistenceKey, type WorkspaceTab, type WorkspaceTabTarget, } from "@/stores/workspace-tabs-store"; import { clampNormalizedSizes, closeTabInLayout, collectAllPanes, collectAllTabs, createDefaultLayout, findPaneById, findPaneContainingTab, focusPaneInLayout, focusTabInLayout, getTreeDepth, insertSplit, moveTabToPaneInLayout, normalizeLayout, openTabInLayout, removePaneFromTree, removeTabFromTree, reorderFocusedPaneTabsInLayout, reorderPaneTabsInLayout, retargetTabInLayout, splitPaneInLayout, type SplitGroup, type SplitNode, type SplitPane, type WorkspaceLayout, } from "@/stores/workspace-layout-actions"; import { normalizeWorkspaceTabTarget } from "@/utils/workspace-tab-identity"; export { buildWorkspaceTabPersistenceKey }; export { collectAllPanes, collectAllTabs, createDefaultLayout, findPaneById, findPaneContainingTab, getTreeDepth, insertSplit, normalizeLayout, removePaneFromTree, removeTabFromTree, }; export type { SplitGroup, SplitNode, SplitPane, WorkspaceLayout }; interface WorkspaceLayoutStore { layoutByWorkspace: Record; splitSizesByWorkspace: Record>; openTab: (workspaceKey: string, target: WorkspaceTabTarget) => string | null; closeTab: (workspaceKey: string, tabId: string) => void; focusTab: (workspaceKey: string, tabId: string) => void; retargetTab: (workspaceKey: string, tabId: string, target: WorkspaceTabTarget) => string | null; reorderTabs: (workspaceKey: string, tabIds: string[]) => void; getWorkspaceTabs: (workspaceKey: string) => WorkspaceTab[]; splitPane: ( workspaceKey: string, input: { tabId: string; targetPaneId: string; position: "left" | "right" | "top" | "bottom"; } ) => string | null; moveTabToPane: (workspaceKey: string, tabId: string, toPaneId: string) => void; focusPane: (workspaceKey: string, paneId: string) => void; resizeSplit: (workspaceKey: string, groupId: string, sizes: number[]) => void; reorderTabsInPane: (workspaceKey: string, paneId: string, tabIds: string[]) => void; } const MAX_TREE_DEPTH = 4; function trimNonEmpty(value: string | null | undefined): string | null { if (typeof value !== "string") { return null; } const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; } function getWorkspaceLayout(state: Record, workspaceKey: string): WorkspaceLayout { return normalizeLayout(state[workspaceKey] ?? createDefaultLayout()); } export const useWorkspaceLayoutStore = create()( persist( (set, get) => ({ layoutByWorkspace: {}, splitSizesByWorkspace: {}, openTab: (workspaceKey, target) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); const normalizedTarget = normalizeWorkspaceTabTarget(target); if (!normalizedWorkspaceKey || !normalizedTarget) { return null; } const result = openTabInLayout({ layout: getWorkspaceLayout(get().layoutByWorkspace, normalizedWorkspaceKey), target: normalizedTarget, now: Date.now(), }); set((state) => ({ layoutByWorkspace: { ...state.layoutByWorkspace, [normalizedWorkspaceKey]: result.layout, }, })); return result.tabId; }, closeTab: (workspaceKey, tabId) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); const normalizedTabId = trimNonEmpty(tabId); if (!normalizedWorkspaceKey || !normalizedTabId) { return; } set((state) => { const nextLayout = closeTabInLayout({ layout: getWorkspaceLayout(state.layoutByWorkspace, normalizedWorkspaceKey), tabId: normalizedTabId, }); if (!nextLayout) { return state; } return { layoutByWorkspace: { ...state.layoutByWorkspace, [normalizedWorkspaceKey]: nextLayout, }, }; }); }, focusTab: (workspaceKey, tabId) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); const normalizedTabId = trimNonEmpty(tabId); if (!normalizedWorkspaceKey || !normalizedTabId) { return; } set((state) => { const nextLayout = focusTabInLayout({ layout: getWorkspaceLayout(state.layoutByWorkspace, normalizedWorkspaceKey), tabId: normalizedTabId, }); if (!nextLayout) { return state; } return { layoutByWorkspace: { ...state.layoutByWorkspace, [normalizedWorkspaceKey]: nextLayout, }, }; }); }, retargetTab: (workspaceKey, tabId, target) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); const normalizedTabId = trimNonEmpty(tabId); const normalizedTarget = normalizeWorkspaceTabTarget(target); if (!normalizedWorkspaceKey || !normalizedTabId || !normalizedTarget) { return null; } const result = retargetTabInLayout({ layout: getWorkspaceLayout(get().layoutByWorkspace, normalizedWorkspaceKey), tabId: normalizedTabId, target: normalizedTarget, }); if (!result) { return null; } set((state) => ({ layoutByWorkspace: { ...state.layoutByWorkspace, [normalizedWorkspaceKey]: result.layout, }, })); return result.tabId; }, reorderTabs: (workspaceKey, tabIds) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); if (!normalizedWorkspaceKey) { return; } set((state) => { const nextLayout = reorderFocusedPaneTabsInLayout({ layout: getWorkspaceLayout(state.layoutByWorkspace, normalizedWorkspaceKey), tabIds, }); if (!nextLayout) { return state; } return { layoutByWorkspace: { ...state.layoutByWorkspace, [normalizedWorkspaceKey]: nextLayout, }, }; }); }, getWorkspaceTabs: (workspaceKey) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); if (!normalizedWorkspaceKey) { return []; } return collectAllTabs(getWorkspaceLayout(get().layoutByWorkspace, normalizedWorkspaceKey).root); }, splitPane: (workspaceKey, input) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); const normalizedTabId = trimNonEmpty(input.tabId); const normalizedTargetPaneId = trimNonEmpty(input.targetPaneId); if (!normalizedWorkspaceKey || !normalizedTabId || !normalizedTargetPaneId) { return null; } const result = splitPaneInLayout({ layout: getWorkspaceLayout(get().layoutByWorkspace, normalizedWorkspaceKey), tabId: normalizedTabId, targetPaneId: normalizedTargetPaneId, position: input.position, maxTreeDepth: MAX_TREE_DEPTH, createNodeId: (prefix) => { const randomValue = typeof globalThis.crypto?.randomUUID === "function" ? globalThis.crypto.randomUUID() : `${Date.now()}-${Math.random().toString(16).slice(2)}`; return `${prefix}_${randomValue}`; }, }); if (!result) { return null; } set((state) => ({ layoutByWorkspace: { ...state.layoutByWorkspace, [normalizedWorkspaceKey]: result.layout, }, })); return result.paneId; }, moveTabToPane: (workspaceKey, tabId, toPaneId) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); const normalizedTabId = trimNonEmpty(tabId); const normalizedToPaneId = trimNonEmpty(toPaneId); if (!normalizedWorkspaceKey || !normalizedTabId || !normalizedToPaneId) { return; } set((state) => { const nextLayout = moveTabToPaneInLayout({ layout: getWorkspaceLayout(state.layoutByWorkspace, normalizedWorkspaceKey), tabId: normalizedTabId, toPaneId: normalizedToPaneId, }); if (!nextLayout) { return state; } return { layoutByWorkspace: { ...state.layoutByWorkspace, [normalizedWorkspaceKey]: nextLayout, }, }; }); }, focusPane: (workspaceKey, paneId) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); const normalizedPaneId = trimNonEmpty(paneId); if (!normalizedWorkspaceKey || !normalizedPaneId) { return; } set((state) => { const nextLayout = focusPaneInLayout({ layout: getWorkspaceLayout(state.layoutByWorkspace, normalizedWorkspaceKey), paneId: normalizedPaneId, }); if (!nextLayout) { return state; } return { layoutByWorkspace: { ...state.layoutByWorkspace, [normalizedWorkspaceKey]: nextLayout, }, }; }); }, resizeSplit: (workspaceKey, groupId, sizes) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); const normalizedGroupId = trimNonEmpty(groupId); if (!normalizedWorkspaceKey || !normalizedGroupId) { return; } set((state) => ({ splitSizesByWorkspace: { ...state.splitSizesByWorkspace, [normalizedWorkspaceKey]: { ...(state.splitSizesByWorkspace[normalizedWorkspaceKey] ?? {}), [normalizedGroupId]: clampNormalizedSizes(sizes), }, }, })); }, reorderTabsInPane: (workspaceKey, paneId, tabIds) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); const normalizedPaneId = trimNonEmpty(paneId); if (!normalizedWorkspaceKey || !normalizedPaneId) { return; } set((state) => { const nextLayout = reorderPaneTabsInLayout({ layout: getWorkspaceLayout(state.layoutByWorkspace, normalizedWorkspaceKey), paneId: normalizedPaneId, tabIds, }); if (!nextLayout) { return state; } return { layoutByWorkspace: { ...state.layoutByWorkspace, [normalizedWorkspaceKey]: nextLayout, }, }; }); }, }), { name: "workspace-layout-state", version: 1, storage: createJSONStorage(() => AsyncStorage), partialize: (state) => { const layoutByWorkspace: Record = {}; for (const key in state.layoutByWorkspace) { layoutByWorkspace[key] = normalizeLayout(state.layoutByWorkspace[key]); } return { layoutByWorkspace, splitSizesByWorkspace: state.splitSizesByWorkspace, }; }, } ) );