9740bc64c9
Firmware QEMU Tests (ADR-061) / QEMU Test (edge-tier1) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / QEMU Test (full-adr060) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / QEMU Test (tdm-3node) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / Swarm Test (ADR-062) (push) Has been skipped
npm packages / tools/ruview-mcp (node 22) (push) Failing after 1s
nvsim-server → ghcr.io / build-and-publish (push) Failing after 1s
ruview-swarm CI guard / tests (full+train) (push) Failing after 2s
Bench Regression Guard / bench compile-verify (--no-run) (push) Failing after 0s
Bench Regression Guard / bench fast-run (informational, non-gating) (push) Has been skipped
Firmware CI / Verify version.txt matches release tag (push) Has been skipped
Dashboard a11y + cross-browser / a11y (push) Failing after 0s
nvsim Dashboard → GitHub Pages / build-and-deploy (push) Failing after 2s
Firmware CI / Build firmware (esp32s3 / 4mb) (push) Failing after 15s
Firmware QEMU Tests (ADR-061) / Build Espressif QEMU (push) Failing after 1s
Firmware QEMU Tests (ADR-061) / Fuzz Testing (ADR-061 Layer 6) (push) Failing after 1s
Continuous Deployment / Pre-deployment Checks (push) Has been skipped
Firmware CI / Build firmware (esp32c6 / c6-4mb) (push) Failing after 15s
Firmware CI / Build firmware (esp32s3 / 8mb) (push) Failing after 15s
Firmware QEMU Tests (ADR-061) / QEMU Test (boundary-max) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / QEMU Test (boundary-min) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / QEMU Test (default) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / QEMU Test (edge-tier0) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / NVS Matrix Generation (push) Failing after 1s
Security Scanning / Security Policy Compliance (push) Failing after 0s
Security Scanning / Dependency Vulnerability Scan (push) Failing after 0s
Security Scanning / Static Application Security Testing (push) Failing after 1s
Security Scanning / Infrastructure Security Scan (push) Failing after 1s
Security Scanning / Secret Scanning (push) Failing after 1s
npm packages / harness/ruview (node 22) (push) Failing after 17s
Security Scanning / License Compliance Scan (push) Failing after 1s
Security Scanning / Container Security Scan (push) Failing after 4s
three.js demos → GitHub Pages / build-and-deploy (push) Failing after 1s
Verify Pipeline Determinism / Verify Pipeline Determinism (3.11) (push) Failing after 1s
Fix-Marker Regression Guard / Verify fix markers (push) Failing after 1s
ADR-115 MQTT integration tests / mqtt-integration (push) Failing after 1s
npm packages / harness/ruview (node 20) (push) Failing after 1s
npm packages / tools/ruview-mcp (node 20) (push) Failing after 1s
npm packages / tools/ruview-cli (node 20) (push) Failing after 1s
npm packages / tools/ruview-cli (node 22) (push) Failing after 1s
BFLD MQTT Integration / cargo test --features mqtt (live mosquitto) (push) Failing after 29s
ruview-swarm CI guard / build train_marl bin (push) Failing after 2s
ruview-swarm CI guard / clippy (-D warnings, --no-deps) (push) Failing after 3s
ruview-swarm CI guard / tests (ruflo) (push) Failing after 1s
ruview-swarm CI guard / tests (train) (push) Failing after 2s
ruview-swarm CI guard / tests (default) (push) Failing after 2s
Point Cloud Viewer → GitHub Pages / build-and-deploy (push) Failing after 8s
ruview-swarm CI guard / ITAR / publish guard (push) Failing after 0s
wifi-densepose sensing-server → Docker Hub + ghcr.io / build · push · smoke-test (push) Failing after 1s
Continuous Deployment / Deploy to Production (push) Has been cancelled
Continuous Deployment / Rollback Deployment (push) Has been cancelled
Continuous Deployment / Post-deployment Monitoring (push) Has been cancelled
Continuous Deployment / Notify Deployment Status (push) Has been cancelled
Continuous Deployment / Deploy to Staging (push) Has been cancelled
Security Scanning / Security Report (push) Has been cancelled
172 lines
5.1 KiB
JavaScript
172 lines
5.1 KiB
JavaScript
// Mobile Navigation - Hamburger menu for small screens
|
|
// Replaces wrapped tab bar with a slide-out drawer on mobile
|
|
|
|
export class MobileNav {
|
|
constructor() {
|
|
this.drawer = null;
|
|
this.backdrop = null;
|
|
this.hamburger = null;
|
|
this.isOpen = false;
|
|
this.mql = window.matchMedia('(max-width: 768px)');
|
|
}
|
|
|
|
init() {
|
|
this.createHamburger();
|
|
this.createDrawer();
|
|
this.bindEvents();
|
|
this.onMediaChange(this.mql);
|
|
}
|
|
|
|
createHamburger() {
|
|
this.hamburger = document.createElement('button');
|
|
this.hamburger.className = 'mobile-hamburger';
|
|
this.hamburger.setAttribute('aria-label', 'Open navigation menu');
|
|
this.hamburger.setAttribute('aria-expanded', 'false');
|
|
this.hamburger.innerHTML = `
|
|
<span class="hamburger-line"></span>
|
|
<span class="hamburger-line"></span>
|
|
<span class="hamburger-line"></span>
|
|
`;
|
|
this.hamburger.addEventListener('click', () => this.toggle());
|
|
|
|
const header = document.querySelector('.header');
|
|
if (header) {
|
|
header.style.position = 'relative';
|
|
header.appendChild(this.hamburger);
|
|
}
|
|
}
|
|
|
|
createDrawer() {
|
|
// Backdrop
|
|
this.backdrop = document.createElement('div');
|
|
this.backdrop.className = 'mobile-nav-backdrop';
|
|
this.backdrop.addEventListener('click', () => this.close());
|
|
document.body.appendChild(this.backdrop);
|
|
|
|
// Drawer
|
|
this.drawer = document.createElement('nav');
|
|
this.drawer.className = 'mobile-nav-drawer';
|
|
this.drawer.setAttribute('role', 'navigation');
|
|
this.drawer.setAttribute('aria-label', 'Mobile navigation');
|
|
|
|
// Clone tabs into drawer
|
|
const tabs = document.querySelectorAll('.nav-tabs .nav-tab');
|
|
const list = document.createElement('div');
|
|
list.className = 'mobile-nav-list';
|
|
|
|
tabs.forEach(tab => {
|
|
const item = document.createElement(tab.tagName === 'A' ? 'a' : 'button');
|
|
item.className = 'mobile-nav-item';
|
|
item.textContent = tab.textContent.trim();
|
|
|
|
if (tab.tagName === 'A') {
|
|
item.href = tab.href;
|
|
} else {
|
|
const tabId = tab.getAttribute('data-tab');
|
|
item.dataset.tab = tabId;
|
|
if (tab.classList.contains('active')) {
|
|
item.classList.add('active');
|
|
}
|
|
item.addEventListener('click', () => {
|
|
// Activate tab via the original tab manager
|
|
tab.click();
|
|
this.close();
|
|
// Update active states in drawer
|
|
list.querySelectorAll('.mobile-nav-item').forEach(i => i.classList.remove('active'));
|
|
item.classList.add('active');
|
|
});
|
|
}
|
|
|
|
list.appendChild(item);
|
|
});
|
|
|
|
this.drawer.appendChild(list);
|
|
|
|
// Keyboard hint at bottom
|
|
const hint = document.createElement('div');
|
|
hint.className = 'mobile-nav-hint';
|
|
hint.textContent = 'Tip: Press Ctrl+K for command palette';
|
|
this.drawer.appendChild(hint);
|
|
|
|
document.body.appendChild(this.drawer);
|
|
|
|
// Sync active tab when tabs change externally
|
|
const observer = new MutationObserver(() => {
|
|
const activeTab = document.querySelector('.nav-tabs .nav-tab.active');
|
|
if (activeTab) {
|
|
const activeId = activeTab.getAttribute('data-tab');
|
|
list.querySelectorAll('.mobile-nav-item').forEach(item => {
|
|
item.classList.toggle('active', item.dataset.tab === activeId);
|
|
});
|
|
}
|
|
});
|
|
|
|
const navTabs = document.querySelector('.nav-tabs');
|
|
if (navTabs) {
|
|
observer.observe(navTabs, { attributes: true, subtree: true, attributeFilter: ['class'] });
|
|
}
|
|
}
|
|
|
|
bindEvents() {
|
|
// Listen for media query changes
|
|
this.mql.addEventListener('change', (e) => this.onMediaChange(e));
|
|
|
|
// Close on escape
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape' && this.isOpen) this.close();
|
|
});
|
|
|
|
// Swipe to close
|
|
let touchStartX = 0;
|
|
this.drawer.addEventListener('touchstart', (e) => {
|
|
touchStartX = e.touches[0].clientX;
|
|
}, { passive: true });
|
|
this.drawer.addEventListener('touchend', (e) => {
|
|
const deltaX = e.changedTouches[0].clientX - touchStartX;
|
|
if (deltaX < -50) this.close(); // Swipe left to close
|
|
}, { passive: true });
|
|
}
|
|
|
|
onMediaChange(mql) {
|
|
const isMobile = mql.matches !== undefined ? mql.matches : mql;
|
|
document.body.classList.toggle('mobile-nav-active', isMobile);
|
|
|
|
if (!isMobile && this.isOpen) {
|
|
this.close();
|
|
}
|
|
}
|
|
|
|
toggle() {
|
|
this.isOpen ? this.close() : this.open();
|
|
}
|
|
|
|
open() {
|
|
this.isOpen = true;
|
|
this.drawer.classList.add('open');
|
|
this.backdrop.classList.add('open');
|
|
this.hamburger.classList.add('open');
|
|
this.hamburger.setAttribute('aria-expanded', 'true');
|
|
document.body.style.overflow = 'hidden';
|
|
|
|
// Focus first item
|
|
const first = this.drawer.querySelector('.mobile-nav-item');
|
|
if (first) first.focus();
|
|
}
|
|
|
|
close() {
|
|
this.isOpen = false;
|
|
this.drawer.classList.remove('open');
|
|
this.backdrop.classList.remove('open');
|
|
this.hamburger.classList.remove('open');
|
|
this.hamburger.setAttribute('aria-expanded', 'false');
|
|
document.body.style.overflow = '';
|
|
}
|
|
|
|
dispose() {
|
|
this.close();
|
|
this.hamburger?.remove();
|
|
this.drawer?.remove();
|
|
this.backdrop?.remove();
|
|
}
|
|
}
|