@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.
Files changed (81) hide show
  1. package/README.md +213 -108
  2. package/dist/GlobalDebugOverlay-Cf3GOn2A.cjs +1 -0
  3. package/dist/{GlobalDebugOverlay-DYCR0FIu.js → GlobalDebugOverlay-DQtUdNeF.js} +28 -28
  4. package/dist/debugOverlayStore-D7FT6zPE.js +135 -0
  5. package/dist/debugOverlayStore-DY09saU8.cjs +1 -0
  6. package/dist/index-DZ0itiBD.js +1299 -0
  7. package/dist/index-DqATP_BA.cjs +2 -0
  8. package/dist/index.cjs +1 -1
  9. package/dist/index.d.ts +2 -29
  10. package/dist/index.js +25 -13
  11. package/dist/src/GlobalDebugOverlay.d.ts.map +1 -0
  12. package/dist/src/Panel.d.ts +46 -0
  13. package/dist/src/Panel.d.ts.map +1 -0
  14. package/dist/src/PanelDebugInfo.d.ts +34 -0
  15. package/dist/src/PanelDebugInfo.d.ts.map +1 -0
  16. package/dist/src/PanelGroup.d.ts.map +1 -0
  17. package/dist/{PanelResizeHandle.d.ts → src/PanelResizeHandle.d.ts} +1 -1
  18. package/dist/src/PanelResizeHandle.d.ts.map +1 -0
  19. package/dist/src/allocateLayout.d.ts +60 -0
  20. package/dist/src/allocateLayout.d.ts.map +1 -0
  21. package/dist/src/context.d.ts.map +1 -0
  22. package/dist/{debugOverlayStore.d.ts → src/debugOverlayStore.d.ts} +10 -2
  23. package/dist/src/debugOverlayStore.d.ts.map +1 -0
  24. package/dist/src/hooks.d.ts +48 -0
  25. package/dist/src/hooks.d.ts.map +1 -0
  26. package/dist/src/index.d.ts +32 -0
  27. package/dist/src/index.d.ts.map +1 -0
  28. package/dist/src/reducer.d.ts +49 -0
  29. package/dist/src/reducer.d.ts.map +1 -0
  30. package/dist/src/roundHalfToEven.d.ts.map +1 -0
  31. package/dist/src/types.d.ts +481 -0
  32. package/dist/src/types.d.ts.map +1 -0
  33. package/dist/src/utils/simple-logger.d.ts.map +1 -0
  34. package/dist/src/utils.d.ts +164 -0
  35. package/dist/src/utils.d.ts.map +1 -0
  36. package/dist/styles/resize-panels.standalone.css +1 -1
  37. package/dist/tests/support/dom-harness.d.ts +89 -0
  38. package/dist/tests/support/dom-harness.d.ts.map +1 -0
  39. package/package.json +3 -3
  40. package/src/GlobalDebugOverlay.tsx +3 -3
  41. package/src/Panel.tsx +203 -541
  42. package/src/PanelDebugInfo.tsx +52 -106
  43. package/src/PanelGroup.tsx +27 -42
  44. package/src/PanelResizeHandle.tsx +544 -504
  45. package/src/allocateLayout.ts +412 -0
  46. package/src/debugOverlayStore.ts +19 -15
  47. package/src/hooks.ts +187 -779
  48. package/src/index.ts +37 -6
  49. package/src/reducer.ts +364 -1126
  50. package/src/types.ts +282 -65
  51. package/src/utils.ts +205 -164
  52. package/dist/GlobalDebugOverlay-afs0vF-Q.cjs +0 -1
  53. package/dist/GlobalDebugOverlay.d.ts.map +0 -1
  54. package/dist/Panel.d.ts +0 -7
  55. package/dist/Panel.d.ts.map +0 -1
  56. package/dist/PanelDebugInfo.d.ts +0 -26
  57. package/dist/PanelDebugInfo.d.ts.map +0 -1
  58. package/dist/PanelGroup.d.ts.map +0 -1
  59. package/dist/PanelResizeHandle.d.ts.map +0 -1
  60. package/dist/context.d.ts.map +0 -1
  61. package/dist/debugOverlayStore-Cntyxboe.js +0 -141
  62. package/dist/debugOverlayStore-Dkl-fHoa.cjs +0 -1
  63. package/dist/debugOverlayStore.d.ts.map +0 -1
  64. package/dist/hooks.d.ts +0 -45
  65. package/dist/hooks.d.ts.map +0 -1
  66. package/dist/index-BzgId8aQ.js +0 -1972
  67. package/dist/index-xTV-c0PN.cjs +0 -2
  68. package/dist/index.d.ts.map +0 -1
  69. package/dist/reducer.d.ts +0 -39
  70. package/dist/reducer.d.ts.map +0 -1
  71. package/dist/roundHalfToEven.d.ts.map +0 -1
  72. package/dist/types.d.ts +0 -253
  73. package/dist/types.d.ts.map +0 -1
  74. package/dist/utils/simple-logger.d.ts.map +0 -1
  75. package/dist/utils.d.ts +0 -61
  76. package/dist/utils.d.ts.map +0 -1
  77. /package/dist/{GlobalDebugOverlay.d.ts → src/GlobalDebugOverlay.d.ts} +0 -0
  78. /package/dist/{PanelGroup.d.ts → src/PanelGroup.d.ts} +0 -0
  79. /package/dist/{context.d.ts → src/context.d.ts} +0 -0
  80. /package/dist/{roundHalfToEven.d.ts → src/roundHalfToEven.d.ts} +0 -0
  81. /package/dist/{utils → src/utils}/simple-logger.d.ts +0 -0
package/src/utils.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { roundHalfToEven } from "./roundHalfToEven"
7
- import { type ContainerAxisMeasurement, type FlexibleSize, PANEL_COLLAPSE_THRESHOLD_MAX, PANEL_COLLAPSE_THRESHOLD_RATIO, PANEL_MIN_SNAP_THRESHOLD, PANEL_SNAP_THRESHOLD, type PanelDirection, type PanelLayoutData, type SizeConfig, type SizeUnit } from "./types"
7
+ import { type ContainerAxisMeasurement, type FlexibleSize, PANEL_COLLAPSE_THRESHOLD_MAX, PANEL_COLLAPSE_THRESHOLD_RATIO, PANEL_SNAP_THRESHOLD, type PanelDirection, type PersistedPanelSize, type SizeConfig, type SizeUnit } from "./types"
8
8
 
9
9
  /**
10
10
  * Number of significant fractional digits to preserve when rounding percentage-based sizes.
@@ -13,23 +13,30 @@ import { type ContainerAxisMeasurement, type FlexibleSize, PANEL_COLLAPSE_THRESH
13
13
  export const PERCENTAGE_DIGITS = 6
14
14
 
15
15
  /**
16
- * Convert a pixel measurement to a percentage of the container using bankers rounding.
16
+ * Convert a pixel measurement to a percentage of the container using banker's rounding.
17
17
  * ピクセル寸法をバンカーズラウンディングでコンテナ比率のパーセンテージへ変換する。
18
+ *
19
+ * @param size - Length in pixels / 長さ (px)
20
+ * @param base - Container length the percentage is relative to / 基準となるコンテナ長
21
+ * @returns Percentage of the container, 0 when the base is not positive / コンテナ比 (%)、基準が非正なら 0
18
22
  */
19
23
  export const toRoundedPercentage = (size: number, base: number): number => {
20
24
  if (base <= 0) {
21
25
  return 0
22
26
  }
23
- const percentage = roundHalfToEven((size / base) * 100, PERCENTAGE_DIGITS)
24
- return percentage
27
+ return roundHalfToEven((size / base) * 100, PERCENTAGE_DIGITS)
25
28
  }
26
29
 
27
30
  /**
28
31
  * Translate a percentage-based size back into pixels relative to the container.
29
32
  * パーセンテージ指定のサイズをコンテナ基準のピクセル値へ変換する。
33
+ *
34
+ * @param percentage - Share of the container in percent / コンテナ比 (%)
35
+ * @param base - Container length the percentage is relative to / 基準となるコンテナ長
36
+ * @returns Length in pixels / 長さ (px)
30
37
  */
31
- export const percentageToPixels = (percentage: number | undefined, base: number): number => {
32
- if (!percentage || base <= 0) {
38
+ export const percentageToPixels = (percentage: number, base: number): number => {
39
+ if (base <= 0) {
33
40
  return 0
34
41
  }
35
42
  return (percentage / 100) * base
@@ -38,9 +45,15 @@ export const percentageToPixels = (percentage: number | undefined, base: number)
38
45
  /**
39
46
  * Normalize a flexible size into a size configuration object.
40
47
  * 柔軟なサイズ指定をサイズ設定オブジェクトへ正規化する。
48
+ *
49
+ * @param size - Author-provided size / 利用者が指定したサイズ
50
+ * @param defaultUnit - Unit applied to bare numbers / 数値のみの指定に適用する単位
51
+ * @returns Normalized configuration, or undefined when nothing was provided / 正規化後の設定、未指定なら undefined
41
52
  */
42
53
  export const normalizeSizeConfig = (size: FlexibleSize | undefined, defaultUnit: SizeUnit = "percentage"): SizeConfig | undefined => {
43
- if (size === undefined) return undefined
54
+ if (size === undefined) {
55
+ return undefined
56
+ }
44
57
  if (typeof size === "number") {
45
58
  return { value: size, unit: defaultUnit }
46
59
  }
@@ -50,9 +63,16 @@ export const normalizeSizeConfig = (size: FlexibleSize | undefined, defaultUnit:
50
63
  /**
51
64
  * Convert flexible size constraints into pixel values based on container size.
52
65
  * 柔軟サイズ制約をコンテナサイズ基準のピクセル値へ変換する。
66
+ *
67
+ * @param size - Constraint to resolve; bare numbers are percentages / 解決する制約 (数値のみの指定はパーセンテージ)
68
+ * @param defaultValue - Value used when the constraint is absent / 制約が無い場合の値
69
+ * @param containerSize - Container content-box length / コンテナ内容領域長
70
+ * @returns Constraint in pixels / ピクセル換算した制約
53
71
  */
54
72
  export const getConstraintInPixels = (size: FlexibleSize | undefined, defaultValue: number, containerSize: number): number => {
55
- if (!size) return defaultValue
73
+ if (size === undefined) {
74
+ return defaultValue
75
+ }
56
76
  if (typeof size === "number") {
57
77
  return (size / 100) * containerSize
58
78
  }
@@ -65,6 +85,11 @@ export const getConstraintInPixels = (size: FlexibleSize | undefined, defaultVal
65
85
  /**
66
86
  * Clamp a numeric value between the provided bounds.
67
87
  * 数値を指定した範囲に収めるためのクリッピング処理。
88
+ *
89
+ * @param value - Value to clamp / 収める値
90
+ * @param min - Lower bound / 下限
91
+ * @param max - Upper bound / 上限
92
+ * @returns Clamped value / 範囲内へ収めた値
68
93
  */
69
94
  export const clamp = (value: number, min: number, max: number): number => {
70
95
  return Math.min(Math.max(value, min), max)
@@ -73,205 +98,221 @@ export const clamp = (value: number, min: number, max: number): number => {
73
98
  /**
74
99
  * Derive a snap-to-zero threshold from the container axis length using shared heuristics.
75
100
  * 共通ヒューリスティクスに基づきコンテナ軸長からゼロスナップ閾値を算出する。
101
+ *
102
+ * @param axisSize - Container content-box length along the axis / 軸方向のコンテナ内容領域長
103
+ * @returns Threshold in pixels below which a panel snaps closed / 閉じたとみなすピクセル閾値
76
104
  */
77
105
  export const calculateSnapThreshold = (axisSize: number): number => {
78
106
  if (!Number.isFinite(axisSize) || axisSize <= 0) {
79
- return PANEL_MIN_SNAP_THRESHOLD
107
+ return 0
80
108
  }
81
- const thresholdFromRatio = axisSize * PANEL_COLLAPSE_THRESHOLD_RATIO
82
- const cappedThreshold = Math.min(PANEL_SNAP_THRESHOLD, PANEL_COLLAPSE_THRESHOLD_MAX, thresholdFromRatio)
83
- return Math.max(PANEL_MIN_SNAP_THRESHOLD, cappedThreshold)
109
+ return Math.max(0, Math.min(PANEL_SNAP_THRESHOLD, PANEL_COLLAPSE_THRESHOLD_MAX, axisSize * PANEL_COLLAPSE_THRESHOLD_RATIO))
84
110
  }
85
111
 
86
112
  /**
87
- * Adjust pixel-based panels in priority order to absorb remaining space.
88
- * 残余スペースを吸収するためにピクセル基準パネルを優先度順に調整する。
113
+ * Parse a CSS pixel string and return the numeric portion.
114
+ * CSS ピクセル表現から数値を抽出する処理。
89
115
  */
90
- type AdjustPixelOptions = {
91
- enforceAutoMinSize?: boolean
116
+ const parsePixelValue = (value: string | null | undefined): number => {
117
+ if (!value) {
118
+ return 0
119
+ }
120
+ const parsed = Number.parseFloat(value)
121
+ return Number.isFinite(parsed) ? parsed : 0
92
122
  }
93
123
 
94
- export const adjustPixelPanelsByPriority = (panels: PanelLayoutData[], sizeDiff: number, containerSize: number, options: AdjustPixelOptions = {}): number => {
95
- if (Math.abs(sizeDiff) <= 0) {
96
- return sizeDiff
97
- }
124
+ /**
125
+ * Measure a container along one axis, returning its content-box and border-box lengths.
126
+ * 指定軸でコンテナを計測し、内容領域長と境界領域長を返す処理。
127
+ *
128
+ * The content box is the authority for every layout number in this package: a panel's `flex-basis` resolves
129
+ * against it, so the allocator must fill exactly this length for the rendered geometry and the reducer state
130
+ * to agree. The measurement is therefore taken in LAYOUT pixels — `getBoundingClientRect` reports the visual
131
+ * rect, so a scaled ancestor would otherwise make the group allocate a container it does not have.
132
+ * このパッケージのレイアウト数値の基準は内容領域である。パネルの `flex-basis` はこの領域に解決されるため、
133
+ * 描画結果とリデューサー状態を一致させるには配分器がこの長さを満たす必要がある。計測は**レイアウトピクセル**で
134
+ * 行う: `getBoundingClientRect` は視覚上の矩形を返すため、拡大縮小された祖先の下では存在しない長さを配ってしまう。
135
+ *
136
+ * @param element - Element to measure / 計測対象の要素
137
+ * @param direction - Axis to measure along / 計測する軸
138
+ * @param entry - Observation that triggered the measurement, when it came from a ResizeObserver / 計測契機となった観測結果
139
+ * @returns Content-box and border-box lengths in pixels / 内容領域長と境界領域長 (px)
140
+ */
141
+ export const getContainerSize = (element: HTMLElement, direction: PanelDirection, entry?: ResizeObserverEntry): ContainerAxisMeasurement => {
142
+ const isHorizontal = direction === "horizontal"
143
+ const styles = typeof window !== "undefined" ? window.getComputedStyle(element) : null
144
+ const padding = styles ? (isHorizontal ? parsePixelValue(styles.paddingLeft) + parsePixelValue(styles.paddingRight) : parsePixelValue(styles.paddingTop) + parsePixelValue(styles.paddingBottom)) : 0
145
+ const borders = styles ? (isHorizontal ? parsePixelValue(styles.borderLeftWidth) + parsePixelValue(styles.borderRightWidth) : parsePixelValue(styles.borderTopWidth) + parsePixelValue(styles.borderBottomWidth)) : 0
98
146
 
99
- const enforceAutoMinSize = options.enforceAutoMinSize ?? false
147
+ // ResizeObserver の内容ボックス寸法はレイアウトピクセルの厳密値なので、得られるときは最優先で使う
148
+ const observedContent = entry?.contentBoxSize?.[0]
149
+ const observed = observedContent ? (isHorizontal ? observedContent.inlineSize : observedContent.blockSize) : undefined
150
+ // clientWidth / clientHeight もレイアウトピクセルだが整数に丸められる。端数を持つコンテナのために
151
+ // 内容ボックス寸法を優先し、無い環境でのみこちらへ落とす
152
+ const clientLength = isHorizontal ? element.clientWidth : element.clientHeight
153
+ const fallback = clientLength > 0 ? Math.max(0, clientLength - padding) : Math.max(0, (isHorizontal ? element.offsetWidth : element.offsetHeight) - padding - borders)
154
+ const content = observed ?? fallback
100
155
 
101
- // ピクセル指定パネルを抽出してインデックスを保持
102
- const pixelPanels = panels.map((panel, index) => ({ panel, index })).filter(({ panel }) => !panel.collapsed && (panel.sizeUnit === "pixels" || panel.originalPixelSize !== undefined))
156
+ return { content: Math.max(0, content), border: Math.max(0, content + padding + borders) }
157
+ }
103
158
 
104
- if (pixelPanels.length === 0) {
105
- return sizeDiff
106
- }
159
+ /**
160
+ * Selector that identifies a panel group element.
161
+ * パネルグループ要素を特定するセレクタ。
162
+ */
163
+ const GROUP_SELECTOR = "[data-panel-group-direction]"
107
164
 
108
- // 優先度順にソート (優先度が同じ場合はインデックス順)
109
- const isGrowing = sizeDiff > 0
110
- const sortedPanels = [...pixelPanels].sort((a, b) => {
111
- const priorityA = a.panel.pixelAdjustPriority ?? a.index + 1
112
- const priorityB = b.panel.pixelAdjustPriority ?? b.index + 1
113
- if (priorityA === priorityB) {
114
- return isGrowing ? b.index - a.index : a.index - b.index
115
- }
116
- return isGrowing ? priorityB - priorityA : priorityA - priorityB
117
- })
165
+ /**
166
+ * Selector that matches the elements a group lays out: its panels and its handle anchors.
167
+ * グループが配置する要素 (パネルとハンドルのアンカー) に一致するセレクタ。
168
+ */
169
+ export const LAYOUT_ELEMENT_SELECTOR = "[data-panel-id], [data-resize-handle-anchor]"
118
170
 
119
- let remaining = sizeDiff
171
+ /**
172
+ * Selector that matches a panel element.
173
+ * パネル要素に一致するセレクタ。
174
+ */
175
+ const PANEL_SELECTOR = "[data-panel-id]"
120
176
 
121
- /**
122
- * Execute a single adjustment pass for pixel panels.
123
- * ピクセルパネル調整処理
124
- */
125
- const adjustPass = (respectPreferred: boolean) => {
126
- for (const { index } of sortedPanels) {
127
- if (Math.abs(remaining) <= 0) {
128
- break
129
- }
177
+ /**
178
+ * Resolve the innermost panel group that owns an element.
179
+ * ある要素を所有する最も内側のパネルグループを解決する処理。
180
+ *
181
+ * The owner is never simply the parent: a wrapper element (including `display: contents`, which flex layout
182
+ * flattens away) sits between the group and its panels without changing the geometry, and nested groups make
183
+ * "the closest group" the only answer that is right in both directions.
184
+ * 所有者は単なる親要素ではない。中間要素 (フレックスレイアウトが畳む `display: contents` を含む) は幾何を
185
+ * 変えずにグループとパネルの間へ入り込み、入れ子グループもあるため「最も近いグループ」だけが正解となる。
186
+ *
187
+ * @param element - Element to resolve the owner for / 所有者を解決する対象の要素
188
+ * @returns Owning group element, or null when the element is outside every group / 所有するグループ要素、どのグループにも属さないなら null
189
+ */
190
+ export const findOwningGroup = (element: HTMLElement | null | undefined): HTMLElement | null => element?.closest<HTMLElement>(GROUP_SELECTOR) ?? null
130
191
 
131
- const panel = panels[index]
132
- if (!panel || panel.collapsed) {
192
+ /**
193
+ * Collect the panels and handle anchors a group owns, in document order.
194
+ * グループが所有するパネルとハンドルのアンカーを、文書順で収集する処理。
195
+ *
196
+ * Direct children are not enough: a wrapper (including `display: contents`, which flex layout flattens away)
197
+ * leaves the geometry intact while hiding the panels from a sibling scan, and the handle would silently lose
198
+ * its neighbours.
199
+ * 直下の子だけでは足りない。中間要素 (フレックスレイアウトが畳む `display: contents` を含む) があると幾何は
200
+ * 保たれたままパネルが兄弟走査から消え、ハンドルが隣接パネルを見失う。
201
+ *
202
+ * @param group - Group element / グループ要素
203
+ * @returns Owned panel and anchor elements in document order / 所有するパネル・アンカー要素 (文書順)
204
+ */
205
+ export const getGroupLayoutElements = (group: HTMLElement): HTMLElement[] =>
206
+ Array.from(group.querySelectorAll<HTMLElement>(LAYOUT_ELEMENT_SELECTOR)).filter((element) => {
207
+ if (findOwningGroup(element) !== group) {
208
+ return false
209
+ }
210
+ // `data-panel-id` は他パッケージも出力する。パネルの中に現れたものを配置要素と見なすと、
211
+ // ハンドルが本物の隣を見失って動かなくなる
212
+ const enclosingPanel = element.parentElement?.closest<HTMLElement>(PANEL_SELECTOR)
213
+ return !(enclosingPanel && group.contains(enclosingPanel))
214
+ })
215
+
216
+ /**
217
+ * Report whether a DOM mutation touched an element the group lays out.
218
+ * DOM の変更がグループの配置要素に触れたかどうかを判定する処理。
219
+ *
220
+ * Content inside a panel changes constantly (a virtualised list recycles rows every frame), so a group-wide
221
+ * rescan on every mutation costs time proportional to the whole subtree for changes that can never move a
222
+ * panel. Only additions and removals that carry a layout element can.
223
+ * パネルの内容は絶え間なく変わる (仮想化リストは毎フレーム行を入れ替える) ため、変更のたびにグループ全体を
224
+ * 走査すると、パネルを動かしようのない変更に対して部分木全体に比例した時間を払う。配置要素を含む
225
+ * 追加・削除だけがパネルを動かしうる。
226
+ *
227
+ * @param records - Mutation records delivered by the observer / オブザーバーが届けた変更記録
228
+ * @returns True when at least one record added or removed a layout element / 配置要素の追加・削除が 1 件でもあれば true
229
+ */
230
+ export const touchesLayoutElements = (records: ReadonlyArray<MutationRecord>): boolean => {
231
+ const carriesLayoutElement = (nodes: NodeList): boolean => {
232
+ for (const node of Array.from(nodes)) {
233
+ if (!(node instanceof Element)) {
133
234
  continue
134
235
  }
135
-
136
- // パネルの最小・最大サイズをピクセル値へ変換
137
- // bare number は他の全経路 (getConstraintInPixels) と同じくパーセンテージとして解釈する
138
- const minSizeFromConstraint = getConstraintInPixels(panel.minSize, 0, containerSize)
139
- const maxSize = getConstraintInPixels(panel.maxSize, containerSize, containerSize)
140
- const autoMinSize = enforceAutoMinSize ? getConstraintInPixels(panel.autoMinSize, 0, containerSize) : 0
141
- const minSize = Math.max(minSizeFromConstraint, autoMinSize)
142
- const preferredPixel = panel.preferredPixelSize !== undefined ? clamp(panel.preferredPixelSize, minSize, maxSize) : panel.size
143
-
144
- const panelIsCollapsible = panel.collapseFromStart || panel.collapseFromEnd
145
- if (remaining > 0) {
146
- // パネルサイズがゼロかどうかを判定
147
- const isZero = Math.abs(panel.size) <= 0 && (panel.percentageSize ?? 0) <= 0
148
- // ゼロのパネルはスキップ
149
- if (isZero) {
150
- continue
151
- }
152
-
153
- const targetSize = Math.max(panel.size, preferredPixel)
154
- const effectiveMax = respectPreferred ? Math.min(maxSize, targetSize) : maxSize
155
- const expansionCapacity = Math.max(0, effectiveMax - panel.size)
156
- if (expansionCapacity <= 0) {
157
- continue
158
- }
159
- const applied = Math.min(expansionCapacity, remaining)
160
- if (applied <= 0) {
161
- continue
162
- }
163
- const nextSize = panel.size + applied
164
- const nextPercentage = toRoundedPercentage(nextSize, containerSize)
165
- const nextSizeBeforeCollapse = panelIsCollapsible ? nextSize : panel.sizeBeforeCollapse
166
- const nextMeasuredPixelBeforeCollapse = panelIsCollapsible ? (panel.measuredPixelSizeBeforeCollapse ?? panel.measuredPixelSize ?? nextSize) : panel.measuredPixelSizeBeforeCollapse
167
- panels[index] = {
168
- ...panel,
169
- size: nextSize,
170
- percentageSize: nextPercentage,
171
- sizeBeforeCollapse: nextSizeBeforeCollapse,
172
- measuredPixelSizeBeforeCollapse: nextMeasuredPixelBeforeCollapse,
173
- measuredPixelSize: undefined,
174
- measuredPercentageSize: undefined,
175
- }
176
- remaining -= applied
177
- } else {
178
- const shrinkFloor = respectPreferred ? preferredPixel : minSize
179
- const reductionCapacity = Math.max(0, panel.size - shrinkFloor)
180
- if (reductionCapacity <= 0) {
181
- continue
182
- }
183
- const applied = Math.min(reductionCapacity, Math.abs(remaining))
184
- if (applied <= 0) {
185
- continue
186
- }
187
- const nextSize = panel.size - applied
188
- const nextPercentage = toRoundedPercentage(nextSize, containerSize)
189
- const nextSizeBeforeCollapse = panelIsCollapsible ? nextSize : panel.sizeBeforeCollapse
190
- const nextMeasuredPixelBeforeCollapse = panelIsCollapsible ? (panel.measuredPixelSizeBeforeCollapse ?? panel.measuredPixelSize ?? nextSize) : panel.measuredPixelSizeBeforeCollapse
191
- panels[index] = {
192
- ...panel,
193
- size: nextSize,
194
- percentageSize: nextPercentage,
195
- sizeBeforeCollapse: nextSizeBeforeCollapse,
196
- measuredPixelSizeBeforeCollapse: nextMeasuredPixelBeforeCollapse,
197
- measuredPixelSize: undefined,
198
- measuredPercentageSize: undefined,
199
- }
200
- remaining += applied
236
+ if (node.matches(LAYOUT_ELEMENT_SELECTOR) || node.querySelector(LAYOUT_ELEMENT_SELECTOR) !== null) {
237
+ return true
201
238
  }
202
239
  }
240
+ return false
203
241
  }
204
-
205
- adjustPass(true)
206
-
207
- if ((isGrowing && remaining > 0) || (!isGrowing && remaining < 0)) {
208
- adjustPass(false)
209
- }
210
-
211
- return remaining
242
+ return records.some((record) => carriesLayoutElement(record.addedNodes) || carriesLayoutElement(record.removedNodes))
212
243
  }
213
244
 
214
245
  /**
215
- * Parse a CSS pixel string and return the numeric portion.
216
- * CSS ピクセル表現から数値を抽出する処理。
246
+ * Ratio between visual pixels (pointer coordinates) and layout pixels (the lengths the allocator hands out).
247
+ * 視覚ピクセル (ポインタ座標) とレイアウトピクセル (配分器が配る長さ) の比。
248
+ *
249
+ * A transformed ancestor scales pointer coordinates but not the lengths the layout is computed in, so a drag
250
+ * delta read straight from the pointer moves the boundary by `travel × scale` instead of `travel` — the grab
251
+ * point slides out from under the cursor, and the error grows without bound as the gesture continues.
252
+ * 変形された祖先の下ではポインタ座標だけが拡大縮小され、レイアウトが計算される長さは変わらない。
253
+ * ポインタの変位をそのまま使うと境界は `移動量 × 倍率` しか動かず、掴んだ点がカーソルから滑り出し、
254
+ * その誤差はジェスチャーが続く限り増え続ける。
255
+ *
256
+ * @param element - Element whose box is compared in both spaces / 双方の空間で矩形を比べる要素
257
+ * @param direction - Axis to measure along / 計測する軸
258
+ * @returns Visual length divided by layout length, 1 when either is unavailable / 視覚長 ÷ レイアウト長 (いずれか不明なら 1)
217
259
  */
218
- const parsePixelValue = (value: string | null): number => {
219
- if (!value) {
220
- return 0
260
+ export const getVisualScale = (element: HTMLElement, direction: PanelDirection): number => {
261
+ if (typeof window === "undefined") {
262
+ return 1
221
263
  }
222
- const parsed = parseFloat(value)
223
- return Number.isFinite(parsed) ? parsed : 0
224
- }
225
-
226
- /**
227
- * Measure container size in a given direction, returning inner (borderless) and outer dimensions.
228
- * 指定方向で枠線控除後と控除前の両方の寸法を測定して返す。
229
- */
230
- export const getContainerSize = (element: HTMLElement, direction: PanelDirection): ContainerAxisMeasurement => {
264
+ const isHorizontal = direction === "horizontal"
231
265
  const rect = element.getBoundingClientRect()
232
- const styles = typeof window !== "undefined" ? window.getComputedStyle(element) : null
233
- const horizontalBorder = styles ? parsePixelValue(styles.borderLeftWidth) + parsePixelValue(styles.borderRightWidth) : 0
234
- const verticalBorder = styles ? parsePixelValue(styles.borderTopWidth) + parsePixelValue(styles.borderBottomWidth) : 0
235
- if (direction === "horizontal") {
236
- const rectWidth = rect.width > 0 ? rect.width : element.offsetWidth
237
- const innerWidth = rect.width > 0 ? Math.max(0, rect.width - horizontalBorder) : element.clientWidth
238
- const outerWidthFallback = element.offsetWidth > 0 ? element.offsetWidth : element.clientWidth + horizontalBorder
239
- // 枠線の厚みを控除した実効幅と控除前の幅をそれぞれ算出
240
- return {
241
- inner: innerWidth > 0 ? innerWidth : Math.max(0, element.clientWidth),
242
- outer: rectWidth > 0 ? rectWidth : Math.max(0, outerWidthFallback),
243
- }
244
- }
245
- const rectHeight = rect.height > 0 ? rect.height : element.offsetHeight
246
- const innerHeight = rect.height > 0 ? Math.max(0, rect.height - verticalBorder) : element.clientHeight
247
- const outerHeightFallback = element.offsetHeight > 0 ? element.offsetHeight : element.clientHeight + verticalBorder
248
- // 枠線控除後の高さと控除前の高さを併せて返却
249
- return {
250
- inner: innerHeight > 0 ? innerHeight : Math.max(0, element.clientHeight),
251
- outer: rectHeight > 0 ? rectHeight : Math.max(0, outerHeightFallback),
266
+ const visual = isHorizontal ? rect.width : rect.height
267
+ const styles = window.getComputedStyle(element)
268
+ // グループは box-sizing: border-box を所有するため、計算値の幅・高さは視覚矩形と同じ境界領域を指す。
269
+ // ただし配置されていない要素では指定値 (`100%` など) が返るため、px でなければ換算しない
270
+ const declared = isHorizontal ? styles.width : styles.height
271
+ if (!declared.endsWith("px")) {
272
+ return 1
252
273
  }
274
+ const layout = Number.parseFloat(declared)
275
+ return visual > 0 && Number.isFinite(layout) && layout > 0 ? visual / layout : 1
253
276
  }
254
277
 
255
278
  /**
256
- * Persist panel layout sizes to local storage.
257
- * パネルレイアウトのサイズをローカルストレージへ保存する。
279
+ * Persist a panel layout to local storage.
280
+ * パネルレイアウトをローカルストレージへ保存する。
281
+ *
282
+ * @param id - Auto-save identifier / 自動保存の識別子
283
+ * @param entries - Panel lengths with their identifiers, in DOM order / DOM 順の識別子つきパネル長
258
284
  */
259
- export const saveLayout = (id: string, sizes: number[]): void => {
285
+ export const saveLayout = (id: string, entries: PersistedPanelSize[]): void => {
260
286
  try {
261
- localStorage.setItem(`panel-layout-${id}`, JSON.stringify(sizes))
287
+ localStorage.setItem(`panel-layout-${id}`, JSON.stringify(entries))
262
288
  } catch (error) {
263
289
  console.warn("Failed to save panel layout:", error)
264
290
  }
265
291
  }
266
292
 
267
293
  /**
268
- * Restore panel layout sizes from local storage.
269
- * パネルレイアウトのサイズをローカルストレージから復元する。
294
+ * Restore a panel layout from local storage.
295
+ * パネルレイアウトをローカルストレージから復元する。
296
+ *
297
+ * A stored layout is only usable when every entry names the panel it belongs to, so a payload written by a
298
+ * different panel set is rejected instead of being applied by position.
299
+ * 保存レイアウトは各項目が所属パネルを名指ししている場合にだけ利用できる。別構成が書いた内容は位置で当てはめず拒否する。
300
+ *
301
+ * @param id - Auto-save identifier / 自動保存の識別子
302
+ * @returns Stored layout, or null when nothing usable is stored / 保存済みレイアウト、利用できなければ null
270
303
  */
271
- export const loadLayout = (id: string): number[] | null => {
304
+ export const loadLayout = (id: string): PersistedPanelSize[] | null => {
272
305
  try {
273
306
  const saved = localStorage.getItem(`panel-layout-${id}`)
274
- return saved ? JSON.parse(saved) : null
307
+ if (!saved) {
308
+ return null
309
+ }
310
+ const parsed: unknown = JSON.parse(saved)
311
+ if (!Array.isArray(parsed)) {
312
+ return null
313
+ }
314
+ const isEntry = (value: unknown): value is PersistedPanelSize => typeof value === "object" && value !== null && typeof (value as PersistedPanelSize).id === "string" && Number.isFinite((value as PersistedPanelSize).size)
315
+ return parsed.every(isEntry) ? parsed : null
275
316
  } catch (error) {
276
317
  console.warn("Failed to load panel layout:", error)
277
318
  return null
@@ -1 +0,0 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("react/jsx-runtime"),A=require("react"),R=require("react-dom"),x=require("./debugOverlayStore-Dkl-fHoa.cjs"),P=require("./index-xTV-c0PN.cjs"),o=(n,r)=>typeof n!="number"||!Number.isFinite(n)?"—":`${(n<0?0:n).toFixed(8)}${r}`,T={position:"fixed",left:"50%",bottom:"20px",transform:"translate(-50%, 0)",zIndex:100,maxWidth:"min(90vw, 1120px)",width:"100%",borderRadius:"14px",backgroundColor:"rgba(15, 23, 42, 0.72)",color:"#e2e8f0",fontFamily:"monospace",fontSize:"11px",lineHeight:1.6,boxShadow:"0 22px 45px rgba(15,23,42,0.45)",border:"1px solid rgba(148, 163, 184, 0.22)",backdropFilter:"blur(10px)",WebkitBackdropFilter:"blur(10px)",pointerEvents:"auto",overflow:"auto"},Y={display:"flex",justifyContent:"space-between",alignItems:"center",flexWrap:"wrap",gap:"12px"},w={display:"flex",flexDirection:"column",gap:"4px"},D={padding:"6px 10px",borderRadius:"9999px",border:"1px solid rgba(148, 163, 184, 0.35)",color:"#e2e8f0",fontWeight:600,fontSize:"10px",letterSpacing:"0.03em",textTransform:"uppercase",cursor:"pointer",transition:"background-color 0.18s ease, border-color 0.18s ease"},C={padding:"10px 12px",borderRadius:"10px",backgroundColor:"rgba(30, 41, 59, 0.55)",border:"1px solid rgba(148, 163, 184, 0.18)",display:"flex",flexDirection:"column",gap:"10px"},N={padding:"8px 10px",borderRadius:"8px",backgroundColor:"rgba(15, 23, 42, 0.55)",border:"1px solid rgba(148, 163, 184, 0.16)",display:"flex",flexDirection:"column",gap:"4px"},p={display:"flex",justifyContent:"space-between",alignItems:"center",gap:"6px"},k={display:"grid",gridTemplateColumns:"auto 1fr",gap:"2px 10px",color:"#e2e8f0"},G={padding:"2px 6px",borderRadius:"9999px",fontSize:"10px",textTransform:"uppercase"},I=({groupId:n})=>x.useDebugOverlayOwnerId()!==n?null:e.jsx(u,{}),u=()=>{const r=x.useDebugOverlaySnapshot().groups,[t,g]=A.useState(!0);if(r.length===0||typeof document>"u")return null;const b=r.reduce((i,a)=>i+a.panels.length,0),h=r.reduce((i,a)=>i+(a.handles?.length??0),0),m=()=>g(i=>!i),f=e.jsxs("div",{style:{...T,padding:t?"10px 18px":"14px 18px",maxHeight:t?"52px":"min(65vh, 520px)",transition:"max-height 0.24s ease, padding 0.24s ease"},children:[e.jsxs("div",{style:{...Y,marginBottom:t?0:"12px"},children:[e.jsxs("div",{style:w,children:[e.jsx("span",{style:{fontWeight:700,fontSize:"12px",letterSpacing:"0.03em"},children:"Panel Overview"}),e.jsxs("span",{style:{color:"#cbd5f5"},children:["groups: ",r.length," / panels: ",b," / resizers: ",h]})]}),e.jsx("button",{type:"button",onClick:m,style:{...D,backgroundColor:t?"rgba(59, 130, 246, 0.2)":"rgba(71, 85, 105, 0.45)"},title:t?"Panel Overview を開く":"Panel Overview を閉じる",children:t?"開く":"閉じる"})]}),!t&&e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:"12px",maxHeight:"45vh",overflowY:"auto",paddingRight:"6px"},children:r.map((i,a)=>{const l=i.direction==="horizontal"?i.containerSize.width:i.containerSize.height;return e.jsxs("div",{style:C,children:[e.jsxs("div",{style:{...p,flexWrap:"wrap",gap:"10px",color:"#dbeafe"},children:[e.jsxs("span",{style:{fontWeight:600,fontSize:"11px",letterSpacing:"0.02em",textTransform:"uppercase"},children:["group ",a+1,": ",i.displayName]}),e.jsxs("span",{children:["direction: ",i.direction," / container: ",o(i.containerSize.width,"px")," × ",o(i.containerSize.height,"px")]})]}),e.jsx("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fill, minmax(220px, 1fr))",gap:"10px"},children:i.panels.map((s,y)=>{const d=s.measuredPixelSize??s.size,S=s.size,j=o(S,"px"),v=s.percentageSize??(l>0?s.size/l*100:void 0),O=s.measuredPercentageSize!==void 0?s.measuredPercentageSize:l>0?d/l*100:void 0,c=s.collapsed||d<=P.PANEL_SNAP_THRESHOLD,E=s.collapsed?"collapsed":c?"hidden":"visible",z=o(v,"%"),L=o(O,"%"),_=o(d,"px");return e.jsxs("div",{style:N,children:[e.jsxs("div",{style:p,children:[e.jsx("span",{style:{flex:1,minWidth:0,overflow:"hidden",textOverflow:"ellipsis"},children:s.id}),e.jsx("span",{style:{...G,backgroundColor:s.collapsed?"rgba(251, 113, 133, 0.25)":c?"rgba(250, 204, 21, 0.25)":"rgba(74, 222, 128, 0.25)",color:s.collapsed?"#fecdd3":c?"#fef08a":"#bbf7d0"},children:E})]}),e.jsxs("div",{style:k,children:[e.jsx("span",{children:"size (state):"}),e.jsx("span",{children:j}),e.jsx("span",{children:"size (measured):"}),e.jsx("span",{children:_}),e.jsx("span",{children:"percentage (state):"}),e.jsx("span",{children:z}),e.jsx("span",{children:"percentage (measured):"}),e.jsx("span",{children:L}),e.jsx("span",{children:"unit:"}),e.jsx("span",{children:s.sizeUnit}),e.jsx("span",{children:"min:"}),e.jsx("span",{children:s.minSize!==void 0?JSON.stringify(s.minSize):"—"}),e.jsx("span",{children:"auto min:"}),e.jsx("span",{children:s.autoMinSize!==void 0?JSON.stringify(s.autoMinSize):"—"}),e.jsx("span",{children:"max:"}),e.jsx("span",{children:s.maxSize!==void 0?JSON.stringify(s.maxSize):"—"}),e.jsx("span",{children:"priority:"}),e.jsx("span",{children:s.pixelAdjustPriority??"—"}),e.jsx("span",{children:"flex priority:"}),e.jsx("span",{children:s.flexAdjustPriority??"—"})]})]},s.id||y)})})]},i.groupId)})})]});return R.createPortal(f,document.body)};exports.GlobalDebugOverlay=u;exports.GlobalDebugOverlayGate=I;
@@ -1 +0,0 @@
1
- {"version":3,"file":"GlobalDebugOverlay.d.ts","sourceRoot":"","sources":["../src/GlobalDebugOverlay.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAyGH;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,GAAI,aAAa;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,uCAMtE,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,kBAAkB,0CA2J9B,CAAA"}
package/dist/Panel.d.ts DELETED
@@ -1,7 +0,0 @@
1
- import { PanelProps } from './types';
2
- /**
3
- * Render a resizable panel within a managed group, keeping DOM measurements in sync with reducer state.
4
- * 管理対象グループ内で DOM 計測とリデューサー状態を同期させながらリサイズ可能パネルを描画するコンポーネント。
5
- */
6
- export declare const Panel: import('react').MemoExoticComponent<({ id: providedId, defaultSize, minSize, maxSize, autoMinSize, className, style, children, order, collapsible, defaultCollapsed, pixelAdjustPriority, flexAdjustPriority, contentOverflow }: PanelProps) => import("react").JSX.Element>;
7
- //# sourceMappingURL=Panel.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Panel.d.ts","sourceRoot":"","sources":["../src/Panel.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,OAAO,KAAK,EAA6C,UAAU,EAAE,MAAM,SAAS,CAAA;AA6FpF;;;GAGG;AACH,eAAO,MAAM,KAAK,mOACwO,UAAU,iCA+gBnQ,CAAA"}
@@ -1,26 +0,0 @@
1
- import { PanelDirection, PanelLayoutData } from './types';
2
- type PanelDebugInfoProps = {
3
- panel: PanelLayoutData | undefined;
4
- measuredPixelSize: number;
5
- measuredPercentageSize: number;
6
- containerAxisSize: number;
7
- direction: PanelDirection;
8
- };
9
- type FormatPanelDebugInfoArgs = {
10
- panel: PanelLayoutData | undefined;
11
- measuredPixelSize: number;
12
- measuredPercentageSize: number;
13
- containerAxisSize: number;
14
- containerLabel: string;
15
- };
16
- type FormattedPanelDebugInfo = {
17
- stateValueDisplay: string;
18
- measuredValueDisplay: string;
19
- containerDisplay: string;
20
- headlineLabel: string;
21
- titleText: string;
22
- };
23
- export declare const formatPanelDebugInfo: ({ panel, measuredPixelSize, measuredPercentageSize, containerAxisSize, containerLabel }: FormatPanelDebugInfoArgs) => FormattedPanelDebugInfo;
24
- export declare const PanelDebugInfo: import('react').MemoExoticComponent<({ panel, measuredPixelSize, measuredPercentageSize, containerAxisSize, direction }: PanelDebugInfoProps) => import("react").JSX.Element>;
25
- export {};
26
- //# sourceMappingURL=PanelDebugInfo.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"PanelDebugInfo.d.ts","sourceRoot":"","sources":["../src/PanelDebugInfo.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAE9D,KAAK,mBAAmB,GAAG;IACvB,KAAK,EAAE,eAAe,GAAG,SAAS,CAAA;IAClC,iBAAiB,EAAE,MAAM,CAAA;IACzB,sBAAsB,EAAE,MAAM,CAAA;IAC9B,iBAAiB,EAAE,MAAM,CAAA;IACzB,SAAS,EAAE,cAAc,CAAA;CAC5B,CAAA;AAED,KAAK,wBAAwB,GAAG;IAC5B,KAAK,EAAE,eAAe,GAAG,SAAS,CAAA;IAClC,iBAAiB,EAAE,MAAM,CAAA;IACzB,sBAAsB,EAAE,MAAM,CAAA;IAC9B,iBAAiB,EAAE,MAAM,CAAA;IACzB,cAAc,EAAE,MAAM,CAAA;CACzB,CAAA;AAED,KAAK,uBAAuB,GAAG;IAC3B,iBAAiB,EAAE,MAAM,CAAA;IACzB,oBAAoB,EAAE,MAAM,CAAA;IAC5B,gBAAgB,EAAE,MAAM,CAAA;IACxB,aAAa,EAAE,MAAM,CAAA;IACrB,SAAS,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,eAAO,MAAM,oBAAoB,GAAI,yFAAyF,wBAAwB,KAAG,uBAkBxJ,CAAA;AAED,eAAO,MAAM,cAAc,2HAA6F,mBAAmB,iCA+FzI,CAAA"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"PanelGroup.d.ts","sourceRoot":"","sources":["../src/PanelGroup.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AA2E9C;;;GAGG;AACH,eAAO,MAAM,UAAU,GAAI,OAAO,eAAe,gCA2KhD,CAAA"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"PanelResizeHandle.d.ts","sourceRoot":"","sources":["../src/PanelResizeHandle.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,EAUH,KAAK,sBAAsB,EAE9B,MAAM,SAAS,CAAA;AAGhB;;;GAGG;AACH,eAAO,MAAM,iBAAiB,iGAA2E,sBAAsB,iCAkrB7H,CAAA"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAA;AAErD;;;GAGG;AACH,eAAO,MAAM,iBAAiB,wDAAqD,CAAA;AAEnF;;;GAGG;AACH,eAAO,MAAM,aAAa,8BAMzB,CAAA"}