Files
canvas-frontend/src/scenes/Landing.tsx
T
shad 456243c31c fix: gate unfinished surfaces honestly (oracle round 2)
- Login.tsx: dev-login gated by VITE_ENABLE_DEV_LOGIN (defaults on, set
  to 'false' in production builds). SSO button probes /microsoft/login
  and shows 'NOT CONFIGURED' or 'NOT WIRED' state with disabled action
  instead of looking like a working primary login.
- Hub.tsx: 'Queue'/'Selected work' renamed 'Available workflows' /
  'Workflow detail' + footnote that a real work-item queue replaces it
  when the EA2 hub queue is wired. No more semantic overclaim.
- Landing.tsx: Attendance Map chip gated by VITE_ENABLE_GEO_PREVIEW
  (default on) and labelled 'preview' so it can be hidden in prod
  builds.
- flowCuration.ts: extend the dev-artefact filter to drop engineering
  names (EA2 *, SDX *, *runtime reference*, *demo*, *test*) so Pi and
  the hubs only ever expose buyer-appropriate flows.
- All 29 unit tests + 26 dogfood QA assertions still green.
2026-06-14 13:27:59 +04:00

167 lines
7.3 KiB
TypeScript

// Cinematic landing scene: brand, scenarios pick, start tour CTA.
import { motion } from "framer-motion";
import { useApp } from "../state/store";
import { liveMeta } from "../data/scenarios";
import { Sparkles, Arrow, Cmd, Bot, Pulse, Sun, Moon } from "../components/icons";
export default function Landing() {
const setScene = useApp((s) => s.setScene);
const setScenarioId = useApp((s) => s.setScenarioId);
const setCmdOpen = useApp((s) => s.setCmdOpen);
const setConsoleOpen = useApp((s) => s.setConsoleOpen);
const scenarios = useApp((s) => s.scenarios);
const mode = useApp((s) => s.mode);
const setMode = useApp((s) => s.setMode);
const refreshLive = useApp((s) => s.refreshLive);
const liveLoading = useApp((s) => s.liveLoading);
const liveTotals = useApp((s) => s.liveTotals);
return (
<div className="landing">
<div className="landing-bg" aria-hidden />
<div className="landing-grid" aria-hidden />
<header className="landing-top">
<div className="brand-lock">
<span className="brand-mark" />
<span className="brand-name">FlowMaster</span>
<span className="brand-divider" />
<span className="brand-sub">Mission Control</span>
</div>
<div className="landing-top-actions">
<ThemeToggle />
<button className="link-btn" onClick={() => setCmdOpen(true)}>
<Cmd size={13} /> Command <kbd>K</kbd>
</button>
</div>
</header>
<main className="landing-main">
<motion.div
className="landing-hero"
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.45 }}
>
<span className="hero-eyebrow"><Sparkles size={13} /> Business-as-code · Mission Control</span>
<h1 className="hero-title">
Every process. <span className="hl">One control surface.</span>
</h1>
<p className="hero-sub">
FlowMaster turns the operational map of a company into living,
typed processes backed by humans, agents, and rules and gives you
one control surface to drive them. Every procurement, finance, people,
and service workflow runs end-to-end through EA2 on{" "}
<span className="mono">{liveMeta.fetchedFrom?.replace("https://", "") ?? "the FlowMaster backend"}</span>.
</p>
<div className="hero-actions">
<button className="btn btn-primary btn-lg" onClick={() => setScene("mission")}>
<Arrow size={14} /> Enter Mission Control
</button>
<button
className={`btn btn-ghost btn-lg${mode === "live" ? " is-on" : ""}`}
onClick={() => mode === "live" ? refreshLive() : setMode("live")}
disabled={liveLoading}
title={mode === "live" ? "Re-fetch live scenarios from demo.flow-master.ai" : "Toggle between bundled snapshot and a live in-browser fetch"}
>
{liveLoading ? <span className="spin" /> : <Pulse size={13} />}
{mode === "live" ? "Live · refresh" : "Go live"}
</button>
<button className="btn btn-ghost btn-lg" onClick={() => { setScene("studio"); }}>
<Sparkles size={13} /> Design a process
</button>
<button className="btn btn-ghost btn-lg" onClick={() => setConsoleOpen(true)}>
<Bot size={13} /> Open live console
</button>
</div>
<div className="hero-hub-row">
<button className="hub-chip" onClick={() => setScene("hub-procurement")}>Procurement Hub</button>
<button className="hub-chip" onClick={() => setScene("hub-hr")}>People Hub</button>
<button className="hub-chip" onClick={() => setScene("hub-it")}>IT Hub</button>
{(import.meta.env.VITE_ENABLE_GEO_PREVIEW ?? "true") !== "false" && (
<button className="hub-chip" onClick={() => setScene("geo-attendance")}>Attendance Map · preview</button>
)}
<button className="hub-chip" onClick={() => setScene("agent")}>Command Assistant</button>
<button className="hub-chip" onClick={() => setScene("chat")}>Team Chat</button>
<button className="hub-chip" onClick={() => setScene("explainer")}>What is FlowMaster?</button>
</div>
<div className="hero-stats">
<div className="stat">
<Pulse size={12} />
<span className="stat-v mono">{liveTotals?.workItems ?? liveMeta.workItems}</span>
<span className="stat-l">{mode === "live" ? "live work items (now)" : "snapshot work items"}</span>
</div>
<div className="stat">
<span className="stat-v mono">{liveTotals?.distinctDefs ?? liveMeta.distinctDefs}</span>
<span className="stat-l">process definitions</span>
</div>
<div className="stat">
<Bot size={12} />
<span className="stat-v mono">{scenarios.length}</span>
<span className="stat-l">scenarios in catalog</span>
</div>
<div className="stat">
<span className={`mode-pill mode-${mode}`}>{mode === "live" ? "LIVE" : "SNAPSHOT"}</span>
<span className="stat-l">data mode</span>
</div>
</div>
</motion.div>
<motion.div
className="landing-cards"
initial={{ opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.12, duration: 0.45 }}
>
{scenarios.map((s) => (
<button
key={s.id}
className="sc-card"
style={{ ["--sc-accent" as string]: s.family.accent }}
onClick={() => { setScenarioId(s.id); setScene("mission"); }}
>
<div className="sc-card-top">
<span className="sc-card-mark" />
<span className={`tag ${s.live ? "tag-live" : "tag-syn"}`}>{s.live ? "live" : "blueprint"}</span>
</div>
<h3 className="sc-card-title">{s.family.label}</h3>
<p className="sc-card-sub">{s.family.subtitle}</p>
<div className="sc-card-meta">
<span>{s.steps.length} steps</span>
<span>·</span>
<span>{s.queue.length} cases</span>
<span>·</span>
<span className="mono">{s.version}</span>
</div>
<div className="sc-card-cta">Open <Arrow size={12} /></div>
</button>
))}
</motion.div>
</main>
<footer className="landing-foot">
<span className="foot-eyebrow">FlowMaster · Mission Control · live operator surface for FlowMaster processes</span>
</footer>
</div>
);
}
function ThemeToggle() {
const theme = useApp((s) => s.theme);
const setTheme = useApp((s) => s.setTheme);
const isDark = theme === "dark";
return (
<button
className="link-btn theme-toggle"
onClick={() => setTheme(isDark ? "light" : "dark")}
title={isDark ? "Switch to light theme" : "Switch to dark theme"}
aria-label="Toggle theme"
>
{isDark ? <Sun size={13} /> : <Moon size={13} />}
<span className="theme-toggle-label">{isDark ? "LIGHT" : "DARK"}</span>
</button>
);
}