mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-06-07 02:11:45 +00:00
Mobile-only changes (`@media (max-width: 900px)` or `isMobile()` gates); desktop unchanged. All iterations were CDP-verified in the local browser against staging at 390x844 and 1206x928 viewports. ### #1415 / #1458 / #1461 — packets-list density - Kill empty chevron rail on mobile - Slim sticky THEAD (24px, retains sort affordance per operator preference) - Hide page-header on mobile entirely - Convert group-header `toggle-select` to `select-hash` on mobile so the row stops being a dead-end (no chevron, nothing to expand) ### #1458 / #1461 — detail panel cleanup - Drop redundant src->dst line (identity already in sticky header) - Hide the boxed "decoded message" duplication card - Hide PAYLOAD TYPE row (already in the header badge) - 2-col label/value grid (~40%% panel-height reduction) - Sticky in-sheet header for packet identity - Kill iOS-style drag handle (conflicts with browser pull-to-refresh) - Make X close visible + always reachable - Outer sheet `overflow:hidden`, inner content `overflow-y:auto` (scrollable region distinct, scrollbar visible) - Bottom-nav clearance (`padding-bottom: 60px`) - Close detail sheet on route change away from /packets - Tap-to-toast popovers for score tooltips (title= does not fire on touch) ### #1467 — mobile nav surface - Mirror pause + Filters pill into navbar via new mobile-page-actions.js - Mirror Favorites / Search / Customize into the bottom-nav More sheet - Brand stays in top nav; per-page controls inject into `.nav-left` Closes #1415, #1458, #1461, #1467.
This commit is contained in:
@@ -170,5 +170,6 @@
|
||||
<script src="compare.js?v=__BUST__" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="node-analytics.js?v=__BUST__" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="perf.js?v=__BUST__" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="mobile-page-actions.js?v=__BUST__" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
/* #1461 mobile page-actions: mirror per-page header buttons (pause, filter
|
||||
* toggle) into the top nav's .nav-right area on mobile so the page-header
|
||||
* row can be hidden entirely. On desktop this script is a no-op. */
|
||||
(function () {
|
||||
'use strict';
|
||||
const MOBILE_BP = 900;
|
||||
const SLOT_ID = 'navPageActions';
|
||||
|
||||
function isMobile() { return window.innerWidth <= MOBILE_BP; }
|
||||
|
||||
function ensureSlot() {
|
||||
let slot = document.getElementById(SLOT_ID);
|
||||
if (slot) return slot;
|
||||
// On mobile, .nav-right is display:none — use .nav-left so the slot is
|
||||
// visible. Append after the brand link.
|
||||
const navLeft = document.querySelector('.nav-left');
|
||||
if (!navLeft) return null;
|
||||
slot = document.createElement('div');
|
||||
slot.id = SLOT_ID;
|
||||
slot.style.cssText = 'display:inline-flex;gap:4px;align-items:center;margin-left:8px;';
|
||||
navLeft.appendChild(slot);
|
||||
return slot;
|
||||
}
|
||||
|
||||
function clearSlot() {
|
||||
const slot = document.getElementById(SLOT_ID);
|
||||
if (slot) slot.innerHTML = '';
|
||||
}
|
||||
|
||||
function makeBtn(label, title, onClick) {
|
||||
const b = document.createElement('button');
|
||||
b.className = 'nav-btn';
|
||||
b.type = 'button';
|
||||
b.title = title;
|
||||
b.textContent = label;
|
||||
b.addEventListener('click', onClick);
|
||||
return b;
|
||||
}
|
||||
|
||||
function syncForRoute() {
|
||||
// #1461 #6: close mobile detail sheet on route change away from packets
|
||||
try {
|
||||
const sheet = document.getElementById('mobileDetailSheet');
|
||||
if (sheet && !/^#\/packets/.test(location.hash || '')) {
|
||||
sheet.classList.remove('open');
|
||||
}
|
||||
} catch (_e) {}
|
||||
|
||||
if (!isMobile()) { clearSlot(); return; }
|
||||
const hash = location.hash || '';
|
||||
const slot = ensureSlot();
|
||||
if (!slot) return;
|
||||
slot.innerHTML = '';
|
||||
|
||||
if (/^#\/packets(\/|$|\?)/.test(hash)) {
|
||||
// Mirror pause button (icon only — small)
|
||||
const pause = makeBtn('⏸', 'Pause live updates', function () {
|
||||
const real = document.getElementById('pktPauseBtn');
|
||||
if (real) real.click();
|
||||
});
|
||||
pause.style.cssText = 'padding:4px 6px;font-size:14px;background:transparent;border:1px solid var(--border,#444);border-radius:6px;color:var(--nav-text,#fff);cursor:pointer;';
|
||||
slot.appendChild(pause);
|
||||
// Mirror filter toggle as a labeled "Filters ▾" pill (matches inline style)
|
||||
const filt = makeBtn('Filters ▾', 'Toggle filters', function () {
|
||||
const real = document.querySelector('.filter-bar .filter-toggle-btn, #filterToggleBtn');
|
||||
if (real) real.click();
|
||||
});
|
||||
filt.className = 'nav-btn filter-toggle-btn-mirror';
|
||||
filt.style.cssText = 'padding:4px 10px;font-size:12px;background:transparent;border:1px solid var(--border,#444);border-radius:999px;color:var(--nav-text,#fff);cursor:pointer;font-weight:500;';
|
||||
slot.appendChild(filt);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('hashchange', syncForRoute);
|
||||
window.addEventListener('resize', syncForRoute);
|
||||
|
||||
/* #1461 #7: on mobile, packets-list group-header expand is a UX dead-end
|
||||
* (we hid the chevron so there's no way to collapse). Intercept those
|
||||
* clicks and force them to the single-select code path instead — the
|
||||
* detail pane has all the obs info anyway. */
|
||||
document.addEventListener('click', function (e) {
|
||||
if (!isMobile()) return;
|
||||
const row = e.target.closest && e.target.closest('#pktTable tr[data-action="toggle-select"]');
|
||||
if (!row) return;
|
||||
// Convert to a select-hash event by re-dispatching synthetically — simpler
|
||||
// to mutate the attribute briefly so the existing delegated handler
|
||||
// routes it correctly.
|
||||
row.setAttribute('data-action', 'select-hash');
|
||||
}, true);
|
||||
|
||||
/* #1461 #8: traffic_share_score / bridge_score tooltips use title= which
|
||||
* doesn't fire on touch. Show a click-to-toast popover on mobile when
|
||||
* operator taps a TD whose title mentions traffic/bridge/centrality. */
|
||||
document.addEventListener('click', function (e) {
|
||||
if (!isMobile()) return;
|
||||
const el = e.target.closest('[title]');
|
||||
if (!el) return;
|
||||
const text = el.getAttribute('title');
|
||||
if (!text) return;
|
||||
// Limit to score / metric explanations to avoid spamming on every titled element
|
||||
if (!/traffic share|bridge|centrality|score|usefulness/i.test(text + ' ' + el.textContent)) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showToast(text);
|
||||
}, true);
|
||||
function showToast(msg) {
|
||||
let t = document.getElementById('mcMobileToast');
|
||||
if (!t) {
|
||||
t = document.createElement('div');
|
||||
t.id = 'mcMobileToast';
|
||||
t.style.cssText = 'position:fixed;left:50%;bottom:90px;transform:translateX(-50%);background:var(--surface-2,#222);color:var(--text,#fff);padding:10px 16px;border:1px solid var(--border,#444);border-radius:8px;font-size:13px;max-width:80vw;z-index:9999;box-shadow:0 4px 12px rgba(0,0,0,0.4);transition:opacity .3s;';
|
||||
document.body.appendChild(t);
|
||||
}
|
||||
t.textContent = msg;
|
||||
t.style.opacity = '1';
|
||||
clearTimeout(t._timer);
|
||||
t._timer = setTimeout(() => { t.style.opacity = '0'; }, 4000);
|
||||
}
|
||||
|
||||
/* #1467: mirror missing top-nav controls (Favorites, Search, Customize)
|
||||
* into the bottom-nav More sheet. bottom-nav.js only wired Dark mode;
|
||||
* the others have no mobile surface today. Insert above the existing
|
||||
* dark-mode separator so the new items group with the other route items. */
|
||||
function addMissingMoreSheetItems() {
|
||||
const sheet = document.querySelector('[data-bottom-nav-sheet]');
|
||||
if (!sheet) { setTimeout(addMissingMoreSheetItems, 500); return; }
|
||||
if (sheet.querySelector('[data-mpa-mirror]')) return; // already injected
|
||||
|
||||
const mirrors = [
|
||||
{ id: 'favToggle', icon: '⭐', label: 'Favorites' },
|
||||
{ id: 'searchToggle', icon: '🔍', label: 'Search' },
|
||||
{ id: 'customizeToggle', icon: '🎨', label: 'Customize' },
|
||||
];
|
||||
|
||||
const sep = sheet.querySelector('.bottom-nav-sheet-sep');
|
||||
mirrors.forEach((m) => {
|
||||
const real = document.getElementById(m.id);
|
||||
if (!real) return;
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'bottom-nav-sheet-item';
|
||||
btn.setAttribute('role', 'menuitem');
|
||||
btn.setAttribute('data-mpa-mirror', m.id);
|
||||
|
||||
const ic = document.createElement('span');
|
||||
ic.className = 'bottom-nav-sheet-icon';
|
||||
ic.setAttribute('aria-hidden', 'true');
|
||||
ic.textContent = m.icon;
|
||||
|
||||
const lb = document.createElement('span');
|
||||
lb.className = 'bottom-nav-sheet-label';
|
||||
lb.textContent = m.label;
|
||||
|
||||
btn.appendChild(ic);
|
||||
btn.appendChild(lb);
|
||||
btn.addEventListener('click', function () {
|
||||
real.click();
|
||||
// close the sheet after delegating
|
||||
try { sheet.classList.remove('open'); } catch (_e) {}
|
||||
});
|
||||
|
||||
if (sep) sheet.insertBefore(btn, sep);
|
||||
else sheet.appendChild(btn);
|
||||
});
|
||||
}
|
||||
// Also re-run when sheet is opened (bottom-nav rebuilds it on open)
|
||||
document.addEventListener('click', function (e) {
|
||||
const target = e.target.closest && e.target.closest('[data-bottom-nav-more]');
|
||||
if (target) setTimeout(addMissingMoreSheetItems, 50);
|
||||
}, true);
|
||||
|
||||
// Run after page-header is rendered (packets.js builds it async); retry briefly
|
||||
let tries = 0;
|
||||
function init() {
|
||||
syncForRoute();
|
||||
addMissingMoreSheetItems();
|
||||
if (tries++ < 20 && /^#\/packets/.test(location.hash) && !document.getElementById('pktPauseBtn')) {
|
||||
setTimeout(init, 250);
|
||||
}
|
||||
}
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
@@ -879,6 +879,121 @@ img.brand-logo {
|
||||
width: 32px; min-width: 32px; max-width: 32px;
|
||||
padding: 3px 4px; text-align: center;
|
||||
}
|
||||
|
||||
/* #1461 Tufte v2 mobile prescriptions (iterating; not yet committed). */
|
||||
@media (max-width: 900px) {
|
||||
/* #1: kill empty chevron rail */
|
||||
#pktTable th.col-expand,
|
||||
#pktTable td.col-expand { display: none !important; }
|
||||
|
||||
/* #1b: slim THEAD ~24px sticky */
|
||||
#pktTable thead th {
|
||||
padding: 3px 6px !important;
|
||||
font-size: 9px !important;
|
||||
line-height: 1.2 !important;
|
||||
letter-spacing: 0.04em !important;
|
||||
text-transform: uppercase;
|
||||
height: 24px !important;
|
||||
}
|
||||
#pktTable thead { position: sticky; top: 0; z-index: 5; background: var(--surface-1, #1a1a1a); }
|
||||
|
||||
/* #2: hide page-header row + filter-expr + collapsed filter-bar.
|
||||
* pause + Filters pill are mirrored into .nav-left by mobile-page-actions.js */
|
||||
#pktLeft .page-header { display: none !important; }
|
||||
#pktLeft .pkt-filter-expr { display: none !important; }
|
||||
.filter-bar { display: none !important; }
|
||||
.filter-bar.filters-expanded {
|
||||
display: flex !important;
|
||||
padding: 6px 8px !important;
|
||||
margin-bottom: 4px !important;
|
||||
background: var(--surface-2, rgba(0,0,0,0.05)) !important;
|
||||
border: 1px solid var(--border, #333) !important;
|
||||
border-radius: 6px !important;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* #3: detail panel cleanup */
|
||||
/* Strip redundant src→dst line (identity in sticky header) */
|
||||
.mobile-detail-sheet .detail-srcdst { display: none !important; }
|
||||
/* Hide boxed "decoded message" card on mobile — content already in
|
||||
* the sticky header's semantic summary */
|
||||
.mobile-detail-sheet .detail-message { display: none !important; }
|
||||
/* Hide PAYLOAD TYPE row (first dt+dd pair — already in header badge) */
|
||||
.mobile-detail-sheet dl.detail-meta > dt:first-child,
|
||||
.mobile-detail-sheet dl.detail-meta > dt:first-child + dd { display: none !important; }
|
||||
/* 2-col grid for label/value rows */
|
||||
.mobile-detail-sheet dl.detail-meta {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(90px, 30%) 1fr;
|
||||
gap: 4px 10px;
|
||||
margin: 8px 0 !important;
|
||||
}
|
||||
.mobile-detail-sheet dl.detail-meta dt {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
align-self: center;
|
||||
}
|
||||
.mobile-detail-sheet dl.detail-meta dd {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* #4: sheet structure — kill handle, sticky header inside sheet, clear
|
||||
* bottom-nav (58px tall) so last row doesn't get cut off, taller max */
|
||||
/* Restructure: outer sheet has fixed height + overflow:hidden; the
|
||||
* .mobile-sheet-content is the ONLY scroll region. ✕ close stays sticky
|
||||
* pinned at top of the sheet. Title also stays sticky within the
|
||||
* scroll region. */
|
||||
.mobile-detail-sheet {
|
||||
max-height: calc(100vh - 110px) !important; /* leave room for bottom nav */
|
||||
padding: 0 !important;
|
||||
overflow: hidden !important;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
}
|
||||
.mobile-detail-sheet.open { display: flex !important; }
|
||||
.mobile-sheet-handle { display: none !important; } /* conflicts with browser pull-to-refresh */
|
||||
.mobile-sheet-close {
|
||||
position: absolute !important;
|
||||
top: 6px !important;
|
||||
right: 10px !important;
|
||||
background: var(--surface-2, rgba(0,0,0,0.6)) !important;
|
||||
border-radius: 999px !important;
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px !important;
|
||||
z-index: 20 !important;
|
||||
border: 1px solid var(--border, #444) !important;
|
||||
}
|
||||
.mobile-sheet-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 12px 12px 60px !important;
|
||||
/* visible scrollbar so operator sees there's more */
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.mobile-sheet-content::-webkit-scrollbar { width: 6px; }
|
||||
.mobile-sheet-content::-webkit-scrollbar-thumb { background: var(--border, #555); border-radius: 3px; }
|
||||
/* Sticky in-sheet header for packet identity */
|
||||
.mobile-detail-sheet .detail-title {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--detail-bg);
|
||||
z-index: 5;
|
||||
padding: 6px 36px 8px 0;
|
||||
margin: 0 0 8px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
}
|
||||
.data-table td:has(.spark-bar), .data-table td.col-spark { max-width: none; overflow: visible; min-width: 80px; }
|
||||
.data-table .col-time { min-width: clamp(72px, 8vw, 108px); white-space: nowrap; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user