summaryrefslogtreecommitdiff
path: root/frontend/src/components/MissionDrawer.tsx
blob: 82908510d20a284d3a6c402c1c26ee11b0ae43ca (plain) (blame)
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>
  )
}