feat(desktop): hide header strip, splash screen, native locale switcher

Three QoL fixes for the Tauri shell:

  1. Hide the entire AppHeader strip in VITE_DESKTOP, not just the
     marketing nav. The previous gate left the black bar painting
     over the editor with the brand + auto-save + share + auth
     slot, all of which are irrelevant in desktop (cloud Pro
     features, license is handled by DesktopWelcomePage, the title
     bar already says "Velxio Desktop"). Return null at the top so
     the editor takes the full window height.

  2. Splash screen during sidecar boot + Monaco hydration. Cold
     launch was a 3-8 s black window — now there's an inline SVG
     logo, "Velxio" wordmark, slogan, animated spinner, and a
     "Starting local backend…" caption. Lives in index.html as a
     fixed-position overlay with display:none by default; the inline
     script reveals it only when `window.__TAURI__` is present, so
     web users never see it. main.tsx fades it out (250 ms ease-out)
     after two animation frames — guarantees React's first paint has
     committed before the handoff, no black flash. Self-contained:
     inline styles, inline SVG, inline CSS keyframes, zero external
     requests.

  3. Native locale switcher under View → Language. Emits
     `velxio://menu` with action='set-locale' + the locale code; the
     desktop/menu.ts handler navigates via history.pushState +
     popstate so React Router picks it up without a hard reload
     (Monaco + simulator state preserved). Locale list mirrors
     i18n/config.ts::LOCALES.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
davidmonterocrespo24 2026-05-22 19:39:10 -03:00
parent 8c58d2a1a7
commit 2146b09c29
4 changed files with 97 additions and 3 deletions

View File

@ -205,6 +205,40 @@
#root-seo { visibility: hidden; position: absolute; pointer-events: none; }
</style>
<body style="margin:0;background:#1e1e1e;">
<!--
Desktop-only splash. Sidecar boot + Monaco hydration take 3-8s
on a cold launch; without this the user stares at a black window.
Hidden by default and revealed by the inline script below only
when window.__TAURI__ is present (Tauri webview), so web users
never see it. Removed from the DOM when React's first render
lands (main.tsx).
Self-contained: inline SVG logo + CSS keyframes, no external
requests. Safe to keep in OSS since the gate is the Tauri
global, not VITE_DESKTOP.
-->
<div id="velxio-splash" aria-hidden="true" style="display:none;position:fixed;inset:0;z-index:2147483647;background:linear-gradient(160deg,#0d1117 0%,#161b22 50%,#0d1117 100%);color:#e6edf3;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;align-items:center;justify-content:center;flex-direction:column;gap:24px;">
<svg width="96" height="96" viewBox="0 0 24 24" fill="none" stroke="#58a6ff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="filter:drop-shadow(0 0 18px rgba(88,166,255,0.35));">
<rect x="5" y="5" width="14" height="14" rx="2"></rect>
<rect x="9" y="9" width="6" height="6"></rect>
<path d="M9 1v4M15 1v4M9 19v4M15 19v4M1 9h4M1 15h4M19 9h4M19 15h4"></path>
</svg>
<div style="font-size:32px;font-weight:600;letter-spacing:0.5px;">Velxio</div>
<div style="font-size:14px;color:#8b949e;max-width:340px;text-align:center;line-height:1.5;">Offline Arduino, RP2040 &amp; ESP32 simulator</div>
<div style="width:32px;height:32px;border:3px solid rgba(88,166,255,0.18);border-top-color:#58a6ff;border-radius:50%;animation:velxio-spin 0.9s linear infinite;margin-top:8px;"></div>
<div style="position:absolute;bottom:24px;left:0;right:0;text-align:center;font-size:11px;color:#484f58;">Starting local backend…</div>
<style>@keyframes velxio-spin{to{transform:rotate(360deg)}}</style>
</div>
<script>
// Reveal the splash ONLY inside Tauri. Detection is synchronous:
// `withGlobalTauri: true` in tauri.conf.json injects __TAURI__
// before page JS runs. Web users land here with __TAURI__
// undefined and the splash stays display:none.
if (typeof window !== 'undefined' && window.__TAURI__) {
var s = document.getElementById('velxio-splash');
if (s) s.style.display = 'flex';
}
</script>
<div id="root"></div>
<!--
Static pre-rendered content for search engine crawlers.

View File

@ -76,6 +76,18 @@ export const AppHeader: React.FC<AppHeaderProps> = ({ autoSave }) => {
setMenuOpen(false);
}, [location.pathname]);
// Tauri desktop: skip the header entirely. The marketing nav was
// already hidden, but the strip itself was still painting an empty
// black bar over the editor. Brand/auto-save/share/auth-slot all
// live elsewhere in desktop: title bar shows "Velxio Desktop", the
// native menubar has File/Edit/View/Help, auto-save is a Pro cloud
// feature (desktop saves to .vlx), share generates a velxio.dev URL
// that doesn't apply to a desktop session, and the license flow
// owns its own DesktopWelcomePage.
if (import.meta.env.VITE_DESKTOP) {
return null;
}
const isActive = (path: string) =>
location.pathname === localize(path) ? ' header-nav-link-active' : '';

View File

@ -26,6 +26,8 @@ import { listen } from './tauriBridge';
import { dlog } from './log';
import { triggerDownloadVlx, importVlxFile } from '../utils/vlxFile';
import { useSimulatorStore } from '../store/useSimulatorStore';
import { switchLocale } from '../i18n/path';
import { LOCALES, type Locale } from '../i18n/config';
type MenuAction =
| 'new-project'
@ -34,10 +36,14 @@ type MenuAction =
| 'find-in-editor'
| 'toggle-file-explorer'
| 'toggle-serial-monitor'
| 'check-for-updates';
| 'check-for-updates'
| 'set-locale';
interface MenuEventPayload {
action: MenuAction;
// Only present when action='set-locale'. Matches an entry in
// i18n/config.ts::LOCALES.
locale?: string;
}
let installed = false;
@ -47,11 +53,11 @@ export async function installDesktopMenuListener(): Promise<void> {
installed = true;
await listen<MenuEventPayload>('velxio://menu', (event) => {
dlog('menu event', event.payload);
void handle(event.payload.action);
void handle(event.payload.action, event.payload);
});
}
async function handle(action: MenuAction): Promise<void> {
async function handle(action: MenuAction, payload?: MenuEventPayload): Promise<void> {
switch (action) {
case 'save-vlx':
triggerDownloadVlx();
@ -70,9 +76,34 @@ async function handle(action: MenuAction): Promise<void> {
case 'check-for-updates':
await checkForUpdates();
return;
case 'set-locale':
if (payload?.locale) setLocale(payload.locale);
return;
}
}
function setLocale(locale: string): void {
// Defensive: ignore unknown locales coming from the menu so a
// stale shell doesn't navigate to a broken URL.
if (!(LOCALES as readonly string[]).includes(locale)) {
dlog('set-locale: ignoring unknown locale', { locale });
return;
}
const target = locale as Locale;
const next =
switchLocale(window.location.pathname, target) +
window.location.search +
window.location.hash;
if (next === window.location.pathname + window.location.search + window.location.hash) {
return;
}
// history.pushState + popstate lets React Router pick the change up
// without a full reload, preserving the editor state. Reload would
// re-spawn the sidecar handshake and lose Monaco/sim state for ~5s.
window.history.pushState(null, '', next);
window.dispatchEvent(new PopStateEvent('popstate'));
}
function pickAndImportVlx(): void {
const input = document.createElement('input');
input.type = 'file';

View File

@ -25,6 +25,23 @@ loader.config({ paths: { vs: monacoVsPath } });
createRoot(document.getElementById('root')!).render(<App />);
// Tear down the Tauri-only splash now that React has mounted. Wait
// two animation frames so React's first paint commits before we
// touch the splash — otherwise users see a black flash between the
// splash fading and the editor first appearing. Fade via CSS
// transition for a smoother handoff, then remove the node entirely
// once the transition finishes.
requestAnimationFrame(() => {
requestAnimationFrame(() => {
const splash = document.getElementById('velxio-splash');
if (!splash) return;
splash.style.transition = 'opacity 250ms ease-out';
splash.style.opacity = '0';
splash.style.pointerEvents = 'none';
window.setTimeout(() => splash.remove(), 320);
});
});
// Optional pro overlay. The `@pro` import resolves to a no-op stub in the
// open-source build (see vite.config.ts) and to the real overlay only when
// VITE_PRO_BUILD=true at build time. The dynamic import keeps the pro chunk