sidebar-manager-state.tsx182 lines · main
1import { ReactNode, useEffect } from 'react'
2import { proxy, snapshot, useSnapshot } from 'valtio'
3
4import useLatest from '@/hooks/misc/useLatest'
5
6type SidebarHandlers = {
7 onOpen?: () => void
8 onClose?: () => void
9}
10
11type ManagedSidebar = SidebarHandlers & {
12 id: string
13 component: () => ReactNode | undefined
14}
15
16type SidebarManagerData = {
17 sidebars: Partial<Record<string, ManagedSidebar>>
18 activeSidebar: ManagedSidebar | undefined
19 pendingSidebarOpen: string | undefined
20}
21
22type SidebarManagerState = SidebarManagerData & {
23 registerSidebar: (
24 id: string,
25 component: () => ReactNode | undefined,
26 handlers?: SidebarHandlers
27 ) => void
28 unregisterSidebar: (id: string) => void
29 openSidebar: (id: string) => void
30 toggleSidebar: (id: string) => void
31 closeSidebar: (id: string) => void
32 isSidebarOpen: (id: string) => boolean
33 closeActive: () => void
34 clearActiveSidebar: () => void
35}
36
37const INITIAL_SIDEBAR_MANAGER_DATA: SidebarManagerData = {
38 sidebars: {},
39 activeSidebar: undefined,
40 pendingSidebarOpen: undefined,
41}
42
43const createSidebarManagerState = () => {
44 const state: SidebarManagerState = proxy({
45 ...INITIAL_SIDEBAR_MANAGER_DATA,
46
47 registerSidebar(
48 id: string,
49 component: () => ReactNode | undefined,
50 handlers: SidebarHandlers = {}
51 ) {
52 state.sidebars[id] = {
53 id,
54 component,
55 ...handlers,
56 }
57
58 // If this sidebar was pending to be opened, open it now
59 if (state.pendingSidebarOpen === id) {
60 state.pendingSidebarOpen = undefined
61 state.openSidebar(id)
62 }
63 },
64
65 unregisterSidebar(id: string) {
66 const panel = state.sidebars[id]
67 if (!panel) return
68
69 delete state.sidebars[id]
70
71 // Clear pending open if this sidebar was pending
72 if (state.pendingSidebarOpen === id) {
73 state.pendingSidebarOpen = undefined
74 }
75
76 if (state.activeSidebar?.id === id) {
77 state.activeSidebar = undefined
78 panel.onClose?.()
79 }
80 },
81
82 openSidebar(id: string) {
83 const panel = state.sidebars[id]
84 if (!panel) {
85 // Queue the request - sidebar will be opened when it gets registered
86 state.pendingSidebarOpen = id
87 return
88 }
89
90 // Clear any pending request since we're opening a sidebar now
91 state.pendingSidebarOpen = undefined
92
93 if (state.activeSidebar?.id === id) {
94 return
95 }
96
97 if (state.activeSidebar && state.activeSidebar.id !== id) {
98 const previousPanel = state.activeSidebar
99 previousPanel?.onClose?.()
100 }
101
102 state.activeSidebar = panel
103 panel.onOpen?.()
104 },
105
106 toggleSidebar(id: string) {
107 const panel = state.sidebars[id]
108 if (!panel) {
109 console.warn(`Sidebar "${id}" is not registered. Register it before toggling.`)
110 return
111 }
112
113 if (state.activeSidebar?.id === id) {
114 panel.onClose?.()
115 state.activeSidebar = undefined
116 return
117 }
118
119 if (state.activeSidebar && state.activeSidebar.id !== id) {
120 const previousPanel = state.activeSidebar
121 previousPanel?.onClose?.()
122 }
123
124 state.activeSidebar = panel
125 panel.onOpen?.()
126 },
127
128 closeSidebar(id: string) {
129 if (state.activeSidebar?.id !== id) return
130
131 const panel = state.sidebars[id]
132 if (!panel) return
133
134 panel.onClose?.()
135 state.activeSidebar = undefined
136 return
137 },
138
139 isSidebarOpen(id: string) {
140 return state.activeSidebar?.id === id
141 },
142
143 closeActive() {
144 if (!state.activeSidebar) return
145 state.activeSidebar?.onClose?.()
146 state.activeSidebar = undefined
147 },
148
149 clearActiveSidebar() {
150 state.activeSidebar = undefined
151 },
152 })
153
154 return state
155}
156
157export const sidebarManagerState = createSidebarManagerState()
158
159export const getSidebarManagerSnapshot = () => snapshot(sidebarManagerState)
160
161export const useSidebarManagerSnapshot = (options?: Parameters<typeof useSnapshot>[1]) =>
162 useSnapshot(sidebarManagerState, options)
163
164export const useRegisterSidebar = (
165 id: string,
166 component: () => ReactNode,
167 handlers: SidebarHandlers = {},
168 enabled?: boolean
169) => {
170 const componentRef = useLatest(component)
171 const handlersRef = useLatest(handlers)
172
173 useEffect(() => {
174 if (enabled) {
175 sidebarManagerState.registerSidebar(id, () => componentRef.current(), handlersRef.current)
176 }
177
178 return () => {
179 sidebarManagerState.unregisterSidebar(id)
180 }
181 }, [id, enabled])
182}