Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 11:59:54 +08:00

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();
}
}