@aiquants/resize-panels 1.9.0 → 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 (80) hide show
  1. package/README.md +129 -22
  2. package/dist/GlobalDebugOverlay-Cf3GOn2A.cjs +1 -0
  3. package/dist/{GlobalDebugOverlay-Do70T2l6.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/src/PanelResizeHandle.d.ts.map +1 -0
  18. package/dist/src/allocateLayout.d.ts +60 -0
  19. package/dist/src/allocateLayout.d.ts.map +1 -0
  20. package/dist/src/context.d.ts.map +1 -0
  21. package/dist/{debugOverlayStore.d.ts → src/debugOverlayStore.d.ts} +10 -2
  22. package/dist/src/debugOverlayStore.d.ts.map +1 -0
  23. package/dist/src/hooks.d.ts +48 -0
  24. package/dist/src/hooks.d.ts.map +1 -0
  25. package/dist/src/index.d.ts +32 -0
  26. package/dist/src/index.d.ts.map +1 -0
  27. package/dist/src/reducer.d.ts +49 -0
  28. package/dist/src/reducer.d.ts.map +1 -0
  29. package/dist/src/roundHalfToEven.d.ts.map +1 -0
  30. package/dist/{types.d.ts → src/types.d.ts} +164 -60
  31. package/dist/src/types.d.ts.map +1 -0
  32. package/dist/src/utils/simple-logger.d.ts.map +1 -0
  33. package/dist/src/utils.d.ts +164 -0
  34. package/dist/src/utils.d.ts.map +1 -0
  35. package/dist/styles/resize-panels.standalone.css +1 -1
  36. package/dist/tests/support/dom-harness.d.ts +89 -0
  37. package/dist/tests/support/dom-harness.d.ts.map +1 -0
  38. package/package.json +1 -1
  39. package/src/GlobalDebugOverlay.tsx +3 -3
  40. package/src/Panel.tsx +203 -541
  41. package/src/PanelDebugInfo.tsx +52 -106
  42. package/src/PanelGroup.tsx +27 -42
  43. package/src/PanelResizeHandle.tsx +470 -509
  44. package/src/allocateLayout.ts +412 -0
  45. package/src/debugOverlayStore.ts +19 -15
  46. package/src/hooks.ts +187 -779
  47. package/src/index.ts +12 -3
  48. package/src/reducer.ts +364 -1126
  49. package/src/types.ts +149 -63
  50. package/src/utils.ts +205 -164
  51. package/dist/GlobalDebugOverlay-sEQN6exo.cjs +0 -1
  52. package/dist/GlobalDebugOverlay.d.ts.map +0 -1
  53. package/dist/Panel.d.ts +0 -7
  54. package/dist/Panel.d.ts.map +0 -1
  55. package/dist/PanelDebugInfo.d.ts +0 -26
  56. package/dist/PanelDebugInfo.d.ts.map +0 -1
  57. package/dist/PanelGroup.d.ts.map +0 -1
  58. package/dist/PanelResizeHandle.d.ts.map +0 -1
  59. package/dist/context.d.ts.map +0 -1
  60. package/dist/debugOverlayStore-Cntyxboe.js +0 -141
  61. package/dist/debugOverlayStore-Dkl-fHoa.cjs +0 -1
  62. package/dist/debugOverlayStore.d.ts.map +0 -1
  63. package/dist/hooks.d.ts +0 -45
  64. package/dist/hooks.d.ts.map +0 -1
  65. package/dist/index-B6YGX2DH.js +0 -2009
  66. package/dist/index-DssZUxGw.cjs +0 -2
  67. package/dist/index.d.ts.map +0 -1
  68. package/dist/reducer.d.ts +0 -39
  69. package/dist/reducer.d.ts.map +0 -1
  70. package/dist/roundHalfToEven.d.ts.map +0 -1
  71. package/dist/types.d.ts.map +0 -1
  72. package/dist/utils/simple-logger.d.ts.map +0 -1
  73. package/dist/utils.d.ts +0 -61
  74. package/dist/utils.d.ts.map +0 -1
  75. /package/dist/{GlobalDebugOverlay.d.ts → src/GlobalDebugOverlay.d.ts} +0 -0
  76. /package/dist/{PanelGroup.d.ts → src/PanelGroup.d.ts} +0 -0
  77. /package/dist/{PanelResizeHandle.d.ts → src/PanelResizeHandle.d.ts} +0 -0
  78. /package/dist/{context.d.ts → src/context.d.ts} +0 -0
  79. /package/dist/{roundHalfToEven.d.ts → src/roundHalfToEven.d.ts} +0 -0
  80. /package/dist/{utils → src/utils}/simple-logger.d.ts +0 -0
@@ -0,0 +1,412 @@
1
+ /**
2
+ * @file Pure layout allocator: the single source of truth for panel sizes.
3
+ * パネルサイズの唯一の真実源となる純粋なレイアウト配分器。
4
+ *
5
+ * The allocator answers one question: given the panels' preferences, their box constraints and the
6
+ * container's content-box length C, how many pixels does each panel get? Its result satisfies
7
+ * `Σ active size == C` whenever the constraints allow it, which is what lets the rendered DOM and the
8
+ * reducer state describe the same geometry.
9
+ * 配分器が答える問いは 1 つ: 各パネルの希望・箱制約・コンテナ内容領域長 C が与えられたとき、各パネルへ
10
+ * 何ピクセル配るか。制約が許す限り `Σ アクティブサイズ == C` を満たすため、描画結果とリデューサー状態が
11
+ * 同一の幾何を表す。
12
+ */
13
+
14
+ import { type LayoutConstraintViolation, PANEL_ALLOCATION_EPSILON, type PanelLayoutData } from "./types"
15
+ import { clamp, getConstraintInPixels } from "./utils"
16
+
17
+ /**
18
+ * One panel reduced to the quantities the allocator needs.
19
+ * 配分器が必要とする量だけに還元したパネル 1 枚分の入力。
20
+ */
21
+ type AllocationItem = {
22
+ index: number
23
+ /** Ratio weight for flexible panels; 0 for pixel panels and for panels dragged to zero. 比例配分の重み。 */
24
+ weight: number
25
+ /** Size this panel is filled to before ratio sharing starts; null for pure ratio panels. 先行充当の目標値。 */
26
+ target: number | null
27
+ /** Serving order inside the sequential tiers, higher first. 逐次充当の順位 (大きいほど先)。 */
28
+ priority: number
29
+ /** Sequential tier: pixel panels are served before prioritized flexible panels. 逐次充当の段。 */
30
+ tier: number
31
+ min: number
32
+ max: number
33
+ }
34
+
35
+ /**
36
+ * Resolved pixel bounds of a panel for a given container length.
37
+ * 指定コンテナ長におけるパネルのピクセル境界。
38
+ */
39
+ export type PanelBounds = {
40
+ min: number
41
+ max: number
42
+ }
43
+
44
+ /**
45
+ * Sequential tier index of pixel panels (served before prioritized flexible panels).
46
+ * ピクセルパネルの逐次充当段 (優先度つき柔軟パネルより先に充当)。
47
+ */
48
+ const PIXEL_TIER = 0
49
+
50
+ /**
51
+ * Sequential tier index of flexible panels that declare `flexAdjustPriority`.
52
+ * `flexAdjustPriority` を宣言した柔軟パネルの逐次充当段。
53
+ */
54
+ const PRIORITY_FLEXIBLE_TIER = 1
55
+
56
+ /**
57
+ * Reduce a length to a finite non-negative number.
58
+ * 長さを有限かつ非負の数値へ整流する処理。
59
+ *
60
+ * A non-finite preference or constraint would propagate through the whole allocation as NaN and defeat every
61
+ * comparison that guards it, so it is turned into 0 at the boundary instead of being carried further.
62
+ * 有限でない希望値や制約は配分全体へ NaN として伝播し、それを守るはずの比較をすべて無効化するため、
63
+ * 先へ渡さず境界で 0 に落とす。
64
+ *
65
+ * @param value - Length to sanitize / 整流する長さ
66
+ * @returns The value itself when finite and non-negative, otherwise 0 / 有限かつ非負ならその値、それ以外は 0
67
+ */
68
+ const sanitizeLength = (value: number): number => (Number.isFinite(value) && value > 0 ? value : 0)
69
+
70
+ /**
71
+ * Report whether a panel is sized in pixels rather than as a share of the container.
72
+ * パネルがコンテナ比ではなくピクセルでサイズ指定されているかの判定。
73
+ *
74
+ * @param panel - Panel layout data / パネルレイアウトデータ
75
+ * @returns True when the panel keeps an absolute pixel size / 絶対ピクセル指定なら true
76
+ */
77
+ export const isPixelPanel = (panel: PanelLayoutData): boolean => panel.sizeUnit === "pixels"
78
+
79
+ /**
80
+ * Resolve a panel's bounds in pixels for the given container length.
81
+ * 指定コンテナ長に対するパネルの境界をピクセルで解決する処理。
82
+ *
83
+ * The same bounds govern the allocator and an explicit drag: a size the user cannot reach by hand but the
84
+ * allocator may produce (or the reverse) would make the two disagree about where a panel is allowed to be.
85
+ * 配分器と明示的なドラッグは同じ境界に従う。片方だけが到達できるサイズがあると、パネルが居てよい範囲について
86
+ * 両者の認識が食い違うためである。
87
+ *
88
+ * @param panel - Panel layout data / パネルレイアウトデータ
89
+ * @param containerSize - Container content-box length in pixels / コンテナ内容領域長 (px)
90
+ * @returns Pixel bounds with `min <= max` guaranteed / `min <= max` を保証したピクセル境界
91
+ */
92
+ export const resolvePanelBounds = (panel: PanelLayoutData, containerSize: number): PanelBounds => {
93
+ const min = Math.max(0, sanitizeLength(getConstraintInPixels(panel.minSize, 0, containerSize)))
94
+ const max = Math.max(min, sanitizeLength(getConstraintInPixels(panel.maxSize, containerSize, containerSize)))
95
+ return { min, max }
96
+ }
97
+
98
+ /**
99
+ * Distribute a budget across items proportionally to their weights while honouring box constraints.
100
+ * 箱制約を守りつつ、重みに比例して予算を配分する処理 (water-filling)。
101
+ *
102
+ * Solves `sᵢ = clamp(λ·wᵢ, minᵢ, maxᵢ)` with `Σ sᵢ = budget`. The solution is unique even when λ is not,
103
+ * and it preserves the ratio `sᵢ : sⱼ = wᵢ : wⱼ` for every pair that is not clamped, so the residue of a
104
+ * clamped panel is shared out in the remaining panels' own ratio.
105
+ * `Σ sᵢ = budget` を満たす `sᵢ = clamp(λ·wᵢ, minᵢ, maxᵢ)` を解く。λ が一意でなくとも解は一意であり、
106
+ * クランプされていない組では比 `sᵢ : sⱼ = wᵢ : wⱼ` が保たれるため、クランプ分の余りは残りのパネルの比で分配される。
107
+ *
108
+ * @param items - Items to size, each with a weight and pixel bounds / 重みとピクセル境界を持つ配分対象
109
+ * @param budget - Total pixels to distribute / 配分する総ピクセル数
110
+ * @returns Pixel size per item, index-aligned with `items` / `items` と同順のピクセルサイズ
111
+ */
112
+ const waterFill = (items: ReadonlyArray<AllocationItem>, budget: number): number[] => {
113
+ const sizes = items.map((item) => item.min)
114
+ const frozen = items.map(() => false)
115
+
116
+ for (;;) {
117
+ let freeWeight = 0
118
+ let frozenTotal = 0
119
+ let freeCount = 0
120
+ for (let index = 0; index < items.length; index += 1) {
121
+ if (frozen[index]) {
122
+ frozenTotal += sizes[index]
123
+ continue
124
+ }
125
+ freeCount += 1
126
+ freeWeight += items[index].weight
127
+ }
128
+ if (freeCount === 0) {
129
+ return sizes
130
+ }
131
+
132
+ const remaining = budget - frozenTotal
133
+ if (freeWeight <= 0) {
134
+ // 重みが無いパネルは比例配分の対象にならないため下限で確定する
135
+ for (let index = 0; index < items.length; index += 1) {
136
+ if (!frozen[index]) {
137
+ sizes[index] = items[index].min
138
+ }
139
+ }
140
+ return sizes
141
+ }
142
+
143
+ const lambda = Math.max(0, remaining / freeWeight)
144
+ let violation = 0
145
+ for (let index = 0; index < items.length; index += 1) {
146
+ if (frozen[index]) {
147
+ continue
148
+ }
149
+ const raw = lambda * items[index].weight
150
+ const clamped = clamp(raw, items[index].min, items[index].max)
151
+ sizes[index] = clamped
152
+ violation += clamped - raw
153
+ }
154
+
155
+ // violation > 0 は下限側が持ち上げた分だけ超過したことを意味し、真の λ はさらに小さい。
156
+ // その状況で下限に張り付いたパネルは解でも下限なので、確定させて残りを配り直す (上限側は対称)
157
+ let froze = false
158
+ for (let index = 0; index < items.length; index += 1) {
159
+ if (frozen[index]) {
160
+ continue
161
+ }
162
+ const raw = lambda * items[index].weight
163
+ if (violation > PANEL_ALLOCATION_EPSILON && sizes[index] > raw + PANEL_ALLOCATION_EPSILON) {
164
+ frozen[index] = true
165
+ sizes[index] = items[index].min
166
+ froze = true
167
+ } else if (violation < -PANEL_ALLOCATION_EPSILON && sizes[index] < raw - PANEL_ALLOCATION_EPSILON) {
168
+ frozen[index] = true
169
+ sizes[index] = items[index].max
170
+ froze = true
171
+ }
172
+ }
173
+ if (!froze) {
174
+ return sizes
175
+ }
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Build the allocator's view of a panel from its layout data.
181
+ * パネルレイアウトデータから配分器用の入力を構築する処理。
182
+ */
183
+ const toAllocationItem = (panel: PanelLayoutData, index: number, containerSize: number): AllocationItem => {
184
+ const { min, max } = resolvePanelBounds(panel, containerSize)
185
+
186
+ if (isPixelPanel(panel)) {
187
+ return {
188
+ index,
189
+ weight: 0,
190
+ target: clamp(sanitizeLength(panel.preferredPixelSize), min, max),
191
+ priority: panel.pixelAdjustPriority ?? 0,
192
+ tier: PIXEL_TIER,
193
+ min,
194
+ max,
195
+ }
196
+ }
197
+
198
+ const weight = sanitizeLength(panel.preferredPercentageSize)
199
+ const hasPriority = panel.flexAdjustPriority !== undefined
200
+ return {
201
+ index,
202
+ weight,
203
+ target: hasPriority ? clamp((weight / 100) * containerSize, min, max) : null,
204
+ priority: panel.flexAdjustPriority ?? 0,
205
+ tier: PRIORITY_FLEXIBLE_TIER,
206
+ min,
207
+ max,
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Fill items up to a per-item ceiling in tier and priority order, consuming the available budget.
213
+ * 段・優先度の順に上限まで充当し、利用可能な予算を消費する処理。
214
+ *
215
+ * @param items - Items to fill, already ordered / 充当対象 (整列済み)
216
+ * @param sizes - Sizes mutated in place / その場で更新されるサイズ配列
217
+ * @param ceilingOf - Upper bound for each item / 各要素の充当上限
218
+ * @param available - Pixels available for filling / 充当に使えるピクセル数
219
+ * @returns Pixels left after filling / 充当後に残ったピクセル数
220
+ */
221
+ const fillInOrder = (items: ReadonlyArray<AllocationItem>, sizes: number[], ceilingOf: (item: AllocationItem) => number, available: number): number => {
222
+ let remaining = available
223
+ for (const item of items) {
224
+ if (remaining <= PANEL_ALLOCATION_EPSILON) {
225
+ break
226
+ }
227
+ const room = ceilingOf(item) - sizes[item.index]
228
+ if (room <= 0) {
229
+ continue
230
+ }
231
+ const applied = Math.min(room, remaining)
232
+ sizes[item.index] += applied
233
+ remaining -= applied
234
+ }
235
+ return remaining
236
+ }
237
+
238
+ /**
239
+ * Share the remaining pixels among items in proportion to their weights, never below what they already have.
240
+ * 残りピクセルを重みの比で各要素へ分け与える処理 (既に持っている分を下回らせない)。
241
+ *
242
+ * @param items - Items that take part in the sharing / 分け合いに参加する要素
243
+ * @param sizes - Sizes mutated in place / その場で更新されるサイズ配列
244
+ * @param available - Pixels available for sharing / 分け合いに使えるピクセル数
245
+ * @returns Pixels left after sharing / 分け合い後に残ったピクセル数
246
+ */
247
+ const shareByWeight = (items: ReadonlyArray<AllocationItem>, sizes: number[], available: number): number => {
248
+ if (items.length === 0 || available <= PANEL_ALLOCATION_EPSILON) {
249
+ return available
250
+ }
251
+ const floors = items.map((item) => ({ ...item, min: sizes[item.index] }))
252
+ const shared = waterFill(floors, available + floors.reduce((total, item) => total + item.min, 0))
253
+ let distributed = 0
254
+ for (let index = 0; index < items.length; index += 1) {
255
+ distributed += shared[index] - sizes[items[index].index]
256
+ sizes[items[index].index] = shared[index]
257
+ }
258
+ return available - distributed
259
+ }
260
+
261
+ /**
262
+ * Share the remaining pixels equally among items that declare no ratio weight.
263
+ * 取り分の重みを持たない要素へ、残りピクセルを均等に分け与える処理。
264
+ *
265
+ * @param items - Items that take part in the sharing / 分け合いに参加する要素
266
+ * @param sizes - Sizes mutated in place / その場で更新されるサイズ配列
267
+ * @param available - Pixels available for sharing / 分け合いに使えるピクセル数
268
+ * @returns Pixels left after sharing / 分け合い後に残ったピクセル数
269
+ */
270
+ const shareEvenly = (items: ReadonlyArray<AllocationItem>, sizes: number[], available: number): number =>
271
+ shareByWeight(
272
+ items.map((item) => ({ ...item, weight: 1 })),
273
+ sizes,
274
+ available,
275
+ )
276
+
277
+ /**
278
+ * Order sequential items by tier, then by descending priority, then by DOM position.
279
+ * 逐次充当の要素を 段 → 優先度降順 → DOM 順 で整列する処理。
280
+ */
281
+ const bySequentialOrder = (left: AllocationItem, right: AllocationItem): number => {
282
+ if (left.tier !== right.tier) {
283
+ return left.tier - right.tier
284
+ }
285
+ if (left.priority !== right.priority) {
286
+ return right.priority - left.priority
287
+ }
288
+ return left.index - right.index
289
+ }
290
+
291
+ /**
292
+ * Result of one allocation pass.
293
+ * 1 回の配分結果。
294
+ */
295
+ export type LayoutAllocation = {
296
+ /** Pixel size per panel, index-aligned with the input, collapsed panels at 0. パネルごとのピクセルサイズ。 */
297
+ sizes: number[]
298
+ /** Constraint conflict detected while allocating, or null. 配分中に検出した制約矛盾。 */
299
+ violation: LayoutConstraintViolation | null
300
+ }
301
+
302
+ /**
303
+ * Allocate pixel sizes to every panel of a group for a given container length.
304
+ * 指定コンテナ長に対してグループ内の全パネルへピクセルサイズを配分する処理。
305
+ *
306
+ * The allocation runs in three stages: collapsed panels take no space, panels with an absolute target
307
+ * (pixel panels first, then flexible panels that declare a priority) are filled in order, and the rest of
308
+ * the container is shared by the remaining flexible panels in proportion to their weights. Any residue
309
+ * left because every ratio panel hit its maximum is offered back to the sequential panels up to their own
310
+ * maximum, so the container is filled whenever the constraints allow it.
311
+ * 配分は 3 段階: 折りたたみパネルは領域を取らず、絶対目標を持つパネル (ピクセル → 優先度つき柔軟) を順に
312
+ * 充当し、残りを柔軟パネルが重みに比例して分け合う。全ての比例パネルが上限に達して余りが出た場合は逐次段の
313
+ * パネルへ上限まで戻すため、制約が許す限りコンテナは満たされる。
314
+ *
315
+ * @param panels - Panels in DOM order / DOM 順のパネル一覧
316
+ * @param containerSize - Container content-box length in pixels / コンテナ内容領域長 (px)
317
+ * @returns Sizes and the constraint violation detected, if any / 配分結果と検出した制約違反
318
+ */
319
+ export const allocatePanelSizes = (panels: ReadonlyArray<PanelLayoutData>, containerSize: number): LayoutAllocation => {
320
+ const sizes = panels.map(() => 0)
321
+ if (!Number.isFinite(containerSize) || containerSize <= 0 || panels.length === 0) {
322
+ return { sizes, violation: null }
323
+ }
324
+
325
+ const items: AllocationItem[] = []
326
+ for (let index = 0; index < panels.length; index += 1) {
327
+ const panel = panels[index]
328
+ if (panel.collapsed) {
329
+ continue
330
+ }
331
+ items.push(toAllocationItem(panel, index, containerSize))
332
+ }
333
+ if (items.length === 0) {
334
+ return { sizes, violation: null }
335
+ }
336
+
337
+ let totalMinimum = 0
338
+ let totalMaximum = 0
339
+ for (const item of items) {
340
+ sizes[item.index] = item.min
341
+ totalMinimum += item.min
342
+ totalMaximum += item.max
343
+ }
344
+
345
+ const violation = detectViolation(totalMinimum, totalMaximum, containerSize)
346
+ let remaining = containerSize - totalMinimum
347
+ if (remaining <= PANEL_ALLOCATION_EPSILON) {
348
+ // 最小サイズの合計がコンテナを超える不能構成。下限を保ったまま返し、通知は violation が担う
349
+ return { sizes, violation }
350
+ }
351
+
352
+ const sequential = items.filter((item) => item.target !== null).sort(bySequentialOrder)
353
+ const ratioFlexible = items.filter((item) => item.target === null && item.weight > 0)
354
+ // 受け皿の資格は「柔軟であり、重みでは取り分を得られないこと」。優先度の宣言は関係しない
355
+ // (優先度つきパネルを端まで引くと重みが 0 になり、ここで外すと余りの引き取り手が消える)
356
+ const pinnedFlexible = items.filter((item) => item.tier !== PIXEL_TIER && item.weight <= 0)
357
+ const prioritizedFlexible = sequential.filter((item) => item.tier === PRIORITY_FLEXIBLE_TIER)
358
+ const pixelItems = sequential.filter((item) => item.tier === PIXEL_TIER)
359
+
360
+ // 配分の順序は「誰の希望を先に満たすか」ではなく「余りを誰が引き取るか」の全順序でもある。
361
+ // 最後の段まで用意するのは、引き取り手が尽きてコンテナに隙間が残る状態を作らないため
362
+ // (隙間が残れば Σ サイズ = コンテナ長 の不変条件が破れ、ハンドルと境界が食い違う)
363
+
364
+ // 1. 領域が不足する場合に誰から満たすかを決める段。ピクセルパネル → 優先度つき柔軟パネルの順
365
+ remaining = fillInOrder(sequential, sizes, (item) => item.target ?? item.min, remaining)
366
+
367
+ // 2. 残りを比例配分の柔軟パネルが重みの比で分け合う
368
+ remaining = shareByWeight(ratioFlexible, sizes, remaining)
369
+
370
+ // 3. 比例配分側が上限に達してなお余る場合、優先度つき柔軟パネルが希望を超えて重みの比で引き取る
371
+ remaining = shareByWeight(prioritizedFlexible, sizes, remaining)
372
+
373
+ // 4. 柔軟パネルが 1 枚も無い構成では、ピクセルパネルが優先度順に上限まで引き取る
374
+ remaining = fillInOrder(pixelItems, sizes, (item) => item.max, remaining)
375
+
376
+ // 5. それでも余る場合の最後の受け皿は、取り分を望まない (重み 0 の) 柔軟パネル。
377
+ // ここまで来るのは他の全パネルが上限に達したときだけなので、ゼロへ縮めたパネルが
378
+ // 通常のリサイズで復活することはない。
379
+ // この段が無いと Σ サイズ = コンテナ長 が破れ、しかも Σ 上限 >= コンテナ長 なので違反としても報告されない
380
+ remaining = shareEvenly(pinnedFlexible, sizes, remaining)
381
+
382
+ return { sizes, violation }
383
+ }
384
+
385
+ /**
386
+ * Detect whether the panels' constraints can be satisfied by the container.
387
+ * パネルの制約をコンテナが満たせるかどうかを判定する処理。
388
+ *
389
+ * @param totalMinimum - Sum of the active panels' minimum sizes / アクティブパネルの最小サイズ合計
390
+ * @param totalMaximum - Sum of the active panels' maximum sizes / アクティブパネルの最大サイズ合計
391
+ * @param containerSize - Container content-box length in pixels / コンテナ内容領域長 (px)
392
+ * @returns The violation to report, or null when the layout is feasible / 報告すべき違反、可能なら null
393
+ */
394
+ const detectViolation = (totalMinimum: number, totalMaximum: number, containerSize: number): LayoutConstraintViolation | null => {
395
+ if (totalMinimum - containerSize > PANEL_ALLOCATION_EPSILON) {
396
+ return {
397
+ reason: "minimum-exceeded",
398
+ totalMinimumSize: totalMinimum,
399
+ totalMaximumSize: totalMaximum,
400
+ availableContainerSize: containerSize,
401
+ }
402
+ }
403
+ if (containerSize - totalMaximum > PANEL_ALLOCATION_EPSILON) {
404
+ return {
405
+ reason: "maximum-insufficient",
406
+ totalMinimumSize: totalMinimum,
407
+ totalMaximumSize: totalMaximum,
408
+ availableContainerSize: containerSize,
409
+ }
410
+ }
411
+ return null
412
+ }
@@ -6,6 +6,15 @@
6
6
  import { useSyncExternalStore } from "react"
7
7
  import type { ContainerSize, PanelDirection, PanelLayoutData, ResizeHandleLayoutData } from "./types"
8
8
 
9
+ /**
10
+ * Panel layout data enriched with the DOM measurement the overlay compares it against.
11
+ * オーバーレイが突き合わせる DOM 実測値を添えたパネルレイアウトデータ。
12
+ */
13
+ export type DebugPanelSnapshot = PanelLayoutData & {
14
+ measuredPixelSize?: number
15
+ measuredPercentageSize?: number
16
+ }
17
+
9
18
  /**
10
19
  * Snapshot of the debug overlay state across panel groups.
11
20
  * デバッグオーバーレイのスナップショット状態を表す構造体。
@@ -24,7 +33,7 @@ export type DebugGroupState = {
24
33
  displayName: string
25
34
  direction: PanelDirection
26
35
  containerSize: ContainerSize
27
- panels: PanelLayoutData[]
36
+ panels: DebugPanelSnapshot[]
28
37
  handles: ResizeHandleLayoutData[]
29
38
  index: number
30
39
  }
@@ -33,7 +42,7 @@ type DebugGroupUpdate = {
33
42
  displayName: string
34
43
  direction: PanelDirection
35
44
  containerSize: ContainerSize
36
- panels: PanelLayoutData[]
45
+ panels: DebugPanelSnapshot[]
37
46
  handles: ResizeHandleLayoutData[]
38
47
  }
39
48
 
@@ -44,27 +53,21 @@ type DebugGroupEntry = DebugGroupState & {
44
53
  serializedHandles: string
45
54
  }
46
55
 
47
- const serializePanels = (panels: PanelLayoutData[]) => {
56
+ const serializePanels = (panels: DebugPanelSnapshot[]) => {
48
57
  return JSON.stringify(
49
58
  panels.map((panel) => ({
50
59
  id: panel.id,
51
60
  size: panel.size,
52
- percentageSize: panel.percentageSize ?? null,
53
- preferredPercentageSize: panel.preferredPercentageSize ?? null,
54
- preferredPixelSize: panel.preferredPixelSize ?? null,
55
61
  sizeUnit: panel.sizeUnit,
56
- originalPixelSize: panel.originalPixelSize ?? null,
62
+ preferredPercentageSize: panel.preferredPercentageSize,
63
+ preferredPixelSize: panel.preferredPixelSize,
57
64
  minSize: panel.minSize ?? null,
58
65
  maxSize: panel.maxSize ?? null,
59
- autoMinSize: panel.autoMinSize ?? null,
60
66
  collapseFromStart: panel.collapseFromStart,
61
67
  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
+ collapsed: panel.collapsed,
69
+ collapsedByDirection: panel.collapsedByDirection,
70
+ preferenceBeforeCollapse: panel.preferenceBeforeCollapse,
68
71
  pixelAdjustPriority: panel.pixelAdjustPriority ?? null,
69
72
  flexAdjustPriority: panel.flexAdjustPriority ?? null,
70
73
  measuredPixelSize: panel.measuredPixelSize ?? null,
@@ -203,7 +206,8 @@ const createDebugOverlayStore = () => {
203
206
  if (
204
207
  prevPanel.id !== nextPanel.id ||
205
208
  prevPanel.size !== nextPanel.size ||
206
- prevPanel.percentageSize !== nextPanel.percentageSize ||
209
+ prevPanel.preferredPercentageSize !== nextPanel.preferredPercentageSize ||
210
+ prevPanel.preferredPixelSize !== nextPanel.preferredPixelSize ||
207
211
  prevPanel.collapsed !== nextPanel.collapsed ||
208
212
  prevPanel.measuredPixelSize !== nextPanel.measuredPixelSize ||
209
213
  prevPanel.measuredPercentageSize !== nextPanel.measuredPercentageSize