diff --git a/viewer/featurelens.js b/viewer/featurelens.js index dd78709..65dec6e 100644 --- a/viewer/featurelens.js +++ b/viewer/featurelens.js @@ -1,6 +1,6 @@ /** * FeatureLens — Visual Feedback & AI Task Staging Overlay - * Zero-dependency Web Component with Shadow DOM Isolation + * Zero-dependency Web Component with Shadow DOM Isolation & Mobile Touch Ergonomics */ class FeatureLensElement extends HTMLElement { constructor() { @@ -23,7 +23,6 @@ class FeatureLensElement extends HTMLElement { } setupGlobalEvents() { - // Listen for custom open event window.addEventListener('featurelens:toggle', () => this.toggleDrawer()); } @@ -36,7 +35,7 @@ class FeatureLensElement extends HTMLElement { this.renderNotesList(); } } catch (err) { - console.warn('[FeatureLens] Could not fetch notes from backend, using memory cache:', err); + console.warn('[FeatureLens] Backend unreachable, using memory cache:', err); } } @@ -67,7 +66,7 @@ class FeatureLensElement extends HTMLElement { } // ------------------------------------------------------------- - // Visual Inspector (2D DOM & 3D Three.js Raycaster) + // Visual Inspector (2D DOM & 3D Three.js Touch/Pointer Engine) // ------------------------------------------------------------- initInspector() { this.hoverOverlay = document.createElement('div'); @@ -108,7 +107,7 @@ class FeatureLensElement extends HTMLElement { padding: 0.5rem 1rem; border-radius: 9999px; font-family: 'JetBrains Mono', monospace; - font-size: 0.8rem; + font-size: 0.775rem; font-weight: 700; box-shadow: 0 10px 25px rgba(0,0,0,0.8); z-index: 999999; @@ -116,17 +115,23 @@ class FeatureLensElement extends HTMLElement { align-items: center; gap: 0.5rem; cursor: pointer; + max-width: 90vw; + box-sizing: border-box; + text-align: center; + justify-content: center; `; - banner.innerHTML = `🎯 Click any 2D UI element or 3D CAD part to attach note [ESC to cancel]`; + banner.innerHTML = `🎯 Tap any 2D UI element or 3D CAD part [Cancel]`; banner.onclick = () => this.stopInspectMode(); document.body.appendChild(banner); - this._onMouseMove = this.handlePointerMove.bind(this); - this._onClick = this.handlePointerClick.bind(this); + this._onPointerMove = this.handlePointerMove.bind(this); + this._onPointerClick = this.handlePointerClick.bind(this); this._onKeyDown = (e) => { if (e.key === 'Escape') this.stopInspectMode(); }; - window.addEventListener('mousemove', this._onMouseMove, true); - window.addEventListener('click', this._onClick, true); + window.addEventListener('mousemove', this._onPointerMove, true); + window.addEventListener('touchmove', this._onPointerMove, { passive: false, capture: true }); + window.addEventListener('click', this._onPointerClick, true); + window.addEventListener('touchend', this._onPointerClick, { passive: false, capture: true }); window.addEventListener('keydown', this._onKeyDown, true); } @@ -136,36 +141,45 @@ class FeatureLensElement extends HTMLElement { const banner = document.getElementById('fl-inspect-banner'); if (banner) banner.remove(); - window.removeEventListener('mousemove', this._onMouseMove, true); - window.removeEventListener('click', this._onClick, true); + window.removeEventListener('mousemove', this._onPointerMove, true); + window.removeEventListener('touchmove', this._onPointerMove, { passive: false, capture: true }); + window.removeEventListener('click', this._onPointerClick, true); + window.removeEventListener('touchend', this._onPointerClick, { passive: false, capture: true }); window.removeEventListener('keydown', this._onKeyDown, true); } + getPointerCoords(e) { + if (e.touches && e.touches.length > 0) { + return { clientX: e.touches[0].clientX, clientY: e.touches[0].clientY, target: document.elementFromPoint(e.touches[0].clientX, e.touches[0].clientY) }; + } + if (e.changedTouches && e.changedTouches.length > 0) { + return { clientX: e.changedTouches[0].clientX, clientY: e.changedTouches[0].clientY, target: document.elementFromPoint(e.changedTouches[0].clientX, e.changedTouches[0].clientY) }; + } + return { clientX: e.clientX, clientY: e.clientY, target: e.target }; + } + handlePointerMove(e) { if (!this.isInspectMode) return; - + const { clientX, clientY, target } = this.getPointerCoords(e); + if (!target) return; + // Check if hovering 3D canvas - if (window.scene && window.camera && e.target.tagName === 'CANVAS' && this.raycaster) { - const rect = e.target.getBoundingClientRect(); - this.mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1; - this.mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1; + if (window.scene && window.camera && target.tagName === 'CANVAS' && this.raycaster) { + const rect = target.getBoundingClientRect(); + this.mouse.x = ((clientX - rect.left) / rect.width) * 2 - 1; + this.mouse.y = -((clientY - rect.top) / rect.height) * 2 + 1; this.raycaster.setFromCamera(this.mouse, window.camera); const intersects = this.raycaster.intersectObjects(window.scene.children, true); if (intersects.length > 0) { - const hit = intersects[0]; - let mesh = hit.object; - let partId = mesh.userData?.partId || mesh.name || '3d-mesh'; - - // Highlight 3D part this.hoverOverlay.style.display = 'none'; return; } } // 2D Element highlight - if (e.target && e.target !== this.hoverOverlay && !e.target.closest('feature-lens')) { - const rect = e.target.getBoundingClientRect(); + if (target && target !== this.hoverOverlay && !target.closest('feature-lens')) { + const rect = target.getBoundingClientRect(); this.hoverOverlay.style.display = 'block'; this.hoverOverlay.style.top = `${rect.top + window.scrollY}px`; this.hoverOverlay.style.left = `${rect.left + window.scrollX}px`; @@ -176,19 +190,22 @@ class FeatureLensElement extends HTMLElement { handlePointerClick(e) { if (!this.isInspectMode) return; - e.preventDefault(); + if (e.cancelable) e.preventDefault(); e.stopPropagation(); + const { clientX, clientY, target } = this.getPointerCoords(e); + if (!target) return; + let targetType = 'dom_element'; let targetSelector = ''; let partId = null; let coords3d = null; // 3D Intersect check - if (window.scene && window.camera && e.target.tagName === 'CANVAS' && this.raycaster) { - const rect = e.target.getBoundingClientRect(); - this.mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1; - this.mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1; + if (window.scene && window.camera && target.tagName === 'CANVAS' && this.raycaster) { + const rect = target.getBoundingClientRect(); + this.mouse.x = ((clientX - rect.left) / rect.width) * 2 - 1; + this.mouse.y = -((clientY - rect.top) / rect.height) * 2 + 1; this.raycaster.setFromCamera(this.mouse, window.camera); const intersects = this.raycaster.intersectObjects(window.scene.children, true); @@ -205,7 +222,7 @@ class FeatureLensElement extends HTMLElement { } if (targetType === 'dom_element') { - targetSelector = this.computeSelectorPath(e.target); + targetSelector = this.computeSelectorPath(target); } this.currentTarget = { @@ -264,13 +281,13 @@ Technical Context & Candidate Source Files: async copyAiPrompt(noteId) { const note = this.notes.find(n => n.id === noteId); if (!note) return; - const prompt = this.generateAiPrompt(note); + const promptText = this.generateAiPrompt(note); try { - await navigator.clipboard.writeText(prompt); + await navigator.clipboard.writeText(promptText); alert('✅ AI Task Prompt copied to clipboard! Paste it directly into your AI assistant.'); } catch (err) { console.warn('Clipboard write failed, showing text box:', err); - prompt('Copy your AI task prompt:', prompt); + prompt('Copy your AI task prompt:', promptText); } } @@ -318,7 +335,6 @@ Technical Context & Candidate Source Files: return; } - // Auto deduce candidate files let suggestedFiles = ['viewer/index.html']; if (this.currentTarget?.type === '3d_mesh') { suggestedFiles.push('viewer/index.html#threejs-cad', 'mechanical/'); @@ -411,9 +427,18 @@ Technical Context & Candidate Source Files: position: relative; } + .fl-sheet-handle { + display: none; + width: 36px; + height: 4px; + background: #475569; + border-radius: 9999px; + margin: 0 auto 0.5rem auto; + } + #fl-activator { position: fixed; - bottom: 1.25rem; + bottom: max(1.25rem, env(safe-area-inset-bottom)); right: 1.25rem; background: linear-gradient(135deg, #0f172a 0%, #1e1b4b 100%); border: 1px solid var(--fl-accent); @@ -430,6 +455,8 @@ Technical Context & Candidate Source Files: transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); z-index: 999991; user-select: none; + min-height: 44px; + touch-action: manipulation; } #fl-activator:hover { transform: translateY(-2px); @@ -453,14 +480,14 @@ Technical Context & Candidate Source Files: #fl-drawer { position: fixed; top: 0; - right: -420px; - width: 400px; + right: -440px; + width: 420px; height: 100vh; - background: rgba(11, 15, 23, 0.95); - backdrop-filter: blur(16px); + background: rgba(11, 15, 23, 0.98); + backdrop-filter: blur(20px); border-left: 1px solid var(--fl-border); - box-shadow: -15px 0 35px rgba(0, 0, 0, 0.7); - transition: right 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: -15px 0 35px rgba(0, 0, 0, 0.85); + transition: right 0.3s cubic-bezier(0.4, 0, 0.2, 1), bottom 0.3s cubic-bezier(0.4, 0, 0.2, 1); z-index: 999992; display: flex; flex-direction: column; @@ -490,10 +517,15 @@ Technical Context & Candidate Source Files: background: transparent; border: none; color: var(--fl-text-muted); - font-size: 1.2rem; + font-size: 1.4rem; cursor: pointer; - padding: 0.2rem 0.5rem; - border-radius: 4px; + padding: 0.25rem 0.6rem; + border-radius: 6px; + min-width: 44px; + min-height: 44px; + display: flex; + align-items: center; + justify-content: center; } .fl-close-btn:hover { color: var(--fl-text); @@ -507,6 +539,7 @@ Technical Context & Candidate Source Files: display: flex; flex-direction: column; gap: 0.75rem; + -webkit-overflow-scrolling: touch; } .fl-actions { @@ -518,9 +551,9 @@ Technical Context & Candidate Source Files: background: var(--fl-card-bg); border: 1px solid var(--fl-border); color: var(--fl-text); - padding: 0.5rem 0.75rem; + padding: 0.55rem 0.75rem; border-radius: 6px; - font-size: 0.75rem; + font-size: 0.775rem; font-weight: 600; cursor: pointer; display: flex; @@ -528,7 +561,9 @@ Technical Context & Candidate Source Files: justify-content: center; gap: 0.35rem; flex: 1; + min-height: 44px; transition: all 0.15s ease; + touch-action: manipulation; } .fl-btn:hover { border-color: var(--fl-accent); @@ -548,19 +583,16 @@ Technical Context & Candidate Source Files: background: var(--fl-card-bg); border: 1px solid var(--fl-border); border-radius: 8px; - padding: 0.75rem; + padding: 0.85rem; display: flex; flex-direction: column; - gap: 0.4rem; - font-size: 0.75rem; + gap: 0.45rem; + font-size: 0.775rem; position: relative; } - .fl-card:hover { - border-color: #334155; - } .fl-tag { - padding: 0.15rem 0.4rem; + padding: 0.15rem 0.45rem; border-radius: 4px; font-size: 0.65rem; font-weight: 700; @@ -576,11 +608,12 @@ Technical Context & Candidate Source Files: background: #090d16; border: 1px solid var(--fl-border); color: var(--fl-text); - padding: 0.5rem; + padding: 0.6rem; border-radius: 6px; - font-size: 0.75rem; + font-size: 0.8rem; box-sizing: border-box; font-family: inherit; + min-height: 44px; } .fl-input:focus, .fl-textarea:focus, .fl-select:focus { outline: none; @@ -588,7 +621,50 @@ Technical Context & Candidate Source Files: } .fl-textarea { resize: vertical; - min-height: 80px; + min-height: 90px; + } + + /* Mobile Responsive Bottom Sheet Transformation */ + @media (max-width: 640px) { + #fl-activator { + bottom: max(1rem, env(safe-area-inset-bottom)); + right: 1rem; + padding: 0.5rem 0.85rem; + font-size: 0.75rem; + } + + .fl-sheet-handle { + display: block; + } + + #fl-drawer { + top: auto; + bottom: -100%; + right: 0; + left: 0; + width: 100%; + height: 82vh; + border-left: none; + border-top: 1px solid var(--fl-border); + border-radius: 20px 20px 0 0; + padding-bottom: max(1rem, env(safe-area-inset-bottom)); + } + #fl-drawer.open { + bottom: 0; + right: 0; + } + + .fl-header { + flex-direction: column; + align-items: stretch; + gap: 0.25rem; + padding: 0.6rem 1rem 0.5rem 1rem; + } + .fl-close-btn { + position: absolute; + top: 0.6rem; + right: 1rem; + } } @@ -598,9 +674,10 @@ Technical Context & Candidate Source Files: - +