Files
warmbly/web/src/stores/useAppStore.ts
T
Skylinerffm97 3ec2ff7d95 web(unibox): let the scope rail be resized like the conversation list
A drag handle between the scope rail and the list reuses useResizablePane:
drag, arrow keys (Shift for a bigger step), Home/End, Enter or a double-click
to restore the default 220px. The width is kept in the persisted store and
clamped on rehydration; the list re-measures its cap when the rail moves.
2026-09-30 17:18:40 +02:00

125 lines
5.7 KiB
TypeScript

import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
import { useShallow } from 'zustand/react/shallow'
import { createUserSlice, type UserSlice } from './slices/userSlice'
import { createOrganizationSlice, type OrganizationSlice } from './slices/organizationSlice'
import { createUISlice, clampUniboxListWidth, clampUniboxRailWidth, sanitizeNavCollapsedSections, sanitizeUniboxRailFavorites, sanitizeUniboxRailHidden, sanitizeUniboxRailOrder, type UISlice } from './slices/uiSlice'
import { createShortcutSlice, type ShortcutSlice } from './slices/shortcutSlice'
import { createDataSlice, type DataSlice } from './slices/dataSlice'
import { createRealtimeSlice, type RealtimeSlice } from './slices/realtimeSlice'
import { createCRMSlice, type CRMSlice } from './slices/crmSlice'
import { createUniboxSlice, type UniboxSlice } from './slices/uniboxSlice'
import { createAnalyticsSlice, type AnalyticsSlice } from './slices/analyticsSlice'
import { createSubscriptionSlice, type SubscriptionSlice } from './slices/subscriptionSlice'
import { createAPIKeysSlice, type APIKeysSlice } from './slices/apiKeysSlice'
import { createPresenceSlice, type PresenceSlice } from './slices/presenceSlice'
import { createAgentSlice, type AgentSlice } from './slices/agentSlice'
export type AppStore = UserSlice & OrganizationSlice & UISlice & ShortcutSlice & DataSlice
& RealtimeSlice & CRMSlice & UniboxSlice & AnalyticsSlice & SubscriptionSlice & APIKeysSlice & PresenceSlice & AgentSlice
export const useAppStore = create<AppStore>()(
devtools(
persist(
(...args) => ({
...createUserSlice(...args),
...createOrganizationSlice(...args),
...createUISlice(...args),
...createShortcutSlice(...args),
...createDataSlice(...args),
...createRealtimeSlice(...args),
...createCRMSlice(...args),
...createUniboxSlice(...args),
...createAnalyticsSlice(...args),
...createSubscriptionSlice(...args),
...createAPIKeysSlice(...args),
...createPresenceSlice(...args),
...createAgentSlice(...args),
}),
{
name: 'warmbly-storage',
// Rehydration does not go through the slice setters, so re-clamp the
// stored values that have bounds. Without this a value from an older build
// (or a hand-edited one) renders as `width: NaNpx`.
merge: (persisted, current) => {
const p = (persisted ?? {}) as Partial<AppStore>
return {
...current,
...p,
uniboxListWidth: clampUniboxListWidth(p.uniboxListWidth),
uniboxRailWidth: clampUniboxRailWidth(p.uniboxRailWidth),
navCollapsedSections: sanitizeNavCollapsedSections(p.navCollapsedSections),
uniboxRailFolded: sanitizeNavCollapsedSections(p.uniboxRailFolded),
uniboxRailHidden: sanitizeUniboxRailHidden(p.uniboxRailHidden),
uniboxRailOrder: sanitizeUniboxRailOrder(p.uniboxRailOrder),
uniboxRailSectionOrder: sanitizeUniboxRailHidden(p.uniboxRailSectionOrder),
uniboxRailFavorites: sanitizeUniboxRailFavorites(p.uniboxRailFavorites),
}
},
partialize: (state) => ({
// Only persist UI preferences
theme: state.theme,
navCollapsed: state.navCollapsed,
navCollapsedSections: state.navCollapsedSections,
// Assistant panel layout (edge + width + floating window geometry)
agentSide: state.agentSide,
agentWidth: state.agentWidth,
agentFloating: state.agentFloating,
agentFloatRect: state.agentFloatRect,
// Unibox layout (pane widths + CRM rail default)
uniboxListWidth: state.uniboxListWidth,
uniboxRailWidth: state.uniboxRailWidth,
uniboxContactRailOpen: state.uniboxContactRailOpen,
// Unibox scope rail (folds, hidden rows, row and section order, favorites)
uniboxRailFolded: state.uniboxRailFolded,
uniboxRailHidden: state.uniboxRailHidden,
uniboxRailOrder: state.uniboxRailOrder,
uniboxRailSectionOrder: state.uniboxRailSectionOrder,
uniboxRailFavorites: state.uniboxRailFavorites,
// Persist current organization selection
currentOrganization: state.currentOrganization,
}),
}
),
{
name: 'Warmbly Store',
}
)
)
// Selectors for commonly used state combinations
export const useUser = () => useAppStore((state) => state.user)
export const useIsAuthenticated = () => useAppStore((state) => state.isAuthenticated)
export const useTheme = () =>
useAppStore(useShallow((state) => ({ theme: state.theme, resolvedTheme: state.resolvedTheme })))
export const useSidebar = () =>
useAppStore(useShallow((state) => ({
collapsed: state.navCollapsed,
mobileOpen: state.sidebarMobileOpen,
toggle: state.toggleSidebar,
setCollapsed: state.setSidebarCollapsed,
setMobileOpen: state.setSidebarMobileOpen,
})))
export const useCurrentOrg = () => useAppStore((state) => state.currentOrganization)
export const useOrganizations = () =>
useAppStore(useShallow((state) => ({
organizations: state.organizations,
current: state.currentOrganization,
switch: state.switchOrganization,
})))
export const useCachedData = () =>
useAppStore(useShallow((state) => ({
campaigns: state.campaigns,
emails: state.emails,
tags: state.tags,
folders: state.folders,
categories: state.categories,
})))
export const useConnectionStatus = () =>
useAppStore(useShallow((state) => ({
status: state.connectionStatus,
quality: state.connectionQuality,
reconnectAttempt: state.reconnectAttempt,
})))
export const useUnseenCount = () => useAppStore((state) => state.unseenCount)