@arbidocs/blocks 0.4.109 → 0.4.111

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/dist/{NativeDocxEditor-WODZAURG.cjs → NativeDocxEditor-AQKRZBIU.cjs} +7 -7
  2. package/dist/{NativeDocxEditor-WODZAURG.cjs.map → NativeDocxEditor-AQKRZBIU.cjs.map} +1 -1
  3. package/dist/{NativeDocxEditor-4PY4YZYC.js → NativeDocxEditor-IN2HEO5I.js} +3 -3
  4. package/dist/{NativeDocxEditor-4PY4YZYC.js.map → NativeDocxEditor-IN2HEO5I.js.map} +1 -1
  5. package/dist/agents/index.cjs +286 -286
  6. package/dist/agents/index.js +10 -10
  7. package/dist/agents/tasks/ConversationListView.d.ts +5 -1
  8. package/dist/agents/tasks/TaskActivityTimeline.d.ts +5 -1
  9. package/dist/calendar/EventDetails.d.ts +1 -1
  10. package/dist/calendar/EventEditor.d.ts +1 -1
  11. package/dist/calendar/draft.d.ts +7 -1
  12. package/dist/calendar/index.cjs +71 -10
  13. package/dist/calendar/index.cjs.map +1 -1
  14. package/dist/calendar/index.d.ts +1 -1
  15. package/dist/calendar/index.js +69 -11
  16. package/dist/calendar/index.js.map +1 -1
  17. package/dist/calendar/types.d.ts +23 -0
  18. package/dist/calls/CallSidePanel.d.ts +32 -0
  19. package/dist/calls/CallStage.d.ts +57 -0
  20. package/dist/calls/ControlBar.d.ts +113 -0
  21. package/dist/calls/ControlButton.d.ts +44 -0
  22. package/dist/calls/PreJoin.d.ts +35 -0
  23. package/dist/calls/cards.d.ts +58 -0
  24. package/dist/calls/hooks.d.ts +20 -0
  25. package/dist/calls/index.cjs +2884 -0
  26. package/dist/calls/index.cjs.map +1 -0
  27. package/dist/calls/index.d.ts +21 -0
  28. package/dist/calls/index.js +2753 -0
  29. package/dist/calls/index.js.map +1 -0
  30. package/dist/calls/join-choices.d.ts +11 -0
  31. package/dist/calls/layout.d.ts +106 -0
  32. package/dist/calls/panels.d.ts +66 -0
  33. package/dist/calls/status.d.ts +69 -0
  34. package/dist/calls/tiles.d.ts +59 -0
  35. package/dist/calls/webinar/ChatPermissionSetting.d.ts +18 -0
  36. package/dist/calls/webinar/OnStageCheck.d.ts +23 -0
  37. package/dist/calls/webinar/QAPanel.d.ts +56 -0
  38. package/dist/calls/webinar/RaiseHandButton.d.ts +14 -0
  39. package/dist/calls/webinar/WebinarAudiencePanel.d.ts +23 -0
  40. package/dist/calls/webinar/WebinarViewer.d.ts +39 -0
  41. package/dist/calls/webinar/WebinarWaiting.d.ts +19 -0
  42. package/dist/calls/webinar/hooks.d.ts +13 -0
  43. package/dist/calls/webinar/index.d.ts +14 -0
  44. package/dist/calls/webinar/live.d.ts +43 -0
  45. package/dist/calls/webinar/types.d.ts +49 -0
  46. package/dist/chat/Message.d.ts +17 -2
  47. package/dist/chat/TranscriptLine.d.ts +50 -0
  48. package/dist/chat/index.cjs +147 -126
  49. package/dist/chat/index.d.ts +3 -1
  50. package/dist/chat/index.js +5 -4
  51. package/dist/chat/transcript-runs.d.ts +6 -0
  52. package/dist/{chunk-GKDWQLVU.js → chunk-2C7TCVQZ.js} +172 -121
  53. package/dist/chunk-2C7TCVQZ.js.map +1 -0
  54. package/dist/{chunk-QKWQMOHN.cjs → chunk-2UAJVK3C.cjs} +4 -4
  55. package/dist/{chunk-QKWQMOHN.cjs.map → chunk-2UAJVK3C.cjs.map} +1 -1
  56. package/dist/{chunk-54WBIVHV.cjs → chunk-2WO5L7YT.cjs} +5 -5
  57. package/dist/{chunk-54WBIVHV.cjs.map → chunk-2WO5L7YT.cjs.map} +1 -1
  58. package/dist/{chunk-7C35WXYB.js → chunk-5HFPRI3W.js} +5 -5
  59. package/dist/chunk-5HFPRI3W.js.map +1 -0
  60. package/dist/{chunk-ZSV2NQQP.js → chunk-6RHHMBZ5.js} +158 -5
  61. package/dist/chunk-6RHHMBZ5.js.map +1 -0
  62. package/dist/{chunk-FQX6N5G6.js → chunk-6Z7DLJU2.js} +35 -18
  63. package/dist/chunk-6Z7DLJU2.js.map +1 -0
  64. package/dist/{chunk-PGDL26TQ.js → chunk-AY32GGKU.js} +10 -4
  65. package/dist/{chunk-PGDL26TQ.js.map → chunk-AY32GGKU.js.map} +1 -1
  66. package/dist/{chunk-S55VDTX3.js → chunk-CECWAIHL.js} +3 -3
  67. package/dist/{chunk-S55VDTX3.js.map → chunk-CECWAIHL.js.map} +1 -1
  68. package/dist/{chunk-UABPHZWZ.js → chunk-CXZ7FPTQ.js} +3 -3
  69. package/dist/{chunk-UABPHZWZ.js.map → chunk-CXZ7FPTQ.js.map} +1 -1
  70. package/dist/{chunk-C2HRZSAT.cjs → chunk-DJM7DLCT.cjs} +5 -5
  71. package/dist/chunk-DJM7DLCT.cjs.map +1 -0
  72. package/dist/{chunk-GBYY3PIC.cjs → chunk-FXUQZFAN.cjs} +79 -62
  73. package/dist/chunk-FXUQZFAN.cjs.map +1 -0
  74. package/dist/{chunk-W7NZRZ54.js → chunk-GFYGYME3.js} +3 -3
  75. package/dist/{chunk-W7NZRZ54.js.map → chunk-GFYGYME3.js.map} +1 -1
  76. package/dist/chunk-GICUGQNN.cjs +76 -0
  77. package/dist/chunk-GICUGQNN.cjs.map +1 -0
  78. package/dist/{chunk-I4XQCE6U.cjs → chunk-HBBJYRGI.cjs} +162 -5
  79. package/dist/{chunk-ZSV2NQQP.js.map → chunk-HBBJYRGI.cjs.map} +1 -1
  80. package/dist/chunk-JJLMCV3T.js +73 -0
  81. package/dist/chunk-JJLMCV3T.js.map +1 -0
  82. package/dist/chunk-MWDW5UBW.cjs +199 -0
  83. package/dist/chunk-MWDW5UBW.cjs.map +1 -0
  84. package/dist/{chunk-NUTLXVPX.js → chunk-NRUPUDWB.js} +16 -7
  85. package/dist/chunk-NRUPUDWB.js.map +1 -0
  86. package/dist/{chunk-5YXG342J.cjs → chunk-OR4CO743.cjs} +3 -3
  87. package/dist/{chunk-5YXG342J.cjs.map → chunk-OR4CO743.cjs.map} +1 -1
  88. package/dist/{chunk-DDF5GPIJ.js → chunk-P5ST47VY.js} +3 -3
  89. package/dist/{chunk-DDF5GPIJ.js.map → chunk-P5ST47VY.js.map} +1 -1
  90. package/dist/{chunk-PEHLUB52.js → chunk-Q6B77ZWZ.js} +3 -3
  91. package/dist/{chunk-PEHLUB52.js.map → chunk-Q6B77ZWZ.js.map} +1 -1
  92. package/dist/{chunk-ELDUWTRR.cjs → chunk-TF47ATYS.cjs} +4 -4
  93. package/dist/{chunk-ELDUWTRR.cjs.map → chunk-TF47ATYS.cjs.map} +1 -1
  94. package/dist/{chunk-PDRYGPZB.cjs → chunk-VP4KJ4LK.cjs} +16 -6
  95. package/dist/chunk-VP4KJ4LK.cjs.map +1 -0
  96. package/dist/{chunk-SKSE4OT6.cjs → chunk-WPVIOGDG.cjs} +5 -5
  97. package/dist/{chunk-SKSE4OT6.cjs.map → chunk-WPVIOGDG.cjs.map} +1 -1
  98. package/dist/{chunk-JB32KYAH.js → chunk-WTM65ZNG.js} +3 -3
  99. package/dist/{chunk-JB32KYAH.js.map → chunk-WTM65ZNG.js.map} +1 -1
  100. package/dist/{chunk-2NIL2ASF.cjs → chunk-XIGWUDOD.cjs} +170 -119
  101. package/dist/chunk-XIGWUDOD.cjs.map +1 -0
  102. package/dist/{chunk-3US5OIDQ.cjs → chunk-XSUOEJB3.cjs} +9 -3
  103. package/dist/chunk-XSUOEJB3.cjs.map +1 -0
  104. package/dist/chunk-Z3AZ7Q5X.js +177 -0
  105. package/dist/chunk-Z3AZ7Q5X.js.map +1 -0
  106. package/dist/{chunk-Z5VUZ6YD.cjs → chunk-ZV3BKLFY.cjs} +3 -3
  107. package/dist/{chunk-Z5VUZ6YD.cjs.map → chunk-ZV3BKLFY.cjs.map} +1 -1
  108. package/dist/collaboration/index.cjs +42 -42
  109. package/dist/collaboration/index.js +2 -2
  110. package/dist/connect/docx.cjs +6 -6
  111. package/dist/connect/docx.js +4 -4
  112. package/dist/connect/index.cjs +72 -72
  113. package/dist/connect/index.js +18 -18
  114. package/dist/docx/AgentUndo.d.ts +4 -0
  115. package/dist/docx/index.cjs +46 -46
  116. package/dist/docx/index.js +3 -3
  117. package/dist/facts/index.cjs +4 -4
  118. package/dist/facts/index.js +4 -4
  119. package/dist/files/index.cjs +6 -6
  120. package/dist/files/index.js +6 -6
  121. package/dist/home/index.cjs +17 -17
  122. package/dist/home/index.js +3 -3
  123. package/dist/index.cjs +958 -937
  124. package/dist/index.cjs.map +1 -1
  125. package/dist/index.js +24 -23
  126. package/dist/index.js.map +1 -1
  127. package/dist/retrieval/index.cjs +1 -1
  128. package/dist/retrieval/index.js +1 -1
  129. package/dist/settings/index.cjs +50 -104
  130. package/dist/settings/index.cjs.map +1 -1
  131. package/dist/settings/index.js +12 -72
  132. package/dist/settings/index.js.map +1 -1
  133. package/dist/shell/index.cjs +12 -12
  134. package/dist/shell/index.js +4 -4
  135. package/dist/site/index.cjs +5 -5
  136. package/dist/site/index.js +1 -1
  137. package/dist/studio/index.cjs +5 -5
  138. package/dist/studio/index.js +4 -4
  139. package/dist/workflow-canvas/index.cjs +53 -53
  140. package/dist/workflow-canvas/index.js +10 -10
  141. package/dist/workspaces/index.cjs +2 -2
  142. package/dist/workspaces/index.js +2 -2
  143. package/node_modules/@arbidocs/coedit/dist/commands.cjs +179 -9
  144. package/node_modules/@arbidocs/coedit/dist/commands.cjs.map +1 -1
  145. package/node_modules/@arbidocs/coedit/dist/commands.d.cts +81 -1
  146. package/node_modules/@arbidocs/coedit/dist/commands.d.ts +81 -1
  147. package/node_modules/@arbidocs/coedit/dist/commands.js +173 -11
  148. package/node_modules/@arbidocs/coedit/dist/commands.js.map +1 -1
  149. package/node_modules/@arbidocs/coedit/dist/server.cjs +58 -2
  150. package/node_modules/@arbidocs/coedit/dist/server.cjs.map +1 -1
  151. package/node_modules/@arbidocs/coedit/dist/server.d.cts +29 -1
  152. package/node_modules/@arbidocs/coedit/dist/server.d.ts +29 -1
  153. package/node_modules/@arbidocs/coedit/dist/server.js +58 -3
  154. package/node_modules/@arbidocs/coedit/dist/server.js.map +1 -1
  155. package/node_modules/@arbidocs/coedit/package.json +1 -1
  156. package/package.json +15 -5
  157. package/src/calls/calls.css +157 -0
  158. package/dist/chunk-2NIL2ASF.cjs.map +0 -1
  159. package/dist/chunk-3US5OIDQ.cjs.map +0 -1
  160. package/dist/chunk-7C35WXYB.js.map +0 -1
  161. package/dist/chunk-C2HRZSAT.cjs.map +0 -1
  162. package/dist/chunk-FQX6N5G6.js.map +0 -1
  163. package/dist/chunk-GBYY3PIC.cjs.map +0 -1
  164. package/dist/chunk-GKDWQLVU.js.map +0 -1
  165. package/dist/chunk-I4XQCE6U.cjs.map +0 -1
  166. package/dist/chunk-NUTLXVPX.js.map +0 -1
  167. package/dist/chunk-PDRYGPZB.cjs.map +0 -1
@@ -0,0 +1,2753 @@
1
+ "use client";
2
+ import { ConfirmDialog } from '../chunk-JJLMCV3T.js';
3
+ import { TabHeader } from '../chunk-4WWM3GQ5.js';
4
+ import '../chunk-TX2ZICS7.js';
5
+ import { NOTICE_MS, gridSlots, gridLayout, spotlightBox, CAPTION_TTL_MS, CAPTION_LINES, speakerAccent, formatElapsed, initialsOf } from '../chunk-Z3AZ7Q5X.js';
6
+ export { CAPTION_LINES, CAPTION_TTL_MS, INITIAL_SPOTLIGHT, MAX_GRID_TILES, NOTICE_MS, SPEAKER_ACCENTS, SPEAKING_LEVEL, TILE_ASPECT, formatElapsed, gridLayout, gridSlots, initialsOf, nearestCorner, nextSpotlight, pickSpotlight, rowCounts, speakerAccent, speakerBorder, spotlightBox, stageContentHeight } from '../chunk-Z3AZ7Q5X.js';
7
+ import '../chunk-NPLYHR2E.js';
8
+ import '../chunk-7F6CBP3Z.js';
9
+ import { fromNow } from '../chunk-OXF2IFXG.js';
10
+ import '../chunk-VUNV25KB.js';
11
+ import { forwardRef, useState, useRef, useMemo, useEffect, useCallback } from 'react';
12
+ import { Button, cn, Tooltip, TooltipTrigger, TooltipContent, Separator, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuLabel, DropdownMenuItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuCheckboxItem, TabsList, TabsTrigger, Tabs, TabsContent, Input, Textarea, Dialog, DialogContent, DialogTitle, DialogDescription, Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@arbidocs/react/ui';
13
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
14
+ import { MonitorUp, MicOff, AudioLines, SignalLow, Users, Mic, Captions, Lock, LockOpen, Video, VideoOff, ChevronUp, Loader2, Circle, PhoneOff, MoreHorizontal, LayoutGrid, UserRound, PictureInPicture2, Phone, X, Maximize2, BellOff, Bell, CalendarCheck, Radio, SquareX, EyeOff, Hand, Search, Send, MessageCircleQuestion, UserX, CheckCheck, RotateCcw, Trash2, MessageSquareText } from 'lucide-react';
15
+
16
+ var TONE = {
17
+ neutral: "bg-muted text-foreground hover:bg-accent",
18
+ // Solid: a control whose thing is on reads as on at a glance (CC on, chat open, sharing).
19
+ active: "bg-primary text-primary-foreground hover:bg-primary/90",
20
+ off: "bg-red-bg text-red-text hover:bg-red-bg/80",
21
+ danger: "bg-destructive text-destructive-foreground hover:bg-destructive/90"
22
+ };
23
+ var CallControlButton = forwardRef(
24
+ function CallControlButton2({
25
+ label,
26
+ shortcut,
27
+ tone = "neutral",
28
+ side = "top",
29
+ tooltip = true,
30
+ className,
31
+ children,
32
+ ...props
33
+ }, ref) {
34
+ const button = /* @__PURE__ */ jsx(
35
+ Button,
36
+ {
37
+ ref,
38
+ type: "button",
39
+ variant: "ghost",
40
+ size: "icon",
41
+ "aria-label": label,
42
+ className: cn("relative shrink-0 [&_svg]:size-[18px]", TONE[tone], className),
43
+ ...props,
44
+ children
45
+ }
46
+ );
47
+ if (!tooltip) return button;
48
+ return /* @__PURE__ */ jsxs(Tooltip, { children: [
49
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx("span", { className: "inline-flex shrink-0", children: button }) }),
50
+ /* @__PURE__ */ jsxs(TooltipContent, { side, children: [
51
+ label,
52
+ shortcut && /* @__PURE__ */ jsx("kbd", { className: "ml-2 rounded-sm border border-border px-1 font-sans text-[11px] text-muted-foreground", children: shortcut })
53
+ ] })
54
+ ] });
55
+ }
56
+ );
57
+ function ControlBadge({
58
+ count,
59
+ tone = "alert",
60
+ testId
61
+ }) {
62
+ return /* @__PURE__ */ jsx(
63
+ "span",
64
+ {
65
+ className: cn(
66
+ "pointer-events-none absolute -right-1 min-w-[1.125rem] rounded-full px-1 text-center text-[10px] font-semibold leading-[1.125rem]",
67
+ tone === "alert" ? "-top-1 bg-destructive text-destructive-foreground ring-2 ring-background" : "-bottom-1 bg-background text-muted-foreground ring-1 ring-border"
68
+ ),
69
+ "data-testid": testId,
70
+ children: count > 99 ? "99+" : count
71
+ }
72
+ );
73
+ }
74
+ function CallStatusChip({
75
+ tone,
76
+ icon,
77
+ label,
78
+ compact = false,
79
+ pulse = false,
80
+ title,
81
+ children,
82
+ className,
83
+ testId,
84
+ ...rest
85
+ }) {
86
+ const tones = {
87
+ red: "border-red-border bg-red-bg text-red-text",
88
+ green: "border-green-border bg-green-bg text-green-text",
89
+ yellow: "border-yellow-border bg-yellow-bg text-yellow-text",
90
+ neutral: "border-border bg-muted text-muted-foreground"
91
+ };
92
+ return /* @__PURE__ */ jsxs(
93
+ "span",
94
+ {
95
+ className: cn(
96
+ "inline-flex h-6 shrink-0 items-center gap-1.5 rounded-md border px-2 text-xs font-medium",
97
+ tones[tone],
98
+ className
99
+ ),
100
+ title,
101
+ "data-testid": testId,
102
+ ...rest,
103
+ children: [
104
+ pulse && /* @__PURE__ */ jsxs("span", { className: "relative flex size-2 shrink-0", "aria-hidden": true, children: [
105
+ /* @__PURE__ */ jsx("span", { className: "absolute inline-flex size-full animate-ping rounded-full bg-red-solid opacity-60 motion-reduce:hidden" }),
106
+ /* @__PURE__ */ jsx("span", { className: "relative inline-flex size-2 rounded-full bg-red-solid" })
107
+ ] }),
108
+ icon,
109
+ /* @__PURE__ */ jsx("span", { className: cn("min-w-0 truncate", compact && "sr-only"), children: label }),
110
+ children
111
+ ]
112
+ }
113
+ );
114
+ }
115
+ function useElementSize() {
116
+ const [size, setSize] = useState({ width: 0, height: 0 });
117
+ const observer = useRef(null);
118
+ const ref = useMemo(
119
+ () => (el) => {
120
+ observer.current?.disconnect();
121
+ observer.current = null;
122
+ if (!el || typeof ResizeObserver === "undefined") return;
123
+ const ro = new ResizeObserver((entries) => {
124
+ const r = entries[0]?.contentRect;
125
+ if (!r) return;
126
+ setSize(
127
+ (s) => s.width === r.width && s.height === r.height ? s : { width: r.width, height: r.height }
128
+ );
129
+ });
130
+ ro.observe(el);
131
+ observer.current = ro;
132
+ },
133
+ []
134
+ );
135
+ useEffect(() => () => observer.current?.disconnect(), []);
136
+ return [ref, size];
137
+ }
138
+ function useBriefNotice(announce = true, onFolded, ms = NOTICE_MS) {
139
+ const [expanded, setExpanded] = useState(announce);
140
+ const folded = useRef(onFolded);
141
+ folded.current = onFolded;
142
+ useEffect(() => {
143
+ if (!expanded) return;
144
+ const id = setTimeout(() => {
145
+ setExpanded(false);
146
+ folded.current?.();
147
+ }, ms);
148
+ return () => clearTimeout(id);
149
+ }, [expanded, ms]);
150
+ return expanded;
151
+ }
152
+ function typing(e) {
153
+ const el = e.target;
154
+ return !!el?.closest?.('input, textarea, select, [contenteditable="true"], [role="textbox"]');
155
+ }
156
+ function useShortcutKeys(ref, enabled) {
157
+ useEffect(() => {
158
+ if (!enabled) return;
159
+ const onKey = (e) => {
160
+ if (e.repeat || e.ctrlKey || e.metaKey || e.altKey || typing(e)) return;
161
+ const key = e.key.toLowerCase();
162
+ if (!/^[a-z]$/.test(key)) return;
163
+ const button = ref.current?.querySelector(
164
+ `[data-call-shortcut="${key}"]:not(:disabled)`
165
+ );
166
+ if (!button) return;
167
+ e.preventDefault();
168
+ button.click();
169
+ };
170
+ window.addEventListener("keydown", onKey);
171
+ return () => window.removeEventListener("keydown", onKey);
172
+ }, [ref, enabled]);
173
+ }
174
+ var CallTile = forwardRef(function CallTile2({
175
+ name,
176
+ initials,
177
+ video,
178
+ source = "camera",
179
+ local = false,
180
+ mirror = false,
181
+ speaking = false,
182
+ level = 0,
183
+ micMuted = false,
184
+ poorConnection = false,
185
+ size = "normal",
186
+ youLabel,
187
+ className,
188
+ style,
189
+ ...rest
190
+ }, ref) {
191
+ const screen = source === "screen";
192
+ const ring = speaking && !screen ? Math.min(1, Math.max(0, level)) : 0;
193
+ return /* @__PURE__ */ jsxs(
194
+ "div",
195
+ {
196
+ ref,
197
+ className: cn("call-tile", className),
198
+ "data-call-tile": "",
199
+ "data-source": source,
200
+ "data-local": local,
201
+ "data-mirror": mirror && source === "camera" && !!video,
202
+ "data-speaking": speaking && !screen,
203
+ style: { "--call-level": ring.toFixed(2), ...style },
204
+ ...rest,
205
+ children: [
206
+ video ?? /* @__PURE__ */ jsx("div", { className: "call-tile-placeholder", "aria-hidden": true, children: /* @__PURE__ */ jsx("span", { className: cn("call-tile-avatar", size === "small" && "call-tile-avatar-sm"), children: initials }) }),
207
+ /* @__PURE__ */ jsxs("div", { className: "call-tile-name", children: [
208
+ typeof name === "string" ? /* @__PURE__ */ jsxs("span", { className: "call-tile-chip min-w-0", children: [
209
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: name }),
210
+ local && !screen && youLabel && /* @__PURE__ */ jsx("span", { className: "shrink-0 opacity-80", children: youLabel })
211
+ ] }) : /* @__PURE__ */ jsx("span", { className: "call-tile-chip min-w-0 overflow-hidden py-0.5 pl-0.5", children: name }),
212
+ (screen || micMuted || speaking) && /* @__PURE__ */ jsxs("span", { className: "call-tile-chip shrink-0", children: [
213
+ screen && /* @__PURE__ */ jsx(MonitorUp, { className: "size-3.5", "aria-label": "Screen" }),
214
+ !screen && micMuted && /* @__PURE__ */ jsx(MicOff, { className: "size-3.5", "aria-label": "Microphone off" }),
215
+ speaking && !screen && /* @__PURE__ */ jsx(AudioLines, { className: "call-tile-speaking size-3.5", "aria-label": "Speaking" })
216
+ ] })
217
+ ] }),
218
+ poorConnection && /* @__PURE__ */ jsx("span", { className: "call-tile-signal", title: "Poor connection", children: /* @__PURE__ */ jsx(SignalLow, { className: "size-3.5", "aria-label": "Poor connection" }) })
219
+ ]
220
+ }
221
+ );
222
+ });
223
+ var GRID_GAP = 8;
224
+ function CallGrid({
225
+ items,
226
+ getKey,
227
+ renderTile,
228
+ moreLabel = (n) => `${n} more`,
229
+ className
230
+ }) {
231
+ const [ref, size] = useElementSize();
232
+ const { shown, hidden } = gridSlots(items);
233
+ const count = shown.length + (hidden ? 1 : 0);
234
+ const layout = gridLayout(count, size.width, size.height, GRID_GAP);
235
+ const box = { width: layout.tileWidth, height: layout.tileHeight };
236
+ const rows = [];
237
+ let next = 0;
238
+ for (const n of layout.rowCounts) rows.push(Array.from({ length: n }, () => next++));
239
+ return /* @__PURE__ */ jsx(
240
+ "div",
241
+ {
242
+ ref,
243
+ className: cn("flex size-full min-h-0 flex-col items-center", className),
244
+ style: { gap: GRID_GAP },
245
+ "data-testid": "call-grid",
246
+ "data-cols": layout.cols,
247
+ children: layout.tileWidth > 0 && rows.map((row, r) => /* @__PURE__ */ jsx("div", { className: "flex justify-center", style: { gap: GRID_GAP }, children: row.map(
248
+ (i) => i < shown.length ? /* @__PURE__ */ jsx("div", { className: "call-tile-box", style: box, children: renderTile(shown[i]) }, getKey(shown[i])) : /* @__PURE__ */ jsxs("div", { className: "call-tile call-tile-more", style: box, children: [
249
+ /* @__PURE__ */ jsx(Users, { className: "size-5", "aria-hidden": true }),
250
+ /* @__PURE__ */ jsx("span", { children: moreLabel(hidden) })
251
+ ] }, "more")
252
+ ) }, r))
253
+ }
254
+ );
255
+ }
256
+ function CallFocus({
257
+ focus,
258
+ others,
259
+ getKey,
260
+ renderTile,
261
+ className,
262
+ testId = "call-focus"
263
+ }) {
264
+ const [ref, size] = useElementSize();
265
+ const narrow = size.width > 0 && size.width < 640;
266
+ const box = spotlightBox(size.width, size.height, others.length, narrow);
267
+ return /* @__PURE__ */ jsxs(
268
+ "div",
269
+ {
270
+ ref,
271
+ className: cn(
272
+ "flex size-full min-h-0 gap-2",
273
+ narrow ? "flex-col items-center" : "flex-row items-start justify-center",
274
+ className
275
+ ),
276
+ "data-testid": testId,
277
+ children: [
278
+ /* @__PURE__ */ jsx(
279
+ "div",
280
+ {
281
+ className: "call-tile-box shrink-0",
282
+ style: { width: box.width, height: box.height },
283
+ "data-testid": "call-spotlight",
284
+ children: box.width > 0 && renderTile(focus, "normal")
285
+ },
286
+ getKey(focus)
287
+ ),
288
+ others.length > 0 && /* @__PURE__ */ jsx(
289
+ "div",
290
+ {
291
+ className: cn(
292
+ "grid shrink-0 gap-2",
293
+ narrow ? "w-full grid-cols-3 content-start" : "w-60 grid-cols-1 overflow-y-auto"
294
+ ),
295
+ style: narrow ? { width: box.width } : void 0,
296
+ "data-testid": "call-filmstrip",
297
+ children: others.map((t) => /* @__PURE__ */ jsx("div", { className: "call-tile-box aspect-video w-full min-w-0", children: renderTile(t, "small") }, getKey(t)))
298
+ }
299
+ )
300
+ ]
301
+ }
302
+ );
303
+ }
304
+ function FoldingLabel({
305
+ sentence,
306
+ short,
307
+ expanded
308
+ }) {
309
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
310
+ /* @__PURE__ */ jsx("span", { className: cn(!expanded && "sr-only"), children: sentence }),
311
+ !expanded && /* @__PURE__ */ jsx("span", { "aria-hidden": true, children: short })
312
+ ] });
313
+ }
314
+ function RecordingStatusChip({
315
+ sentence,
316
+ short = "Recording",
317
+ compact = false,
318
+ announce = true,
319
+ onAnnounced,
320
+ className,
321
+ ...rest
322
+ }) {
323
+ const expanded = useBriefNotice(announce, onAnnounced);
324
+ return /* @__PURE__ */ jsx(
325
+ CallStatusChip,
326
+ {
327
+ tone: "red",
328
+ pulse: true,
329
+ role: "status",
330
+ "aria-live": "polite",
331
+ label: /* @__PURE__ */ jsx(FoldingLabel, { sentence, short, expanded }),
332
+ compact: compact && !expanded,
333
+ title: sentence,
334
+ className,
335
+ "data-expanded": expanded,
336
+ ...rest
337
+ }
338
+ );
339
+ }
340
+ function TranscriptionStatusChip({
341
+ sentence,
342
+ announce = true,
343
+ onAnnounced,
344
+ short = "Transcribing",
345
+ detail,
346
+ muted,
347
+ onMutedChange,
348
+ muteLabel,
349
+ unmuteLabel,
350
+ compact = false,
351
+ className,
352
+ ...rest
353
+ }) {
354
+ const expanded = useBriefNotice(announce, onAnnounced);
355
+ const label = muted ? unmuteLabel : muteLabel;
356
+ return /* @__PURE__ */ jsx(
357
+ CallStatusChip,
358
+ {
359
+ tone: "red",
360
+ role: "status",
361
+ "aria-live": "polite",
362
+ icon: /* @__PURE__ */ jsx(Captions, { className: "size-3.5 shrink-0", "aria-hidden": true }),
363
+ label: /* @__PURE__ */ jsx(FoldingLabel, { sentence, short, expanded }),
364
+ compact: compact && !expanded,
365
+ title: detail ? `${sentence}. ${detail}` : sentence,
366
+ className: cn("pr-0.5", className),
367
+ "data-expanded": expanded,
368
+ ...rest,
369
+ children: /* @__PURE__ */ jsxs(Tooltip, { children: [
370
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
371
+ "button",
372
+ {
373
+ type: "button",
374
+ className: "inline-flex h-5! w-5! shrink-0 items-center justify-center rounded-sm p-0! hover:bg-red-text/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
375
+ "aria-pressed": muted,
376
+ "aria-label": label,
377
+ onClick: () => onMutedChange(!muted),
378
+ "data-testid": "transcription-mute",
379
+ children: muted ? /* @__PURE__ */ jsx(MicOff, { className: "size-3.5", "aria-hidden": true }) : /* @__PURE__ */ jsx(Mic, { className: "size-3.5", "aria-hidden": true })
380
+ }
381
+ ) }),
382
+ /* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: label })
383
+ ] })
384
+ }
385
+ );
386
+ }
387
+ function EncryptionStatusChip({
388
+ state,
389
+ compact = false,
390
+ labels = {
391
+ on: "Encrypted",
392
+ pending: "Encrypting\u2026",
393
+ off: "Not encrypted",
394
+ onTitle: "Media is end-to-end encrypted; the media server only sees ciphertext.",
395
+ offTitle: "This call is not end-to-end encrypted."
396
+ }
397
+ }) {
398
+ const on = state === "on";
399
+ return /* @__PURE__ */ jsx(
400
+ CallStatusChip,
401
+ {
402
+ tone: on ? "neutral" : "yellow",
403
+ icon: on ? /* @__PURE__ */ jsx(Lock, { className: "size-3.5 shrink-0", "aria-hidden": true }) : /* @__PURE__ */ jsx(LockOpen, { className: "size-3.5 shrink-0", "aria-hidden": true }),
404
+ label: on ? labels.on : state === "pending" ? labels.pending : labels.off,
405
+ compact,
406
+ title: on ? labels.onTitle : labels.offTitle,
407
+ "data-testid": "call-encryption-badge",
408
+ "data-state": state
409
+ }
410
+ );
411
+ }
412
+ function CaptionsStrip({
413
+ captions,
414
+ reserve = false,
415
+ waitingText = "Captions appear here as people speak.",
416
+ className
417
+ }) {
418
+ const [now, setNow] = useState(() => Date.now());
419
+ const visible = captions.filter((c) => now - c.receivedAt < CAPTION_TTL_MS).slice(-CAPTION_LINES);
420
+ const showing = visible.length > 0;
421
+ const newest = captions[captions.length - 1]?.receivedAt;
422
+ useEffect(() => {
423
+ setNow(Date.now());
424
+ }, [newest]);
425
+ useEffect(() => {
426
+ if (!showing) return;
427
+ const id = setInterval(() => setNow(Date.now()), 1e3);
428
+ return () => clearInterval(id);
429
+ }, [showing]);
430
+ if (!showing && !reserve) return null;
431
+ return /* @__PURE__ */ jsx(
432
+ "div",
433
+ {
434
+ className: cn(
435
+ "shrink-0 rounded-md bg-muted px-3 py-2 text-sm leading-5 text-foreground",
436
+ className
437
+ ),
438
+ "aria-live": "polite",
439
+ "data-testid": "captions-overlay",
440
+ children: /* @__PURE__ */ jsxs("div", { className: "flex h-10 flex-col justify-end overflow-hidden", children: [
441
+ !showing && /* @__PURE__ */ jsx("p", { className: "truncate text-muted-foreground", "data-testid": "captions-waiting", children: waitingText }),
442
+ visible.map((c) => /* @__PURE__ */ jsxs(
443
+ "p",
444
+ {
445
+ className: "flex min-w-0 shrink-0 gap-1.5",
446
+ "data-testid": "caption-line",
447
+ "data-speaker": c.speakerId,
448
+ children: [
449
+ /* @__PURE__ */ jsx(
450
+ "span",
451
+ {
452
+ className: cn(
453
+ "max-w-[40%] shrink-0 truncate font-semibold",
454
+ speakerAccent(c.speakerId)
455
+ ),
456
+ children: c.name
457
+ }
458
+ ),
459
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: ": " }),
460
+ /* @__PURE__ */ jsx(
461
+ "span",
462
+ {
463
+ className: "flex min-w-0 flex-1 flex-col justify-end overflow-hidden",
464
+ style: { height: `${(visible.length > 1 ? 1 : CAPTION_LINES) * 1.25}rem` },
465
+ children: /* @__PURE__ */ jsx("span", { className: "break-words", children: c.text })
466
+ }
467
+ )
468
+ ]
469
+ },
470
+ `${c.speakerId}:${c.id}`
471
+ ))
472
+ ] })
473
+ }
474
+ );
475
+ }
476
+ function CallControlBar({
477
+ left,
478
+ right,
479
+ children,
480
+ shortcuts = false,
481
+ className
482
+ }) {
483
+ const ref = useRef(null);
484
+ useShortcutKeys(ref, shortcuts);
485
+ return /* @__PURE__ */ jsxs(
486
+ "div",
487
+ {
488
+ ref,
489
+ className: cn(
490
+ "grid h-16 shrink-0 grid-cols-[1fr_auto_1fr] items-center gap-2 border-t border-border bg-background px-3 sm:px-4",
491
+ className
492
+ ),
493
+ "data-testid": "call-controls",
494
+ children: [
495
+ /* @__PURE__ */ jsx("div", { className: "flex min-w-0 items-center gap-2", children: left }),
496
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children }),
497
+ /* @__PURE__ */ jsx("div", { className: "flex items-center justify-end gap-2", children: right })
498
+ ]
499
+ }
500
+ );
501
+ }
502
+ function ControlGroup({ children }) {
503
+ return /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children });
504
+ }
505
+ function ControlDivider() {
506
+ return /* @__PURE__ */ jsx(Separator, { orientation: "vertical", className: "mx-1 h-6" });
507
+ }
508
+ function ElapsedTime({ since, title }) {
509
+ const [now, setNow] = useState(() => Date.now());
510
+ useEffect(() => {
511
+ const id = setInterval(() => setNow(Date.now()), 1e3);
512
+ return () => clearInterval(id);
513
+ }, []);
514
+ return /* @__PURE__ */ jsx("span", { className: "text-sm tabular-nums text-muted-foreground", title, children: formatElapsed(now - since) });
515
+ }
516
+ function MediaControl({
517
+ kind,
518
+ enabled,
519
+ pending = false,
520
+ onToggle,
521
+ devices,
522
+ activeDeviceId,
523
+ onSelectDevice,
524
+ shortcut,
525
+ testId,
526
+ options = []
527
+ }) {
528
+ const mic = kind === "microphone";
529
+ const label = enabled ? `Turn off ${kind}` : `Turn on ${kind}`;
530
+ const Icon = mic ? enabled ? Mic : MicOff : enabled ? Video : VideoOff;
531
+ const title = mic ? "Microphone" : "Camera";
532
+ return /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center", role: "group", "aria-label": title, children: [
533
+ /* @__PURE__ */ jsx(
534
+ CallControlButton,
535
+ {
536
+ label,
537
+ shortcut,
538
+ tone: enabled ? "neutral" : "off",
539
+ "aria-pressed": enabled,
540
+ disabled: pending,
541
+ onClick: onToggle,
542
+ className: "rounded-r-none",
543
+ "data-testid": testId,
544
+ "data-call-shortcut": shortcut?.toLowerCase(),
545
+ "data-enabled": enabled,
546
+ children: /* @__PURE__ */ jsx(Icon, { "aria-hidden": true })
547
+ }
548
+ ),
549
+ /* @__PURE__ */ jsxs(DropdownMenu, { children: [
550
+ /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
551
+ Button,
552
+ {
553
+ type: "button",
554
+ variant: "ghost",
555
+ "aria-label": `Choose ${kind}`,
556
+ title: `Choose ${kind}`,
557
+ className: cn(
558
+ "h-9 w-6 shrink-0 rounded-l-none border-l px-0 pointer-coarse:h-11! pointer-coarse:w-8",
559
+ enabled ? "border-background bg-muted text-muted-foreground hover:bg-accent hover:text-foreground" : "border-background bg-red-bg text-red-text hover:bg-red-bg/80"
560
+ ),
561
+ "data-testid": `${testId}-devices`,
562
+ children: /* @__PURE__ */ jsx(ChevronUp, { className: "size-3.5", "aria-hidden": true })
563
+ }
564
+ ) }),
565
+ /* @__PURE__ */ jsxs(DropdownMenuContent, { side: "top", align: "start", className: "w-72", children: [
566
+ /* @__PURE__ */ jsx(DropdownMenuLabel, { className: "text-xs font-medium text-muted-foreground", children: title }),
567
+ devices.length === 0 ? /* @__PURE__ */ jsxs(DropdownMenuItem, { disabled: true, children: [
568
+ "No ",
569
+ kind,
570
+ " found"
571
+ ] }) : /* @__PURE__ */ jsx(DropdownMenuRadioGroup, { value: activeDeviceId, onValueChange: onSelectDevice, children: devices.map((d, i) => /* @__PURE__ */ jsx(DropdownMenuRadioItem, { value: d.deviceId, children: /* @__PURE__ */ jsx("span", { className: "truncate", children: d.label || `${title} ${i + 1}` }) }, d.deviceId)) }),
572
+ options.length > 0 && /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
573
+ options.map((o) => /* @__PURE__ */ jsx(
574
+ DropdownMenuCheckboxItem,
575
+ {
576
+ checked: o.checked,
577
+ onCheckedChange: (c) => o.onChange(c === true),
578
+ "data-testid": o.testId,
579
+ children: o.label
580
+ },
581
+ o.label
582
+ ))
583
+ ] })
584
+ ] })
585
+ ] });
586
+ }
587
+ function RecordControl({
588
+ on,
589
+ busy = false,
590
+ modes,
591
+ note,
592
+ onStart,
593
+ onStop
594
+ }) {
595
+ const [choosing, setChoosing] = useState(false);
596
+ const menuRef = useRef(null);
597
+ useEffect(() => {
598
+ if (!choosing) return;
599
+ const onDown = (e) => {
600
+ if (!menuRef.current?.contains(e.target)) setChoosing(false);
601
+ };
602
+ const onKey = (e) => {
603
+ if (e.key === "Escape") setChoosing(false);
604
+ };
605
+ document.addEventListener("pointerdown", onDown);
606
+ document.addEventListener("keydown", onKey);
607
+ return () => {
608
+ document.removeEventListener("pointerdown", onDown);
609
+ document.removeEventListener("keydown", onKey);
610
+ };
611
+ }, [choosing]);
612
+ const base = "h-9 shrink-0 gap-2 px-3 pointer-coarse:h-11!";
613
+ if (on) {
614
+ return /* @__PURE__ */ jsxs(
615
+ Button,
616
+ {
617
+ type: "button",
618
+ variant: "ghost",
619
+ className: cn(base, "bg-red-bg text-red-text hover:bg-red-bg/80"),
620
+ disabled: busy,
621
+ onClick: onStop,
622
+ "data-testid": "recording-stop",
623
+ children: [
624
+ busy ? /* @__PURE__ */ jsx(Loader2, { className: "size-4 animate-spin", "aria-hidden": true }) : /* @__PURE__ */ jsxs("span", { className: "relative flex size-2.5", "aria-hidden": true, children: [
625
+ /* @__PURE__ */ jsx("span", { className: "absolute inline-flex size-full animate-ping rounded-full bg-red-solid opacity-60 motion-reduce:hidden" }),
626
+ /* @__PURE__ */ jsx("span", { className: "relative inline-flex size-2.5 rounded-full bg-red-solid" })
627
+ ] }),
628
+ "Stop recording"
629
+ ]
630
+ }
631
+ );
632
+ }
633
+ return /* @__PURE__ */ jsxs("div", { ref: menuRef, className: "relative", children: [
634
+ /* @__PURE__ */ jsxs(
635
+ Button,
636
+ {
637
+ type: "button",
638
+ variant: "ghost",
639
+ className: cn(base, "bg-muted text-foreground hover:bg-accent"),
640
+ disabled: busy,
641
+ "aria-expanded": choosing,
642
+ "aria-haspopup": "menu",
643
+ onClick: () => setChoosing((c) => !c),
644
+ "data-testid": "recording-start",
645
+ children: [
646
+ busy ? /* @__PURE__ */ jsx(Loader2, { className: "size-4 animate-spin", "aria-hidden": true }) : /* @__PURE__ */ jsx(Circle, { className: "size-3 fill-red-solid text-red-solid", "aria-hidden": true }),
647
+ "Record"
648
+ ]
649
+ }
650
+ ),
651
+ choosing && /* @__PURE__ */ jsxs(
652
+ "div",
653
+ {
654
+ role: "menu",
655
+ "aria-label": "Record",
656
+ className: "absolute bottom-full left-1/2 z-50 mb-2 w-64 -translate-x-1/2 rounded-md border bg-popover p-1 text-popover-foreground shadow-lg",
657
+ children: [
658
+ note && /* @__PURE__ */ jsx("p", { className: "px-2 py-1.5 text-xs text-muted-foreground", children: note }),
659
+ modes.map((m) => /* @__PURE__ */ jsxs(
660
+ "button",
661
+ {
662
+ type: "button",
663
+ role: "menuitem",
664
+ className: "flex w-full flex-col items-start rounded-sm px-2 py-1.5 text-left text-sm outline-none hover:bg-accent focus-visible:bg-accent pointer-coarse:min-h-11",
665
+ onClick: () => {
666
+ setChoosing(false);
667
+ onStart(m.mode);
668
+ },
669
+ "data-testid": m.testId,
670
+ children: [
671
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: m.label }),
672
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: m.description })
673
+ ]
674
+ },
675
+ m.mode
676
+ ))
677
+ ]
678
+ }
679
+ )
680
+ ] });
681
+ }
682
+ function LeaveButton({
683
+ onLeave,
684
+ compact = false,
685
+ label = "Leave",
686
+ title = "Leave call",
687
+ options
688
+ }) {
689
+ const menu = !!options?.length;
690
+ const button = /* @__PURE__ */ jsxs(
691
+ Button,
692
+ {
693
+ type: "button",
694
+ variant: "destructive",
695
+ size: compact ? "icon" : "default",
696
+ className: cn("shrink-0", !compact && "px-4"),
697
+ "aria-label": title,
698
+ title,
699
+ onClick: menu ? void 0 : onLeave,
700
+ "data-testid": "call-leave",
701
+ children: [
702
+ /* @__PURE__ */ jsx(PhoneOff, { className: "size-4", "aria-hidden": true }),
703
+ !compact && label
704
+ ]
705
+ }
706
+ );
707
+ if (!menu) return button;
708
+ return /* @__PURE__ */ jsxs(DropdownMenu, { children: [
709
+ /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: button }),
710
+ /* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", className: "w-56", children: [
711
+ /* @__PURE__ */ jsxs(DropdownMenuItem, { onSelect: onLeave, "data-testid": "call-leave-only", children: [
712
+ /* @__PURE__ */ jsx(PhoneOff, { "aria-hidden": true }),
713
+ label
714
+ ] }),
715
+ options.map((o) => /* @__PURE__ */ jsx(
716
+ DropdownMenuItem,
717
+ {
718
+ onSelect: o.onSelect,
719
+ className: "text-red-text focus:text-red-text",
720
+ "data-testid": o.testId,
721
+ children: o.label
722
+ },
723
+ o.key
724
+ ))
725
+ ] })
726
+ ] });
727
+ }
728
+ var isSection = (g) => !Array.isArray(g);
729
+ function MoreMenu({
730
+ details,
731
+ groups,
732
+ label = "More options"
733
+ }) {
734
+ const shown = groups.filter((g) => isSection(g) || g.length > 0);
735
+ return /* @__PURE__ */ jsxs(DropdownMenu, { children: [
736
+ /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(CallControlButton, { label, tooltip: false, "data-testid": "call-more", children: /* @__PURE__ */ jsx(MoreHorizontal, { "aria-hidden": true }) }) }),
737
+ /* @__PURE__ */ jsxs(DropdownMenuContent, { side: "top", align: "end", className: "w-72", "data-testid": "call-more-menu", children: [
738
+ details && /* @__PURE__ */ jsxs(Fragment, { children: [
739
+ /* @__PURE__ */ jsx(DropdownMenuLabel, { className: "flex flex-col gap-1 font-normal", children: details }),
740
+ shown.length > 0 && /* @__PURE__ */ jsx(DropdownMenuSeparator, {})
741
+ ] }),
742
+ shown.map((group, i) => /* @__PURE__ */ jsxs("div", { role: "none", children: [
743
+ i > 0 && /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
744
+ isSection(group) ? group.content : group.map((item) => /* @__PURE__ */ jsxs(
745
+ DropdownMenuItem,
746
+ {
747
+ onSelect: item.onSelect,
748
+ disabled: item.disabled,
749
+ "data-testid": item.testId,
750
+ children: [
751
+ item.icon,
752
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: item.label }),
753
+ item.trailing
754
+ ]
755
+ },
756
+ item.key
757
+ ))
758
+ ] }, isSection(group) ? group.key : group[0].key))
759
+ ] })
760
+ ] });
761
+ }
762
+ function HeaderIconButton({
763
+ label,
764
+ onClick,
765
+ children,
766
+ testId,
767
+ pressed
768
+ }) {
769
+ return /* @__PURE__ */ jsxs(Tooltip, { children: [
770
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
771
+ Button,
772
+ {
773
+ type: "button",
774
+ variant: "ghost",
775
+ size: "icon",
776
+ className: "text-muted-foreground hover:text-foreground",
777
+ "aria-label": label,
778
+ "aria-pressed": pressed,
779
+ onClick,
780
+ "data-testid": testId,
781
+ children
782
+ }
783
+ ) }),
784
+ /* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: label })
785
+ ] });
786
+ }
787
+ function CallHeader({
788
+ title,
789
+ icon,
790
+ status,
791
+ view,
792
+ onViewChange,
793
+ onMinimise,
794
+ minimiseLabel = "Minimise \u2014 keep working while the call goes on"
795
+ }) {
796
+ return /* @__PURE__ */ jsx("div", { className: "@container", children: /* @__PURE__ */ jsx(
797
+ TabHeader,
798
+ {
799
+ tabs: [
800
+ {
801
+ id: "call",
802
+ label: /* @__PURE__ */ jsxs(Tooltip, { children: [
803
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, showWithText: true, children: /* @__PURE__ */ jsx(
804
+ "span",
805
+ {
806
+ className: "block min-w-0 max-w-[calc(100cqw-16rem)] truncate",
807
+ "data-testid": "call-title",
808
+ children: title
809
+ }
810
+ ) }),
811
+ /* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: title })
812
+ ] }),
813
+ icon: icon ?? /* @__PURE__ */ jsx(Phone, { className: "size-4" })
814
+ }
815
+ ],
816
+ activeTabId: "call",
817
+ onTabChange: () => void 0,
818
+ testId: "call-header",
819
+ className: "pb-0",
820
+ rightContent: (compact) => /* @__PURE__ */ jsxs(Fragment, { children: [
821
+ typeof status === "function" ? status(compact) : status,
822
+ view && onViewChange && /* @__PURE__ */ jsx(
823
+ HeaderIconButton,
824
+ {
825
+ label: view === "speaker" ? "Gallery view (everyone equal)" : "Speaker view (who talks, large)",
826
+ onClick: () => onViewChange(view === "speaker" ? "gallery" : "speaker"),
827
+ testId: "call-view-toggle",
828
+ children: view === "speaker" ? /* @__PURE__ */ jsx(LayoutGrid, { className: "size-4" }) : /* @__PURE__ */ jsx(UserRound, { className: "size-4" })
829
+ }
830
+ ),
831
+ onMinimise && /* @__PURE__ */ jsx(
832
+ HeaderIconButton,
833
+ {
834
+ label: minimiseLabel,
835
+ onClick: onMinimise,
836
+ testId: "call-dock-toggle",
837
+ children: /* @__PURE__ */ jsx(PictureInPicture2, { className: "size-4" })
838
+ }
839
+ )
840
+ ] })
841
+ }
842
+ ) });
843
+ }
844
+ function SpeakingNow({
845
+ names,
846
+ idleText = "No one is speaking",
847
+ className
848
+ }) {
849
+ const talking = names.length > 0;
850
+ return /* @__PURE__ */ jsxs(
851
+ "p",
852
+ {
853
+ className: cn(
854
+ "flex min-w-0 items-center gap-1.5 text-sm",
855
+ talking ? "text-foreground" : "text-muted-foreground",
856
+ className
857
+ ),
858
+ "aria-live": "polite",
859
+ "data-testid": "call-speaking-now",
860
+ children: [
861
+ /* @__PURE__ */ jsx(
862
+ AudioLines,
863
+ {
864
+ className: cn(
865
+ "size-4 shrink-0",
866
+ talking ? "text-theme-cyan motion-safe:animate-pulse" : "text-muted-foreground"
867
+ ),
868
+ "aria-hidden": true
869
+ }
870
+ ),
871
+ talking ? /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-center gap-1 overflow-hidden", children: [
872
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 text-muted-foreground", children: "Speaking:" }),
873
+ names.map((n, i) => /* @__PURE__ */ jsx("span", { className: "flex min-w-0 shrink-0 items-center", children: n }, i))
874
+ ] }) : /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: idleText })
875
+ ]
876
+ }
877
+ );
878
+ }
879
+ function CallNotice({ children }) {
880
+ return /* @__PURE__ */ jsxs(
881
+ "p",
882
+ {
883
+ className: "flex min-w-0 items-center gap-1.5 text-sm text-red-text motion-safe:animate-in motion-safe:fade-in-0",
884
+ role: "status",
885
+ "aria-live": "polite",
886
+ "data-testid": "call-notice",
887
+ children: [
888
+ /* @__PURE__ */ jsx("span", { className: "size-2 shrink-0 rounded-full bg-red-solid", "aria-hidden": true }),
889
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children })
890
+ ]
891
+ }
892
+ );
893
+ }
894
+ function CallStageFrame({
895
+ header,
896
+ subheader,
897
+ stage,
898
+ stageRef,
899
+ stageOverlay,
900
+ people,
901
+ stageHeight,
902
+ controls,
903
+ className
904
+ }) {
905
+ const [bodyRef, body] = useElementSize();
906
+ const PAD_X = 24;
907
+ const PAD_B = 12;
908
+ const sized = !!people && !!stageHeight && body.width > 0 && body.height > 0 ? Math.ceil(stageHeight(body.width - PAD_X, Math.round(body.height * 0.62)) + PAD_B) : null;
909
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex size-full min-h-0 flex-col bg-background text-foreground", className), children: [
910
+ header,
911
+ subheader && /* @__PURE__ */ jsx("div", { className: "flex min-h-9 shrink-0 flex-col justify-center gap-0.5 px-3 py-1", children: subheader }),
912
+ /* @__PURE__ */ jsxs("div", { ref: bodyRef, className: "relative flex min-h-0 flex-1 flex-col", children: [
913
+ /* @__PURE__ */ jsx(
914
+ "div",
915
+ {
916
+ className: cn("flex min-h-0 flex-col px-3 pb-3", sized == null ? "flex-1" : "shrink-0"),
917
+ style: sized == null ? void 0 : { height: sized },
918
+ children: /* @__PURE__ */ jsxs("div", { ref: stageRef, className: "relative min-h-0 flex-1", "data-testid": "call-stage", children: [
919
+ stage,
920
+ stageOverlay
921
+ ] })
922
+ }
923
+ ),
924
+ people
925
+ ] }),
926
+ controls
927
+ ] });
928
+ }
929
+ function CallSidePanel({
930
+ title,
931
+ icon,
932
+ actions,
933
+ onClose,
934
+ closeLabel,
935
+ closeTestId,
936
+ tabs,
937
+ activeTab,
938
+ onTabChange,
939
+ children,
940
+ className,
941
+ ...rest
942
+ }) {
943
+ const tabbed = !!tabs?.length;
944
+ const current = activeTab ?? tabs?.[0]?.id;
945
+ const header = /* @__PURE__ */ jsxs("div", { className: "flex h-12 shrink-0 items-center gap-2 border-b border-border pl-3 pr-1.5", children: [
946
+ tabbed ? /* @__PURE__ */ jsx(TabsList, { className: "h-8 min-w-0 flex-1 justify-start overflow-x-auto bg-transparent p-0", children: tabs.map((t) => /* @__PURE__ */ jsxs(
947
+ TabsTrigger,
948
+ {
949
+ value: t.id,
950
+ className: "h-8 gap-1.5 px-2.5 data-[state=active]:bg-muted data-[state=active]:text-primary data-[state=active]:shadow-none",
951
+ "data-testid": t.testId,
952
+ children: [
953
+ t.label,
954
+ t.count !== void 0 && /* @__PURE__ */ jsx("span", { className: "text-xs tabular-nums text-muted-foreground", children: t.count })
955
+ ]
956
+ },
957
+ t.id
958
+ )) }) : /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 items-center gap-2 text-base font-semibold text-primary", children: [
959
+ icon,
960
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: title })
961
+ ] }),
962
+ actions,
963
+ /* @__PURE__ */ jsx(
964
+ Button,
965
+ {
966
+ type: "button",
967
+ variant: "ghost",
968
+ size: "icon",
969
+ className: "size-8 text-muted-foreground hover:text-foreground",
970
+ "aria-label": closeLabel,
971
+ title: closeLabel,
972
+ onClick: onClose,
973
+ "data-testid": closeTestId,
974
+ children: /* @__PURE__ */ jsx(X, { className: "size-4" })
975
+ }
976
+ )
977
+ ] });
978
+ return /* @__PURE__ */ jsx(
979
+ "aside",
980
+ {
981
+ className: cn("flex min-h-0 flex-col bg-background text-foreground", className),
982
+ "aria-label": title,
983
+ ...rest,
984
+ children: tabbed ? /* @__PURE__ */ jsxs(
985
+ Tabs,
986
+ {
987
+ value: current,
988
+ onValueChange: (id) => onTabChange?.(id),
989
+ className: "flex min-h-0 flex-1 flex-col",
990
+ children: [
991
+ header,
992
+ current && /* @__PURE__ */ jsx(TabsContent, { value: current, className: "mt-0 flex min-h-0 flex-1 flex-col", children })
993
+ ]
994
+ }
995
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
996
+ header,
997
+ children
998
+ ] })
999
+ }
1000
+ );
1001
+ }
1002
+ function VoiceMeter({
1003
+ level,
1004
+ muted = false,
1005
+ className
1006
+ }) {
1007
+ if (muted) {
1008
+ return /* @__PURE__ */ jsx("span", { className: cn("flex items-center text-red-text", className), title: "Microphone off", children: /* @__PURE__ */ jsx(MicOff, { className: "size-4", "aria-label": "Microphone off" }) });
1009
+ }
1010
+ const lit = Math.round(Math.min(1, Math.max(0, level)) * 5);
1011
+ return /* @__PURE__ */ jsx(
1012
+ "span",
1013
+ {
1014
+ className: cn("flex h-4 items-end gap-0.5", className),
1015
+ role: "meter",
1016
+ "aria-label": "Voice level",
1017
+ "aria-valuemin": 0,
1018
+ "aria-valuemax": 5,
1019
+ "aria-valuenow": lit,
1020
+ children: [0.35, 0.55, 0.75, 0.9, 1].map((h, i) => /* @__PURE__ */ jsx(
1021
+ "span",
1022
+ {
1023
+ className: cn("w-1 rounded-sm", i < lit ? "bg-theme-cyan" : "bg-muted-foreground/30"),
1024
+ style: { height: `${h * 100}%` }
1025
+ },
1026
+ i
1027
+ ))
1028
+ }
1029
+ );
1030
+ }
1031
+ function CallPersonLabel({
1032
+ name,
1033
+ entity,
1034
+ isLocal = false,
1035
+ youLabel = "(me)"
1036
+ }) {
1037
+ if (entity) return /* @__PURE__ */ jsx(Fragment, { children: entity });
1038
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1039
+ /* @__PURE__ */ jsx(
1040
+ "span",
1041
+ {
1042
+ className: "flex size-7 shrink-0 items-center justify-center rounded-full bg-primary/10 text-xs font-semibold text-primary",
1043
+ "aria-hidden": true,
1044
+ children: initialsOf(name)
1045
+ }
1046
+ ),
1047
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 truncate", children: [
1048
+ name,
1049
+ isLocal && /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
1050
+ " ",
1051
+ youLabel
1052
+ ] })
1053
+ ] })
1054
+ ] });
1055
+ }
1056
+ function PersonRow({
1057
+ p,
1058
+ canRemove,
1059
+ removingId,
1060
+ onRemove,
1061
+ onAskToMute,
1062
+ onMute
1063
+ }) {
1064
+ const hasActions = !p.isLocal && (canRemove || (onAskToMute || onMute) && !p.micMuted || onMute && p.cameraOn);
1065
+ return /* @__PURE__ */ jsxs(
1066
+ "li",
1067
+ {
1068
+ className: "flex h-10 items-center gap-2 rounded-md px-2 hover:bg-muted",
1069
+ "data-testid": `call-person-${p.id}`,
1070
+ "data-speaking": p.speaking || void 0,
1071
+ children: [
1072
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 items-center gap-1.5 text-sm", children: [
1073
+ p.entity ? (
1074
+ // The ring hugs the person's pill, not the whole row.
1075
+ /* @__PURE__ */ jsx(
1076
+ "span",
1077
+ {
1078
+ className: cn(
1079
+ "inline-flex min-w-0 rounded-full",
1080
+ p.speaking && "ring-2 ring-theme-cyan ring-offset-2 ring-offset-background"
1081
+ ),
1082
+ children: p.entity
1083
+ }
1084
+ )
1085
+ ) : /* @__PURE__ */ jsx(CallPersonLabel, { name: p.name, isLocal: p.isLocal }),
1086
+ p.speaking && /* @__PURE__ */ jsx("span", { className: "sr-only", children: " \u2014 speaking" }),
1087
+ p.note && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: p.note })
1088
+ ] }),
1089
+ /* @__PURE__ */ jsx(VoiceMeter, { level: p.level ?? 0, muted: p.micMuted }),
1090
+ /* @__PURE__ */ jsx(
1091
+ "span",
1092
+ {
1093
+ className: cn(
1094
+ "flex w-4 justify-center",
1095
+ p.cameraOn ? "text-muted-foreground" : "text-red-text"
1096
+ ),
1097
+ title: p.cameraOn ? "Camera on" : "Camera off",
1098
+ children: p.cameraOn ? /* @__PURE__ */ jsx(Video, { className: "size-4", "aria-label": "Camera on" }) : /* @__PURE__ */ jsx(VideoOff, { className: "size-4", "aria-label": "Camera off" })
1099
+ }
1100
+ ),
1101
+ hasActions ? /* @__PURE__ */ jsxs(DropdownMenu, { children: [
1102
+ /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
1103
+ Button,
1104
+ {
1105
+ type: "button",
1106
+ variant: "ghost",
1107
+ size: "icon",
1108
+ className: "size-8 text-muted-foreground hover:text-foreground",
1109
+ "aria-label": `Actions for ${p.name}`,
1110
+ "data-testid": `call-person-menu-${p.id}`,
1111
+ children: /* @__PURE__ */ jsx(MoreHorizontal, { className: "size-4" })
1112
+ }
1113
+ ) }),
1114
+ /* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", className: "w-56", children: [
1115
+ onAskToMute && !p.micMuted && /* @__PURE__ */ jsxs(
1116
+ DropdownMenuItem,
1117
+ {
1118
+ onSelect: () => onAskToMute(p),
1119
+ "data-testid": `call-ask-mute-${p.id}`,
1120
+ children: [
1121
+ /* @__PURE__ */ jsx(MicOff, { "aria-hidden": true }),
1122
+ "Ask to mute"
1123
+ ]
1124
+ }
1125
+ ),
1126
+ onMute && !p.micMuted && /* @__PURE__ */ jsxs(
1127
+ DropdownMenuItem,
1128
+ {
1129
+ onSelect: () => onMute(p, "audio"),
1130
+ "data-testid": `call-mute-${p.id}`,
1131
+ children: [
1132
+ /* @__PURE__ */ jsx(MicOff, { "aria-hidden": true }),
1133
+ "Mute"
1134
+ ]
1135
+ }
1136
+ ),
1137
+ onMute && p.cameraOn && /* @__PURE__ */ jsxs(
1138
+ DropdownMenuItem,
1139
+ {
1140
+ onSelect: () => onMute(p, "video"),
1141
+ "data-testid": `call-camera-off-${p.id}`,
1142
+ children: [
1143
+ /* @__PURE__ */ jsx(VideoOff, { "aria-hidden": true }),
1144
+ "Turn off camera"
1145
+ ]
1146
+ }
1147
+ ),
1148
+ canRemove && /* @__PURE__ */ jsxs(
1149
+ DropdownMenuItem,
1150
+ {
1151
+ onSelect: () => onRemove(p),
1152
+ disabled: removingId === p.id,
1153
+ className: "text-destructive focus:text-destructive",
1154
+ "data-testid": `call-remove-${p.id}`,
1155
+ children: [
1156
+ /* @__PURE__ */ jsx(UserX, { "aria-hidden": true }),
1157
+ "Remove from call"
1158
+ ]
1159
+ }
1160
+ )
1161
+ ] })
1162
+ ] }) : (
1163
+ // Keeps every row's meter and icons in the same column.
1164
+ /* @__PURE__ */ jsx("span", { className: "size-8 shrink-0", "aria-hidden": true })
1165
+ )
1166
+ ]
1167
+ }
1168
+ );
1169
+ }
1170
+ function CallParticipants({
1171
+ people,
1172
+ canRemove,
1173
+ removingId,
1174
+ onRemove,
1175
+ onAskToMute,
1176
+ onMute,
1177
+ compact = false,
1178
+ className
1179
+ }) {
1180
+ const [expanded, setExpanded] = useState(false);
1181
+ const rows = /* @__PURE__ */ jsx("ul", { className: "min-h-0 flex-1 overflow-y-auto px-1 pb-2", "data-testid": "call-people-list", children: people.map((p) => /* @__PURE__ */ jsx(
1182
+ PersonRow,
1183
+ {
1184
+ p,
1185
+ canRemove,
1186
+ removingId,
1187
+ onRemove,
1188
+ onAskToMute,
1189
+ onMute
1190
+ },
1191
+ p.id
1192
+ )) });
1193
+ const heading = /* @__PURE__ */ jsxs("span", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: [
1194
+ "People (",
1195
+ people.length,
1196
+ ")"
1197
+ ] });
1198
+ if (compact && !expanded) {
1199
+ const talking = people.filter((p) => p.speaking);
1200
+ return /* @__PURE__ */ jsxs(
1201
+ "section",
1202
+ {
1203
+ className: cn(
1204
+ "flex h-11 shrink-0 items-center gap-2 border-t border-border px-3",
1205
+ className
1206
+ ),
1207
+ "aria-label": "People",
1208
+ "data-testid": "call-people",
1209
+ "data-compact": "",
1210
+ children: [
1211
+ heading,
1212
+ /* @__PURE__ */ jsx("span", { className: "flex min-w-0 flex-1 items-center gap-1 overflow-hidden", children: talking.map((p) => /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center", children: p.entity ?? p.name }, p.id)) }),
1213
+ /* @__PURE__ */ jsxs(
1214
+ Button,
1215
+ {
1216
+ type: "button",
1217
+ variant: "ghost",
1218
+ size: "sm",
1219
+ className: "h-8 shrink-0",
1220
+ "aria-expanded": false,
1221
+ onClick: () => setExpanded(true),
1222
+ "data-testid": "call-people-show-all",
1223
+ children: [
1224
+ "Show all (",
1225
+ people.length,
1226
+ ")"
1227
+ ]
1228
+ }
1229
+ )
1230
+ ]
1231
+ }
1232
+ );
1233
+ }
1234
+ return /* @__PURE__ */ jsxs(
1235
+ "section",
1236
+ {
1237
+ className: cn(
1238
+ "flex min-h-0 flex-col border-t border-border",
1239
+ compact && "absolute inset-x-0 bottom-0 z-20 max-h-[80%] bg-background shadow-lg",
1240
+ className
1241
+ ),
1242
+ "aria-label": "People",
1243
+ "data-testid": "call-people",
1244
+ children: [
1245
+ /* @__PURE__ */ jsxs("div", { className: "flex h-9 shrink-0 items-center justify-between px-3", children: [
1246
+ heading,
1247
+ compact && /* @__PURE__ */ jsx(
1248
+ Button,
1249
+ {
1250
+ type: "button",
1251
+ variant: "ghost",
1252
+ size: "sm",
1253
+ className: "h-7",
1254
+ "aria-expanded": true,
1255
+ onClick: () => setExpanded(false),
1256
+ "data-testid": "call-people-show-less",
1257
+ children: "Show less"
1258
+ }
1259
+ )
1260
+ ] }),
1261
+ rows
1262
+ ]
1263
+ }
1264
+ );
1265
+ }
1266
+ function LobbyRequests({
1267
+ requests,
1268
+ onDecide
1269
+ }) {
1270
+ if (!requests.length) return null;
1271
+ return /* @__PURE__ */ jsx(
1272
+ "div",
1273
+ {
1274
+ className: "absolute left-3 top-3 z-10 flex w-72 max-w-[calc(100%-1.5rem)] flex-col gap-2",
1275
+ role: "region",
1276
+ "aria-label": "Lobby",
1277
+ "data-testid": "call-lobby",
1278
+ children: requests.map((k) => /* @__PURE__ */ jsxs(
1279
+ "div",
1280
+ {
1281
+ className: "flex items-center gap-3 rounded-md border border-border bg-popover p-3 text-sm text-popover-foreground shadow-lg",
1282
+ "data-testid": "call-lobby-knock",
1283
+ children: [
1284
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-wrap items-center gap-1", children: [
1285
+ k.entity ?? /* @__PURE__ */ jsx("span", { className: "font-medium", children: k.name }),
1286
+ /* @__PURE__ */ jsx("span", { children: "wants to join." })
1287
+ ] }),
1288
+ /* @__PURE__ */ jsx(
1289
+ Button,
1290
+ {
1291
+ type: "button",
1292
+ variant: "ghost",
1293
+ size: "sm",
1294
+ className: "h-8",
1295
+ onClick: () => onDecide(k.id, false),
1296
+ "data-testid": "call-lobby-deny",
1297
+ children: "Deny"
1298
+ }
1299
+ ),
1300
+ /* @__PURE__ */ jsx(
1301
+ Button,
1302
+ {
1303
+ type: "button",
1304
+ size: "sm",
1305
+ className: "h-8",
1306
+ onClick: () => onDecide(k.id, true),
1307
+ "data-testid": "call-lobby-admit",
1308
+ children: "Admit"
1309
+ }
1310
+ )
1311
+ ]
1312
+ },
1313
+ k.id
1314
+ ))
1315
+ }
1316
+ );
1317
+ }
1318
+ function MinimisedCall({
1319
+ title,
1320
+ video,
1321
+ status,
1322
+ speaking,
1323
+ micEnabled,
1324
+ onToggleMic,
1325
+ cameraEnabled,
1326
+ onToggleCamera,
1327
+ onRestore,
1328
+ onLeave,
1329
+ restoreLabel = "Back to the call",
1330
+ shortcuts = true,
1331
+ className
1332
+ }) {
1333
+ const ref = useRef(null);
1334
+ useShortcutKeys(ref, shortcuts);
1335
+ return /* @__PURE__ */ jsxs("div", { ref, className: cn("flex flex-col", className), "data-testid": "call-minimised", children: [
1336
+ /* @__PURE__ */ jsx(
1337
+ "button",
1338
+ {
1339
+ type: "button",
1340
+ className: "group relative aspect-video w-full overflow-hidden rounded-t-md bg-hero focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
1341
+ onClick: onRestore,
1342
+ "aria-label": restoreLabel,
1343
+ "data-testid": "call-minimised-restore",
1344
+ children: /* @__PURE__ */ jsx("span", { className: "call-tile-box absolute inset-0", children: video ?? /* @__PURE__ */ jsx("span", { className: "call-tile", children: /* @__PURE__ */ jsx("span", { className: "call-tile-placeholder", children: /* @__PURE__ */ jsx("span", { className: "call-tile-avatar call-tile-avatar-sm", children: /* @__PURE__ */ jsx(Phone, { className: "size-4", "aria-hidden": true }) }) }) }) })
1345
+ }
1346
+ ),
1347
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 px-3 py-2", children: [
1348
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [
1349
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-sm font-medium", title, children: title }),
1350
+ status && /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center gap-1", children: status })
1351
+ ] }),
1352
+ speaking,
1353
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1354
+ onToggleMic && micEnabled !== void 0 && /* @__PURE__ */ jsx(
1355
+ CallControlButton,
1356
+ {
1357
+ label: micEnabled ? "Turn off microphone" : "Turn on microphone",
1358
+ shortcut: "M",
1359
+ tone: micEnabled ? "neutral" : "off",
1360
+ "aria-pressed": micEnabled,
1361
+ onClick: onToggleMic,
1362
+ className: "size-8",
1363
+ "data-testid": "call-minimised-mic",
1364
+ "data-call-shortcut": "m",
1365
+ children: micEnabled ? /* @__PURE__ */ jsx(Mic, { "aria-hidden": true }) : /* @__PURE__ */ jsx(MicOff, { "aria-hidden": true })
1366
+ }
1367
+ ),
1368
+ onToggleCamera && cameraEnabled !== void 0 && /* @__PURE__ */ jsx(
1369
+ CallControlButton,
1370
+ {
1371
+ label: cameraEnabled ? "Turn off camera" : "Turn on camera",
1372
+ shortcut: "V",
1373
+ tone: cameraEnabled ? "neutral" : "off",
1374
+ "aria-pressed": cameraEnabled,
1375
+ onClick: onToggleCamera,
1376
+ className: "size-8",
1377
+ "data-testid": "call-minimised-camera",
1378
+ "data-call-shortcut": "v",
1379
+ children: cameraEnabled ? /* @__PURE__ */ jsx(Video, { "aria-hidden": true }) : /* @__PURE__ */ jsx(VideoOff, { "aria-hidden": true })
1380
+ }
1381
+ ),
1382
+ /* @__PURE__ */ jsx("span", { className: "flex-1" }),
1383
+ /* @__PURE__ */ jsx(
1384
+ CallControlButton,
1385
+ {
1386
+ label: restoreLabel,
1387
+ onClick: onRestore,
1388
+ className: "size-8",
1389
+ "data-testid": "call-dock-expand",
1390
+ children: /* @__PURE__ */ jsx(Maximize2, { "aria-hidden": true })
1391
+ }
1392
+ ),
1393
+ /* @__PURE__ */ jsx(
1394
+ CallControlButton,
1395
+ {
1396
+ label: "Leave call",
1397
+ tone: "danger",
1398
+ onClick: onLeave,
1399
+ className: "size-8",
1400
+ "data-testid": "call-dock-leave",
1401
+ children: /* @__PURE__ */ jsx(PhoneOff, { "aria-hidden": true })
1402
+ }
1403
+ )
1404
+ ] })
1405
+ ] })
1406
+ ] });
1407
+ }
1408
+ function IncomingCallCard({
1409
+ callerName,
1410
+ caller,
1411
+ context,
1412
+ busy = false,
1413
+ muted,
1414
+ onMutedChange,
1415
+ onAccept,
1416
+ onDecline,
1417
+ className
1418
+ }) {
1419
+ return /* @__PURE__ */ jsxs(
1420
+ "div",
1421
+ {
1422
+ role: "alertdialog",
1423
+ "aria-labelledby": "incoming-call-title",
1424
+ "aria-describedby": "incoming-call-desc",
1425
+ "data-testid": "incoming-call-dialog",
1426
+ className: cn(
1427
+ "w-80 max-w-[calc(100vw-2rem)] rounded-md border border-border bg-popover p-4 text-popover-foreground shadow-xl motion-safe:animate-in motion-safe:fade-in-0 motion-safe:slide-in-from-top-2",
1428
+ className
1429
+ ),
1430
+ children: [
1431
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
1432
+ /* @__PURE__ */ jsxs("span", { className: "relative flex size-10 shrink-0", "aria-hidden": true, children: [
1433
+ /* @__PURE__ */ jsx("span", { className: "absolute inset-0 rounded-full bg-primary/20 motion-safe:animate-ping" }),
1434
+ /* @__PURE__ */ jsx("span", { className: "relative flex size-10 items-center justify-center rounded-full bg-primary/10 text-primary", children: /* @__PURE__ */ jsx(Phone, { className: "size-5" }) })
1435
+ ] }),
1436
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 pt-0.5", children: [
1437
+ /* @__PURE__ */ jsxs("div", { id: "incoming-call-title", className: "flex min-w-0 text-sm font-semibold", children: [
1438
+ caller ?? /* @__PURE__ */ jsx("span", { className: "truncate", children: callerName }),
1439
+ caller && /* @__PURE__ */ jsx("span", { className: "sr-only", children: callerName })
1440
+ ] }),
1441
+ /* @__PURE__ */ jsx("p", { id: "incoming-call-desc", className: "text-xs text-muted-foreground", children: context })
1442
+ ] }),
1443
+ /* @__PURE__ */ jsx(
1444
+ Button,
1445
+ {
1446
+ type: "button",
1447
+ variant: "ghost",
1448
+ size: "icon",
1449
+ className: "-mr-1 -mt-1 size-8 shrink-0 text-muted-foreground hover:text-foreground",
1450
+ "aria-pressed": muted,
1451
+ "aria-label": muted ? "Unmute ringtone" : "Mute ringtone",
1452
+ title: muted ? "Unmute ringtone" : "Mute ringtone",
1453
+ onClick: () => onMutedChange(!muted),
1454
+ "data-testid": "incoming-call-mute",
1455
+ children: muted ? /* @__PURE__ */ jsx(BellOff, { className: "size-4" }) : /* @__PURE__ */ jsx(Bell, { className: "size-4" })
1456
+ }
1457
+ )
1458
+ ] }),
1459
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 grid grid-cols-2 gap-2", children: [
1460
+ /* @__PURE__ */ jsxs(
1461
+ Button,
1462
+ {
1463
+ type: "button",
1464
+ variant: "destructive",
1465
+ onClick: onDecline,
1466
+ disabled: busy,
1467
+ "data-testid": "incoming-call-decline",
1468
+ children: [
1469
+ /* @__PURE__ */ jsx(PhoneOff, { className: "size-4", "aria-hidden": true }),
1470
+ "Decline"
1471
+ ]
1472
+ }
1473
+ ),
1474
+ /* @__PURE__ */ jsxs(
1475
+ Button,
1476
+ {
1477
+ type: "button",
1478
+ onClick: onAccept,
1479
+ disabled: busy,
1480
+ "data-testid": "incoming-call-accept",
1481
+ autoFocus: true,
1482
+ children: [
1483
+ busy ? /* @__PURE__ */ jsx(Loader2, { className: "size-4 animate-spin", "aria-hidden": true }) : /* @__PURE__ */ jsx(Phone, { className: "size-4", "aria-hidden": true }),
1484
+ "Accept"
1485
+ ]
1486
+ }
1487
+ )
1488
+ ] })
1489
+ ]
1490
+ }
1491
+ );
1492
+ }
1493
+ function CallStateMessage({
1494
+ title,
1495
+ description,
1496
+ spinner = true,
1497
+ icon,
1498
+ action,
1499
+ children,
1500
+ testId
1501
+ }) {
1502
+ return /* @__PURE__ */ jsxs(
1503
+ "div",
1504
+ {
1505
+ className: "flex size-full flex-col items-center justify-center gap-3 p-6 text-center",
1506
+ role: "status",
1507
+ "data-testid": testId,
1508
+ children: [
1509
+ icon ?? (spinner && /* @__PURE__ */ jsx(Loader2, { className: "size-6 animate-spin text-muted-foreground", "aria-hidden": true })),
1510
+ /* @__PURE__ */ jsx("p", { className: "text-base font-semibold", children: title }),
1511
+ description && /* @__PURE__ */ jsx("p", { className: "max-w-sm text-sm text-muted-foreground", children: description }),
1512
+ children,
1513
+ action
1514
+ ]
1515
+ }
1516
+ );
1517
+ }
1518
+ function ConnectionOverlay({ label }) {
1519
+ return /* @__PURE__ */ jsxs("div", { className: "absolute inset-0 z-10 flex items-center justify-center rounded-md bg-background/70 text-sm text-muted-foreground", children: [
1520
+ /* @__PURE__ */ jsx(Loader2, { className: "mr-2 size-4 animate-spin", "aria-hidden": true }),
1521
+ label
1522
+ ] });
1523
+ }
1524
+
1525
+ // src/calls/join-choices.ts
1526
+ var DEFAULT_JOIN_CHOICES = { audioEnabled: true, videoEnabled: false };
1527
+ function describeMediaError(err, kind) {
1528
+ const Kind = kind === "camera" ? "Camera" : "Microphone";
1529
+ const name = err?.name;
1530
+ if (name === "NotAllowedError" || name === "SecurityError")
1531
+ return `${Kind} access is blocked. Allow it in the browser's site settings and try again.`;
1532
+ if (name === "NotFoundError" || name === "OverconstrainedError") return `No ${kind} was found.`;
1533
+ if (name === "NotReadableError") return `${Kind} is in use by another app.`;
1534
+ return `${Kind} could not start${err?.message ? `: ${err.message}` : "."}`;
1535
+ }
1536
+ function useDevices(kind, refreshKey) {
1537
+ const [devices, setDevices] = useState([]);
1538
+ useEffect(() => {
1539
+ const md = navigator.mediaDevices;
1540
+ if (!md?.enumerateDevices) return;
1541
+ let cancelled = false;
1542
+ const load = () => md.enumerateDevices().then((all) => {
1543
+ if (!cancelled) setDevices(all.filter((d) => d.kind === kind && d.deviceId));
1544
+ }).catch(() => {
1545
+ });
1546
+ void load();
1547
+ md.addEventListener?.("devicechange", load);
1548
+ return () => {
1549
+ cancelled = true;
1550
+ md.removeEventListener?.("devicechange", load);
1551
+ };
1552
+ }, [kind, refreshKey]);
1553
+ return devices;
1554
+ }
1555
+ function usePreviewStream(kind, enabled, deviceId, describe) {
1556
+ const [stream, setStream] = useState(null);
1557
+ const [error, setError] = useState(null);
1558
+ const describeRef = useRef(describe);
1559
+ describeRef.current = describe;
1560
+ useEffect(() => {
1561
+ if (!enabled) {
1562
+ setStream(null);
1563
+ return;
1564
+ }
1565
+ const md = navigator.mediaDevices;
1566
+ if (!md?.getUserMedia) {
1567
+ setError(
1568
+ window.isSecureContext ? "This browser cannot access the camera or microphone." : "Camera and microphone need HTTPS (or localhost)."
1569
+ );
1570
+ return;
1571
+ }
1572
+ let cancelled = false;
1573
+ let opened = null;
1574
+ const constraint = deviceId ? { deviceId: { exact: deviceId } } : true;
1575
+ md.getUserMedia(kind === "videoinput" ? { video: constraint } : { audio: constraint }).then((s) => {
1576
+ opened = s;
1577
+ if (cancelled) s.getTracks().forEach((t) => t.stop());
1578
+ else {
1579
+ setError(null);
1580
+ setStream(s);
1581
+ }
1582
+ }).catch((err) => {
1583
+ if (!cancelled)
1584
+ setError(describeRef.current(err, kind === "videoinput" ? "camera" : "microphone"));
1585
+ });
1586
+ return () => {
1587
+ cancelled = true;
1588
+ opened?.getTracks().forEach((t) => t.stop());
1589
+ setStream(null);
1590
+ };
1591
+ }, [kind, enabled, deviceId]);
1592
+ return { stream, error };
1593
+ }
1594
+ function useAudioLevel(stream) {
1595
+ const [level, setLevel] = useState(0);
1596
+ useEffect(() => {
1597
+ if (!stream || typeof AudioContext === "undefined") {
1598
+ setLevel(0);
1599
+ return;
1600
+ }
1601
+ let ctx;
1602
+ try {
1603
+ ctx = new AudioContext();
1604
+ } catch {
1605
+ return;
1606
+ }
1607
+ const analyser = ctx.createAnalyser();
1608
+ analyser.fftSize = 512;
1609
+ ctx.createMediaStreamSource(stream).connect(analyser);
1610
+ const buf = new Uint8Array(analyser.fftSize);
1611
+ let frame = 0;
1612
+ const tick = () => {
1613
+ analyser.getByteTimeDomainData(buf);
1614
+ let peak = 0;
1615
+ for (const v of buf) peak = Math.max(peak, Math.abs(v - 128));
1616
+ setLevel(Math.min(1, peak / 64));
1617
+ frame = requestAnimationFrame(tick);
1618
+ };
1619
+ void ctx.resume().catch(() => {
1620
+ });
1621
+ tick();
1622
+ return () => {
1623
+ cancelAnimationFrame(frame);
1624
+ void ctx.close().catch(() => {
1625
+ });
1626
+ };
1627
+ }, [stream]);
1628
+ return level;
1629
+ }
1630
+ function DeviceSelect({
1631
+ devices,
1632
+ value,
1633
+ onChange,
1634
+ label,
1635
+ icon,
1636
+ disabled,
1637
+ testId
1638
+ }) {
1639
+ const current = value ?? devices[0]?.deviceId;
1640
+ return /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col gap-1.5", children: [
1641
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5 text-xs font-medium text-muted-foreground [&_svg]:size-3.5", children: [
1642
+ icon,
1643
+ label
1644
+ ] }),
1645
+ /* @__PURE__ */ jsxs(Select, { value: current, onValueChange: onChange, disabled: disabled || devices.length < 2, children: [
1646
+ /* @__PURE__ */ jsx(SelectTrigger, { "aria-label": label, className: "h-9 min-w-0 text-sm", "data-testid": testId, children: /* @__PURE__ */ jsx(SelectValue, { placeholder: `Default ${label.toLowerCase()}` }) }),
1647
+ /* @__PURE__ */ jsx(SelectContent, { children: devices.map((d, i) => /* @__PURE__ */ jsx(SelectItem, { value: d.deviceId, children: d.label || `${label} ${i + 1}` }, d.deviceId)) })
1648
+ ] })
1649
+ ] });
1650
+ }
1651
+ function PreJoin({
1652
+ title,
1653
+ heading = "Ready to join?",
1654
+ headingAs: Heading = "h2",
1655
+ children,
1656
+ joinLabel = "Join",
1657
+ cancelLabel = "Cancel",
1658
+ busy = false,
1659
+ error,
1660
+ onJoin,
1661
+ onCancel,
1662
+ describeError = describeMediaError,
1663
+ mirror = true,
1664
+ className
1665
+ }) {
1666
+ const [choices, setChoices] = useState(DEFAULT_JOIN_CHOICES);
1667
+ const video = usePreviewStream(
1668
+ "videoinput",
1669
+ choices.videoEnabled,
1670
+ choices.videoDeviceId,
1671
+ describeError
1672
+ );
1673
+ const audio = usePreviewStream(
1674
+ "audioinput",
1675
+ choices.audioEnabled,
1676
+ choices.audioDeviceId,
1677
+ describeError
1678
+ );
1679
+ const level = useAudioLevel(audio.stream);
1680
+ const cameras = useDevices("videoinput", video.stream);
1681
+ const mics = useDevices("audioinput", audio.stream);
1682
+ const videoRef = useRef(null);
1683
+ useEffect(() => {
1684
+ if (videoRef.current) videoRef.current.srcObject = video.stream;
1685
+ }, [video.stream]);
1686
+ const set = (patch) => setChoices((c) => ({ ...c, ...patch }));
1687
+ const deviceError = video.error ?? audio.error;
1688
+ return /* @__PURE__ */ jsx("div", { className: cn("@container flex w-full", className), "data-testid": "call-prejoin", children: /* @__PURE__ */ jsxs("div", { className: "mx-auto grid w-full max-w-4xl items-center gap-6 @2xl:grid-cols-[3fr_2fr] @2xl:gap-8", children: [
1689
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
1690
+ /* @__PURE__ */ jsx("div", { className: "relative aspect-video w-full overflow-hidden rounded-md bg-hero", children: choices.videoEnabled && video.stream ? /* @__PURE__ */ jsx(
1691
+ "video",
1692
+ {
1693
+ ref: videoRef,
1694
+ autoPlay: true,
1695
+ muted: true,
1696
+ playsInline: true,
1697
+ className: cn("size-full object-cover", mirror && "-scale-x-100"),
1698
+ "data-testid": "call-prejoin-video"
1699
+ }
1700
+ ) : /* @__PURE__ */ jsx("div", { className: "flex size-full items-center justify-center text-sm text-white/80", children: choices.videoEnabled ? /* @__PURE__ */ jsx(Loader2, { className: "size-5 animate-spin", "aria-label": "Starting camera" }) : "Camera is off" }) }),
1701
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center gap-3", children: [
1702
+ /* @__PURE__ */ jsx(
1703
+ CallControlButton,
1704
+ {
1705
+ label: choices.audioEnabled ? "Turn off microphone" : "Turn on microphone",
1706
+ tone: choices.audioEnabled ? "neutral" : "off",
1707
+ "aria-pressed": choices.audioEnabled,
1708
+ onClick: () => set({ audioEnabled: !choices.audioEnabled }),
1709
+ "data-testid": "call-prejoin-mic-toggle",
1710
+ children: choices.audioEnabled ? /* @__PURE__ */ jsx(Mic, { "aria-hidden": true }) : /* @__PURE__ */ jsx(MicOff, { "aria-hidden": true })
1711
+ }
1712
+ ),
1713
+ /* @__PURE__ */ jsx(
1714
+ CallControlButton,
1715
+ {
1716
+ label: choices.videoEnabled ? "Turn off camera" : "Turn on camera",
1717
+ tone: choices.videoEnabled ? "neutral" : "off",
1718
+ "aria-pressed": choices.videoEnabled,
1719
+ onClick: () => set({ videoEnabled: !choices.videoEnabled }),
1720
+ "data-testid": "call-prejoin-camera-toggle",
1721
+ children: choices.videoEnabled ? /* @__PURE__ */ jsx(Video, { "aria-hidden": true }) : /* @__PURE__ */ jsx(VideoOff, { "aria-hidden": true })
1722
+ }
1723
+ )
1724
+ ] })
1725
+ ] }),
1726
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col gap-4", children: [
1727
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
1728
+ /* @__PURE__ */ jsx(Heading, { className: "text-xl font-semibold", children: heading }),
1729
+ title && /* @__PURE__ */ jsx("p", { className: "truncate text-sm text-muted-foreground", children: title })
1730
+ ] }),
1731
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
1732
+ /* @__PURE__ */ jsx(
1733
+ DeviceSelect,
1734
+ {
1735
+ devices: mics,
1736
+ value: choices.audioDeviceId,
1737
+ onChange: (id) => set({ audioDeviceId: id }),
1738
+ label: "Microphone",
1739
+ icon: /* @__PURE__ */ jsx(Mic, {}),
1740
+ disabled: !choices.audioEnabled,
1741
+ testId: "call-prejoin-mic-select"
1742
+ }
1743
+ ),
1744
+ /* @__PURE__ */ jsx(
1745
+ "div",
1746
+ {
1747
+ className: "h-1.5 w-full overflow-hidden rounded-full bg-muted",
1748
+ role: "meter",
1749
+ "aria-label": "Microphone level",
1750
+ "aria-valuemin": 0,
1751
+ "aria-valuemax": 100,
1752
+ "aria-valuenow": Math.round(level * 100),
1753
+ "data-testid": "call-prejoin-mic-level",
1754
+ children: /* @__PURE__ */ jsx(
1755
+ "div",
1756
+ {
1757
+ className: "h-full bg-primary transition-[width] duration-75 motion-reduce:transition-none",
1758
+ style: { width: `${Math.round(level * 100)}%` }
1759
+ }
1760
+ )
1761
+ }
1762
+ ),
1763
+ /* @__PURE__ */ jsx(
1764
+ DeviceSelect,
1765
+ {
1766
+ devices: cameras,
1767
+ value: choices.videoDeviceId,
1768
+ onChange: (id) => set({ videoDeviceId: id }),
1769
+ label: "Camera",
1770
+ icon: /* @__PURE__ */ jsx(Video, {}),
1771
+ disabled: !choices.videoEnabled,
1772
+ testId: "call-prejoin-camera-select"
1773
+ }
1774
+ )
1775
+ ] }),
1776
+ children,
1777
+ (error || deviceError) && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-sm text-destructive", "data-testid": "call-prejoin-error", children: error ?? deviceError }),
1778
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
1779
+ /* @__PURE__ */ jsxs(
1780
+ Button,
1781
+ {
1782
+ type: "button",
1783
+ className: "flex-1",
1784
+ disabled: busy,
1785
+ onClick: () => onJoin(choices),
1786
+ "data-testid": "call-prejoin-join",
1787
+ children: [
1788
+ busy && /* @__PURE__ */ jsx(Loader2, { className: "size-4 animate-spin", "aria-hidden": true }),
1789
+ joinLabel
1790
+ ]
1791
+ }
1792
+ ),
1793
+ onCancel && /* @__PURE__ */ jsx(
1794
+ Button,
1795
+ {
1796
+ type: "button",
1797
+ variant: "outline",
1798
+ onClick: onCancel,
1799
+ disabled: busy,
1800
+ "data-testid": "call-prejoin-cancel",
1801
+ children: cancelLabel
1802
+ }
1803
+ )
1804
+ ] })
1805
+ ] })
1806
+ ] }) });
1807
+ }
1808
+ function WebinarWaiting({
1809
+ variant,
1810
+ title,
1811
+ when,
1812
+ host,
1813
+ onLeave,
1814
+ leaveLabel = "Leave"
1815
+ }) {
1816
+ const waiting = variant === "waiting";
1817
+ return /* @__PURE__ */ jsx(
1818
+ CallStateMessage,
1819
+ {
1820
+ title: waiting ? "Starting soon" : "This webinar has ended",
1821
+ description: waiting ? "The host hasn't gone live yet. You'll join automatically when the webinar starts." : "Thanks for attending.",
1822
+ spinner: waiting,
1823
+ icon: waiting ? void 0 : /* @__PURE__ */ jsx(CalendarCheck, { className: "size-6 text-muted-foreground", "aria-hidden": true }),
1824
+ testId: waiting ? "webinar-waiting" : "webinar-ended",
1825
+ action: onLeave && /* @__PURE__ */ jsx(
1826
+ Button,
1827
+ {
1828
+ type: "button",
1829
+ variant: "outline",
1830
+ onClick: onLeave,
1831
+ "data-testid": "webinar-waiting-leave",
1832
+ children: leaveLabel
1833
+ }
1834
+ ),
1835
+ children: (title || when || host) && /* @__PURE__ */ jsxs("div", { className: "flex max-w-sm flex-col items-center gap-1 text-sm", children: [
1836
+ title && /* @__PURE__ */ jsx("p", { className: "font-medium", "data-testid": "webinar-waiting-title", children: title }),
1837
+ when && /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", "data-testid": "webinar-waiting-when", children: when }),
1838
+ host && /* @__PURE__ */ jsxs(
1839
+ "p",
1840
+ {
1841
+ className: "flex flex-wrap items-center justify-center gap-1 text-muted-foreground",
1842
+ "data-testid": "webinar-waiting-host",
1843
+ children: [
1844
+ /* @__PURE__ */ jsx("span", { children: "Hosted by" }),
1845
+ host
1846
+ ]
1847
+ }
1848
+ )
1849
+ ] })
1850
+ }
1851
+ );
1852
+ }
1853
+ function usePending() {
1854
+ const [keys, setKeys] = useState(() => /* @__PURE__ */ new Set());
1855
+ const mounted = useRef(true);
1856
+ useEffect(() => {
1857
+ mounted.current = true;
1858
+ return () => {
1859
+ mounted.current = false;
1860
+ };
1861
+ }, []);
1862
+ const set = useCallback((key, on) => {
1863
+ if (!mounted.current) return;
1864
+ setKeys((prev) => {
1865
+ if (prev.has(key) === on) return prev;
1866
+ const next = new Set(prev);
1867
+ if (on) next.add(key);
1868
+ else next.delete(key);
1869
+ return next;
1870
+ });
1871
+ }, []);
1872
+ const run = useCallback(
1873
+ async (key, fn) => {
1874
+ let result;
1875
+ try {
1876
+ result = fn();
1877
+ } catch {
1878
+ return false;
1879
+ }
1880
+ if (!result || typeof result.then !== "function") return true;
1881
+ set(key, true);
1882
+ try {
1883
+ await result;
1884
+ return true;
1885
+ } catch {
1886
+ return false;
1887
+ } finally {
1888
+ set(key, false);
1889
+ }
1890
+ },
1891
+ [set]
1892
+ );
1893
+ const isPending = useCallback((key) => keys.has(key), [keys]);
1894
+ return { isPending, run };
1895
+ }
1896
+ function useNow(ms = 3e4, fixed) {
1897
+ const [now, setNow] = useState(() => /* @__PURE__ */ new Date());
1898
+ const ticking = !fixed;
1899
+ useEffect(() => {
1900
+ if (!ticking) return;
1901
+ const id = setInterval(() => setNow(/* @__PURE__ */ new Date()), ms);
1902
+ return () => clearInterval(id);
1903
+ }, [ms, ticking]);
1904
+ return fixed ?? now;
1905
+ }
1906
+ function WebinarLiveChip({
1907
+ compact = false,
1908
+ className
1909
+ }) {
1910
+ return /* @__PURE__ */ jsx(
1911
+ CallStatusChip,
1912
+ {
1913
+ tone: "red",
1914
+ icon: /* @__PURE__ */ jsx(Radio, { className: "size-3.5 shrink-0", "aria-hidden": true }),
1915
+ label: "Live",
1916
+ compact,
1917
+ title: "Live: attendees see and hear the stage",
1918
+ role: "status",
1919
+ className,
1920
+ testId: "webinar-live-chip"
1921
+ }
1922
+ );
1923
+ }
1924
+ function WebinarLiveControl({
1925
+ live,
1926
+ onGoLive,
1927
+ onEnd,
1928
+ compact = false,
1929
+ className
1930
+ }) {
1931
+ const { isPending, run } = usePending();
1932
+ const [confirming, setConfirming] = useState(false);
1933
+ if (!live) {
1934
+ if (!onGoLive) return null;
1935
+ const busy = isPending("live");
1936
+ return /* @__PURE__ */ jsxs(
1937
+ Button,
1938
+ {
1939
+ type: "button",
1940
+ size: compact ? "icon" : "sm",
1941
+ className: cn("h-8 shrink-0", !compact && "px-3", className),
1942
+ disabled: busy,
1943
+ "aria-label": "Go live",
1944
+ title: "Go live: let attendees see and hear the stage",
1945
+ onClick: () => void run("live", onGoLive),
1946
+ "data-testid": "webinar-go-live",
1947
+ children: [
1948
+ busy ? /* @__PURE__ */ jsx(Loader2, { className: "size-4 animate-spin", "aria-hidden": true }) : /* @__PURE__ */ jsx(Radio, { className: "size-4", "aria-hidden": true }),
1949
+ !compact && "Go live"
1950
+ ]
1951
+ }
1952
+ );
1953
+ }
1954
+ if (!onEnd) return null;
1955
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1956
+ /* @__PURE__ */ jsxs(
1957
+ Button,
1958
+ {
1959
+ type: "button",
1960
+ variant: "outline",
1961
+ size: compact ? "icon" : "default",
1962
+ className: cn(
1963
+ "shrink-0 border-red-border text-red-text hover:bg-red-bg hover:text-red-text",
1964
+ !compact && "px-4",
1965
+ className
1966
+ ),
1967
+ "aria-label": "End webinar",
1968
+ title: "End webinar for everyone",
1969
+ onClick: () => setConfirming(true),
1970
+ "data-testid": "webinar-end",
1971
+ children: [
1972
+ /* @__PURE__ */ jsx(SquareX, { className: "size-4", "aria-hidden": true }),
1973
+ !compact && "End webinar"
1974
+ ]
1975
+ }
1976
+ ),
1977
+ /* @__PURE__ */ jsx(WebinarEndConfirm, { open: confirming, onOpenChange: setConfirming, onEnd })
1978
+ ] });
1979
+ }
1980
+ function WebinarEndConfirm({
1981
+ open,
1982
+ onOpenChange,
1983
+ onEnd
1984
+ }) {
1985
+ const { isPending, run } = usePending();
1986
+ const ending = isPending("end");
1987
+ return /* @__PURE__ */ jsx(
1988
+ ConfirmDialog,
1989
+ {
1990
+ isOpen: open,
1991
+ onClose: () => onOpenChange(false),
1992
+ onConfirm: () => void run("end", onEnd).then((ok) => {
1993
+ if (ok) onOpenChange(false);
1994
+ }),
1995
+ title: "End the webinar?",
1996
+ description: "Everyone leaves, attendees and panelists alike. The stage closes and cannot be reopened in this session.",
1997
+ confirmText: "End webinar",
1998
+ isLoading: ending,
1999
+ testId: "webinar-end-confirm",
2000
+ testIds: { cancel: "webinar-end-cancel", confirm: "webinar-end-confirm-button" }
2001
+ }
2002
+ );
2003
+ }
2004
+ function BackstageBanner({
2005
+ onGoLive,
2006
+ className
2007
+ }) {
2008
+ return /* @__PURE__ */ jsxs(
2009
+ "div",
2010
+ {
2011
+ className: cn(
2012
+ "flex min-h-10 min-w-0 items-center gap-2 rounded-md border border-yellow-border bg-yellow-bg py-1 pl-3 pr-1 text-sm text-yellow-text",
2013
+ className
2014
+ ),
2015
+ role: "status",
2016
+ "data-testid": "webinar-backstage",
2017
+ children: [
2018
+ /* @__PURE__ */ jsx(EyeOff, { className: "size-4 shrink-0", "aria-hidden": true }),
2019
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
2020
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: "Backstage" }),
2021
+ " \u2014 attendees can't see or hear you yet",
2022
+ !onGoLive && /* @__PURE__ */ jsx("span", { className: "text-yellow-text/80", children: ". The host goes live when ready." })
2023
+ ] }),
2024
+ onGoLive && /* @__PURE__ */ jsx(WebinarLiveControl, { live: false, onGoLive })
2025
+ ]
2026
+ }
2027
+ );
2028
+ }
2029
+ function RaiseHandButton({
2030
+ raised,
2031
+ onToggle,
2032
+ pending = false,
2033
+ shortcut = "H",
2034
+ className
2035
+ }) {
2036
+ const { isPending, run } = usePending();
2037
+ const busy = pending || isPending("hand");
2038
+ return /* @__PURE__ */ jsx(
2039
+ CallControlButton,
2040
+ {
2041
+ label: raised ? "Lower hand" : "Raise hand",
2042
+ shortcut,
2043
+ tone: raised ? "active" : "neutral",
2044
+ "aria-pressed": raised,
2045
+ disabled: busy,
2046
+ "aria-busy": busy || void 0,
2047
+ onClick: () => void run("hand", () => onToggle(!raised)),
2048
+ className,
2049
+ "data-testid": "webinar-raise-hand",
2050
+ "data-raised": raised,
2051
+ "data-call-shortcut": shortcut.toLowerCase(),
2052
+ children: /* @__PURE__ */ jsx(Hand, { "aria-hidden": true })
2053
+ }
2054
+ );
2055
+ }
2056
+ var ROLE_NOTE = {
2057
+ host: "Host",
2058
+ cohost: "Co-host",
2059
+ panelist: "Panelist",
2060
+ attendee: null
2061
+ };
2062
+ function SectionTitle({ children }) {
2063
+ return /* @__PURE__ */ jsx("h3", { className: "flex items-center gap-1.5 px-2 pb-1 pt-3 text-xs font-medium text-muted-foreground", children });
2064
+ }
2065
+ function Row({ testId, children }) {
2066
+ return /* @__PURE__ */ jsx(
2067
+ "li",
2068
+ {
2069
+ className: "flex min-h-11 items-center gap-3 rounded-md px-2 py-1 hover:bg-muted",
2070
+ "data-testid": testId,
2071
+ children
2072
+ }
2073
+ );
2074
+ }
2075
+ function WebinarAudiencePanel({
2076
+ hands,
2077
+ attendees,
2078
+ onPromote,
2079
+ onDemote,
2080
+ searchThreshold = 8,
2081
+ now: fixedNow,
2082
+ className
2083
+ }) {
2084
+ const now = useNow(3e4, fixedNow);
2085
+ const { isPending, run } = usePending();
2086
+ const [query, setQuery] = useState("");
2087
+ const queue = useMemo(
2088
+ () => [...hands].sort((a, b) => Date.parse(a.raisedAt) - Date.parse(b.raisedAt)),
2089
+ [hands]
2090
+ );
2091
+ const searchable = attendees.length > searchThreshold;
2092
+ const q = searchable ? query.trim().toLowerCase() : "";
2093
+ const shown = q ? attendees.filter((a) => a.name.toLowerCase().includes(q)) : attendees;
2094
+ const action = (id, name, promote, where) => {
2095
+ const key = `${promote ? "promote" : "demote"}:${id}`;
2096
+ const busy = isPending(key);
2097
+ return /* @__PURE__ */ jsxs(
2098
+ Button,
2099
+ {
2100
+ type: "button",
2101
+ variant: "outline",
2102
+ size: "sm",
2103
+ className: "h-8 shrink-0",
2104
+ disabled: busy,
2105
+ "aria-label": promote ? `Promote ${name} to the stage` : `Move ${name} to the audience`,
2106
+ onClick: () => void run(key, () => promote ? onPromote(id) : onDemote(id)),
2107
+ "data-testid": `webinar-${where === "hand" ? "hand-" : ""}${promote ? "promote" : "demote"}-${id}`,
2108
+ children: [
2109
+ busy && /* @__PURE__ */ jsx(Loader2, { className: "size-4 animate-spin", "aria-hidden": true }),
2110
+ promote ? "Promote" : "Move to audience"
2111
+ ]
2112
+ }
2113
+ );
2114
+ };
2115
+ return /* @__PURE__ */ jsxs(
2116
+ "div",
2117
+ {
2118
+ className: cn("min-h-0 flex-1 overflow-y-auto px-2 pb-2", className),
2119
+ "data-testid": "webinar-audience",
2120
+ children: [
2121
+ /* @__PURE__ */ jsxs("section", { "aria-label": "Raised hands", children: [
2122
+ /* @__PURE__ */ jsxs(SectionTitle, { children: [
2123
+ /* @__PURE__ */ jsx(Hand, { className: "size-3.5", "aria-hidden": true }),
2124
+ "Raised hands (",
2125
+ queue.length,
2126
+ ")"
2127
+ ] }),
2128
+ queue.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-2 py-2 text-sm text-muted-foreground", "data-testid": "webinar-hands-empty", children: "No hands raised." }) : /* @__PURE__ */ jsx("ol", { children: queue.map((h) => /* @__PURE__ */ jsxs(Row, { testId: `webinar-hand-${h.id}`, children: [
2129
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [
2130
+ /* @__PURE__ */ jsx("span", { className: "flex min-w-0 items-center gap-1.5 text-sm", children: /* @__PURE__ */ jsx(CallPersonLabel, { name: h.name, entity: h.entity }) }),
2131
+ /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
2132
+ "Raised ",
2133
+ fromNow(h.raisedAt, now)
2134
+ ] })
2135
+ ] }),
2136
+ action(h.id, h.name, true, "hand")
2137
+ ] }, h.id)) })
2138
+ ] }),
2139
+ /* @__PURE__ */ jsxs("section", { "aria-label": "Attendees", children: [
2140
+ /* @__PURE__ */ jsxs(SectionTitle, { children: [
2141
+ "Attendees (",
2142
+ attendees.length,
2143
+ ")"
2144
+ ] }),
2145
+ searchable && /* @__PURE__ */ jsxs("div", { className: "relative px-2 pb-1", children: [
2146
+ /* @__PURE__ */ jsx(
2147
+ Search,
2148
+ {
2149
+ className: "pointer-events-none absolute left-4.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground",
2150
+ "aria-hidden": true
2151
+ }
2152
+ ),
2153
+ /* @__PURE__ */ jsx(
2154
+ Input,
2155
+ {
2156
+ type: "search",
2157
+ value: query,
2158
+ onChange: (e) => setQuery(e.target.value),
2159
+ placeholder: "Find someone",
2160
+ "aria-label": "Find an attendee",
2161
+ className: "h-8 pl-8 text-sm",
2162
+ "data-testid": "webinar-attendee-search"
2163
+ }
2164
+ )
2165
+ ] }),
2166
+ shown.length === 0 ? /* @__PURE__ */ jsx(
2167
+ "p",
2168
+ {
2169
+ className: "px-2 py-2 text-sm text-muted-foreground",
2170
+ "data-testid": "webinar-attendees-empty",
2171
+ children: q ? `No one matches \u201C${query.trim()}\u201D.` : "No attendees yet."
2172
+ }
2173
+ ) : /* @__PURE__ */ jsx("ul", { children: shown.map((a) => {
2174
+ const note = ROLE_NOTE[a.role];
2175
+ return /* @__PURE__ */ jsxs(Row, { testId: `webinar-attendee-${a.id}`, children: [
2176
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 items-center gap-1.5 text-sm", children: [
2177
+ /* @__PURE__ */ jsx(CallPersonLabel, { name: a.name, entity: a.entity }),
2178
+ (note || a.onStage) && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: [note, a.onStage ? "On stage" : null].filter(Boolean).join(" \xB7 ") })
2179
+ ] }),
2180
+ a.handRaised && /* @__PURE__ */ jsx(
2181
+ Hand,
2182
+ {
2183
+ className: "size-4 shrink-0 text-primary",
2184
+ "aria-label": "Hand raised",
2185
+ "data-testid": `webinar-attendee-hand-${a.id}`
2186
+ }
2187
+ ),
2188
+ a.onStage ? a.role !== "host" && action(a.id, a.name, false, "list") : !a.handRaised && action(a.id, a.name, true, "list")
2189
+ ] }, a.id);
2190
+ }) })
2191
+ ] })
2192
+ ]
2193
+ }
2194
+ );
2195
+ }
2196
+ var DEFAULT_MAX = 500;
2197
+ function sortQuestions(questions, filter) {
2198
+ const match = (s) => filter === "open" ? s === "open" || s === "answering" : s === filter;
2199
+ return questions.filter((q) => match(q.status)).sort(
2200
+ (a, b) => Number(b.status === "answering") - Number(a.status === "answering") || b.voteCount - a.voteCount || Date.parse(a.createdAt) - Date.parse(b.createdAt)
2201
+ );
2202
+ }
2203
+ function PanelComposer({
2204
+ placeholder,
2205
+ ariaLabel,
2206
+ sendLabel,
2207
+ maxLength,
2208
+ busy,
2209
+ onSend,
2210
+ onCancel,
2211
+ autoFocus,
2212
+ testId
2213
+ }) {
2214
+ const [text, setText] = useState("");
2215
+ const body = text.trim();
2216
+ const canSend = !!body && body.length <= maxLength && !busy;
2217
+ const send = () => {
2218
+ if (!canSend) return;
2219
+ void onSend(body).then((ok) => {
2220
+ if (ok) setText("");
2221
+ });
2222
+ };
2223
+ const onKeyDown = (e) => {
2224
+ if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
2225
+ e.preventDefault();
2226
+ send();
2227
+ } else if (e.key === "Escape" && onCancel) {
2228
+ e.preventDefault();
2229
+ onCancel();
2230
+ }
2231
+ };
2232
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", "data-testid": testId, children: [
2233
+ /* @__PURE__ */ jsx(
2234
+ Textarea,
2235
+ {
2236
+ value: text,
2237
+ onChange: (e) => setText(e.target.value),
2238
+ onKeyDown,
2239
+ placeholder,
2240
+ "aria-label": ariaLabel,
2241
+ maxLength,
2242
+ rows: 2,
2243
+ disabled: busy,
2244
+ autoFocus,
2245
+ className: "min-h-[60px] resize-none",
2246
+ "data-testid": `${testId}-input`
2247
+ }
2248
+ ),
2249
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2250
+ /* @__PURE__ */ jsxs(
2251
+ "span",
2252
+ {
2253
+ className: cn(
2254
+ "flex-1 text-xs tabular-nums text-muted-foreground",
2255
+ text.length >= maxLength && "text-red-text"
2256
+ ),
2257
+ "aria-live": "polite",
2258
+ "data-testid": `${testId}-count`,
2259
+ children: [
2260
+ text.length,
2261
+ "/",
2262
+ maxLength
2263
+ ]
2264
+ }
2265
+ ),
2266
+ onCancel && /* @__PURE__ */ jsx(
2267
+ Button,
2268
+ {
2269
+ type: "button",
2270
+ variant: "ghost",
2271
+ size: "sm",
2272
+ className: "h-8",
2273
+ onClick: onCancel,
2274
+ disabled: busy,
2275
+ "data-testid": `${testId}-cancel`,
2276
+ children: "Cancel"
2277
+ }
2278
+ ),
2279
+ /* @__PURE__ */ jsxs(
2280
+ Button,
2281
+ {
2282
+ type: "button",
2283
+ size: "sm",
2284
+ className: "h-8",
2285
+ onClick: send,
2286
+ disabled: !canSend,
2287
+ "data-testid": `${testId}-send`,
2288
+ children: [
2289
+ busy ? /* @__PURE__ */ jsx(Loader2, { className: "size-4 animate-spin", "aria-hidden": true }) : /* @__PURE__ */ jsx(Send, { className: "size-4", "aria-hidden": true }),
2290
+ sendLabel
2291
+ ]
2292
+ }
2293
+ )
2294
+ ] })
2295
+ ] });
2296
+ }
2297
+ function QuestionItem({
2298
+ q,
2299
+ team,
2300
+ now,
2301
+ maxLength,
2302
+ pending,
2303
+ run,
2304
+ onVote,
2305
+ onStatusChange,
2306
+ onAnswer,
2307
+ onDelete
2308
+ }) {
2309
+ const [answering, setAnswering] = useState(false);
2310
+ const voteKey = `vote:${q.id}`;
2311
+ const statusKey = `status:${q.id}`;
2312
+ const answerKey = `answer:${q.id}`;
2313
+ const busy = pending(statusKey) || pending(answerKey) || pending(`delete:${q.id}`);
2314
+ const setStatus = (status) => () => onStatusChange ? onStatusChange(q, status) : void 0;
2315
+ const can = team && !!onStatusChange;
2316
+ const primary = !can ? null : q.status === "open" ? { key: "live", label: "Answer live", icon: /* @__PURE__ */ jsx(Radio, {}), run: setStatus("answering") } : q.status === "answering" ? {
2317
+ key: "answered",
2318
+ label: "Mark answered",
2319
+ icon: /* @__PURE__ */ jsx(CheckCheck, {}),
2320
+ run: setStatus("answered")
2321
+ } : { key: "reopen", label: "Reopen", icon: /* @__PURE__ */ jsx(RotateCcw, {}), run: setStatus("open") };
2322
+ const more = [
2323
+ ...can && q.status === "open" ? [
2324
+ {
2325
+ key: "answered",
2326
+ label: "Mark answered",
2327
+ icon: /* @__PURE__ */ jsx(CheckCheck, {}),
2328
+ run: setStatus("answered")
2329
+ }
2330
+ ] : [],
2331
+ ...can && q.status === "answering" ? [{ key: "reopen", label: "Reopen", icon: /* @__PURE__ */ jsx(RotateCcw, {}), run: setStatus("open") }] : [],
2332
+ ...can && q.status !== "dismissed" ? [{ key: "dismiss", label: "Dismiss", icon: /* @__PURE__ */ jsx(X, {}), run: setStatus("dismissed") }] : [],
2333
+ ...q.mine && q.status === "open" && onDelete ? [
2334
+ {
2335
+ key: "delete",
2336
+ label: "Delete question",
2337
+ icon: /* @__PURE__ */ jsx(Trash2, {}),
2338
+ run: () => onDelete(q),
2339
+ destructive: true
2340
+ }
2341
+ ] : []
2342
+ ];
2343
+ const canAnswerInText = team && !!onAnswer && !q.answer && q.status !== "dismissed";
2344
+ return /* @__PURE__ */ jsxs(
2345
+ "li",
2346
+ {
2347
+ className: "flex gap-2 rounded-md border border-border p-2",
2348
+ "data-testid": `webinar-question-${q.id}`,
2349
+ "data-status": q.status,
2350
+ children: [
2351
+ /* @__PURE__ */ jsxs(
2352
+ Button,
2353
+ {
2354
+ type: "button",
2355
+ variant: "ghost",
2356
+ className: cn(
2357
+ "h-auto w-10 shrink-0 flex-col gap-0 self-start px-0 py-1 text-xs tabular-nums",
2358
+ q.voted ? "bg-primary/10 text-primary hover:bg-primary/15" : "text-muted-foreground"
2359
+ ),
2360
+ "aria-pressed": q.voted,
2361
+ "aria-label": q.mine ? `${q.voteCount} upvotes (your question)` : q.voted ? `Remove upvote (${q.voteCount})` : `Upvote (${q.voteCount})`,
2362
+ title: q.mine ? "You can't upvote your own question" : void 0,
2363
+ disabled: q.mine || pending(voteKey),
2364
+ onClick: () => void run(voteKey, () => onVote(q, !q.voted)),
2365
+ "data-testid": `webinar-question-vote-${q.id}`,
2366
+ children: [
2367
+ /* @__PURE__ */ jsx(ChevronUp, { className: "size-4", "aria-hidden": true }),
2368
+ /* @__PURE__ */ jsx("span", { "data-testid": `webinar-question-votes-${q.id}`, children: q.voteCount })
2369
+ ]
2370
+ }
2371
+ ),
2372
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1.5", children: [
2373
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground", children: [
2374
+ /* @__PURE__ */ jsx(
2375
+ "span",
2376
+ {
2377
+ className: "flex min-w-0 items-center gap-1.5 text-sm text-foreground",
2378
+ "data-testid": `webinar-question-asker-${q.id}`,
2379
+ children: q.askerName === null && !q.asker ? /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "Attendee" }) : /* @__PURE__ */ jsx(CallPersonLabel, { name: q.askerName ?? "", entity: q.asker, isLocal: q.mine })
2380
+ }
2381
+ ),
2382
+ /* @__PURE__ */ jsx("time", { dateTime: q.createdAt, children: fromNow(q.createdAt, now) }),
2383
+ q.status === "answering" && /* @__PURE__ */ jsx(
2384
+ CallStatusChip,
2385
+ {
2386
+ tone: "red",
2387
+ icon: /* @__PURE__ */ jsx(Radio, { className: "size-3.5 shrink-0", "aria-hidden": true }),
2388
+ label: "Answering live",
2389
+ testId: `webinar-question-live-${q.id}`
2390
+ }
2391
+ ),
2392
+ q.status === "answered" && /* @__PURE__ */ jsx(
2393
+ CallStatusChip,
2394
+ {
2395
+ tone: "green",
2396
+ icon: /* @__PURE__ */ jsx(CheckCheck, { className: "size-3.5 shrink-0", "aria-hidden": true }),
2397
+ label: "Answered",
2398
+ testId: `webinar-question-answered-${q.id}`
2399
+ }
2400
+ ),
2401
+ q.status === "dismissed" && /* @__PURE__ */ jsx(CallStatusChip, { tone: "neutral", label: "Dismissed" })
2402
+ ] }),
2403
+ /* @__PURE__ */ jsx(
2404
+ "p",
2405
+ {
2406
+ className: "whitespace-pre-wrap break-words text-sm",
2407
+ "data-testid": `webinar-question-body-${q.id}`,
2408
+ children: q.body
2409
+ }
2410
+ ),
2411
+ q.answer && /* @__PURE__ */ jsxs(
2412
+ "div",
2413
+ {
2414
+ className: "flex flex-col gap-1 rounded-md bg-muted px-2.5 py-2 text-sm",
2415
+ "data-testid": `webinar-question-answer-${q.id}`,
2416
+ children: [
2417
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-center gap-1.5 text-xs font-medium text-muted-foreground", children: [
2418
+ "Answer",
2419
+ q.answeredBy
2420
+ ] }),
2421
+ /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap break-words", children: q.answer })
2422
+ ]
2423
+ }
2424
+ ),
2425
+ answering && onAnswer && /* @__PURE__ */ jsx(
2426
+ PanelComposer,
2427
+ {
2428
+ placeholder: "Write an answer",
2429
+ ariaLabel: "Answer in text",
2430
+ sendLabel: "Send answer",
2431
+ maxLength,
2432
+ busy: pending(answerKey),
2433
+ autoFocus: true,
2434
+ onSend: (text) => run(answerKey, () => onAnswer(q, text)).then((ok) => {
2435
+ if (ok) setAnswering(false);
2436
+ return ok;
2437
+ }),
2438
+ onCancel: () => setAnswering(false),
2439
+ testId: `webinar-answer-${q.id}`
2440
+ }
2441
+ ),
2442
+ (primary || canAnswerInText || more.length > 0) && !answering && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [
2443
+ primary && /* @__PURE__ */ jsxs(
2444
+ Button,
2445
+ {
2446
+ type: "button",
2447
+ variant: "outline",
2448
+ size: "sm",
2449
+ className: "h-7 gap-1.5 px-2 text-xs [&_svg]:size-3.5",
2450
+ disabled: busy,
2451
+ onClick: () => void run(statusKey, primary.run),
2452
+ "data-testid": `webinar-question-${primary.key}-${q.id}`,
2453
+ children: [
2454
+ pending(statusKey) ? /* @__PURE__ */ jsx(Loader2, { className: "animate-spin", "aria-hidden": true }) : primary.icon,
2455
+ primary.label
2456
+ ]
2457
+ }
2458
+ ),
2459
+ canAnswerInText && /* @__PURE__ */ jsxs(
2460
+ Button,
2461
+ {
2462
+ type: "button",
2463
+ variant: "ghost",
2464
+ size: "sm",
2465
+ className: "h-7 gap-1.5 px-2 text-xs [&_svg]:size-3.5",
2466
+ disabled: busy,
2467
+ onClick: () => setAnswering(true),
2468
+ "data-testid": `webinar-question-answer-text-${q.id}`,
2469
+ children: [
2470
+ /* @__PURE__ */ jsx(MessageSquareText, { "aria-hidden": true }),
2471
+ "Answer in text"
2472
+ ]
2473
+ }
2474
+ ),
2475
+ more.length > 0 && /* @__PURE__ */ jsxs(DropdownMenu, { children: [
2476
+ /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
2477
+ Button,
2478
+ {
2479
+ type: "button",
2480
+ variant: "ghost",
2481
+ size: "icon",
2482
+ className: "ml-auto size-7 text-muted-foreground hover:text-foreground",
2483
+ "aria-label": "More question actions",
2484
+ disabled: busy,
2485
+ "data-testid": `webinar-question-menu-${q.id}`,
2486
+ children: /* @__PURE__ */ jsx(MoreHorizontal, { className: "size-4" })
2487
+ }
2488
+ ) }),
2489
+ /* @__PURE__ */ jsx(DropdownMenuContent, { align: "end", className: "w-48", children: more.map((a) => /* @__PURE__ */ jsxs(
2490
+ DropdownMenuItem,
2491
+ {
2492
+ onSelect: () => void run(a.key === "delete" ? `delete:${q.id}` : statusKey, a.run),
2493
+ className: cn(a.destructive && "text-destructive focus:text-destructive"),
2494
+ "data-testid": `webinar-question-${a.key}-${q.id}`,
2495
+ children: [
2496
+ a.icon,
2497
+ a.label
2498
+ ]
2499
+ },
2500
+ a.key
2501
+ )) })
2502
+ ] })
2503
+ ] })
2504
+ ] })
2505
+ ]
2506
+ }
2507
+ );
2508
+ }
2509
+ var EMPTY = {
2510
+ open: {
2511
+ team: "Questions from the audience appear here.",
2512
+ audience: "No questions yet. Ask the first one."
2513
+ },
2514
+ answered: { team: "No answered questions yet.", audience: "No answered questions yet." },
2515
+ dismissed: { team: "Nothing dismissed.", audience: "Nothing dismissed." }
2516
+ };
2517
+ function QAPanel({
2518
+ questions,
2519
+ team = false,
2520
+ maxLength = DEFAULT_MAX,
2521
+ canAsk = true,
2522
+ onAsk,
2523
+ onVote,
2524
+ onStatusChange,
2525
+ onAnswer,
2526
+ onDelete,
2527
+ defaultFilter = "open",
2528
+ now: fixedNow,
2529
+ className
2530
+ }) {
2531
+ const now = useNow(3e4, fixedNow);
2532
+ const { isPending, run } = usePending();
2533
+ const [filter, setFilter] = useState(
2534
+ defaultFilter === "dismissed" && !team ? "open" : defaultFilter
2535
+ );
2536
+ const filters = [
2537
+ { id: "open", label: "Open" },
2538
+ { id: "answered", label: "Answered" },
2539
+ ...team ? [{ id: "dismissed", label: "Dismissed" }] : []
2540
+ ];
2541
+ const lists = useMemo(
2542
+ () => ({
2543
+ open: sortQuestions(questions, "open"),
2544
+ answered: sortQuestions(questions, "answered"),
2545
+ // Dismissed questions are the team's business only.
2546
+ dismissed: team ? sortQuestions(questions, "dismissed") : []
2547
+ }),
2548
+ [questions, team]
2549
+ );
2550
+ const shown = lists[filter];
2551
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex min-h-0 flex-1 flex-col", className), "data-testid": "webinar-qa", children: [
2552
+ /* @__PURE__ */ jsxs(
2553
+ Tabs,
2554
+ {
2555
+ value: filter,
2556
+ onValueChange: (v) => setFilter(v),
2557
+ className: "flex min-h-0 flex-1 flex-col",
2558
+ children: [
2559
+ /* @__PURE__ */ jsx("div", { className: "shrink-0 px-3 pt-3", children: /* @__PURE__ */ jsx(TabsList, { className: "h-8 w-full", children: filters.map((f) => /* @__PURE__ */ jsxs(
2560
+ TabsTrigger,
2561
+ {
2562
+ value: f.id,
2563
+ className: "h-6 flex-1 gap-1.5 px-2 text-xs",
2564
+ "data-testid": `webinar-qa-filter-${f.id}`,
2565
+ children: [
2566
+ f.label,
2567
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums text-muted-foreground", children: lists[f.id].length })
2568
+ ]
2569
+ },
2570
+ f.id
2571
+ )) }) }),
2572
+ /* @__PURE__ */ jsx(TabsContent, { value: filter, className: "mt-0 min-h-0 flex-1 overflow-y-auto p-3", children: shown.length === 0 ? /* @__PURE__ */ jsx(
2573
+ "p",
2574
+ {
2575
+ className: "py-6 text-center text-sm text-muted-foreground",
2576
+ "data-testid": "webinar-qa-empty",
2577
+ children: filter === "open" && questions.length > 0 ? "No open questions right now." : EMPTY[filter][team ? "team" : "audience"]
2578
+ }
2579
+ ) : /* @__PURE__ */ jsx("ol", { className: "flex flex-col gap-2", children: shown.map((q) => /* @__PURE__ */ jsx(
2580
+ QuestionItem,
2581
+ {
2582
+ q,
2583
+ team,
2584
+ now,
2585
+ maxLength,
2586
+ pending: isPending,
2587
+ run,
2588
+ onVote,
2589
+ onStatusChange,
2590
+ onAnswer,
2591
+ onDelete
2592
+ },
2593
+ q.id
2594
+ )) }) })
2595
+ ]
2596
+ }
2597
+ ),
2598
+ canAsk && /* @__PURE__ */ jsx("div", { className: "shrink-0 border-t border-border p-3", children: /* @__PURE__ */ jsx(
2599
+ PanelComposer,
2600
+ {
2601
+ placeholder: "Ask a question",
2602
+ ariaLabel: "Ask a question",
2603
+ sendLabel: "Ask",
2604
+ maxLength,
2605
+ busy: isPending("ask"),
2606
+ onSend: (text) => run("ask", () => onAsk(text)),
2607
+ testId: "webinar-ask"
2608
+ }
2609
+ ) })
2610
+ ] });
2611
+ }
2612
+ var OPTIONS = [
2613
+ { value: "panelists", label: "Panelists only" },
2614
+ { value: "everyone", label: "Everyone" }
2615
+ ];
2616
+ function ChatPermissionSetting({
2617
+ value,
2618
+ onChange,
2619
+ pending = false
2620
+ }) {
2621
+ const [choosing, setChoosing] = useState(null);
2622
+ const busy = pending || choosing !== null;
2623
+ const select = (next) => {
2624
+ const v = next;
2625
+ if (v === value || busy) return;
2626
+ const result = onChange(v);
2627
+ if (result && typeof result.then === "function") {
2628
+ setChoosing(v);
2629
+ void result.then(
2630
+ () => setChoosing(null),
2631
+ () => setChoosing(null)
2632
+ );
2633
+ }
2634
+ };
2635
+ return /* @__PURE__ */ jsxs("div", { role: "group", "aria-label": "Who can chat", "data-testid": "webinar-chat-permission", children: [
2636
+ /* @__PURE__ */ jsxs(DropdownMenuLabel, { className: "flex items-center gap-1.5 text-xs font-medium text-muted-foreground", children: [
2637
+ "Who can chat",
2638
+ busy && /* @__PURE__ */ jsx(Loader2, { className: "size-3.5 animate-spin", "aria-label": "Saving" })
2639
+ ] }),
2640
+ /* @__PURE__ */ jsx(DropdownMenuRadioGroup, { value: choosing ?? value, onValueChange: select, children: OPTIONS.map((o) => /* @__PURE__ */ jsx(
2641
+ DropdownMenuRadioItem,
2642
+ {
2643
+ value: o.value,
2644
+ disabled: busy,
2645
+ "data-testid": `webinar-chat-${o.value}`,
2646
+ children: o.label
2647
+ },
2648
+ o.value
2649
+ )) })
2650
+ ] });
2651
+ }
2652
+ function chatPermissionSection(props) {
2653
+ return { key: "webinar-chat", content: /* @__PURE__ */ jsx(ChatPermissionSetting, { ...props }) };
2654
+ }
2655
+ function OnStageCheck({
2656
+ open,
2657
+ onJoinStage,
2658
+ onStayInAudience,
2659
+ busy = false,
2660
+ error,
2661
+ mirror = true,
2662
+ describeError = describeMediaError
2663
+ }) {
2664
+ const { isPending, run } = usePending();
2665
+ const working = busy || isPending("join") || isPending("stay");
2666
+ const stay = () => {
2667
+ if (!working) void run("stay", onStayInAudience);
2668
+ };
2669
+ return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange: (next) => !next && stay(), children: /* @__PURE__ */ jsx(DialogContent, { presentation: "sheet", className: "max-w-3xl", "data-testid": "webinar-on-stage", children: /* @__PURE__ */ jsx(
2670
+ PreJoin,
2671
+ {
2672
+ heading: "You're on stage",
2673
+ headingAs: DialogTitle,
2674
+ joinLabel: "Join stage",
2675
+ cancelLabel: "Stay in audience",
2676
+ busy: working,
2677
+ error,
2678
+ mirror,
2679
+ describeError,
2680
+ onJoin: (choices) => void run("join", () => onJoinStage(choices)),
2681
+ onCancel: stay,
2682
+ children: /* @__PURE__ */ jsx(DialogDescription, { children: "A host invited you to speak. Check your microphone and camera: once you join, everyone sees and hears you." })
2683
+ }
2684
+ ) }) });
2685
+ }
2686
+ function WebinarViewer({
2687
+ header,
2688
+ stage,
2689
+ stageRef,
2690
+ stageOverlay,
2691
+ captions,
2692
+ side,
2693
+ sideToggle,
2694
+ hand,
2695
+ onLeave,
2696
+ left,
2697
+ controls,
2698
+ shortcuts = true,
2699
+ className
2700
+ }) {
2701
+ return /* @__PURE__ */ jsxs(
2702
+ "div",
2703
+ {
2704
+ className: cn(
2705
+ "@container flex size-full min-h-0 flex-col bg-background text-foreground",
2706
+ className
2707
+ ),
2708
+ "data-testid": "webinar-viewer",
2709
+ children: [
2710
+ header,
2711
+ /* @__PURE__ */ jsxs("div", { className: "relative flex min-h-0 flex-1", children: [
2712
+ /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col gap-2 px-3 pb-3 pt-1", children: [
2713
+ /* @__PURE__ */ jsxs("div", { ref: stageRef, className: "relative min-h-0 flex-1", "data-testid": "call-stage", children: [
2714
+ stage,
2715
+ stageOverlay
2716
+ ] }),
2717
+ captions
2718
+ ] }),
2719
+ side && /* @__PURE__ */ jsx(
2720
+ "div",
2721
+ {
2722
+ className: "absolute inset-0 z-20 flex min-h-0 flex-col border-t border-border motion-safe:animate-in motion-safe:fade-in-0 @3xl:static @3xl:z-auto @3xl:w-80 @3xl:shrink-0 @3xl:border-l @3xl:border-t-0",
2723
+ "data-testid": "webinar-viewer-side",
2724
+ children: side
2725
+ }
2726
+ )
2727
+ ] }),
2728
+ /* @__PURE__ */ jsx(CallControlBar, { shortcuts, left, right: /* @__PURE__ */ jsx(LeaveButton, { onLeave }), children: /* @__PURE__ */ jsxs(ControlGroup, { children: [
2729
+ /* @__PURE__ */ jsx(RaiseHandButton, { ...hand }),
2730
+ sideToggle && /* @__PURE__ */ jsxs(
2731
+ CallControlButton,
2732
+ {
2733
+ label: sideToggle.open ? `Close ${sideToggle.label}` : sideToggle.label,
2734
+ tone: sideToggle.open ? "active" : "neutral",
2735
+ "aria-pressed": sideToggle.open,
2736
+ onClick: sideToggle.onToggle,
2737
+ "data-testid": "webinar-side-toggle",
2738
+ children: [
2739
+ /* @__PURE__ */ jsx(MessageCircleQuestion, { "aria-hidden": true }),
2740
+ !!sideToggle.count && /* @__PURE__ */ jsx(ControlBadge, { count: sideToggle.count, tone: "quiet" })
2741
+ ]
2742
+ }
2743
+ ),
2744
+ controls
2745
+ ] }) })
2746
+ ]
2747
+ }
2748
+ );
2749
+ }
2750
+
2751
+ export { BackstageBanner, CallControlBar, CallControlButton, CallFocus, CallGrid, CallHeader, CallNotice, CallParticipants, CallPersonLabel, CallSidePanel, CallStageFrame, CallStateMessage, CallStatusChip, CallTile, CaptionsStrip, ChatPermissionSetting, ConnectionOverlay, ControlBadge, ControlDivider, ControlGroup, DEFAULT_JOIN_CHOICES, ElapsedTime, EncryptionStatusChip, IncomingCallCard, LeaveButton, LobbyRequests, MediaControl, MinimisedCall, MoreMenu, OnStageCheck, PanelComposer, PreJoin, QAPanel, RaiseHandButton, RecordControl, RecordingStatusChip, SpeakingNow, TranscriptionStatusChip, VoiceMeter, WebinarAudiencePanel, WebinarEndConfirm, WebinarLiveChip, WebinarLiveControl, WebinarViewer, WebinarWaiting, chatPermissionSection, describeMediaError, sortQuestions, useBriefNotice, useElementSize, useShortcutKeys };
2752
+ //# sourceMappingURL=index.js.map
2753
+ //# sourceMappingURL=index.js.map