feat(ui): add vertical logo lockup splash screen on app load

Introduce a full-screen brand splash shown while the project store is
loading (status !== ready). Uses a new vertical logo lockup (P tile /
PubScript wordmark / S tile) matching the existing typewriter-tile
design, centered on the active theme background with a subtle
fade/appear animation and a graceful fade-out once the app is ready.

- apps/web/public/logo-lockup.svg: vertical lockup variant of the mark
- SplashScreen.tsx: overlay driven by the store's load status
- splash.css: full-screen overlay + keyframe animations
- wire into App.tsx and import splash.css in main.tsx
This commit is contained in:
Schwitzz
2026-10-27 07:33:46 -05:00
parent 2a522977e9
commit 5085357dad
6 changed files with 154 additions and 1 deletions
+2 -1
View File
@@ -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 (
<div className="ps-app">
<SplashScreen />
<Sidebar nav={NAV} />
<Topbar onOpenPalette={() => setPaletteOpen(true)} onOpenExport={() => setExportOpen(true)} />
<main className="ps-main">
+38
View File
@@ -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 (
<div className={`ps-splash${exiting ? " ps-splash-exit" : ""}`} aria-hidden="true">
<img
src="/logo-lockup.svg"
alt="PubScript"
className="ps-splash-logo"
width="360"
height="420"
/>
<div className="ps-splash-status">
{status === "loading" ? "Loading project…" : "Initializing…"}
</div>
</div>
);
}
+1
View File
@@ -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";
+1
View File
@@ -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(
<StrictMode>
+63
View File
@@ -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; }
}