mirror of
https://github.com/smittix/intercept.git
synced 2026-07-05 16:18:12 -07:00
Improve cross-app UX: accessibility, mode consistency, and render performance
This commit is contained in:
@@ -8,7 +8,7 @@ const CommandPalette = (function() {
|
||||
let activeIndex = 0;
|
||||
let filteredItems = [];
|
||||
|
||||
const modeCommands = [
|
||||
const fallbackModeCommands = [
|
||||
{ mode: 'pager', label: 'Pager' },
|
||||
{ mode: 'sensor', label: '433MHz Sensors' },
|
||||
{ mode: 'rtlamr', label: 'Meters' },
|
||||
@@ -30,6 +30,38 @@ const CommandPalette = (function() {
|
||||
{ mode: 'spaceweather', label: 'Space Weather' },
|
||||
];
|
||||
|
||||
function getModeCommands() {
|
||||
const commands = [];
|
||||
const seenModes = new Set();
|
||||
|
||||
const catalog = window.interceptModeCatalog;
|
||||
if (catalog && typeof catalog === 'object') {
|
||||
for (const [mode, meta] of Object.entries(catalog)) {
|
||||
if (!mode || seenModes.has(mode)) continue;
|
||||
const label = String((meta && meta.label) || mode).trim();
|
||||
commands.push({ mode, label });
|
||||
seenModes.add(mode);
|
||||
}
|
||||
if (commands.length > 0) return commands;
|
||||
}
|
||||
|
||||
const navNodes = document.querySelectorAll('.mode-nav-btn[data-mode], .mobile-nav-btn[data-mode]');
|
||||
navNodes.forEach((node) => {
|
||||
if (node.tagName === 'A') {
|
||||
const href = String(node.getAttribute('href') || '');
|
||||
if (href.includes('/dashboard')) return;
|
||||
}
|
||||
const mode = String(node.dataset.mode || '').trim();
|
||||
if (!mode || seenModes.has(mode)) return;
|
||||
const label = String(node.dataset.modeLabel || node.textContent || mode).trim();
|
||||
commands.push({ mode, label });
|
||||
seenModes.add(mode);
|
||||
});
|
||||
if (commands.length > 0) return commands;
|
||||
|
||||
return fallbackModeCommands.slice();
|
||||
}
|
||||
|
||||
function init() {
|
||||
buildDOM();
|
||||
registerHotkeys();
|
||||
@@ -189,7 +221,7 @@ const CommandPalette = (function() {
|
||||
},
|
||||
];
|
||||
|
||||
for (const modeEntry of modeCommands) {
|
||||
for (const modeEntry of getModeCommands()) {
|
||||
commands.push({
|
||||
title: `Switch Mode: ${modeEntry.label}`,
|
||||
description: 'Navigate directly to mode',
|
||||
|
||||
@@ -3,6 +3,12 @@ const RunState = (function() {
|
||||
|
||||
const REFRESH_MS = 5000;
|
||||
const CHIP_MODES = ['pager', 'sensor', 'wifi', 'bluetooth', 'adsb', 'ais', 'acars', 'vdl2', 'aprs', 'dsc', 'dmr', 'subghz'];
|
||||
const MODE_ALIASES = {
|
||||
bt: 'bluetooth',
|
||||
bt_locate: 'bluetooth',
|
||||
btlocate: 'bluetooth',
|
||||
aircraft: 'adsb',
|
||||
};
|
||||
|
||||
const modeLabels = {
|
||||
pager: 'Pager',
|
||||
@@ -69,7 +75,7 @@ const RunState = (function() {
|
||||
const original = window.switchMode;
|
||||
const wrapped = function(mode) {
|
||||
if (mode) {
|
||||
activeMode = String(mode);
|
||||
activeMode = normalizeMode(String(mode));
|
||||
}
|
||||
const result = original.apply(this, arguments);
|
||||
markActiveChip();
|
||||
@@ -110,7 +116,7 @@ const RunState = (function() {
|
||||
return;
|
||||
}
|
||||
|
||||
const processes = data.processes || {};
|
||||
const processes = normalizeProcesses(data.processes || {});
|
||||
for (const mode of CHIP_MODES) {
|
||||
const isRunning = Boolean(processes[mode]);
|
||||
chipsContainer.appendChild(buildChip(modeLabels[mode] || mode.toUpperCase(), isRunning, mode));
|
||||
@@ -146,7 +152,7 @@ const RunState = (function() {
|
||||
|
||||
document.querySelectorAll('#runStateChips .run-state-chip').forEach((chip) => {
|
||||
chip.classList.remove('active');
|
||||
if (chip.dataset.mode && chip.dataset.mode === activeMode) {
|
||||
if (chip.dataset.mode && chip.dataset.mode === normalizeMode(activeMode)) {
|
||||
chip.classList.add('active');
|
||||
}
|
||||
});
|
||||
@@ -154,7 +160,11 @@ const RunState = (function() {
|
||||
|
||||
function inferCurrentMode() {
|
||||
const modeParam = new URLSearchParams(window.location.search).get('mode');
|
||||
if (modeParam) return modeParam;
|
||||
if (modeParam) return normalizeMode(modeParam);
|
||||
|
||||
if (typeof window.currentMode === 'string' && window.currentMode) {
|
||||
return normalizeMode(window.currentMode);
|
||||
}
|
||||
|
||||
const indicator = document.getElementById('activeModeIndicator');
|
||||
if (!indicator) return 'pager';
|
||||
@@ -163,6 +173,7 @@ const RunState = (function() {
|
||||
const normalized = text.toLowerCase();
|
||||
if (normalized.includes('wifi')) return 'wifi';
|
||||
if (normalized.includes('bluetooth')) return 'bluetooth';
|
||||
if (normalized.includes('bt locate')) return 'bluetooth';
|
||||
if (normalized.includes('ads-b')) return 'adsb';
|
||||
if (normalized.includes('ais')) return 'ais';
|
||||
if (normalized.includes('acars')) return 'acars';
|
||||
@@ -175,6 +186,29 @@ const RunState = (function() {
|
||||
return 'pager';
|
||||
}
|
||||
|
||||
function normalizeMode(mode) {
|
||||
const value = String(mode || '').trim().toLowerCase();
|
||||
if (!value) return 'pager';
|
||||
return MODE_ALIASES[value] || value;
|
||||
}
|
||||
|
||||
function normalizeProcesses(raw) {
|
||||
const processes = Object.assign({}, raw || {});
|
||||
processes.bluetooth = Boolean(
|
||||
processes.bluetooth ||
|
||||
processes.bt ||
|
||||
processes.bt_scan ||
|
||||
processes.btlocate ||
|
||||
processes.bt_locate
|
||||
);
|
||||
processes.wifi = Boolean(
|
||||
processes.wifi ||
|
||||
processes.wifi_scan ||
|
||||
processes.wlan
|
||||
);
|
||||
return processes;
|
||||
}
|
||||
|
||||
function extractMessage(err) {
|
||||
if (!err) return 'Unknown error';
|
||||
if (typeof err === 'string') return err;
|
||||
|
||||
@@ -435,10 +435,16 @@ const Settings = {
|
||||
};
|
||||
|
||||
// Settings modal functions
|
||||
let lastSettingsFocusEl = null;
|
||||
|
||||
function showSettings() {
|
||||
const modal = document.getElementById('settingsModal');
|
||||
if (modal) {
|
||||
lastSettingsFocusEl = document.activeElement;
|
||||
modal.classList.add('active');
|
||||
modal.setAttribute('aria-hidden', 'false');
|
||||
const content = modal.querySelector('.settings-content');
|
||||
if (content) content.focus();
|
||||
Settings.init().then(() => {
|
||||
Settings.checkAssets();
|
||||
});
|
||||
@@ -449,18 +455,27 @@ function hideSettings() {
|
||||
const modal = document.getElementById('settingsModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('active');
|
||||
modal.setAttribute('aria-hidden', 'true');
|
||||
if (lastSettingsFocusEl && typeof lastSettingsFocusEl.focus === 'function') {
|
||||
lastSettingsFocusEl.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function switchSettingsTab(tabName) {
|
||||
// Update tab buttons
|
||||
document.querySelectorAll('.settings-tab').forEach(tab => {
|
||||
tab.classList.toggle('active', tab.dataset.tab === tabName);
|
||||
const isActive = tab.dataset.tab === tabName;
|
||||
tab.classList.toggle('active', isActive);
|
||||
tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
||||
});
|
||||
|
||||
// Update sections
|
||||
document.querySelectorAll('.settings-section').forEach(section => {
|
||||
section.classList.toggle('active', section.id === `settings-${tabName}`);
|
||||
const isActive = section.id === `settings-${tabName}`;
|
||||
section.classList.toggle('active', isActive);
|
||||
section.hidden = !isActive;
|
||||
section.setAttribute('role', 'tabpanel');
|
||||
});
|
||||
|
||||
// Load tools/dependencies when that tab is selected
|
||||
@@ -560,6 +575,7 @@ function loadSettingsTools() {
|
||||
// Initialize settings on page load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
Settings.init();
|
||||
switchSettingsTab('offline');
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
@@ -919,12 +935,17 @@ const _originalSwitchSettingsTab = typeof switchSettingsTab !== 'undefined' ? sw
|
||||
function switchSettingsTab(tabName) {
|
||||
// Update tab buttons
|
||||
document.querySelectorAll('.settings-tab').forEach(tab => {
|
||||
tab.classList.toggle('active', tab.dataset.tab === tabName);
|
||||
const isActive = tab.dataset.tab === tabName;
|
||||
tab.classList.toggle('active', isActive);
|
||||
tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
||||
});
|
||||
|
||||
// Update sections
|
||||
document.querySelectorAll('.settings-section').forEach(section => {
|
||||
section.classList.toggle('active', section.id === `settings-${tabName}`);
|
||||
const isActive = section.id === `settings-${tabName}`;
|
||||
section.classList.toggle('active', isActive);
|
||||
section.hidden = !isActive;
|
||||
section.setAttribute('role', 'tabpanel');
|
||||
});
|
||||
|
||||
// Load content based on tab
|
||||
@@ -1026,3 +1047,14 @@ function setAnimationsEnabled(enabled) {
|
||||
}
|
||||
localStorage.setItem('intercept-animations', enabled ? 'on' : 'off');
|
||||
}
|
||||
|
||||
if (!window._settingsEscapeHandlerBound) {
|
||||
window._settingsEscapeHandlerBound = true;
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key !== 'Escape') return;
|
||||
const modal = document.getElementById('settingsModal');
|
||||
if (modal && modal.classList.contains('active')) {
|
||||
hideSettings();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -177,6 +177,37 @@ const AppFeedback = (function() {
|
||||
return text.includes('script error') || text.includes('resizeobserver loop limit exceeded');
|
||||
}
|
||||
|
||||
function renderCollectionState(container, options) {
|
||||
if (!container) return null;
|
||||
const opts = options || {};
|
||||
const type = String(opts.type || 'empty').toLowerCase();
|
||||
const message = String(opts.message || (type === 'loading' ? 'Loading...' : 'No data available'));
|
||||
const className = opts.className || `app-collection-state is-${type}`;
|
||||
|
||||
container.innerHTML = '';
|
||||
|
||||
if (container.tagName === 'TBODY') {
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'app-collection-state-row';
|
||||
const cell = document.createElement('td');
|
||||
const columns = Number.isFinite(opts.columns) ? opts.columns : 1;
|
||||
cell.colSpan = Math.max(1, columns);
|
||||
const state = document.createElement('div');
|
||||
state.className = className;
|
||||
state.textContent = message;
|
||||
cell.appendChild(state);
|
||||
row.appendChild(cell);
|
||||
container.appendChild(row);
|
||||
return row;
|
||||
}
|
||||
|
||||
const state = document.createElement('div');
|
||||
state.className = className;
|
||||
state.textContent = message;
|
||||
container.appendChild(state);
|
||||
return state;
|
||||
}
|
||||
|
||||
function isNetworkError(message) {
|
||||
const text = String(message || '').toLowerCase();
|
||||
return text.includes('networkerror') || text.includes('failed to fetch') || text.includes('timeout');
|
||||
@@ -192,6 +223,7 @@ const AppFeedback = (function() {
|
||||
toast,
|
||||
reportError,
|
||||
removeToast,
|
||||
renderCollectionState,
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -207,6 +239,10 @@ window.reportActionableError = function(context, error, options) {
|
||||
return AppFeedback.reportError(context, error, options);
|
||||
};
|
||||
|
||||
window.renderCollectionState = function(container, options) {
|
||||
return AppFeedback.renderCollectionState(container, options);
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
AppFeedback.init();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user