summaryrefslogtreecommitdiff
path: root/frontend/src/components
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2025-12-01 18:44:33 +0000
committersoryu <soryu@soryu.co>2025-12-23 14:47:17 +0000
commit4b21765ab94f0b40469dc1205c5057f480316c20 (patch)
tree3ec9814c29d148f4d4301337cf9132dc79bec603 /frontend/src/components
parenta69fff7c31c6b76fc7cbddf74e34587a6c378bc2 (diff)
downloadsoryu-4b21765ab94f0b40469dc1205c5057f480316c20.tar.gz
soryu-4b21765ab94f0b40469dc1205c5057f480316c20.zip
Frontend: mission screen transform with dimming, auto-pan image; hide stats in mission mode
Diffstat (limited to 'frontend/src/components')
-rw-r--r--frontend/src/components/LandingPage.tsx53
-rw-r--r--frontend/src/components/MissionDrawer.tsx120
2 files changed, 155 insertions, 18 deletions
diff --git a/frontend/src/components/LandingPage.tsx b/frontend/src/components/LandingPage.tsx
index 9c262be..ea8dfbc 100644
--- a/frontend/src/components/LandingPage.tsx
+++ b/frontend/src/components/LandingPage.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from 'react'
+import React, { useState, useEffect, useRef } from 'react'
import { LoadingScreen } from './LoadingScreen'
import { HeartLogo } from './HeartLogo'
@@ -14,6 +14,7 @@ export function LandingPage({ onLogin }: LandingPageProps) {
const [energy, setEnergy] = useState(0)
const [ramped, setRamped] = useState(false)
const [pendingAction, setPendingAction] = useState<null | 'makima' | 'mission'>(null)
+ const [missionMode, setMissionMode] = useState(false)
// Fade-in landing page content after mount
useEffect(() => {
@@ -91,8 +92,8 @@ export function LandingPage({ onLogin }: LandingPageProps) {
}
const handleMission = () => {
- // Placeholder action for now
- setPendingAction('mission')
+ // Toggle screen transformation instead of modal/drawer
+ setMissionMode((m) => !m)
}
return (
@@ -147,7 +148,8 @@ export function LandingPage({ onLogin }: LandingPageProps) {
</div>
{/* Minimal overlay: masthead, issue badge, and CTA */}
- <div className="taisho-cover">
+ <div className={`taisho-cover ${missionMode ? 'mission-mode' : ''}`}>
+ <div className="cover-backdrop" aria-hidden="true" />
<div className="cover-content">
{/* Masthead + Issue badge (kept) */}
<div className="masthead">
@@ -158,14 +160,26 @@ export function LandingPage({ onLogin }: LandingPageProps) {
<div className="issue-badge"><span className="led-heart" aria-hidden="true"></span>かはいい Vol.01</div>
</div>
- {/* Empty hero area to preserve grid; background sits behind */}
- <div className="hero" />
+ {/* Hero area becomes Mission content when in mission mode */}
+ {missionMode ? (
+ <div className="mission-screen" role="region" aria-label="Mission">
+ <h1 className="mission-headline">Building real‑time systems for mission-critical observability and surveillance </h1>
+ <img src="/PC98Doukuusei.webp" alt="Mission montage" className="mission-image" />
+ <p className="mission-paragraph">
+ We deliver low‑latency streaming & infrastructure that turns live data into
+ reliable, secure insight. Target selection, monitoring and full end to end observability
+ to make vital decisions where it matters most.
+ </p>
+ </div>
+ ) : (
+ <div className="hero" />
+ )}
{/* CTA row spanning full width: left Mission/Contact, right Login */}
<div className="cta-area">
<div className="cta-left">
<button className="taisho-cta" onClick={handleMission}>
- <span className="cta-text">Mission</span>
+ <span className="cta-text">{missionMode ? 'Close' : 'Mission'}</span>
</button>
<button className="taisho-cta" onClick={() => {/* placeholder contact */}}>
<span className="cta-text">Contact</span>
@@ -181,20 +195,23 @@ export function LandingPage({ onLogin }: LandingPageProps) {
</div>
</div>
- {/* Bottom stats: Velocity + Energy only */}
- <div className="bottom-stats">
- <div className="rf-stats">
- <div className="rf-stat">
- <span className="label">Velocity</span>
- <span className="value">{Math.round(velocity)} km/h</span>
- </div>
- <div className="rf-stat">
- <span className="label">Energy</span>
- <span className="value">{energy.toFixed(1)} MJ</span>
+ {/* Bottom stats: Velocity + Energy only (hidden in mission mode) */}
+ {!missionMode && (
+ <div className="bottom-stats">
+ <div className="rf-stats">
+ <div className="rf-stat">
+ <span className="label">Velocity</span>
+ <span className="value">{Math.round(velocity)} km/h</span>
+ </div>
+ <div className="rf-stat">
+ <span className="label">Energy</span>
+ <span className="value">{energy.toFixed(1)} MJ</span>
+ </div>
</div>
</div>
- </div>
+ )}
</div>
+ {/* MissionDrawer removed in favor of mission screen transformation */}
</div>
)
}
diff --git a/frontend/src/components/MissionDrawer.tsx b/frontend/src/components/MissionDrawer.tsx
new file mode 100644
index 0000000..8290851
--- /dev/null
+++ b/frontend/src/components/MissionDrawer.tsx
@@ -0,0 +1,120 @@
+import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
+
+type AnchorRect = { top: number; left: number; width: number; height: number }
+
+type Props = {
+ isOpen: boolean
+ onClose: () => void
+ anchorRect: AnchorRect | null
+}
+
+export const MissionDrawer: React.FC<Props> = ({ isOpen, onClose, anchorRect }) => {
+ const [box, setBox] = useState<AnchorRect | null>(null)
+ const [expanded, setExpanded] = useState(false)
+ const closingRef = useRef(false)
+
+ const finalBox = useMemo<AnchorRect | null>(() => {
+ if (!isOpen) return null
+ const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0)
+ const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0)
+
+ const margin = 24
+ const maxW = Math.min(640, vw - margin * 2)
+ const maxH = Math.min(Math.round(vh * 0.86), 560)
+ const left = Math.round((vw - maxW) / 2)
+ const top = Math.round((vh - maxH) / 2)
+ return { top, left, width: maxW, height: maxH }
+ }, [isOpen])
+
+ useLayoutEffect(() => {
+ if (!isOpen || !anchorRect) return
+ closingRef.current = false
+ setBox(anchorRect)
+ // Defer to next frame to allow transition from anchor -> final
+ const id = requestAnimationFrame(() => {
+ setExpanded(true)
+ if (finalBox) setBox(finalBox)
+ })
+ return () => cancelAnimationFrame(id)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isOpen, anchorRect])
+
+ useEffect(() => {
+ if (!isOpen) return
+ const handler = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') handleClose()
+ }
+ window.addEventListener('keydown', handler)
+ return () => window.removeEventListener('keydown', handler)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isOpen])
+
+ if (!isOpen || !anchorRect || !box) return null
+
+ const handleClose = () => {
+ if (!anchorRect) return onClose()
+ closingRef.current = true
+ setExpanded(false)
+ setBox(anchorRect)
+ }
+
+ const onTransitionEnd = () => {
+ if (closingRef.current) {
+ closingRef.current = false
+ onClose()
+ }
+ }
+
+ const style: React.CSSProperties = {
+ position: 'fixed',
+ top: box.top,
+ left: box.left,
+ width: box.width,
+ height: box.height
+ }
+
+ return (
+ <div className="mission-overlay" onClick={handleClose}>
+ <div
+ className={`mission-morph ${expanded ? 'expanded' : ''}`}
+ style={style}
+ onClick={(e) => e.stopPropagation()}
+ onTransitionEnd={onTransitionEnd}
+ role="dialog"
+ aria-modal="true"
+ aria-labelledby="mission-title"
+ >
+ <div className={`mission-modal ${expanded ? 'visible' : ''}`}>
+ <div className="mission-modal-header">
+ <div className="mission-brand">
+ <span className="jp">そりゅう</span>
+ <span className="en">SORYU</span>
+ </div>
+ <h2 id="mission-title" className="mission-title">Our Mission</h2>
+ <button className="mission-close-btn" aria-label="Close" onClick={handleClose}>×</button>
+ </div>
+ <div className="mission-content">
+ <p>
+ At Soryu, our mission is to make real‑time conversation
+ understanding feel instant, reliable, and human. We build low‑latency
+ infrastructure for streaming transcription and interaction so
+ products can turn live dialogue into actionable, privacy‑respecting
+ insight.
+ </p>
+ <p>
+ We obsess over end‑to‑end performance — from the first byte on the
+ wire to the words on the screen — so teams can deliver
+ conversational experiences that are responsive, accessible, and
+ trustworthy.
+ </p>
+ <p>
+ By combining efficient streaming (SSE/WS), robust clients, and
+ thoughtful design, we help developers ship experiences where every
+ millisecond matters and every conversation counts.
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+}