feat: Implement mobile-first responsive architecture, bottom sheets, and touch ergonomics
This commit is contained in:
parent
03bc547769
commit
81ff22e502
@ -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);
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user