220 lines
7.2 KiB
TypeScript
220 lines
7.2 KiB
TypeScript
import { create } from "zustand";
|
|
import { persist, createJSONStorage } from "zustand/middleware";
|
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
import { Platform } from "react-native";
|
|
|
|
/**
|
|
* Mobile panel state machine.
|
|
*
|
|
* On mobile, exactly one panel can be visible at a time:
|
|
* - 'agent': Main agent view (no overlay panel)
|
|
* - 'agent-list': Agent list sidebar (left overlay)
|
|
* - 'file-explorer': File explorer sidebar (right overlay)
|
|
*
|
|
* This makes impossible states unrepresentable - you cannot have both
|
|
* sidebars open at the same time on mobile.
|
|
*/
|
|
type MobilePanelView = "agent" | "agent-list" | "file-explorer";
|
|
|
|
/**
|
|
* Desktop sidebar state.
|
|
*
|
|
* On desktop, sidebars are independent toggleable panels that don't overlay
|
|
* the main content - they sit alongside it. Both can be open simultaneously.
|
|
*/
|
|
interface DesktopSidebarState {
|
|
agentListOpen: boolean;
|
|
fileExplorerOpen: boolean;
|
|
}
|
|
|
|
export type ExplorerTab = "changes" | "files";
|
|
export type ViewMode = "list" | "grid";
|
|
export type SortOption = "name" | "modified" | "size";
|
|
|
|
export const DEFAULT_EXPLORER_SIDEBAR_WIDTH = 400;
|
|
export const MIN_EXPLORER_SIDEBAR_WIDTH = 280;
|
|
export const MAX_EXPLORER_SIDEBAR_WIDTH = 800;
|
|
|
|
interface PanelState {
|
|
// Mobile: which panel is currently shown
|
|
mobileView: MobilePanelView;
|
|
|
|
// Desktop: independent sidebar toggles
|
|
desktop: DesktopSidebarState;
|
|
|
|
// File explorer settings (shared between mobile/desktop)
|
|
explorerTab: ExplorerTab;
|
|
explorerWidth: number;
|
|
explorerViewMode: ViewMode;
|
|
explorerSortOption: SortOption;
|
|
|
|
// Actions
|
|
openAgentList: () => void;
|
|
openFileExplorer: () => void;
|
|
closeToAgent: () => void;
|
|
toggleAgentList: () => void;
|
|
toggleFileExplorer: () => void;
|
|
|
|
// File explorer settings actions
|
|
setExplorerTab: (tab: ExplorerTab) => void;
|
|
setExplorerWidth: (width: number) => void;
|
|
setExplorerViewMode: (mode: ViewMode) => void;
|
|
setExplorerSortOption: (option: SortOption) => void;
|
|
}
|
|
|
|
function clampWidth(width: number): number {
|
|
return Math.max(MIN_EXPLORER_SIDEBAR_WIDTH, Math.min(MAX_EXPLORER_SIDEBAR_WIDTH, width));
|
|
}
|
|
|
|
const DEFAULT_DESKTOP_OPEN = Platform.OS === "web";
|
|
|
|
export const usePanelStore = create<PanelState>()(
|
|
persist(
|
|
(set) => ({
|
|
// Mobile always starts at agent view
|
|
mobileView: "agent",
|
|
|
|
// Desktop defaults based on platform
|
|
desktop: {
|
|
agentListOpen: DEFAULT_DESKTOP_OPEN,
|
|
fileExplorerOpen: DEFAULT_DESKTOP_OPEN,
|
|
},
|
|
|
|
// File explorer defaults
|
|
explorerTab: "changes",
|
|
explorerWidth: DEFAULT_EXPLORER_SIDEBAR_WIDTH,
|
|
explorerViewMode: "list",
|
|
explorerSortOption: "name",
|
|
|
|
openAgentList: () =>
|
|
set((state) => ({
|
|
mobileView: "agent-list",
|
|
desktop: { ...state.desktop, agentListOpen: true },
|
|
})),
|
|
|
|
openFileExplorer: () =>
|
|
set((state) => ({
|
|
mobileView: "file-explorer",
|
|
desktop: { ...state.desktop, fileExplorerOpen: true },
|
|
})),
|
|
|
|
closeToAgent: () =>
|
|
set((state) => ({
|
|
mobileView: "agent",
|
|
// On desktop, closing depends on which panel triggered it
|
|
// This is called when closing via gesture/backdrop, so we close the currently active mobile panel
|
|
desktop: {
|
|
agentListOpen:
|
|
state.mobileView === "agent-list" ? false : state.desktop.agentListOpen,
|
|
fileExplorerOpen:
|
|
state.mobileView === "file-explorer" ? false : state.desktop.fileExplorerOpen,
|
|
},
|
|
})),
|
|
|
|
toggleAgentList: () =>
|
|
set((state) => {
|
|
// Mobile: toggle between agent and agent-list
|
|
const newMobileView = state.mobileView === "agent-list" ? "agent" : "agent-list";
|
|
return {
|
|
mobileView: newMobileView,
|
|
desktop: {
|
|
...state.desktop,
|
|
agentListOpen: !state.desktop.agentListOpen,
|
|
},
|
|
};
|
|
}),
|
|
|
|
toggleFileExplorer: () =>
|
|
set((state) => {
|
|
// Mobile: toggle between agent and file-explorer
|
|
const newMobileView = state.mobileView === "file-explorer" ? "agent" : "file-explorer";
|
|
return {
|
|
mobileView: newMobileView,
|
|
desktop: {
|
|
...state.desktop,
|
|
fileExplorerOpen: !state.desktop.fileExplorerOpen,
|
|
},
|
|
};
|
|
}),
|
|
|
|
setExplorerTab: (tab) => set({ explorerTab: tab }),
|
|
setExplorerWidth: (width) => set({ explorerWidth: clampWidth(width) }),
|
|
setExplorerViewMode: (mode) => set({ explorerViewMode: mode }),
|
|
setExplorerSortOption: (option) => set({ explorerSortOption: option }),
|
|
}),
|
|
{
|
|
name: "panel-state",
|
|
storage: createJSONStorage(() => AsyncStorage),
|
|
partialize: (state) => ({
|
|
mobileView: state.mobileView,
|
|
desktop: state.desktop,
|
|
explorerTab: state.explorerTab,
|
|
explorerWidth: state.explorerWidth,
|
|
explorerViewMode: state.explorerViewMode,
|
|
explorerSortOption: state.explorerSortOption,
|
|
}),
|
|
}
|
|
)
|
|
);
|
|
|
|
/**
|
|
* Hook that provides platform-aware panel state.
|
|
*
|
|
* On mobile, uses the state machine (mobileView).
|
|
* On desktop, uses independent booleans (desktop.agentListOpen, desktop.fileExplorerOpen).
|
|
*
|
|
* @param isMobile - Whether the current breakpoint is mobile
|
|
*/
|
|
export function usePanelState(isMobile: boolean) {
|
|
const store = usePanelStore();
|
|
|
|
if (isMobile) {
|
|
return {
|
|
isAgentListOpen: store.mobileView === "agent-list",
|
|
isFileExplorerOpen: store.mobileView === "file-explorer",
|
|
openAgentList: store.openAgentList,
|
|
openFileExplorer: store.openFileExplorer,
|
|
closeAgentList: store.closeToAgent,
|
|
closeFileExplorer: store.closeToAgent,
|
|
toggleAgentList: store.toggleAgentList,
|
|
toggleFileExplorer: store.toggleFileExplorer,
|
|
// Explorer settings
|
|
explorerTab: store.explorerTab,
|
|
explorerWidth: store.explorerWidth,
|
|
explorerViewMode: store.explorerViewMode,
|
|
explorerSortOption: store.explorerSortOption,
|
|
setExplorerTab: store.setExplorerTab,
|
|
setExplorerWidth: store.setExplorerWidth,
|
|
setExplorerViewMode: store.setExplorerViewMode,
|
|
setExplorerSortOption: store.setExplorerSortOption,
|
|
};
|
|
}
|
|
|
|
// Desktop: independent toggles
|
|
return {
|
|
isAgentListOpen: store.desktop.agentListOpen,
|
|
isFileExplorerOpen: store.desktop.fileExplorerOpen,
|
|
openAgentList: store.openAgentList,
|
|
openFileExplorer: store.openFileExplorer,
|
|
closeAgentList: () =>
|
|
usePanelStore.setState((state) => ({
|
|
desktop: { ...state.desktop, agentListOpen: false },
|
|
})),
|
|
closeFileExplorer: () =>
|
|
usePanelStore.setState((state) => ({
|
|
desktop: { ...state.desktop, fileExplorerOpen: false },
|
|
})),
|
|
toggleAgentList: store.toggleAgentList,
|
|
toggleFileExplorer: store.toggleFileExplorer,
|
|
// Explorer settings
|
|
explorerTab: store.explorerTab,
|
|
explorerWidth: store.explorerWidth,
|
|
explorerViewMode: store.explorerViewMode,
|
|
explorerSortOption: store.explorerSortOption,
|
|
setExplorerTab: store.setExplorerTab,
|
|
setExplorerWidth: store.setExplorerWidth,
|
|
setExplorerViewMode: store.setExplorerViewMode,
|
|
setExplorerSortOption: store.setExplorerSortOption,
|
|
};
|
|
}
|