import { useState, useEffect, useLayoutEffect, useRef } from 'react'; import { Link, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useProjectStore } from '../../store/useProjectStore'; import { ShareModal } from './ShareModal'; import { LanguageSwitcher } from './LanguageSwitcher'; import { useLocalizedHref, useCurrentLocale } from '../../i18n/useLocalizedNavigate'; import { blogUrlFor } from '../../i18n/path'; import { trackVisitGitHub, trackVisitDiscord } from '../../utils/analytics'; import { applyStripLayout, STRIP_BELOW_CLASS } from './headerStripFit'; import './LanguageSwitcher.css'; const GITHUB_URL = 'https://github.com/davidmonterocrespo24/velxio'; const DISCORD_URL = 'https://discord.gg/3mARjJrh4E'; interface AppHeaderProps { /** Editor variant: a File/Edit menu bar rendered next to the logo. When * set, the marketing nav links (Home / Docs / Pricing / …) are hidden — * inside the editor they are noise that costs exactly the width the * toolbar is starved of on small screens; the logo still links home. * Same mechanism the Tauri desktop build uses (VITE_DESKTOP). */ editorMenu?: React.ReactNode; /** Editor variant: the unified toolbar strip rendered in the header's * middle — the space the marketing nav used to occupy. One row instead * of header + toolbar stacked; the strip wraps internally when narrow * and the header grows to fit (height: auto on the modifier class). */ editorToolbar?: React.ReactNode; } export const AppHeader: React.FC = ({ editorMenu, editorToolbar }) => { const location = useLocation(); const currentProject = useProjectStore((s) => s.currentProject); const [menuOpen, setMenuOpen] = useState(false); const [showShareModal, setShowShareModal] = useState(false); const { t } = useTranslation(); const localize = useLocalizedHref(); const currentLocale = useCurrentLocale(); // Close mobile menu on route change useEffect(() => { setMenuOpen(false); }, [location.pathname]); // Editor variant: where does the toolbar strip go — on the brand row or // on its own bar below, labelled or icon-only? Measured, not guessed — // see headerStripFit.ts, which sets `app-header--strip-below` on the // header and `unified-toolbar--compact` on the strip. Re-measured // whenever the strip host, the brand block or a strip zone resizes // (window, docked chat, board controls mounting). The first measure runs // before paint; later ones are deferred a frame so toggling the classes // never re-enters ResizeObserver delivery. const headerRef = useRef(null); const hasStrip = !!editorToolbar; useLayoutEffect(() => { const header = headerRef.current; if (!header || !hasStrip) return; const apply = () => { applyStripLayout(header); }; apply(); if (typeof ResizeObserver === 'undefined') return; let raf = 0; const schedule = () => { if (raf) return; raf = requestAnimationFrame(() => { raf = 0; apply(); }); }; const ro = new ResizeObserver(schedule); const content = header.querySelector(':scope > .header-content'); const left = content?.querySelector(':scope > .header-left'); const host = content?.querySelector(':scope > .header-editor-toolbar'); const strip = host?.firstElementChild; for (const el of [left, host, ...(strip ? Array.from(strip.children) : [])]) { if (el) ro.observe(el); } // Zones mount their controls later (the canvas side is a portal) — // pick up children that appear after mount. const mo = strip ? new MutationObserver(() => { for (const z of Array.from(strip.children)) ro.observe(z); schedule(); }) : null; if (strip && mo) mo.observe(strip, { childList: true }); return () => { ro.disconnect(); mo?.disconnect(); if (raf) cancelAnimationFrame(raf); header.classList.remove(STRIP_BELOW_CLASS); }; }, [hasStrip]); // 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; } // Compare with the trailing slash ignored: /editor is served (and // canonicalized) as /editor/ since it is prerendered, while client-side // navigation still lands on /editor. const samePath = (a: string, b: string) => a.replace(/\/+$/, '') === b.replace(/\/+$/, ''); const isActive = (path: string) => samePath(location.pathname, localize(path)) ? ' header-nav-link-active' : ''; return (
{/* Brand */}
Velxio
{/* Main nav links (web only). The Tauri desktop build hides this nav and surfaces the equivalent actions via the native menubar (see pro/desktop/src-tauri/src/menu.rs in velxio-prod). VITE_DESKTOP is the env flag the Tauri build sets — main.tsx already uses it to gate the @pro overlay, same pattern here. */} {editorMenu} {!import.meta.env.VITE_DESKTOP && !editorMenu && ( )}
{/* Editor toolbar strip — fills the middle the nav vacated. */} {editorToolbar &&
{editorToolbar}
} {/* Right: language + share + auth + mobile hamburger. In the desktop-editor variant this block does not render at all: the language switcher and the account button move to the corner box below (bottom-left), Share lives in File > Share/Embed, and the autosave dot rides next to the menus — every pixel of the row goes to the toolbar, which is what lets 1440px-with-chat keep the single-row layout. */} {!editorToolbar && (
{/* Share button — visible when a project is loaded */} {currentProject && samePath(location.pathname, localize('/editor')) && ( )} {/* Auth UI lives in the pro overlay — sign-in/sign-up buttons when anonymous, user dropdown when logged in. The overlay's mountPro() portals its HeaderAuth component into this slot via mountIntoSlot('header-auth'). In OSS without the overlay this slot stays empty, which is correct because the OSS image has no auth backend either. */}
{/* Mobile hamburger — useless in desktop where the nav it would expand is itself hidden, and in the editor variant, where there is no nav to expand at all. */} {!import.meta.env.VITE_DESKTOP && !editorMenu && ( )}
)}
{/* The account + language block lives in the file-explorer footer now (EditorPage renders it) — fused so a long file tree never scrolls underneath a floating box. */} {showShareModal && setShowShareModal(false)} />}
); };