summaryrefslogtreecommitdiff
path: root/frontend/src
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
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')
-rw-r--r--frontend/src/components/LandingPage.tsx53
-rw-r--r--frontend/src/components/MissionDrawer.tsx120
-rw-r--r--frontend/src/styles/pc98.css84
3 files changed, 238 insertions, 19 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>
+ )
+}
diff --git a/frontend/src/styles/pc98.css b/frontend/src/styles/pc98.css
index c7d6fb6..f4b9450 100644
--- a/frontend/src/styles/pc98.css
+++ b/frontend/src/styles/pc98.css
@@ -1713,7 +1713,16 @@ button:focus-visible {
--shadow: rgba(26, 15, 8, 0.25);
}
-.taisho-cover .cover-backdrop { display: none; }
+.taisho-cover .cover-backdrop {
+ position: absolute;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.55);
+ backdrop-filter: blur(4px);
+ -webkit-backdrop-filter: blur(4px);
+ z-index: 0;
+ display: none;
+}
+.taisho-cover.mission-mode .cover-backdrop { display: block; }
.taisho-cover .paper-tone {
position: absolute;
@@ -1758,6 +1767,21 @@ button:focus-visible {
padding: 40px 48px;
}
+/* Mission mode adjusts spacing and masthead presence */
+.taisho-cover.mission-mode .cover-content {
+ gap: 20px;
+ padding: 36px 44px;
+}
+.taisho-cover.mission-mode .masthead-vertical {
+ transform: translateX(-4px) scale(0.96);
+ transition: transform 220ms ease, box-shadow 220ms ease;
+ box-shadow:
+ inset 0 0 0 2px rgba(0,0,0,0.6),
+ 0 0 0 2px rgba(102,204,255,0.2),
+ 2px 2px 0 rgba(0,0,0,0.35);
+}
+.taisho-cover.mission-mode .issue-badge { transform: translateY(-2px); transition: transform 220ms ease; }
+
/* Masthead */
.masthead { grid-area: masthead; display: flex; flex-direction: column; align-items: flex-start; gap: 12px; }
.masthead-vertical {
@@ -1774,6 +1798,7 @@ button:focus-visible {
4px 4px 0 rgba(0,0,0,0.35);
padding: 14px 8px;
letter-spacing: 2px;
+ transition: transform 220ms ease;
}
.masthead-vertical::after {
content: '';
@@ -1811,6 +1836,62 @@ button:focus-visible {
/* Hero */
.hero { grid-area: hero; display: flex; align-items: center; justify-content: center; }
+
+/* Mission screen replaces hero in mission mode */
+.mission-screen {
+ grid-area: hero;
+ display: grid;
+ grid-template-rows: auto auto 1fr;
+ align-content: start;
+ gap: 16px;
+ animation: fadeIn 200ms ease;
+ max-height: 100%;
+ overflow: hidden;
+}
+
+.mission-headline {
+ font-family: 'Sylfaen', serif;
+ font-weight: 700;
+ font-size: 28px;
+ color: #eaf7ff;
+ text-shadow: 1px 1px 0 rgba(0,0,0,0.6);
+ letter-spacing: 0.5px;
+ margin: 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.mission-image {
+ width: 100%;
+ height: clamp(160px, 38vh, 400px);
+ object-fit: cover;
+ object-position: 50% 0%;
+ border: 2px solid #66ccff;
+ box-shadow: 0 6px 18px rgba(0,0,0,0.35);
+ animation: mission-pan 22s ease-in-out infinite alternate;
+ will-change: object-position;
+}
+
+.mission-paragraph {
+ font-family: 'MS Gothic', monospace;
+ font-size: 14px;
+ line-height: 1.7;
+ color: #cfefff;
+ margin: 0;
+}
+
+@media (max-width: 768px) {
+ .taisho-cover.mission-mode .cover-content { gap: 14px; padding: 20px 16px 120px; }
+ .mission-headline { font-size: 18px; white-space: normal; text-overflow: initial; }
+ .mission-image { height: clamp(120px, 26vh, 220px); animation: mission-pan 20s ease-in-out infinite alternate; }
+ .mission-paragraph { font-size: 13px; }
+}
+
+@keyframes mission-pan {
+ 0% { object-position: 50% 0%; }
+ 100% { object-position: 50% 100%; }
+}
.hero-frame {
position: relative;
width: 100%;
@@ -3660,6 +3741,7 @@ button:focus-visible {
animation: fadeIn 0.3s ease;
}
+
.settings-modal {
background: rgba(0, 0, 0, 0.95);
border: 2px solid rgba(102, 204, 255, 0.5);