diff --git a/frontend/dcViewer.css b/frontend/dcViewer.css index 4d0f604..ed53bda 100644 --- a/frontend/dcViewer.css +++ b/frontend/dcViewer.css @@ -720,11 +720,8 @@ header { display: block; width: 100%; height: 100%; - fill: none; - stroke: currentColor; - stroke-width: 1.7; - stroke-linecap: round; - stroke-linejoin: round; + fill: currentColor; + stroke: none; filter: drop-shadow(0 5px 12px rgba(0, 0, 0, 0.48)); overflow: visible; } @@ -1491,6 +1488,10 @@ header { #commentModalBackdrop { z-index: 30045; + background: transparent; + backdrop-filter: none; + -webkit-backdrop-filter: none; + pointer-events: none; } .templates-panel.measurement-modal { @@ -1632,7 +1633,7 @@ header { position: fixed; --env-control-h: 42px; --ops-comment-accent: var(--comment-target-active, #c4ff67); - --ops-detail-bg: rgba(18, 18, 20, 0.96); + --ops-detail-bg: color-mix(in srgb, var(--modal-bg) var(--modal-bg-alpha, 100%), transparent); --ops-detail-surface: rgba(28, 28, 31, 0.76); --ops-detail-surface-strong: rgba(32, 32, 35, 0.92); --ops-detail-border: rgba(255, 255, 255, 0.055); @@ -1647,7 +1648,6 @@ header { border: 1px solid rgba(255, 255, 255, 0.06); border-radius: 30px; background: - radial-gradient(circle at 80% 20%, rgba(130, 58, 120, 0.13), transparent 34%), linear-gradient(180deg, rgba(255, 255, 255, 0.018), rgba(255, 255, 255, 0.006)), var(--ops-detail-bg); box-shadow: 0 28px 80px rgba(0, 0, 0, 0.64); @@ -1669,6 +1669,81 @@ header { display: none; } +.window-resize-handle { + position: absolute; + z-index: 4; + background: transparent; + touch-action: none; +} + +.window-resize-handle--n, +.window-resize-handle--s { + left: 22px; + right: 22px; + height: 10px; + cursor: ns-resize; +} + +.window-resize-handle--n { + top: 0; +} + +.window-resize-handle--s { + bottom: 0; +} + +.window-resize-handle--e, +.window-resize-handle--w { + top: 22px; + bottom: 22px; + width: 10px; + cursor: ew-resize; +} + +.window-resize-handle--e { + right: 0; +} + +.window-resize-handle--w { + left: 0; +} + +.window-resize-handle--ne, +.window-resize-handle--nw, +.window-resize-handle--se, +.window-resize-handle--sw { + width: 22px; + height: 22px; +} + +.window-resize-handle--ne { + top: 0; + right: 0; + cursor: nesw-resize; +} + +.window-resize-handle--nw { + top: 0; + left: 0; + cursor: nwse-resize; +} + +.window-resize-handle--se { + right: 0; + bottom: 0; + cursor: nwse-resize; +} + +.window-resize-handle--sw { + left: 0; + bottom: 0; + cursor: nesw-resize; +} + +.comment-card-modal.is-expanded .window-resize-handle { + display: none; +} + .comment-card-modal__header { display: flex; align-items: center; diff --git a/frontend/dcViewer.js b/frontend/dcViewer.js index 8c82ab5..c745897 100644 --- a/frontend/dcViewer.js +++ b/frontend/dcViewer.js @@ -525,7 +525,7 @@ const getShareTokenFromPath = () => { const COMMENT_TARGET_ICON = ` `; @@ -1485,6 +1485,115 @@ const setupDraggableFixedWindow = (element, handle) => { handle.addEventListener("pointercancel", endDrag); }; +const setupResizableFixedWindow = (element, { minWidth = 520, minHeight = 420, margin = 8 } = {}) => { + if (!element || element.dataset.resizableReady === "true") return; + element.dataset.resizableReady = "true"; + const directions = ["n", "e", "s", "w", "ne", "nw", "se", "sw"]; + let resize = null; + + const fixCurrentRect = () => { + const rect = element.getBoundingClientRect(); + element.style.left = `${rect.left}px`; + element.style.top = `${rect.top}px`; + element.style.right = "auto"; + element.style.bottom = "auto"; + element.style.width = `${rect.width}px`; + element.style.height = `${rect.height}px`; + element.style.maxHeight = "none"; + element.style.transform = "none"; + return rect; + }; + + const beginResize = (direction, pointerId, clientX, clientY) => { + bringFloatingWindowToFront(element.id === "commentModal" ? "commentModal" : element.id); + const rect = fixCurrentRect(); + resize = { + pointerId, + direction, + startX: clientX, + startY: clientY, + left: rect.left, + top: rect.top, + width: rect.width, + height: rect.height, + }; + }; + + const updateResize = (clientX, clientY) => { + if (!resize) return; + const dx = clientX - resize.startX; + const dy = clientY - resize.startY; + const right = resize.left + resize.width; + const bottom = resize.top + resize.height; + let left = resize.left; + let top = resize.top; + let width = resize.width; + let height = resize.height; + + if (resize.direction.includes("w")) { + left = Math.max(margin, Math.min(resize.left + dx, right - minWidth)); + width = right - left; + } + if (resize.direction.includes("e")) { + width = Math.max(minWidth, resize.width + dx); + width = Math.min(width, window.innerWidth - margin - left); + } + if (resize.direction.includes("n")) { + top = Math.max(margin, Math.min(resize.top + dy, bottom - minHeight)); + height = bottom - top; + } + if (resize.direction.includes("s")) { + height = Math.max(minHeight, resize.height + dy); + height = Math.min(height, window.innerHeight - margin - top); + } + + element.style.left = `${Math.round(left)}px`; + element.style.top = `${Math.round(top)}px`; + element.style.width = `${Math.round(width)}px`; + element.style.height = `${Math.round(height)}px`; + }; + + directions.forEach((direction) => { + const handle = document.createElement("span"); + handle.className = `window-resize-handle window-resize-handle--${direction}`; + handle.dataset.resizeDirection = direction; + handle.setAttribute("aria-hidden", "true"); + element.appendChild(handle); + handle.addEventListener("pointerdown", (event) => { + if (event.pointerType === "mouse" && event.button !== 0) return; + if (element.classList.contains("is-expanded")) return; + event.preventDefault(); + event.stopPropagation(); + beginResize(direction, event.pointerId, event.clientX, event.clientY); + handle.setPointerCapture?.(event.pointerId); + }); + handle.addEventListener("pointermove", (event) => { + if (!resize || resize.pointerId !== event.pointerId) return; + updateResize(event.clientX, event.clientY); + }); + handle.addEventListener("mousedown", (event) => { + if (event.button !== 0 || element.classList.contains("is-expanded")) return; + event.preventDefault(); + event.stopPropagation(); + beginResize(direction, "mouse", event.clientX, event.clientY); + }); + const endResize = (event) => { + if (!resize || resize.pointerId !== event.pointerId) return; + handle.releasePointerCapture?.(event.pointerId); + resize = null; + }; + handle.addEventListener("pointerup", endResize); + handle.addEventListener("pointercancel", endResize); + }); + window.addEventListener("mousemove", (event) => { + if (!resize || resize.pointerId !== "mouse") return; + updateResize(event.clientX, event.clientY); + }); + window.addEventListener("mouseup", () => { + if (resize?.pointerId === "mouse") resize = null; + }); +}; + const bindFooterButtonAction = (button, handler) => { if (!button || typeof handler !== "function") return; button.addEventListener("click", handler); @@ -5308,6 +5417,7 @@ const loadProjectSnapshot = async (project) => { registerFloatingWindowElement("commentModal", commentModal); setupDraggableFixedWindow(commentsPanel, commentsPanelHeader); setupDraggableFixedWindow(commentModal, commentModalHeader); + setupResizableFixedWindow(commentModal, { minWidth: 560, minHeight: 420 }); commentsCollapseButton?.addEventListener("click", () => setCommentsCollapsed(true)); commentsRestoreButton?.addEventListener("click", () => { commentsModeActive = true; diff --git a/frontend/index.html b/frontend/index.html index 9bca116..be6e819 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -134,7 +134,7 @@ - +