@prmichaelsen/acp-visualizer 0.14.0 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createContext, useContext, useState, ReactNode } from 'react'
|
|
1
|
+
import { createContext, useContext, useState, useCallback, ReactNode } from 'react'
|
|
2
2
|
|
|
3
3
|
type PanelContent =
|
|
4
4
|
| { type: 'milestone'; id: string }
|
|
@@ -22,6 +22,7 @@ const SidePanelContext = createContext<SidePanelContextValue | undefined>(undefi
|
|
|
22
22
|
const MIN_WIDTH = 300
|
|
23
23
|
const DEFAULT_WIDTH = 500
|
|
24
24
|
const STORAGE_KEY = 'acp-visualizer.side-panel-size'
|
|
25
|
+
const PANEL_PARAM = 'panel'
|
|
25
26
|
|
|
26
27
|
function loadWidth(): number {
|
|
27
28
|
if (typeof window === 'undefined') return DEFAULT_WIDTH
|
|
@@ -33,36 +34,91 @@ function loadWidth(): number {
|
|
|
33
34
|
return DEFAULT_WIDTH
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
/** Serialize panel content to a URL search param value */
|
|
38
|
+
function serializePanel(content: PanelContent): string | null {
|
|
39
|
+
if (!content) return null
|
|
40
|
+
switch (content.type) {
|
|
41
|
+
case 'milestone': return `milestone:${content.id}`
|
|
42
|
+
case 'task': return `task:${content.id}`
|
|
43
|
+
case 'document': return `doc:${content.dirPath}:${content.slug}`
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Parse panel content from a URL search param value */
|
|
48
|
+
function deserializePanel(value: string | null): PanelContent {
|
|
49
|
+
if (!value) return null
|
|
50
|
+
if (value.startsWith('milestone:')) {
|
|
51
|
+
return { type: 'milestone', id: value.slice('milestone:'.length) }
|
|
52
|
+
}
|
|
53
|
+
if (value.startsWith('task:')) {
|
|
54
|
+
return { type: 'task', id: value.slice('task:'.length) }
|
|
55
|
+
}
|
|
56
|
+
if (value.startsWith('doc:')) {
|
|
57
|
+
const rest = value.slice('doc:'.length)
|
|
58
|
+
const colonIdx = rest.indexOf(':')
|
|
59
|
+
if (colonIdx === -1) return null
|
|
60
|
+
return { type: 'document', dirPath: rest.slice(0, colonIdx), slug: rest.slice(colonIdx + 1) }
|
|
61
|
+
}
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Read initial panel state from URL on first render */
|
|
66
|
+
function loadPanelFromUrl(): PanelContent {
|
|
67
|
+
if (typeof window === 'undefined') return null
|
|
68
|
+
const params = new URLSearchParams(window.location.search)
|
|
69
|
+
return deserializePanel(params.get(PANEL_PARAM))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Update the panel search param without pushing to browser history */
|
|
73
|
+
function syncPanelToUrl(content: PanelContent) {
|
|
74
|
+
if (typeof window === 'undefined') return
|
|
75
|
+
const url = new URL(window.location.href)
|
|
76
|
+
const serialized = serializePanel(content)
|
|
77
|
+
if (serialized) {
|
|
78
|
+
url.searchParams.set(PANEL_PARAM, serialized)
|
|
79
|
+
} else {
|
|
80
|
+
url.searchParams.delete(PANEL_PARAM)
|
|
81
|
+
}
|
|
82
|
+
window.history.replaceState(window.history.state, '', url.toString())
|
|
83
|
+
}
|
|
84
|
+
|
|
36
85
|
export function SidePanelProvider({ children }: { children: ReactNode }) {
|
|
37
|
-
const [content, setContent] = useState<PanelContent>(
|
|
38
|
-
const [isOpen, setIsOpen] = useState(
|
|
86
|
+
const [content, setContent] = useState<PanelContent>(loadPanelFromUrl)
|
|
87
|
+
const [isOpen, setIsOpen] = useState(() => loadPanelFromUrl() !== null)
|
|
39
88
|
const [width, setWidthState] = useState(loadWidth)
|
|
40
89
|
|
|
41
|
-
const openMilestone = (id: string) => {
|
|
42
|
-
|
|
90
|
+
const openMilestone = useCallback((id: string) => {
|
|
91
|
+
const panel: PanelContent = { type: 'milestone', id }
|
|
92
|
+
setContent(panel)
|
|
43
93
|
setIsOpen(true)
|
|
44
|
-
|
|
94
|
+
syncPanelToUrl(panel)
|
|
95
|
+
}, [])
|
|
45
96
|
|
|
46
|
-
const openTask = (id: string) => {
|
|
47
|
-
|
|
97
|
+
const openTask = useCallback((id: string) => {
|
|
98
|
+
const panel: PanelContent = { type: 'task', id }
|
|
99
|
+
setContent(panel)
|
|
48
100
|
setIsOpen(true)
|
|
49
|
-
|
|
101
|
+
syncPanelToUrl(panel)
|
|
102
|
+
}, [])
|
|
50
103
|
|
|
51
|
-
const openDocument = (dirPath: string, slug: string) => {
|
|
52
|
-
|
|
104
|
+
const openDocument = useCallback((dirPath: string, slug: string) => {
|
|
105
|
+
const panel: PanelContent = { type: 'document', dirPath, slug }
|
|
106
|
+
setContent(panel)
|
|
53
107
|
setIsOpen(true)
|
|
54
|
-
|
|
108
|
+
syncPanelToUrl(panel)
|
|
109
|
+
}, [])
|
|
55
110
|
|
|
56
|
-
const close = () => {
|
|
111
|
+
const close = useCallback(() => {
|
|
57
112
|
setIsOpen(false)
|
|
113
|
+
syncPanelToUrl(null)
|
|
58
114
|
setTimeout(() => setContent(null), 300) // Wait for animation
|
|
59
|
-
}
|
|
115
|
+
}, [])
|
|
60
116
|
|
|
61
|
-
const setWidth = (newWidth: number) => {
|
|
117
|
+
const setWidth = useCallback((newWidth: number) => {
|
|
62
118
|
const clamped = Math.max(MIN_WIDTH, newWidth)
|
|
63
119
|
setWidthState(clamped)
|
|
64
120
|
localStorage.setItem(STORAGE_KEY, String(clamped))
|
|
65
|
-
}
|
|
121
|
+
}, [])
|
|
66
122
|
|
|
67
123
|
return (
|
|
68
124
|
<SidePanelContext.Provider value={{ content, isOpen, width, openMilestone, openTask, openDocument, close, setWidth }}>
|