@aiquants/drag-drop-panels 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +32 -10
  3. package/dist/drag-drop/hooks/useCustomDragHandlers.d.ts.map +1 -1
  4. package/dist/index.es.js +88 -88
  5. package/dist/index.umd.js +2 -2
  6. package/dist/styles/drag-drop-panels.standalone.css +3 -0
  7. package/package.json +11 -4
  8. package/src/DragDropLayout.tsx +751 -0
  9. package/src/drag-drop/ColumnControlPanel.tsx +123 -0
  10. package/src/drag-drop/ColumnInsertPanel.tsx +106 -0
  11. package/src/drag-drop/DebugComponents.tsx +222 -0
  12. package/src/drag-drop/DebugControlPanel.tsx +200 -0
  13. package/src/drag-drop/DebugOverlay.tsx +250 -0
  14. package/src/drag-drop/DragDropLayout.css +130 -0
  15. package/src/drag-drop/ResizeHandle.tsx +208 -0
  16. package/src/drag-drop/ResponsiveControl.tsx +33 -0
  17. package/src/drag-drop/hooks/dragGhostScale.spec.tsx +153 -0
  18. package/src/drag-drop/hooks/useColumnLayout.ts +457 -0
  19. package/src/drag-drop/hooks/useCustomDragHandlers.tsx +333 -0
  20. package/src/drag-drop/hooks/useDragDropColumns.tsx +446 -0
  21. package/src/drag-drop/hooks/useDragDropState.tsx +200 -0
  22. package/src/drag-drop/hooks/useFitScale.ts +48 -0
  23. package/src/drag-drop/hooks/useMouseTracking.tsx +102 -0
  24. package/src/drag-drop/hooks/useNormalDragHandlers.tsx +427 -0
  25. package/src/drag-drop/hooks/usePanelManagement.tsx +238 -0
  26. package/src/drag-drop/hooks/useResponsiveColumns.ts +210 -0
  27. package/src/drag-drop/resize-handle.css +71 -0
  28. package/src/drag-drop/types.ts +248 -0
  29. package/src/drag-drop/utils/columnIdUtils.ts +104 -0
  30. package/src/drag-drop/utils/edgeScrollUtils.ts +124 -0
  31. package/src/drag-drop/utils/simple-logger.ts +162 -0
  32. package/src/index.ts +60 -0
  33. package/src/styles/standalone.entry.css +27 -0
@@ -0,0 +1,250 @@
1
+ /**
2
+ * @fileoverview Defines debug overlay components for the drag-and-drop layout.
3
+ * This includes a placeholder for insertions and a debug overlay for drop zones.
4
+ *
5
+ * ドラッグ&ドロップレイアウト用のデバッグオーバーレイコンポーネントを定義します。
6
+ * これには、挿入用のプレースホルダーやドロップゾーンのデバッグオーバーレイが含まれます。
7
+ */
8
+ import React, { useEffect, useRef, useState } from "react"
9
+ import { debugLog } from "./DebugComponents"
10
+ import type { DebugConfig, DragOverPosition, OriginalPanelPosition } from "./types"
11
+
12
+ /**
13
+ * Props for the InsertPlaceholder component.
14
+ *
15
+ * InsertPlaceholder コンポーネントのプロパティ。
16
+ */
17
+ interface InsertPlaceholderProps {
18
+ /** The index where the placeholder appears in the column. / カラム内でのプレースホルダーのインデックス。 */
19
+ index: number
20
+ /** The index of the column containing the placeholder. / プレースホルダーが含まれるカラムのインデックス。 */
21
+ columnIndex: number
22
+ /** A flag indicating if a drag operation is in progress. / ドラッグ操作が進行中かどうかを示すフラグ。 */
23
+ isDragging: boolean
24
+ /** The position where the dragged panel is hovering over. / ドラッグ中のパネルがホバーしている位置。 */
25
+ dragOverPosition: DragOverPosition | null
26
+ /** The original position of the panel being dragged. / ドラッグされているパネルの元の位置。 */
27
+ originalPosition: OriginalPanelPosition | null
28
+ /** Callback when a dragged item enters the placeholder. / ドラッグアイテムがプレースホルダーに入ったときのコールバック。 */
29
+ onDragEnter: (columnIndex: number, insertIndex: number) => void
30
+ /** Callback when a dragged item leaves the placeholder. / ドラッグアイテムがプレースホルダーから離れたときのコールバック。 */
31
+ onDragLeave: (e?: React.DragEvent, columnIndex?: number, insertIndex?: number) => void
32
+ /** Debug configuration object. / デバッグ設定オブジェクト。 */
33
+ config: DebugConfig
34
+ /** State for custom (non-HTML5) drag handling. / カスタム(非HTML5)ドラッグ処理の状態。 */
35
+ customDrag?: {
36
+ isActive: boolean
37
+ panelId: string
38
+ }
39
+ }
40
+
41
+ /**
42
+ * A component that renders a placeholder to indicate where a dragged panel can be dropped.
43
+ * It highlights when a panel is dragged over it and is hidden when no drag is active.
44
+ * Features smooth fade-in/fade-out animations for a polished user experience.
45
+ *
46
+ * ドラッグされたパネルをドロップできる場所を示すプレースホルダーをレンダリングするコンポーネント。
47
+ * パネルが上にドラッグされるとハイライトされ、ドラッグがアクティブでないときは非表示になります。
48
+ * 洗練されたユーザーエクスペリエンスのために、滑らかなフェードイン・フェードアウトアニメーションを特徴とします。
49
+ */
50
+ export const InsertPlaceholder = React.memo<InsertPlaceholderProps>(({ index, columnIndex, isDragging, dragOverPosition, originalPosition, onDragEnter, onDragLeave, config, customDrag }) => {
51
+ // ドラッグ中のパネルの元の位置と同じ場所、またはそのすぐ下のプレースホルダーは無効化する
52
+ const isSameAsOriginalPosition = originalPosition && originalPosition.columnIndex === columnIndex && (originalPosition.panelIndex === index || originalPosition.panelIndex + 1 === index)
53
+
54
+ // 現在のプレースホルダーがハイライトされるべきかどうかの判定
55
+ const isHighlighted = !isSameAsOriginalPosition && dragOverPosition?.columnIndex === columnIndex && dragOverPosition?.insertIndex === index
56
+
57
+ const placeholderRef = useRef<HTMLDivElement>(null)
58
+ const [sizeInfo, setSizeInfo] = useState<{ width: number; height: number; x: number; y: number } | null>(null)
59
+
60
+ // 表示状態の制御(CSS アニメーション用)
61
+ const shouldShow = isDragging || (customDrag?.isActive ?? false)
62
+
63
+ // プレースホルダーのサイズと位置情報を計算・更新
64
+ useEffect(() => {
65
+ if (placeholderRef.current) {
66
+ const rect = placeholderRef.current.getBoundingClientRect()
67
+ const newSizeInfo = {
68
+ width: Math.round(rect.width),
69
+ height: Math.round(rect.height),
70
+ x: Math.round(rect.left),
71
+ y: Math.round(rect.top),
72
+ }
73
+
74
+ setSizeInfo(newSizeInfo)
75
+
76
+ // ハイライトされたプレースホルダーの詳細情報をログ出力
77
+ if (isHighlighted) {
78
+ debugLog(config, 1, `🎯 アクティブプレースホルダー ${index}-${columnIndex}:`, {
79
+ size: `${newSizeInfo.width}×${newSizeInfo.height}`,
80
+ position: `(${newSizeInfo.x}, ${newSizeInfo.y})`,
81
+ bottomRight: `(${newSizeInfo.x + newSizeInfo.width}, ${newSizeInfo.y + newSizeInfo.height})`,
82
+ center: `(${newSizeInfo.x + newSizeInfo.width / 2}, ${newSizeInfo.y + newSizeInfo.height / 2})`,
83
+ })
84
+ }
85
+ }
86
+ }, [isHighlighted, index, columnIndex, config])
87
+
88
+ debugLog(config, 2, `プレースホルダー${index}-${columnIndex}: 表示中`, { isHighlighted, isDragging, dragOverPosition, shouldShow })
89
+
90
+ return (
91
+ // biome-ignore lint/a11y/noStaticElementInteractions: ドラッグイベントを処理するための対話的な要素。デバッグ目的のため、キーボード操作は必須ではない。
92
+ <div
93
+ ref={placeholderRef}
94
+ data-placeholder-index={index}
95
+ data-placeholder-column={columnIndex}
96
+ onDragEnter={() => onDragEnter(columnIndex, index)}
97
+ onDragLeave={(e) => onDragLeave(e, columnIndex, index)}
98
+ className={`relative flex w-full items-center justify-center transition-all duration-0 ${
99
+ isSameAsOriginalPosition
100
+ ? "pointer-events-none h-0 overflow-hidden opacity-0"
101
+ : !shouldShow
102
+ ? "pointer-events-none h-0 overflow-hidden opacity-0"
103
+ : isHighlighted
104
+ ? "my-2 scale-[1.02] rounded-xl border-4 border-sky-500 border-dashed bg-sky-200 px-4 py-6 shadow-lg ring-2 ring-sky-400 ring-opacity-50"
105
+ : "my-2 rounded-xl border-2 border-sky-300 border-dashed bg-sky-50/70 px-2 py-4 opacity-80"
106
+ }`}>
107
+ {/* デバッグ用サイズ情報(プレースホルダー用) */}
108
+ {!isSameAsOriginalPosition && shouldShow && config.showPlaceholderInfo && sizeInfo && (
109
+ <div className="absolute top-1 right-1 z-10 rounded-md bg-blue-500/90 px-2 py-1 font-mono text-white text-xs leading-tight shadow-lg">
110
+ <div>
111
+ {sizeInfo.width}×{sizeInfo.height}
112
+ </div>
113
+ <div>
114
+ X:{sizeInfo.x} Y:{sizeInfo.y}
115
+ </div>
116
+ <div className="text-[10px] opacity-80">min:200px max:500px</div>
117
+ <div className="mt-0.5 text-[10px] opacity-80">
118
+ PH-{columnIndex}-{index} {isHighlighted ? "ACTIVE" : "IDLE"}
119
+ </div>
120
+ </div>
121
+ )}
122
+ {/* ドロップ指示のUI */}
123
+ {!isSameAsOriginalPosition && shouldShow && (
124
+ <div className="flex items-center space-x-2 text-sky-600">
125
+ <svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
126
+ <title>パネルをここにドロップ</title>
127
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
128
+ </svg>
129
+ <span className="font-medium text-sm">パネルをここにドロップ</span>
130
+ </div>
131
+ )}
132
+ </div>
133
+ )
134
+ })
135
+
136
+ InsertPlaceholder.displayName = "InsertPlaceholder"
137
+
138
+ /**
139
+ * Props for the DropZoneDebugOverlay component.
140
+ *
141
+ * DropZoneDebugOverlay コンポーネントのプロパティ。
142
+ */
143
+ interface DropZoneDebugOverlayProps {
144
+ /** The index of the column this overlay is for. / このオーバーレイが対象とするカラムのインデックス。 */
145
+ columnIndex: number
146
+ /** A flag indicating if a drag operation is in progress. / ドラッグ操作が進行中かどうかを示すフラグ。 */
147
+ isDragging: boolean
148
+ /** The position where the dragged panel is hovering over. / ドラッグ中のパネルがホバーしている位置。 */
149
+ dragOverPosition: DragOverPosition | null
150
+ /** The child elements to be rendered within the overlay. / オーバーレイ内にレンダリングされる子要素。 */
151
+ children: React.ReactNode
152
+ /** Debug configuration object. / デバッグ設定オブジェクト。 */
153
+ config: DebugConfig
154
+ }
155
+
156
+ /**
157
+ * A component that renders a debug overlay over a drop zone (a column).
158
+ * It shows the boundaries and state (active/idle) of the drop zone during a drag operation。
159
+ *
160
+ * ドロップゾーン(カラム)の上にデバッグオーバーレイをレンダリングするコンポーネント。
161
+ * ドラッグ操作中にドロップゾーンの境界と状態(アクティブ/アイドル)を表示します。
162
+ */
163
+ export const DropZoneDebugOverlay = ({ columnIndex, isDragging, dragOverPosition, children, config }: DropZoneDebugOverlayProps) => {
164
+ const debugRef = useRef<HTMLDivElement>(null)
165
+ const [zoneInfo, setZoneInfo] = useState<{
166
+ width: number
167
+ height: number
168
+ x: number
169
+ y: number
170
+ isActive: boolean
171
+ } | null>(null)
172
+
173
+ // ドロップゾーンの情報を計算・更新
174
+ useEffect(() => {
175
+ if (debugRef.current && isDragging) {
176
+ const rect = debugRef.current.getBoundingClientRect()
177
+ const isActive = dragOverPosition?.columnIndex === columnIndex
178
+
179
+ const newZoneInfo = {
180
+ width: Math.round(rect.width),
181
+ height: Math.round(rect.height),
182
+ x: Math.round(rect.left),
183
+ y: Math.round(rect.top),
184
+ isActive,
185
+ }
186
+
187
+ setZoneInfo(newZoneInfo)
188
+
189
+ // アクティブなドロップゾーンの情報をログ出力
190
+ if (isActive) {
191
+ debugLog(config, 1, `🎯 アクティブドロップゾーン ${columnIndex}:`, {
192
+ size: `${newZoneInfo.width}×${newZoneInfo.height}`,
193
+ position: `(${newZoneInfo.x}, ${newZoneInfo.y})`,
194
+ bottomRight: `(${newZoneInfo.x + newZoneInfo.width}, ${newZoneInfo.y + newZoneInfo.height})`,
195
+ center: `(${newZoneInfo.x + newZoneInfo.width / 2}, ${newZoneInfo.y + newZoneInfo.height / 2})`,
196
+ })
197
+ }
198
+ }
199
+ }, [isDragging, dragOverPosition, columnIndex, config])
200
+
201
+ return (
202
+ <div ref={debugRef} className="relative">
203
+ {children}
204
+
205
+ {/* デバッグ用オーバーレイ - ドラッグ中かつデバッグ設定が有効な場合のみ表示 */}
206
+ {isDragging && zoneInfo && config.showDropZoneOutlines && (
207
+ <>
208
+ {/* ドロップゾーン境界線 */}
209
+ <div className={`pointer-events-none absolute inset-0 rounded-lg border-2 border-dashed transition-all duration-200 ${zoneInfo.isActive ? "border-red-500 bg-red-100/20 shadow-lg" : "border-gray-400 bg-gray-100/10"}`} />
210
+
211
+ {/* ドロップゾーン情報表示 */}
212
+ <div className={`absolute top-2 left-2 z-20 rounded-md px-2 py-1 font-mono text-xs leading-tight shadow-lg transition-all duration-200 ${zoneInfo.isActive ? "bg-red-500/90 text-white" : "bg-gray-500/90 text-white"}`}>
213
+ <div className="font-bold">DropZone-{columnIndex}</div>
214
+ <div>
215
+ {zoneInfo.width}×{zoneInfo.height}
216
+ </div>
217
+ <div>
218
+ X:{zoneInfo.x} Y:{zoneInfo.y}
219
+ </div>
220
+ <div className="mt-0.5 text-[10px] opacity-80">{zoneInfo.isActive ? "ACTIVE" : "IDLE"}</div>
221
+ </div>
222
+
223
+ {/* 中央座標マーカー */}
224
+ <div
225
+ className={`pointer-events-none absolute transition-all duration-200 ${zoneInfo.isActive ? "opacity-100" : "opacity-50"}`}
226
+ style={
227
+ {
228
+ left: `${zoneInfo.width / 2 - 6}px`,
229
+ top: `${zoneInfo.height / 2 - 6}px`,
230
+ } as React.CSSProperties
231
+ }>
232
+ <div className={`h-3 w-3 rounded-full border-2 ${zoneInfo.isActive ? "border-red-700 bg-red-500" : "border-gray-700 bg-gray-500"}`} />
233
+ </div>
234
+
235
+ {/* 四隅のマーカー */}
236
+ {zoneInfo.isActive && (
237
+ <>
238
+ <div className="pointer-events-none absolute top-0 left-0 h-2 w-2 rounded-full bg-red-500" />
239
+ <div className="pointer-events-none absolute top-0 right-0 h-2 w-2 rounded-full bg-red-500" />
240
+ <div className="pointer-events-none absolute bottom-0 left-0 h-2 w-2 rounded-full bg-red-500" />
241
+ <div className="pointer-events-none absolute right-0 bottom-0 h-2 w-2 rounded-full bg-red-500" />
242
+ </>
243
+ )}
244
+ </>
245
+ )}
246
+ </div>
247
+ )
248
+ }
249
+
250
+ DropZoneDebugOverlay.displayName = "DropZoneDebugOverlay"
@@ -0,0 +1,130 @@
1
+ /* DragDropLayout コンポーネント全体のカスタムスタイル */
2
+
3
+ /* 固定幅カラムの幅を固定し縮小を防止 */
4
+ .resize-enabled-column {
5
+ flex-shrink: 0;
6
+ flex-grow: 0;
7
+ width: var(--column-width, 350px);
8
+ }
9
+
10
+ /* 残余スペースを埋めるカラム設定 */
11
+ .auto-width-column {
12
+ flex: 1;
13
+ }
14
+
15
+ /* リサイズ操作中にカーソル形状を統一 */
16
+ .resizing-cursor {
17
+ cursor: col-resize !important;
18
+ user-select: none !important;
19
+ }
20
+
21
+ /* リサイズ中カラムの視覚変化を柔らかく付与 */
22
+ .column-resizing {
23
+ opacity: 0.8;
24
+ transition: opacity 0.2s ease;
25
+ }
26
+
27
+ /* アクティブリサイズ中のカラムを強調表示 */
28
+ .column-resizing-active {
29
+ opacity: 1;
30
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5);
31
+ transition: none;
32
+ }
33
+
34
+ /* 全体リサイズ時にボディカーソルを固定 */
35
+ body.resizing {
36
+ cursor: col-resize !important;
37
+ user-select: none !important;
38
+ }
39
+ /* カスタムドラッグ中のカーソルと選択状態を固定 */
40
+ body.custom-dragging,
41
+ body.custom-dragging * {
42
+ cursor: grabbing !important;
43
+ user-select: none !important;
44
+ }
45
+
46
+ /* カスタムドラッグ中に疑似要素も同じカーソルを使用 */
47
+ body.custom-dragging *::before,
48
+ body.custom-dragging *::after {
49
+ cursor: grabbing !important;
50
+ }
51
+
52
+ /* ドラッグ対象パネルの演出を設定 */
53
+ .panel-draggable[data-dragging="true"] {
54
+ transform: rotate(1deg);
55
+ opacity: 0.2;
56
+ box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.5);
57
+ transition:
58
+ transform 1s ease,
59
+ opacity 1s ease,
60
+ box-shadow 1s ease;
61
+ }
62
+
63
+ /* 移動中パネル内部のオーバーフローを抑制 */
64
+ .panel.is-moving * {
65
+ overflow: hidden !important;
66
+ }
67
+
68
+ /* プレースホルダー非表示時に寸法と余白をゼロ化 */
69
+ .panel-with-placeholder[data-panel-hidden="true"] {
70
+ margin-top: 0 !important;
71
+ margin-bottom: 0 !important;
72
+ padding-top: 0 !important;
73
+ padding-bottom: 0 !important;
74
+ height: 0 !important;
75
+ opacity: 0;
76
+ }
77
+
78
+ /* ドロップ領域内のプレースホルダー表示間隔を調整 */
79
+ [data-drop-zone="true"] > .panel-with-placeholder {
80
+ margin-top: 0;
81
+ transition: margin 0.2s ease;
82
+ }
83
+
84
+ /* ドロップ領域内の複数プレースホルダー間に余白を確保 */
85
+ [data-drop-zone="true"] > .panel-with-placeholder:not([data-panel-hidden="true"]) ~ .panel-with-placeholder:not([data-panel-hidden="true"]) {
86
+ margin-top: 1rem;
87
+ }
88
+
89
+ /* パネルスクロール領域のスクロールバーを細く設定 */
90
+ .panel-scroll-area {
91
+ scrollbar-width: thin;
92
+ scrollbar-color: rgba(15, 23, 42, 0.25) transparent;
93
+ }
94
+
95
+ /* ホバー時にスクロールバー色を強調 */
96
+ .panel-scroll-area:hover {
97
+ scrollbar-color: rgba(15, 23, 42, 0.35) transparent;
98
+ }
99
+
100
+ /* WebKit 系ブラウザでスクロールバー幅を細く定義 */
101
+ .panel-scroll-area::-webkit-scrollbar {
102
+ width: 6px;
103
+ }
104
+
105
+ /* WebKit 系ブラウザでスクロールバーのトラックを透明化 */
106
+ .panel-scroll-area::-webkit-scrollbar-track {
107
+ background: transparent;
108
+ }
109
+
110
+ /* WebKit 系ブラウザでスクロールバーつまみの形状と色を指定 */
111
+ .panel-scroll-area::-webkit-scrollbar-thumb {
112
+ border-radius: 9999px;
113
+ background-color: rgba(15, 23, 42, 0.25);
114
+ }
115
+
116
+ /* ホバー時の WebKit 系スクロールバーつまみ色を変更 */
117
+ .panel-scroll-area:hover::-webkit-scrollbar-thumb {
118
+ background-color: rgba(15, 23, 42, 0.35);
119
+ }
120
+
121
+ /* スクロールバーを完全に非表示にするユーティリティ */
122
+ .no-scrollbar {
123
+ scrollbar-width: none;
124
+ -ms-overflow-style: none;
125
+ }
126
+
127
+ /* WebKit 系ブラウザでスクロールバーを消去 */
128
+ .no-scrollbar::-webkit-scrollbar {
129
+ display: none;
130
+ }
@@ -0,0 +1,208 @@
1
+ import { type MouseEvent as ReactMouseEvent, type ReactNode, type TouchEvent as ReactTouchEvent, useCallback, useEffect, useRef } from "react"
2
+ import { debugLog } from "./DebugComponents"
3
+ import type { DebugConfig } from "./types"
4
+ import "./resize-handle.css"
5
+
6
+ export interface ResizeHandleProps {
7
+ columnId: string
8
+ onResizeStart: (columnId: string, startX: number) => void
9
+ onResize: (currentX: number) => void
10
+ onResizeEnd: () => void
11
+ isResizing?: boolean
12
+ position?: "left" | "right"
13
+ className?: string
14
+ debug?: DebugConfig
15
+ }
16
+
17
+ export const ResizeHandle = ({ columnId, onResizeStart, onResize, onResizeEnd, isResizing = false, position = "right", className = "", debug = {} }: ResizeHandleProps) => {
18
+ const isDragging = useRef(false)
19
+ const handleRef = useRef<HTMLDivElement>(null)
20
+
21
+ // 外部からリサイズ状態が解除された場合の処理
22
+ useEffect(() => {
23
+ if (!isResizing && isDragging.current) {
24
+ debugLog(debug, 1, "外部からのリサイズ解除を検出、ドラッグ状態を強制解除:", columnId)
25
+
26
+ isDragging.current = false
27
+
28
+ document.body.style.cursor = ""
29
+ document.body.style.userSelect = ""
30
+ document.body.classList.remove("resizing-column")
31
+ }
32
+ }, [isResizing, columnId, debug])
33
+
34
+ // マウスダウンでリサイズ開始
35
+ const handleMouseDown = useCallback(
36
+ (e: ReactMouseEvent) => {
37
+ e.preventDefault()
38
+ e.stopPropagation()
39
+
40
+ debugLog(debug, 2, "リサイズハンドル マウスダウン:", columnId)
41
+
42
+ isDragging.current = true
43
+ onResizeStart(columnId, e.clientX)
44
+
45
+ document.body.style.cursor = "col-resize"
46
+ document.body.style.userSelect = "none"
47
+ document.body.classList.add("resizing-column")
48
+ },
49
+ [columnId, onResizeStart, debug],
50
+ )
51
+
52
+ // グローバルマウスイベントの処理
53
+ useEffect(() => {
54
+ const handleMouseMove = (e: MouseEvent) => {
55
+ if (!isDragging.current) return
56
+
57
+ e.preventDefault()
58
+ onResize(e.clientX)
59
+ }
60
+
61
+ const handleMouseUp = () => {
62
+ if (!isDragging.current) return
63
+
64
+ debugLog(debug, 2, "リサイズハンドル マウスアップ:", columnId)
65
+
66
+ isDragging.current = false
67
+ onResizeEnd()
68
+
69
+ document.body.style.cursor = ""
70
+ document.body.style.userSelect = ""
71
+ document.body.classList.remove("resizing-column")
72
+ }
73
+
74
+ document.addEventListener("mousemove", handleMouseMove)
75
+ document.addEventListener("mouseup", handleMouseUp)
76
+
77
+ return () => {
78
+ document.removeEventListener("mousemove", handleMouseMove)
79
+ document.removeEventListener("mouseup", handleMouseUp)
80
+ }
81
+ }, [columnId, onResize, onResizeEnd, debug])
82
+
83
+ // タッチイベント対応
84
+ const handleTouchStart = useCallback(
85
+ (e: ReactTouchEvent) => {
86
+ e.preventDefault()
87
+ e.stopPropagation()
88
+
89
+ const touch = e.touches[0]
90
+ debugLog(debug, 2, "リサイズハンドル タッチスタート:", columnId)
91
+
92
+ isDragging.current = true
93
+ onResizeStart(columnId, touch.clientX)
94
+
95
+ document.body.style.cursor = "col-resize"
96
+ document.body.style.userSelect = "none"
97
+ document.body.classList.add("resizing-column")
98
+ },
99
+ [columnId, onResizeStart, debug],
100
+ )
101
+
102
+ useEffect(() => {
103
+ const handleTouchMove = (e: TouchEvent) => {
104
+ if (!isDragging.current) return
105
+
106
+ e.preventDefault()
107
+ const touch = e.touches[0]
108
+ onResize(touch.clientX)
109
+ }
110
+
111
+ const handleTouchEnd = () => {
112
+ if (!isDragging.current) return
113
+
114
+ debugLog(debug, 2, "リサイズハンドル タッチエンド:", columnId)
115
+
116
+ isDragging.current = false
117
+ onResizeEnd()
118
+
119
+ document.body.style.cursor = ""
120
+ document.body.style.userSelect = ""
121
+ document.body.classList.remove("resizing-column")
122
+ }
123
+
124
+ document.addEventListener("touchmove", handleTouchMove, { passive: false })
125
+ document.addEventListener("touchend", handleTouchEnd)
126
+
127
+ return () => {
128
+ document.removeEventListener("touchmove", handleTouchMove)
129
+ document.removeEventListener("touchend", handleTouchEnd)
130
+ }
131
+ }, [columnId, onResize, onResizeEnd, debug])
132
+
133
+ // コンポーネントアンマウント時のクリーンアップ
134
+ useEffect(() => {
135
+ return () => {
136
+ if (isDragging.current) {
137
+ debugLog(debug, 1, "リサイズハンドルアンマウント時のクリーンアップ:", columnId)
138
+ document.body.style.cursor = ""
139
+ document.body.style.userSelect = ""
140
+ document.body.classList.remove("resizing-column")
141
+ }
142
+ }
143
+ }, [columnId, debug])
144
+
145
+ const handleClassName = ["resize-handle", position, isResizing ? "resizing" : "", className].filter(Boolean).join(" ")
146
+
147
+ return (
148
+ // biome-ignore lint/a11y/useFocusableInteractive: リサイズハンドルとして機能するためインタラクティブである必要がある
149
+ // biome-ignore lint/a11y/useSemanticElements: divを使用するが、aria-roleでセマンティックな意味を提供している
150
+ // biome-ignore lint/a11y/useAriaPropsForRole: リサイズハンドルのため、値の範囲を指定することが難しい
151
+ <div ref={handleRef} className={handleClassName} onMouseDown={handleMouseDown} onTouchStart={handleTouchStart} data-column-id={columnId} data-position={position} aria-label={`${columnId}のカラム幅を調整`} role="separator" aria-orientation="vertical">
152
+ <div className="resize-handle-bar" />
153
+
154
+ {/* デバッグ情報表示 */}
155
+ {debug.showPanelSizes && (
156
+ <div className="resize-handle-debug-info">
157
+ {columnId}
158
+ {isResizing && " (resizing)"}
159
+ </div>
160
+ )}
161
+ </div>
162
+ )
163
+ }
164
+
165
+ // リサイズハンドル付きラッパーコンポーネント
166
+ export interface ResizableColumnProps {
167
+ columnId: string
168
+ width: number
169
+ minWidth?: number
170
+ maxWidth?: number
171
+ onResizeStart: (columnId: string, startX: number) => void
172
+ onResize: (currentX: number) => void
173
+ onResizeEnd: () => void
174
+ isResizing?: boolean
175
+ resizable?: boolean
176
+ className?: string
177
+ debug?: DebugConfig
178
+ children: ReactNode
179
+ }
180
+
181
+ export const ResizableColumn = ({ columnId, width, minWidth = 100, maxWidth = 800, onResizeStart, onResize, onResizeEnd, isResizing = false, resizable = true, className = "", debug = {}, children }: ResizableColumnProps) => {
182
+ const columnClassName = ["resizable-column", isResizing ? "resizing" : "", className].filter(Boolean).join(" ")
183
+
184
+ useEffect(() => {
185
+ const element = document.querySelector(`[data-column-id="${columnId}"]`) as HTMLElement
186
+ if (element) {
187
+ element.style.setProperty("--column-width", `${width}px`)
188
+ element.style.setProperty("--column-min-width", `${minWidth}px`)
189
+ element.style.setProperty("--column-max-width", `${maxWidth}px`)
190
+ }
191
+ }, [columnId, width, minWidth, maxWidth])
192
+
193
+ return (
194
+ <div className={columnClassName} data-column-id={columnId} data-width={width} data-min-width={minWidth} data-max-width={maxWidth}>
195
+ {children}
196
+
197
+ {resizable && <ResizeHandle columnId={columnId} onResizeStart={onResizeStart} onResize={onResize} onResizeEnd={onResizeEnd} isResizing={isResizing} position="right" debug={debug} />}
198
+
199
+ {/* デバッグ情報 */}
200
+ {debug.showPanelSizes && (
201
+ <div className="column-debug-size">
202
+ {width}px
203
+ {isResizing && " ↔"}
204
+ </div>
205
+ )}
206
+ </div>
207
+ )
208
+ }
@@ -0,0 +1,33 @@
1
+ import { Monitor, Smartphone } from "lucide-react"
2
+
3
+ export interface ResponsiveControlProps {
4
+ isResponsiveMode: boolean
5
+ responsiveColumnCount: number
6
+ currentColumnCount: number
7
+ screenWidth: number
8
+ onToggleResponsive: () => void
9
+ }
10
+
11
+ export const ResponsiveControl = ({ isResponsiveMode, responsiveColumnCount, currentColumnCount, screenWidth, onToggleResponsive }: ResponsiveControlProps) => {
12
+ return (
13
+ <div className="flex items-center gap-2 rounded-lg border border-slate-300 bg-white/90 px-3 py-1">
14
+ <button
15
+ type="button"
16
+ onClick={onToggleResponsive}
17
+ className={`flex items-center gap-1 rounded px-2 py-1 text-xs transition-colors ${isResponsiveMode ? "border border-blue-300 bg-blue-100 text-blue-700" : "border border-slate-300 text-slate-600 hover:bg-blue-50 hover:text-blue-600"}`}
18
+ title={isResponsiveMode ? "レスポンシブモードを無効化" : "レスポンシブモードを有効化"}>
19
+ {isResponsiveMode ? <Smartphone className="h-3 w-3" /> : <Monitor className="h-3 w-3" />}
20
+ <span>{isResponsiveMode ? "レスポンシブ" : "固定"}</span>
21
+ </button>
22
+
23
+ {isResponsiveMode && (
24
+ <div className="flex items-center gap-1 text-slate-600 text-xs">
25
+ <span className="font-mono">{screenWidth}px</span>
26
+ <span>→</span>
27
+ <span className="font-semibold text-blue-600">{responsiveColumnCount}列</span>
28
+ {responsiveColumnCount !== currentColumnCount && <span className="text-orange-600">(変更予定)</span>}
29
+ </div>
30
+ )}
31
+ </div>
32
+ )
33
+ }