diff options
| author | soryu <soryu@soryu.co> | 2025-12-01 18:44:33 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2025-12-23 14:47:17 +0000 |
| commit | 4b21765ab94f0b40469dc1205c5057f480316c20 (patch) | |
| tree | 3ec9814c29d148f4d4301337cf9132dc79bec603 /frontend | |
| parent | a69fff7c31c6b76fc7cbddf74e34587a6c378bc2 (diff) | |
| download | soryu-4b21765ab94f0b40469dc1205c5057f480316c20.tar.gz soryu-4b21765ab94f0b40469dc1205c5057f480316c20.zip | |
Frontend: mission screen transform with dimming, auto-pan image; hide stats in mission mode
Diffstat (limited to 'frontend')
| -rw-r--r-- | frontend/src/components/LandingPage.tsx | 53 | ||||
| -rw-r--r-- | frontend/src/components/MissionDrawer.tsx | 120 | ||||
| -rw-r--r-- | frontend/src/styles/pc98.css | 84 | ||||
| -rw-r--r-- | frontend/tsconfig.tsbuildinfo | 2 |
4 files changed, 239 insertions, 20 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); diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo index fd99352..4c2c550 100644 --- a/frontend/tsconfig.tsbuildinfo +++ b/frontend/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/main.tsx","./src/types.ts","./src/components/bottombar.tsx","./src/components/choicemenu.tsx","./src/components/cityscapebackground.tsx","./src/components/configmodal.tsx","./src/components/dialoguebox.tsx","./src/components/landingpage.tsx","./src/components/loadingscreen.tsx","./src/components/topbar.tsx","./src/components/vnapp.tsx","./src/components/vninterface.tsx","./src/components/vnviewport.tsx","./src/services/ws.ts","./src/stores/index.ts"],"version":"5.9.2"}
\ No newline at end of file +{"root":["./src/app.tsx","./src/main.tsx","./src/types.ts","./src/components/bottombar.tsx","./src/components/choicemenu.tsx","./src/components/cityscapebackground.tsx","./src/components/configmodal.tsx","./src/components/dialoguebox.tsx","./src/components/heartlogo.tsx","./src/components/landingpage.tsx","./src/components/loadingscreen.tsx","./src/components/missiondrawer.tsx","./src/components/origamidragonlogo.tsx","./src/components/topbar.tsx","./src/components/vnapp.tsx","./src/components/vninterface.tsx","./src/components/vnviewport.tsx","./src/services/ws.ts","./src/stores/index.ts"],"version":"5.9.2"}
\ No newline at end of file |
