feat: Implement mobile-first responsive architecture, bottom sheets, and touch ergonomics

This commit is contained in:
Tyler G 2026-08-23 15:58:43 -07:00
parent 03bc547769
commit 81ff22e502
2 changed files with 360 additions and 69 deletions

View File

@ -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 = `<span>🎯 Click any 2D UI element or 3D CAD part to attach note</span> <span style="color:#ef4444; margin-left:0.5rem;">[ESC to cancel]</span>`;
banner.innerHTML = `<span>🎯 Tap any 2D UI element or 3D CAD part</span> <span style="color:#ef4444; margin-left:0.5rem;">[Cancel]</span>`;
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;
}
}
</style>
@ -598,9 +674,10 @@ Technical Context & Candidate Source Files:
<span id="fl-badge" class="fl-badge" style="display:none;">0</span>
</div>
<!-- Slide-Out Drawer Panel -->
<!-- Slide-Out Drawer / Bottom Sheet Panel -->
<div id="fl-drawer">
<div class="fl-header">
<div class="fl-sheet-handle"></div>
<div class="fl-title">
<span>FeatureLens Studio</span>
<span style="font-size:0.65rem; color:var(--fl-text-muted); font-weight:400;">(OSHW-AI)</span>
@ -618,11 +695,11 @@ Technical Context & Candidate Source Files:
</button>
</div>
<!-- Create Note Form (Hidden by default) -->
<!-- Create Note Form -->
<div id="fl-form-container" style="display:none;">
<div class="fl-card" style="border-color:var(--fl-accent);">
<div style="font-weight:700; color:var(--fl-accent);">Create AI Staging Note</div>
<div id="fl-target-label" style="font-size:0.65rem; color:var(--fl-text-muted);">🌐 Global Target</div>
<div id="fl-target-label" style="font-size:0.7rem; color:var(--fl-text-muted);">🌐 Global Target</div>
<input type="text" id="fl-title-input" class="fl-input" placeholder="Title (e.g. Wire WebSocket telemetry)" />
@ -683,18 +760,18 @@ Technical Context & Candidate Source Files:
<span class="fl-tag fl-tag-${note.category}">${note.category}</span>
<span style="font-size:0.65rem; color:${note.status === 'completed' ? 'var(--fl-emerald)' : 'var(--fl-text-muted)'}; font-weight:700; text-transform:uppercase;">${note.status}</span>
</div>
<div style="font-weight:700; color:var(--fl-text); font-size:0.8rem;">${note.title}</div>
<div style="color:var(--fl-text-muted); font-size:0.65rem;">${targetInfo}</div>
<div style="color:#cbd5e1; font-size:0.7rem; margin-top:0.2rem; line-height:1.3;">${note.description || 'No description provided.'}</div>
<div style="font-weight:700; color:var(--fl-text); font-size:0.85rem;">${note.title}</div>
<div style="color:var(--fl-text-muted); font-size:0.7rem;">${targetInfo}</div>
<div style="color:#cbd5e1; font-size:0.75rem; margin-top:0.2rem; line-height:1.35;">${note.description || 'No description provided.'}</div>
<div style="display:flex; gap:0.4rem; margin-top:0.4rem; padding-top:0.4rem; border-top:1px solid var(--fl-border);">
<button class="fl-btn fl-btn-primary" style="font-size:0.65rem; padding:0.25rem 0.5rem;" onclick="this.getRootNode().host.copyAiPrompt('${note.id}')">
<button class="fl-btn fl-btn-primary" style="font-size:0.7rem; padding:0.35rem 0.5rem;" onclick="this.getRootNode().host.copyAiPrompt('${note.id}')">
📋 Copy AI Prompt
</button>
<button class="fl-btn" style="font-size:0.65rem; padding:0.25rem 0.4rem;" onclick="this.getRootNode().host.updateStatus('${note.id}', '${note.status === 'completed' ? 'open' : 'completed'}')">
<button class="fl-btn" style="font-size:0.7rem; padding:0.35rem 0.4rem;" onclick="this.getRootNode().host.updateStatus('${note.id}', '${note.status === 'completed' ? 'open' : 'completed'}')">
${note.status === 'completed' ? 'Reopen' : 'Complete'}
</button>
<button class="fl-btn" style="font-size:0.65rem; padding:0.25rem 0.4rem; color:var(--fl-rose);" onclick="this.getRootNode().host.deleteNote('${note.id}')">
<button class="fl-btn" style="font-size:0.7rem; padding:0.35rem 0.4rem; color:var(--fl-rose); flex:0 0 44px;" onclick="this.getRootNode().host.deleteNote('${note.id}')">
🗑
</button>
</div>
@ -704,7 +781,6 @@ Technical Context & Candidate Source Files:
}
}
// Register Custom Web Component
if (!customElements.get('feature-lens')) {
customElements.define('feature-lens', FeatureLensElement);
}

View File

@ -132,6 +132,7 @@
width: 100%;
height: 100%;
display: block;
touch-action: none;
}
/* Viewport Overlays */
@ -493,7 +494,206 @@
background: rgba(6, 182, 212, 0.15);
border-color: var(--accent-cyan);
}
/* ==========================================================================
Mobile-First & Tablet Touch Ergonomics Breakpoints (WCAG 48px Compliance)
========================================================================== */
.sheet-drag-handle {
display: none;
width: 40px;
height: 4px;
background: #475569;
border-radius: 9999px;
margin: 0 auto 0.5rem auto;
}
@media (max-width: 768px) {
header {
flex-direction: column;
align-items: stretch;
padding: 0.5rem 0.75rem;
gap: 0.4rem;
}
.logo-group {
justify-content: space-between;
width: 100%;
}
.logo-title {
font-size: 0.95rem;
}
.logo-badge {
font-size: 0.65rem;
padding: 0.15rem 0.35rem;
}
.nav-tabs {
width: 100%;
overflow-x: auto;
white-space: nowrap;
padding-bottom: 0.35rem;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
gap: 0.3rem;
flex-wrap: nowrap;
}
.nav-tabs::-webkit-scrollbar {
display: none;
}
.nav-tab {
padding: 0.45rem 0.75rem;
font-size: 0.725rem;
flex-shrink: 0;
border-radius: 6px;
min-height: 40px;
display: inline-flex;
align-items: center;
gap: 0.35rem;
}
.sheet-drag-handle {
display: block;
}
/* Content Panels Transformed to Slide-Up Bottom Sheets */
.content-panel {
position: fixed !important;
top: auto !important;
bottom: 0 !important;
left: 0 !important;
right: 0 !important;
width: 100% !important;
max-width: 100% !important;
height: 78vh !important;
max-height: 85vh !important;
border-radius: 20px 20px 0 0 !important;
border-left: none !important;
border-top: 1px solid var(--border-color) !important;
background: rgba(17, 24, 39, 0.98) !important;
backdrop-filter: blur(20px) !important;
box-shadow: 0 -15px 40px rgba(0, 0, 0, 0.85) !important;
transform: translateY(100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
display: block !important;
z-index: 100 !important;
padding: 0.75rem 1rem max(1rem, env(safe-area-inset-bottom)) 1rem !important;
box-sizing: border-box !important;
}
.content-panel.active {
transform: translateY(0) !important;
}
.content-panel.minimized {
transform: translateY(calc(100% - 58px)) !important;
overflow: hidden !important;
}
.panel-header {
position: sticky;
top: -0.75rem;
background: rgba(17, 24, 39, 0.98);
margin: -0.75rem -1rem 1rem -1rem;
padding: 0.6rem 1rem 0.5rem 1rem;
border-bottom: 1px solid var(--border-color);
z-index: 5;
flex-direction: column;
align-items: stretch;
gap: 0.25rem;
}
.panel-header-actions {
position: absolute;
top: 0.6rem;
right: 1rem;
}
/* Viewport Controls on Mobile */
.viewport-overlay {
top: auto !important;
bottom: 0.75rem !important;
left: 0.75rem !important;
right: 0.75rem !important;
width: auto !important;
max-height: 38vh !important;
overflow-y: auto !important;
border-radius: 12px !important;
padding: 0.75rem !important;
}
.viewport-quickbar {
top: 0.5rem !important;
right: 0.5rem !important;
left: auto !important;
gap: 0.25rem !important;
padding: 0.25rem !important;
border-radius: 6px !important;
}
.quick-btn span {
display: none; /* Icon-only buttons on mobile */
}
.quick-btn {
padding: 0.4rem 0.5rem !important;
font-size: 0.85rem !important;
min-width: 38px;
min-height: 38px;
justify-content: center;
}
/* Touch Target Optimization (WCAG Minimum 44px-48px) */
.btn, .btn-outline {
min-height: 44px;
padding: 0.55rem 0.85rem;
font-size: 0.8rem;
display: inline-flex;
align-items: center;
justify-content: center;
}
.check-item {
min-height: 48px;
padding: 0.5rem 0.25rem;
display: flex;
align-items: center;
gap: 0.75rem;
}
.check-item input[type="checkbox"] {
width: 22px;
height: 22px;
min-width: 22px;
min-height: 22px;
cursor: pointer;
}
.data-table {
display: block;
overflow-x: auto;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
}
}
@media (max-width: 480px) {
.logo-title {
font-size: 0.85rem;
}
.content-panel {
height: 84vh !important;
}
.viewport-overlay {
max-height: 34vh !important;
}
}
</style>
</head>
<body>
@ -658,6 +858,7 @@
<!-- Panel 1: Supply Chain & Dynamic BOM Suite -->
<div id="supply-chain" class="content-panel">
<div class="panel-header">
<div class="sheet-drag-handle"></div>
<div class="panel-title"><i class="fa-solid fa-truck-ramp-box"></i> <span>Supply Chain & Procurement Matrix</span></div>
<div class="panel-header-actions">
<button class="panel-btn" title="Minimize / Expand" onclick="toggleMinimizePanel(this)"><i class="fa-solid fa-compress"></i></button>
@ -713,6 +914,7 @@
<!-- Panel 2: Material Science & Physics Engine -->
<div id="materials" class="content-panel">
<div class="panel-header">
<div class="sheet-drag-handle"></div>
<div class="panel-title"><i class="fa-solid fa-atom"></i> <span>Material Science & Physics Selector</span></div>
<div class="panel-header-actions">
<button class="panel-btn" title="Minimize / Expand" onclick="toggleMinimizePanel(this)"><i class="fa-solid fa-compress"></i></button>
@ -768,6 +970,7 @@
<!-- Panel 3: Distributed 3D Print Farm & Machine Profiles -->
<div id="print-farm" class="content-panel">
<div class="panel-header">
<div class="sheet-drag-handle"></div>
<div class="panel-title"><i class="fa-solid fa-print"></i> <span>3D Print Farm & Slicer Profiles</span></div>
<div class="panel-header-actions">
<button class="panel-btn" title="Minimize / Expand" onclick="toggleMinimizePanel(this)"><i class="fa-solid fa-compress"></i></button>
@ -811,6 +1014,7 @@
<!-- Panel 4: WebSerial Live Supervisor & Flasher -->
<div id="diagnostics" class="content-panel">
<div class="panel-header">
<div class="sheet-drag-handle"></div>
<div class="panel-title"><i class="fa-solid fa-terminal"></i> <span>WebSerial Supervisor Console</span></div>
<div class="panel-header-actions">
<button class="panel-btn" title="Minimize / Expand" onclick="toggleMinimizePanel(this)"><i class="fa-solid fa-compress"></i></button>
@ -852,6 +1056,7 @@
<!-- Panel 5: Interactive QA & Step-by-Step Assembly Guide -->
<div id="qa-assembly" class="content-panel">
<div class="panel-header">
<div class="sheet-drag-handle"></div>
<div class="panel-title"><i class="fa-solid fa-screwdriver-wrench"></i> <span>Assembly QA Checklist</span></div>
<div class="panel-header-actions">
<button class="panel-btn" title="Minimize / Expand" onclick="toggleMinimizePanel(this)"><i class="fa-solid fa-compress"></i></button>
@ -875,6 +1080,7 @@
<!-- Panel: Serialized MES Chassis Travelers -->
<div id="travelers" class="content-panel">
<div class="panel-header">
<div class="sheet-drag-handle"></div>
<div class="panel-title"><i class="fa-solid fa-barcode"></i> <span>Chassis Manufacturing Execution (MES)</span></div>
<div class="panel-header-actions">
<button class="panel-btn" title="Minimize / Expand" onclick="toggleMinimizePanel(this)"><i class="fa-solid fa-compress"></i></button>
@ -896,6 +1102,7 @@
<!-- Panel: Automated OS & Provisioning Config Generator -->
<div id="provisioning" class="content-panel">
<div class="panel-header">
<div class="sheet-drag-handle"></div>
<div class="panel-title"><i class="fa-solid fa-server"></i> <span>ZFS & OS Provisioning Studio</span></div>
<div class="panel-header-actions">
<button class="panel-btn" title="Minimize / Expand" onclick="toggleMinimizePanel(this)"><i class="fa-solid fa-compress"></i></button>
@ -937,6 +1144,7 @@
<!-- Panel 6: Parts Catalog & 3D CAD Component Library -->
<div id="parts-catalog" class="content-panel">
<div class="panel-header">
<div class="sheet-drag-handle"></div>
<div class="panel-title"><i class="fa-solid fa-boxes-stacked"></i> <span>Parts & 3D CAD Library</span></div>
<div class="panel-header-actions">
<button class="panel-btn" title="Minimize / Expand" onclick="toggleMinimizePanel(this)"><i class="fa-solid fa-compress"></i></button>
@ -1062,7 +1270,13 @@
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.dampingFactor = 0.08;
controls.touches = {
ONE: THREE.TOUCH.ROTATE,
TWO: THREE.TOUCH.DOLLY_PAN
};
controls.minDistance = 200;
controls.maxDistance = 1800;
// Lighting
ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
@ -1860,10 +2074,11 @@
];
function updateViewportOffset() {
const isMobile = window.innerWidth <= 768;
const activePanel = document.querySelector('.content-panel.active');
const isMinimized = activePanel ? activePanel.classList.contains('minimized') : true;
// Shift view window gently to the left (140px) when sidebar is open and expanded
targetViewOffset = (activePanel && !isMinimized) ? 140 : 0;
// Shift view window gently to the left (140px) ONLY on desktop when sidebar is open
targetViewOffset = (!isMobile && activePanel && !isMinimized) ? 140 : 0;
}
function toggleTurntable() {