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 * 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 { class FeatureLensElement extends HTMLElement {
constructor() { constructor() {
@ -23,7 +23,6 @@ class FeatureLensElement extends HTMLElement {
} }
setupGlobalEvents() { setupGlobalEvents() {
// Listen for custom open event
window.addEventListener('featurelens:toggle', () => this.toggleDrawer()); window.addEventListener('featurelens:toggle', () => this.toggleDrawer());
} }
@ -36,7 +35,7 @@ class FeatureLensElement extends HTMLElement {
this.renderNotesList(); this.renderNotesList();
} }
} catch (err) { } 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() { initInspector() {
this.hoverOverlay = document.createElement('div'); this.hoverOverlay = document.createElement('div');
@ -108,7 +107,7 @@ class FeatureLensElement extends HTMLElement {
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
border-radius: 9999px; border-radius: 9999px;
font-family: 'JetBrains Mono', monospace; font-family: 'JetBrains Mono', monospace;
font-size: 0.8rem; font-size: 0.775rem;
font-weight: 700; font-weight: 700;
box-shadow: 0 10px 25px rgba(0,0,0,0.8); box-shadow: 0 10px 25px rgba(0,0,0,0.8);
z-index: 999999; z-index: 999999;
@ -116,17 +115,23 @@ class FeatureLensElement extends HTMLElement {
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
cursor: pointer; 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(); banner.onclick = () => this.stopInspectMode();
document.body.appendChild(banner); document.body.appendChild(banner);
this._onMouseMove = this.handlePointerMove.bind(this); this._onPointerMove = this.handlePointerMove.bind(this);
this._onClick = this.handlePointerClick.bind(this); this._onPointerClick = this.handlePointerClick.bind(this);
this._onKeyDown = (e) => { if (e.key === 'Escape') this.stopInspectMode(); }; this._onKeyDown = (e) => { if (e.key === 'Escape') this.stopInspectMode(); };
window.addEventListener('mousemove', this._onMouseMove, true); window.addEventListener('mousemove', this._onPointerMove, true);
window.addEventListener('click', this._onClick, 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); window.addEventListener('keydown', this._onKeyDown, true);
} }
@ -136,36 +141,45 @@ class FeatureLensElement extends HTMLElement {
const banner = document.getElementById('fl-inspect-banner'); const banner = document.getElementById('fl-inspect-banner');
if (banner) banner.remove(); if (banner) banner.remove();
window.removeEventListener('mousemove', this._onMouseMove, true); window.removeEventListener('mousemove', this._onPointerMove, true);
window.removeEventListener('click', this._onClick, 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); 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) { handlePointerMove(e) {
if (!this.isInspectMode) return; if (!this.isInspectMode) return;
const { clientX, clientY, target } = this.getPointerCoords(e);
if (!target) return;
// Check if hovering 3D canvas // Check if hovering 3D canvas
if (window.scene && window.camera && e.target.tagName === 'CANVAS' && this.raycaster) { if (window.scene && window.camera && target.tagName === 'CANVAS' && this.raycaster) {
const rect = e.target.getBoundingClientRect(); const rect = target.getBoundingClientRect();
this.mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1; this.mouse.x = ((clientX - rect.left) / rect.width) * 2 - 1;
this.mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1; this.mouse.y = -((clientY - rect.top) / rect.height) * 2 + 1;
this.raycaster.setFromCamera(this.mouse, window.camera); this.raycaster.setFromCamera(this.mouse, window.camera);
const intersects = this.raycaster.intersectObjects(window.scene.children, true); const intersects = this.raycaster.intersectObjects(window.scene.children, true);
if (intersects.length > 0) { 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'; this.hoverOverlay.style.display = 'none';
return; return;
} }
} }
// 2D Element highlight // 2D Element highlight
if (e.target && e.target !== this.hoverOverlay && !e.target.closest('feature-lens')) { if (target && target !== this.hoverOverlay && !target.closest('feature-lens')) {
const rect = e.target.getBoundingClientRect(); const rect = target.getBoundingClientRect();
this.hoverOverlay.style.display = 'block'; this.hoverOverlay.style.display = 'block';
this.hoverOverlay.style.top = `${rect.top + window.scrollY}px`; this.hoverOverlay.style.top = `${rect.top + window.scrollY}px`;
this.hoverOverlay.style.left = `${rect.left + window.scrollX}px`; this.hoverOverlay.style.left = `${rect.left + window.scrollX}px`;
@ -176,19 +190,22 @@ class FeatureLensElement extends HTMLElement {
handlePointerClick(e) { handlePointerClick(e) {
if (!this.isInspectMode) return; if (!this.isInspectMode) return;
e.preventDefault(); if (e.cancelable) e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const { clientX, clientY, target } = this.getPointerCoords(e);
if (!target) return;
let targetType = 'dom_element'; let targetType = 'dom_element';
let targetSelector = ''; let targetSelector = '';
let partId = null; let partId = null;
let coords3d = null; let coords3d = null;
// 3D Intersect check // 3D Intersect check
if (window.scene && window.camera && e.target.tagName === 'CANVAS' && this.raycaster) { if (window.scene && window.camera && target.tagName === 'CANVAS' && this.raycaster) {
const rect = e.target.getBoundingClientRect(); const rect = target.getBoundingClientRect();
this.mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1; this.mouse.x = ((clientX - rect.left) / rect.width) * 2 - 1;
this.mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1; this.mouse.y = -((clientY - rect.top) / rect.height) * 2 + 1;
this.raycaster.setFromCamera(this.mouse, window.camera); this.raycaster.setFromCamera(this.mouse, window.camera);
const intersects = this.raycaster.intersectObjects(window.scene.children, true); const intersects = this.raycaster.intersectObjects(window.scene.children, true);
@ -205,7 +222,7 @@ class FeatureLensElement extends HTMLElement {
} }
if (targetType === 'dom_element') { if (targetType === 'dom_element') {
targetSelector = this.computeSelectorPath(e.target); targetSelector = this.computeSelectorPath(target);
} }
this.currentTarget = { this.currentTarget = {
@ -264,13 +281,13 @@ Technical Context & Candidate Source Files:
async copyAiPrompt(noteId) { async copyAiPrompt(noteId) {
const note = this.notes.find(n => n.id === noteId); const note = this.notes.find(n => n.id === noteId);
if (!note) return; if (!note) return;
const prompt = this.generateAiPrompt(note); const promptText = this.generateAiPrompt(note);
try { 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.'); alert('✅ AI Task Prompt copied to clipboard! Paste it directly into your AI assistant.');
} catch (err) { } catch (err) {
console.warn('Clipboard write failed, showing text box:', 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; return;
} }
// Auto deduce candidate files
let suggestedFiles = ['viewer/index.html']; let suggestedFiles = ['viewer/index.html'];
if (this.currentTarget?.type === '3d_mesh') { if (this.currentTarget?.type === '3d_mesh') {
suggestedFiles.push('viewer/index.html#threejs-cad', 'mechanical/'); suggestedFiles.push('viewer/index.html#threejs-cad', 'mechanical/');
@ -411,9 +427,18 @@ Technical Context & Candidate Source Files:
position: relative; position: relative;
} }
.fl-sheet-handle {
display: none;
width: 36px;
height: 4px;
background: #475569;
border-radius: 9999px;
margin: 0 auto 0.5rem auto;
}
#fl-activator { #fl-activator {
position: fixed; position: fixed;
bottom: 1.25rem; bottom: max(1.25rem, env(safe-area-inset-bottom));
right: 1.25rem; right: 1.25rem;
background: linear-gradient(135deg, #0f172a 0%, #1e1b4b 100%); background: linear-gradient(135deg, #0f172a 0%, #1e1b4b 100%);
border: 1px solid var(--fl-accent); 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); transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 999991; z-index: 999991;
user-select: none; user-select: none;
min-height: 44px;
touch-action: manipulation;
} }
#fl-activator:hover { #fl-activator:hover {
transform: translateY(-2px); transform: translateY(-2px);
@ -453,14 +480,14 @@ Technical Context & Candidate Source Files:
#fl-drawer { #fl-drawer {
position: fixed; position: fixed;
top: 0; top: 0;
right: -420px; right: -440px;
width: 400px; width: 420px;
height: 100vh; height: 100vh;
background: rgba(11, 15, 23, 0.95); background: rgba(11, 15, 23, 0.98);
backdrop-filter: blur(16px); backdrop-filter: blur(20px);
border-left: 1px solid var(--fl-border); border-left: 1px solid var(--fl-border);
box-shadow: -15px 0 35px rgba(0, 0, 0, 0.7); box-shadow: -15px 0 35px rgba(0, 0, 0, 0.85);
transition: right 0.3s cubic-bezier(0.4, 0, 0.2, 1); 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; z-index: 999992;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -490,10 +517,15 @@ Technical Context & Candidate Source Files:
background: transparent; background: transparent;
border: none; border: none;
color: var(--fl-text-muted); color: var(--fl-text-muted);
font-size: 1.2rem; font-size: 1.4rem;
cursor: pointer; cursor: pointer;
padding: 0.2rem 0.5rem; padding: 0.25rem 0.6rem;
border-radius: 4px; border-radius: 6px;
min-width: 44px;
min-height: 44px;
display: flex;
align-items: center;
justify-content: center;
} }
.fl-close-btn:hover { .fl-close-btn:hover {
color: var(--fl-text); color: var(--fl-text);
@ -507,6 +539,7 @@ Technical Context & Candidate Source Files:
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.75rem; gap: 0.75rem;
-webkit-overflow-scrolling: touch;
} }
.fl-actions { .fl-actions {
@ -518,9 +551,9 @@ Technical Context & Candidate Source Files:
background: var(--fl-card-bg); background: var(--fl-card-bg);
border: 1px solid var(--fl-border); border: 1px solid var(--fl-border);
color: var(--fl-text); color: var(--fl-text);
padding: 0.5rem 0.75rem; padding: 0.55rem 0.75rem;
border-radius: 6px; border-radius: 6px;
font-size: 0.75rem; font-size: 0.775rem;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
@ -528,7 +561,9 @@ Technical Context & Candidate Source Files:
justify-content: center; justify-content: center;
gap: 0.35rem; gap: 0.35rem;
flex: 1; flex: 1;
min-height: 44px;
transition: all 0.15s ease; transition: all 0.15s ease;
touch-action: manipulation;
} }
.fl-btn:hover { .fl-btn:hover {
border-color: var(--fl-accent); border-color: var(--fl-accent);
@ -548,19 +583,16 @@ Technical Context & Candidate Source Files:
background: var(--fl-card-bg); background: var(--fl-card-bg);
border: 1px solid var(--fl-border); border: 1px solid var(--fl-border);
border-radius: 8px; border-radius: 8px;
padding: 0.75rem; padding: 0.85rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.4rem; gap: 0.45rem;
font-size: 0.75rem; font-size: 0.775rem;
position: relative; position: relative;
} }
.fl-card:hover {
border-color: #334155;
}
.fl-tag { .fl-tag {
padding: 0.15rem 0.4rem; padding: 0.15rem 0.45rem;
border-radius: 4px; border-radius: 4px;
font-size: 0.65rem; font-size: 0.65rem;
font-weight: 700; font-weight: 700;
@ -576,11 +608,12 @@ Technical Context & Candidate Source Files:
background: #090d16; background: #090d16;
border: 1px solid var(--fl-border); border: 1px solid var(--fl-border);
color: var(--fl-text); color: var(--fl-text);
padding: 0.5rem; padding: 0.6rem;
border-radius: 6px; border-radius: 6px;
font-size: 0.75rem; font-size: 0.8rem;
box-sizing: border-box; box-sizing: border-box;
font-family: inherit; font-family: inherit;
min-height: 44px;
} }
.fl-input:focus, .fl-textarea:focus, .fl-select:focus { .fl-input:focus, .fl-textarea:focus, .fl-select:focus {
outline: none; outline: none;
@ -588,7 +621,50 @@ Technical Context & Candidate Source Files:
} }
.fl-textarea { .fl-textarea {
resize: vertical; 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> </style>
@ -598,9 +674,10 @@ Technical Context & Candidate Source Files:
<span id="fl-badge" class="fl-badge" style="display:none;">0</span> <span id="fl-badge" class="fl-badge" style="display:none;">0</span>
</div> </div>
<!-- Slide-Out Drawer Panel --> <!-- Slide-Out Drawer / Bottom Sheet Panel -->
<div id="fl-drawer"> <div id="fl-drawer">
<div class="fl-header"> <div class="fl-header">
<div class="fl-sheet-handle"></div>
<div class="fl-title"> <div class="fl-title">
<span>FeatureLens Studio</span> <span>FeatureLens Studio</span>
<span style="font-size:0.65rem; color:var(--fl-text-muted); font-weight:400;">(OSHW-AI)</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> </button>
</div> </div>
<!-- Create Note Form (Hidden by default) --> <!-- Create Note Form -->
<div id="fl-form-container" style="display:none;"> <div id="fl-form-container" style="display:none;">
<div class="fl-card" style="border-color:var(--fl-accent);"> <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 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)" /> <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 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> <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>
<div style="font-weight:700; color:var(--fl-text); font-size:0.8rem;">${note.title}</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.65rem;">${targetInfo}</div> <div style="color:var(--fl-text-muted); font-size:0.7rem;">${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="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);"> <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 📋 Copy AI Prompt
</button> </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'} ${note.status === 'completed' ? 'Reopen' : 'Complete'}
</button> </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> </button>
</div> </div>
@ -704,7 +781,6 @@ Technical Context & Candidate Source Files:
} }
} }
// Register Custom Web Component
if (!customElements.get('feature-lens')) { if (!customElements.get('feature-lens')) {
customElements.define('feature-lens', FeatureLensElement); customElements.define('feature-lens', FeatureLensElement);
} }

View File

@ -132,6 +132,7 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
display: block; display: block;
touch-action: none;
} }
/* Viewport Overlays */ /* Viewport Overlays */
@ -493,7 +494,206 @@
background: rgba(6, 182, 212, 0.15); background: rgba(6, 182, 212, 0.15);
border-color: var(--accent-cyan); 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> </style>
</head> </head>
<body> <body>
@ -658,6 +858,7 @@
<!-- Panel 1: Supply Chain & Dynamic BOM Suite --> <!-- Panel 1: Supply Chain & Dynamic BOM Suite -->
<div id="supply-chain" class="content-panel"> <div id="supply-chain" class="content-panel">
<div class="panel-header"> <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-title"><i class="fa-solid fa-truck-ramp-box"></i> <span>Supply Chain & Procurement Matrix</span></div>
<div class="panel-header-actions"> <div class="panel-header-actions">
<button class="panel-btn" title="Minimize / Expand" onclick="toggleMinimizePanel(this)"><i class="fa-solid fa-compress"></i></button> <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 --> <!-- Panel 2: Material Science & Physics Engine -->
<div id="materials" class="content-panel"> <div id="materials" class="content-panel">
<div class="panel-header"> <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-title"><i class="fa-solid fa-atom"></i> <span>Material Science & Physics Selector</span></div>
<div class="panel-header-actions"> <div class="panel-header-actions">
<button class="panel-btn" title="Minimize / Expand" onclick="toggleMinimizePanel(this)"><i class="fa-solid fa-compress"></i></button> <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 --> <!-- Panel 3: Distributed 3D Print Farm & Machine Profiles -->
<div id="print-farm" class="content-panel"> <div id="print-farm" class="content-panel">
<div class="panel-header"> <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-title"><i class="fa-solid fa-print"></i> <span>3D Print Farm & Slicer Profiles</span></div>
<div class="panel-header-actions"> <div class="panel-header-actions">
<button class="panel-btn" title="Minimize / Expand" onclick="toggleMinimizePanel(this)"><i class="fa-solid fa-compress"></i></button> <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 --> <!-- Panel 4: WebSerial Live Supervisor & Flasher -->
<div id="diagnostics" class="content-panel"> <div id="diagnostics" class="content-panel">
<div class="panel-header"> <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-title"><i class="fa-solid fa-terminal"></i> <span>WebSerial Supervisor Console</span></div>
<div class="panel-header-actions"> <div class="panel-header-actions">
<button class="panel-btn" title="Minimize / Expand" onclick="toggleMinimizePanel(this)"><i class="fa-solid fa-compress"></i></button> <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 --> <!-- Panel 5: Interactive QA & Step-by-Step Assembly Guide -->
<div id="qa-assembly" class="content-panel"> <div id="qa-assembly" class="content-panel">
<div class="panel-header"> <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-title"><i class="fa-solid fa-screwdriver-wrench"></i> <span>Assembly QA Checklist</span></div>
<div class="panel-header-actions"> <div class="panel-header-actions">
<button class="panel-btn" title="Minimize / Expand" onclick="toggleMinimizePanel(this)"><i class="fa-solid fa-compress"></i></button> <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 --> <!-- Panel: Serialized MES Chassis Travelers -->
<div id="travelers" class="content-panel"> <div id="travelers" class="content-panel">
<div class="panel-header"> <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-title"><i class="fa-solid fa-barcode"></i> <span>Chassis Manufacturing Execution (MES)</span></div>
<div class="panel-header-actions"> <div class="panel-header-actions">
<button class="panel-btn" title="Minimize / Expand" onclick="toggleMinimizePanel(this)"><i class="fa-solid fa-compress"></i></button> <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 --> <!-- Panel: Automated OS & Provisioning Config Generator -->
<div id="provisioning" class="content-panel"> <div id="provisioning" class="content-panel">
<div class="panel-header"> <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-title"><i class="fa-solid fa-server"></i> <span>ZFS & OS Provisioning Studio</span></div>
<div class="panel-header-actions"> <div class="panel-header-actions">
<button class="panel-btn" title="Minimize / Expand" onclick="toggleMinimizePanel(this)"><i class="fa-solid fa-compress"></i></button> <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 --> <!-- Panel 6: Parts Catalog & 3D CAD Component Library -->
<div id="parts-catalog" class="content-panel"> <div id="parts-catalog" class="content-panel">
<div class="panel-header"> <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-title"><i class="fa-solid fa-boxes-stacked"></i> <span>Parts & 3D CAD Library</span></div>
<div class="panel-header-actions"> <div class="panel-header-actions">
<button class="panel-btn" title="Minimize / Expand" onclick="toggleMinimizePanel(this)"><i class="fa-solid fa-compress"></i></button> <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 = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; 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 // Lighting
ambientLight = new THREE.AmbientLight(0xffffff, 0.7); ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
@ -1860,10 +2074,11 @@
]; ];
function updateViewportOffset() { function updateViewportOffset() {
const isMobile = window.innerWidth <= 768;
const activePanel = document.querySelector('.content-panel.active'); const activePanel = document.querySelector('.content-panel.active');
const isMinimized = activePanel ? activePanel.classList.contains('minimized') : true; const isMinimized = activePanel ? activePanel.classList.contains('minimized') : true;
// Shift view window gently to the left (140px) when sidebar is open and expanded // Shift view window gently to the left (140px) ONLY on desktop when sidebar is open
targetViewOffset = (activePanel && !isMinimized) ? 140 : 0; targetViewOffset = (!isMobile && activePanel && !isMinimized) ? 140 : 0;
} }
function toggleTurntable() { function toggleTurntable() {