@aiquants/resize-panels 2.0.0 → 2.1.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 +63 -5
- package/dist/{GlobalDebugOverlay-DQtUdNeF.js → GlobalDebugOverlay-CIiVujEL.js} +1 -1
- package/dist/{GlobalDebugOverlay-Cf3GOn2A.cjs → GlobalDebugOverlay-DA1__b7e.cjs} +1 -1
- package/dist/GlobalDebugOverlay.d.ts +4 -0
- package/dist/Panel.d.ts +13 -0
- package/dist/{src/PanelDebugInfo.d.ts → PanelDebugInfo.d.ts} +2 -14
- package/dist/PanelGroup.d.ts +2 -0
- package/dist/PanelResizeHandle.d.ts +2 -0
- package/dist/allocateLayout.d.ts +12 -0
- package/dist/context.d.ts +3 -0
- package/dist/debugOverlayStore.d.ts +32 -0
- package/dist/hooks.d.ts +33 -0
- package/dist/index-BplkuFjn.cjs +2 -0
- package/dist/index-DlQDkOjq.js +1332 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +13 -2
- package/dist/index.js +1 -1
- package/dist/reducer.d.ts +21 -0
- package/dist/roundHalfToEven.d.ts +1 -0
- package/dist/styles/resize-panels.standalone.css +1 -1
- package/dist/types.d.ts +188 -0
- package/dist/utils/simple-logger.d.ts +37 -0
- package/dist/utils.d.ts +16 -0
- package/package.json +5 -6
- package/dist/index-DZ0itiBD.js +0 -1299
- package/dist/index-DqATP_BA.cjs +0 -2
- package/dist/src/GlobalDebugOverlay.d.ts +0 -21
- package/dist/src/GlobalDebugOverlay.d.ts.map +0 -1
- package/dist/src/Panel.d.ts +0 -46
- package/dist/src/Panel.d.ts.map +0 -1
- package/dist/src/PanelDebugInfo.d.ts.map +0 -1
- package/dist/src/PanelGroup.d.ts +0 -7
- package/dist/src/PanelGroup.d.ts.map +0 -1
- package/dist/src/PanelResizeHandle.d.ts +0 -7
- package/dist/src/PanelResizeHandle.d.ts.map +0 -1
- package/dist/src/allocateLayout.d.ts +0 -60
- package/dist/src/allocateLayout.d.ts.map +0 -1
- package/dist/src/context.d.ts +0 -12
- package/dist/src/context.d.ts.map +0 -1
- package/dist/src/debugOverlayStore.d.ts +0 -69
- package/dist/src/debugOverlayStore.d.ts.map +0 -1
- package/dist/src/hooks.d.ts +0 -48
- package/dist/src/hooks.d.ts.map +0 -1
- package/dist/src/index.d.ts +0 -32
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/reducer.d.ts +0 -49
- package/dist/src/reducer.d.ts.map +0 -1
- package/dist/src/roundHalfToEven.d.ts +0 -10
- package/dist/src/roundHalfToEven.d.ts.map +0 -1
- package/dist/src/types.d.ts +0 -481
- package/dist/src/types.d.ts.map +0 -1
- package/dist/src/utils/simple-logger.d.ts +0 -111
- package/dist/src/utils/simple-logger.d.ts.map +0 -1
- package/dist/src/utils.d.ts +0 -164
- package/dist/src/utils.d.ts.map +0 -1
- package/dist/tests/support/dom-harness.d.ts +0 -89
- package/dist/tests/support/dom-harness.d.ts.map +0 -1
- package/src/GlobalDebugOverlay.tsx +0 -284
- package/src/Panel.tsx +0 -297
- package/src/PanelDebugInfo.tsx +0 -94
- package/src/PanelGroup.tsx +0 -245
- package/src/PanelResizeHandle.tsx +0 -758
- package/src/allocateLayout.ts +0 -412
- package/src/context.ts +0 -25
- package/src/debugOverlayStore.ts +0 -355
- package/src/hooks.ts +0 -376
- package/src/index.ts +0 -93
- package/src/reducer.ts +0 -472
- package/src/roundHalfToEven.ts +0 -40
- package/src/styles/components.entry.css +0 -10
- package/src/styles/resize-panels.css +0 -1
- package/src/styles/standalone.entry.css +0 -12
- package/src/types.ts +0 -505
- package/src/utils/simple-logger.ts +0 -186
- package/src/utils.ts +0 -320
package/src/Panel.tsx
DELETED
|
@@ -1,297 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Panel component for custom resizable panels
|
|
3
|
-
* カスタムリサイズ可能パネルのための Panel コンポーネント
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { type CSSProperties, memo, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef } from "react"
|
|
7
|
-
import { twMerge } from "tailwind-merge"
|
|
8
|
-
import { resolvePanelBounds } from "./allocateLayout"
|
|
9
|
-
import { usePanelGroup } from "./context"
|
|
10
|
-
import { PanelDebugInfo } from "./PanelDebugInfo"
|
|
11
|
-
import { type CollapsibleConfig, PANEL_ALLOCATION_EPSILON, type PanelLayoutData, type PanelProps, type PanelRegistration, type SizeConfig } from "./types"
|
|
12
|
-
import { getContainerSize, normalizeSizeConfig, toRoundedPercentage } from "./utils"
|
|
13
|
-
import { Logger, LogLevel } from "./utils/simple-logger"
|
|
14
|
-
|
|
15
|
-
const logger = new Logger(LogLevel.INFO, "[resize-panels]")
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Directional collapse flags derived from the collapsible configuration.
|
|
19
|
-
* 折りたたみ設定から導出した方向フラグ。
|
|
20
|
-
*/
|
|
21
|
-
type CollapseFlags = {
|
|
22
|
-
collapseFromStart: boolean
|
|
23
|
-
collapseFromEnd: boolean
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const COLLAPSE_FLAGS: Record<CollapsibleConfig["from"], CollapseFlags> = {
|
|
27
|
-
both: { collapseFromStart: true, collapseFromEnd: true },
|
|
28
|
-
start: { collapseFromStart: true, collapseFromEnd: false },
|
|
29
|
-
end: { collapseFromStart: false, collapseFromEnd: true },
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Normalize collapsible configuration into directional flags.
|
|
34
|
-
* 折りたたみ設定を方向フラグへ正規化する処理。
|
|
35
|
-
*
|
|
36
|
-
* @param config - Collapsible configuration from props / props で指定された折りたたみ設定
|
|
37
|
-
* @returns Directional collapse flags / 方向ごとの折りたたみ可否
|
|
38
|
-
* @throws When the configuration names an unsupported direction / 未対応の方向が指定された場合
|
|
39
|
-
*/
|
|
40
|
-
const normalizeCollapsibleConfig = (config: CollapsibleConfig | undefined): CollapseFlags => {
|
|
41
|
-
if (!config) {
|
|
42
|
-
return { collapseFromStart: false, collapseFromEnd: false }
|
|
43
|
-
}
|
|
44
|
-
const flags = COLLAPSE_FLAGS[config.from]
|
|
45
|
-
if (!flags) {
|
|
46
|
-
throw new Error("Invalid collapsible configuration: unsupported 'from' value")
|
|
47
|
-
}
|
|
48
|
-
return flags
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Split a normalized default size into the preference fields of the layout state.
|
|
53
|
-
* 正規化済みの既定サイズを、レイアウト状態の希望値フィールドへ分解する処理。
|
|
54
|
-
*
|
|
55
|
-
* Exactly one of the two preferences carries meaning, selected by the unit, so the state never holds the
|
|
56
|
-
* same intent twice.
|
|
57
|
-
* 意味を持つ希望値は単位によって選ばれる 1 つだけであり、状態が同じ意図を二重に持つことはない。
|
|
58
|
-
*
|
|
59
|
-
* @param defaultSize - Normalized default size / 正規化済みの既定サイズ
|
|
60
|
-
* @returns Size unit and both preference fields / サイズ単位と 2 つの希望値フィールド
|
|
61
|
-
*/
|
|
62
|
-
const toPreference = (defaultSize: SizeConfig): Pick<PanelRegistration, "sizeUnit" | "preferredPercentageSize" | "preferredPixelSize"> => {
|
|
63
|
-
const value = Math.max(0, defaultSize.value)
|
|
64
|
-
return defaultSize.unit === "pixels" ? { sizeUnit: "pixels", preferredPixelSize: value, preferredPercentageSize: 0 } : { sizeUnit: "percentage", preferredPercentageSize: value, preferredPixelSize: 0 }
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Flex triple a panel renders: the allocated length plus the weights that absorb a container change.
|
|
69
|
-
* パネルが描画する flex の三つ組: 配分された長さと、コンテナ変化を吸収する重み。
|
|
70
|
-
*/
|
|
71
|
-
type PanelFlexStyle = Required<Pick<CSSProperties, "flexBasis" | "flexGrow" | "flexShrink">>
|
|
72
|
-
|
|
73
|
-
type ResolvePanelFlexArgs = {
|
|
74
|
-
size: number
|
|
75
|
-
panel: PanelLayoutData | undefined
|
|
76
|
-
registration: PanelRegistration
|
|
77
|
-
containerSize: number
|
|
78
|
-
isCollapsed: boolean
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Resolve the flex triple that renders a panel's current state.
|
|
83
|
-
* パネルの現在の状態を描画する flex の三つ組を解決する処理。
|
|
84
|
-
*
|
|
85
|
-
* The basis is always the allocated length, so the rendered geometry is the allocator's answer and nothing
|
|
86
|
-
* else. The weights are the first-order continuation of that answer: a panel grows with its ratio weight
|
|
87
|
-
* until it reaches its maximum and shrinks only while it is strictly between its bounds, which is exactly
|
|
88
|
-
* how the allocator would move it for a small container change.
|
|
89
|
-
* basis は常に配分された長さであり、描画される幾何は配分器の答えそのものとなる。重みはその答えの
|
|
90
|
-
* 一次近似であり、上限に達するまでは比率の重みで伸び、上下限の内側にいる間だけ縮む。これは小さな
|
|
91
|
-
* コンテナ変化に対して配分器が動かす量と一致する。
|
|
92
|
-
*
|
|
93
|
-
* @param args - Allocated size, panel state, registration and container length / 配分サイズ・パネル状態・登録情報・コンテナ長
|
|
94
|
-
* @returns Flex basis, grow and shrink to render / 描画する flex basis・grow・shrink
|
|
95
|
-
*/
|
|
96
|
-
export const resolvePanelFlex = ({ size, panel, registration, containerSize, isCollapsed }: ResolvePanelFlexArgs): PanelFlexStyle => {
|
|
97
|
-
if (isCollapsed) {
|
|
98
|
-
return { flexBasis: "0px", flexGrow: 0, flexShrink: 0 }
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (!panel || containerSize <= 0) {
|
|
102
|
-
// まだ計測できていない間は宣言された既定サイズで描く。サーバー描画や初回フレームで
|
|
103
|
-
// 幅ゼロのパネルが一瞬見えるのを防ぎ、計測後の配分結果と連続した見た目になる
|
|
104
|
-
const isPixelRegistration = registration.sizeUnit === "pixels"
|
|
105
|
-
return {
|
|
106
|
-
flexBasis: isPixelRegistration ? `${registration.preferredPixelSize}px` : `${registration.preferredPercentageSize}%`,
|
|
107
|
-
flexGrow: isPixelRegistration ? 0 : registration.preferredPercentageSize,
|
|
108
|
-
flexShrink: isPixelRegistration ? 0 : 1,
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (panel.sizeUnit === "pixels") {
|
|
113
|
-
// 固定長パネルはコンテナ変化を引き受けない。引き受けると配分器の答えと描画がずれる
|
|
114
|
-
return { flexBasis: `${size}px`, flexGrow: 0, flexShrink: 0 }
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// 内側にいるパネルの縮小重みが一律 1 で正しいのは、CSS の縮小配分が「重み × basis」であり
|
|
118
|
-
// basis が配分結果 (= λ × 重み) だからで、結果として重みの比で縮むため
|
|
119
|
-
const bounds = resolvePanelBounds(panel, containerSize)
|
|
120
|
-
const canGrow = size < bounds.max - PANEL_ALLOCATION_EPSILON
|
|
121
|
-
const isInterior = canGrow && size > bounds.min + PANEL_ALLOCATION_EPSILON
|
|
122
|
-
return { flexBasis: `${size}px`, flexGrow: canGrow ? panel.preferredPercentageSize : 0, flexShrink: isInterior ? 1 : 0 }
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Render a resizable panel whose size is decided by the group's allocator.
|
|
127
|
-
* グループの配分器が決めたサイズで描画されるリサイズ可能パネル。
|
|
128
|
-
*
|
|
129
|
-
* The panel publishes what it wants (its preference and constraints) and renders what it was given. Its
|
|
130
|
-
* flex basis is the allocated length itself, while `flex-grow` / `flex-shrink` describe how it should
|
|
131
|
-
* absorb a container change during the frames before the allocator runs again — a growth weight that
|
|
132
|
-
* vanishes at the panel's maximum and a shrink weight that vanishes at either bound, which is exactly how
|
|
133
|
-
* the allocator would move this panel for a small change.
|
|
134
|
-
* パネルは希望と制約を公開し、与えられたサイズで描画する。flex basis は配分結果そのもので、
|
|
135
|
-
* `flex-grow` / `flex-shrink` は配分器が再実行されるまでの数フレームでコンテナ変化をどう吸収するかを表す
|
|
136
|
-
* (上限で消える成長重みと、上下限のいずれかで消える縮小重み)。これは小さな変化に対して配分器が動かす量と一致する。
|
|
137
|
-
*/
|
|
138
|
-
export const Panel = memo(
|
|
139
|
-
({ id: providedId, defaultSize = { value: 50, unit: "percentage" }, minSize, maxSize, className, style, children, collapsible, defaultCollapsed = false, pixelAdjustPriority, flexAdjustPriority, contentOverflow = "auto", ...elementProps }: PanelProps) => {
|
|
140
|
-
const { direction, registerPanel, unregisterPanel, getPanel, containerSize, showDebugInfo, reportPanelMeasurement } = usePanelGroup()
|
|
141
|
-
const panelRef = useRef<HTMLDivElement>(null)
|
|
142
|
-
const generatedId = useId()
|
|
143
|
-
const panelIdRef = useRef(providedId || `panel-${generatedId}`)
|
|
144
|
-
const panelId = panelIdRef.current
|
|
145
|
-
|
|
146
|
-
useEffect(() => {
|
|
147
|
-
// id はマウント後に変更できない (登録・DOM 属性・計測が初期 id に固定されるため)
|
|
148
|
-
if (providedId && providedId !== panelId) {
|
|
149
|
-
logger.warn("Panel id prop change after mount is ignored.", { currentId: panelId, attemptedId: providedId })
|
|
150
|
-
}
|
|
151
|
-
}, [providedId, panelId])
|
|
152
|
-
|
|
153
|
-
const collapseFlags = useMemo(() => normalizeCollapsibleConfig(collapsible), [collapsible])
|
|
154
|
-
const isCollapsible = collapseFlags.collapseFromStart || collapseFlags.collapseFromEnd
|
|
155
|
-
|
|
156
|
-
useEffect(() => {
|
|
157
|
-
if (defaultCollapsed && !isCollapsible) {
|
|
158
|
-
logger.warn("Panel defaultCollapsed is ignored because collapsible configuration is not provided.", { panelId })
|
|
159
|
-
}
|
|
160
|
-
}, [defaultCollapsed, isCollapsible, panelId])
|
|
161
|
-
|
|
162
|
-
const registration = useMemo<PanelRegistration>(() => {
|
|
163
|
-
const normalized = normalizeSizeConfig(defaultSize) ?? { value: 50, unit: "percentage" as const }
|
|
164
|
-
return {
|
|
165
|
-
id: panelId,
|
|
166
|
-
...toPreference(normalized),
|
|
167
|
-
minSize,
|
|
168
|
-
maxSize,
|
|
169
|
-
...collapseFlags,
|
|
170
|
-
collapsed: isCollapsible && defaultCollapsed,
|
|
171
|
-
pixelAdjustPriority,
|
|
172
|
-
flexAdjustPriority,
|
|
173
|
-
}
|
|
174
|
-
}, [panelId, defaultSize, minSize, maxSize, collapseFlags, isCollapsible, defaultCollapsed, pixelAdjustPriority, flexAdjustPriority])
|
|
175
|
-
|
|
176
|
-
useLayoutEffect(() => {
|
|
177
|
-
registerPanel(registration)
|
|
178
|
-
}, [registerPanel, registration])
|
|
179
|
-
|
|
180
|
-
useLayoutEffect(() => {
|
|
181
|
-
// 登録と同じフェーズで解除する。パッシブ効果だと、同一コミット内で同じ id が
|
|
182
|
-
// 貼り替わったとき「新しい登録 → 古い解除」の順になり、パネルが状態から消えたまま戻らない
|
|
183
|
-
return () => {
|
|
184
|
-
unregisterPanel(panelId)
|
|
185
|
-
}
|
|
186
|
-
}, [unregisterPanel, panelId])
|
|
187
|
-
|
|
188
|
-
const panel = getPanel(panelId)
|
|
189
|
-
const size = panel?.size ?? 0
|
|
190
|
-
const isCollapsed = panel?.collapsed ?? false
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Measure the rendered length and report it for the debug overlay.
|
|
194
|
-
* 描画後の実寸を計測し、デバッグオーバーレイへ報告する処理。
|
|
195
|
-
*/
|
|
196
|
-
const containerSizeRef = useRef(containerSize)
|
|
197
|
-
containerSizeRef.current = containerSize
|
|
198
|
-
const measure = useCallback(() => {
|
|
199
|
-
const element = panelRef.current
|
|
200
|
-
if (!element) {
|
|
201
|
-
return
|
|
202
|
-
}
|
|
203
|
-
const pixelSize = getContainerSize(element, direction).border
|
|
204
|
-
reportPanelMeasurement(panelId, { pixelSize, percentageSize: toRoundedPercentage(pixelSize, containerSizeRef.current) })
|
|
205
|
-
}, [direction, panelId, reportPanelMeasurement])
|
|
206
|
-
|
|
207
|
-
useEffect(() => {
|
|
208
|
-
// 実測値はデバッグ表示専用であり、レイアウトへは戻さない。
|
|
209
|
-
// 表示していないときに監視すると、全パネルで無用な強制レイアウトを起こす
|
|
210
|
-
if (!showDebugInfo) {
|
|
211
|
-
reportPanelMeasurement(panelId, null)
|
|
212
|
-
return
|
|
213
|
-
}
|
|
214
|
-
const element = panelRef.current
|
|
215
|
-
if (!element || typeof ResizeObserver === "undefined") {
|
|
216
|
-
measure()
|
|
217
|
-
return
|
|
218
|
-
}
|
|
219
|
-
const observer = new ResizeObserver(measure)
|
|
220
|
-
observer.observe(element)
|
|
221
|
-
measure()
|
|
222
|
-
return () => {
|
|
223
|
-
observer.disconnect()
|
|
224
|
-
reportPanelMeasurement(panelId, null)
|
|
225
|
-
}
|
|
226
|
-
}, [showDebugInfo, measure, panelId, reportPanelMeasurement])
|
|
227
|
-
|
|
228
|
-
const panelStyle = useMemo<CSSProperties>(() => {
|
|
229
|
-
// 見た目に関わる既定値は利用者が上書きできる。
|
|
230
|
-
// `isolation` はパネル内部の z-index がハンドルより手前に出ないようにするための既定値だが、
|
|
231
|
-
// パネルの外まで覆う overlay (position: fixed の最大化表示など) を置きたい利用者は
|
|
232
|
-
// `isolation: "auto"` で降りられる (その場合、内容がハンドルを覆いうることを引き受ける)
|
|
233
|
-
const presentation: CSSProperties = { overflow: "hidden", isolation: "isolate" }
|
|
234
|
-
const collapsed: CSSProperties = isCollapsed ? { ...(direction === "horizontal" ? { width: "0px" } : { height: "0px" }), pointerEvents: "none", visibility: "hidden", display: "none" } : {}
|
|
235
|
-
|
|
236
|
-
return {
|
|
237
|
-
...presentation,
|
|
238
|
-
...style,
|
|
239
|
-
// ここから下はレイアウトの前提そのもの。利用者のスタイルやクラスで変わると配分結果と
|
|
240
|
-
// 描画が一致しなくなるため、最後に適用して上書きを受け付けない:
|
|
241
|
-
// flex の三つ組は配分器が決めた長さそのもの、margin はパネルの合計に含まれない隙間を作り、
|
|
242
|
-
// order は DOM 順と視覚順をずらす
|
|
243
|
-
...resolvePanelFlex({ size, panel, registration, containerSize, isCollapsed }),
|
|
244
|
-
order: 0,
|
|
245
|
-
margin: 0,
|
|
246
|
-
// 内容を通常フローで内容ボックスいっぱいに伸ばすための入れ物 (パディングを効かせる唯一の形)
|
|
247
|
-
display: "flex",
|
|
248
|
-
flexDirection: "column",
|
|
249
|
-
position: "relative",
|
|
250
|
-
boxSizing: "border-box",
|
|
251
|
-
// 主軸の縮小をコンテンツの固有幅に妨げられないようにし、
|
|
252
|
-
// 上限側のクランプ (max-w-* のようなユーティリティクラスを含む) も受け付けない。
|
|
253
|
-
// 配分器が配った長さより短く描くと、状態と描画が食い違ったままハンドルが動かなくなる
|
|
254
|
-
minWidth: 0,
|
|
255
|
-
minHeight: 0,
|
|
256
|
-
maxWidth: "none",
|
|
257
|
-
maxHeight: "none",
|
|
258
|
-
...(direction === "horizontal" ? { height: "100%" } : { width: "100%" }),
|
|
259
|
-
...collapsed,
|
|
260
|
-
}
|
|
261
|
-
}, [size, direction, isCollapsed, panel, registration, containerSize, style])
|
|
262
|
-
|
|
263
|
-
const contentWrapperStyle = useMemo<CSSProperties>(
|
|
264
|
-
() => ({
|
|
265
|
-
// 内容は通常フローの伸縮子として置く。絶対配置にするとパディングボックスを基準にしてしまい、
|
|
266
|
-
// 利用者がパネルへ与えたパディングが何の効果も持たなくなる
|
|
267
|
-
flex: "1 1 auto",
|
|
268
|
-
position: "relative",
|
|
269
|
-
overflow: contentOverflow,
|
|
270
|
-
display: "flex",
|
|
271
|
-
flexDirection: "column",
|
|
272
|
-
minWidth: "0px",
|
|
273
|
-
minHeight: "0px",
|
|
274
|
-
}),
|
|
275
|
-
[contentOverflow],
|
|
276
|
-
)
|
|
277
|
-
|
|
278
|
-
return (
|
|
279
|
-
<div
|
|
280
|
-
{...elementProps}
|
|
281
|
-
ref={panelRef}
|
|
282
|
-
className={twMerge("panel", className)}
|
|
283
|
-
style={panelStyle}
|
|
284
|
-
data-panel-id={panelId}
|
|
285
|
-
data-panel-size={size}
|
|
286
|
-
data-panel-size-unit={panel?.sizeUnit ?? "percentage"}
|
|
287
|
-
data-panel-collapsible={isCollapsible}
|
|
288
|
-
data-panel-collapse-start={collapseFlags.collapseFromStart}
|
|
289
|
-
data-panel-collapse-end={collapseFlags.collapseFromEnd}
|
|
290
|
-
data-panel-collapsed={isCollapsed}
|
|
291
|
-
aria-hidden={isCollapsed}>
|
|
292
|
-
<div style={contentWrapperStyle}>{children}</div>
|
|
293
|
-
{showDebugInfo && <PanelDebugInfo panel={panel} containerAxisSize={containerSize} direction={direction} />}
|
|
294
|
-
</div>
|
|
295
|
-
)
|
|
296
|
-
},
|
|
297
|
-
)
|
package/src/PanelDebugInfo.tsx
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Debug info overlay component for panels
|
|
3
|
-
* パネル用デバッグ情報オーバーレイコンポーネント
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { type CSSProperties, memo, useMemo } from "react"
|
|
7
|
-
import { resolvePanelBounds } from "./allocateLayout"
|
|
8
|
-
import type { PanelDirection, PanelLayoutData } from "./types"
|
|
9
|
-
import { toRoundedPercentage } from "./utils"
|
|
10
|
-
|
|
11
|
-
type PanelDebugInfoProps = {
|
|
12
|
-
panel: PanelLayoutData | undefined
|
|
13
|
-
containerAxisSize: number
|
|
14
|
-
direction: PanelDirection
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
type FormatPanelDebugInfoArgs = {
|
|
18
|
-
panel: PanelLayoutData | undefined
|
|
19
|
-
containerAxisSize: number
|
|
20
|
-
containerLabel: string
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
type FormattedPanelDebugInfo = {
|
|
24
|
-
sizeDisplay: string
|
|
25
|
-
preferenceDisplay: string
|
|
26
|
-
boundsDisplay: string
|
|
27
|
-
containerDisplay: string
|
|
28
|
-
headlineLabel: string
|
|
29
|
-
titleText: string
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Format one panel's allocation for the debug overlay.
|
|
34
|
-
* デバッグ表示用に 1 枚のパネルの配分結果を整形する処理。
|
|
35
|
-
*
|
|
36
|
-
* @param args - Panel, container length and the axis label / パネル・コンテナ長・軸ラベル
|
|
37
|
-
* @returns Display strings for the overlay / オーバーレイ表示用の文字列群
|
|
38
|
-
*/
|
|
39
|
-
export const formatPanelDebugInfo = ({ panel, containerAxisSize, containerLabel }: FormatPanelDebugInfoArgs): FormattedPanelDebugInfo => {
|
|
40
|
-
if (!panel) {
|
|
41
|
-
return { sizeDisplay: "-", preferenceDisplay: "-", boundsDisplay: "-", containerDisplay: "-", headlineLabel: "Unregistered", titleText: "Unregistered" }
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const isPixel = panel.sizeUnit === "pixels"
|
|
45
|
-
const sizeDisplay = `${panel.size.toFixed(3)}px / ${toRoundedPercentage(panel.size, containerAxisSize).toFixed(3)}%`
|
|
46
|
-
const preferenceDisplay = isPixel ? `${panel.preferredPixelSize.toFixed(3)}px` : `${panel.preferredPercentageSize.toFixed(3)}%`
|
|
47
|
-
const bounds = resolvePanelBounds(panel, containerAxisSize)
|
|
48
|
-
const boundsDisplay = `${bounds.min.toFixed(1)} - ${bounds.max.toFixed(1)}px`
|
|
49
|
-
const containerDisplay = Number.isFinite(containerAxisSize) ? `${containerAxisSize.toFixed(3)}px` : "-"
|
|
50
|
-
const headlineLabel = isPixel ? "Fixed" : "Flexible"
|
|
51
|
-
const titleText = [headlineLabel, `${containerLabel}: ${containerDisplay}`, `size: ${sizeDisplay}`, `preference: ${preferenceDisplay}`, `bounds: ${boundsDisplay}`].join("\n")
|
|
52
|
-
|
|
53
|
-
return { sizeDisplay, preferenceDisplay, boundsDisplay, containerDisplay, headlineLabel, titleText }
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Render the per-panel debug badge inside a panel.
|
|
58
|
-
* パネル内にパネル単位のデバッグバッジを描画するコンポーネント。
|
|
59
|
-
*/
|
|
60
|
-
export const PanelDebugInfo = memo(({ panel, containerAxisSize, direction }: PanelDebugInfoProps) => {
|
|
61
|
-
const containerLabel = direction === "horizontal" ? "container width" : "container height"
|
|
62
|
-
const { sizeDisplay, preferenceDisplay, boundsDisplay, containerDisplay, headlineLabel, titleText } = useMemo(() => formatPanelDebugInfo({ panel, containerAxisSize, containerLabel }), [panel, containerAxisSize, containerLabel])
|
|
63
|
-
|
|
64
|
-
const badgeStyle: CSSProperties = {
|
|
65
|
-
position: "absolute",
|
|
66
|
-
top: 2,
|
|
67
|
-
left: 2,
|
|
68
|
-
zIndex: 30,
|
|
69
|
-
pointerEvents: "none",
|
|
70
|
-
maxWidth: "calc(100% - 4px)",
|
|
71
|
-
padding: "2px 4px",
|
|
72
|
-
borderRadius: 4,
|
|
73
|
-
border: "1px solid rgba(255,255,255,0.1)",
|
|
74
|
-
backgroundColor: "rgba(15, 23, 42, 0.78)",
|
|
75
|
-
color: "#e2e8f0",
|
|
76
|
-
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
77
|
-
fontSize: 10,
|
|
78
|
-
lineHeight: 1.35,
|
|
79
|
-
whiteSpace: "pre",
|
|
80
|
-
overflow: "hidden",
|
|
81
|
-
textOverflow: "ellipsis",
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return (
|
|
85
|
-
<div style={badgeStyle} title={titleText} data-panel-debug-info="">
|
|
86
|
-
<div>
|
|
87
|
-
{headlineLabel} · {containerLabel}: {containerDisplay}
|
|
88
|
-
</div>
|
|
89
|
-
<div>size: {sizeDisplay}</div>
|
|
90
|
-
<div>preference: {preferenceDisplay}</div>
|
|
91
|
-
<div>bounds: {boundsDisplay}</div>
|
|
92
|
-
</div>
|
|
93
|
-
)
|
|
94
|
-
})
|
package/src/PanelGroup.tsx
DELETED
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file PanelGroup component for custom resizable panels
|
|
3
|
-
* カスタムリサイズ可能パネルのための PanelGroup コンポーネント
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { lazy, Suspense, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"
|
|
7
|
-
import { twMerge } from "tailwind-merge"
|
|
8
|
-
import { PanelGroupContext } from "./context"
|
|
9
|
-
import type { DebugPanelSnapshot } from "./debugOverlayStore"
|
|
10
|
-
import { useResizablePanels } from "./hooks"
|
|
11
|
-
import type { PanelGroupProps, PanelLayoutData } from "./types"
|
|
12
|
-
import { getContainerSize } from "./utils"
|
|
13
|
-
|
|
14
|
-
// デバッグオーバーレイ (UI とストア) は dynamic import で遅延読み込みする。
|
|
15
|
-
// showDebugInfo を使わないアプリのバンドルへオーバーレイ一式を含めず、ストア購読も発生させないための分離
|
|
16
|
-
const LazyDebugOverlayGate = lazy(() => import("./GlobalDebugOverlay").then((module) => ({ default: module.GlobalDebugOverlayGate })))
|
|
17
|
-
|
|
18
|
-
type DebugOverlayStoreModule = typeof import("./debugOverlayStore")
|
|
19
|
-
|
|
20
|
-
/** Stable empty collections used while the debug overlay is off. デバッグ表示が無効な間に使う不変の空コレクション。 */
|
|
21
|
-
const EMPTY_PANELS: ReadonlyArray<PanelLayoutData> = []
|
|
22
|
-
const EMPTY_MEASUREMENTS: Record<string, never> = {}
|
|
23
|
-
|
|
24
|
-
let debugGroupIdCounter = 0
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Generate unique debug group identifier for panel groups without explicit IDs.
|
|
28
|
-
* 明示的な ID を持たないパネルグループ用の一意なデバッググループ識別子を生成。
|
|
29
|
-
*/
|
|
30
|
-
const generateDebugGroupId = () => {
|
|
31
|
-
debugGroupIdCounter += 1
|
|
32
|
-
return `panel-group-${debugGroupIdCounter}`
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
type ResizablePanelsContextValue = ReturnType<typeof useResizablePanels>["contextValue"]
|
|
36
|
-
type LayoutConstraintViolation = NonNullable<ResizablePanelsContextValue["layoutConstraintViolation"]>
|
|
37
|
-
type DebugPanelMeasurementMap = ResizablePanelsContextValue["panelMeasurements"]
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Builds summary and detail labels for layout constraint placeholders.
|
|
41
|
-
* レイアウト制約プレースホルダー用のサマリーと詳細ラベルを生成。
|
|
42
|
-
*/
|
|
43
|
-
const buildConstraintCopy = (violation: LayoutConstraintViolation) => {
|
|
44
|
-
const isMinimum = violation.reason === "minimum-exceeded"
|
|
45
|
-
const summary = isMinimum ? "Layout unavailable: minimum panel sizes exceed the container." : "Layout unavailable: panels cannot fill the container with their maximum sizes."
|
|
46
|
-
const detail = isMinimum ? `Min required: ${Math.ceil(violation.totalMinimumSize)}px | Available: ${Math.floor(violation.availableContainerSize)}px` : `Max total: ${Math.floor(violation.totalMaximumSize)}px | Needed: ${Math.floor(violation.availableContainerSize)}px`
|
|
47
|
-
return { summary, detail }
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Synchronizes measured panel dimensions into overlay panel descriptors.
|
|
52
|
-
* 測定済みパネル寸法をオーバーレイ用パネル情報へ同期。
|
|
53
|
-
*/
|
|
54
|
-
const mergePanelMeasurements = (panels: ReadonlyArray<PanelLayoutData>, measurementMap: DebugPanelMeasurementMap): DebugPanelSnapshot[] =>
|
|
55
|
-
panels.map((panel) => {
|
|
56
|
-
const measurement = measurementMap[panel.id]
|
|
57
|
-
if (!measurement) {
|
|
58
|
-
return panel
|
|
59
|
-
}
|
|
60
|
-
return { ...panel, measuredPixelSize: measurement.pixelSize, measuredPercentageSize: measurement.percentageSize }
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Measures overlay container dimensions with graceful fallbacks.
|
|
65
|
-
* フォールバックを考慮しつつオーバーレイ用コンテナ寸法を計測。
|
|
66
|
-
*/
|
|
67
|
-
const measureDebugOverlayContainerSize = (element: HTMLElement, fallbackWidth?: number, fallbackHeight?: number) => {
|
|
68
|
-
const horizontal = getContainerSize(element, "horizontal")
|
|
69
|
-
const vertical = getContainerSize(element, "vertical")
|
|
70
|
-
const rect = element.getBoundingClientRect()
|
|
71
|
-
const width = horizontal.content > 0 ? horizontal.content : (fallbackWidth ?? rect.width)
|
|
72
|
-
const height = vertical.content > 0 ? vertical.content : (fallbackHeight ?? rect.height)
|
|
73
|
-
return { width, height }
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Provide layout context and container for coordinated panels.
|
|
78
|
-
* 連動するパネルのためのレイアウトコンテキストとコンテナを提供するコンポーネント。
|
|
79
|
-
*/
|
|
80
|
-
export const PanelGroup = (props: PanelGroupProps) => {
|
|
81
|
-
const { id, className, style, children, direction, onLayout, autoSaveId, showDebugInfo, ...elementProps } = props
|
|
82
|
-
const { groupRef, contextValue, groupStyle } = useResizablePanels(props)
|
|
83
|
-
|
|
84
|
-
const [debugOverlayContainerSize, setDebugOverlayContainerSize] = useState({ width: 0, height: 0 })
|
|
85
|
-
|
|
86
|
-
const constraintViolation = contextValue.layoutConstraintViolation
|
|
87
|
-
const shouldRenderPlaceholder = Boolean(constraintViolation)
|
|
88
|
-
const placeholderInner = useMemo(() => {
|
|
89
|
-
if (!constraintViolation) {
|
|
90
|
-
return null
|
|
91
|
-
}
|
|
92
|
-
const { summary, detail } = buildConstraintCopy(constraintViolation)
|
|
93
|
-
return (
|
|
94
|
-
<div
|
|
95
|
-
className="flex w-full flex-1 flex-col items-center justify-center gap-2 rounded-lg border border-slate-300 border-dashed bg-slate-100/70 px-6 py-8 text-center text-slate-600 text-sm dark:border-slate-600 dark:bg-slate-800/50 dark:text-slate-200"
|
|
96
|
-
data-panel-group-placeholder="constraint-violation">
|
|
97
|
-
<span>{summary}</span>
|
|
98
|
-
<span className="text-slate-500 text-xs dark:text-slate-400">{detail}</span>
|
|
99
|
-
</div>
|
|
100
|
-
)
|
|
101
|
-
}, [constraintViolation])
|
|
102
|
-
|
|
103
|
-
// useRef の引数は毎レンダー評価されるため、そこで採番するとレンダーが純粋でなくなる
|
|
104
|
-
const debugGroupIdRef = useRef<string | null>(null)
|
|
105
|
-
if (debugGroupIdRef.current === null) {
|
|
106
|
-
debugGroupIdRef.current = id ?? generateDebugGroupId()
|
|
107
|
-
}
|
|
108
|
-
const debugGroupId = debugGroupIdRef.current
|
|
109
|
-
const debugDisplayName = id ?? debugGroupId
|
|
110
|
-
|
|
111
|
-
const shouldShowDebugOverlay = contextValue.showDebugInfo && contextValue.panels.length > 0
|
|
112
|
-
// 無効時に空のリテラルを作ると参照が毎レンダー変わり、下流の useMemo が無駄に再計算される
|
|
113
|
-
const debugOverlayPanels = shouldShowDebugOverlay ? contextValue.panels : EMPTY_PANELS
|
|
114
|
-
const debugOverlayMeasurementMap = shouldShowDebugOverlay ? contextValue.panelMeasurements : EMPTY_MEASUREMENTS
|
|
115
|
-
const debugOverlayHandles = useMemo(() => {
|
|
116
|
-
if (!shouldShowDebugOverlay) {
|
|
117
|
-
return []
|
|
118
|
-
}
|
|
119
|
-
const handles = Object.values(contextValue.handleMeasurements)
|
|
120
|
-
if (handles.length <= 1) {
|
|
121
|
-
return handles
|
|
122
|
-
}
|
|
123
|
-
return handles.slice().sort((a, b) => a.id.localeCompare(b.id))
|
|
124
|
-
}, [contextValue.handleMeasurements, shouldShowDebugOverlay])
|
|
125
|
-
const debugOverlayContainerWidth = debugOverlayContainerSize.width
|
|
126
|
-
const debugOverlayContainerHeight = debugOverlayContainerSize.height
|
|
127
|
-
|
|
128
|
-
useLayoutEffect(() => {
|
|
129
|
-
// shouldShowDebugOverlay と groupRef に応じてデバッグオーバーレイ用コンテナ寸法を監視
|
|
130
|
-
if (!shouldShowDebugOverlay) {
|
|
131
|
-
setDebugOverlayContainerSize({ width: 0, height: 0 })
|
|
132
|
-
return
|
|
133
|
-
}
|
|
134
|
-
if (typeof window === "undefined" || typeof ResizeObserver === "undefined") {
|
|
135
|
-
return
|
|
136
|
-
}
|
|
137
|
-
const element = groupRef.current
|
|
138
|
-
if (!element) {
|
|
139
|
-
return
|
|
140
|
-
}
|
|
141
|
-
const updateSize = (nextSize: { width: number; height: number }) => {
|
|
142
|
-
setDebugOverlayContainerSize((previous) => (previous.width === nextSize.width && previous.height === nextSize.height ? previous : nextSize))
|
|
143
|
-
}
|
|
144
|
-
const observer = new ResizeObserver((entries) => {
|
|
145
|
-
for (const entry of entries) {
|
|
146
|
-
if (entry.target !== element) {
|
|
147
|
-
continue
|
|
148
|
-
}
|
|
149
|
-
const nextSize = measureDebugOverlayContainerSize(element, entry.contentRect.width, entry.contentRect.height)
|
|
150
|
-
updateSize(nextSize)
|
|
151
|
-
}
|
|
152
|
-
})
|
|
153
|
-
observer.observe(element)
|
|
154
|
-
updateSize(measureDebugOverlayContainerSize(element))
|
|
155
|
-
return () => {
|
|
156
|
-
observer.disconnect()
|
|
157
|
-
}
|
|
158
|
-
}, [groupRef, shouldShowDebugOverlay])
|
|
159
|
-
|
|
160
|
-
const debugOverlayPanelsWithMeasurements = useMemo(() => mergePanelMeasurements(debugOverlayPanels, debugOverlayMeasurementMap), [debugOverlayPanels, debugOverlayMeasurementMap])
|
|
161
|
-
|
|
162
|
-
useEffect(() => {
|
|
163
|
-
// shouldShowDebugOverlay と debugGroupId に応じてデバッグオーバーレイ有効状態を同期
|
|
164
|
-
// ストアは遅延読み込みのため、有効化されるまで import しない (未読込なら解除処理も不要)
|
|
165
|
-
if (!shouldShowDebugOverlay) {
|
|
166
|
-
return
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
let cancelled = false
|
|
170
|
-
let loadedStore: DebugOverlayStoreModule | null = null
|
|
171
|
-
import("./debugOverlayStore").then((storeModule) => {
|
|
172
|
-
if (cancelled) {
|
|
173
|
-
return
|
|
174
|
-
}
|
|
175
|
-
loadedStore = storeModule
|
|
176
|
-
storeModule.enableDebugOverlayGroup(debugGroupId)
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
return () => {
|
|
180
|
-
cancelled = true
|
|
181
|
-
if (loadedStore) {
|
|
182
|
-
loadedStore.disableDebugOverlayGroup(debugGroupId)
|
|
183
|
-
loadedStore.removeDebugOverlayGroup(debugGroupId)
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}, [shouldShowDebugOverlay, debugGroupId])
|
|
187
|
-
|
|
188
|
-
useEffect(() => {
|
|
189
|
-
// 依存配列の変化ごとに最新のオーバーレイ集計をストアへ送出
|
|
190
|
-
if (!shouldShowDebugOverlay) {
|
|
191
|
-
return
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
let cancelled = false
|
|
195
|
-
import("./debugOverlayStore").then((storeModule) => {
|
|
196
|
-
if (cancelled) {
|
|
197
|
-
return
|
|
198
|
-
}
|
|
199
|
-
storeModule.updateDebugOverlayGroup(debugGroupId, {
|
|
200
|
-
displayName: debugDisplayName,
|
|
201
|
-
direction,
|
|
202
|
-
containerSize: {
|
|
203
|
-
width: debugOverlayContainerWidth,
|
|
204
|
-
height: debugOverlayContainerHeight,
|
|
205
|
-
},
|
|
206
|
-
panels: debugOverlayPanelsWithMeasurements,
|
|
207
|
-
handles: debugOverlayHandles,
|
|
208
|
-
})
|
|
209
|
-
})
|
|
210
|
-
return () => {
|
|
211
|
-
cancelled = true
|
|
212
|
-
}
|
|
213
|
-
}, [shouldShowDebugOverlay, debugGroupId, debugDisplayName, direction, debugOverlayContainerWidth, debugOverlayContainerHeight, debugOverlayPanelsWithMeasurements, debugOverlayHandles])
|
|
214
|
-
|
|
215
|
-
return (
|
|
216
|
-
<PanelGroupContext.Provider value={contextValue}>
|
|
217
|
-
<div {...elementProps} ref={groupRef} className={twMerge("panel-group", direction === "horizontal" ? "flex-row" : "flex-col", className)} style={groupStyle} data-panel-group-id={id} data-panel-group-direction={direction}>
|
|
218
|
-
{children}
|
|
219
|
-
{shouldRenderPlaceholder && placeholderInner ? (
|
|
220
|
-
<div
|
|
221
|
-
className="pointer-events-auto"
|
|
222
|
-
style={{
|
|
223
|
-
position: "absolute",
|
|
224
|
-
inset: 0,
|
|
225
|
-
// パネル内部要素 (VirtualScroll のスクロールバー z-50 等) を完全に隠蔽するため zIndex を 60 に設定
|
|
226
|
-
zIndex: 60,
|
|
227
|
-
display: "flex",
|
|
228
|
-
flexDirection: "column",
|
|
229
|
-
alignItems: "center",
|
|
230
|
-
justifyContent: "center",
|
|
231
|
-
backgroundColor: "rgba(248, 250, 252, 0.82)",
|
|
232
|
-
backdropFilter: "blur(2px)",
|
|
233
|
-
}}>
|
|
234
|
-
{placeholderInner}
|
|
235
|
-
</div>
|
|
236
|
-
) : null}
|
|
237
|
-
</div>
|
|
238
|
-
{shouldShowDebugOverlay && (
|
|
239
|
-
<Suspense fallback={null}>
|
|
240
|
-
<LazyDebugOverlayGate groupId={debugGroupId} />
|
|
241
|
-
</Suspense>
|
|
242
|
-
)}
|
|
243
|
-
</PanelGroupContext.Provider>
|
|
244
|
-
)
|
|
245
|
-
}
|