1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
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>
)
}
|