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:
@@ -0,0 +1,49 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 560" width="480" height="560" role="img" aria-label="PubScript">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="lk-gold" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#f9edc4"/>
|
||||||
|
<stop offset="0.27" stop-color="#e8c76a"/>
|
||||||
|
<stop offset="0.52" stop-color="#c9a23a"/>
|
||||||
|
<stop offset="0.72" stop-color="#f4d683"/>
|
||||||
|
<stop offset="1" stop-color="#937420"/>
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient id="lk-ivory" cx="45%" cy="38%" r="72%">
|
||||||
|
<stop offset="0" stop-color="#fffdf4"/>
|
||||||
|
<stop offset="0.72" stop-color="#f5ecd6"/>
|
||||||
|
<stop offset="1" stop-color="#e8dcbd"/>
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id="lk-word" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#f4d683"/>
|
||||||
|
<stop offset="0.5" stop-color="#d8b25a"/>
|
||||||
|
<stop offset="1" stop-color="#b8912f"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="lk-shadow" x="-35%" y="-35%" width="170%" height="170%">
|
||||||
|
<feDropShadow dx="0" dy="5" stdDeviation="8" flood-color="#000000" flood-opacity="0.4"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- P tile -->
|
||||||
|
<g filter="url(#lk-shadow)">
|
||||||
|
<circle cx="240" cy="120" r="92" fill="url(#lk-gold)"/>
|
||||||
|
<circle cx="240" cy="120" r="78" fill="url(#lk-ivory)"/>
|
||||||
|
<circle cx="240" cy="120" r="78" fill="none" stroke="#8a6d1c" stroke-width="1.5" opacity="0.5"/>
|
||||||
|
<text x="240" y="120" text-anchor="middle" dominant-baseline="central"
|
||||||
|
font-family="Georgia, 'Times New Roman', serif" font-size="116" font-weight="600"
|
||||||
|
fill="#21242c">P</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Wordmark -->
|
||||||
|
<text x="240" y="300" text-anchor="middle" dominant-baseline="central"
|
||||||
|
font-family="Georgia, 'Times New Roman', serif" font-size="80" letter-spacing="1"
|
||||||
|
fill="url(#lk-word)">PubScript</text>
|
||||||
|
|
||||||
|
<!-- S tile -->
|
||||||
|
<g filter="url(#lk-shadow)">
|
||||||
|
<circle cx="240" cy="480" r="92" fill="url(#lk-gold)"/>
|
||||||
|
<circle cx="240" cy="480" r="78" fill="url(#lk-ivory)"/>
|
||||||
|
<circle cx="240" cy="480" r="78" fill="none" stroke="#8a6d1c" stroke-width="1.5" opacity="0.5"/>
|
||||||
|
<text x="240" y="480" text-anchor="middle" dominant-baseline="central"
|
||||||
|
font-family="Georgia, 'Times New Roman', serif" font-size="116" font-weight="600"
|
||||||
|
fill="#21242c">S</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Routes, Route } from "react-router-dom";
|
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 { ExportDialog } from "./features/export/ExportDialog.js";
|
||||||
import { ToastViewport } from "@pubscript/ui";
|
import { ToastViewport } from "@pubscript/ui";
|
||||||
import Write from "./routes/Write.js";
|
import Write from "./routes/Write.js";
|
||||||
@@ -42,6 +42,7 @@ export default function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ps-app">
|
<div className="ps-app">
|
||||||
|
<SplashScreen />
|
||||||
<Sidebar nav={NAV} />
|
<Sidebar nav={NAV} />
|
||||||
<Topbar onOpenPalette={() => setPaletteOpen(true)} onOpenExport={() => setExportOpen(true)} />
|
<Topbar onOpenPalette={() => setPaletteOpen(true)} onOpenExport={() => setExportOpen(true)} />
|
||||||
<main className="ps-main">
|
<main className="ps-main">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,3 +5,4 @@ export type { TopbarProps } from "./Topbar.js";
|
|||||||
export { CommandPalette } from "./CommandPalette.js";
|
export { CommandPalette } from "./CommandPalette.js";
|
||||||
export type { CommandPaletteProps } from "./CommandPalette.js";
|
export type { CommandPaletteProps } from "./CommandPalette.js";
|
||||||
export { Settings } from "./Settings.js";
|
export { Settings } from "./Settings.js";
|
||||||
|
export { default as SplashScreen } from "./SplashScreen.js";
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import "./styles/plot.css";
|
|||||||
import "./styles/timeline.css";
|
import "./styles/timeline.css";
|
||||||
import "./styles/whiteboard.css";
|
import "./styles/whiteboard.css";
|
||||||
import "./styles/export.css";
|
import "./styles/export.css";
|
||||||
|
import "./styles/splash.css";
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user