@aiquants/resize-panels 1.8.1 → 2.0.0
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/README.md +213 -108
- package/dist/GlobalDebugOverlay-Cf3GOn2A.cjs +1 -0
- package/dist/{GlobalDebugOverlay-DYCR0FIu.js → GlobalDebugOverlay-DQtUdNeF.js} +28 -28
- package/dist/debugOverlayStore-D7FT6zPE.js +135 -0
- package/dist/debugOverlayStore-DY09saU8.cjs +1 -0
- package/dist/index-DZ0itiBD.js +1299 -0
- package/dist/index-DqATP_BA.cjs +2 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +2 -29
- package/dist/index.js +25 -13
- package/dist/src/GlobalDebugOverlay.d.ts.map +1 -0
- package/dist/src/Panel.d.ts +46 -0
- package/dist/src/Panel.d.ts.map +1 -0
- package/dist/src/PanelDebugInfo.d.ts +34 -0
- package/dist/src/PanelDebugInfo.d.ts.map +1 -0
- package/dist/src/PanelGroup.d.ts.map +1 -0
- package/dist/{PanelResizeHandle.d.ts → src/PanelResizeHandle.d.ts} +1 -1
- package/dist/src/PanelResizeHandle.d.ts.map +1 -0
- package/dist/src/allocateLayout.d.ts +60 -0
- package/dist/src/allocateLayout.d.ts.map +1 -0
- package/dist/src/context.d.ts.map +1 -0
- package/dist/{debugOverlayStore.d.ts → src/debugOverlayStore.d.ts} +10 -2
- package/dist/src/debugOverlayStore.d.ts.map +1 -0
- package/dist/src/hooks.d.ts +48 -0
- package/dist/src/hooks.d.ts.map +1 -0
- package/dist/src/index.d.ts +32 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/reducer.d.ts +49 -0
- package/dist/src/reducer.d.ts.map +1 -0
- package/dist/src/roundHalfToEven.d.ts.map +1 -0
- package/dist/src/types.d.ts +481 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/utils/simple-logger.d.ts.map +1 -0
- package/dist/src/utils.d.ts +164 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/styles/resize-panels.standalone.css +1 -1
- package/dist/tests/support/dom-harness.d.ts +89 -0
- package/dist/tests/support/dom-harness.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/GlobalDebugOverlay.tsx +3 -3
- package/src/Panel.tsx +203 -541
- package/src/PanelDebugInfo.tsx +52 -106
- package/src/PanelGroup.tsx +27 -42
- package/src/PanelResizeHandle.tsx +544 -504
- package/src/allocateLayout.ts +412 -0
- package/src/debugOverlayStore.ts +19 -15
- package/src/hooks.ts +187 -779
- package/src/index.ts +37 -6
- package/src/reducer.ts +364 -1126
- package/src/types.ts +282 -65
- package/src/utils.ts +205 -164
- package/dist/GlobalDebugOverlay-afs0vF-Q.cjs +0 -1
- package/dist/GlobalDebugOverlay.d.ts.map +0 -1
- package/dist/Panel.d.ts +0 -7
- package/dist/Panel.d.ts.map +0 -1
- package/dist/PanelDebugInfo.d.ts +0 -26
- package/dist/PanelDebugInfo.d.ts.map +0 -1
- package/dist/PanelGroup.d.ts.map +0 -1
- package/dist/PanelResizeHandle.d.ts.map +0 -1
- package/dist/context.d.ts.map +0 -1
- package/dist/debugOverlayStore-Cntyxboe.js +0 -141
- package/dist/debugOverlayStore-Dkl-fHoa.cjs +0 -1
- package/dist/debugOverlayStore.d.ts.map +0 -1
- package/dist/hooks.d.ts +0 -45
- package/dist/hooks.d.ts.map +0 -1
- package/dist/index-BzgId8aQ.js +0 -1972
- package/dist/index-xTV-c0PN.cjs +0 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/reducer.d.ts +0 -39
- package/dist/reducer.d.ts.map +0 -1
- package/dist/roundHalfToEven.d.ts.map +0 -1
- package/dist/types.d.ts +0 -253
- package/dist/types.d.ts.map +0 -1
- package/dist/utils/simple-logger.d.ts.map +0 -1
- package/dist/utils.d.ts +0 -61
- package/dist/utils.d.ts.map +0 -1
- /package/dist/{GlobalDebugOverlay.d.ts → src/GlobalDebugOverlay.d.ts} +0 -0
- /package/dist/{PanelGroup.d.ts → src/PanelGroup.d.ts} +0 -0
- /package/dist/{context.d.ts → src/context.d.ts} +0 -0
- /package/dist/{roundHalfToEven.d.ts → src/roundHalfToEven.d.ts} +0 -0
- /package/dist/{utils → src/utils}/simple-logger.d.ts +0 -0
package/src/hooks.ts
CHANGED
|
@@ -1,647 +1,220 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file Custom hooks for resizable panels logic
|
|
3
3
|
* リサイズ可能パネルのロジックのためのカスタムフック
|
|
4
|
+
*
|
|
5
|
+
* The hook owns everything the reducer cannot: it measures the container, keeps the panel order in step
|
|
6
|
+
* with the DOM, and forwards user intent as actions. It never computes a panel size itself.
|
|
7
|
+
* フックはリデューサーが担えない役割だけを持つ: コンテナの計測、DOM 順との同期、利用者の意図のアクション化。
|
|
8
|
+
* パネルサイズ自体はここでは一切計算しない。
|
|
4
9
|
*/
|
|
5
10
|
|
|
6
11
|
import { type CSSProperties, useCallback, useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState } from "react"
|
|
7
12
|
import { usePanelGroup } from "./context"
|
|
8
|
-
import { panelReducer } from "./reducer"
|
|
9
|
-
import type {
|
|
10
|
-
import {
|
|
13
|
+
import { getPanelPreferenceInPixels, initialPanelGroupLayoutState, panelReducer } from "./reducer"
|
|
14
|
+
import type { PanelCollapseDirection, PanelGroupProps, PanelMeasurement, PanelRegistration, PersistedPanelSize, ResizeHandleLayoutData } from "./types"
|
|
15
|
+
import { getContainerSize, getGroupLayoutElements, loadLayout, saveLayout, touchesLayoutElements } from "./utils"
|
|
11
16
|
import { Logger, LogLevel } from "./utils/simple-logger"
|
|
12
17
|
|
|
13
18
|
const logger = new Logger(LogLevel.INFO, "[resize-panels]")
|
|
14
19
|
|
|
15
20
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
21
|
+
* Delay before the layout is written to storage, in milliseconds.
|
|
22
|
+
* レイアウトを保存領域へ書き出すまでの待機時間 (ms)。
|
|
23
|
+
*/
|
|
24
|
+
const AUTO_SAVE_DELAY_MS = 200
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Placeholder that stands for a handle anchor in the group's DOM-order key.
|
|
28
|
+
* グループの DOM 順を表す鍵の中で、ハンドルのアンカーを表す標識。
|
|
29
|
+
*/
|
|
30
|
+
const HANDLE_ANCHOR_ORDER_MARK = "\u0000handle"
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Orchestrate measurement, ordering, persistence and user intent for one panel group.
|
|
34
|
+
* 1 つのパネルグループにおける計測・順序同期・永続化・利用者操作の統括。
|
|
35
|
+
*
|
|
36
|
+
* @param props - Panel group props / パネルグループのプロパティ
|
|
37
|
+
* @returns Group ref, context value and the group's base style / グループ ref・コンテキスト値・基準スタイル
|
|
18
38
|
*/
|
|
19
39
|
export const useResizablePanels = ({ direction, style, onLayout, autoSaveId, showDebugInfo = false }: PanelGroupProps) => {
|
|
20
|
-
const [
|
|
21
|
-
const [containerSize, setContainerSize] = useState({ width: 0, height: 0 })
|
|
22
|
-
const [isContainerReady, setIsContainerReady] = useState(false)
|
|
23
|
-
const [isInitialized, setIsInitialized] = useState(false)
|
|
24
|
-
const [layoutConstraintViolation, setLayoutConstraintViolation] = useState<LayoutConstraintViolation | null>(null)
|
|
40
|
+
const [layout, dispatch] = useReducer(panelReducer, initialPanelGroupLayoutState)
|
|
25
41
|
const [panelMeasurements, setPanelMeasurements] = useState<Record<string, PanelMeasurement>>({})
|
|
26
42
|
const [handleMeasurements, setHandleMeasurements] = useState<Record<string, ResizeHandleLayoutData>>({})
|
|
43
|
+
|
|
27
44
|
const groupRef = useRef<HTMLDivElement>(null)
|
|
45
|
+
const [layoutOrderKey, setLayoutOrderKey] = useState("")
|
|
46
|
+
const registrationSignaturesRef = useRef<Record<string, string>>({})
|
|
28
47
|
const layoutCallbackRef = useRef(onLayout)
|
|
29
|
-
|
|
30
|
-
const initializedPanelCountRef = useRef(0)
|
|
31
|
-
const prevContainerSizeRef = useRef(containerSize)
|
|
32
|
-
const containerInfoRef = useRef(containerSize)
|
|
33
|
-
const isContainerReadyRef = useRef(isContainerReady)
|
|
34
|
-
const violationReasonRef = useRef<LayoutConstraintViolation["reason"] | null>(layoutConstraintViolation?.reason ?? null)
|
|
35
|
-
const panelRegistrationSnapshotRef = useRef<Record<string, string>>({})
|
|
36
|
-
const lastStablePanelsRef = useRef<PanelLayoutData[] | null>(null)
|
|
37
|
-
const previousViolationRef = useRef<LayoutConstraintViolation | null>(null)
|
|
38
|
-
const containerSyncTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
39
|
-
const lastRawContainerSizeRef = useRef({ width: 0, height: 0 })
|
|
40
|
-
const committedContainerSizeRef = useRef({ width: 0, height: 0 })
|
|
41
|
-
const panelSyncTimersRef = useRef<Record<string, ReturnType<typeof setTimeout>>>({})
|
|
42
|
-
const lastRawPanelMeasurementsRef = useRef<Record<string, PanelMeasurement>>({})
|
|
43
|
-
|
|
44
|
-
const clonePanels = useCallback((panelList: PanelLayoutData[]) => {
|
|
45
|
-
return panelList.map((panel) => ({ ...panel }))
|
|
46
|
-
}, [])
|
|
47
|
-
|
|
48
|
-
useEffect(() => {
|
|
49
|
-
containerInfoRef.current = containerSize
|
|
50
|
-
}, [containerSize])
|
|
51
|
-
|
|
52
|
-
useEffect(() => {
|
|
53
|
-
isContainerReadyRef.current = isContainerReady
|
|
54
|
-
}, [isContainerReady])
|
|
55
|
-
|
|
56
|
-
useEffect(() => {
|
|
57
|
-
violationReasonRef.current = layoutConstraintViolation?.reason ?? null
|
|
58
|
-
}, [layoutConstraintViolation?.reason])
|
|
48
|
+
layoutCallbackRef.current = onLayout
|
|
59
49
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
if (panels.length === 0) {
|
|
50
|
+
useLayoutEffect(() => {
|
|
51
|
+
// ResizeObserver でグループの内容領域長を追跡する。観測値は丸めも遅延もせずそのまま確定させる:
|
|
52
|
+
// 端数を落とすと、状態が持つ長さと描画される長さがずれたままになる。
|
|
53
|
+
// 主軸の長さが動かないフレーム (交差軸だけのリサイズ) はここで捨てる。reducer は同一状態を
|
|
54
|
+
// 返すものの、dispatch 自体がレンダーを予約してしまうため
|
|
55
|
+
const element = groupRef.current
|
|
56
|
+
if (!element || typeof ResizeObserver === "undefined") {
|
|
69
57
|
return
|
|
70
58
|
}
|
|
71
|
-
lastStablePanelsRef.current = clonePanels(panels)
|
|
72
|
-
}, [layoutConstraintViolation, panels, direction, containerSize.width, containerSize.height, clonePanels])
|
|
73
|
-
|
|
74
|
-
// 回復 effect から最新のパネル一覧を参照するための ref (依存に panels を入れると復元が違反解除の一回に限定できない)
|
|
75
|
-
const latestPanelsRef = useRef(panels)
|
|
76
|
-
useEffect(() => {
|
|
77
|
-
latestPanelsRef.current = panels
|
|
78
|
-
}, [panels])
|
|
79
59
|
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return
|
|
85
|
-
}
|
|
86
|
-
if (previousViolation && !layoutConstraintViolation && lastStablePanelsRef.current) {
|
|
87
|
-
previousViolationRef.current = null
|
|
88
|
-
// 現在も登録されているパネルだけを復元する。
|
|
89
|
-
// 違反中に登録解除されたパネルをスナップショットから復活させると、対応するコンポーネントが
|
|
90
|
-
// 存在しないゾンビパネルとなり、違反も再発して解除不能になる
|
|
91
|
-
const registeredIds = new Set(latestPanelsRef.current.map((panel) => panel.id))
|
|
92
|
-
const snapshot = clonePanels(lastStablePanelsRef.current).filter((panel) => registeredIds.has(panel.id))
|
|
93
|
-
if (snapshot.length > 0) {
|
|
94
|
-
dispatch({ type: "SET_PANELS", panels: snapshot })
|
|
60
|
+
let lastContent = Number.NaN
|
|
61
|
+
const commitContentLength = (content: number) => {
|
|
62
|
+
if (content === lastContent) {
|
|
63
|
+
return
|
|
95
64
|
}
|
|
96
|
-
|
|
65
|
+
lastContent = content
|
|
66
|
+
dispatch({ type: "SET_CONTAINER_SIZE", containerSize: content })
|
|
97
67
|
}
|
|
98
|
-
previousViolationRef.current = layoutConstraintViolation
|
|
99
|
-
}, [layoutConstraintViolation, clonePanels])
|
|
100
68
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (!groupRef.current) return
|
|
104
|
-
|
|
105
|
-
const resizeObserver = new ResizeObserver((entries) => {
|
|
106
|
-
for (const entry of entries) {
|
|
107
|
-
const targetElement = entry.target as HTMLElement
|
|
108
|
-
// getContainerSize の inner (枠線控除済み) を優先し、取得できない場合は contentRect をフォールバックに使用
|
|
109
|
-
const widthMeasurement = targetElement ? getContainerSize(targetElement, "horizontal") : null
|
|
110
|
-
const heightMeasurement = targetElement ? getContainerSize(targetElement, "vertical") : null
|
|
111
|
-
const measuredWidth = widthMeasurement ? widthMeasurement.inner : 0
|
|
112
|
-
const measuredHeight = heightMeasurement ? heightMeasurement.inner : 0
|
|
113
|
-
const fallbackWidth = entry.contentRect.width
|
|
114
|
-
const fallbackHeight = entry.contentRect.height
|
|
115
|
-
const width = measuredWidth > 0 ? measuredWidth : fallbackWidth
|
|
116
|
-
const height = measuredHeight > 0 ? measuredHeight : fallbackHeight
|
|
117
|
-
lastRawContainerSizeRef.current = { width, height }
|
|
118
|
-
|
|
119
|
-
const prev = committedContainerSizeRef.current
|
|
120
|
-
const isWidthChanged = Math.abs(prev.width - width) >= 0.5
|
|
121
|
-
const isHeightChanged = Math.abs(prev.height - height) >= 0.5
|
|
122
|
-
|
|
123
|
-
if (isWidthChanged || isHeightChanged) {
|
|
124
|
-
// 閾値を超える変化は即座に反映し、待機中の同期をキャンセル
|
|
125
|
-
if (containerSyncTimerRef.current) {
|
|
126
|
-
clearTimeout(containerSyncTimerRef.current)
|
|
127
|
-
containerSyncTimerRef.current = null
|
|
128
|
-
}
|
|
129
|
-
if (!isContainerReady && width > 0 && height > 0) {
|
|
130
|
-
setIsContainerReady(true)
|
|
131
|
-
}
|
|
132
|
-
setContainerSize({ width, height })
|
|
133
|
-
committedContainerSizeRef.current = { width, height }
|
|
134
|
-
} else {
|
|
135
|
-
// 微小な変化はノイズとして即時更新はせず、静止後に最終同期をスケジュール
|
|
136
|
-
if (containerSyncTimerRef.current) {
|
|
137
|
-
clearTimeout(containerSyncTimerRef.current)
|
|
138
|
-
}
|
|
139
|
-
containerSyncTimerRef.current = setTimeout(() => {
|
|
140
|
-
const finalSize = lastRawContainerSizeRef.current
|
|
141
|
-
setContainerSize(finalSize)
|
|
142
|
-
committedContainerSizeRef.current = finalSize
|
|
143
|
-
containerSyncTimerRef.current = null
|
|
144
|
-
}, 150)
|
|
145
|
-
}
|
|
146
|
-
}
|
|
69
|
+
const observer = new ResizeObserver((entries) => {
|
|
70
|
+
commitContentLength(getContainerSize(element, direction, entries[0]).content)
|
|
147
71
|
})
|
|
148
72
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (containerSyncTimerRef.current) {
|
|
153
|
-
clearTimeout(containerSyncTimerRef.current)
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}, [isContainerReady])
|
|
73
|
+
observer.observe(element)
|
|
74
|
+
// 初回描画を正しい配分で塗るための種。観測が届いた時点で厳密値へ置き換わる
|
|
75
|
+
commitContentLength(getContainerSize(element, direction).content)
|
|
157
76
|
|
|
158
|
-
useEffect(() => {
|
|
159
|
-
// アンマウント時に全てのパネル同期タイマーをクリア
|
|
160
77
|
return () => {
|
|
161
|
-
|
|
162
|
-
clearTimeout(timer)
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}, [])
|
|
166
|
-
|
|
167
|
-
useLayoutEffect(() => {
|
|
168
|
-
const prevSize = prevContainerSizeRef.current
|
|
169
|
-
const currentSize = containerSize
|
|
170
|
-
// 0.5px 未満の微小な変化はノイズとして扱い、リサイズイベントの発火を抑制
|
|
171
|
-
const hasResized = direction === "horizontal" ? Math.abs(prevSize.width - currentSize.width) >= 0.5 : Math.abs(prevSize.height - currentSize.height) >= 0.5
|
|
172
|
-
|
|
173
|
-
// 初期化完了後にコンテナサイズが一定以上変化した場合、リサイズイベントを発行
|
|
174
|
-
if (isInitialized && hasResized) {
|
|
175
|
-
const newContainerSize = direction === "horizontal" ? currentSize.width : currentSize.height
|
|
176
|
-
dispatch({ type: "CONTAINER_RESIZED", containerSize: newContainerSize, direction })
|
|
78
|
+
observer.disconnect()
|
|
177
79
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (panel.collapsed) {
|
|
191
|
-
return false
|
|
192
|
-
}
|
|
193
|
-
if (panel.sizeUnit !== "percentage") {
|
|
194
|
-
return false
|
|
195
|
-
}
|
|
196
|
-
// 折り畳み前または直近測定のピクセルサイズを取得し、スナップゼロの復元に備える
|
|
197
|
-
const panelIsCollapsible = panel.collapseFromStart || panel.collapseFromEnd
|
|
198
|
-
const storedPixel = panelIsCollapsible ? (panel.sizeBeforeCollapse ?? panel.measuredPixelSizeBeforeCollapse ?? panel.originalPixelSize ?? panel.measuredPixelSize ?? 0) : (panel.measuredPixelSize ?? panel.originalPixelSize ?? panel.size)
|
|
199
|
-
const hasStoredPercentage = panel.percentageSize !== undefined && panel.percentageSize > 0
|
|
200
|
-
return panel.size <= 0 && (storedPixel > 0 || hasStoredPercentage)
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
// containerSize は getContainerSize(inner) により枠線控除済みの実寸法
|
|
204
|
-
const axisSize = direction === "horizontal" ? containerSize.width : containerSize.height
|
|
205
|
-
const hasContainerMeasurement = axisSize > 0
|
|
206
|
-
|
|
207
|
-
// 制約違反が検出されている場合は初期化をスキップ
|
|
208
|
-
if (layoutConstraintViolation) {
|
|
209
|
-
return
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// コンテナサイズが未計測の場合は初期化をスキップ
|
|
213
|
-
if (!hasContainerMeasurement) {
|
|
80
|
+
}, [direction])
|
|
81
|
+
|
|
82
|
+
const panels = layout.panels
|
|
83
|
+
const panelsRef = useRef(panels)
|
|
84
|
+
panelsRef.current = panels
|
|
85
|
+
|
|
86
|
+
const syncPanelOrder = useCallback(() => {
|
|
87
|
+
// パネルは DOM 順に並んでいることが前提の状態 (保存・onLayout・優先度の同順位比較) なので、
|
|
88
|
+
// 登録順と DOM 順がずれたら DOM 側へ合わせ直す
|
|
89
|
+
const element = groupRef.current
|
|
90
|
+
const currentPanels = panelsRef.current
|
|
91
|
+
if (!element) {
|
|
214
92
|
return
|
|
215
93
|
}
|
|
216
|
-
|
|
217
|
-
//
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
94
|
+
const layoutElements = getGroupLayoutElements(element)
|
|
95
|
+
// ハンドルが別の境界へ移っただけの並べ替えはパネルの id 列を変えない。
|
|
96
|
+
// アンカーの位置も含めた並びを鍵にして、ハンドル側の再検出も同じ変化で起こす
|
|
97
|
+
setLayoutOrderKey((previous) => {
|
|
98
|
+
const next = layoutElements.map((layoutElement) => layoutElement.dataset.panelId ?? HANDLE_ANCHOR_ORDER_MARK).join("\u0000")
|
|
99
|
+
return previous === next ? previous : next
|
|
100
|
+
})
|
|
101
|
+
if (currentPanels.length === 0) {
|
|
221
102
|
return
|
|
222
103
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
return sum + Math.max(0, preferredPixelSize ?? 0)
|
|
229
|
-
}, 0)
|
|
230
|
-
|
|
231
|
-
const activePercentagePanels = panels.filter((panel) => panel.sizeUnit === "percentage" && !panel.collapsed)
|
|
232
|
-
const activePercentageIds = activePercentagePanels.map((panel) => panel.id)
|
|
233
|
-
// パーセンテージ指定パネルの希望ピクセル値を再計算
|
|
234
|
-
const preferredPixelSnapshots = activePercentagePanels.map((panel) => {
|
|
235
|
-
const preferredPercentage = panel.preferredPercentageSize ?? panel.percentageSize
|
|
236
|
-
if (preferredPercentage !== undefined && preferredPercentage > 0) {
|
|
237
|
-
return percentageToPixels(preferredPercentage, axisSize)
|
|
238
|
-
}
|
|
239
|
-
if (panel.size > 0) {
|
|
240
|
-
return panel.size
|
|
241
|
-
}
|
|
242
|
-
return 0
|
|
243
|
-
})
|
|
244
|
-
const totalPreferredPixels = preferredPixelSnapshots.reduce((sum, value) => sum + value, 0)
|
|
245
|
-
|
|
246
|
-
// 固定領域を除いた残余スペースを算出し柔軟パネル側の調整有無を判断
|
|
247
|
-
const remainingSpace = axisSize - allocatedPixelSpace
|
|
248
|
-
const remainingFlexibleSpace = Math.max(0, remainingSpace)
|
|
249
|
-
const hasFlexibleRoom = remainingFlexibleSpace > 0
|
|
250
|
-
|
|
251
|
-
// パネルごとに復元・再計算した最終的な状態を集約
|
|
252
|
-
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Panel resolution logic
|
|
253
|
-
const resolvedPanels = panels.map((panel) => {
|
|
254
|
-
if (panel.collapsed) {
|
|
255
|
-
// 折り畳み中パネルの復元用サイズを優先順で決定 (ピクセル基準)
|
|
256
|
-
// 優先順: sizeBeforeCollapse → measuredPixelSizeBeforeCollapse → originalPixelSize → size (pixels単位のみ)
|
|
257
|
-
const storedPixel = panel.sizeBeforeCollapse ?? panel.measuredPixelSizeBeforeCollapse ?? panel.originalPixelSize ?? (panel.sizeUnit === "pixels" ? panel.size : undefined)
|
|
258
|
-
|
|
259
|
-
// パーセンテージ単位パネルの場合は割合からピクセルへ変換
|
|
260
|
-
const restoredSizeFromPercentage = panel.sizeUnit === "percentage" && panel.percentageSize !== undefined ? percentageToPixels(panel.percentageSize, axisSize) : undefined
|
|
261
|
-
|
|
262
|
-
// 復元後に採用するサイズと測定スナップショットを決定
|
|
263
|
-
const recoveredSize = storedPixel ?? restoredSizeFromPercentage ?? 0
|
|
264
|
-
const recoveredMeasured = panel.measuredPixelSizeBeforeCollapse ?? panel.measuredPixelSize ?? recoveredSize
|
|
265
|
-
|
|
266
|
-
return {
|
|
267
|
-
...panel,
|
|
268
|
-
size: 0,
|
|
269
|
-
percentageSize: 0,
|
|
270
|
-
preferredPercentageSize: panel.preferredPercentageSize,
|
|
271
|
-
sizeBeforeCollapse: recoveredSize,
|
|
272
|
-
percentageSizeBeforeCollapse: panel.percentageSizeBeforeCollapse ?? panel.percentageSize ?? panel.preferredPercentageSize ?? 0,
|
|
273
|
-
preferredPercentageSizeBeforeCollapse: panel.preferredPercentageSizeBeforeCollapse ?? panel.preferredPercentageSize ?? panel.percentageSize ?? undefined,
|
|
274
|
-
preferredPixelSizeBeforeCollapse: panel.preferredPixelSizeBeforeCollapse ?? panel.preferredPixelSize ?? recoveredSize,
|
|
275
|
-
measuredPixelSizeBeforeCollapse: recoveredMeasured,
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (panel.sizeUnit === "pixels" || panel.originalPixelSize !== undefined) {
|
|
280
|
-
// 既にピクセル基準のパネルは測定値を再計算しつつそのまま採用
|
|
281
|
-
const pixelSize = panel.size
|
|
282
|
-
const percentageSize = axisSize > 0 ? toRoundedPercentage(pixelSize, axisSize) : 0
|
|
283
|
-
|
|
284
|
-
const nextPanel = {
|
|
285
|
-
...panel,
|
|
286
|
-
size: pixelSize,
|
|
287
|
-
percentageSize,
|
|
288
|
-
preferredPixelSize: panel.preferredPixelSize ?? panel.originalPixelSize ?? pixelSize,
|
|
289
|
-
preferredPercentageSize: panel.preferredPercentageSize !== undefined ? panel.preferredPercentageSize : percentageSize > 0 ? percentageSize : undefined,
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (panel.collapseFromStart || panel.collapseFromEnd) {
|
|
293
|
-
// 折り畳み可能パネルは復元用のピクセルサイズと測定スナップショットを保持
|
|
294
|
-
const storedSize = panel.sizeBeforeCollapse ?? pixelSize
|
|
295
|
-
const storedMeasured = panel.measuredPixelSizeBeforeCollapse ?? panel.measuredPixelSize ?? storedSize
|
|
296
|
-
nextPanel.sizeBeforeCollapse = storedSize
|
|
297
|
-
nextPanel.measuredPixelSizeBeforeCollapse = storedMeasured
|
|
298
|
-
nextPanel.preferredPixelSizeBeforeCollapse = panel.preferredPixelSizeBeforeCollapse ?? panel.preferredPixelSize ?? storedSize
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
return nextPanel
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
if (panel.sizeUnit === "percentage") {
|
|
305
|
-
// パーセンテージ基準パネルは希望寸法と制約を照らし合わせて実ピクセルへ再配分
|
|
306
|
-
const activeIndex = activePercentageIds.indexOf(panel.id)
|
|
307
|
-
const preferredPixels = activeIndex !== -1 ? preferredPixelSnapshots[activeIndex] : 0
|
|
308
|
-
const minSize = getConstraintInPixels(panel.minSize, 0, axisSize)
|
|
309
|
-
const maxSize = getConstraintInPixels(panel.maxSize, axisSize, axisSize)
|
|
310
|
-
// 過去の測定や折り畳み前サイズを重みとして再利用し柔軟パネルの再配分に最低限の基準を設ける
|
|
311
|
-
const fallbackPixels = Math.max(
|
|
312
|
-
minSize,
|
|
313
|
-
panel.sizeBeforeCollapse ?? 0,
|
|
314
|
-
panel.measuredPixelSizeBeforeCollapse ?? 0,
|
|
315
|
-
panel.measuredPixelSize ?? 0,
|
|
316
|
-
panel.originalPixelSize ?? 0,
|
|
317
|
-
panel.size > 0 ? panel.size : 0,
|
|
318
|
-
panel.percentageSize !== undefined ? percentageToPixels(panel.percentageSize, axisSize) : 0,
|
|
319
|
-
1,
|
|
320
|
-
)
|
|
321
|
-
const effectivePreferred = preferredPixels > 0 ? preferredPixels : fallbackPixels
|
|
322
|
-
|
|
323
|
-
let targetPixels: number
|
|
324
|
-
|
|
325
|
-
if (!hasFlexibleRoom) {
|
|
326
|
-
// 柔軟領域が尽きている場合(ピクセルパネルがコンテナを使い切っている)、
|
|
327
|
-
// パーセンテージパネルは最小サイズ(通常0)にする
|
|
328
|
-
targetPixels = minSize
|
|
329
|
-
} else if (activeIndex !== -1 && totalPreferredPixels > 0) {
|
|
330
|
-
// 希望比率に沿って残余領域を配分
|
|
331
|
-
const ratio = effectivePreferred / totalPreferredPixels
|
|
332
|
-
const candidatePixels = remainingFlexibleSpace * ratio
|
|
333
|
-
targetPixels = clamp(candidatePixels, minSize, maxSize)
|
|
334
|
-
} else {
|
|
335
|
-
// 現状または復元候補の寸法を基準に上限・下限内へ補正
|
|
336
|
-
const currentPixels = panel.percentageSize !== undefined ? percentageToPixels(panel.percentageSize, axisSize) : panel.size
|
|
337
|
-
const baselinePixels = currentPixels > 0 ? currentPixels : effectivePreferred
|
|
338
|
-
targetPixels = clamp(baselinePixels, minSize, maxSize)
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
const nextPercentage = axisSize > 0 ? toRoundedPercentage(targetPixels, axisSize) : 0
|
|
342
|
-
|
|
343
|
-
const nextPanel = {
|
|
344
|
-
...panel,
|
|
345
|
-
size: targetPixels,
|
|
346
|
-
percentageSize: nextPercentage,
|
|
347
|
-
preferredPercentageSize: panel.preferredPercentageSize !== undefined ? panel.preferredPercentageSize : nextPercentage > 0 ? nextPercentage : undefined,
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
if (panel.collapseFromStart || panel.collapseFromEnd) {
|
|
351
|
-
const storedSize = panel.sizeBeforeCollapse ?? targetPixels
|
|
352
|
-
const storedMeasured = panel.measuredPixelSizeBeforeCollapse ?? panel.measuredPixelSize ?? storedSize
|
|
353
|
-
nextPanel.sizeBeforeCollapse = storedSize
|
|
354
|
-
nextPanel.measuredPixelSizeBeforeCollapse = storedMeasured
|
|
355
|
-
nextPanel.preferredPixelSizeBeforeCollapse = panel.preferredPixelSizeBeforeCollapse ?? panel.preferredPixelSize ?? storedSize
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
return nextPanel
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
return panel
|
|
362
|
-
})
|
|
363
|
-
|
|
364
|
-
// 柔軟パネルのインデックスを収集
|
|
365
|
-
const flexibleIndices: number[] = []
|
|
366
|
-
for (let index = 0; index < resolvedPanels.length; index += 1) {
|
|
367
|
-
const panel = resolvedPanels[index]
|
|
368
|
-
if (panel.sizeUnit === "percentage" && !panel.collapsed) {
|
|
369
|
-
flexibleIndices.push(index)
|
|
104
|
+
const ids: string[] = []
|
|
105
|
+
for (const layoutElement of layoutElements) {
|
|
106
|
+
const id = layoutElement.dataset.panelId
|
|
107
|
+
if (id !== undefined) {
|
|
108
|
+
ids.push(id)
|
|
370
109
|
}
|
|
371
110
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
// 差分が閾値を超える場合はピクセルパネルの優先度順に吸収
|
|
378
|
-
if (Math.abs(flexibleDiff) > 0) {
|
|
379
|
-
adjustPixelPanelsByPriority(resolvedPanels, flexibleDiff, axisSize, { enforceAutoMinSize: true })
|
|
111
|
+
if (new Set(ids).size !== ids.length) {
|
|
112
|
+
// 同じ id のパネルが同時に存在すると 1 つの状態を 2 枚が描画し、合計がコンテナを超える
|
|
113
|
+
logger.error("Duplicate panel id in one group.", { ids })
|
|
114
|
+
return
|
|
380
115
|
}
|
|
381
|
-
|
|
382
|
-
dispatch({ type: "SET_PANELS", panels: resolvedPanels })
|
|
383
|
-
initializedPanelCountRef.current = panels.length
|
|
384
|
-
setIsInitialized(true)
|
|
385
|
-
}, [panels, direction, containerSize.width, containerSize.height, isInitialized, layoutConstraintViolation])
|
|
386
|
-
|
|
387
|
-
// 制約チェックを独立して実行(コンテナサイズ変更時にも常に動作)
|
|
388
|
-
useEffect(() => {
|
|
389
|
-
// containerSize は getContainerSize(inner) により枠線控除済みの実寸法
|
|
390
|
-
const axisSize = direction === "horizontal" ? containerSize.width : containerSize.height
|
|
391
|
-
const hasContainerMeasurement = axisSize > 0
|
|
392
|
-
|
|
393
|
-
// 実測コンテナ寸法と登録済みパネルの有無から制約評価の要否を判断
|
|
394
|
-
const shouldEvaluateConstraints = hasContainerMeasurement && panels.length > 0
|
|
395
|
-
|
|
396
|
-
// パネルが存在しなければ制約違反そのものが成立しない
|
|
397
|
-
// (下の stale な違反 totals へのフォールバックに入ると、全パネル登録解除後も違反が永続してしまう)
|
|
398
|
-
if (hasContainerMeasurement && panels.length === 0) {
|
|
399
|
-
if (layoutConstraintViolation) {
|
|
400
|
-
setLayoutConstraintViolation(null)
|
|
401
|
-
}
|
|
116
|
+
if (ids.length !== currentPanels.length || ids.every((id, index) => id === currentPanels[index].id)) {
|
|
402
117
|
return
|
|
403
118
|
}
|
|
119
|
+
dispatch({ type: "REORDER_PANELS", ids })
|
|
120
|
+
}, [])
|
|
404
121
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
}
|
|
411
|
-
const minPixels = getConstraintInPixels(panel.minSize, 0, axisSize)
|
|
412
|
-
return sum + Math.max(0, minPixels)
|
|
413
|
-
}, 0)
|
|
414
|
-
: null
|
|
415
|
-
const totalMaximum = shouldEvaluateConstraints
|
|
416
|
-
? panels.reduce((sum, panel) => {
|
|
417
|
-
if (panel.collapsed) {
|
|
418
|
-
return sum
|
|
419
|
-
}
|
|
420
|
-
const maxPixels = getConstraintInPixels(panel.maxSize, axisSize, axisSize)
|
|
421
|
-
return sum + Math.max(0, maxPixels)
|
|
422
|
-
}, 0)
|
|
423
|
-
: null
|
|
424
|
-
|
|
425
|
-
// 直近の違反情報をフォールバックとして利用しつつ最新計算結果を優先
|
|
426
|
-
const effectiveMinimum = totalMinimum ?? layoutConstraintViolation?.totalMinimumSize ?? null
|
|
427
|
-
const effectiveMaximum = totalMaximum ?? layoutConstraintViolation?.totalMaximumSize ?? null
|
|
428
|
-
|
|
429
|
-
if (!hasContainerMeasurement) {
|
|
430
|
-
// 寸法が取得できていない場合は違反情報のみ更新し後段処理を停止
|
|
431
|
-
if (layoutConstraintViolation) {
|
|
432
|
-
setLayoutConstraintViolation((previous) => {
|
|
433
|
-
if (!previous) {
|
|
434
|
-
return previous
|
|
435
|
-
}
|
|
436
|
-
if (previous.availableContainerSize === axisSize) {
|
|
437
|
-
return previous
|
|
438
|
-
}
|
|
439
|
-
return {
|
|
440
|
-
reason: previous.reason,
|
|
441
|
-
// totalMinimumSize: previous.totalMinimumSize,
|
|
442
|
-
// totalMaximumSize: previous.totalMaximumSize,
|
|
443
|
-
totalMinimumSize: null,
|
|
444
|
-
totalMaximumSize: null,
|
|
445
|
-
availableContainerSize: axisSize,
|
|
446
|
-
}
|
|
447
|
-
})
|
|
448
|
-
}
|
|
122
|
+
useLayoutEffect(() => {
|
|
123
|
+
// 並べ替えは登録・解除を伴わないことがある (JSX 上の順序だけを入れ替えた場合など)。
|
|
124
|
+
// 状態の変化を待つと取りこぼすため、DOM の子要素の変化そのものを監視する
|
|
125
|
+
const element = groupRef.current
|
|
126
|
+
if (!element) {
|
|
449
127
|
return
|
|
450
128
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
const minimumExceeded = effectiveMinimum !== null && effectiveMinimum - axisSize > Number.EPSILON
|
|
454
|
-
const maximumInsufficient = effectiveMaximum !== null && axisSize - effectiveMaximum > Number.EPSILON
|
|
455
|
-
|
|
456
|
-
if (minimumExceeded || maximumInsufficient) {
|
|
457
|
-
const reason = minimumExceeded ? "minimum-exceeded" : "maximum-insufficient"
|
|
458
|
-
const nextViolation: LayoutConstraintViolation = {
|
|
459
|
-
reason,
|
|
460
|
-
totalMinimumSize: effectiveMinimum,
|
|
461
|
-
totalMaximumSize: effectiveMaximum,
|
|
462
|
-
availableContainerSize: axisSize,
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// 同一内容の違反通知を繰り返さないよう差分がある場合のみ更新
|
|
466
|
-
setLayoutConstraintViolation((previous) => {
|
|
467
|
-
if (
|
|
468
|
-
previous &&
|
|
469
|
-
previous.reason === nextViolation.reason &&
|
|
470
|
-
Math.abs((previous.totalMinimumSize ?? 0) - (nextViolation.totalMinimumSize ?? 0)) <= Number.EPSILON &&
|
|
471
|
-
Math.abs((previous.totalMaximumSize ?? 0) - (nextViolation.totalMaximumSize ?? 0)) <= Number.EPSILON &&
|
|
472
|
-
Math.abs(previous.availableContainerSize - nextViolation.availableContainerSize) <= Number.EPSILON
|
|
473
|
-
) {
|
|
474
|
-
return previous
|
|
475
|
-
}
|
|
476
|
-
return nextViolation
|
|
477
|
-
})
|
|
129
|
+
syncPanelOrder()
|
|
130
|
+
if (typeof MutationObserver === "undefined") {
|
|
478
131
|
return
|
|
479
132
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
//
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
const hasMinimumData = effectiveMinimum !== null
|
|
486
|
-
const hasMaximumData = effectiveMaximum !== null
|
|
487
|
-
const canEvaluateMinimum = !requiresMinimum || hasMinimumData
|
|
488
|
-
const canEvaluateMaximum = !requiresMaximum || hasMaximumData
|
|
489
|
-
|
|
490
|
-
if (canEvaluateMinimum && canEvaluateMaximum && !minimumExceeded && !maximumInsufficient) {
|
|
491
|
-
// 条件が揃い再違反が検出されなければ違反状態を解除
|
|
492
|
-
setLayoutConstraintViolation((previous) => (previous ? null : previous))
|
|
133
|
+
const observer = new MutationObserver((records) => {
|
|
134
|
+
// 配置要素に触れない変更は無視する。パネルの内容 (仮想化リストの行など) は毎フレーム
|
|
135
|
+
// 入れ替わるため、すべての変更で走査するとグループ全体に比例した費用を払い続ける
|
|
136
|
+
if (touchesLayoutElements(records)) {
|
|
137
|
+
syncPanelOrder()
|
|
493
138
|
}
|
|
139
|
+
})
|
|
140
|
+
// 中間要素の中での並べ替えも拾うため subtree ごと監視する
|
|
141
|
+
observer.observe(element, { childList: true, subtree: true })
|
|
142
|
+
return () => {
|
|
143
|
+
observer.disconnect()
|
|
494
144
|
}
|
|
495
|
-
}, [
|
|
145
|
+
}, [syncPanelOrder])
|
|
496
146
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}, [initializePanelsIfNeeded])
|
|
501
|
-
|
|
502
|
-
// 最新のレイアウト通知コールバックを参照に保持
|
|
503
|
-
useEffect(() => {
|
|
504
|
-
layoutCallbackRef.current = onLayout
|
|
505
|
-
}, [onLayout])
|
|
147
|
+
useLayoutEffect(() => {
|
|
148
|
+
syncPanelOrder()
|
|
149
|
+
}, [syncPanelOrder, panels])
|
|
506
150
|
|
|
507
|
-
// レイアウト通知のためにパネルサイズ一覧をメモ化
|
|
508
151
|
const panelSizes = useMemo(() => panels.map((panel) => panel.size), [panels])
|
|
152
|
+
const hasMeasuredContainer = layout.containerSize > 0
|
|
509
153
|
|
|
510
|
-
// 初期化前の未確定サイズ (登録直後のパーセンテージパネルは 0) を onLayout や自動保存へ流さないためのゲート。
|
|
511
|
-
// 一度確定したら以降はゼロを含むレイアウト (スナップ等) も正しい状態として通知する
|
|
512
|
-
const hasSettledLayoutRef = useRef(false)
|
|
513
|
-
|
|
514
|
-
// 初期化後にレイアウトリスナーへ寸法を通知
|
|
515
154
|
useLayoutEffect(() => {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
hasSettledLayoutRef.current = false
|
|
519
|
-
return
|
|
520
|
-
}
|
|
521
|
-
if (!hasSettledLayoutRef.current && (isInitialized || panels.every((panel) => panel.collapsed || panel.size > 0))) {
|
|
522
|
-
hasSettledLayoutRef.current = true
|
|
523
|
-
}
|
|
524
|
-
if (!hasSettledLayoutRef.current) {
|
|
155
|
+
// 未計測の 0 配列を通知しない。配分はコンテナ計測後に一度で確定するため、通知は常に確定値になる
|
|
156
|
+
if (!hasMeasuredContainer || panelSizes.length === 0) {
|
|
525
157
|
return
|
|
526
158
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
}
|
|
530
|
-
}, [panelSizes, panels, isInitialized])
|
|
159
|
+
layoutCallbackRef.current?.(panelSizes)
|
|
160
|
+
}, [panelSizes, hasMeasuredContainer])
|
|
531
161
|
|
|
532
|
-
|
|
162
|
+
const restoredKeyRef = useRef<string | null>(null)
|
|
533
163
|
useEffect(() => {
|
|
534
|
-
//
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
164
|
+
// 保存済みレイアウトは識別子ごとに一度だけ適用する
|
|
165
|
+
// (識別子が変われば、その識別子で保存された配置を読み直す)
|
|
166
|
+
if (!autoSaveId || restoredKeyRef.current === autoSaveId || !hasMeasuredContainer || panels.length === 0) {
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
restoredKeyRef.current = autoSaveId
|
|
170
|
+
const entries = loadLayout(autoSaveId)
|
|
171
|
+
if (entries) {
|
|
172
|
+
dispatch({ type: "RESTORE_LAYOUT", entries })
|
|
542
173
|
}
|
|
543
|
-
}, [autoSaveId,
|
|
174
|
+
}, [autoSaveId, hasMeasuredContainer, panels.length])
|
|
544
175
|
|
|
545
|
-
|
|
176
|
+
const pendingSaveRef = useRef<{ id: string; entries: PersistedPanelSize[] } | null>(null)
|
|
546
177
|
useEffect(() => {
|
|
547
|
-
if (!autoSaveId || panels.length === 0
|
|
178
|
+
if (!(autoSaveId && restoredKeyRef.current === autoSaveId && hasMeasuredContainer) || panels.length === 0) {
|
|
548
179
|
return
|
|
549
180
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
181
|
+
const entries = panels.map((panel) => ({ id: panel.id, size: getPanelPreferenceInPixels(panel, layout.containerSize), collapsed: panel.collapsed }))
|
|
182
|
+
pendingSaveRef.current = { id: autoSaveId, entries }
|
|
183
|
+
const timer = setTimeout(() => {
|
|
184
|
+
pendingSaveRef.current = null
|
|
185
|
+
saveLayout(autoSaveId, entries)
|
|
186
|
+
}, AUTO_SAVE_DELAY_MS)
|
|
187
|
+
// ここで保存まで行うと、ドラッグやリサイズの 1 フレームごとに同期 I/O が走り、待機時間が無意味になる
|
|
188
|
+
return () => {
|
|
189
|
+
clearTimeout(timer)
|
|
554
190
|
}
|
|
191
|
+
}, [autoSaveId, panels, layout.containerSize, hasMeasuredContainer])
|
|
555
192
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
return {
|
|
565
|
-
...panel,
|
|
566
|
-
size: clampedSize,
|
|
567
|
-
percentageSize: nextPercentage,
|
|
568
|
-
preferredPercentageSize: nextPercentage > 0 ? nextPercentage : panel.preferredPercentageSize,
|
|
569
|
-
preferredPixelSize: nextPreferredPixelSize,
|
|
570
|
-
}
|
|
571
|
-
})
|
|
572
|
-
dispatch({ type: "SET_PANELS", panels: updatedPanels })
|
|
573
|
-
initializedRef.current = true
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
// 書き出し前に画面を離れた場合でも最後の配置を残す (遷移直前に動かした幅が消えないように)
|
|
195
|
+
return () => {
|
|
196
|
+
const pending = pendingSaveRef.current
|
|
197
|
+
if (pending) {
|
|
198
|
+
pendingSaveRef.current = null
|
|
199
|
+
saveLayout(pending.id, pending.entries)
|
|
200
|
+
}
|
|
574
201
|
}
|
|
575
|
-
}, [
|
|
576
|
-
|
|
577
|
-
const registerPanel = useCallback((panel: PanelLayoutData) => {
|
|
578
|
-
const containerInfo = containerInfoRef.current
|
|
579
|
-
const registry = panelRegistrationSnapshotRef.current
|
|
580
|
-
const registrationSignature = JSON.stringify({
|
|
581
|
-
id: panel.id,
|
|
582
|
-
size: panel.size ?? null,
|
|
583
|
-
percentageSize: panel.percentageSize ?? null,
|
|
584
|
-
preferredPercentageSize: panel.preferredPercentageSize ?? null,
|
|
585
|
-
preferredPixelSize: panel.preferredPixelSize ?? null,
|
|
586
|
-
sizeUnit: panel.sizeUnit,
|
|
587
|
-
originalPixelSize: panel.originalPixelSize ?? null,
|
|
588
|
-
minSize: panel.minSize ?? null,
|
|
589
|
-
maxSize: panel.maxSize ?? null,
|
|
590
|
-
collapseFromStart: panel.collapseFromStart,
|
|
591
|
-
collapseFromEnd: panel.collapseFromEnd,
|
|
592
|
-
collapsed: panel.collapsed ?? false,
|
|
593
|
-
collapsedByDirection: panel.collapsedByDirection ?? null,
|
|
594
|
-
sizeBeforeCollapse: panel.sizeBeforeCollapse ?? null,
|
|
595
|
-
percentageSizeBeforeCollapse: panel.percentageSizeBeforeCollapse ?? null,
|
|
596
|
-
preferredPercentageSizeBeforeCollapse: panel.preferredPercentageSizeBeforeCollapse ?? null,
|
|
597
|
-
preferredPixelSizeBeforeCollapse: panel.preferredPixelSizeBeforeCollapse ?? null,
|
|
598
|
-
pixelAdjustPriority: panel.pixelAdjustPriority ?? null,
|
|
599
|
-
flexAdjustPriority: panel.flexAdjustPriority ?? null,
|
|
600
|
-
autoMinSize: panel.autoMinSize ?? null,
|
|
601
|
-
})
|
|
202
|
+
}, [])
|
|
602
203
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
204
|
+
const registerPanel = useCallback((panel: PanelRegistration) => {
|
|
205
|
+
// 同一内容の再登録は状態を触らない。props が変わっていないのに希望値を配り直すと、
|
|
206
|
+
// 直前のドラッグ結果を巻き戻してしまう
|
|
207
|
+
const signature = JSON.stringify(panel)
|
|
208
|
+
if (registrationSignaturesRef.current[panel.id] === signature) {
|
|
608
209
|
return
|
|
609
210
|
}
|
|
610
|
-
|
|
611
|
-
registry[panel.id] = registrationSignature
|
|
612
|
-
logger.debug("useResizablePanels.registerPanel", {
|
|
613
|
-
panelId: panel.id,
|
|
614
|
-
containerReady: isContainerReadyRef.current,
|
|
615
|
-
violationReason: violationReasonRef.current,
|
|
616
|
-
containerSize: {
|
|
617
|
-
width: containerInfo.width,
|
|
618
|
-
height: containerInfo.height,
|
|
619
|
-
},
|
|
620
|
-
})
|
|
621
|
-
// パネル登録アクションをディスパッチ
|
|
211
|
+
registrationSignaturesRef.current[panel.id] = signature
|
|
622
212
|
dispatch({ type: "REGISTER_PANEL", panel })
|
|
623
213
|
}, [])
|
|
624
214
|
|
|
625
215
|
const unregisterPanel = useCallback((id: string) => {
|
|
626
|
-
|
|
627
|
-
logger.debug("useResizablePanels.unregisterPanel", {
|
|
628
|
-
panelId: id,
|
|
629
|
-
containerReady: isContainerReadyRef.current,
|
|
630
|
-
violationReason: violationReasonRef.current,
|
|
631
|
-
containerSize: {
|
|
632
|
-
width: containerInfo.width,
|
|
633
|
-
height: containerInfo.height,
|
|
634
|
-
},
|
|
635
|
-
})
|
|
636
|
-
// パネル登録解除アクションをディスパッチし、測定情報もクリア
|
|
216
|
+
delete registrationSignaturesRef.current[id]
|
|
637
217
|
dispatch({ type: "UNREGISTER_PANEL", id })
|
|
638
|
-
delete panelRegistrationSnapshotRef.current[id]
|
|
639
|
-
// 保留中の最終整合タイマーも破棄してゴーストエントリの再挿入を防ぐ
|
|
640
|
-
if (panelSyncTimersRef.current[id]) {
|
|
641
|
-
clearTimeout(panelSyncTimersRef.current[id])
|
|
642
|
-
delete panelSyncTimersRef.current[id]
|
|
643
|
-
}
|
|
644
|
-
delete lastRawPanelMeasurementsRef.current[id]
|
|
645
218
|
setPanelMeasurements((previous) => {
|
|
646
219
|
if (!(id in previous)) {
|
|
647
220
|
return previous
|
|
@@ -652,201 +225,41 @@ export const useResizablePanels = ({ direction, style, onLayout, autoSaveId, sho
|
|
|
652
225
|
})
|
|
653
226
|
}, [])
|
|
654
227
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
*/
|
|
659
|
-
const resizePanels = useCallback(
|
|
660
|
-
(leftId: string, rightId: string, leftSize: number, rightSize: number) => {
|
|
661
|
-
// フック側は「制約を大きく外さない初期値」を作る役割、リデューサー側が「最終確定」の役割を担う
|
|
662
|
-
// フック側はドラッグ中の左右ペアに即座に制約を適用して「安全な候補サイズ」を算出するだけで、他パネルとの整合や割合正規化までは扱わない
|
|
663
|
-
// ここではリデューサーの最終判定前にドラッグ中のペアに即応する安全な暫定寸法だけを算出する
|
|
664
|
-
|
|
665
|
-
const currentContainerSize = direction === "horizontal" ? containerSize.width : containerSize.height
|
|
666
|
-
|
|
667
|
-
const leftPanel = panels.find((panel) => panel.id === leftId)
|
|
668
|
-
const rightPanel = panels.find((panel) => panel.id === rightId)
|
|
669
|
-
|
|
670
|
-
// パネル情報が未取得でも従来ロジックで更新を継続
|
|
671
|
-
if (!(leftPanel && rightPanel)) {
|
|
672
|
-
dispatch({ type: "RESIZE_PANELS", leftId, rightId, leftSize, rightSize, containerSize: currentContainerSize })
|
|
673
|
-
return
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
// いずれかのパネルが折りたたまれている場合はリサイズを中断
|
|
677
|
-
if (leftPanel.collapsed || rightPanel.collapsed) {
|
|
678
|
-
return
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
const leftMinSize = getConstraintInPixels(leftPanel.minSize, 0, currentContainerSize)
|
|
682
|
-
const leftMaxSize = getConstraintInPixels(leftPanel.maxSize, currentContainerSize, currentContainerSize)
|
|
683
|
-
const rightMinSize = getConstraintInPixels(rightPanel.minSize, 0, currentContainerSize)
|
|
684
|
-
const rightMaxSize = getConstraintInPixels(rightPanel.maxSize, currentContainerSize, currentContainerSize)
|
|
685
|
-
|
|
686
|
-
let constrainedLeftSize = clamp(leftSize, leftMinSize, leftMaxSize)
|
|
687
|
-
let constrainedRightSize = clamp(rightSize, rightMinSize, rightMaxSize)
|
|
688
|
-
|
|
689
|
-
const targetTotalSize = constrainedLeftSize + constrainedRightSize
|
|
690
|
-
const originalTotalSize = leftSize + rightSize
|
|
691
|
-
|
|
692
|
-
// 制約適用後のサイズ合計と元のサイズ合計が異なる場合は追加の調整を実施
|
|
693
|
-
if (Math.abs(targetTotalSize - originalTotalSize) > 1e-9) {
|
|
694
|
-
const sizeDifference = originalTotalSize - targetTotalSize
|
|
695
|
-
|
|
696
|
-
if (sizeDifference > 0) {
|
|
697
|
-
// 差分が正の場合は両パネルの拡張可能量を按分して補填
|
|
698
|
-
const leftExpansionCapacity = leftMaxSize - constrainedLeftSize
|
|
699
|
-
const rightExpansionCapacity = rightMaxSize - constrainedRightSize
|
|
700
|
-
const totalExpansionCapacity = leftExpansionCapacity + rightExpansionCapacity
|
|
701
|
-
|
|
702
|
-
if (totalExpansionCapacity > 0) {
|
|
703
|
-
const leftExpansion = Math.min(sizeDifference * (leftExpansionCapacity / totalExpansionCapacity), leftExpansionCapacity)
|
|
704
|
-
const rightExpansion = Math.min(sizeDifference - leftExpansion, rightExpansionCapacity)
|
|
705
|
-
|
|
706
|
-
constrainedLeftSize += leftExpansion
|
|
707
|
-
constrainedRightSize += rightExpansion
|
|
708
|
-
}
|
|
709
|
-
} else if (sizeDifference < 0) {
|
|
710
|
-
// 差分が負の場合は両パネルの縮小可能量を按分して削減
|
|
711
|
-
const absoluteDifference = Math.abs(sizeDifference)
|
|
712
|
-
const leftReductionCapacity = constrainedLeftSize - leftMinSize
|
|
713
|
-
const rightReductionCapacity = constrainedRightSize - rightMinSize
|
|
714
|
-
const totalReductionCapacity = leftReductionCapacity + rightReductionCapacity
|
|
715
|
-
|
|
716
|
-
if (totalReductionCapacity > 0) {
|
|
717
|
-
const leftReduction = Math.min(absoluteDifference * (leftReductionCapacity / totalReductionCapacity), leftReductionCapacity)
|
|
718
|
-
const rightReduction = Math.min(absoluteDifference - leftReduction, rightReductionCapacity)
|
|
719
|
-
|
|
720
|
-
constrainedLeftSize -= leftReduction
|
|
721
|
-
constrainedRightSize -= rightReduction
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
// 最終的に制約範囲内に再クランプしてリサイズアクションを発行
|
|
727
|
-
constrainedLeftSize = clamp(constrainedLeftSize, leftMinSize, leftMaxSize)
|
|
728
|
-
constrainedRightSize = clamp(constrainedRightSize, rightMinSize, rightMaxSize)
|
|
729
|
-
|
|
730
|
-
if (Math.abs(constrainedLeftSize + constrainedRightSize - originalTotalSize) > 1e-9) {
|
|
731
|
-
logger.error("Total size after constraints should be close to the original total size.", { constrainedLeftSize, constrainedRightSize, originalTotalSize })
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
dispatch({ type: "RESIZE_PANELS", leftId, rightId, leftSize: constrainedLeftSize, rightSize: constrainedRightSize, containerSize: currentContainerSize })
|
|
735
|
-
},
|
|
736
|
-
[direction, containerSize.width, containerSize.height, panels],
|
|
737
|
-
)
|
|
228
|
+
const resizePanels = useCallback((startId: string, endId: string, startSize: number) => {
|
|
229
|
+
dispatch({ type: "RESIZE_PANELS", startId, endId, startSize })
|
|
230
|
+
}, [])
|
|
738
231
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
*/
|
|
743
|
-
const collapsePanel = useCallback(
|
|
744
|
-
(id: string, from: PanelCollapseDirection) => {
|
|
745
|
-
// 現在のコンテナサイズを渡してパネル折りたたみアクションを発行
|
|
746
|
-
const currentContainerSize = direction === "horizontal" ? containerSize.width : containerSize.height
|
|
747
|
-
dispatch({ type: "COLLAPSE_PANEL", id, from, containerSize: currentContainerSize })
|
|
748
|
-
},
|
|
749
|
-
[direction, containerSize.width, containerSize.height],
|
|
750
|
-
)
|
|
232
|
+
const collapsePanel = useCallback((id: string, from: PanelCollapseDirection) => {
|
|
233
|
+
dispatch({ type: "COLLAPSE_PANEL", id, from })
|
|
234
|
+
}, [])
|
|
751
235
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
*/
|
|
756
|
-
const expandPanel = useCallback(
|
|
757
|
-
(id: string, from: PanelCollapseDirection, targetSize?: number) => {
|
|
758
|
-
// 現在のコンテナサイズを渡してパネル展開アクションを発行
|
|
759
|
-
const currentContainerSize = direction === "horizontal" ? containerSize.width : containerSize.height
|
|
760
|
-
dispatch({ type: "EXPAND_PANEL", id, from, containerSize: currentContainerSize, targetSize })
|
|
761
|
-
},
|
|
762
|
-
[direction, containerSize.width, containerSize.height],
|
|
763
|
-
)
|
|
236
|
+
const expandPanel = useCallback((id: string, from: PanelCollapseDirection, targetSize?: number) => {
|
|
237
|
+
dispatch({ type: "EXPAND_PANEL", id, from, targetSize })
|
|
238
|
+
}, [])
|
|
764
239
|
|
|
765
|
-
|
|
766
|
-
* Retrieve current layout data for the target panel.
|
|
767
|
-
* 対象パネルの最新レイアウトデータの取得。
|
|
768
|
-
*/
|
|
769
|
-
const getPanel = useCallback(
|
|
770
|
-
(id: string) => {
|
|
771
|
-
// 指定 ID のパネルデータを返す
|
|
772
|
-
return panels.find((panel) => panel.id === id)
|
|
773
|
-
},
|
|
774
|
-
[panels],
|
|
775
|
-
)
|
|
240
|
+
const getPanel = useCallback((id: string) => panels.find((panel) => panel.id === id), [panels])
|
|
776
241
|
|
|
777
|
-
/**
|
|
778
|
-
* Synchronize reported panel measurements with local measurement state.
|
|
779
|
-
* パネルから報告された測定情報の状態同期。
|
|
780
|
-
*/
|
|
781
242
|
const reportPanelMeasurement = useCallback((panelId: string, measurement: PanelMeasurement | null) => {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
// (残したままだとパネル登録解除後にタイマーが発火してゴーストエントリを再挿入する)
|
|
785
|
-
if (panelSyncTimersRef.current[panelId]) {
|
|
786
|
-
clearTimeout(panelSyncTimersRef.current[panelId])
|
|
787
|
-
delete panelSyncTimersRef.current[panelId]
|
|
788
|
-
}
|
|
789
|
-
delete lastRawPanelMeasurementsRef.current[panelId]
|
|
790
|
-
|
|
791
|
-
// 測定結果が null の場合はエントリを削除
|
|
792
|
-
setPanelMeasurements((previous) => {
|
|
243
|
+
setPanelMeasurements((previous) => {
|
|
244
|
+
if (measurement === null) {
|
|
793
245
|
if (!(panelId in previous)) {
|
|
794
246
|
return previous
|
|
795
247
|
}
|
|
796
248
|
const next = { ...previous }
|
|
797
249
|
delete next[panelId]
|
|
798
250
|
return next
|
|
799
|
-
}
|
|
800
|
-
return
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
setPanelMeasurements((previous) => {
|
|
251
|
+
}
|
|
804
252
|
const current = previous[panelId]
|
|
805
|
-
|
|
806
|
-
const isPixelStable = current && Math.abs(current.pixelSize - measurement.pixelSize) < 0.1
|
|
807
|
-
const isPercentageStable = current && Math.abs(current.percentageSize - measurement.percentageSize) < 0.01
|
|
808
|
-
|
|
809
|
-
if (isPixelStable && isPercentageStable) {
|
|
810
|
-
// 静止後の最終整合のためにタイマーを再設定
|
|
811
|
-
if (panelSyncTimersRef.current[panelId]) {
|
|
812
|
-
clearTimeout(panelSyncTimersRef.current[panelId])
|
|
813
|
-
}
|
|
814
|
-
lastRawPanelMeasurementsRef.current[panelId] = measurement
|
|
815
|
-
panelSyncTimersRef.current[panelId] = setTimeout(() => {
|
|
816
|
-
const finalMeasurement = lastRawPanelMeasurementsRef.current[panelId]
|
|
817
|
-
if (finalMeasurement) {
|
|
818
|
-
setPanelMeasurements((prev) => ({
|
|
819
|
-
...prev,
|
|
820
|
-
[panelId]: finalMeasurement,
|
|
821
|
-
}))
|
|
822
|
-
}
|
|
823
|
-
delete panelSyncTimersRef.current[panelId]
|
|
824
|
-
}, 200)
|
|
825
|
-
|
|
253
|
+
if (current && current.pixelSize === measurement.pixelSize && current.percentageSize === measurement.percentageSize) {
|
|
826
254
|
return previous
|
|
827
255
|
}
|
|
828
|
-
|
|
829
|
-
// 大きな変化があった場合は待機中のタイマーを解除
|
|
830
|
-
if (panelSyncTimersRef.current[panelId]) {
|
|
831
|
-
clearTimeout(panelSyncTimersRef.current[panelId])
|
|
832
|
-
delete panelSyncTimersRef.current[panelId]
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
return {
|
|
836
|
-
...previous,
|
|
837
|
-
[panelId]: measurement,
|
|
838
|
-
}
|
|
256
|
+
return { ...previous, [panelId]: measurement }
|
|
839
257
|
})
|
|
840
258
|
}, [])
|
|
841
259
|
|
|
842
|
-
/**
|
|
843
|
-
* Synchronize resize-handle measurements while ignoring negligible differences.
|
|
844
|
-
* リサイズハンドル測定値の同期と僅少差分の無視。
|
|
845
|
-
*/
|
|
846
260
|
const reportHandleMeasurement = useCallback((handleId: string, measurement: ResizeHandleLayoutData | null) => {
|
|
847
261
|
setHandleMeasurements((previous) => {
|
|
848
262
|
if (measurement === null) {
|
|
849
|
-
// 測定結果が null の場合はエントリを削除
|
|
850
263
|
if (!(handleId in previous)) {
|
|
851
264
|
return previous
|
|
852
265
|
}
|
|
@@ -854,24 +267,11 @@ export const useResizablePanels = ({ direction, style, onLayout, autoSaveId, sho
|
|
|
854
267
|
delete next[handleId]
|
|
855
268
|
return next
|
|
856
269
|
}
|
|
857
|
-
|
|
858
270
|
const current = previous[handleId]
|
|
859
|
-
|
|
860
|
-
if (
|
|
861
|
-
current &&
|
|
862
|
-
Math.abs(current.thickness - measurement.thickness) <= 0.25 &&
|
|
863
|
-
current.visible === measurement.visible &&
|
|
864
|
-
current.direction === measurement.direction &&
|
|
865
|
-
current.startPanelId === measurement.startPanelId &&
|
|
866
|
-
current.endPanelId === measurement.endPanelId
|
|
867
|
-
) {
|
|
271
|
+
if (current && current.thickness === measurement.thickness && current.visible === measurement.visible && current.direction === measurement.direction && current.startPanelId === measurement.startPanelId && current.endPanelId === measurement.endPanelId) {
|
|
868
272
|
return previous
|
|
869
273
|
}
|
|
870
|
-
|
|
871
|
-
return {
|
|
872
|
-
...previous,
|
|
873
|
-
[handleId]: measurement,
|
|
874
|
-
}
|
|
274
|
+
return { ...previous, [handleId]: measurement }
|
|
875
275
|
})
|
|
876
276
|
}, [])
|
|
877
277
|
|
|
@@ -879,8 +279,8 @@ export const useResizablePanels = ({ direction, style, onLayout, autoSaveId, sho
|
|
|
879
279
|
() => ({
|
|
880
280
|
direction,
|
|
881
281
|
panels,
|
|
882
|
-
containerSize,
|
|
883
|
-
|
|
282
|
+
containerSize: layout.containerSize,
|
|
283
|
+
layoutOrderKey,
|
|
884
284
|
showDebugInfo,
|
|
885
285
|
registerPanel,
|
|
886
286
|
unregisterPanel,
|
|
@@ -892,37 +292,45 @@ export const useResizablePanels = ({ direction, style, onLayout, autoSaveId, sho
|
|
|
892
292
|
panelMeasurements,
|
|
893
293
|
reportHandleMeasurement,
|
|
894
294
|
handleMeasurements,
|
|
895
|
-
layoutConstraintViolation,
|
|
295
|
+
layoutConstraintViolation: layout.violation,
|
|
896
296
|
}),
|
|
897
|
-
[direction, panels, containerSize,
|
|
297
|
+
[direction, panels, layout.containerSize, layoutOrderKey, layout.violation, showDebugInfo, registerPanel, unregisterPanel, resizePanels, collapsePanel, expandPanel, getPanel, reportPanelMeasurement, panelMeasurements, reportHandleMeasurement, handleMeasurements],
|
|
898
298
|
)
|
|
899
299
|
|
|
900
300
|
const groupStyle: CSSProperties = useMemo(
|
|
901
301
|
() => ({
|
|
902
|
-
|
|
903
|
-
flexDirection: direction === "horizontal" ? "row" : "column",
|
|
904
|
-
alignItems: "stretch",
|
|
302
|
+
// 見た目に関わる既定値は利用者が上書きできる
|
|
905
303
|
width: "100%",
|
|
906
304
|
height: "100%",
|
|
907
305
|
minWidth: "0px",
|
|
908
306
|
minHeight: "0px",
|
|
909
|
-
|
|
910
|
-
overflow: "hidden",
|
|
307
|
+
// ハンドルは境界をまたいで描かれるため、その張り出しでスクロールバーを出さない
|
|
308
|
+
overflow: "hidden",
|
|
911
309
|
...style,
|
|
310
|
+
// ここから下はレイアウトの前提そのもの。利用者のスタイルやクラスで変わると
|
|
311
|
+
// 配分結果と描画が一致しなくなるため、最後に適用して上書きを受け付けない:
|
|
312
|
+
// gap はパネルの合計に含まれない隙間を作り、flex-direction の反転は DOM 順と視覚順をずらし、
|
|
313
|
+
// writing-mode は主軸の向きそのものを変えてしまう
|
|
314
|
+
display: "flex",
|
|
315
|
+
flexDirection: direction === "horizontal" ? "row" : "column",
|
|
316
|
+
alignItems: "stretch",
|
|
317
|
+
gap: "0px",
|
|
318
|
+
position: "relative",
|
|
319
|
+
boxSizing: "border-box",
|
|
320
|
+
writingMode: "horizontal-tb",
|
|
912
321
|
}),
|
|
913
322
|
[direction, style],
|
|
914
323
|
)
|
|
915
324
|
|
|
916
|
-
return {
|
|
917
|
-
groupRef,
|
|
918
|
-
contextValue,
|
|
919
|
-
groupStyle,
|
|
920
|
-
}
|
|
325
|
+
return { groupRef, contextValue, groupStyle }
|
|
921
326
|
}
|
|
922
327
|
|
|
923
328
|
/**
|
|
924
|
-
* Provide imperative helpers for toggling a specific panel while preserving stored
|
|
925
|
-
*
|
|
329
|
+
* Provide imperative helpers for toggling a specific panel while preserving its stored preference.
|
|
330
|
+
* 特定パネルの希望値を保ったまま開閉を制御する命令型ユーティリティを提供するカスタムフック。
|
|
331
|
+
*
|
|
332
|
+
* @param panelId - Identifier of the panel to control / 操作対象パネルの識別子
|
|
333
|
+
* @returns Panel state and collapse/expand/toggle helpers / パネル状態と開閉ヘルパー
|
|
926
334
|
*/
|
|
927
335
|
export const usePanelControls = (panelId: string) => {
|
|
928
336
|
const { getPanel, collapsePanel, expandPanel } = usePanelGroup()
|