diff --git a/apps/web/public/logo-lockup.svg b/apps/web/public/logo-lockup.svg new file mode 100644 index 0000000..48ee162 --- /dev/null +++ b/apps/web/public/logo-lockup.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + P + + + + PubScript + + + + + + + S + + diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index af2952b..71228de 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; import { Routes, Route } from "react-router-dom"; -import { Sidebar, Topbar, CommandPalette } from "./components/index.js"; +import { Sidebar, Topbar, CommandPalette, SplashScreen } from "./components/index.js"; import { ExportDialog } from "./features/export/ExportDialog.js"; import { ToastViewport } from "@pubscript/ui"; import Write from "./routes/Write.js"; @@ -42,6 +42,7 @@ export default function App() { return (
+ setPaletteOpen(true)} onOpenExport={() => setExportOpen(true)} />
diff --git a/apps/web/src/components/SplashScreen.tsx b/apps/web/src/components/SplashScreen.tsx new file mode 100644 index 0000000..1db38df --- /dev/null +++ b/apps/web/src/components/SplashScreen.tsx @@ -0,0 +1,38 @@ +import { useEffect, useState } from "react"; +import { useProjectStore } from "../store/project-store.js"; + +/** + * Full-screen brand splash shown while the project store is loading + * (`status !== "ready"`). Uses the vertical logo lockup so the app opens + * with a polished brand moment before the writing UI appears. Fades out + * gracefully once the store is ready. + */ +export default function SplashScreen() { + const status = useProjectStore((s) => s.status); + const [exiting, setExiting] = useState(false); + + useEffect(() => { + if (status === "ready" && !exiting) { + // Fade out, then unmount once the 300ms exit animation finishes. + const t = setTimeout(() => setExiting(true), 300); + return () => clearTimeout(t); + } + }, [status, exiting]); + + if (status === "ready" && exiting) return null; + + return ( + + ); +} diff --git a/apps/web/src/components/index.ts b/apps/web/src/components/index.ts index 074d47c..9cd91e6 100644 --- a/apps/web/src/components/index.ts +++ b/apps/web/src/components/index.ts @@ -5,3 +5,4 @@ export type { TopbarProps } from "./Topbar.js"; export { CommandPalette } from "./CommandPalette.js"; export type { CommandPaletteProps } from "./CommandPalette.js"; export { Settings } from "./Settings.js"; +export { default as SplashScreen } from "./SplashScreen.js"; diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index 9762f96..af8f338 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -9,6 +9,7 @@ import "./styles/plot.css"; import "./styles/timeline.css"; import "./styles/whiteboard.css"; import "./styles/export.css"; +import "./styles/splash.css"; createRoot(document.getElementById("root")!).render( diff --git a/apps/web/src/styles/splash.css b/apps/web/src/styles/splash.css new file mode 100644 index 0000000..faf0b54 --- /dev/null +++ b/apps/web/src/styles/splash.css @@ -0,0 +1,63 @@ +/* ============================================================================ + * SplashScreen — full-screen brand overlay shown while the project store loads. + * ============================================================================ */ + +.ps-splash { + position: fixed; + inset: 0; + z-index: 1000; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 28px; + background: var(--ps-bg); + color: var(--ps-text); + animation: ps-splash-fade-in 0.35s ease-out both; +} + +.ps-splash-logo { + height: 30vh; + max-height: 360px; + width: auto; + animation: ps-splash-appear 0.6s cubic-bezier(0.22, 1, 0.36, 1) both; +} + +.ps-splash-status { + font-family: var(--ps-font-ui); + font-size: 15px; + letter-spacing: 0.04em; + color: var(--ps-text-muted); + animation: ps-splash-pulse 1.4s ease-in-out infinite; +} + +@keyframes ps-splash-appear { + from { + opacity: 0; + transform: translateY(12px) scale(0.96); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes ps-splash-fade-in { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes ps-splash-pulse { + 0%, 100% { opacity: 0.55; } + 50% { opacity: 1; } +} + +/* Fade the overlay out once the app is ready to avoid a jarring cut. */ +.ps-splash.ps-splash-exit { + animation: ps-splash-disappear 0.3s ease-in forwards; +} + +@keyframes ps-splash-disappear { + from { opacity: 1; } + to { opacity: 0; pointer-events: none; } +}