sidebar-manager-state.tsx 4.4 KB

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