From 4b21765ab94f0b40469dc1205c5057f480316c20 Mon Sep 17 00:00:00 2001 From: soryu Date: Mon, 1 Dec 2025 18:44:33 +0000 Subject: Frontend: mission screen transform with dimming, auto-pan image; hide stats in mission mode --- frontend/src/components/MissionDrawer.tsx | 120 ++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 frontend/src/components/MissionDrawer.tsx (limited to 'frontend/src/components/MissionDrawer.tsx') 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 = ({ isOpen, onClose, anchorRect }) => { + const [box, setBox] = useState(null) + const [expanded, setExpanded] = useState(false) + const closingRef = useRef(false) + + const finalBox = useMemo(() => { + 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 ( +
+
e.stopPropagation()} + onTransitionEnd={onTransitionEnd} + role="dialog" + aria-modal="true" + aria-labelledby="mission-title" + > +
+
+
+ そりゅう + SORYU +
+

Our Mission

+ +
+
+

+ 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. +

+

+ 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. +

+

+ By combining efficient streaming (SSE/WS), robust clients, and + thoughtful design, we help developers ship experiences where every + millisecond matters and every conversation counts. +

+
+
+
+
+ ) +} -- cgit v1.2.3