Browse Source

feat(ui): redesign startup flow with Foxglove-style SPA loading screen

This commit refactors the initialization phase of the Visualizer to act as a Single Page Application (SPA). A persistent workspace shell is now initially hidden behind a "Data Synchronizer" Start Screen, which handles file ingestion before seamlessly revealing the canvas.

Part 1: Implement SPA Start Screen Modal and Data Routing
* steps/index.html: Added the full-screen `start-screen-modal` overlay to intercept the user on load. Removed text placeholders inside the video and canvas containers since the workspace is now exclusively visible when populated.
* steps/src/dom.js: Exported references for the new modal, drop zone, and primary load buttons.
* steps/src/main.js: Routed standard drag-and-drop and click events to target the new Start Screen elements rather than the legacy footer buttons, initiating the existing file pipeline.

Part 2: Integrate Loading Progress Natively
* steps/index.html: Added a hidden `start-progress-container` directly inside the Start Screen card to prevent overlapping popups.
* steps/src/dom.js: Exported references to the native progress bar and text elements.
* steps/src/modal.js: Refactored `showLoadingModal`, `updateLoadingModal`, and `hideModal` to dynamically route progress updates. If the Start Screen is active, it renders the inline progress bar and disables background buttons rather than spawning the generic popup modal.
* steps/src/fileLoader.js: Updated `processFilePipeline` to fully dismiss the `start-screen-modal` upon successful data ingestion and precomputation, seamlessly transitioning the user.

Part 3: Replicate Accessibility and Theme Controls
* steps/index.html: Duplicated the Quick Start Guide, Codebase Overview, What's New, and Theme Toggle buttons, positioning them in the top-right corner of the Start Screen. Also fixed a `-webkit-appearance` CSS warning on the range slider.
* steps/src/dom.js: Exported references for the new accessibility buttons and theme toggle SVGs.
* steps/src/ui.js: Attached existing `toggleGuideModal`, `toggleCodebaseModal`, and `toggleChangelogModal` event listeners to the new replicated buttons, granting users access to documentation before data load.
* steps/src/theme.js: Updated `initializeTheme()` so switching the theme from either the workspace or the Start Screen bidirectionally syncs the Light/Dark SVGs on both buttons simultaneously.
refactor/sync-centralize
RUSHIL AMBARISH KADU 2 months ago
parent
commit
4d8c03ffe2
  1. 70
      steps/index.html
  2. 16
      steps/src/dom.js
  3. 8
      steps/src/fileLoader.js
  4. 44
      steps/src/main.js
  5. 45
      steps/src/modal.js
  6. 14
      steps/src/theme.js
  7. 15
      steps/src/ui.js

70
steps/index.html

@ -223,6 +223,70 @@
</head>
<body class="bg-gray-100 dark:bg-gray-900 text-gray-800 dark:text-gray-200 flex flex-col min-h-screen">
<!-- START SCREEN MODAL -->
<div id="start-screen-modal" class="fixed inset-0 z-50 flex items-center justify-center bg-gray-100 dark:bg-gray-900 transition-opacity duration-300">
<!-- Header buttons on start screen -->
<div class="absolute top-4 right-4 flex items-center gap-2">
<button id="start-user-manual-btn" type="button"
class="bg-green-100 dark:bg-green-700 text-green-800 dark:text-green-100 border border-green-300 dark:border-green-600 shadow-sm hover:bg-green-200 dark:hover:bg-green-600 active:scale-95 active:shadow-inner font-medium rounded-lg text-xs px-4 py-2 transition-all text-center">
Quick-Start<br>Guide
</button>
<button id="start-codebase-btn" type="button"
class="bg-purple-100 dark:bg-purple-700 text-purple-800 dark:text-purple-100 border border-purple-300 dark:border-purple-600 shadow-sm hover:bg-purple-200 dark:hover:bg-purple-600 active:scale-95 active:shadow-inner font-medium rounded-lg text-xs px-4 py-2 transition-all text-center">
Codebase<br>Overview
</button>
<button id="start-changelog-btn" type="button"
class="bg-cyan-100 dark:bg-cyan-700 text-cyan-800 dark:text-cyan-100 border border-cyan-300 dark:border-cyan-600 shadow-sm hover:bg-cyan-200 dark:hover:bg-cyan-600 active:scale-95 active:shadow-inner font-medium rounded-lg text-xs px-4 py-2 transition-all text-center">
What's<br>New?
</button>
<button id="start-theme-toggle" type="button" class="bg-indigo-900 dark:bg-amber-100 text-indigo-100 dark:text-amber-800 border border-indigo-700 dark:border-amber-300 shadow-sm hover:bg-indigo-800 dark:hover:bg-amber-200 active:scale-95 active:shadow-inner flex flex-row items-center gap-2 rounded-lg text-xs px-4 py-3.5 transition-all">
<svg id="start-theme-toggle-dark-icon" class="hidden w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
</svg>
<svg id="start-theme-toggle-light-icon" class="hidden w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm2 0a1 1 0 112 0v-1a1 1 0 00-2 0v1zm-14 0a1 1 0 112 0v-1a1 1 0 00-2 0v1zm8 6a1 1 0 100 2 1 1 0 000-2zm5.657-11.657a1 1 0 010 1.414l-1.414 1.414a1 1 0 11-1.414-1.414l1.414-1.414a1 1 0 011.414 0zm-11.314 0a1 1 0 011.414 0l1.414 1.414a1 1 0 11-1.414 1.414l-1.414-1.414a1 1 0 010-1.414zm11.314 11.314a1 1 0 01-1.414 0l-1.414-1.414a1 1 0 011.414-1.414l1.414 1.414a1 1 0 010 1.414zm-11.314 0a1 1 0 010-1.414l1.414-1.414a1 1 0 011.414 1.414l-1.414 1.414a1 1 0 01-1.414 0z" fill-rule="evenodd" clip-rule="evenodd"></path>
</svg>
</button>
</div>
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-[0_0_50px_rgba(0,0,0,0.2)] p-10 w-full max-w-2xl text-center border border-gray-200 dark:border-gray-700">
<h1 class="text-4xl font-bold text-gray-900 dark:text-white mb-2 tracking-tight">Data Synchronizer</h1>
<p class="text-gray-500 dark:text-gray-400 mb-8 text-lg">Provide radar dataset and video parameters to initialize workspace</p>
<div id="start-drop-zone" class="border-4 border-dashed border-gray-300 dark:border-gray-600 rounded-xl p-12 mb-8 hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-gray-700 transition-colors cursor-pointer group">
<svg class="mx-auto h-16 w-16 text-gray-400 group-hover:text-blue-500 transition-colors mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<p class="text-xl font-medium text-gray-700 dark:text-gray-300">Drag & Drop files here</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-2">Requires .json (and optionally video) format</p>
</div>
<div class="flex items-center justify-center gap-6">
<button id="start-load-json-btn" class="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition-all text-lg font-semibold w-48 shadow-lg active:scale-95 active:shadow-inner flex items-center justify-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
Select JSON
</button>
<button id="start-load-video-btn" class="bg-green-600 text-white px-6 py-3 rounded-lg hover:bg-green-700 transition-all text-lg font-semibold w-48 shadow-lg active:scale-95 active:shadow-inner flex items-center justify-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
Select Video
</button>
</div>
<div class="mt-8 text-sm text-gray-500 dark:text-gray-400">
<button id="start-clear-cache-btn" class="hover:text-red-500 dark:hover:text-red-400 transition-colors uppercase tracking-wider text-xs font-bold">Clear Cached Session</button>
</div>
<!-- INTEGRATED LOADER UI (Hidden by default) -->
<div id="start-progress-container" class="hidden mt-6 w-full max-w-md mx-auto">
<div class="w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700">
<div id="start-progress-bar" class="bg-blue-600 h-2.5 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
<div id="start-progress-text" class="text-sm font-medium text-gray-700 dark:text-gray-300 mt-2 text-center animate-pulse">Initializing...</div>
</div>
</div>
</div>
<header class="bg-white dark:bg-gray-800 shadow-md pt-0 px-4 pb-4 z-20 relative">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
Radar and Video Synchronizer
@ -399,7 +463,7 @@
<div class="relative">
<div id="canvas-container"
class="w-full h-[75vh] bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg shadow-inner flex items-center justify-center relative">
<p id="canvas-placeholder" class="text-gray-500 dark:text-gray-400 text-lg">Load JSON data to start
<p id="canvas-placeholder" class="hidden text-gray-500 dark:text-gray-400 text-lg">Load JSON data to start
visualization</p>
<!-- Range Slider (Vertical) -->
@ -407,7 +471,7 @@
<span id="range-value-display" class="bg-black bg-opacity-60 text-white text-[10px] px-1.5 py-0.5 rounded font-mono">80m</span>
<input type="range" id="range-slider" min="40" max="200" value="80" step="10"
class="h-32 cursor-pointer accent-blue-600"
style="writing-mode: bt-lr; -webkit-appearance: slider-vertical; width: 8px;" />
style="writing-mode: bt-lr; -webkit-appearance: slider-vertical; appearance: slider-vertical; width: 8px;" />
<span class="text-[10px] font-bold text-gray-400 uppercase tracking-tighter" style="writing-mode: vertical-lr; transform: rotate(180deg);">Range</span>
</div>
</div>
@ -425,7 +489,7 @@
<div class="lg:w-1/2 flex flex-col gap-4">
<div class="w-full h-[50vh] bg-black rounded-lg shadow-inner flex items-center justify-center relative">
<video id="video-player" class="w-full h-full object-contain hidden" muted playsinline></video>
<p id="video-placeholder" class="text-gray-500 dark:text-gray-400 text-lg">Load a video file</p>
<p id="video-placeholder" class="hidden text-gray-500 dark:text-gray-400 text-lg">Load a video file</p>
<div id="video-info-overlay"
class="absolute top-1 left-2 z-10 bg-black bg-opacity-60 text-white font-mono text-xs p-2 rounded-md hidden">
</div>

16
steps/src/dom.js

@ -23,6 +23,22 @@ function getTimingColor(diffMs) {
// --- DOM Element References --- //
export const startScreenModal = document.getElementById("start-screen-modal");
export const startDropZone = document.getElementById("start-drop-zone");
export const startLoadJsonBtn = document.getElementById("start-load-json-btn");
export const startLoadVideoBtn = document.getElementById("start-load-video-btn");
export const startClearCacheBtn = document.getElementById("start-clear-cache-btn");
export const startProgressContainer = document.getElementById("start-progress-container");
export const startProgressBar = document.getElementById("start-progress-bar");
export const startProgressText = document.getElementById("start-progress-text");
export const startUserManualBtn = document.getElementById("start-user-manual-btn");
export const startCodebaseBtn = document.getElementById("start-codebase-btn");
export const startChangelogBtn = document.getElementById("start-changelog-btn");
export const startThemeToggleBtn = document.getElementById("start-theme-toggle");
export const startThemeToggleDarkIcon = document.getElementById("start-theme-toggle-dark-icon");
export const startThemeToggleLightIcon = document.getElementById("start-theme-toggle-light-icon");
export const themeToggleBtn = document.getElementById("theme-toggle");
export const canvasContainer = document.getElementById("canvas-container");
export const canvasPlaceholder = document.getElementById("canvas-placeholder");

8
steps/src/fileLoader.js

@ -31,6 +31,7 @@ import {
updatePersistentOverlays,
updateDebugOverlay,
resetUIForNewLoad,
startScreenModal,
} from "./dom.js";
import { forceResyncWithOffset } from "./sync.js";
@ -164,11 +165,12 @@ async function processFilePipeline(jsonFile, videoFile, fromCache) {
// --- PART F: Finalize UI ---
finalizeSetup();
// Hide modal only if the video didn't fail. If it failed, the video
// loader has already handled showing an error/choice modal.
if (!appState.videoMissing) {
updateLoadingModal(100, "Complete!");
setTimeout(hideModal, 300);
setTimeout(() => {
hideModal();
startScreenModal.classList.add("hidden");
}, 300);
}
// Log the results of the non-blocking cache operations once they complete.

44
steps/src/main.js

@ -53,6 +53,11 @@ import {
shortcutsModal,
shortcutsModalCloseBtn,
guideModalCloseBtn,
startScreenModal,
startDropZone,
startLoadJsonBtn,
startLoadVideoBtn,
startClearCacheBtn,
} from "./dom.js";
import { initializeTheme } from "./theme.js";
@ -69,23 +74,35 @@ videoFileInput.addEventListener("change", (event) =>
handleFiles(event.target.files)
);
// Wire up the drag-and-drop functionality
const dropZone = document.querySelector("main");
dropZone.addEventListener("dragover", (event) => {
// Wire up the drag-and-drop functionality for the start screen
startDropZone.addEventListener("dragover", (event) => {
event.preventDefault();
dropZone.style.border = "2px dashed #3b82f6";
startDropZone.classList.add("border-blue-500", "bg-blue-50", "dark:bg-gray-700");
});
dropZone.addEventListener("dragleave", () => {
dropZone.style.border = "none";
startDropZone.addEventListener("dragleave", () => {
startDropZone.classList.remove("border-blue-500", "bg-blue-50", "dark:bg-gray-700");
});
dropZone.addEventListener("drop", (event) => {
startDropZone.addEventListener("drop", (event) => {
event.preventDefault();
dropZone.style.border = "none";
startDropZone.classList.remove("border-blue-500", "bg-blue-50", "dark:bg-gray-700");
handleFiles(event.dataTransfer.files);
});
// Also keep the main body as a backup drop zone for modifying active sessions
const mainDropZone = document.querySelector("main");
mainDropZone.addEventListener("dragover", (event) => {
event.preventDefault();
});
mainDropZone.addEventListener("drop", (event) => {
event.preventDefault();
handleFiles(event.dataTransfer.files);
});
// Event listeners for loading files (Start Screen)
startLoadJsonBtn.addEventListener("click", () => jsonFileInput.click());
startLoadVideoBtn.addEventListener("click", () => videoFileInput.click());
// Event listener for loading JSON file.
// Event listeners for loading files (Workspace Footer - Legacy)
loadJsonBtn.addEventListener("click", () => jsonFileInput.click());
loadVideoBtn.addEventListener("click", () => videoFileInput.click());
@ -98,6 +115,15 @@ clearCacheBtn.addEventListener("click", async () => {
}
});
startClearCacheBtn.addEventListener("click", async () => {
const confirmed = await showModal("Clear all cached data and reload?", true);
if (confirmed) {
indexedDB.deleteDatabase("visualizerDB");
localStorage.clear();
window.location.reload();
}
});
// Event listener for offset input change.
offsetInput.addEventListener("input", () => {
autoOffsetIndicator.classList.add("hidden");

45
steps/src/modal.js

@ -8,6 +8,13 @@ import {
modalProgressContainer,
modalProgressBar,
modalProgressText,
startScreenModal,
startProgressContainer,
startProgressBar,
startProgressText,
startDropZone,
startLoadJsonBtn,
startLoadVideoBtn,
} from "./dom.js";
let modalResolve = null;
@ -38,7 +45,19 @@ export function showModal(
}
// A new function specifically for the loading modal
export function showLoadingModal(message) {
export function showLoadingModal(message, forcePopup = false) {
if (!startScreenModal.classList.contains('hidden') && !forcePopup) {
// Integrated start screen flow
startProgressContainer.classList.remove('hidden');
startProgressBar.style.width = '0%';
startProgressText.textContent = message;
// Optionally disable the interactive elements so users don't multi-click
startDropZone.classList.add('opacity-50', 'pointer-events-none');
startLoadJsonBtn.classList.add('opacity-50', 'pointer-events-none');
startLoadVideoBtn.classList.add('opacity-50', 'pointer-events-none');
} else {
// Fallback generic popup modal flow
modalText.textContent = message;
modalOkBtn.classList.add('hidden'); // Hide OK button for loading
modalCancelBtn.classList.add('hidden'); // Initially hide cancel button
@ -51,12 +70,21 @@ export function showLoadingModal(message) {
modalOverlay.classList.remove("opacity-0");
modalContent.classList.remove("scale-95");
}, 10);
}
}
// A new function to update the progress bar and text
export function updateLoadingModal(percent, message) {
if (modalProgressBar && modalProgressText) {
const p = Math.max(0, Math.min(100, Math.round(percent))); // Clamp between 0-100
// Update integrated start screen loader if active
if (!startProgressContainer.classList.contains('hidden')) {
startProgressBar.style.width = `${p}%`;
startProgressText.textContent = message;
}
// Update generic modal loader if active
if (!modalProgressContainer.classList.contains('hidden')) {
modalProgressBar.style.width = `${p}%`;
modalProgressText.textContent = message;
}
@ -64,7 +92,7 @@ export function updateLoadingModal(percent, message) {
export function runStartupLoader(durationMs = 10000) {
return new Promise((resolve, reject) => {
showLoadingModal("Opening Quick Start Guide...");
showLoadingModal("Opening Quick Start Guide...", true);
modalCancelBtn.textContent = "Skip Guide";
modalCancelBtn.classList.remove("hidden"); // Show cancel button for startup loader
@ -103,6 +131,17 @@ export function runStartupLoader(durationMs = 10000) {
// The hideModal function now also resets the progress bar
export function hideModal(value) { // This now returns a promise
return new Promise(resolve => {
// Hide the Integrated Loading elements if active
if (!startProgressContainer.classList.contains('hidden')) {
startProgressContainer.classList.add('hidden');
startProgressBar.style.width = '0%';
startProgressText.textContent = '';
startDropZone.classList.remove('opacity-50', 'pointer-events-none');
startLoadJsonBtn.classList.remove('opacity-50', 'pointer-events-none');
startLoadVideoBtn.classList.remove('opacity-50', 'pointer-events-none');
}
modalOverlay.classList.add("opacity-0");
modalContent.classList.add("scale-95");
setTimeout(() => {

14
steps/src/theme.js

@ -1,5 +1,5 @@
import { appState } from "./state.js";
import { videoPlayer, themeToggleBtn} from "./dom.js";
import { videoPlayer, themeToggleBtn, startThemeToggleBtn, startThemeToggleDarkIcon, startThemeToggleLightIcon } from "./dom.js";
const darkIcon = document.getElementById("theme-toggle-dark-icon");
const lightIcon = document.getElementById("theme-toggle-light-icon");
@ -8,11 +8,15 @@ function setTheme(theme) {
document.documentElement.classList.add("dark");
lightIcon.classList.remove("hidden");
darkIcon.classList.add("hidden");
if (startThemeToggleLightIcon) startThemeToggleLightIcon.classList.remove("hidden");
if (startThemeToggleDarkIcon) startThemeToggleDarkIcon.classList.add("hidden");
localStorage.setItem("color-theme", "dark");
} else {
document.documentElement.classList.remove("dark");
darkIcon.classList.remove("hidden");
lightIcon.classList.add("hidden");
if (startThemeToggleDarkIcon) startThemeToggleDarkIcon.classList.remove("hidden");
if (startThemeToggleLightIcon) startThemeToggleLightIcon.classList.add("hidden");
localStorage.setItem("color-theme", "light");
}
@ -62,4 +66,12 @@ export function initializeTheme() {
setTheme("dark");
}
});
startThemeToggleBtn.addEventListener("click", () => {
if (document.documentElement.classList.contains("dark")) {
setTheme("light");
} else {
setTheme("dark");
}
});
}

15
steps/src/ui.js

@ -45,6 +45,9 @@ import {
changelogBtn,
changelogModal,
changelogModalCloseBtn,
startUserManualBtn,
startCodebaseBtn,
startChangelogBtn,
} from "./dom.js";
function toggleMenu(show) {
@ -134,6 +137,10 @@ export function initUIEventListeners() {
e.preventDefault();
toggleGuideModal(true);
});
startUserManualBtn.addEventListener("click", (e) => {
e.preventDefault();
toggleGuideModal(true);
});
guideModalCloseBtn.addEventListener("click", () => toggleGuideModal(false));
guideModal.addEventListener("click", (e) => {
if (e.target === guideModal) {
@ -146,6 +153,10 @@ export function initUIEventListeners() {
e.preventDefault();
toggleCodebaseModal(true);
});
startCodebaseBtn.addEventListener("click", (e) => {
e.preventDefault();
toggleCodebaseModal(true);
});
codebaseModalCloseBtn.addEventListener("click", () => toggleCodebaseModal(false));
codebaseModal.addEventListener("click", (e) => {
if (e.target === codebaseModal) {
@ -158,6 +169,10 @@ export function initUIEventListeners() {
e.preventDefault();
toggleChangelogModal(true);
});
startChangelogBtn.addEventListener("click", (e) => {
e.preventDefault();
toggleChangelogModal(true);
});
changelogModalCloseBtn.addEventListener("click", () => toggleChangelogModal(false));
changelogModal.addEventListener("click", (e) => {
if (e.target === changelogModal) {

Loading…
Cancel
Save