@aiquants/resize-panels 1.8.0 → 1.9.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 +91 -93
- package/dist/{GlobalDebugOverlay-DYCR0FIu.js → GlobalDebugOverlay-Do70T2l6.js} +1 -1
- package/dist/{GlobalDebugOverlay-afs0vF-Q.cjs → GlobalDebugOverlay-sEQN6exo.cjs} +1 -1
- package/dist/PanelResizeHandle.d.ts +1 -1
- package/dist/PanelResizeHandle.d.ts.map +1 -1
- package/dist/index-B6YGX2DH.js +2009 -0
- package/dist/index-DssZUxGw.cjs +2 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/styles/resize-panels.standalone.css +1 -1
- package/dist/types.d.ts +126 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/PanelResizeHandle.tsx +169 -90
- package/src/index.ts +25 -3
- package/src/styles/components.entry.css +3 -1
- package/src/styles/resize-panels.css +1 -3
- package/src/types.ts +133 -2
- package/dist/index-BzgId8aQ.js +0 -1972
- package/dist/index-xTV-c0PN.cjs +0 -2
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { type CSSProperties, memo, useCallback, useEffect, useId, useMemo, useRef, useState } from "react"
|
|
7
7
|
import { twMerge } from "tailwind-merge"
|
|
8
8
|
import { usePanelGroup } from "./context"
|
|
9
|
+
import { roundHalfToEven } from "./roundHalfToEven"
|
|
9
10
|
import {
|
|
10
11
|
PANEL_COLLAPSE_THRESHOLD_MAX,
|
|
11
12
|
PANEL_COLLAPSE_THRESHOLD_RATIO,
|
|
@@ -14,6 +15,12 @@ import {
|
|
|
14
15
|
PANEL_HANDLE_DRAG_THROTTLE_MS,
|
|
15
16
|
PANEL_HANDLE_THICKNESS,
|
|
16
17
|
PANEL_HANDLE_VISIBILITY_THRESHOLD,
|
|
18
|
+
PANEL_INDICATOR_BAR_LENGTH_HORIZONTAL,
|
|
19
|
+
PANEL_INDICATOR_BAR_LENGTH_VERTICAL,
|
|
20
|
+
PANEL_INDICATOR_BAR_THICKNESS,
|
|
21
|
+
PANEL_INDICATOR_LENGTH_HORIZONTAL,
|
|
22
|
+
PANEL_INDICATOR_LENGTH_VERTICAL,
|
|
23
|
+
PANEL_INDICATOR_THICKNESS,
|
|
17
24
|
PANEL_KEYBOARD_RESIZE_STEP,
|
|
18
25
|
PANEL_VISIBLE_INDICATOR_SPACING,
|
|
19
26
|
type PanelResizeHandleProps,
|
|
@@ -21,11 +28,50 @@ import {
|
|
|
21
28
|
} from "./types"
|
|
22
29
|
import { calculateSnapThreshold, getConstraintInPixels, getContainerSize } from "./utils"
|
|
23
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Common styles applied to hide the handle and its interactive area entirely.
|
|
33
|
+
* 非表示時のハンドルおよび操作領域全体に適用する共通スタイル定数。
|
|
34
|
+
*/
|
|
35
|
+
const HIDDEN_HANDLE_STYLE: CSSProperties = {
|
|
36
|
+
display: "none",
|
|
37
|
+
pointerEvents: "none",
|
|
38
|
+
visibility: "hidden",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Common styles applied to collapse the visual indicator.
|
|
43
|
+
* 視覚インジケーターを非表示にする共通スタイル定数。
|
|
44
|
+
*/
|
|
45
|
+
const HIDDEN_INDICATOR_STYLE: CSSProperties = {
|
|
46
|
+
display: "none",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Sanitize an optional dimension to ensure a positive finite number.
|
|
51
|
+
* オプション寸法を正の有限数値へ正規化する。
|
|
52
|
+
*/
|
|
53
|
+
const sanitizeDimension = (value: unknown, defaultValue: number): number => {
|
|
54
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
55
|
+
return defaultValue
|
|
56
|
+
}
|
|
57
|
+
return value
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Ensure a numeric spacing value is an even integer for symmetric pixel alignment.
|
|
62
|
+
* ピクセル配置の対称性を担保するため数値を偶数整数に整流する。
|
|
63
|
+
*/
|
|
64
|
+
const ensureEven = (value: number): number => {
|
|
65
|
+
const rounded = Math.round(value)
|
|
66
|
+
return rounded % 2 === 0 ? rounded : rounded + 1
|
|
67
|
+
}
|
|
68
|
+
|
|
24
69
|
/**
|
|
25
70
|
* Render an interactive divider that manages adjacent panel resizing gestures.
|
|
26
71
|
* 隣接パネルのリサイズ操作を管理するインタラクティブなディバイダーを描画するコンポーネント。
|
|
27
72
|
*/
|
|
28
|
-
export const PanelResizeHandle = memo((
|
|
73
|
+
export const PanelResizeHandle = memo((props: PanelResizeHandleProps) => {
|
|
74
|
+
const { id, disabled = false, className, style, children, onDragging, thickness, indicator = true, indicatorThickness, indicatorLength, indicatorBarThickness, indicatorBarLength, title } = props
|
|
29
75
|
const { direction, resizePanels, getPanel, collapsePanel, expandPanel, panels, reportHandleMeasurement, containerSize, layoutConstraintViolation } = usePanelGroup()
|
|
30
76
|
const [isDragging, setIsDragging] = useState(false)
|
|
31
77
|
const handleRef = useRef<HTMLButtonElement>(null)
|
|
@@ -66,7 +112,6 @@ export const PanelResizeHandle = memo(({ id, disabled = false, className, style,
|
|
|
66
112
|
let leftPanelElement: HTMLElement | null = null
|
|
67
113
|
let rightPanelElement: HTMLElement | null = null
|
|
68
114
|
|
|
69
|
-
// ハンドルより前方の要素を逆順に走査して左パネルを特定
|
|
70
115
|
for (let i = index - 1; i >= 0; i -= 1) {
|
|
71
116
|
const element = childrenElements[i] as HTMLElement
|
|
72
117
|
if (element.dataset.panelId) {
|
|
@@ -75,7 +120,6 @@ export const PanelResizeHandle = memo(({ id, disabled = false, className, style,
|
|
|
75
120
|
}
|
|
76
121
|
}
|
|
77
122
|
|
|
78
|
-
// ハンドルより後方の要素を走査して右パネルを特定
|
|
79
123
|
for (let i = index + 1; i < childrenElements.length; i += 1) {
|
|
80
124
|
const element = childrenElements[i] as HTMLElement
|
|
81
125
|
if (element.dataset.panelId) {
|
|
@@ -87,7 +131,6 @@ export const PanelResizeHandle = memo(({ id, disabled = false, className, style,
|
|
|
87
131
|
const nextLeftId = leftPanelElement?.dataset.panelId || ""
|
|
88
132
|
const nextRightId = rightPanelElement?.dataset.panelId || ""
|
|
89
133
|
|
|
90
|
-
// 検出結果が前回と同じなら state 更新を省略する
|
|
91
134
|
if (nextLeftId === leftPanelId.current && nextRightId === rightPanelId.current) {
|
|
92
135
|
return
|
|
93
136
|
}
|
|
@@ -194,15 +237,12 @@ export const PanelResizeHandle = memo(({ id, disabled = false, className, style,
|
|
|
194
237
|
moveEvent.preventDefault()
|
|
195
238
|
moveEvent.stopPropagation()
|
|
196
239
|
|
|
197
|
-
// スロットル処理: 頻繁な更新を抑制して計算負荷を下げる
|
|
198
240
|
const now = performance.now()
|
|
199
241
|
if (now - lastUpdateTime < throttleMs) return
|
|
200
242
|
lastUpdateTime = now
|
|
201
243
|
|
|
202
|
-
// ドラッグ開始位置からのピクセル変位を計算 (RTL の水平ドラッグは符号を反転)
|
|
203
244
|
const rawPixelDelta = direction === "horizontal" ? moveEvent.clientX - startPosition.x : moveEvent.clientY - startPosition.y
|
|
204
245
|
const pixelDelta = isRtlDrag ? -rawPixelDelta : rawPixelDelta
|
|
205
|
-
// 差分がゼロのときは状態変化がなく後続処理が無駄になるため中断
|
|
206
246
|
if (pixelDelta === 0) {
|
|
207
247
|
return
|
|
208
248
|
}
|
|
@@ -210,23 +250,18 @@ export const PanelResizeHandle = memo(({ id, disabled = false, className, style,
|
|
|
210
250
|
const newLeftSizeInPixels = initialLeftSize + pixelDelta
|
|
211
251
|
const newRightSizeInPixels = initialRightSize - pixelDelta
|
|
212
252
|
|
|
213
|
-
// 最新のパネル状態を取得して折りたたみ状態などの変化を反映
|
|
214
253
|
const updatedLeftPanel = getPanel(leftPanelId.current)
|
|
215
254
|
const updatedRightPanel = getPanel(rightPanelId.current)
|
|
216
255
|
|
|
217
256
|
if (updatedLeftPanel && updatedRightPanel && leftPanelId.current && rightPanelId.current) {
|
|
218
257
|
const leftMinSizePixels = getConstraintInPixels(updatedLeftPanel.minSize, 0, containerSize)
|
|
219
258
|
const rightMinSizePixels = getConstraintInPixels(updatedRightPanel.minSize, 0, containerSize)
|
|
220
|
-
const leftMinConstraintPixels = leftMinSizePixels
|
|
221
|
-
const rightMinConstraintPixels = rightMinSizePixels
|
|
222
259
|
|
|
223
|
-
// 折りたたみ・展開の閾値をコンテナサイズから算出
|
|
224
260
|
const collapseThresholdPixels = Math.min(containerSize * PANEL_COLLAPSE_THRESHOLD_RATIO, PANEL_COLLAPSE_THRESHOLD_MAX)
|
|
225
261
|
const expandThresholdPixels = Math.min(containerSize * PANEL_EXPAND_THRESHOLD_RATIO, PANEL_EXPAND_THRESHOLD_MAX)
|
|
226
262
|
|
|
227
263
|
const rightCanCollapseFromStart = updatedRightPanel.collapseFromStart
|
|
228
264
|
const rightCollapseDirection = updatedRightPanel.collapsedByDirection ?? "start"
|
|
229
|
-
// 右パネルが折りたたみ可能で、新しいサイズが閾値を下回る場合は折りたたむ
|
|
230
265
|
if (rightCanCollapseFromStart && !updatedRightPanel.collapsed && newRightSizeInPixels <= Math.max(rightMinSizePixels * 0.3, collapseThresholdPixels)) {
|
|
231
266
|
collapsePanel(rightPanelId.current, "start")
|
|
232
267
|
return
|
|
@@ -234,46 +269,39 @@ export const PanelResizeHandle = memo(({ id, disabled = false, className, style,
|
|
|
234
269
|
|
|
235
270
|
const leftCanCollapseFromEnd = updatedLeftPanel.collapseFromEnd
|
|
236
271
|
const leftCollapseDirection = updatedLeftPanel.collapsedByDirection ?? "end"
|
|
237
|
-
// 左パネルが折りたたみ可能で、新しいサイズが閾値を下回る場合は折りたたむ
|
|
238
272
|
if (leftCanCollapseFromEnd && !updatedLeftPanel.collapsed && newLeftSizeInPixels <= Math.max(leftMinSizePixels * 0.3, collapseThresholdPixels)) {
|
|
239
273
|
collapsePanel(leftPanelId.current, "end")
|
|
240
274
|
return
|
|
241
275
|
}
|
|
242
276
|
|
|
243
|
-
// 右パネルが折りたたまれており、新しいサイズが展開閾値を超えた場合は展開
|
|
244
277
|
if (updatedRightPanel.collapsed && newRightSizeInPixels > Math.max(rightMinSizePixels * 0.3, expandThresholdPixels)) {
|
|
245
278
|
expandPanel(rightPanelId.current, rightCollapseDirection, newRightSizeInPixels)
|
|
246
279
|
return
|
|
247
280
|
}
|
|
248
281
|
|
|
249
|
-
// 左パネルが折りたたまれており、新しいサイズが展開閾値を超えた場合は展開
|
|
250
282
|
if (updatedLeftPanel.collapsed && newLeftSizeInPixels > Math.max(leftMinSizePixels * 0.3, expandThresholdPixels)) {
|
|
251
283
|
expandPanel(leftPanelId.current, leftCollapseDirection, newLeftSizeInPixels)
|
|
252
284
|
return
|
|
253
285
|
}
|
|
254
286
|
|
|
255
|
-
// スナップ処理: 最小サイズ以下になった場合はゼロサイズにスナップさせる
|
|
256
287
|
if (containerSize > 0) {
|
|
257
288
|
const totalPanelPixels = updatedLeftPanel.size + updatedRightPanel.size
|
|
258
|
-
// コンテナ比率と定数に基づきゼロスナップの閾値を算出
|
|
259
289
|
const snapThreshold = calculateSnapThreshold(containerSize)
|
|
260
290
|
let snapped = false
|
|
261
291
|
|
|
262
|
-
|
|
263
|
-
if (leftMinConstraintPixels <= snapThreshold && newLeftSizeInPixels <= snapThreshold && initialLeftSize !== 0) {
|
|
292
|
+
if (leftMinSizePixels <= snapThreshold && newLeftSizeInPixels <= snapThreshold && initialLeftSize !== 0) {
|
|
264
293
|
const snappedLeft = 0
|
|
265
294
|
const snappedRight = Math.max(0, totalPanelPixels - snappedLeft)
|
|
266
295
|
|
|
267
|
-
if (snappedRight >=
|
|
296
|
+
if (snappedRight >= rightMinSizePixels) {
|
|
268
297
|
resizePanels(leftPanelId.current, rightPanelId.current, snappedLeft, snappedRight)
|
|
269
298
|
snapped = true
|
|
270
299
|
}
|
|
271
|
-
} else if (
|
|
272
|
-
// 右パネルがスナップ可能で、新しいサイズがスナップ閾値以下の場合はゼロへスナップ
|
|
300
|
+
} else if (rightMinSizePixels <= snapThreshold && newRightSizeInPixels <= snapThreshold && initialRightSize !== 0) {
|
|
273
301
|
const snappedRight = 0
|
|
274
302
|
const snappedLeft = Math.max(0, totalPanelPixels - snappedRight)
|
|
275
303
|
|
|
276
|
-
if (snappedLeft >=
|
|
304
|
+
if (snappedLeft >= leftMinSizePixels) {
|
|
277
305
|
resizePanels(leftPanelId.current, rightPanelId.current, snappedLeft, snappedRight)
|
|
278
306
|
snapped = true
|
|
279
307
|
}
|
|
@@ -284,12 +312,10 @@ export const PanelResizeHandle = memo(({ id, disabled = false, className, style,
|
|
|
284
312
|
}
|
|
285
313
|
}
|
|
286
314
|
|
|
287
|
-
// いずれかのパネルが折りたたまれている場合はリサイズ処理を中断
|
|
288
315
|
if (updatedLeftPanel.collapsed || updatedRightPanel.collapsed) {
|
|
289
316
|
return
|
|
290
317
|
}
|
|
291
318
|
|
|
292
|
-
// 通常のリサイズ処理を実行して隣接パネルのサイズを更新
|
|
293
319
|
resizePanels(leftPanelId.current, rightPanelId.current, newLeftSizeInPixels, newRightSizeInPixels)
|
|
294
320
|
}
|
|
295
321
|
}
|
|
@@ -381,20 +407,23 @@ export const PanelResizeHandle = memo(({ id, disabled = false, className, style,
|
|
|
381
407
|
const isHorizontal = direction === "horizontal"
|
|
382
408
|
const cursor = isHorizontal ? "cursor-col-resize" : "cursor-row-resize"
|
|
383
409
|
|
|
384
|
-
const baseHandleClasses =
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
410
|
+
const baseHandleClasses = useMemo(
|
|
411
|
+
() =>
|
|
412
|
+
twMerge(
|
|
413
|
+
"group absolute z-20 flex select-none items-center justify-center border-none bg-transparent transition-colors duration-150 overflow-visible",
|
|
414
|
+
cursor,
|
|
415
|
+
isHorizontal ? "h-full" : "w-full",
|
|
416
|
+
!isHandleDisabled && "hover:bg-gray-200/70 dark:hover:bg-gray-700/70",
|
|
417
|
+
!isHandleDisabled && "active:bg-blue-300/60 dark:active:bg-blue-700/60",
|
|
418
|
+
isDragging && "bg-blue-200/80 dark:bg-blue-800/60",
|
|
419
|
+
isHandleDisabled && "cursor-not-allowed opacity-50",
|
|
420
|
+
"focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50",
|
|
421
|
+
className,
|
|
422
|
+
),
|
|
423
|
+
[cursor, isHorizontal, isHandleDisabled, isDragging, className],
|
|
394
424
|
)
|
|
395
425
|
|
|
396
426
|
const adjacentIndices = useMemo(() => {
|
|
397
|
-
// パネル ID からパネル配列内のインデックスを探索するヘルパー関数
|
|
398
427
|
const findIndex = (panelId: string | null) => {
|
|
399
428
|
if (!panelId) {
|
|
400
429
|
return -1
|
|
@@ -416,7 +445,6 @@ export const PanelResizeHandle = memo(({ id, disabled = false, className, style,
|
|
|
416
445
|
const containerLength = isHorizontal ? containerSize.width : containerSize.height
|
|
417
446
|
|
|
418
447
|
const boundaryOffset = useMemo(() => {
|
|
419
|
-
// 指定範囲のパネルサイズ合計を安全に算出するヘルパー
|
|
420
448
|
const safeSum = (startIndex: number, endIndexInclusive: number) => {
|
|
421
449
|
if (startIndex > endIndexInclusive) {
|
|
422
450
|
return 0
|
|
@@ -446,7 +474,6 @@ export const PanelResizeHandle = memo(({ id, disabled = false, className, style,
|
|
|
446
474
|
}, [adjacentIndices.left, adjacentIndices.right, panels])
|
|
447
475
|
|
|
448
476
|
const clampedOffset = useMemo(() => {
|
|
449
|
-
// 境界位置が無効値やコンテナ範囲外に逸脱した場合に補正
|
|
450
477
|
if (!Number.isFinite(boundaryOffset) || boundaryOffset < 0) {
|
|
451
478
|
return 0
|
|
452
479
|
}
|
|
@@ -456,34 +483,68 @@ export const PanelResizeHandle = memo(({ id, disabled = false, className, style,
|
|
|
456
483
|
return boundaryOffset
|
|
457
484
|
}, [boundaryOffset, containerLength])
|
|
458
485
|
|
|
486
|
+
const indicatorConfig = typeof indicator === "object" && indicator !== null ? indicator : undefined
|
|
487
|
+
const isIndicatorExplicitlyHidden = indicator === false || indicatorConfig?.visible === false
|
|
488
|
+
|
|
489
|
+
const rawThickness = sanitizeDimension(thickness, PANEL_HANDLE_THICKNESS)
|
|
490
|
+
const defaultIndicatorLength = isHorizontal ? PANEL_INDICATOR_LENGTH_HORIZONTAL : PANEL_INDICATOR_LENGTH_VERTICAL
|
|
491
|
+
const defaultBarLength = isHorizontal ? PANEL_INDICATOR_BAR_LENGTH_HORIZONTAL : PANEL_INDICATOR_BAR_LENGTH_VERTICAL
|
|
492
|
+
|
|
493
|
+
const rawIndicatorThickness = sanitizeDimension(indicatorThickness ?? indicatorConfig?.thickness, PANEL_INDICATOR_THICKNESS)
|
|
494
|
+
const rawIndicatorLength = sanitizeDimension(indicatorLength ?? indicatorConfig?.length, defaultIndicatorLength)
|
|
495
|
+
const rawBarThickness = sanitizeDimension(indicatorBarThickness ?? indicatorConfig?.barThickness, PANEL_INDICATOR_BAR_THICKNESS)
|
|
496
|
+
const rawBarLength = sanitizeDimension(indicatorBarLength ?? indicatorConfig?.barLength, defaultBarLength)
|
|
497
|
+
|
|
498
|
+
// 幾何学的内包不変条件: 内部バー寸法がインジケーター外枠およびハンドル操作領域を超えないようクランプ
|
|
499
|
+
const resolvedThickness = Math.max(1, rawThickness)
|
|
500
|
+
const resolvedIndicatorThickness = Math.min(resolvedThickness, Math.max(1, rawIndicatorThickness))
|
|
501
|
+
const resolvedIndicatorLength = Math.max(1, rawIndicatorLength)
|
|
502
|
+
const resolvedBarThickness = Math.min(resolvedIndicatorThickness, Math.max(1, rawBarThickness))
|
|
503
|
+
const resolvedBarLength = Math.min(resolvedIndicatorLength, Math.max(1, rawBarLength))
|
|
504
|
+
|
|
505
|
+
// 静的 CSS 変数はドラッグ中に変化しないため、スタイル再計算時の不要なプロパティ再設定を防止
|
|
506
|
+
const customProperties = useMemo<Record<string, string>>(
|
|
507
|
+
() => ({
|
|
508
|
+
"--rp-handle-thickness": `${resolvedThickness}px`,
|
|
509
|
+
"--rp-indicator-thickness": `${resolvedIndicatorThickness}px`,
|
|
510
|
+
"--rp-indicator-length": `${resolvedIndicatorLength}px`,
|
|
511
|
+
"--rp-indicator-bar-thickness": `${resolvedBarThickness}px`,
|
|
512
|
+
"--rp-indicator-bar-length": `${resolvedBarLength}px`,
|
|
513
|
+
}),
|
|
514
|
+
[resolvedThickness, resolvedIndicatorThickness, resolvedIndicatorLength, resolvedBarThickness, resolvedBarLength],
|
|
515
|
+
)
|
|
516
|
+
|
|
459
517
|
const overlayBaseStyle = useMemo<CSSProperties>(() => {
|
|
518
|
+
// RTL では右端起点から論理距離を差し引いた位置が物理境界中心線となる
|
|
519
|
+
const centerLine = isHorizontal && isRtl && containerLength > 0 ? containerLength - clampedOffset : clampedOffset
|
|
520
|
+
|
|
521
|
+
// transform: translate(-50%) による 0.5px サブピクセル滲みを防ぐため、中心線から半厚みを引き銀行丸めで整数ピクセル原点を決定
|
|
522
|
+
const startCoord = roundHalfToEven(centerLine - resolvedThickness / 2, 0)
|
|
523
|
+
|
|
460
524
|
const base: CSSProperties = {
|
|
461
525
|
position: "absolute",
|
|
462
526
|
zIndex: 20,
|
|
463
527
|
touchAction: "none",
|
|
528
|
+
...customProperties,
|
|
464
529
|
}
|
|
465
530
|
|
|
466
531
|
if (isHorizontal) {
|
|
467
532
|
base.top = 0
|
|
468
|
-
|
|
469
|
-
base.
|
|
470
|
-
base.transform = "translateX(-50%)"
|
|
471
|
-
base.width = `${PANEL_HANDLE_THICKNESS}px`
|
|
533
|
+
base.left = `${startCoord}px`
|
|
534
|
+
base.width = `${resolvedThickness}px`
|
|
472
535
|
base.height = "100%"
|
|
473
536
|
} else {
|
|
474
537
|
base.left = 0
|
|
475
|
-
base.top = `${
|
|
476
|
-
base.
|
|
477
|
-
base.height = `${PANEL_HANDLE_THICKNESS}px`
|
|
538
|
+
base.top = `${startCoord}px`
|
|
539
|
+
base.height = `${resolvedThickness}px`
|
|
478
540
|
base.width = "100%"
|
|
479
541
|
}
|
|
480
542
|
|
|
481
543
|
return base
|
|
482
|
-
}, [clampedOffset, isHorizontal, isRtl, containerLength])
|
|
544
|
+
}, [clampedOffset, isHorizontal, isRtl, containerLength, resolvedThickness, customProperties])
|
|
483
545
|
|
|
484
546
|
const indicatorGrouping = useMemo(() => {
|
|
485
547
|
const snapThreshold = calculateSnapThreshold(containerLength)
|
|
486
|
-
// パネルが実質的に折りたたまれているか判定するヘルパー関数
|
|
487
548
|
const isEffectivelyCollapsed = (panelIndex: number) => {
|
|
488
549
|
if (panelIndex < 0 || panelIndex >= panels.length) {
|
|
489
550
|
return false
|
|
@@ -498,7 +559,6 @@ export const PanelResizeHandle = memo(({ id, disabled = false, className, style,
|
|
|
498
559
|
return target.size <= snapThreshold
|
|
499
560
|
}
|
|
500
561
|
|
|
501
|
-
// 指定インデックスから連続する折りたたみパネルの数を数える
|
|
502
562
|
const countCollapsed = (startIndex: number, step: 1 | -1) => {
|
|
503
563
|
if (!isEffectivelyCollapsed(startIndex)) {
|
|
504
564
|
return 0
|
|
@@ -531,60 +591,86 @@ export const PanelResizeHandle = memo(({ id, disabled = false, className, style,
|
|
|
531
591
|
if (hasLayoutConstraintViolation) {
|
|
532
592
|
return true
|
|
533
593
|
}
|
|
534
|
-
// グループサイズが 1 以下の場合は単独ハンドルなので非表示にしない
|
|
535
594
|
if (indicatorGrouping.groupSize <= 1) {
|
|
536
595
|
return false
|
|
537
596
|
}
|
|
538
|
-
//
|
|
597
|
+
// 折りたたみ重なり時の視覚重複を防ぐため端点以外の重複ハンドルを不可視化
|
|
539
598
|
const lastIndex = indicatorGrouping.groupSize - 1
|
|
540
599
|
return indicatorGrouping.overlapIndex > 0 && indicatorGrouping.overlapIndex < lastIndex
|
|
541
600
|
}, [indicatorGrouping, hasLayoutConstraintViolation])
|
|
542
601
|
|
|
543
602
|
const indicatorOffset = useMemo(() => {
|
|
544
|
-
// ハンドルが非表示の場合またはグループサイズが 1 以下の場合はオフセット不要
|
|
545
603
|
if (indicatorGrouping.groupSize <= 1 || shouldHideHandle) {
|
|
546
604
|
return 0
|
|
547
605
|
}
|
|
548
606
|
|
|
549
|
-
// 表示すべきインジケーターの個数を決定
|
|
550
607
|
const visibleCount = indicatorGrouping.groupSize > 1 ? 2 : 1
|
|
551
608
|
if (visibleCount === 1) {
|
|
552
609
|
return 0
|
|
553
610
|
}
|
|
554
611
|
|
|
555
|
-
//
|
|
612
|
+
// 半ピクセル除算による非対称性を防ぐため偶数スペーシングを保証
|
|
613
|
+
const dynamicSpacing = ensureEven(Math.max(PANEL_VISIBLE_INDICATOR_SPACING, resolvedIndicatorThickness + 4))
|
|
556
614
|
const visibleIndex = indicatorGrouping.overlapIndex === 0 ? 0 : visibleCount - 1
|
|
557
615
|
const centerIndex = (visibleCount - 1) / 2
|
|
558
|
-
|
|
559
|
-
}, [indicatorGrouping, shouldHideHandle])
|
|
616
|
+
const rawOffset = (visibleIndex - centerIndex) * dynamicSpacing
|
|
560
617
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
618
|
+
// 水平かつ RTL の環境では start パネルが物理右側にあるため論理順の正シフトを物理左方向(負)へ反転
|
|
619
|
+
return isHorizontal && isRtl ? -rawOffset : rawOffset
|
|
620
|
+
}, [indicatorGrouping, shouldHideHandle, resolvedIndicatorThickness, isHorizontal, isRtl])
|
|
621
|
+
|
|
622
|
+
const indicatorWrapperStyle = useMemo<CSSProperties>(() => {
|
|
623
|
+
if (shouldHideHandle || isIndicatorExplicitlyHidden) {
|
|
624
|
+
return HIDDEN_INDICATOR_STYLE
|
|
565
625
|
}
|
|
566
626
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
627
|
+
const width = isHorizontal ? resolvedIndicatorThickness : resolvedIndicatorLength
|
|
628
|
+
const height = isHorizontal ? resolvedIndicatorLength : resolvedIndicatorThickness
|
|
629
|
+
|
|
630
|
+
const dynamicStyle: CSSProperties = {
|
|
631
|
+
width: `${width}px`,
|
|
632
|
+
height: `${height}px`,
|
|
633
|
+
flexShrink: 0,
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (indicatorOffset !== 0) {
|
|
637
|
+
dynamicStyle.transform = isHorizontal ? `translateX(${indicatorOffset}px)` : `translateY(${indicatorOffset}px)`
|
|
570
638
|
}
|
|
571
639
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
}, [indicatorOffset, isHorizontal, shouldHideHandle])
|
|
640
|
+
return dynamicStyle
|
|
641
|
+
}, [shouldHideHandle, isIndicatorExplicitlyHidden, isHorizontal, resolvedIndicatorThickness, resolvedIndicatorLength, indicatorOffset])
|
|
575
642
|
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
643
|
+
const indicatorBarStyle = useMemo<CSSProperties>(() => {
|
|
644
|
+
const width = isHorizontal ? resolvedBarThickness : resolvedBarLength
|
|
645
|
+
const height = isHorizontal ? resolvedBarLength : resolvedBarThickness
|
|
646
|
+
|
|
647
|
+
return {
|
|
648
|
+
width: `${width}px`,
|
|
649
|
+
height: `${height}px`,
|
|
650
|
+
flexShrink: 0,
|
|
651
|
+
}
|
|
652
|
+
}, [isHorizontal, resolvedBarThickness, resolvedBarLength])
|
|
653
|
+
|
|
654
|
+
const indicatorWrapperClasses = useMemo(
|
|
655
|
+
() =>
|
|
656
|
+
twMerge(
|
|
657
|
+
"pointer-events-none flex items-center justify-center rounded-full bg-white text-slate-700 ring-1 ring-inset ring-white/80 backdrop-blur-sm transition-colors duration-150 shadow-[0_0_0_1px_rgba(255,255,255,0.85),0_1px_3px_rgba(15,23,42,0.18)]",
|
|
658
|
+
"dark:bg-slate-800/95 dark:text-slate-200 dark:ring-slate-500/60 dark:shadow-[0_0_0_1px_rgba(148,163,184,0.45),0_1px_3px_rgba(2,6,23,0.55)]",
|
|
659
|
+
isDragging && "bg-blue-200/80 ring-blue-300 shadow-[0_0_0_1px_rgba(191,219,254,0.65),0_1px_4px_rgba(59,130,246,0.45)] dark:bg-blue-900/70 dark:ring-blue-400 dark:shadow-[0_0_0_1px_rgba(147,197,253,0.5),0_1px_4px_rgba(59,130,246,0.55)]",
|
|
660
|
+
indicatorConfig?.className,
|
|
661
|
+
),
|
|
662
|
+
[isDragging, indicatorConfig?.className],
|
|
581
663
|
)
|
|
582
664
|
|
|
583
|
-
const indicatorBarClasses =
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
665
|
+
const indicatorBarClasses = useMemo(
|
|
666
|
+
() =>
|
|
667
|
+
twMerge(
|
|
668
|
+
"pointer-events-none rounded-full bg-slate-600 transition-colors duration-150 shadow-[0_0_0_1px_rgba(255,255,255,0.9)] dark:bg-slate-200 dark:shadow-[0_0_0_1px_rgba(15,23,42,0.55)]",
|
|
669
|
+
isDragging && "bg-blue-500 dark:bg-blue-300",
|
|
670
|
+
!isHandleDisabled && "group-hover:bg-slate-700 dark:group-hover:bg-slate-100",
|
|
671
|
+
indicatorConfig?.barClassName,
|
|
672
|
+
),
|
|
673
|
+
[isDragging, isHandleDisabled, indicatorConfig?.barClassName],
|
|
588
674
|
)
|
|
589
675
|
|
|
590
676
|
const updateHandleMeasurement = useCallback(() => {
|
|
@@ -595,13 +681,11 @@ export const PanelResizeHandle = memo(({ id, disabled = false, className, style,
|
|
|
595
681
|
|
|
596
682
|
let thickness = 0
|
|
597
683
|
|
|
598
|
-
// ハンドルが非表示の場合は厚みをゼロとして扱う
|
|
599
684
|
if (!shouldHideHandle) {
|
|
600
685
|
const baseSize = direction === "horizontal" ? element.offsetWidth : element.offsetHeight
|
|
601
686
|
let marginStart = 0
|
|
602
687
|
let marginEnd = 0
|
|
603
688
|
|
|
604
|
-
// CSS マージンを含めた実測厚みを計算
|
|
605
689
|
if (typeof window !== "undefined") {
|
|
606
690
|
const computedStyle = window.getComputedStyle(element)
|
|
607
691
|
if (direction === "horizontal") {
|
|
@@ -626,7 +710,6 @@ export const PanelResizeHandle = memo(({ id, disabled = false, className, style,
|
|
|
626
710
|
thickness = 0
|
|
627
711
|
}
|
|
628
712
|
|
|
629
|
-
// 測定結果オブジェクトを構築
|
|
630
713
|
const measurement: ResizeHandleLayoutData = {
|
|
631
714
|
id: resolvedHandleId,
|
|
632
715
|
thickness,
|
|
@@ -638,7 +721,6 @@ export const PanelResizeHandle = memo(({ id, disabled = false, className, style,
|
|
|
638
721
|
|
|
639
722
|
const lastMeasurement = lastHandleMeasurementRef.current
|
|
640
723
|
|
|
641
|
-
// 測定値が前回と実質的に同一であれば更新をスキップ
|
|
642
724
|
if (
|
|
643
725
|
lastMeasurement &&
|
|
644
726
|
Math.abs(lastMeasurement.thickness - measurement.thickness) <= 0.25 &&
|
|
@@ -664,13 +746,11 @@ export const PanelResizeHandle = memo(({ id, disabled = false, className, style,
|
|
|
664
746
|
return
|
|
665
747
|
}
|
|
666
748
|
|
|
667
|
-
// ResizeObserver が利用できない環境では初回のみ測定
|
|
668
749
|
if (typeof ResizeObserver === "undefined") {
|
|
669
750
|
updateHandleMeasurement()
|
|
670
751
|
return
|
|
671
752
|
}
|
|
672
753
|
|
|
673
|
-
// ハンドル要素のサイズ変化を監視して測定結果を更新
|
|
674
754
|
const observer = new ResizeObserver(() => {
|
|
675
755
|
updateHandleMeasurement()
|
|
676
756
|
})
|
|
@@ -687,9 +767,7 @@ export const PanelResizeHandle = memo(({ id, disabled = false, className, style,
|
|
|
687
767
|
if (shouldHideHandle) {
|
|
688
768
|
return {
|
|
689
769
|
...merged,
|
|
690
|
-
|
|
691
|
-
pointerEvents: "none" as CSSProperties["pointerEvents"],
|
|
692
|
-
visibility: "hidden" as CSSProperties["visibility"],
|
|
770
|
+
...HIDDEN_HANDLE_STYLE,
|
|
693
771
|
}
|
|
694
772
|
}
|
|
695
773
|
return merged
|
|
@@ -707,12 +785,13 @@ export const PanelResizeHandle = memo(({ id, disabled = false, className, style,
|
|
|
707
785
|
aria-hidden={shouldHideHandle || undefined}
|
|
708
786
|
aria-label={`Resize ${isHorizontal ? "columns" : "rows"} - Drag or use arrow keys to resize, drag to the edge to collapse/expand panels`}
|
|
709
787
|
data-resize-handle-id={resolvedHandleId}
|
|
710
|
-
title={
|
|
711
|
-
{children ||
|
|
712
|
-
|
|
713
|
-
<
|
|
714
|
-
|
|
715
|
-
|
|
788
|
+
title={title ?? `Resize ${isHorizontal ? "columns" : "rows"} - Drag or use arrow keys to resize, drag to the edge to collapse/expand panels`}>
|
|
789
|
+
{children ||
|
|
790
|
+
(!isIndicatorExplicitlyHidden && (
|
|
791
|
+
<div className={indicatorWrapperClasses} style={indicatorWrapperStyle} data-resize-handle-indicator="" aria-hidden="true">
|
|
792
|
+
<span className={indicatorBarClasses} style={indicatorBarStyle} data-resize-handle-indicator-bar="" aria-hidden="true" />
|
|
793
|
+
</div>
|
|
794
|
+
))}
|
|
716
795
|
</button>
|
|
717
796
|
)
|
|
718
797
|
})
|
package/src/index.ts
CHANGED
|
@@ -17,13 +17,18 @@ export type {
|
|
|
17
17
|
ContainerAxisMeasurement,
|
|
18
18
|
ContainerSize,
|
|
19
19
|
FlexibleSize,
|
|
20
|
+
LayoutConstraintViolation,
|
|
21
|
+
LayoutConstraintViolationReason,
|
|
20
22
|
PanelCollapseDirection,
|
|
21
23
|
PanelDirection,
|
|
22
24
|
PanelGroupContextValue,
|
|
23
25
|
PanelGroupProps,
|
|
24
26
|
PanelLayoutData,
|
|
27
|
+
PanelMeasurement,
|
|
25
28
|
PanelProps,
|
|
29
|
+
PanelResizeHandleIndicatorConfig,
|
|
26
30
|
PanelResizeHandleProps,
|
|
31
|
+
ResizeHandleLayoutData,
|
|
27
32
|
SizeConfig,
|
|
28
33
|
SizeUnit,
|
|
29
34
|
} from "./types"
|
|
@@ -37,12 +42,23 @@ export {
|
|
|
37
42
|
saveLayout,
|
|
38
43
|
} from "./utils"
|
|
39
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Safely escape a selector identifier for use in CSS query selectors.
|
|
47
|
+
* セレクタ文字列を安全にエスケープする補助関数。
|
|
48
|
+
*/
|
|
49
|
+
const escapeIdentifier = (id: string): string => {
|
|
50
|
+
return typeof CSS !== "undefined" && typeof CSS.escape === "function" ? CSS.escape(id) : id.replace(/["\\]/g, "\\$&")
|
|
51
|
+
}
|
|
52
|
+
|
|
40
53
|
/**
|
|
41
54
|
* Locate a panel element in the DOM by its data attribute.
|
|
42
55
|
* data 属性を手がかりにパネル要素を DOM から取得する補助関数。
|
|
43
56
|
*/
|
|
44
57
|
export const getPanelElement = (id: string): HTMLElement | null => {
|
|
45
|
-
|
|
58
|
+
if (typeof document === "undefined") {
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
return document.querySelector(`[data-panel-id="${escapeIdentifier(id)}"]`)
|
|
46
62
|
}
|
|
47
63
|
|
|
48
64
|
/**
|
|
@@ -50,7 +66,10 @@ export const getPanelElement = (id: string): HTMLElement | null => {
|
|
|
50
66
|
* data 属性を手がかりにパネルグループ要素を DOM から取得する補助関数。
|
|
51
67
|
*/
|
|
52
68
|
export const getPanelGroupElement = (id: string): HTMLElement | null => {
|
|
53
|
-
|
|
69
|
+
if (typeof document === "undefined") {
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
return document.querySelector(`[data-panel-group-id="${escapeIdentifier(id)}"]`)
|
|
54
73
|
}
|
|
55
74
|
|
|
56
75
|
/**
|
|
@@ -58,5 +77,8 @@ export const getPanelGroupElement = (id: string): HTMLElement | null => {
|
|
|
58
77
|
* data 属性を手がかりにリサイズハンドル要素を DOM から取得する補助関数。
|
|
59
78
|
*/
|
|
60
79
|
export const getResizeHandleElement = (id: string): HTMLElement | null => {
|
|
61
|
-
|
|
80
|
+
if (typeof document === "undefined") {
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
return document.querySelector(`[data-resize-handle-id="${escapeIdentifier(id)}"]`)
|
|
62
84
|
}
|