@arbidocs/blocks 0.4.109 → 0.4.110

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 (112) hide show
  1. package/dist/agents/index.cjs +286 -286
  2. package/dist/agents/index.js +10 -10
  3. package/dist/agents/tasks/ConversationListView.d.ts +5 -1
  4. package/dist/agents/tasks/TaskActivityTimeline.d.ts +5 -1
  5. package/dist/calendar/EventDetails.d.ts +1 -1
  6. package/dist/calendar/index.cjs +13 -0
  7. package/dist/calendar/index.cjs.map +1 -1
  8. package/dist/calendar/index.js +13 -0
  9. package/dist/calendar/index.js.map +1 -1
  10. package/dist/calendar/types.d.ts +3 -0
  11. package/dist/calls/CallSidePanel.d.ts +17 -0
  12. package/dist/calls/CallStage.d.ts +57 -0
  13. package/dist/calls/ControlBar.d.ts +91 -0
  14. package/dist/calls/ControlButton.d.ts +44 -0
  15. package/dist/calls/PreJoin.d.ts +31 -0
  16. package/dist/calls/cards.d.ts +54 -0
  17. package/dist/calls/hooks.d.ts +20 -0
  18. package/dist/calls/index.cjs +1853 -0
  19. package/dist/calls/index.cjs.map +1 -0
  20. package/dist/calls/index.d.ts +20 -0
  21. package/dist/calls/index.js +1737 -0
  22. package/dist/calls/index.js.map +1 -0
  23. package/dist/calls/join-choices.d.ts +11 -0
  24. package/dist/calls/layout.d.ts +106 -0
  25. package/dist/calls/panels.d.ts +55 -0
  26. package/dist/calls/status.d.ts +69 -0
  27. package/dist/calls/tiles.d.ts +59 -0
  28. package/dist/chat/Message.d.ts +17 -2
  29. package/dist/chat/TranscriptLine.d.ts +50 -0
  30. package/dist/chat/index.cjs +147 -126
  31. package/dist/chat/index.d.ts +3 -1
  32. package/dist/chat/index.js +5 -4
  33. package/dist/chat/transcript-runs.d.ts +6 -0
  34. package/dist/{chunk-QKWQMOHN.cjs → chunk-2UAJVK3C.cjs} +4 -4
  35. package/dist/{chunk-QKWQMOHN.cjs.map → chunk-2UAJVK3C.cjs.map} +1 -1
  36. package/dist/{chunk-7C35WXYB.js → chunk-5HFPRI3W.js} +5 -5
  37. package/dist/chunk-5HFPRI3W.js.map +1 -0
  38. package/dist/{chunk-ZSV2NQQP.js → chunk-6RHHMBZ5.js} +158 -5
  39. package/dist/chunk-6RHHMBZ5.js.map +1 -0
  40. package/dist/{chunk-FQX6N5G6.js → chunk-A35KT2II.js} +35 -18
  41. package/dist/chunk-A35KT2II.js.map +1 -0
  42. package/dist/{chunk-PGDL26TQ.js → chunk-AY32GGKU.js} +10 -4
  43. package/dist/{chunk-PGDL26TQ.js.map → chunk-AY32GGKU.js.map} +1 -1
  44. package/dist/{chunk-S55VDTX3.js → chunk-CECWAIHL.js} +3 -3
  45. package/dist/{chunk-S55VDTX3.js.map → chunk-CECWAIHL.js.map} +1 -1
  46. package/dist/{chunk-UABPHZWZ.js → chunk-CXZ7FPTQ.js} +3 -3
  47. package/dist/{chunk-UABPHZWZ.js.map → chunk-CXZ7FPTQ.js.map} +1 -1
  48. package/dist/{chunk-C2HRZSAT.cjs → chunk-DJM7DLCT.cjs} +5 -5
  49. package/dist/chunk-DJM7DLCT.cjs.map +1 -0
  50. package/dist/{chunk-I4XQCE6U.cjs → chunk-HBBJYRGI.cjs} +162 -5
  51. package/dist/{chunk-ZSV2NQQP.js.map → chunk-HBBJYRGI.cjs.map} +1 -1
  52. package/dist/chunk-MWDW5UBW.cjs +199 -0
  53. package/dist/chunk-MWDW5UBW.cjs.map +1 -0
  54. package/dist/{chunk-NUTLXVPX.js → chunk-NRUPUDWB.js} +16 -7
  55. package/dist/chunk-NRUPUDWB.js.map +1 -0
  56. package/dist/{chunk-DDF5GPIJ.js → chunk-P5ST47VY.js} +3 -3
  57. package/dist/{chunk-DDF5GPIJ.js.map → chunk-P5ST47VY.js.map} +1 -1
  58. package/dist/{chunk-W7NZRZ54.js → chunk-SQNAJPNR.js} +4 -4
  59. package/dist/{chunk-W7NZRZ54.js.map → chunk-SQNAJPNR.js.map} +1 -1
  60. package/dist/{chunk-PDRYGPZB.cjs → chunk-VP4KJ4LK.cjs} +16 -6
  61. package/dist/chunk-VP4KJ4LK.cjs.map +1 -0
  62. package/dist/{chunk-GBYY3PIC.cjs → chunk-VZKBI2KJ.cjs} +79 -62
  63. package/dist/chunk-VZKBI2KJ.cjs.map +1 -0
  64. package/dist/{chunk-5YXG342J.cjs → chunk-W7ZDBPJ3.cjs} +4 -4
  65. package/dist/{chunk-5YXG342J.cjs.map → chunk-W7ZDBPJ3.cjs.map} +1 -1
  66. package/dist/{chunk-SKSE4OT6.cjs → chunk-WPVIOGDG.cjs} +5 -5
  67. package/dist/{chunk-SKSE4OT6.cjs.map → chunk-WPVIOGDG.cjs.map} +1 -1
  68. package/dist/{chunk-3US5OIDQ.cjs → chunk-XSUOEJB3.cjs} +9 -3
  69. package/dist/chunk-XSUOEJB3.cjs.map +1 -0
  70. package/dist/chunk-Z3AZ7Q5X.js +177 -0
  71. package/dist/chunk-Z3AZ7Q5X.js.map +1 -0
  72. package/dist/{chunk-Z5VUZ6YD.cjs → chunk-ZV3BKLFY.cjs} +3 -3
  73. package/dist/{chunk-Z5VUZ6YD.cjs.map → chunk-ZV3BKLFY.cjs.map} +1 -1
  74. package/dist/collaboration/index.cjs +41 -41
  75. package/dist/collaboration/index.js +1 -1
  76. package/dist/connect/index.cjs +66 -66
  77. package/dist/connect/index.js +13 -13
  78. package/dist/facts/index.cjs +4 -4
  79. package/dist/facts/index.js +4 -4
  80. package/dist/files/index.cjs +6 -6
  81. package/dist/files/index.js +6 -6
  82. package/dist/home/index.cjs +17 -17
  83. package/dist/home/index.js +3 -3
  84. package/dist/index.cjs +846 -825
  85. package/dist/index.cjs.map +1 -1
  86. package/dist/index.js +21 -20
  87. package/dist/index.js.map +1 -1
  88. package/dist/retrieval/index.cjs +1 -1
  89. package/dist/retrieval/index.js +1 -1
  90. package/dist/settings/index.cjs +37 -37
  91. package/dist/settings/index.js +5 -5
  92. package/dist/shell/index.cjs +12 -12
  93. package/dist/shell/index.js +4 -4
  94. package/dist/site/index.cjs +5 -5
  95. package/dist/site/index.js +1 -1
  96. package/dist/studio/index.cjs +5 -5
  97. package/dist/studio/index.js +4 -4
  98. package/dist/workflow-canvas/index.cjs +53 -53
  99. package/dist/workflow-canvas/index.js +10 -10
  100. package/dist/workspaces/index.cjs +2 -2
  101. package/dist/workspaces/index.js +2 -2
  102. package/node_modules/@arbidocs/coedit/package.json +1 -1
  103. package/package.json +15 -5
  104. package/src/calls/calls.css +157 -0
  105. package/dist/chunk-3US5OIDQ.cjs.map +0 -1
  106. package/dist/chunk-7C35WXYB.js.map +0 -1
  107. package/dist/chunk-C2HRZSAT.cjs.map +0 -1
  108. package/dist/chunk-FQX6N5G6.js.map +0 -1
  109. package/dist/chunk-GBYY3PIC.cjs.map +0 -1
  110. package/dist/chunk-I4XQCE6U.cjs.map +0 -1
  111. package/dist/chunk-NUTLXVPX.js.map +0 -1
  112. package/dist/chunk-PDRYGPZB.cjs.map +0 -1
@@ -0,0 +1,1737 @@
1
+ "use client";
2
+ import { TabHeader } from '../chunk-4WWM3GQ5.js';
3
+ import { NOTICE_MS, gridSlots, gridLayout, spotlightBox, CAPTION_TTL_MS, CAPTION_LINES, speakerAccent, formatElapsed, initialsOf } from '../chunk-Z3AZ7Q5X.js';
4
+ 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';
5
+ import '../chunk-NPLYHR2E.js';
6
+ import '../chunk-7F6CBP3Z.js';
7
+ import '../chunk-VUNV25KB.js';
8
+ import { forwardRef, useState, useRef, useMemo, useEffect } from 'react';
9
+ import { Button, cn, Tooltip, TooltipTrigger, TooltipContent, Separator, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuLabel, DropdownMenuItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuCheckboxItem, Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@arbidocs/react/ui';
10
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
11
+ 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, UserX } from 'lucide-react';
12
+
13
+ var TONE = {
14
+ neutral: "bg-muted text-foreground hover:bg-accent",
15
+ // Solid: a control whose thing is on reads as on at a glance (CC on, chat open, sharing).
16
+ active: "bg-primary text-primary-foreground hover:bg-primary/90",
17
+ off: "bg-red-bg text-red-text hover:bg-red-bg/80",
18
+ danger: "bg-destructive text-destructive-foreground hover:bg-destructive/90"
19
+ };
20
+ var CallControlButton = forwardRef(
21
+ function CallControlButton2({
22
+ label,
23
+ shortcut,
24
+ tone = "neutral",
25
+ side = "top",
26
+ tooltip = true,
27
+ className,
28
+ children,
29
+ ...props
30
+ }, ref) {
31
+ const button = /* @__PURE__ */ jsx(
32
+ Button,
33
+ {
34
+ ref,
35
+ type: "button",
36
+ variant: "ghost",
37
+ size: "icon",
38
+ "aria-label": label,
39
+ className: cn("relative shrink-0 [&_svg]:size-[18px]", TONE[tone], className),
40
+ ...props,
41
+ children
42
+ }
43
+ );
44
+ if (!tooltip) return button;
45
+ return /* @__PURE__ */ jsxs(Tooltip, { children: [
46
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx("span", { className: "inline-flex shrink-0", children: button }) }),
47
+ /* @__PURE__ */ jsxs(TooltipContent, { side, children: [
48
+ label,
49
+ shortcut && /* @__PURE__ */ jsx("kbd", { className: "ml-2 rounded-sm border border-border px-1 font-sans text-[11px] text-muted-foreground", children: shortcut })
50
+ ] })
51
+ ] });
52
+ }
53
+ );
54
+ function ControlBadge({
55
+ count,
56
+ tone = "alert",
57
+ testId
58
+ }) {
59
+ return /* @__PURE__ */ jsx(
60
+ "span",
61
+ {
62
+ className: cn(
63
+ "pointer-events-none absolute -right-1 min-w-[1.125rem] rounded-full px-1 text-center text-[10px] font-semibold leading-[1.125rem]",
64
+ tone === "alert" ? "-top-1 bg-destructive text-destructive-foreground ring-2 ring-background" : "-bottom-1 bg-background text-muted-foreground ring-1 ring-border"
65
+ ),
66
+ "data-testid": testId,
67
+ children: count > 99 ? "99+" : count
68
+ }
69
+ );
70
+ }
71
+ function CallStatusChip({
72
+ tone,
73
+ icon,
74
+ label,
75
+ compact = false,
76
+ pulse = false,
77
+ title,
78
+ children,
79
+ className,
80
+ testId,
81
+ ...rest
82
+ }) {
83
+ const tones = {
84
+ red: "border-red-border bg-red-bg text-red-text",
85
+ green: "border-green-border bg-green-bg text-green-text",
86
+ yellow: "border-yellow-border bg-yellow-bg text-yellow-text",
87
+ neutral: "border-border bg-muted text-muted-foreground"
88
+ };
89
+ return /* @__PURE__ */ jsxs(
90
+ "span",
91
+ {
92
+ className: cn(
93
+ "inline-flex h-6 shrink-0 items-center gap-1.5 rounded-md border px-2 text-xs font-medium",
94
+ tones[tone],
95
+ className
96
+ ),
97
+ title,
98
+ "data-testid": testId,
99
+ ...rest,
100
+ children: [
101
+ pulse && /* @__PURE__ */ jsxs("span", { className: "relative flex size-2 shrink-0", "aria-hidden": true, children: [
102
+ /* @__PURE__ */ jsx("span", { className: "absolute inline-flex size-full animate-ping rounded-full bg-red-solid opacity-60 motion-reduce:hidden" }),
103
+ /* @__PURE__ */ jsx("span", { className: "relative inline-flex size-2 rounded-full bg-red-solid" })
104
+ ] }),
105
+ icon,
106
+ /* @__PURE__ */ jsx("span", { className: cn("min-w-0 truncate", compact && "sr-only"), children: label }),
107
+ children
108
+ ]
109
+ }
110
+ );
111
+ }
112
+ function useElementSize() {
113
+ const [size, setSize] = useState({ width: 0, height: 0 });
114
+ const observer = useRef(null);
115
+ const ref = useMemo(
116
+ () => (el) => {
117
+ observer.current?.disconnect();
118
+ observer.current = null;
119
+ if (!el || typeof ResizeObserver === "undefined") return;
120
+ const ro = new ResizeObserver((entries) => {
121
+ const r = entries[0]?.contentRect;
122
+ if (!r) return;
123
+ setSize(
124
+ (s) => s.width === r.width && s.height === r.height ? s : { width: r.width, height: r.height }
125
+ );
126
+ });
127
+ ro.observe(el);
128
+ observer.current = ro;
129
+ },
130
+ []
131
+ );
132
+ useEffect(() => () => observer.current?.disconnect(), []);
133
+ return [ref, size];
134
+ }
135
+ function useBriefNotice(announce = true, onFolded, ms = NOTICE_MS) {
136
+ const [expanded, setExpanded] = useState(announce);
137
+ const folded = useRef(onFolded);
138
+ folded.current = onFolded;
139
+ useEffect(() => {
140
+ if (!expanded) return;
141
+ const id = setTimeout(() => {
142
+ setExpanded(false);
143
+ folded.current?.();
144
+ }, ms);
145
+ return () => clearTimeout(id);
146
+ }, [expanded, ms]);
147
+ return expanded;
148
+ }
149
+ function typing(e) {
150
+ const el = e.target;
151
+ return !!el?.closest?.('input, textarea, select, [contenteditable="true"], [role="textbox"]');
152
+ }
153
+ function useShortcutKeys(ref, enabled) {
154
+ useEffect(() => {
155
+ if (!enabled) return;
156
+ const onKey = (e) => {
157
+ if (e.repeat || e.ctrlKey || e.metaKey || e.altKey || typing(e)) return;
158
+ const key = e.key.toLowerCase();
159
+ if (!/^[a-z]$/.test(key)) return;
160
+ const button = ref.current?.querySelector(
161
+ `[data-call-shortcut="${key}"]:not(:disabled)`
162
+ );
163
+ if (!button) return;
164
+ e.preventDefault();
165
+ button.click();
166
+ };
167
+ window.addEventListener("keydown", onKey);
168
+ return () => window.removeEventListener("keydown", onKey);
169
+ }, [ref, enabled]);
170
+ }
171
+ var CallTile = forwardRef(function CallTile2({
172
+ name,
173
+ initials,
174
+ video,
175
+ source = "camera",
176
+ local = false,
177
+ mirror = false,
178
+ speaking = false,
179
+ level = 0,
180
+ micMuted = false,
181
+ poorConnection = false,
182
+ size = "normal",
183
+ youLabel,
184
+ className,
185
+ style,
186
+ ...rest
187
+ }, ref) {
188
+ const screen = source === "screen";
189
+ const ring = speaking && !screen ? Math.min(1, Math.max(0, level)) : 0;
190
+ return /* @__PURE__ */ jsxs(
191
+ "div",
192
+ {
193
+ ref,
194
+ className: cn("call-tile", className),
195
+ "data-call-tile": "",
196
+ "data-source": source,
197
+ "data-local": local,
198
+ "data-mirror": mirror && source === "camera" && !!video,
199
+ "data-speaking": speaking && !screen,
200
+ style: { "--call-level": ring.toFixed(2), ...style },
201
+ ...rest,
202
+ children: [
203
+ 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 }) }),
204
+ /* @__PURE__ */ jsxs("div", { className: "call-tile-name", children: [
205
+ typeof name === "string" ? /* @__PURE__ */ jsxs("span", { className: "call-tile-chip min-w-0", children: [
206
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: name }),
207
+ local && !screen && youLabel && /* @__PURE__ */ jsx("span", { className: "shrink-0 opacity-80", children: youLabel })
208
+ ] }) : /* @__PURE__ */ jsx("span", { className: "call-tile-chip min-w-0 overflow-hidden py-0.5 pl-0.5", children: name }),
209
+ (screen || micMuted || speaking) && /* @__PURE__ */ jsxs("span", { className: "call-tile-chip shrink-0", children: [
210
+ screen && /* @__PURE__ */ jsx(MonitorUp, { className: "size-3.5", "aria-label": "Screen" }),
211
+ !screen && micMuted && /* @__PURE__ */ jsx(MicOff, { className: "size-3.5", "aria-label": "Microphone off" }),
212
+ speaking && !screen && /* @__PURE__ */ jsx(AudioLines, { className: "call-tile-speaking size-3.5", "aria-label": "Speaking" })
213
+ ] })
214
+ ] }),
215
+ poorConnection && /* @__PURE__ */ jsx("span", { className: "call-tile-signal", title: "Poor connection", children: /* @__PURE__ */ jsx(SignalLow, { className: "size-3.5", "aria-label": "Poor connection" }) })
216
+ ]
217
+ }
218
+ );
219
+ });
220
+ var GRID_GAP = 8;
221
+ function CallGrid({
222
+ items,
223
+ getKey,
224
+ renderTile,
225
+ moreLabel = (n) => `${n} more`,
226
+ className
227
+ }) {
228
+ const [ref, size] = useElementSize();
229
+ const { shown, hidden } = gridSlots(items);
230
+ const count = shown.length + (hidden ? 1 : 0);
231
+ const layout = gridLayout(count, size.width, size.height, GRID_GAP);
232
+ const box = { width: layout.tileWidth, height: layout.tileHeight };
233
+ const rows = [];
234
+ let next = 0;
235
+ for (const n of layout.rowCounts) rows.push(Array.from({ length: n }, () => next++));
236
+ return /* @__PURE__ */ jsx(
237
+ "div",
238
+ {
239
+ ref,
240
+ className: cn("flex size-full min-h-0 flex-col items-center", className),
241
+ style: { gap: GRID_GAP },
242
+ "data-testid": "call-grid",
243
+ "data-cols": layout.cols,
244
+ children: layout.tileWidth > 0 && rows.map((row, r) => /* @__PURE__ */ jsx("div", { className: "flex justify-center", style: { gap: GRID_GAP }, children: row.map(
245
+ (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: [
246
+ /* @__PURE__ */ jsx(Users, { className: "size-5", "aria-hidden": true }),
247
+ /* @__PURE__ */ jsx("span", { children: moreLabel(hidden) })
248
+ ] }, "more")
249
+ ) }, r))
250
+ }
251
+ );
252
+ }
253
+ function CallFocus({
254
+ focus,
255
+ others,
256
+ getKey,
257
+ renderTile,
258
+ className,
259
+ testId = "call-focus"
260
+ }) {
261
+ const [ref, size] = useElementSize();
262
+ const narrow = size.width > 0 && size.width < 640;
263
+ const box = spotlightBox(size.width, size.height, others.length, narrow);
264
+ return /* @__PURE__ */ jsxs(
265
+ "div",
266
+ {
267
+ ref,
268
+ className: cn(
269
+ "flex size-full min-h-0 gap-2",
270
+ narrow ? "flex-col items-center" : "flex-row items-start justify-center",
271
+ className
272
+ ),
273
+ "data-testid": testId,
274
+ children: [
275
+ /* @__PURE__ */ jsx(
276
+ "div",
277
+ {
278
+ className: "call-tile-box shrink-0",
279
+ style: { width: box.width, height: box.height },
280
+ "data-testid": "call-spotlight",
281
+ children: box.width > 0 && renderTile(focus, "normal")
282
+ },
283
+ getKey(focus)
284
+ ),
285
+ others.length > 0 && /* @__PURE__ */ jsx(
286
+ "div",
287
+ {
288
+ className: cn(
289
+ "grid shrink-0 gap-2",
290
+ narrow ? "w-full grid-cols-3 content-start" : "w-60 grid-cols-1 overflow-y-auto"
291
+ ),
292
+ style: narrow ? { width: box.width } : void 0,
293
+ "data-testid": "call-filmstrip",
294
+ children: others.map((t) => /* @__PURE__ */ jsx("div", { className: "call-tile-box aspect-video w-full min-w-0", children: renderTile(t, "small") }, getKey(t)))
295
+ }
296
+ )
297
+ ]
298
+ }
299
+ );
300
+ }
301
+ function FoldingLabel({
302
+ sentence,
303
+ short,
304
+ expanded
305
+ }) {
306
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
307
+ /* @__PURE__ */ jsx("span", { className: cn(!expanded && "sr-only"), children: sentence }),
308
+ !expanded && /* @__PURE__ */ jsx("span", { "aria-hidden": true, children: short })
309
+ ] });
310
+ }
311
+ function RecordingStatusChip({
312
+ sentence,
313
+ short = "Recording",
314
+ compact = false,
315
+ announce = true,
316
+ onAnnounced,
317
+ className,
318
+ ...rest
319
+ }) {
320
+ const expanded = useBriefNotice(announce, onAnnounced);
321
+ return /* @__PURE__ */ jsx(
322
+ CallStatusChip,
323
+ {
324
+ tone: "red",
325
+ pulse: true,
326
+ role: "status",
327
+ "aria-live": "polite",
328
+ label: /* @__PURE__ */ jsx(FoldingLabel, { sentence, short, expanded }),
329
+ compact: compact && !expanded,
330
+ title: sentence,
331
+ className,
332
+ "data-expanded": expanded,
333
+ ...rest
334
+ }
335
+ );
336
+ }
337
+ function TranscriptionStatusChip({
338
+ sentence,
339
+ announce = true,
340
+ onAnnounced,
341
+ short = "Transcribing",
342
+ detail,
343
+ muted,
344
+ onMutedChange,
345
+ muteLabel,
346
+ unmuteLabel,
347
+ compact = false,
348
+ className,
349
+ ...rest
350
+ }) {
351
+ const expanded = useBriefNotice(announce, onAnnounced);
352
+ const label = muted ? unmuteLabel : muteLabel;
353
+ return /* @__PURE__ */ jsx(
354
+ CallStatusChip,
355
+ {
356
+ tone: "red",
357
+ role: "status",
358
+ "aria-live": "polite",
359
+ icon: /* @__PURE__ */ jsx(Captions, { className: "size-3.5 shrink-0", "aria-hidden": true }),
360
+ label: /* @__PURE__ */ jsx(FoldingLabel, { sentence, short, expanded }),
361
+ compact: compact && !expanded,
362
+ title: detail ? `${sentence}. ${detail}` : sentence,
363
+ className: cn("pr-0.5", className),
364
+ "data-expanded": expanded,
365
+ ...rest,
366
+ children: /* @__PURE__ */ jsxs(Tooltip, { children: [
367
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
368
+ "button",
369
+ {
370
+ type: "button",
371
+ 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",
372
+ "aria-pressed": muted,
373
+ "aria-label": label,
374
+ onClick: () => onMutedChange(!muted),
375
+ "data-testid": "transcription-mute",
376
+ children: muted ? /* @__PURE__ */ jsx(MicOff, { className: "size-3.5", "aria-hidden": true }) : /* @__PURE__ */ jsx(Mic, { className: "size-3.5", "aria-hidden": true })
377
+ }
378
+ ) }),
379
+ /* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: label })
380
+ ] })
381
+ }
382
+ );
383
+ }
384
+ function EncryptionStatusChip({
385
+ state,
386
+ compact = false,
387
+ labels = {
388
+ on: "Encrypted",
389
+ pending: "Encrypting\u2026",
390
+ off: "Not encrypted",
391
+ onTitle: "Media is end-to-end encrypted; the media server only sees ciphertext.",
392
+ offTitle: "This call is not end-to-end encrypted."
393
+ }
394
+ }) {
395
+ const on = state === "on";
396
+ return /* @__PURE__ */ jsx(
397
+ CallStatusChip,
398
+ {
399
+ tone: on ? "neutral" : "yellow",
400
+ 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 }),
401
+ label: on ? labels.on : state === "pending" ? labels.pending : labels.off,
402
+ compact,
403
+ title: on ? labels.onTitle : labels.offTitle,
404
+ "data-testid": "call-encryption-badge",
405
+ "data-state": state
406
+ }
407
+ );
408
+ }
409
+ function CaptionsStrip({
410
+ captions,
411
+ reserve = false,
412
+ waitingText = "Captions appear here as people speak.",
413
+ className
414
+ }) {
415
+ const [now, setNow] = useState(() => Date.now());
416
+ const visible = captions.filter((c) => now - c.receivedAt < CAPTION_TTL_MS).slice(-CAPTION_LINES);
417
+ const showing = visible.length > 0;
418
+ const newest = captions[captions.length - 1]?.receivedAt;
419
+ useEffect(() => {
420
+ setNow(Date.now());
421
+ }, [newest]);
422
+ useEffect(() => {
423
+ if (!showing) return;
424
+ const id = setInterval(() => setNow(Date.now()), 1e3);
425
+ return () => clearInterval(id);
426
+ }, [showing]);
427
+ if (!showing && !reserve) return null;
428
+ return /* @__PURE__ */ jsx(
429
+ "div",
430
+ {
431
+ className: cn(
432
+ "shrink-0 rounded-md bg-muted px-3 py-2 text-sm leading-5 text-foreground",
433
+ className
434
+ ),
435
+ "aria-live": "polite",
436
+ "data-testid": "captions-overlay",
437
+ children: /* @__PURE__ */ jsxs("div", { className: "flex h-10 flex-col justify-end overflow-hidden", children: [
438
+ !showing && /* @__PURE__ */ jsx("p", { className: "truncate text-muted-foreground", "data-testid": "captions-waiting", children: waitingText }),
439
+ visible.map((c) => /* @__PURE__ */ jsxs(
440
+ "p",
441
+ {
442
+ className: "flex min-w-0 shrink-0 gap-1.5",
443
+ "data-testid": "caption-line",
444
+ "data-speaker": c.speakerId,
445
+ children: [
446
+ /* @__PURE__ */ jsx(
447
+ "span",
448
+ {
449
+ className: cn(
450
+ "max-w-[40%] shrink-0 truncate font-semibold",
451
+ speakerAccent(c.speakerId)
452
+ ),
453
+ children: c.name
454
+ }
455
+ ),
456
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: ": " }),
457
+ /* @__PURE__ */ jsx(
458
+ "span",
459
+ {
460
+ className: "flex min-w-0 flex-1 flex-col justify-end overflow-hidden",
461
+ style: { height: `${(visible.length > 1 ? 1 : CAPTION_LINES) * 1.25}rem` },
462
+ children: /* @__PURE__ */ jsx("span", { className: "break-words", children: c.text })
463
+ }
464
+ )
465
+ ]
466
+ },
467
+ `${c.speakerId}:${c.id}`
468
+ ))
469
+ ] })
470
+ }
471
+ );
472
+ }
473
+ function CallControlBar({
474
+ left,
475
+ right,
476
+ children,
477
+ shortcuts = false,
478
+ className
479
+ }) {
480
+ const ref = useRef(null);
481
+ useShortcutKeys(ref, shortcuts);
482
+ return /* @__PURE__ */ jsxs(
483
+ "div",
484
+ {
485
+ ref,
486
+ className: cn(
487
+ "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",
488
+ className
489
+ ),
490
+ "data-testid": "call-controls",
491
+ children: [
492
+ /* @__PURE__ */ jsx("div", { className: "flex min-w-0 items-center gap-2", children: left }),
493
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children }),
494
+ /* @__PURE__ */ jsx("div", { className: "flex items-center justify-end gap-2", children: right })
495
+ ]
496
+ }
497
+ );
498
+ }
499
+ function ControlGroup({ children }) {
500
+ return /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children });
501
+ }
502
+ function ControlDivider() {
503
+ return /* @__PURE__ */ jsx(Separator, { orientation: "vertical", className: "mx-1 h-6" });
504
+ }
505
+ function ElapsedTime({ since, title }) {
506
+ const [now, setNow] = useState(() => Date.now());
507
+ useEffect(() => {
508
+ const id = setInterval(() => setNow(Date.now()), 1e3);
509
+ return () => clearInterval(id);
510
+ }, []);
511
+ return /* @__PURE__ */ jsx("span", { className: "text-sm tabular-nums text-muted-foreground", title, children: formatElapsed(now - since) });
512
+ }
513
+ function MediaControl({
514
+ kind,
515
+ enabled,
516
+ pending = false,
517
+ onToggle,
518
+ devices,
519
+ activeDeviceId,
520
+ onSelectDevice,
521
+ shortcut,
522
+ testId,
523
+ options = []
524
+ }) {
525
+ const mic = kind === "microphone";
526
+ const label = enabled ? `Turn off ${kind}` : `Turn on ${kind}`;
527
+ const Icon = mic ? enabled ? Mic : MicOff : enabled ? Video : VideoOff;
528
+ const title = mic ? "Microphone" : "Camera";
529
+ return /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center", role: "group", "aria-label": title, children: [
530
+ /* @__PURE__ */ jsx(
531
+ CallControlButton,
532
+ {
533
+ label,
534
+ shortcut,
535
+ tone: enabled ? "neutral" : "off",
536
+ "aria-pressed": enabled,
537
+ disabled: pending,
538
+ onClick: onToggle,
539
+ className: "rounded-r-none",
540
+ "data-testid": testId,
541
+ "data-call-shortcut": shortcut?.toLowerCase(),
542
+ "data-enabled": enabled,
543
+ children: /* @__PURE__ */ jsx(Icon, { "aria-hidden": true })
544
+ }
545
+ ),
546
+ /* @__PURE__ */ jsxs(DropdownMenu, { children: [
547
+ /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
548
+ Button,
549
+ {
550
+ type: "button",
551
+ variant: "ghost",
552
+ "aria-label": `Choose ${kind}`,
553
+ title: `Choose ${kind}`,
554
+ className: cn(
555
+ "h-9 w-6 shrink-0 rounded-l-none border-l px-0 pointer-coarse:h-11! pointer-coarse:w-8",
556
+ 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"
557
+ ),
558
+ "data-testid": `${testId}-devices`,
559
+ children: /* @__PURE__ */ jsx(ChevronUp, { className: "size-3.5", "aria-hidden": true })
560
+ }
561
+ ) }),
562
+ /* @__PURE__ */ jsxs(DropdownMenuContent, { side: "top", align: "start", className: "w-72", children: [
563
+ /* @__PURE__ */ jsx(DropdownMenuLabel, { className: "text-xs font-medium text-muted-foreground", children: title }),
564
+ devices.length === 0 ? /* @__PURE__ */ jsxs(DropdownMenuItem, { disabled: true, children: [
565
+ "No ",
566
+ kind,
567
+ " found"
568
+ ] }) : /* @__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)) }),
569
+ options.length > 0 && /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
570
+ options.map((o) => /* @__PURE__ */ jsx(
571
+ DropdownMenuCheckboxItem,
572
+ {
573
+ checked: o.checked,
574
+ onCheckedChange: (c) => o.onChange(c === true),
575
+ "data-testid": o.testId,
576
+ children: o.label
577
+ },
578
+ o.label
579
+ ))
580
+ ] })
581
+ ] })
582
+ ] });
583
+ }
584
+ function RecordControl({
585
+ on,
586
+ busy = false,
587
+ modes,
588
+ note,
589
+ onStart,
590
+ onStop
591
+ }) {
592
+ const [choosing, setChoosing] = useState(false);
593
+ const menuRef = useRef(null);
594
+ useEffect(() => {
595
+ if (!choosing) return;
596
+ const onDown = (e) => {
597
+ if (!menuRef.current?.contains(e.target)) setChoosing(false);
598
+ };
599
+ const onKey = (e) => {
600
+ if (e.key === "Escape") setChoosing(false);
601
+ };
602
+ document.addEventListener("pointerdown", onDown);
603
+ document.addEventListener("keydown", onKey);
604
+ return () => {
605
+ document.removeEventListener("pointerdown", onDown);
606
+ document.removeEventListener("keydown", onKey);
607
+ };
608
+ }, [choosing]);
609
+ const base = "h-9 shrink-0 gap-2 px-3 pointer-coarse:h-11!";
610
+ if (on) {
611
+ return /* @__PURE__ */ jsxs(
612
+ Button,
613
+ {
614
+ type: "button",
615
+ variant: "ghost",
616
+ className: cn(base, "bg-red-bg text-red-text hover:bg-red-bg/80"),
617
+ disabled: busy,
618
+ onClick: onStop,
619
+ "data-testid": "recording-stop",
620
+ children: [
621
+ 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: [
622
+ /* @__PURE__ */ jsx("span", { className: "absolute inline-flex size-full animate-ping rounded-full bg-red-solid opacity-60 motion-reduce:hidden" }),
623
+ /* @__PURE__ */ jsx("span", { className: "relative inline-flex size-2.5 rounded-full bg-red-solid" })
624
+ ] }),
625
+ "Stop recording"
626
+ ]
627
+ }
628
+ );
629
+ }
630
+ return /* @__PURE__ */ jsxs("div", { ref: menuRef, className: "relative", children: [
631
+ /* @__PURE__ */ jsxs(
632
+ Button,
633
+ {
634
+ type: "button",
635
+ variant: "ghost",
636
+ className: cn(base, "bg-muted text-foreground hover:bg-accent"),
637
+ disabled: busy,
638
+ "aria-expanded": choosing,
639
+ "aria-haspopup": "menu",
640
+ onClick: () => setChoosing((c) => !c),
641
+ "data-testid": "recording-start",
642
+ children: [
643
+ 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 }),
644
+ "Record"
645
+ ]
646
+ }
647
+ ),
648
+ choosing && /* @__PURE__ */ jsxs(
649
+ "div",
650
+ {
651
+ role: "menu",
652
+ "aria-label": "Record",
653
+ 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",
654
+ children: [
655
+ note && /* @__PURE__ */ jsx("p", { className: "px-2 py-1.5 text-xs text-muted-foreground", children: note }),
656
+ modes.map((m) => /* @__PURE__ */ jsxs(
657
+ "button",
658
+ {
659
+ type: "button",
660
+ role: "menuitem",
661
+ 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",
662
+ onClick: () => {
663
+ setChoosing(false);
664
+ onStart(m.mode);
665
+ },
666
+ "data-testid": m.testId,
667
+ children: [
668
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: m.label }),
669
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: m.description })
670
+ ]
671
+ },
672
+ m.mode
673
+ ))
674
+ ]
675
+ }
676
+ )
677
+ ] });
678
+ }
679
+ function LeaveButton({
680
+ onLeave,
681
+ compact = false,
682
+ label = "Leave",
683
+ title = "Leave call"
684
+ }) {
685
+ return /* @__PURE__ */ jsxs(
686
+ Button,
687
+ {
688
+ type: "button",
689
+ variant: "destructive",
690
+ size: compact ? "icon" : "default",
691
+ className: cn("shrink-0", !compact && "px-4"),
692
+ "aria-label": title,
693
+ title,
694
+ onClick: onLeave,
695
+ "data-testid": "call-leave",
696
+ children: [
697
+ /* @__PURE__ */ jsx(PhoneOff, { className: "size-4", "aria-hidden": true }),
698
+ !compact && label
699
+ ]
700
+ }
701
+ );
702
+ }
703
+ function MoreMenu({
704
+ details,
705
+ groups,
706
+ label = "More options"
707
+ }) {
708
+ const shown = groups.filter((g) => g.length > 0);
709
+ return /* @__PURE__ */ jsxs(DropdownMenu, { children: [
710
+ /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(CallControlButton, { label, tooltip: false, "data-testid": "call-more", children: /* @__PURE__ */ jsx(MoreHorizontal, { "aria-hidden": true }) }) }),
711
+ /* @__PURE__ */ jsxs(DropdownMenuContent, { side: "top", align: "end", className: "w-72", "data-testid": "call-more-menu", children: [
712
+ details && /* @__PURE__ */ jsxs(Fragment, { children: [
713
+ /* @__PURE__ */ jsx(DropdownMenuLabel, { className: "flex flex-col gap-1 font-normal", children: details }),
714
+ shown.length > 0 && /* @__PURE__ */ jsx(DropdownMenuSeparator, {})
715
+ ] }),
716
+ shown.map((group, i) => /* @__PURE__ */ jsxs("div", { role: "none", children: [
717
+ i > 0 && /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
718
+ group.map((item) => /* @__PURE__ */ jsxs(
719
+ DropdownMenuItem,
720
+ {
721
+ onSelect: item.onSelect,
722
+ disabled: item.disabled,
723
+ "data-testid": item.testId,
724
+ children: [
725
+ item.icon,
726
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: item.label }),
727
+ item.trailing
728
+ ]
729
+ },
730
+ item.key
731
+ ))
732
+ ] }, group[0].key))
733
+ ] })
734
+ ] });
735
+ }
736
+ function HeaderIconButton({
737
+ label,
738
+ onClick,
739
+ children,
740
+ testId,
741
+ pressed
742
+ }) {
743
+ return /* @__PURE__ */ jsxs(Tooltip, { children: [
744
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
745
+ Button,
746
+ {
747
+ type: "button",
748
+ variant: "ghost",
749
+ size: "icon",
750
+ className: "text-muted-foreground hover:text-foreground",
751
+ "aria-label": label,
752
+ "aria-pressed": pressed,
753
+ onClick,
754
+ "data-testid": testId,
755
+ children
756
+ }
757
+ ) }),
758
+ /* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: label })
759
+ ] });
760
+ }
761
+ function CallHeader({
762
+ title,
763
+ icon,
764
+ status,
765
+ view,
766
+ onViewChange,
767
+ onMinimise,
768
+ minimiseLabel = "Minimise \u2014 keep working while the call goes on"
769
+ }) {
770
+ return /* @__PURE__ */ jsx("div", { className: "@container", children: /* @__PURE__ */ jsx(
771
+ TabHeader,
772
+ {
773
+ tabs: [
774
+ {
775
+ id: "call",
776
+ label: /* @__PURE__ */ jsxs(Tooltip, { children: [
777
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, showWithText: true, children: /* @__PURE__ */ jsx(
778
+ "span",
779
+ {
780
+ className: "block min-w-0 max-w-[calc(100cqw-16rem)] truncate",
781
+ "data-testid": "call-title",
782
+ children: title
783
+ }
784
+ ) }),
785
+ /* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: title })
786
+ ] }),
787
+ icon: icon ?? /* @__PURE__ */ jsx(Phone, { className: "size-4" })
788
+ }
789
+ ],
790
+ activeTabId: "call",
791
+ onTabChange: () => void 0,
792
+ testId: "call-header",
793
+ className: "pb-0",
794
+ rightContent: (compact) => /* @__PURE__ */ jsxs(Fragment, { children: [
795
+ typeof status === "function" ? status(compact) : status,
796
+ view && onViewChange && /* @__PURE__ */ jsx(
797
+ HeaderIconButton,
798
+ {
799
+ label: view === "speaker" ? "Gallery view (everyone equal)" : "Speaker view (who talks, large)",
800
+ onClick: () => onViewChange(view === "speaker" ? "gallery" : "speaker"),
801
+ testId: "call-view-toggle",
802
+ children: view === "speaker" ? /* @__PURE__ */ jsx(LayoutGrid, { className: "size-4" }) : /* @__PURE__ */ jsx(UserRound, { className: "size-4" })
803
+ }
804
+ ),
805
+ onMinimise && /* @__PURE__ */ jsx(
806
+ HeaderIconButton,
807
+ {
808
+ label: minimiseLabel,
809
+ onClick: onMinimise,
810
+ testId: "call-dock-toggle",
811
+ children: /* @__PURE__ */ jsx(PictureInPicture2, { className: "size-4" })
812
+ }
813
+ )
814
+ ] })
815
+ }
816
+ ) });
817
+ }
818
+ function SpeakingNow({
819
+ names,
820
+ idleText = "No one is speaking",
821
+ className
822
+ }) {
823
+ const talking = names.length > 0;
824
+ return /* @__PURE__ */ jsxs(
825
+ "p",
826
+ {
827
+ className: cn(
828
+ "flex min-w-0 items-center gap-1.5 text-sm",
829
+ talking ? "text-foreground" : "text-muted-foreground",
830
+ className
831
+ ),
832
+ "aria-live": "polite",
833
+ "data-testid": "call-speaking-now",
834
+ children: [
835
+ /* @__PURE__ */ jsx(
836
+ AudioLines,
837
+ {
838
+ className: cn(
839
+ "size-4 shrink-0",
840
+ talking ? "text-theme-cyan motion-safe:animate-pulse" : "text-muted-foreground"
841
+ ),
842
+ "aria-hidden": true
843
+ }
844
+ ),
845
+ talking ? /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-center gap-1 overflow-hidden", children: [
846
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 text-muted-foreground", children: "Speaking:" }),
847
+ names.map((n, i) => /* @__PURE__ */ jsx("span", { className: "flex min-w-0 shrink-0 items-center", children: n }, i))
848
+ ] }) : /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: idleText })
849
+ ]
850
+ }
851
+ );
852
+ }
853
+ function CallNotice({ children }) {
854
+ return /* @__PURE__ */ jsxs(
855
+ "p",
856
+ {
857
+ className: "flex min-w-0 items-center gap-1.5 text-sm text-red-text motion-safe:animate-in motion-safe:fade-in-0",
858
+ role: "status",
859
+ "aria-live": "polite",
860
+ "data-testid": "call-notice",
861
+ children: [
862
+ /* @__PURE__ */ jsx("span", { className: "size-2 shrink-0 rounded-full bg-red-solid", "aria-hidden": true }),
863
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children })
864
+ ]
865
+ }
866
+ );
867
+ }
868
+ function CallStageFrame({
869
+ header,
870
+ subheader,
871
+ stage,
872
+ stageRef,
873
+ stageOverlay,
874
+ people,
875
+ stageHeight,
876
+ controls,
877
+ className
878
+ }) {
879
+ const [bodyRef, body] = useElementSize();
880
+ const PAD_X = 24;
881
+ const PAD_B = 12;
882
+ 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;
883
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex size-full min-h-0 flex-col bg-background text-foreground", className), children: [
884
+ header,
885
+ subheader && /* @__PURE__ */ jsx("div", { className: "flex min-h-9 shrink-0 flex-col justify-center gap-0.5 px-3 py-1", children: subheader }),
886
+ /* @__PURE__ */ jsxs("div", { ref: bodyRef, className: "relative flex min-h-0 flex-1 flex-col", children: [
887
+ /* @__PURE__ */ jsx(
888
+ "div",
889
+ {
890
+ className: cn("flex min-h-0 flex-col px-3 pb-3", sized == null ? "flex-1" : "shrink-0"),
891
+ style: sized == null ? void 0 : { height: sized },
892
+ children: /* @__PURE__ */ jsxs("div", { ref: stageRef, className: "relative min-h-0 flex-1", "data-testid": "call-stage", children: [
893
+ stage,
894
+ stageOverlay
895
+ ] })
896
+ }
897
+ ),
898
+ people
899
+ ] }),
900
+ controls
901
+ ] });
902
+ }
903
+ function CallSidePanel({
904
+ title,
905
+ icon,
906
+ actions,
907
+ onClose,
908
+ closeLabel,
909
+ closeTestId,
910
+ children,
911
+ className,
912
+ ...rest
913
+ }) {
914
+ return /* @__PURE__ */ jsxs(
915
+ "aside",
916
+ {
917
+ className: cn("flex min-h-0 flex-col bg-background text-foreground", className),
918
+ "aria-label": title,
919
+ ...rest,
920
+ children: [
921
+ /* @__PURE__ */ jsxs("div", { className: "flex h-12 shrink-0 items-center gap-2 border-b border-border pl-3 pr-1.5", children: [
922
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 items-center gap-2 text-base font-semibold text-primary", children: [
923
+ icon,
924
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: title })
925
+ ] }),
926
+ actions,
927
+ /* @__PURE__ */ jsx(
928
+ Button,
929
+ {
930
+ type: "button",
931
+ variant: "ghost",
932
+ size: "icon",
933
+ className: "size-8 text-muted-foreground hover:text-foreground",
934
+ "aria-label": closeLabel,
935
+ title: closeLabel,
936
+ onClick: onClose,
937
+ "data-testid": closeTestId,
938
+ children: /* @__PURE__ */ jsx(X, { className: "size-4" })
939
+ }
940
+ )
941
+ ] }),
942
+ children
943
+ ]
944
+ }
945
+ );
946
+ }
947
+ function VoiceMeter({
948
+ level,
949
+ muted = false,
950
+ className
951
+ }) {
952
+ if (muted) {
953
+ 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" }) });
954
+ }
955
+ const lit = Math.round(Math.min(1, Math.max(0, level)) * 5);
956
+ return /* @__PURE__ */ jsx(
957
+ "span",
958
+ {
959
+ className: cn("flex h-4 items-end gap-0.5", className),
960
+ role: "meter",
961
+ "aria-label": "Voice level",
962
+ "aria-valuemin": 0,
963
+ "aria-valuemax": 5,
964
+ "aria-valuenow": lit,
965
+ children: [0.35, 0.55, 0.75, 0.9, 1].map((h, i) => /* @__PURE__ */ jsx(
966
+ "span",
967
+ {
968
+ className: cn("w-1 rounded-sm", i < lit ? "bg-theme-cyan" : "bg-muted-foreground/30"),
969
+ style: { height: `${h * 100}%` }
970
+ },
971
+ i
972
+ ))
973
+ }
974
+ );
975
+ }
976
+ function PersonRow({
977
+ p,
978
+ canRemove,
979
+ removingId,
980
+ onRemove,
981
+ onAskToMute,
982
+ onMute
983
+ }) {
984
+ const hasActions = !p.isLocal && (canRemove || (onAskToMute || onMute) && !p.micMuted || onMute && p.cameraOn);
985
+ return /* @__PURE__ */ jsxs(
986
+ "li",
987
+ {
988
+ className: "flex h-10 items-center gap-2 rounded-md px-2 hover:bg-muted",
989
+ "data-testid": `call-person-${p.id}`,
990
+ "data-speaking": p.speaking || void 0,
991
+ children: [
992
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 items-center gap-1.5 text-sm", children: [
993
+ p.entity ? (
994
+ // The ring hugs the person's pill, not the whole row.
995
+ /* @__PURE__ */ jsx(
996
+ "span",
997
+ {
998
+ className: cn(
999
+ "inline-flex min-w-0 rounded-full",
1000
+ p.speaking && "ring-2 ring-theme-cyan ring-offset-2 ring-offset-background"
1001
+ ),
1002
+ children: p.entity
1003
+ }
1004
+ )
1005
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
1006
+ /* @__PURE__ */ jsx(
1007
+ "span",
1008
+ {
1009
+ className: "flex size-7 shrink-0 items-center justify-center rounded-full bg-primary/10 text-xs font-semibold text-primary",
1010
+ "aria-hidden": true,
1011
+ children: initialsOf(p.name)
1012
+ }
1013
+ ),
1014
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 truncate", children: [
1015
+ p.name,
1016
+ p.isLocal && /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: " (me)" })
1017
+ ] })
1018
+ ] }),
1019
+ p.speaking && /* @__PURE__ */ jsx("span", { className: "sr-only", children: " \u2014 speaking" }),
1020
+ p.note && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: p.note })
1021
+ ] }),
1022
+ /* @__PURE__ */ jsx(VoiceMeter, { level: p.level ?? 0, muted: p.micMuted }),
1023
+ /* @__PURE__ */ jsx(
1024
+ "span",
1025
+ {
1026
+ className: cn(
1027
+ "flex w-4 justify-center",
1028
+ p.cameraOn ? "text-muted-foreground" : "text-red-text"
1029
+ ),
1030
+ title: p.cameraOn ? "Camera on" : "Camera off",
1031
+ children: p.cameraOn ? /* @__PURE__ */ jsx(Video, { className: "size-4", "aria-label": "Camera on" }) : /* @__PURE__ */ jsx(VideoOff, { className: "size-4", "aria-label": "Camera off" })
1032
+ }
1033
+ ),
1034
+ hasActions ? /* @__PURE__ */ jsxs(DropdownMenu, { children: [
1035
+ /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
1036
+ Button,
1037
+ {
1038
+ type: "button",
1039
+ variant: "ghost",
1040
+ size: "icon",
1041
+ className: "size-8 text-muted-foreground hover:text-foreground",
1042
+ "aria-label": `Actions for ${p.name}`,
1043
+ "data-testid": `call-person-menu-${p.id}`,
1044
+ children: /* @__PURE__ */ jsx(MoreHorizontal, { className: "size-4" })
1045
+ }
1046
+ ) }),
1047
+ /* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", className: "w-56", children: [
1048
+ onAskToMute && !p.micMuted && /* @__PURE__ */ jsxs(
1049
+ DropdownMenuItem,
1050
+ {
1051
+ onSelect: () => onAskToMute(p),
1052
+ "data-testid": `call-ask-mute-${p.id}`,
1053
+ children: [
1054
+ /* @__PURE__ */ jsx(MicOff, { "aria-hidden": true }),
1055
+ "Ask to mute"
1056
+ ]
1057
+ }
1058
+ ),
1059
+ onMute && !p.micMuted && /* @__PURE__ */ jsxs(
1060
+ DropdownMenuItem,
1061
+ {
1062
+ onSelect: () => onMute(p, "audio"),
1063
+ "data-testid": `call-mute-${p.id}`,
1064
+ children: [
1065
+ /* @__PURE__ */ jsx(MicOff, { "aria-hidden": true }),
1066
+ "Mute"
1067
+ ]
1068
+ }
1069
+ ),
1070
+ onMute && p.cameraOn && /* @__PURE__ */ jsxs(
1071
+ DropdownMenuItem,
1072
+ {
1073
+ onSelect: () => onMute(p, "video"),
1074
+ "data-testid": `call-camera-off-${p.id}`,
1075
+ children: [
1076
+ /* @__PURE__ */ jsx(VideoOff, { "aria-hidden": true }),
1077
+ "Turn off camera"
1078
+ ]
1079
+ }
1080
+ ),
1081
+ canRemove && /* @__PURE__ */ jsxs(
1082
+ DropdownMenuItem,
1083
+ {
1084
+ onSelect: () => onRemove(p),
1085
+ disabled: removingId === p.id,
1086
+ className: "text-destructive focus:text-destructive",
1087
+ "data-testid": `call-remove-${p.id}`,
1088
+ children: [
1089
+ /* @__PURE__ */ jsx(UserX, { "aria-hidden": true }),
1090
+ "Remove from call"
1091
+ ]
1092
+ }
1093
+ )
1094
+ ] })
1095
+ ] }) : (
1096
+ // Keeps every row's meter and icons in the same column.
1097
+ /* @__PURE__ */ jsx("span", { className: "size-8 shrink-0", "aria-hidden": true })
1098
+ )
1099
+ ]
1100
+ }
1101
+ );
1102
+ }
1103
+ function CallParticipants({
1104
+ people,
1105
+ canRemove,
1106
+ removingId,
1107
+ onRemove,
1108
+ onAskToMute,
1109
+ onMute,
1110
+ compact = false,
1111
+ className
1112
+ }) {
1113
+ const [expanded, setExpanded] = useState(false);
1114
+ 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(
1115
+ PersonRow,
1116
+ {
1117
+ p,
1118
+ canRemove,
1119
+ removingId,
1120
+ onRemove,
1121
+ onAskToMute,
1122
+ onMute
1123
+ },
1124
+ p.id
1125
+ )) });
1126
+ const heading = /* @__PURE__ */ jsxs("span", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: [
1127
+ "People (",
1128
+ people.length,
1129
+ ")"
1130
+ ] });
1131
+ if (compact && !expanded) {
1132
+ const talking = people.filter((p) => p.speaking);
1133
+ return /* @__PURE__ */ jsxs(
1134
+ "section",
1135
+ {
1136
+ className: cn(
1137
+ "flex h-11 shrink-0 items-center gap-2 border-t border-border px-3",
1138
+ className
1139
+ ),
1140
+ "aria-label": "People",
1141
+ "data-testid": "call-people",
1142
+ "data-compact": "",
1143
+ children: [
1144
+ heading,
1145
+ /* @__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)) }),
1146
+ /* @__PURE__ */ jsxs(
1147
+ Button,
1148
+ {
1149
+ type: "button",
1150
+ variant: "ghost",
1151
+ size: "sm",
1152
+ className: "h-8 shrink-0",
1153
+ "aria-expanded": false,
1154
+ onClick: () => setExpanded(true),
1155
+ "data-testid": "call-people-show-all",
1156
+ children: [
1157
+ "Show all (",
1158
+ people.length,
1159
+ ")"
1160
+ ]
1161
+ }
1162
+ )
1163
+ ]
1164
+ }
1165
+ );
1166
+ }
1167
+ return /* @__PURE__ */ jsxs(
1168
+ "section",
1169
+ {
1170
+ className: cn(
1171
+ "flex min-h-0 flex-col border-t border-border",
1172
+ compact && "absolute inset-x-0 bottom-0 z-20 max-h-[80%] bg-background shadow-lg",
1173
+ className
1174
+ ),
1175
+ "aria-label": "People",
1176
+ "data-testid": "call-people",
1177
+ children: [
1178
+ /* @__PURE__ */ jsxs("div", { className: "flex h-9 shrink-0 items-center justify-between px-3", children: [
1179
+ heading,
1180
+ compact && /* @__PURE__ */ jsx(
1181
+ Button,
1182
+ {
1183
+ type: "button",
1184
+ variant: "ghost",
1185
+ size: "sm",
1186
+ className: "h-7",
1187
+ "aria-expanded": true,
1188
+ onClick: () => setExpanded(false),
1189
+ "data-testid": "call-people-show-less",
1190
+ children: "Show less"
1191
+ }
1192
+ )
1193
+ ] }),
1194
+ rows
1195
+ ]
1196
+ }
1197
+ );
1198
+ }
1199
+ function LobbyRequests({
1200
+ requests,
1201
+ onDecide
1202
+ }) {
1203
+ if (!requests.length) return null;
1204
+ return /* @__PURE__ */ jsx(
1205
+ "div",
1206
+ {
1207
+ className: "absolute left-3 top-3 z-10 flex w-72 max-w-[calc(100%-1.5rem)] flex-col gap-2",
1208
+ role: "region",
1209
+ "aria-label": "Lobby",
1210
+ "data-testid": "call-lobby",
1211
+ children: requests.map((k) => /* @__PURE__ */ jsxs(
1212
+ "div",
1213
+ {
1214
+ className: "flex items-center gap-3 rounded-md border border-border bg-popover p-3 text-sm text-popover-foreground shadow-lg",
1215
+ "data-testid": "call-lobby-knock",
1216
+ children: [
1217
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-wrap items-center gap-1", children: [
1218
+ k.entity ?? /* @__PURE__ */ jsx("span", { className: "font-medium", children: k.name }),
1219
+ /* @__PURE__ */ jsx("span", { children: "wants to join." })
1220
+ ] }),
1221
+ /* @__PURE__ */ jsx(
1222
+ Button,
1223
+ {
1224
+ type: "button",
1225
+ variant: "ghost",
1226
+ size: "sm",
1227
+ className: "h-8",
1228
+ onClick: () => onDecide(k.id, false),
1229
+ "data-testid": "call-lobby-deny",
1230
+ children: "Deny"
1231
+ }
1232
+ ),
1233
+ /* @__PURE__ */ jsx(
1234
+ Button,
1235
+ {
1236
+ type: "button",
1237
+ size: "sm",
1238
+ className: "h-8",
1239
+ onClick: () => onDecide(k.id, true),
1240
+ "data-testid": "call-lobby-admit",
1241
+ children: "Admit"
1242
+ }
1243
+ )
1244
+ ]
1245
+ },
1246
+ k.id
1247
+ ))
1248
+ }
1249
+ );
1250
+ }
1251
+ function MinimisedCall({
1252
+ title,
1253
+ video,
1254
+ status,
1255
+ speaking,
1256
+ micEnabled,
1257
+ onToggleMic,
1258
+ cameraEnabled,
1259
+ onToggleCamera,
1260
+ onRestore,
1261
+ onLeave,
1262
+ restoreLabel = "Back to the call",
1263
+ shortcuts = true,
1264
+ className
1265
+ }) {
1266
+ const ref = useRef(null);
1267
+ useShortcutKeys(ref, shortcuts);
1268
+ return /* @__PURE__ */ jsxs("div", { ref, className: cn("flex flex-col", className), "data-testid": "call-minimised", children: [
1269
+ /* @__PURE__ */ jsx(
1270
+ "button",
1271
+ {
1272
+ type: "button",
1273
+ 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",
1274
+ onClick: onRestore,
1275
+ "aria-label": restoreLabel,
1276
+ "data-testid": "call-minimised-restore",
1277
+ 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 }) }) }) }) })
1278
+ }
1279
+ ),
1280
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 px-3 py-2", children: [
1281
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [
1282
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-sm font-medium", title, children: title }),
1283
+ status && /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center gap-1", children: status })
1284
+ ] }),
1285
+ speaking,
1286
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1287
+ onToggleMic && micEnabled !== void 0 && /* @__PURE__ */ jsx(
1288
+ CallControlButton,
1289
+ {
1290
+ label: micEnabled ? "Turn off microphone" : "Turn on microphone",
1291
+ shortcut: "M",
1292
+ tone: micEnabled ? "neutral" : "off",
1293
+ "aria-pressed": micEnabled,
1294
+ onClick: onToggleMic,
1295
+ className: "size-8",
1296
+ "data-testid": "call-minimised-mic",
1297
+ "data-call-shortcut": "m",
1298
+ children: micEnabled ? /* @__PURE__ */ jsx(Mic, { "aria-hidden": true }) : /* @__PURE__ */ jsx(MicOff, { "aria-hidden": true })
1299
+ }
1300
+ ),
1301
+ onToggleCamera && cameraEnabled !== void 0 && /* @__PURE__ */ jsx(
1302
+ CallControlButton,
1303
+ {
1304
+ label: cameraEnabled ? "Turn off camera" : "Turn on camera",
1305
+ shortcut: "V",
1306
+ tone: cameraEnabled ? "neutral" : "off",
1307
+ "aria-pressed": cameraEnabled,
1308
+ onClick: onToggleCamera,
1309
+ className: "size-8",
1310
+ "data-testid": "call-minimised-camera",
1311
+ "data-call-shortcut": "v",
1312
+ children: cameraEnabled ? /* @__PURE__ */ jsx(Video, { "aria-hidden": true }) : /* @__PURE__ */ jsx(VideoOff, { "aria-hidden": true })
1313
+ }
1314
+ ),
1315
+ /* @__PURE__ */ jsx("span", { className: "flex-1" }),
1316
+ /* @__PURE__ */ jsx(
1317
+ CallControlButton,
1318
+ {
1319
+ label: restoreLabel,
1320
+ onClick: onRestore,
1321
+ className: "size-8",
1322
+ "data-testid": "call-dock-expand",
1323
+ children: /* @__PURE__ */ jsx(Maximize2, { "aria-hidden": true })
1324
+ }
1325
+ ),
1326
+ /* @__PURE__ */ jsx(
1327
+ CallControlButton,
1328
+ {
1329
+ label: "Leave call",
1330
+ tone: "danger",
1331
+ onClick: onLeave,
1332
+ className: "size-8",
1333
+ "data-testid": "call-dock-leave",
1334
+ children: /* @__PURE__ */ jsx(PhoneOff, { "aria-hidden": true })
1335
+ }
1336
+ )
1337
+ ] })
1338
+ ] })
1339
+ ] });
1340
+ }
1341
+ function IncomingCallCard({
1342
+ callerName,
1343
+ caller,
1344
+ context,
1345
+ busy = false,
1346
+ muted,
1347
+ onMutedChange,
1348
+ onAccept,
1349
+ onDecline,
1350
+ className
1351
+ }) {
1352
+ return /* @__PURE__ */ jsxs(
1353
+ "div",
1354
+ {
1355
+ role: "alertdialog",
1356
+ "aria-labelledby": "incoming-call-title",
1357
+ "aria-describedby": "incoming-call-desc",
1358
+ "data-testid": "incoming-call-dialog",
1359
+ className: cn(
1360
+ "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",
1361
+ className
1362
+ ),
1363
+ children: [
1364
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
1365
+ /* @__PURE__ */ jsxs("span", { className: "relative flex size-10 shrink-0", "aria-hidden": true, children: [
1366
+ /* @__PURE__ */ jsx("span", { className: "absolute inset-0 rounded-full bg-primary/20 motion-safe:animate-ping" }),
1367
+ /* @__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" }) })
1368
+ ] }),
1369
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 pt-0.5", children: [
1370
+ /* @__PURE__ */ jsxs("div", { id: "incoming-call-title", className: "flex min-w-0 text-sm font-semibold", children: [
1371
+ caller ?? /* @__PURE__ */ jsx("span", { className: "truncate", children: callerName }),
1372
+ caller && /* @__PURE__ */ jsx("span", { className: "sr-only", children: callerName })
1373
+ ] }),
1374
+ /* @__PURE__ */ jsx("p", { id: "incoming-call-desc", className: "text-xs text-muted-foreground", children: context })
1375
+ ] }),
1376
+ /* @__PURE__ */ jsx(
1377
+ Button,
1378
+ {
1379
+ type: "button",
1380
+ variant: "ghost",
1381
+ size: "icon",
1382
+ className: "-mr-1 -mt-1 size-8 shrink-0 text-muted-foreground hover:text-foreground",
1383
+ "aria-pressed": muted,
1384
+ "aria-label": muted ? "Unmute ringtone" : "Mute ringtone",
1385
+ title: muted ? "Unmute ringtone" : "Mute ringtone",
1386
+ onClick: () => onMutedChange(!muted),
1387
+ "data-testid": "incoming-call-mute",
1388
+ children: muted ? /* @__PURE__ */ jsx(BellOff, { className: "size-4" }) : /* @__PURE__ */ jsx(Bell, { className: "size-4" })
1389
+ }
1390
+ )
1391
+ ] }),
1392
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 grid grid-cols-2 gap-2", children: [
1393
+ /* @__PURE__ */ jsxs(
1394
+ Button,
1395
+ {
1396
+ type: "button",
1397
+ variant: "destructive",
1398
+ onClick: onDecline,
1399
+ disabled: busy,
1400
+ "data-testid": "incoming-call-decline",
1401
+ children: [
1402
+ /* @__PURE__ */ jsx(PhoneOff, { className: "size-4", "aria-hidden": true }),
1403
+ "Decline"
1404
+ ]
1405
+ }
1406
+ ),
1407
+ /* @__PURE__ */ jsxs(
1408
+ Button,
1409
+ {
1410
+ type: "button",
1411
+ onClick: onAccept,
1412
+ disabled: busy,
1413
+ "data-testid": "incoming-call-accept",
1414
+ autoFocus: true,
1415
+ children: [
1416
+ busy ? /* @__PURE__ */ jsx(Loader2, { className: "size-4 animate-spin", "aria-hidden": true }) : /* @__PURE__ */ jsx(Phone, { className: "size-4", "aria-hidden": true }),
1417
+ "Accept"
1418
+ ]
1419
+ }
1420
+ )
1421
+ ] })
1422
+ ]
1423
+ }
1424
+ );
1425
+ }
1426
+ function CallStateMessage({
1427
+ title,
1428
+ description,
1429
+ spinner = true,
1430
+ action,
1431
+ testId
1432
+ }) {
1433
+ return /* @__PURE__ */ jsxs(
1434
+ "div",
1435
+ {
1436
+ className: "flex size-full flex-col items-center justify-center gap-3 p-6 text-center",
1437
+ "data-testid": testId,
1438
+ children: [
1439
+ spinner && /* @__PURE__ */ jsx(Loader2, { className: "size-6 animate-spin text-muted-foreground", "aria-hidden": true }),
1440
+ /* @__PURE__ */ jsx("p", { className: "text-base font-semibold", children: title }),
1441
+ description && /* @__PURE__ */ jsx("p", { className: "max-w-sm text-sm text-muted-foreground", children: description }),
1442
+ action
1443
+ ]
1444
+ }
1445
+ );
1446
+ }
1447
+ function ConnectionOverlay({ label }) {
1448
+ 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: [
1449
+ /* @__PURE__ */ jsx(Loader2, { className: "mr-2 size-4 animate-spin", "aria-hidden": true }),
1450
+ label
1451
+ ] });
1452
+ }
1453
+
1454
+ // src/calls/join-choices.ts
1455
+ var DEFAULT_JOIN_CHOICES = { audioEnabled: true, videoEnabled: false };
1456
+ function describeMediaError(err, kind) {
1457
+ const Kind = kind === "camera" ? "Camera" : "Microphone";
1458
+ const name = err?.name;
1459
+ if (name === "NotAllowedError" || name === "SecurityError")
1460
+ return `${Kind} access is blocked. Allow it in the browser's site settings and try again.`;
1461
+ if (name === "NotFoundError" || name === "OverconstrainedError") return `No ${kind} was found.`;
1462
+ if (name === "NotReadableError") return `${Kind} is in use by another app.`;
1463
+ return `${Kind} could not start${err?.message ? `: ${err.message}` : "."}`;
1464
+ }
1465
+ function useDevices(kind, refreshKey) {
1466
+ const [devices, setDevices] = useState([]);
1467
+ useEffect(() => {
1468
+ const md = navigator.mediaDevices;
1469
+ if (!md?.enumerateDevices) return;
1470
+ let cancelled = false;
1471
+ const load = () => md.enumerateDevices().then((all) => {
1472
+ if (!cancelled) setDevices(all.filter((d) => d.kind === kind && d.deviceId));
1473
+ }).catch(() => {
1474
+ });
1475
+ void load();
1476
+ md.addEventListener?.("devicechange", load);
1477
+ return () => {
1478
+ cancelled = true;
1479
+ md.removeEventListener?.("devicechange", load);
1480
+ };
1481
+ }, [kind, refreshKey]);
1482
+ return devices;
1483
+ }
1484
+ function usePreviewStream(kind, enabled, deviceId, describe) {
1485
+ const [stream, setStream] = useState(null);
1486
+ const [error, setError] = useState(null);
1487
+ const describeRef = useRef(describe);
1488
+ describeRef.current = describe;
1489
+ useEffect(() => {
1490
+ if (!enabled) {
1491
+ setStream(null);
1492
+ return;
1493
+ }
1494
+ const md = navigator.mediaDevices;
1495
+ if (!md?.getUserMedia) {
1496
+ setError(
1497
+ window.isSecureContext ? "This browser cannot access the camera or microphone." : "Camera and microphone need HTTPS (or localhost)."
1498
+ );
1499
+ return;
1500
+ }
1501
+ let cancelled = false;
1502
+ let opened = null;
1503
+ const constraint = deviceId ? { deviceId: { exact: deviceId } } : true;
1504
+ md.getUserMedia(kind === "videoinput" ? { video: constraint } : { audio: constraint }).then((s) => {
1505
+ opened = s;
1506
+ if (cancelled) s.getTracks().forEach((t) => t.stop());
1507
+ else {
1508
+ setError(null);
1509
+ setStream(s);
1510
+ }
1511
+ }).catch((err) => {
1512
+ if (!cancelled)
1513
+ setError(describeRef.current(err, kind === "videoinput" ? "camera" : "microphone"));
1514
+ });
1515
+ return () => {
1516
+ cancelled = true;
1517
+ opened?.getTracks().forEach((t) => t.stop());
1518
+ setStream(null);
1519
+ };
1520
+ }, [kind, enabled, deviceId]);
1521
+ return { stream, error };
1522
+ }
1523
+ function useAudioLevel(stream) {
1524
+ const [level, setLevel] = useState(0);
1525
+ useEffect(() => {
1526
+ if (!stream || typeof AudioContext === "undefined") {
1527
+ setLevel(0);
1528
+ return;
1529
+ }
1530
+ let ctx;
1531
+ try {
1532
+ ctx = new AudioContext();
1533
+ } catch {
1534
+ return;
1535
+ }
1536
+ const analyser = ctx.createAnalyser();
1537
+ analyser.fftSize = 512;
1538
+ ctx.createMediaStreamSource(stream).connect(analyser);
1539
+ const buf = new Uint8Array(analyser.fftSize);
1540
+ let frame = 0;
1541
+ const tick = () => {
1542
+ analyser.getByteTimeDomainData(buf);
1543
+ let peak = 0;
1544
+ for (const v of buf) peak = Math.max(peak, Math.abs(v - 128));
1545
+ setLevel(Math.min(1, peak / 64));
1546
+ frame = requestAnimationFrame(tick);
1547
+ };
1548
+ void ctx.resume().catch(() => {
1549
+ });
1550
+ tick();
1551
+ return () => {
1552
+ cancelAnimationFrame(frame);
1553
+ void ctx.close().catch(() => {
1554
+ });
1555
+ };
1556
+ }, [stream]);
1557
+ return level;
1558
+ }
1559
+ function DeviceSelect({
1560
+ devices,
1561
+ value,
1562
+ onChange,
1563
+ label,
1564
+ icon,
1565
+ disabled,
1566
+ testId
1567
+ }) {
1568
+ const current = value ?? devices[0]?.deviceId;
1569
+ return /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col gap-1.5", children: [
1570
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5 text-xs font-medium text-muted-foreground [&_svg]:size-3.5", children: [
1571
+ icon,
1572
+ label
1573
+ ] }),
1574
+ /* @__PURE__ */ jsxs(Select, { value: current, onValueChange: onChange, disabled: disabled || devices.length < 2, children: [
1575
+ /* @__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()}` }) }),
1576
+ /* @__PURE__ */ jsx(SelectContent, { children: devices.map((d, i) => /* @__PURE__ */ jsx(SelectItem, { value: d.deviceId, children: d.label || `${label} ${i + 1}` }, d.deviceId)) })
1577
+ ] })
1578
+ ] });
1579
+ }
1580
+ function PreJoin({
1581
+ title,
1582
+ heading = "Ready to join?",
1583
+ children,
1584
+ joinLabel = "Join",
1585
+ busy = false,
1586
+ error,
1587
+ onJoin,
1588
+ onCancel,
1589
+ describeError = describeMediaError,
1590
+ mirror = true,
1591
+ className
1592
+ }) {
1593
+ const [choices, setChoices] = useState(DEFAULT_JOIN_CHOICES);
1594
+ const video = usePreviewStream(
1595
+ "videoinput",
1596
+ choices.videoEnabled,
1597
+ choices.videoDeviceId,
1598
+ describeError
1599
+ );
1600
+ const audio = usePreviewStream(
1601
+ "audioinput",
1602
+ choices.audioEnabled,
1603
+ choices.audioDeviceId,
1604
+ describeError
1605
+ );
1606
+ const level = useAudioLevel(audio.stream);
1607
+ const cameras = useDevices("videoinput", video.stream);
1608
+ const mics = useDevices("audioinput", audio.stream);
1609
+ const videoRef = useRef(null);
1610
+ useEffect(() => {
1611
+ if (videoRef.current) videoRef.current.srcObject = video.stream;
1612
+ }, [video.stream]);
1613
+ const set = (patch) => setChoices((c) => ({ ...c, ...patch }));
1614
+ const deviceError = video.error ?? audio.error;
1615
+ 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: [
1616
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
1617
+ /* @__PURE__ */ jsx("div", { className: "relative aspect-video w-full overflow-hidden rounded-md bg-hero", children: choices.videoEnabled && video.stream ? /* @__PURE__ */ jsx(
1618
+ "video",
1619
+ {
1620
+ ref: videoRef,
1621
+ autoPlay: true,
1622
+ muted: true,
1623
+ playsInline: true,
1624
+ className: cn("size-full object-cover", mirror && "-scale-x-100"),
1625
+ "data-testid": "call-prejoin-video"
1626
+ }
1627
+ ) : /* @__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" }) }),
1628
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center gap-3", children: [
1629
+ /* @__PURE__ */ jsx(
1630
+ CallControlButton,
1631
+ {
1632
+ label: choices.audioEnabled ? "Turn off microphone" : "Turn on microphone",
1633
+ tone: choices.audioEnabled ? "neutral" : "off",
1634
+ "aria-pressed": choices.audioEnabled,
1635
+ onClick: () => set({ audioEnabled: !choices.audioEnabled }),
1636
+ "data-testid": "call-prejoin-mic-toggle",
1637
+ children: choices.audioEnabled ? /* @__PURE__ */ jsx(Mic, { "aria-hidden": true }) : /* @__PURE__ */ jsx(MicOff, { "aria-hidden": true })
1638
+ }
1639
+ ),
1640
+ /* @__PURE__ */ jsx(
1641
+ CallControlButton,
1642
+ {
1643
+ label: choices.videoEnabled ? "Turn off camera" : "Turn on camera",
1644
+ tone: choices.videoEnabled ? "neutral" : "off",
1645
+ "aria-pressed": choices.videoEnabled,
1646
+ onClick: () => set({ videoEnabled: !choices.videoEnabled }),
1647
+ "data-testid": "call-prejoin-camera-toggle",
1648
+ children: choices.videoEnabled ? /* @__PURE__ */ jsx(Video, { "aria-hidden": true }) : /* @__PURE__ */ jsx(VideoOff, { "aria-hidden": true })
1649
+ }
1650
+ )
1651
+ ] })
1652
+ ] }),
1653
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col gap-4", children: [
1654
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
1655
+ /* @__PURE__ */ jsx("h2", { className: "text-xl font-semibold", children: heading }),
1656
+ title && /* @__PURE__ */ jsx("p", { className: "truncate text-sm text-muted-foreground", children: title })
1657
+ ] }),
1658
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
1659
+ /* @__PURE__ */ jsx(
1660
+ DeviceSelect,
1661
+ {
1662
+ devices: mics,
1663
+ value: choices.audioDeviceId,
1664
+ onChange: (id) => set({ audioDeviceId: id }),
1665
+ label: "Microphone",
1666
+ icon: /* @__PURE__ */ jsx(Mic, {}),
1667
+ disabled: !choices.audioEnabled,
1668
+ testId: "call-prejoin-mic-select"
1669
+ }
1670
+ ),
1671
+ /* @__PURE__ */ jsx(
1672
+ "div",
1673
+ {
1674
+ className: "h-1.5 w-full overflow-hidden rounded-full bg-muted",
1675
+ role: "meter",
1676
+ "aria-label": "Microphone level",
1677
+ "aria-valuemin": 0,
1678
+ "aria-valuemax": 100,
1679
+ "aria-valuenow": Math.round(level * 100),
1680
+ "data-testid": "call-prejoin-mic-level",
1681
+ children: /* @__PURE__ */ jsx(
1682
+ "div",
1683
+ {
1684
+ className: "h-full bg-primary transition-[width] duration-75 motion-reduce:transition-none",
1685
+ style: { width: `${Math.round(level * 100)}%` }
1686
+ }
1687
+ )
1688
+ }
1689
+ ),
1690
+ /* @__PURE__ */ jsx(
1691
+ DeviceSelect,
1692
+ {
1693
+ devices: cameras,
1694
+ value: choices.videoDeviceId,
1695
+ onChange: (id) => set({ videoDeviceId: id }),
1696
+ label: "Camera",
1697
+ icon: /* @__PURE__ */ jsx(Video, {}),
1698
+ disabled: !choices.videoEnabled,
1699
+ testId: "call-prejoin-camera-select"
1700
+ }
1701
+ )
1702
+ ] }),
1703
+ children,
1704
+ (error || deviceError) && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-sm text-destructive", "data-testid": "call-prejoin-error", children: error ?? deviceError }),
1705
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
1706
+ /* @__PURE__ */ jsxs(
1707
+ Button,
1708
+ {
1709
+ type: "button",
1710
+ className: "flex-1",
1711
+ disabled: busy,
1712
+ onClick: () => onJoin(choices),
1713
+ "data-testid": "call-prejoin-join",
1714
+ children: [
1715
+ busy && /* @__PURE__ */ jsx(Loader2, { className: "size-4 animate-spin", "aria-hidden": true }),
1716
+ joinLabel
1717
+ ]
1718
+ }
1719
+ ),
1720
+ onCancel && /* @__PURE__ */ jsx(
1721
+ Button,
1722
+ {
1723
+ type: "button",
1724
+ variant: "outline",
1725
+ onClick: onCancel,
1726
+ "data-testid": "call-prejoin-cancel",
1727
+ children: "Cancel"
1728
+ }
1729
+ )
1730
+ ] })
1731
+ ] })
1732
+ ] }) });
1733
+ }
1734
+
1735
+ export { CallControlBar, CallControlButton, CallFocus, CallGrid, CallHeader, CallNotice, CallParticipants, CallSidePanel, CallStageFrame, CallStateMessage, CallStatusChip, CallTile, CaptionsStrip, ConnectionOverlay, ControlBadge, ControlDivider, ControlGroup, DEFAULT_JOIN_CHOICES, ElapsedTime, EncryptionStatusChip, IncomingCallCard, LeaveButton, LobbyRequests, MediaControl, MinimisedCall, MoreMenu, PreJoin, RecordControl, RecordingStatusChip, SpeakingNow, TranscriptionStatusChip, VoiceMeter, describeMediaError, useBriefNotice, useElementSize, useShortcutKeys };
1736
+ //# sourceMappingURL=index.js.map
1737
+ //# sourceMappingURL=index.js.map