| | | 1 | | import { useEffect, useRef } from "react"; |
| | | 2 | | import type { SessionUser } from "~/lib/session.server"; |
| | | 3 | | |
| | 0 | 4 | | export function TuiUserPanel({ user, open, onClose, onSignOut }: { |
| | | 5 | | user: SessionUser; |
| | | 6 | | open: boolean; |
| | | 7 | | onClose: () => void; |
| | | 8 | | onSignOut: () => void; |
| | | 9 | | }) { |
| | 0 | 10 | | const ref = useRef<HTMLDivElement>(null); |
| | | 11 | | |
| | 0 | 12 | | useEffect(() => { |
| | 0 | 13 | | if (!open) return; |
| | 0 | 14 | | function onDocPointer(e: MouseEvent) { |
| | 0 | 15 | | if (!ref.current?.contains(e.target as Node)) onClose(); |
| | | 16 | | } |
| | 0 | 17 | | function onKey(e: KeyboardEvent) { |
| | 0 | 18 | | if (e.key === "Escape") { |
| | 0 | 19 | | e.preventDefault(); |
| | 0 | 20 | | onClose(); |
| | 0 | 21 | | } else if (e.key === "s") { |
| | | 22 | | // Only fire when focus is inside the panel (avoid hijacking 's' globally) |
| | 0 | 23 | | if (ref.current?.contains(document.activeElement)) { |
| | 0 | 24 | | e.preventDefault(); |
| | 0 | 25 | | onSignOut(); |
| | | 26 | | } |
| | | 27 | | } |
| | | 28 | | } |
| | 0 | 29 | | document.addEventListener("mousedown", onDocPointer); |
| | 0 | 30 | | window.addEventListener("keydown", onKey); |
| | 0 | 31 | | return () => { |
| | 0 | 32 | | document.removeEventListener("mousedown", onDocPointer); |
| | 0 | 33 | | window.removeEventListener("keydown", onKey); |
| | | 34 | | }; |
| | | 35 | | }, [open, onClose, onSignOut]); |
| | | 36 | | |
| | | 37 | | // Focus first action button when opened, so 's' / Esc work without an extra click |
| | 0 | 38 | | useEffect(() => { |
| | 0 | 39 | | if (open) ref.current?.querySelector<HTMLElement>("button")?.focus(); |
| | | 40 | | }, [open]); |
| | | 41 | | |
| | 0 | 42 | | if (!open) return null; |
| | | 43 | | |
| | 0 | 44 | | const displayName = user.name ?? user.preferred_username ?? user.sub; |
| | 0 | 45 | | const username = user.preferred_username ?? ""; |
| | 0 | 46 | | const email = user.email ?? ""; |
| | 0 | 47 | | const groups = (user.groups ?? []).filter(Boolean); |
| | | 48 | | |
| | 0 | 49 | | return ( |
| | | 50 | | <div |
| | | 51 | | ref={ref} |
| | | 52 | | role="dialog" |
| | | 53 | | aria-modal="false" |
| | | 54 | | aria-label="Account" |
| | | 55 | | className="tui-panel tui-user-panel" |
| | | 56 | | > |
| | | 57 | | <span className="tui-panel-title">{`─[ user · ${displayName} ]─`}</span> |
| | | 58 | | |
| | | 59 | | <div className="tui-detail-grid tui-user-panel-grid"> |
| | | 60 | | <Field label="user" value={displayName} emphasis="bright" /> |
| | | 61 | | <Field label="username" value={username || <Dim>(none)</Dim>} /> |
| | | 62 | | <Field label="email" value={email || <Dim>(none)</Dim>} /> |
| | | 63 | | <Field label="groups" value={groups.length > 0 ? groups.join(", ") : <Dim>(none)</Dim>} /> |
| | | 64 | | <div className="tui-detail-field tui-user-panel-sub"> |
| | | 65 | | <span className="tui-detail-label">{"sub "}</span> |
| | | 66 | | <span className="tui-detail-colon">:</span>{" "} |
| | | 67 | | <span className="tui-detail-value tui-user-panel-sub-value" title={user.sub}>{user.sub}</span> |
| | | 68 | | </div> |
| | | 69 | | </div> |
| | | 70 | | |
| | | 71 | | <div className="tui-detail-actions"> |
| | | 72 | | <button type="button" onClick={onSignOut} className="tui-detail-action tui-detail-action--danger"> |
| | | 73 | | [s] sign out |
| | | 74 | | </button> |
| | | 75 | | <span className="tui-detail-sep">·</span> |
| | | 76 | | <button type="button" onClick={onClose} className="tui-detail-action"> |
| | | 77 | | [Esc] close |
| | | 78 | | </button> |
| | | 79 | | </div> |
| | | 80 | | </div> |
| | | 81 | | ); |
| | | 82 | | } |
| | | 83 | | |
| | 0 | 84 | | function Field({ label, value, emphasis }: { |
| | | 85 | | label: string; |
| | | 86 | | value: React.ReactNode; |
| | | 87 | | emphasis?: "bright"; |
| | | 88 | | }) { |
| | 0 | 89 | | return ( |
| | | 90 | | <div className="tui-detail-field"> |
| | | 91 | | <span className="tui-detail-label">{label.padEnd(8, " ")}</span> |
| | | 92 | | <span className="tui-detail-colon">:</span>{" "} |
| | | 93 | | <span className={emphasis ? `tui-detail-value tui-detail-value--${emphasis}` : "tui-detail-value"}> |
| | | 94 | | {value} |
| | | 95 | | </span> |
| | | 96 | | </div> |
| | | 97 | | ); |
| | | 98 | | } |
| | | 99 | | |
| | 0 | 100 | | function Dim({ children }: { children: React.ReactNode }) { |
| | 0 | 101 | | return <span style={{ color: "var(--c-fg-3)" }}>{children}</span>; |
| | | 102 | | } |