@aiquants/resize-panels 1.7.2 → 1.8.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/src/context.ts ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @file PanelGroup context for managing panel state
3
+ * パネル状態を管理するための PanelGroup コンテキスト
4
+ */
5
+
6
+ import { createContext, useContext } from "react"
7
+ import type { PanelGroupContextValue } from "./types"
8
+
9
+ /**
10
+ * Provide shared panel group state to descendant components.
11
+ * 子孫コンポーネントへパネルグループの共有状態を提供するコンテキスト。
12
+ */
13
+ export const PanelGroupContext = createContext<PanelGroupContextValue | null>(null)
14
+
15
+ /**
16
+ * Access panel group behaviors and layout data from the nearest provider.
17
+ * 最も近いプロバイダーからパネルグループの振る舞いとレイアウトデータを取得するフック。
18
+ */
19
+ export const usePanelGroup = () => {
20
+ const context = useContext(PanelGroupContext)
21
+ if (!context) {
22
+ throw new Error("usePanelGroup must be used within a PanelGroup")
23
+ }
24
+ return context
25
+ }
@@ -0,0 +1,351 @@
1
+ /**
2
+ * @file Debug overlay store utilities
3
+ * デバッグオーバーレイのストアユーティリティ
4
+ */
5
+
6
+ import { useSyncExternalStore } from "react"
7
+ import type { ContainerSize, PanelDirection, PanelLayoutData, ResizeHandleLayoutData } from "./types"
8
+
9
+ /**
10
+ * Snapshot of the debug overlay state across panel groups.
11
+ * デバッグオーバーレイのスナップショット状態を表す構造体。
12
+ */
13
+ export type DebugOverlaySnapshot = {
14
+ ownerId: string | null
15
+ groups: DebugGroupState[]
16
+ }
17
+
18
+ /**
19
+ * Aggregated state for a panel group tracked by the debug overlay.
20
+ * デバッグオーバーレイが追跡するパネルグループの集約状態。
21
+ */
22
+ export type DebugGroupState = {
23
+ groupId: string
24
+ displayName: string
25
+ direction: PanelDirection
26
+ containerSize: ContainerSize
27
+ panels: PanelLayoutData[]
28
+ handles: ResizeHandleLayoutData[]
29
+ index: number
30
+ }
31
+
32
+ type DebugGroupUpdate = {
33
+ displayName: string
34
+ direction: PanelDirection
35
+ containerSize: ContainerSize
36
+ panels: PanelLayoutData[]
37
+ handles: ResizeHandleLayoutData[]
38
+ }
39
+
40
+ type StoreListener = () => void
41
+
42
+ type DebugGroupEntry = DebugGroupState & {
43
+ serializedPanels: string
44
+ serializedHandles: string
45
+ }
46
+
47
+ const serializePanels = (panels: PanelLayoutData[]) => {
48
+ return JSON.stringify(
49
+ panels.map((panel) => ({
50
+ id: panel.id,
51
+ size: panel.size,
52
+ percentageSize: panel.percentageSize ?? null,
53
+ preferredPercentageSize: panel.preferredPercentageSize ?? null,
54
+ preferredPixelSize: panel.preferredPixelSize ?? null,
55
+ sizeUnit: panel.sizeUnit,
56
+ originalPixelSize: panel.originalPixelSize ?? null,
57
+ minSize: panel.minSize ?? null,
58
+ maxSize: panel.maxSize ?? null,
59
+ autoMinSize: panel.autoMinSize ?? null,
60
+ collapseFromStart: panel.collapseFromStart,
61
+ collapseFromEnd: panel.collapseFromEnd,
62
+ collapsedByDirection: panel.collapsedByDirection ?? null,
63
+ collapsed: panel.collapsed ?? false,
64
+ sizeBeforeCollapse: panel.sizeBeforeCollapse ?? null,
65
+ preferredPercentageSizeBeforeCollapse: panel.preferredPercentageSizeBeforeCollapse ?? null,
66
+ preferredPixelSizeBeforeCollapse: panel.preferredPixelSizeBeforeCollapse ?? null,
67
+ measuredPixelSizeBeforeCollapse: panel.measuredPixelSizeBeforeCollapse ?? null,
68
+ pixelAdjustPriority: panel.pixelAdjustPriority ?? null,
69
+ flexAdjustPriority: panel.flexAdjustPriority ?? null,
70
+ measuredPixelSize: panel.measuredPixelSize ?? null,
71
+ measuredPercentageSize: panel.measuredPercentageSize ?? null,
72
+ })),
73
+ )
74
+ }
75
+
76
+ const serializeHandles = (handles: ResizeHandleLayoutData[]) => {
77
+ return JSON.stringify(
78
+ handles.map((handle) => ({
79
+ id: handle.id,
80
+ thickness: handle.thickness,
81
+ visible: handle.visible,
82
+ direction: handle.direction,
83
+ startPanelId: handle.startPanelId ?? null,
84
+ endPanelId: handle.endPanelId ?? null,
85
+ })),
86
+ )
87
+ }
88
+
89
+ const createDebugOverlayStore = () => {
90
+ const listeners = new Set<StoreListener>()
91
+ const groups = new Map<string, DebugGroupEntry>()
92
+ const enabledGroups = new Set<string>()
93
+ let ownerId: string | null = null
94
+ let orderCounter = 0
95
+ let snapshotDirty = true
96
+ let cachedGroups: DebugGroupState[] = []
97
+ let cachedSnapshot: DebugOverlaySnapshot = {
98
+ ownerId: null,
99
+ groups: cachedGroups,
100
+ }
101
+
102
+ // リスナーへ変更通知
103
+ const notify = () => {
104
+ for (const listener of listeners) {
105
+ listener()
106
+ }
107
+ }
108
+
109
+ // スナップショット再計算フラグを設定して通知
110
+ const emitChange = () => {
111
+ snapshotDirty = true
112
+ notify()
113
+ }
114
+
115
+ // 有効なグループから次のオーバーレイオーナーを決定
116
+ const resolveOwner = () => {
117
+ const previousOwner = ownerId
118
+ if (previousOwner && enabledGroups.has(previousOwner)) {
119
+ return false
120
+ }
121
+ ownerId = null
122
+ const iterator = enabledGroups.values().next()
123
+ if (!iterator.done) {
124
+ ownerId = iterator.value
125
+ }
126
+ return previousOwner !== ownerId
127
+ }
128
+
129
+ // リスナー登録とサブスクリプション管理
130
+ const subscribe = (listener: StoreListener) => {
131
+ listeners.add(listener)
132
+ return () => {
133
+ listeners.delete(listener)
134
+ }
135
+ }
136
+
137
+ // スナップショット取得 (必要に応じて再構築)
138
+ const getSnapshot = (): DebugOverlaySnapshot => {
139
+ if (resolveOwner()) {
140
+ snapshotDirty = true
141
+ }
142
+
143
+ if (snapshotDirty) {
144
+ const sorted = Array.from(groups.values()).sort((a, b) => a.index - b.index)
145
+ cachedGroups = sorted.map(({ serializedPanels, serializedHandles, ...publicGroup }) => publicGroup)
146
+ cachedSnapshot = {
147
+ ownerId,
148
+ groups: cachedGroups,
149
+ }
150
+ snapshotDirty = false
151
+ }
152
+
153
+ return cachedSnapshot
154
+ }
155
+
156
+ // グループを有効化してオーバーレイ対象へ追加
157
+ const enableGroup = (groupId: string) => {
158
+ const previousSize = enabledGroups.size
159
+ enabledGroups.add(groupId)
160
+ if (enabledGroups.size === previousSize) {
161
+ return
162
+ }
163
+ if (resolveOwner()) {
164
+ emitChange()
165
+ }
166
+ }
167
+
168
+ // グループを無効化してオーバーレイ対象から除外
169
+ const disableGroup = (groupId: string) => {
170
+ if (!enabledGroups.delete(groupId)) {
171
+ return
172
+ }
173
+ if (resolveOwner()) {
174
+ emitChange()
175
+ }
176
+ }
177
+
178
+ // グループ状態を更新 (差分検出して変更時のみ通知)
179
+ const updateGroup = (groupId: string, update: DebugGroupUpdate) => {
180
+ const existing = groups.get(groupId)
181
+ const index = existing?.index ?? orderCounter++
182
+ const serializedPanels = serializePanels(update.panels)
183
+ const serializedHandles = serializeHandles(update.handles)
184
+
185
+ // 各プロパティの変更検出
186
+ const displayChanged = existing?.displayName !== update.displayName
187
+ const directionChanged = existing?.direction !== update.direction
188
+ const containerChanged = existing ? existing.containerSize.width !== update.containerSize.width || existing.containerSize.height !== update.containerSize.height : true
189
+
190
+ // パネル配列の変更を詳細検証
191
+ let panelsChanged = existing?.serializedPanels !== serializedPanels
192
+ if (!panelsChanged && existing) {
193
+ if (existing.panels.length !== update.panels.length) {
194
+ panelsChanged = true
195
+ } else {
196
+ for (let index = 0; index < update.panels.length; index += 1) {
197
+ const prevPanel = existing.panels[index]
198
+ const nextPanel = update.panels[index]
199
+ if (!(prevPanel && nextPanel)) {
200
+ panelsChanged = true
201
+ break
202
+ }
203
+ if (
204
+ prevPanel.id !== nextPanel.id ||
205
+ prevPanel.size !== nextPanel.size ||
206
+ prevPanel.percentageSize !== nextPanel.percentageSize ||
207
+ prevPanel.collapsed !== nextPanel.collapsed ||
208
+ prevPanel.measuredPixelSize !== nextPanel.measuredPixelSize ||
209
+ prevPanel.measuredPercentageSize !== nextPanel.measuredPercentageSize
210
+ ) {
211
+ panelsChanged = true
212
+ break
213
+ }
214
+ }
215
+ }
216
+ }
217
+
218
+ // ハンドル配列の変更を詳細検証
219
+ let handlesChanged = existing?.serializedHandles !== serializedHandles
220
+ if (!handlesChanged && existing) {
221
+ if (existing.handles.length !== update.handles.length) {
222
+ handlesChanged = true
223
+ } else {
224
+ for (let index = 0; index < update.handles.length; index += 1) {
225
+ const prevHandle = existing.handles[index]
226
+ const nextHandle = update.handles[index]
227
+ if (!(prevHandle && nextHandle)) {
228
+ handlesChanged = true
229
+ break
230
+ }
231
+ if (
232
+ prevHandle.id !== nextHandle.id ||
233
+ prevHandle.thickness !== nextHandle.thickness ||
234
+ prevHandle.visible !== nextHandle.visible ||
235
+ prevHandle.direction !== nextHandle.direction ||
236
+ prevHandle.startPanelId !== nextHandle.startPanelId ||
237
+ prevHandle.endPanelId !== nextHandle.endPanelId
238
+ ) {
239
+ handlesChanged = true
240
+ break
241
+ }
242
+ }
243
+ }
244
+ }
245
+
246
+ // 空のグループは初回のみスキップ
247
+ if (!existing && update.panels.length === 0 && update.handles.length === 0) {
248
+ return
249
+ }
250
+
251
+ // 変更なければスキップ
252
+ if (existing && !displayChanged && !directionChanged && !containerChanged && !panelsChanged && !handlesChanged) {
253
+ return
254
+ }
255
+
256
+ // 状態をクローンして保存
257
+ const clonedPanels = update.panels.map((panel) => ({ ...panel }))
258
+ const clonedHandles = update.handles.map((handle) => ({ ...handle }))
259
+ const clonedContainer = { ...update.containerSize }
260
+
261
+ groups.set(groupId, {
262
+ groupId,
263
+ displayName: update.displayName,
264
+ direction: update.direction,
265
+ containerSize: clonedContainer,
266
+ panels: clonedPanels,
267
+ handles: clonedHandles,
268
+ index,
269
+ serializedPanels,
270
+ serializedHandles,
271
+ })
272
+
273
+ emitChange()
274
+ }
275
+
276
+ // グループを削除
277
+ const removeGroup = (groupId: string) => {
278
+ const removed = groups.delete(groupId)
279
+ const disabledNow = enabledGroups.delete(groupId)
280
+ const ownerChanged = resolveOwner()
281
+
282
+ if (removed || disabledNow || ownerChanged) {
283
+ emitChange()
284
+ }
285
+ }
286
+
287
+ return {
288
+ subscribe,
289
+ getSnapshot,
290
+ getOwnerId: () => {
291
+ if (resolveOwner()) {
292
+ snapshotDirty = true
293
+ }
294
+ return ownerId
295
+ },
296
+ enableGroup,
297
+ disableGroup,
298
+ updateGroup,
299
+ removeGroup,
300
+ }
301
+ }
302
+
303
+ const debugOverlayStore = createDebugOverlayStore()
304
+
305
+ /**
306
+ * Subscribe to the identifier of the panel group that renders the overlay.
307
+ * オーバーレイを描画するパネルグループ識別子を購読するフック。
308
+ */
309
+ export const useDebugOverlayOwnerId = () => {
310
+ return useSyncExternalStore(debugOverlayStore.subscribe, debugOverlayStore.getOwnerId, debugOverlayStore.getOwnerId)
311
+ }
312
+
313
+ /**
314
+ * Subscribe to the aggregate debug overlay snapshot.
315
+ * 集約されたデバッグオーバーレイのスナップショットを購読するフック。
316
+ */
317
+ export const useDebugOverlaySnapshot = () => {
318
+ return useSyncExternalStore(debugOverlayStore.subscribe, debugOverlayStore.getSnapshot, debugOverlayStore.getSnapshot)
319
+ }
320
+
321
+ /**
322
+ * Enable a panel group for inclusion in the debug overlay.
323
+ * デバッグオーバーレイへの取り込み対象としてパネルグループを有効化するメソッド。
324
+ */
325
+ export const enableDebugOverlayGroup = (groupId: string) => {
326
+ debugOverlayStore.enableGroup(groupId)
327
+ }
328
+
329
+ /**
330
+ * Disable a panel group from the debug overlay without erasing cached data.
331
+ * キャッシュを保持したままパネルグループをデバッグオーバーレイ対象から外すメソッド。
332
+ */
333
+ export const disableDebugOverlayGroup = (groupId: string) => {
334
+ debugOverlayStore.disableGroup(groupId)
335
+ }
336
+
337
+ /**
338
+ * Persist the latest panel group measurements into the debug overlay store.
339
+ * 最新のパネルグループ測定値をデバッグオーバーレイストアに保存するメソッド。
340
+ */
341
+ export const updateDebugOverlayGroup = (groupId: string, update: DebugGroupUpdate) => {
342
+ debugOverlayStore.updateGroup(groupId, update)
343
+ }
344
+
345
+ /**
346
+ * Remove a panel group from the debug overlay store entirely.
347
+ * パネルグループをデバッグオーバーレイストアから完全に削除するメソッド。
348
+ */
349
+ export const removeDebugOverlayGroup = (groupId: string) => {
350
+ debugOverlayStore.removeGroup(groupId)
351
+ }