@rapidmx/meet-plugin 0.11.0 → 0.12.0

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.
@@ -9,6 +9,12 @@
9
9
  * screen, reactions, raise hand, grid/focus and leave. The effects button opens the video filter picker
10
10
  * (`_EffectsPanel.tsx`) and lights up while any filter is on.
11
11
  *
12
+ * The diagnostics button is the one exception to "opens near its own button": its panel is `fixed` to the
13
+ * viewport's top-left corner rather than anchored above the button, since it reads better as a HUD-style overlay
14
+ * sitting over the call than as a dropdown tucked into the bottom control bar. The outside-click/Escape handling
15
+ * (`barRef`) still closes it the same way, since it's still a DOM descendant of the bar - only its visual position
16
+ * moved, not its open/close wiring.
17
+ *
12
18
  * The microphone button shows a live level while it is unmuted - bars that move with the sound the microphone is
13
19
  * picking up, so a participant can see their audio is being sent - and the camera button shows a green dot while a
14
20
  * camera is sending. Both are driven by `LocalMedia` (`apps/shared/media/useLocalMedia.ts`). The microphone button
@@ -322,7 +328,7 @@ export default function CallControls({
322
328
  <div
323
329
  role="dialog"
324
330
  aria-label="Call diagnostics"
325
- className="absolute bottom-full mb-3 right-0 w-80 max-w-[calc(100vw-1rem)] max-h-[60vh] overflow-y-auto p-3 rounded-2xl bg-[#2b2d30] text-white shadow-xl"
331
+ className="fixed z-30 top-3 left-3 w-80 max-w-[calc(100vw-1rem)] max-h-[70vh] overflow-y-auto p-3 rounded-2xl bg-[#2b2d30]/95 text-white shadow-xl"
326
332
  >
327
333
  <DiagnosticsPanel selfName={selfName} micOn={media.micOn} cameraOn={media.cameraOn} participants={participants} />
328
334
  </div>
@@ -666,113 +666,115 @@ export default function CallView({
666
666
  }
667
667
 
668
668
  return (
669
- <div className="fixed inset-0 z-50 flex flex-col bg-[#202124] text-white overflow-hidden">
669
+ <div className="fixed inset-0 z-50 flex bg-[#202124] text-white overflow-hidden">
670
670
  <style>{`@keyframes meet-float { 0% { transform: translateY(0) scale(.6); opacity: 0; } 12% { opacity: 1; transform: translateY(-4vh) scale(1); } 100% { transform: translateY(-45vh) scale(1); opacity: 0; } }`}</style>
671
- <header className="shrink-0 flex items-center justify-between gap-3 px-4 py-3">
672
- <h1 className="min-w-0 truncate text-base font-medium">{meetingTitle}</h1>
673
- <div className="flex items-center gap-2 shrink-0">
674
- {raisedNames.length > 0 && (
675
- <span className="max-w-[45vw] truncate px-3 py-1.5 rounded-full bg-[#a8c7fa] text-[#062e6f] text-sm font-medium" data-testid="raised-hands">
676
- ✋ {raisedNames.join(", ")}
677
- </span>
678
- )}
679
- {talkingStickActive && (
680
- <span className="max-w-[45vw] truncate px-3 py-1.5 rounded-full bg-[#a8c7fa] text-[#062e6f] text-sm font-medium" data-testid="talking-stick-status">
681
- 🎙️ {talkingStickHolderName ? `${talkingStickHolderName} ${talkingStickHolderName === "You" ? "have" : "has"} the floor` : "Waiting for the host to choose a speaker"}
682
- </span>
683
- )}
684
- {isHost && (
671
+ <div className="relative flex-1 min-w-0 flex flex-col overflow-hidden">
672
+ <header className="shrink-0 flex items-center justify-between gap-3 px-4 py-3">
673
+ <h1 className="min-w-0 truncate text-base font-medium">{meetingTitle}</h1>
674
+ <div className="flex items-center gap-2 shrink-0">
675
+ {raisedNames.length > 0 && (
676
+ <span className="max-w-[45vw] truncate px-3 py-1.5 rounded-full bg-[#a8c7fa] text-[#062e6f] text-sm font-medium" data-testid="raised-hands">
677
+ ✋ {raisedNames.join(", ")}
678
+ </span>
679
+ )}
680
+ {talkingStickActive && (
681
+ <span className="max-w-[45vw] truncate px-3 py-1.5 rounded-full bg-[#a8c7fa] text-[#062e6f] text-sm font-medium" data-testid="talking-stick-status">
682
+ 🎙️ {talkingStickHolderName ? `${talkingStickHolderName} ${talkingStickHolderName === "You" ? "have" : "has"} the floor` : "Waiting for the host to choose a speaker"}
683
+ </span>
684
+ )}
685
+ {isHost && (
686
+ <button
687
+ type="button"
688
+ className={`px-3 py-1.5 rounded-full text-sm font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80 ${talkingStickActive ? "bg-[#a8c7fa] text-[#062e6f] hover:bg-[#8ab4f8]" : "bg-[#3c4043] hover:bg-[#4b4f53]"}`}
689
+ aria-pressed={talkingStickActive}
690
+ onClick={handleToggleTalkingStick}
691
+ >
692
+ {talkingStickActive ? "End talking stick" : "Talking stick"}
693
+ </button>
694
+ )}
685
695
  <button
686
696
  type="button"
687
- className={`px-3 py-1.5 rounded-full text-sm font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80 ${talkingStickActive ? "bg-[#a8c7fa] text-[#062e6f] hover:bg-[#8ab4f8]" : "bg-[#3c4043] hover:bg-[#4b4f53]"}`}
688
- aria-pressed={talkingStickActive}
689
- onClick={handleToggleTalkingStick}
697
+ className="px-3 py-1.5 rounded-full bg-[#3c4043] hover:bg-[#4b4f53] text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80"
698
+ aria-label={`${participants.length + 1} participants`}
699
+ aria-haspopup="dialog"
700
+ aria-expanded={drawerOpen}
701
+ onClick={() => setDrawerOpen((prev) => !prev)}
690
702
  >
691
- {talkingStickActive ? "End talking stick" : "Talking stick"}
703
+ {participants.length + 1}
692
704
  </button>
693
- )}
705
+ </div>
706
+ </header>
707
+ {connectError && (
708
+ <div role="alert" className="shrink-0 mx-3 mb-2 px-3 py-2 rounded-lg bg-[#601410] text-[#f9dedc] text-sm">
709
+ {connectError}
710
+ </div>
711
+ )}
712
+ {audioBlocked && (
694
713
  <button
695
714
  type="button"
696
- className="px-3 py-1.5 rounded-full bg-[#3c4043] hover:bg-[#4b4f53] text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80"
697
- aria-label={`${participants.length + 1} participants`}
698
- aria-haspopup="dialog"
699
- aria-expanded={drawerOpen}
700
- onClick={() => setDrawerOpen((prev) => !prev)}
715
+ className="shrink-0 mx-3 mb-2 px-3 py-2 rounded-lg bg-[#a8c7fa] text-[#062e6f] text-sm font-medium"
716
+ onClick={handleResumeAudio}
701
717
  >
702
- {participants.length + 1}
718
+ Click here to turn on sound
703
719
  </button>
704
- </div>
705
- </header>
706
- {connectError && (
707
- <div role="alert" className="shrink-0 mx-3 mb-2 px-3 py-2 rounded-lg bg-[#601410] text-[#f9dedc] text-sm">
708
- {connectError}
709
- </div>
710
- )}
711
- {audioBlocked && (
712
- <button
713
- type="button"
714
- className="shrink-0 mx-3 mb-2 px-3 py-2 rounded-lg bg-[#a8c7fa] text-[#062e6f] text-sm font-medium"
715
- onClick={handleResumeAudio}
716
- >
717
- Click here to turn on sound
718
- </button>
719
- )}
720
- <main className="relative flex-1 min-h-0 px-3 pb-3">
721
- {stage}
722
- {presenterName && presenterUid !== peerId && (
723
- <p className="absolute top-2 left-5 px-2 py-0.5 rounded bg-black/60 text-sm">{presenterName} is presenting</p>
724
720
  )}
725
- </main>
726
- <footer className="shrink-0 pt-1 pb-[max(0.75rem,env(safe-area-inset-bottom))]">
727
- <CallControls
728
- media={media}
729
- selfName={selfName}
730
- participants={participants}
731
- isPresenting={isPresenting}
732
- presentingElsewhereName={presenterUid && presenterUid !== peerId ? presenterName : undefined}
733
- onToggleShare={() => void handleToggleShare()}
734
- screenTransform={screenTransform}
735
- onRotateScreen={handleRotateScreen}
736
- onFlipScreen={handleFlipScreen}
737
- handRaised={handRaised}
738
- onToggleHand={handleToggleHand}
739
- onReaction={handleReaction}
740
- viewMode={viewMode}
741
- onToggleViewMode={() => setViewMode((prev) => (prev === "grid" ? "focus" : "grid"))}
742
- micLocked={micLocked}
743
- onLeave={onLeave}
744
- />
745
- </footer>
746
-
747
- {!alone && (
748
- <div
749
- className="absolute z-10 right-3 top-14 w-28 sm:top-auto sm:bottom-24 sm:w-52 aspect-video shadow-xl xl:right-4 xl:bottom-4"
750
- data-testid="self-view"
751
- >
752
- {selfTile("h-full")}
753
- </div>
754
- )}
755
-
756
- <div className="pointer-events-none absolute left-4 bottom-28 w-40 h-[45vh]" aria-hidden="true">
757
- {reactions.map((reaction) => (
721
+ <main className="relative flex-1 min-h-0 px-3 pb-3">
722
+ {stage}
723
+ {presenterName && presenterUid !== peerId && (
724
+ <p className="absolute top-2 left-5 px-2 py-0.5 rounded bg-black/60 text-sm">{presenterName} is presenting</p>
725
+ )}
726
+ </main>
727
+ <footer className="shrink-0 pt-1 pb-[max(0.75rem,env(safe-area-inset-bottom))]">
728
+ <CallControls
729
+ media={media}
730
+ selfName={selfName}
731
+ participants={participants}
732
+ isPresenting={isPresenting}
733
+ presentingElsewhereName={presenterUid && presenterUid !== peerId ? presenterName : undefined}
734
+ onToggleShare={() => void handleToggleShare()}
735
+ screenTransform={screenTransform}
736
+ onRotateScreen={handleRotateScreen}
737
+ onFlipScreen={handleFlipScreen}
738
+ handRaised={handRaised}
739
+ onToggleHand={handleToggleHand}
740
+ onReaction={handleReaction}
741
+ viewMode={viewMode}
742
+ onToggleViewMode={() => setViewMode((prev) => (prev === "grid" ? "focus" : "grid"))}
743
+ micLocked={micLocked}
744
+ onLeave={onLeave}
745
+ />
746
+ </footer>
747
+
748
+ {!alone && (
758
749
  <div
759
- key={reaction.id}
760
- className="absolute bottom-0 flex flex-col items-center"
761
- style={{ left: `${(reaction.id % 4) * 36}px`, animation: `meet-float ${REACTION_MS}ms ease-out forwards` }}
762
- data-testid="reaction"
750
+ className="absolute z-10 right-3 top-14 w-28 sm:top-auto sm:bottom-24 sm:w-52 aspect-video shadow-xl xl:right-4 xl:bottom-4"
751
+ data-testid="self-view"
763
752
  >
764
- <span className="text-4xl leading-none">{reaction.emoji}</span>
765
- <span className="mt-1 px-1.5 rounded-full bg-[#a8c7fa] text-[#062e6f] text-xs">{reaction.name}</span>
753
+ {selfTile("h-full")}
766
754
  </div>
767
- ))}
768
- </div>
755
+ )}
756
+
757
+ <div className="pointer-events-none absolute left-4 bottom-28 w-40 h-[45vh]" aria-hidden="true">
758
+ {reactions.map((reaction) => (
759
+ <div
760
+ key={reaction.id}
761
+ className="absolute bottom-0 flex flex-col items-center"
762
+ style={{ left: `${(reaction.id % 4) * 36}px`, animation: `meet-float ${REACTION_MS}ms ease-out forwards` }}
763
+ data-testid="reaction"
764
+ >
765
+ <span className="text-4xl leading-none">{reaction.emoji}</span>
766
+ <span className="mt-1 px-1.5 rounded-full bg-[#a8c7fa] text-[#062e6f] text-xs">{reaction.name}</span>
767
+ </div>
768
+ ))}
769
+ </div>
769
770
 
770
- {participants.map((p) => {
771
- const stream = remoteStreams[p.uid];
772
- return stream ? <RemoteAudio key={`${p.uid}:${audioNonce}`} stream={stream} onBlocked={handleAudioBlocked} /> : null;
773
- })}
774
- <div role="status" aria-live="polite" className="sr-only">
775
- {announcement}
771
+ {participants.map((p) => {
772
+ const stream = remoteStreams[p.uid];
773
+ return stream ? <RemoteAudio key={`${p.uid}:${audioNonce}`} stream={stream} onBlocked={handleAudioBlocked} /> : null;
774
+ })}
775
+ <div role="status" aria-live="polite" className="sr-only">
776
+ {announcement}
777
+ </div>
776
778
  </div>
777
779
 
778
780
  {drawerOpen && (
@@ -8,8 +8,10 @@
8
8
  * shows on a tile, as plain rows instead (a list reads better than tiles once there's a reason to scan for one
9
9
  * name).
10
10
  *
11
- * Closes on Escape, on a click on the backdrop behind it, or its own close button - never on a click inside the
12
- * drawer itself, matching `_CallControls.tsx`'s menus.
11
+ * A docked sidebar, not an overlay: `_CallView.tsx` renders it as an ordinary flex sibling of the call's own
12
+ * content column, which shrinks to make room rather than being covered by a backdrop - meant to stay open and
13
+ * visible for as long as the participant wants, including while the call continues underneath. Closes on Escape or
14
+ * its own close button; there is no backdrop to click, on purpose.
13
15
  *
14
16
  * A "Host" tag marks the host's own row (`isSelfHost`/`isParticipantHost` - see `_CallView.tsx`'s doc comment on
15
17
  * `hostUid` for what that identity is and isn't). Only while `isSelfHost` does every *other* row also get a "Mute"
@@ -328,70 +330,66 @@ export default function ParticipantsDrawer({
328
330
  }, [onClose]);
329
331
 
330
332
  return (
331
- <>
332
- <div className="absolute inset-0 z-20 bg-black/40" onClick={onClose} aria-hidden="true" />
333
- <aside
334
- role="dialog"
335
- aria-label="Participants"
336
- className="absolute z-20 inset-y-0 right-0 w-72 max-w-[85vw] flex flex-col bg-[#2b2d30] text-white shadow-xl"
337
- onClick={(event) => event.stopPropagation()}
338
- >
339
- <div className="flex items-center justify-between gap-2 px-3 py-3 border-b border-white/10">
340
- <h2 className="text-sm font-medium">Participants ({participants.length + 1})</h2>
341
- <button
342
- type="button"
343
- className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80"
344
- aria-label="Close participants"
345
- onClick={onClose}
346
- >
347
- ✕
348
- </button>
349
- </div>
350
- {isSelfHost && (
351
- <label className="flex items-center gap-2 px-3 py-2 border-b border-white/10 text-sm">
352
- <input type="checkbox" checked={forceMuteOnJoin} onChange={onToggleForceMuteOnJoin} className="w-4 h-4" />
353
- Mute new participants on join
354
- </label>
355
- )}
356
- {isSelfHost && <PasswordSection hasPassword={hasPassword} onSetPassword={onSetPassword} />}
357
- {isSelfHost && (
358
- <label className="flex items-center gap-2 px-3 py-2 border-b border-white/10 text-sm">
359
- <input type="checkbox" checked={waitingRoomEnabled} onChange={onToggleWaitingRoomEnabled} className="w-4 h-4" />
360
- Require the host to admit participants
361
- </label>
362
- )}
363
- {isSelfHost && waitingRoomEnabled && (
364
- <WaitingSection waitingParticipants={waitingParticipants} onAdmit={onAdmit} onDeny={onDeny} />
365
- )}
366
- <ul className="flex-1 min-h-0 overflow-y-auto p-2 text-sm">
333
+ <aside
334
+ role="dialog"
335
+ aria-label="Participants"
336
+ className="relative z-10 shrink-0 w-72 max-w-[85vw] h-full flex flex-col bg-[#2b2d30] text-white shadow-xl border-l border-white/10"
337
+ >
338
+ <div className="flex items-center justify-between gap-2 px-3 py-3 border-b border-white/10">
339
+ <h2 className="text-sm font-medium">Participants ({participants.length + 1})</h2>
340
+ <button
341
+ type="button"
342
+ className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80"
343
+ aria-label="Close participants"
344
+ onClick={onClose}
345
+ >
346
+ ✕
347
+ </button>
348
+ </div>
349
+ {isSelfHost && (
350
+ <label className="flex items-center gap-2 px-3 py-2 border-b border-white/10 text-sm">
351
+ <input type="checkbox" checked={forceMuteOnJoin} onChange={onToggleForceMuteOnJoin} className="w-4 h-4" />
352
+ Mute new participants on join
353
+ </label>
354
+ )}
355
+ {isSelfHost && <PasswordSection hasPassword={hasPassword} onSetPassword={onSetPassword} />}
356
+ {isSelfHost && (
357
+ <label className="flex items-center gap-2 px-3 py-2 border-b border-white/10 text-sm">
358
+ <input type="checkbox" checked={waitingRoomEnabled} onChange={onToggleWaitingRoomEnabled} className="w-4 h-4" />
359
+ Require the host to admit participants
360
+ </label>
361
+ )}
362
+ {isSelfHost && waitingRoomEnabled && (
363
+ <WaitingSection waitingParticipants={waitingParticipants} onAdmit={onAdmit} onDeny={onDeny} />
364
+ )}
365
+ <ul className="flex-1 min-h-0 overflow-y-auto p-2 text-sm">
366
+ <Row
367
+ name={selfName}
368
+ isSelf
369
+ isHost={isSelfHost}
370
+ micMuted={!micOn}
371
+ handRaised={handRaised}
372
+ hasStick={talkingStickActive && talkingStickHolder === selfPeerId}
373
+ canGiveStick={isSelfHost && talkingStickActive}
374
+ onGiveStick={() => onGiveTalkingStick(selfPeerId)}
375
+ />
376
+ {participants.map((p) => (
367
377
  <Row
368
- name={selfName}
369
- isSelf
370
- isHost={isSelfHost}
371
- micMuted={!micOn}
372
- handRaised={handRaised}
373
- hasStick={talkingStickActive && talkingStickHolder === selfPeerId}
378
+ key={p.uid}
379
+ name={p.name}
380
+ isHost={isParticipantHost(p.uid)}
381
+ micMuted={!p.audioOn}
382
+ handRaised={p.handRaised}
383
+ transport={p.transport}
384
+ canModerate={isSelfHost}
385
+ onMute={() => onMute(p.uid)}
386
+ onKick={() => onKick(p.uid)}
387
+ hasStick={talkingStickActive && talkingStickHolder === p.uid}
374
388
  canGiveStick={isSelfHost && talkingStickActive}
375
- onGiveStick={() => onGiveTalkingStick(selfPeerId)}
389
+ onGiveStick={() => onGiveTalkingStick(p.uid)}
376
390
  />
377
- {participants.map((p) => (
378
- <Row
379
- key={p.uid}
380
- name={p.name}
381
- isHost={isParticipantHost(p.uid)}
382
- micMuted={!p.audioOn}
383
- handRaised={p.handRaised}
384
- transport={p.transport}
385
- canModerate={isSelfHost}
386
- onMute={() => onMute(p.uid)}
387
- onKick={() => onKick(p.uid)}
388
- hasStick={talkingStickActive && talkingStickHolder === p.uid}
389
- canGiveStick={isSelfHost && talkingStickActive}
390
- onGiveStick={() => onGiveTalkingStick(p.uid)}
391
- />
392
- ))}
393
- </ul>
394
- </aside>
395
- </>
391
+ ))}
392
+ </ul>
393
+ </aside>
396
394
  );
397
395
  }
@@ -5,6 +5,12 @@
5
5
  * screen, reactions, raise hand, grid/focus and leave. The effects button opens the video filter picker
6
6
  * (`_EffectsPanel.tsx`) and lights up while any filter is on.
7
7
  *
8
+ * The diagnostics button is the one exception to "opens near its own button": its panel is `fixed` to the
9
+ * viewport's top-left corner rather than anchored above the button, since it reads better as a HUD-style overlay
10
+ * sitting over the call than as a dropdown tucked into the bottom control bar. The outside-click/Escape handling
11
+ * (`barRef`) still closes it the same way, since it's still a DOM descendant of the bar - only its visual position
12
+ * moved, not its open/close wiring.
13
+ *
8
14
  * The microphone button shows a live level while it is unmuted - bars that move with the sound the microphone is
9
15
  * picking up, so a participant can see their audio is being sent - and the camera button shows a green dot while a
10
16
  * camera is sending. Both are driven by `LocalMedia` (`apps/shared/media/useLocalMedia.ts`). The microphone button
@@ -10,6 +10,12 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
10
10
  * screen, reactions, raise hand, grid/focus and leave. The effects button opens the video filter picker
11
11
  * (`_EffectsPanel.tsx`) and lights up while any filter is on.
12
12
  *
13
+ * The diagnostics button is the one exception to "opens near its own button": its panel is `fixed` to the
14
+ * viewport's top-left corner rather than anchored above the button, since it reads better as a HUD-style overlay
15
+ * sitting over the call than as a dropdown tucked into the bottom control bar. The outside-click/Escape handling
16
+ * (`barRef`) still closes it the same way, since it's still a DOM descendant of the bar - only its visual position
17
+ * moved, not its open/close wiring.
18
+ *
13
19
  * The microphone button shows a live level while it is unmuted - bars that move with the sound the microphone is
14
20
  * picking up, so a participant can see their audio is being sent - and the camera button shows a green dot while a
15
21
  * camera is sending. Both are driven by `LocalMedia` (`apps/shared/media/useLocalMedia.ts`). The microphone button
@@ -62,7 +68,7 @@ export default function CallControls({ media, selfName, participants, isPresenti
62
68
  return (_jsxs("div", { ref: barRef, className: "relative flex flex-wrap items-center justify-center gap-2 px-3", role: "toolbar", "aria-label": "Call controls", children: [_jsxs("div", { className: "sm:relative flex items-stretch gap-px", children: [_jsx("button", { type: "button", className: `${BUTTON} w-9 rounded-r-none ${media.micOn ? NEUTRAL : ALERT}`, "aria-label": "Choose microphone", "aria-haspopup": "menu", "aria-expanded": openMenu === "audio", onClick: () => toggleMenu("audio"), children: _jsx(ChevronUpIcon, {}) }), _jsxs("button", { type: "button", className: `${BUTTON} min-w-14 gap-1.5 px-3 rounded-l-none ${media.micOn ? NEUTRAL : ALERT} disabled:opacity-50 disabled:cursor-not-allowed`, "aria-label": media.micOn ? "Mute microphone" : "Unmute microphone", "aria-pressed": !media.micOn, disabled: micLocked, title: micLocked ? "Only the current talking-stick holder can unmute." : undefined, onClick: () => void media.toggleMic(), children: [media.micOn && _jsx(LevelBars, { level: media.audioLevel }), media.micOn ? _jsx(MicIcon, {}) : _jsx(MicOffIcon, {})] }), openMenu === "audio" && _jsx(DeviceMenu, { kind: "audio", media: media, onClose: () => setOpenMenu(null) })] }), _jsxs("div", { className: "sm:relative flex items-stretch gap-px", children: [_jsx("button", { type: "button", className: `${BUTTON} w-9 rounded-r-none ${media.cameraOn ? NEUTRAL : ALERT}`, "aria-label": "Choose camera", "aria-haspopup": "menu", "aria-expanded": openMenu === "video", onClick: () => toggleMenu("video"), children: _jsx(ChevronUpIcon, {}) }), _jsxs("button", { type: "button", className: `${BUTTON} relative min-w-14 px-3 rounded-l-none ${media.cameraOn ? NEUTRAL : ALERT}`, "aria-label": media.cameraOn ? "Turn off camera" : "Turn on camera", "aria-pressed": !media.cameraOn, onClick: () => void media.toggleCamera(), children: [media.cameraOn ? _jsx(VideoIcon, {}) : _jsx(VideoOffIcon, {}), media.cameraOn && (_jsx("span", { className: "absolute top-2 right-2 w-2 h-2 rounded-full bg-[#34a853] animate-pulse", "data-testid": "camera-live", title: "Your camera is sending" }))] }), openMenu === "video" && _jsx(DeviceMenu, { kind: "video", media: media, onClose: () => setOpenMenu(null) })] }), _jsxs("div", { className: "sm:relative", children: [_jsx("button", { type: "button", className: `${BUTTON} w-12 ${filtersActive(media.filters) ? ACTIVE : NEUTRAL}`, "aria-label": "Video effects", "aria-haspopup": "dialog", "aria-expanded": openMenu === "effects", onClick: () => toggleMenu("effects"), children: _jsx(EffectsIcon, {}) }), openMenu === "effects" && (_jsx("div", { role: "dialog", "aria-label": "Video effects", className: "absolute bottom-full mb-3 left-1/2 -translate-x-1/2 w-96 max-w-[calc(100vw-1rem)] max-h-[60vh] overflow-y-auto p-3 rounded-2xl bg-[#2b2d30] text-white shadow-xl", children: _jsx(EffectsPanel, { media: media, tone: "dark" }) }))] }), _jsx("button", { type: "button", className: `${BUTTON} w-12 ${isPresenting ? ACTIVE : NEUTRAL} disabled:opacity-40 disabled:cursor-not-allowed`, "aria-label": isPresenting ? "Stop sharing" : "Share screen", "aria-pressed": isPresenting, disabled: shareDisabled, title: shareDisabled ? `${presentingElsewhereName} is presenting - stop their share to present yourself.` : undefined, onClick: onToggleShare, children: _jsx(ScreenShareIcon, {}) }), isPresenting && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: `${BUTTON} w-12 ${NEUTRAL}`, "aria-label": "Rotate shared screen", title: "If your shared window looks sideways or upside down to everyone, rotate it here - this is a correction for your own capture, not a browser bug this app can fix automatically.", onClick: onRotateScreen, children: _jsx(RotateIcon, {}) }), _jsx("button", { type: "button", className: `${BUTTON} w-12 ${screenTransform.flipped ? ACTIVE : NEUTRAL}`, "aria-label": screenTransform.flipped ? "Unflip shared screen" : "Flip shared screen", "aria-pressed": screenTransform.flipped, onClick: onFlipScreen, children: _jsx(FlipIcon, {}) })] })), _jsxs("div", { className: "sm:relative", children: [_jsx("button", { type: "button", className: `${BUTTON} w-12 ${NEUTRAL}`, "aria-label": "Send a reaction", "aria-haspopup": "menu", "aria-expanded": openMenu === "emoji", onClick: () => toggleMenu("emoji"), children: _jsx(EmojiIcon, {}) }), openMenu === "emoji" && (_jsx("div", { role: "menu", "aria-label": "Reactions", className: "absolute bottom-full mb-3 left-1/2 -translate-x-1/2 w-max grid grid-cols-4 gap-1 p-2 rounded-2xl bg-[#2b2d30] shadow-xl", children: REACTION_EMOJIS.map((emoji) => (_jsx("button", { type: "button", role: "menuitem", className: "w-11 h-11 rounded-lg text-2xl hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80", "aria-label": `Send ${emoji}`, onClick: () => {
63
69
  onReaction(emoji);
64
70
  setOpenMenu(null);
65
- }, children: emoji }, emoji))) }))] }), _jsx("button", { type: "button", className: `${BUTTON} w-12 ${handRaised ? ACTIVE : NEUTRAL}`, "aria-label": handRaised ? "Lower hand" : "Raise hand", "aria-pressed": handRaised, onClick: onToggleHand, children: _jsx(HandIcon, {}) }), _jsx("button", { type: "button", className: `${BUTTON} w-12 ${NEUTRAL}`, "aria-label": viewMode === "grid" ? "Switch to focused view" : "Switch to grid view", onClick: onToggleViewMode, children: viewMode === "grid" ? _jsx(FocusIcon, {}) : _jsx(GridIcon, {}) }), _jsxs("div", { className: "sm:relative", children: [_jsx("button", { type: "button", className: `${BUTTON} w-12 ${openMenu === "diagnostics" ? ACTIVE : NEUTRAL}`, "aria-label": "Call diagnostics", "aria-haspopup": "dialog", "aria-expanded": openMenu === "diagnostics", onClick: () => toggleMenu("diagnostics"), children: _jsx(DiagnosticsIcon, {}) }), openMenu === "diagnostics" && (_jsx("div", { role: "dialog", "aria-label": "Call diagnostics", className: "absolute bottom-full mb-3 right-0 w-80 max-w-[calc(100vw-1rem)] max-h-[60vh] overflow-y-auto p-3 rounded-2xl bg-[#2b2d30] text-white shadow-xl", children: _jsx(DiagnosticsPanel, { selfName: selfName, micOn: media.micOn, cameraOn: media.cameraOn, participants: participants }) }))] }), _jsx("button", { type: "button", className: `${BUTTON} w-16 bg-[#d93025] text-white hover:bg-[#b3261e]`, "aria-label": "Leave call", onClick: () => onLeave(), children: _jsx(LeaveIcon, {}) })] }));
71
+ }, children: emoji }, emoji))) }))] }), _jsx("button", { type: "button", className: `${BUTTON} w-12 ${handRaised ? ACTIVE : NEUTRAL}`, "aria-label": handRaised ? "Lower hand" : "Raise hand", "aria-pressed": handRaised, onClick: onToggleHand, children: _jsx(HandIcon, {}) }), _jsx("button", { type: "button", className: `${BUTTON} w-12 ${NEUTRAL}`, "aria-label": viewMode === "grid" ? "Switch to focused view" : "Switch to grid view", onClick: onToggleViewMode, children: viewMode === "grid" ? _jsx(FocusIcon, {}) : _jsx(GridIcon, {}) }), _jsxs("div", { className: "sm:relative", children: [_jsx("button", { type: "button", className: `${BUTTON} w-12 ${openMenu === "diagnostics" ? ACTIVE : NEUTRAL}`, "aria-label": "Call diagnostics", "aria-haspopup": "dialog", "aria-expanded": openMenu === "diagnostics", onClick: () => toggleMenu("diagnostics"), children: _jsx(DiagnosticsIcon, {}) }), openMenu === "diagnostics" && (_jsx("div", { role: "dialog", "aria-label": "Call diagnostics", className: "fixed z-30 top-3 left-3 w-80 max-w-[calc(100vw-1rem)] max-h-[70vh] overflow-y-auto p-3 rounded-2xl bg-[#2b2d30]/95 text-white shadow-xl", children: _jsx(DiagnosticsPanel, { selfName: selfName, micOn: media.micOn, cameraOn: media.cameraOn, participants: participants }) }))] }), _jsx("button", { type: "button", className: `${BUTTON} w-16 bg-[#d93025] text-white hover:bg-[#b3261e]`, "aria-label": "Leave call", onClick: () => onLeave(), children: _jsx(LeaveIcon, {}) })] }));
66
72
  }
67
73
  /** The device picker opened from the chevron beside the microphone/camera button: the available devices with the
68
74
  * one in use ticked, or - while there are none to list - why, with a way to ask for access again. */
@@ -501,10 +501,10 @@ export default function CallView({ channel, token, selfUid, selfName, meetingTit
501
501
  else {
502
502
  stage = (_jsx("div", { className: "h-full grid gap-2 auto-rows-fr [grid-template-columns:repeat(auto-fit,minmax(min(100%,320px),1fr))]", children: participants.map((p) => remoteTile(p, "h-full")) }));
503
503
  }
504
- return (_jsxs("div", { className: "fixed inset-0 z-50 flex flex-col bg-[#202124] text-white overflow-hidden", children: [_jsx("style", { children: `@keyframes meet-float { 0% { transform: translateY(0) scale(.6); opacity: 0; } 12% { opacity: 1; transform: translateY(-4vh) scale(1); } 100% { transform: translateY(-45vh) scale(1); opacity: 0; } }` }), _jsxs("header", { className: "shrink-0 flex items-center justify-between gap-3 px-4 py-3", children: [_jsx("h1", { className: "min-w-0 truncate text-base font-medium", children: meetingTitle }), _jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [raisedNames.length > 0 && (_jsxs("span", { className: "max-w-[45vw] truncate px-3 py-1.5 rounded-full bg-[#a8c7fa] text-[#062e6f] text-sm font-medium", "data-testid": "raised-hands", children: ["\u270B ", raisedNames.join(", ")] })), talkingStickActive && (_jsxs("span", { className: "max-w-[45vw] truncate px-3 py-1.5 rounded-full bg-[#a8c7fa] text-[#062e6f] text-sm font-medium", "data-testid": "talking-stick-status", children: ["\uD83C\uDF99\uFE0F ", talkingStickHolderName ? `${talkingStickHolderName} ${talkingStickHolderName === "You" ? "have" : "has"} the floor` : "Waiting for the host to choose a speaker"] })), isHost && (_jsx("button", { type: "button", className: `px-3 py-1.5 rounded-full text-sm font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80 ${talkingStickActive ? "bg-[#a8c7fa] text-[#062e6f] hover:bg-[#8ab4f8]" : "bg-[#3c4043] hover:bg-[#4b4f53]"}`, "aria-pressed": talkingStickActive, onClick: handleToggleTalkingStick, children: talkingStickActive ? "End talking stick" : "Talking stick" })), _jsx("button", { type: "button", className: "px-3 py-1.5 rounded-full bg-[#3c4043] hover:bg-[#4b4f53] text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80", "aria-label": `${participants.length + 1} participants`, "aria-haspopup": "dialog", "aria-expanded": drawerOpen, onClick: () => setDrawerOpen((prev) => !prev), children: participants.length + 1 })] })] }), connectError && (_jsx("div", { role: "alert", className: "shrink-0 mx-3 mb-2 px-3 py-2 rounded-lg bg-[#601410] text-[#f9dedc] text-sm", children: connectError })), audioBlocked && (_jsx("button", { type: "button", className: "shrink-0 mx-3 mb-2 px-3 py-2 rounded-lg bg-[#a8c7fa] text-[#062e6f] text-sm font-medium", onClick: handleResumeAudio, children: "Click here to turn on sound" })), _jsxs("main", { className: "relative flex-1 min-h-0 px-3 pb-3", children: [stage, presenterName && presenterUid !== peerId && (_jsxs("p", { className: "absolute top-2 left-5 px-2 py-0.5 rounded bg-black/60 text-sm", children: [presenterName, " is presenting"] }))] }), _jsx("footer", { className: "shrink-0 pt-1 pb-[max(0.75rem,env(safe-area-inset-bottom))]", children: _jsx(CallControls, { media: media, selfName: selfName, participants: participants, isPresenting: isPresenting, presentingElsewhereName: presenterUid && presenterUid !== peerId ? presenterName : undefined, onToggleShare: () => void handleToggleShare(), screenTransform: screenTransform, onRotateScreen: handleRotateScreen, onFlipScreen: handleFlipScreen, handRaised: handRaised, onToggleHand: handleToggleHand, onReaction: handleReaction, viewMode: viewMode, onToggleViewMode: () => setViewMode((prev) => (prev === "grid" ? "focus" : "grid")), micLocked: micLocked, onLeave: onLeave }) }), !alone && (_jsx("div", { className: "absolute z-10 right-3 top-14 w-28 sm:top-auto sm:bottom-24 sm:w-52 aspect-video shadow-xl xl:right-4 xl:bottom-4", "data-testid": "self-view", children: selfTile("h-full") })), _jsx("div", { className: "pointer-events-none absolute left-4 bottom-28 w-40 h-[45vh]", "aria-hidden": "true", children: reactions.map((reaction) => (_jsxs("div", { className: "absolute bottom-0 flex flex-col items-center", style: { left: `${(reaction.id % 4) * 36}px`, animation: `meet-float ${REACTION_MS}ms ease-out forwards` }, "data-testid": "reaction", children: [_jsx("span", { className: "text-4xl leading-none", children: reaction.emoji }), _jsx("span", { className: "mt-1 px-1.5 rounded-full bg-[#a8c7fa] text-[#062e6f] text-xs", children: reaction.name })] }, reaction.id))) }), participants.map((p) => {
505
- const stream = remoteStreams[p.uid];
506
- return stream ? _jsx(RemoteAudio, { stream: stream, onBlocked: handleAudioBlocked }, `${p.uid}:${audioNonce}`) : null;
507
- }), _jsx("div", { role: "status", "aria-live": "polite", className: "sr-only", children: announcement }), drawerOpen && (_jsx(ParticipantsDrawer, { selfName: selfName, micOn: media.micOn, handRaised: handRaised, participants: participants, isSelfHost: isHost, isParticipantHost: isParticipantHost, onMute: handleMuteParticipant, onKick: handleKickParticipant, forceMuteOnJoin: forceMuteOnJoin, onToggleForceMuteOnJoin: handleToggleForceMuteOnJoin, hasPassword: hasPassword, onSetPassword: handleSetPassword, waitingRoomEnabled: waitingRoomEnabled, onToggleWaitingRoomEnabled: handleToggleWaitingRoomEnabled, waitingParticipants: waitingParticipants, onAdmit: handleAdmitParticipant, onDeny: handleDenyParticipant, talkingStickActive: talkingStickActive, selfPeerId: peerId, talkingStickHolder: talkingStickHolder, onGiveTalkingStick: handleGiveTalkingStick, onClose: () => setDrawerOpen(false) }))] }));
504
+ return (_jsxs("div", { className: "fixed inset-0 z-50 flex bg-[#202124] text-white overflow-hidden", children: [_jsx("style", { children: `@keyframes meet-float { 0% { transform: translateY(0) scale(.6); opacity: 0; } 12% { opacity: 1; transform: translateY(-4vh) scale(1); } 100% { transform: translateY(-45vh) scale(1); opacity: 0; } }` }), _jsxs("div", { className: "relative flex-1 min-w-0 flex flex-col overflow-hidden", children: [_jsxs("header", { className: "shrink-0 flex items-center justify-between gap-3 px-4 py-3", children: [_jsx("h1", { className: "min-w-0 truncate text-base font-medium", children: meetingTitle }), _jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [raisedNames.length > 0 && (_jsxs("span", { className: "max-w-[45vw] truncate px-3 py-1.5 rounded-full bg-[#a8c7fa] text-[#062e6f] text-sm font-medium", "data-testid": "raised-hands", children: ["\u270B ", raisedNames.join(", ")] })), talkingStickActive && (_jsxs("span", { className: "max-w-[45vw] truncate px-3 py-1.5 rounded-full bg-[#a8c7fa] text-[#062e6f] text-sm font-medium", "data-testid": "talking-stick-status", children: ["\uD83C\uDF99\uFE0F ", talkingStickHolderName ? `${talkingStickHolderName} ${talkingStickHolderName === "You" ? "have" : "has"} the floor` : "Waiting for the host to choose a speaker"] })), isHost && (_jsx("button", { type: "button", className: `px-3 py-1.5 rounded-full text-sm font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80 ${talkingStickActive ? "bg-[#a8c7fa] text-[#062e6f] hover:bg-[#8ab4f8]" : "bg-[#3c4043] hover:bg-[#4b4f53]"}`, "aria-pressed": talkingStickActive, onClick: handleToggleTalkingStick, children: talkingStickActive ? "End talking stick" : "Talking stick" })), _jsx("button", { type: "button", className: "px-3 py-1.5 rounded-full bg-[#3c4043] hover:bg-[#4b4f53] text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80", "aria-label": `${participants.length + 1} participants`, "aria-haspopup": "dialog", "aria-expanded": drawerOpen, onClick: () => setDrawerOpen((prev) => !prev), children: participants.length + 1 })] })] }), connectError && (_jsx("div", { role: "alert", className: "shrink-0 mx-3 mb-2 px-3 py-2 rounded-lg bg-[#601410] text-[#f9dedc] text-sm", children: connectError })), audioBlocked && (_jsx("button", { type: "button", className: "shrink-0 mx-3 mb-2 px-3 py-2 rounded-lg bg-[#a8c7fa] text-[#062e6f] text-sm font-medium", onClick: handleResumeAudio, children: "Click here to turn on sound" })), _jsxs("main", { className: "relative flex-1 min-h-0 px-3 pb-3", children: [stage, presenterName && presenterUid !== peerId && (_jsxs("p", { className: "absolute top-2 left-5 px-2 py-0.5 rounded bg-black/60 text-sm", children: [presenterName, " is presenting"] }))] }), _jsx("footer", { className: "shrink-0 pt-1 pb-[max(0.75rem,env(safe-area-inset-bottom))]", children: _jsx(CallControls, { media: media, selfName: selfName, participants: participants, isPresenting: isPresenting, presentingElsewhereName: presenterUid && presenterUid !== peerId ? presenterName : undefined, onToggleShare: () => void handleToggleShare(), screenTransform: screenTransform, onRotateScreen: handleRotateScreen, onFlipScreen: handleFlipScreen, handRaised: handRaised, onToggleHand: handleToggleHand, onReaction: handleReaction, viewMode: viewMode, onToggleViewMode: () => setViewMode((prev) => (prev === "grid" ? "focus" : "grid")), micLocked: micLocked, onLeave: onLeave }) }), !alone && (_jsx("div", { className: "absolute z-10 right-3 top-14 w-28 sm:top-auto sm:bottom-24 sm:w-52 aspect-video shadow-xl xl:right-4 xl:bottom-4", "data-testid": "self-view", children: selfTile("h-full") })), _jsx("div", { className: "pointer-events-none absolute left-4 bottom-28 w-40 h-[45vh]", "aria-hidden": "true", children: reactions.map((reaction) => (_jsxs("div", { className: "absolute bottom-0 flex flex-col items-center", style: { left: `${(reaction.id % 4) * 36}px`, animation: `meet-float ${REACTION_MS}ms ease-out forwards` }, "data-testid": "reaction", children: [_jsx("span", { className: "text-4xl leading-none", children: reaction.emoji }), _jsx("span", { className: "mt-1 px-1.5 rounded-full bg-[#a8c7fa] text-[#062e6f] text-xs", children: reaction.name })] }, reaction.id))) }), participants.map((p) => {
505
+ const stream = remoteStreams[p.uid];
506
+ return stream ? _jsx(RemoteAudio, { stream: stream, onBlocked: handleAudioBlocked }, `${p.uid}:${audioNonce}`) : null;
507
+ }), _jsx("div", { role: "status", "aria-live": "polite", className: "sr-only", children: announcement })] }), drawerOpen && (_jsx(ParticipantsDrawer, { selfName: selfName, micOn: media.micOn, handRaised: handRaised, participants: participants, isSelfHost: isHost, isParticipantHost: isParticipantHost, onMute: handleMuteParticipant, onKick: handleKickParticipant, forceMuteOnJoin: forceMuteOnJoin, onToggleForceMuteOnJoin: handleToggleForceMuteOnJoin, hasPassword: hasPassword, onSetPassword: handleSetPassword, waitingRoomEnabled: waitingRoomEnabled, onToggleWaitingRoomEnabled: handleToggleWaitingRoomEnabled, waitingParticipants: waitingParticipants, onAdmit: handleAdmitParticipant, onDeny: handleDenyParticipant, talkingStickActive: talkingStickActive, selfPeerId: peerId, talkingStickHolder: talkingStickHolder, onGiveTalkingStick: handleGiveTalkingStick, onClose: () => setDrawerOpen(false) }))] }));
508
508
  }
509
509
  /** Plays one remote participant's stream through a hidden `<audio>` element. `onBlocked` fires if the browser
510
510
  * refuses to start it (an autoplay policy) - the view then asks for a click. */
@@ -4,8 +4,10 @@
4
4
  * shows on a tile, as plain rows instead (a list reads better than tiles once there's a reason to scan for one
5
5
  * name).
6
6
  *
7
- * Closes on Escape, on a click on the backdrop behind it, or its own close button - never on a click inside the
8
- * drawer itself, matching `_CallControls.tsx`'s menus.
7
+ * A docked sidebar, not an overlay: `_CallView.tsx` renders it as an ordinary flex sibling of the call's own
8
+ * content column, which shrinks to make room rather than being covered by a backdrop - meant to stay open and
9
+ * visible for as long as the participant wants, including while the call continues underneath. Closes on Escape or
10
+ * its own close button; there is no backdrop to click, on purpose.
9
11
  *
10
12
  * A "Host" tag marks the host's own row (`isSelfHost`/`isParticipantHost` - see `_CallView.tsx`'s doc comment on
11
13
  * `hostUid` for what that identity is and isn't). Only while `isSelfHost` does every *other* row also get a "Mute"
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  ///////////////////////////////////////////////////////////////////////////////
3
3
  // Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
4
4
  // SPDX-License-Identifier: MPL-2.0
@@ -9,8 +9,10 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
9
9
  * shows on a tile, as plain rows instead (a list reads better than tiles once there's a reason to scan for one
10
10
  * name).
11
11
  *
12
- * Closes on Escape, on a click on the backdrop behind it, or its own close button - never on a click inside the
13
- * drawer itself, matching `_CallControls.tsx`'s menus.
12
+ * A docked sidebar, not an overlay: `_CallView.tsx` renders it as an ordinary flex sibling of the call's own
13
+ * content column, which shrinks to make room rather than being covered by a backdrop - meant to stay open and
14
+ * visible for as long as the participant wants, including while the call continues underneath. Closes on Escape or
15
+ * its own close button; there is no backdrop to click, on purpose.
14
16
  *
15
17
  * A "Host" tag marks the host's own row (`isSelfHost`/`isParticipantHost` - see `_CallView.tsx`'s doc comment on
16
18
  * `hostUid` for what that identity is and isn't). Only while `isSelfHost` does every *other* row also get a "Mute"
@@ -87,5 +89,5 @@ export default function ParticipantsDrawer({ selfName, micOn, handRaised, partic
87
89
  document.addEventListener("keydown", onKeyDown);
88
90
  return () => document.removeEventListener("keydown", onKeyDown);
89
91
  }, [onClose]);
90
- return (_jsxs(_Fragment, { children: [_jsx("div", { className: "absolute inset-0 z-20 bg-black/40", onClick: onClose, "aria-hidden": "true" }), _jsxs("aside", { role: "dialog", "aria-label": "Participants", className: "absolute z-20 inset-y-0 right-0 w-72 max-w-[85vw] flex flex-col bg-[#2b2d30] text-white shadow-xl", onClick: (event) => event.stopPropagation(), children: [_jsxs("div", { className: "flex items-center justify-between gap-2 px-3 py-3 border-b border-white/10", children: [_jsxs("h2", { className: "text-sm font-medium", children: ["Participants (", participants.length + 1, ")"] }), _jsx("button", { type: "button", className: "w-8 h-8 flex items-center justify-center rounded-full hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80", "aria-label": "Close participants", onClick: onClose, children: "\u2715" })] }), isSelfHost && (_jsxs("label", { className: "flex items-center gap-2 px-3 py-2 border-b border-white/10 text-sm", children: [_jsx("input", { type: "checkbox", checked: forceMuteOnJoin, onChange: onToggleForceMuteOnJoin, className: "w-4 h-4" }), "Mute new participants on join"] })), isSelfHost && _jsx(PasswordSection, { hasPassword: hasPassword, onSetPassword: onSetPassword }), isSelfHost && (_jsxs("label", { className: "flex items-center gap-2 px-3 py-2 border-b border-white/10 text-sm", children: [_jsx("input", { type: "checkbox", checked: waitingRoomEnabled, onChange: onToggleWaitingRoomEnabled, className: "w-4 h-4" }), "Require the host to admit participants"] })), isSelfHost && waitingRoomEnabled && (_jsx(WaitingSection, { waitingParticipants: waitingParticipants, onAdmit: onAdmit, onDeny: onDeny })), _jsxs("ul", { className: "flex-1 min-h-0 overflow-y-auto p-2 text-sm", children: [_jsx(Row, { name: selfName, isSelf: true, isHost: isSelfHost, micMuted: !micOn, handRaised: handRaised, hasStick: talkingStickActive && talkingStickHolder === selfPeerId, canGiveStick: isSelfHost && talkingStickActive, onGiveStick: () => onGiveTalkingStick(selfPeerId) }), participants.map((p) => (_jsx(Row, { name: p.name, isHost: isParticipantHost(p.uid), micMuted: !p.audioOn, handRaised: p.handRaised, transport: p.transport, canModerate: isSelfHost, onMute: () => onMute(p.uid), onKick: () => onKick(p.uid), hasStick: talkingStickActive && talkingStickHolder === p.uid, canGiveStick: isSelfHost && talkingStickActive, onGiveStick: () => onGiveTalkingStick(p.uid) }, p.uid)))] })] })] }));
92
+ return (_jsxs("aside", { role: "dialog", "aria-label": "Participants", className: "relative z-10 shrink-0 w-72 max-w-[85vw] h-full flex flex-col bg-[#2b2d30] text-white shadow-xl border-l border-white/10", children: [_jsxs("div", { className: "flex items-center justify-between gap-2 px-3 py-3 border-b border-white/10", children: [_jsxs("h2", { className: "text-sm font-medium", children: ["Participants (", participants.length + 1, ")"] }), _jsx("button", { type: "button", className: "w-8 h-8 flex items-center justify-center rounded-full hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80", "aria-label": "Close participants", onClick: onClose, children: "\u2715" })] }), isSelfHost && (_jsxs("label", { className: "flex items-center gap-2 px-3 py-2 border-b border-white/10 text-sm", children: [_jsx("input", { type: "checkbox", checked: forceMuteOnJoin, onChange: onToggleForceMuteOnJoin, className: "w-4 h-4" }), "Mute new participants on join"] })), isSelfHost && _jsx(PasswordSection, { hasPassword: hasPassword, onSetPassword: onSetPassword }), isSelfHost && (_jsxs("label", { className: "flex items-center gap-2 px-3 py-2 border-b border-white/10 text-sm", children: [_jsx("input", { type: "checkbox", checked: waitingRoomEnabled, onChange: onToggleWaitingRoomEnabled, className: "w-4 h-4" }), "Require the host to admit participants"] })), isSelfHost && waitingRoomEnabled && (_jsx(WaitingSection, { waitingParticipants: waitingParticipants, onAdmit: onAdmit, onDeny: onDeny })), _jsxs("ul", { className: "flex-1 min-h-0 overflow-y-auto p-2 text-sm", children: [_jsx(Row, { name: selfName, isSelf: true, isHost: isSelfHost, micMuted: !micOn, handRaised: handRaised, hasStick: talkingStickActive && talkingStickHolder === selfPeerId, canGiveStick: isSelfHost && talkingStickActive, onGiveStick: () => onGiveTalkingStick(selfPeerId) }), participants.map((p) => (_jsx(Row, { name: p.name, isHost: isParticipantHost(p.uid), micMuted: !p.audioOn, handRaised: p.handRaised, transport: p.transport, canModerate: isSelfHost, onMute: () => onMute(p.uid), onKick: () => onKick(p.uid), hasStick: talkingStickActive && talkingStickHolder === p.uid, canGiveStick: isSelfHost && talkingStickActive, onGiveStick: () => onGiveTalkingStick(p.uid) }, p.uid)))] })] }));
91
93
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rapidmx/meet-plugin",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "RapidMX plugin that provides WebRTC video conferencing, joinable from a calendar invite or a shared link.",
5
5
  "repository": "https://github.com/rapidmx/meet-plugin.git",
6
6
  "author": "RapidREST <rapidrests@gmail.com>",