diff options
Diffstat (limited to 'frontend/src/components')
| -rw-r--r-- | frontend/src/components/LandingPage.tsx | 57 | ||||
| -rw-r--r-- | frontend/src/components/TypewriterRotator.tsx | 84 |
2 files changed, 123 insertions, 18 deletions
diff --git a/frontend/src/components/LandingPage.tsx b/frontend/src/components/LandingPage.tsx index f126d6f..2c543a6 100644 --- a/frontend/src/components/LandingPage.tsx +++ b/frontend/src/components/LandingPage.tsx @@ -1,6 +1,15 @@ import React, { useState, useEffect, useRef } from 'react' import { LoadingScreen } from './LoadingScreen' import { HeartLogo } from './HeartLogo' +import { TypewriterRotator } from './TypewriterRotator' + +const HERO_PHRASES = [ + '低遅延ストリーミング · LOW-LATENCY OBSERVABILITY', + 'リアルタイム監視 · REAL-TIME SURVEILLANCE', + 'ミッションクリティカル · MISSION-CRITICAL INFRASTRUCTURE', + 'エンドツーエンド可視化 · END-TO-END VISIBILITY', + '安全な意思決定 · SECURE DECISIONS AT THE EDGE', +] interface LandingPageProps { onLogin: () => void @@ -167,7 +176,7 @@ export function LandingPage({ onLogin }: LandingPageProps) { {activePanel === 'mission' ? ( <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" /> + <img src="/mission-tactical.svg" alt="Tactical observability mesh" 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 @@ -189,27 +198,39 @@ export function LandingPage({ onLogin }: LandingPageProps) { </p> </div> ) : ( - <div className="hero" /> + <div className="hero"> + <div className="hero-tagline" aria-label="Soryu capabilities"> + <TypewriterRotator phrases={HERO_PHRASES} /> + </div> + </div> )} - {/* CTA row spanning full width: left Mission/MAKIMA, right Login */} - <div className="cta-area"> - <div className="cta-left"> - <button className="taisho-cta" onClick={handleMission}> - <span className="cta-text">{activePanel === 'mission' ? 'Close' : 'Mission'}</span> - </button> - <button className="taisho-cta" onClick={handleMakimaPanel}> - <span className="cta-text">{activePanel === 'makima' ? 'Close' : 'MAKIMA'}</span> - </button> - </div> - <div className="cta-right"> - <button className="taisho-cta" onClick={handleLogin}> - <span className="cta-icon">▶</span> - <span className="cta-text">Login</span> - </button> - </div> + {/* CTA row spanning full width: left Mission/MAKIMA, right Login */} + <div className="cta-area"> + <div className="cta-left"> + <button + className={`taisho-cta${activePanel === 'mission' ? ' is-active' : ''}`} + aria-current={activePanel === 'mission' ? 'page' : undefined} + onClick={handleMission} + > + <span className="cta-text">Mission</span> + </button> + <button + className={`taisho-cta${activePanel === 'makima' ? ' is-active' : ''}`} + aria-current={activePanel === 'makima' ? 'page' : undefined} + onClick={handleMakimaPanel} + > + <span className="cta-text">MAKIMA</span> + </button> + </div> + <div className="cta-right"> + <button className="taisho-cta" onClick={handleLogin}> + <span className="cta-icon">▶</span> + <span className="cta-text">Login</span> + </button> </div> </div> + </div> </div> {/* Bottom stats: Velocity + Energy only (hidden in mission mode) */} diff --git a/frontend/src/components/TypewriterRotator.tsx b/frontend/src/components/TypewriterRotator.tsx new file mode 100644 index 0000000..4cd89fa --- /dev/null +++ b/frontend/src/components/TypewriterRotator.tsx @@ -0,0 +1,84 @@ +import React, { useEffect, useRef, useState } from 'react' + +/** + * TypewriterRotator + * + * Cycles through a list of phrases with a typing-and-deleting effect. + * Designed for the soryu landing hero — sits inside a centred container, + * so the caller controls position and the component just renders text. + * + * Honours `prefers-reduced-motion` by rendering the first phrase only, + * statically. + */ + +interface TypewriterRotatorProps { + phrases: string[] + /** ms per character while typing */ + typeMs?: number + /** ms per character while deleting */ + deleteMs?: number + /** ms to hold a fully-typed phrase before deleting */ + holdMs?: number + /** ms to wait after deletion before the next phrase types in */ + gapMs?: number + className?: string +} + +export function TypewriterRotator({ + phrases, + typeMs = 55, + deleteMs = 28, + holdMs = 2200, + gapMs = 320, + className, +}: TypewriterRotatorProps) { + const [text, setText] = useState('') + const [index, setIndex] = useState(0) + const [phase, setPhase] = useState<'typing' | 'holding' | 'deleting' | 'gap'>('typing') + const reduceMotion = useRef( + typeof window !== 'undefined' && + window.matchMedia && + window.matchMedia('(prefers-reduced-motion: reduce)').matches, + ) + + useEffect(() => { + if (reduceMotion.current) { + setText(phrases[0] ?? '') + return + } + const current = phrases[index % phrases.length] ?? '' + let timer: number + + if (phase === 'typing') { + if (text.length < current.length) { + timer = window.setTimeout(() => setText(current.slice(0, text.length + 1)), typeMs) + } else { + timer = window.setTimeout(() => setPhase('holding'), 0) + } + } else if (phase === 'holding') { + // Skip the hold-then-delete cycle entirely if there's only one phrase + if (phrases.length <= 1) return + timer = window.setTimeout(() => setPhase('deleting'), holdMs) + } else if (phase === 'deleting') { + if (text.length > 0) { + timer = window.setTimeout(() => setText(current.slice(0, text.length - 1)), deleteMs) + } else { + timer = window.setTimeout(() => setPhase('gap'), 0) + } + } else if (phase === 'gap') { + timer = window.setTimeout(() => { + setIndex((i) => (i + 1) % phrases.length) + setPhase('typing') + }, gapMs) + } + + return () => window.clearTimeout(timer) + }, [text, phase, index, phrases, typeMs, deleteMs, holdMs, gapMs]) + + return ( + <span className={className} role="status" aria-live="polite"> + <span className="tw-text">{text}</span> + <span className="tw-caret" aria-hidden="true" /> + </span> + ) +} |
