@rapidmx/meet-plugin 0.13.1 → 0.14.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.
Files changed (41) hide show
  1. package/README.md +55 -55
  2. package/apps/meet/[token].tsx +341 -341
  3. package/apps/meet/_CallControls.tsx +366 -394
  4. package/apps/meet/_CallView.tsx +987 -848
  5. package/apps/meet/_DiagnosticsWindow.tsx +80 -0
  6. package/apps/meet/_MeetLobby.tsx +224 -224
  7. package/apps/meet/_ParticipantTile.tsx +169 -169
  8. package/apps/meet/_ParticipantsDrawer.tsx +10 -104
  9. package/apps/meet/_SettingsModal.tsx +152 -0
  10. package/apps/meet/_icons.tsx +80 -82
  11. package/apps/meet/_meetApi.ts +212 -212
  12. package/apps/shared/media/filters/VideoFilterProcessor.ts +408 -408
  13. package/apps/shared/media/filters/mlModels.ts +174 -174
  14. package/apps/shared/push/GuestSignalingClient.ts +359 -359
  15. package/apps/shared/relay/RelayClient.ts +7 -2
  16. package/apps/shared/webrtc/MeshConnectionManager.ts +949 -795
  17. package/apps/shared/webrtc/types.ts +247 -222
  18. package/dist/apps/meet/_CallControls.d.ts +3 -8
  19. package/dist/apps/meet/_CallControls.js +3 -10
  20. package/dist/apps/meet/_CallView.d.ts +15 -2
  21. package/dist/apps/meet/_CallView.js +70 -4
  22. package/dist/apps/meet/_DiagnosticsWindow.d.ts +25 -0
  23. package/dist/apps/meet/_DiagnosticsWindow.js +45 -0
  24. package/dist/apps/meet/_ParticipantsDrawer.d.ts +10 -21
  25. package/dist/apps/meet/_ParticipantsDrawer.js +9 -39
  26. package/dist/apps/meet/_SettingsModal.d.ts +28 -0
  27. package/dist/apps/meet/_SettingsModal.js +59 -0
  28. package/dist/apps/meet/_icons.d.ts +1 -1
  29. package/dist/apps/meet/_icons.js +1 -1
  30. package/dist/apps/shared/relay/RelayClient.d.ts +6 -1
  31. package/dist/apps/shared/relay/RelayClient.js +7 -2
  32. package/dist/apps/shared/webrtc/MeshConnectionManager.d.ts +55 -2
  33. package/dist/apps/shared/webrtc/MeshConnectionManager.js +153 -9
  34. package/dist/apps/shared/webrtc/types.d.ts +25 -1
  35. package/dist/lib/util/RedisRelayBus.js +61 -5
  36. package/dist/lib/util/RedisRelayBus.js.map +1 -1
  37. package/dist/lib/util/RelayHub.js +10 -3
  38. package/dist/lib/util/RelayHub.js.map +1 -1
  39. package/dist/types/util/RedisRelayBus.d.ts +45 -6
  40. package/dist/types/util/RelayHub.d.ts +9 -2
  41. package/package.json +208 -208
@@ -0,0 +1,152 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ /**
6
+ * The host-only meeting settings modal, opened from the navbar's "…" menu (`_CallView.tsx`) - host-only because
7
+ * `_CallView.tsx` never renders this for anyone else. Holds the settings that used to live in the participants
8
+ * drawer's header: "Mute new participants on join" (`forceMuteOnJoin`), the join password
9
+ * (`hasPassword`/`onSetPassword`), and "Require the host to admit participants" (`waitingRoomEnabled`) - each a
10
+ * setting for whoever joins *next*, not an action on anyone already here (muting someone already in the call is
11
+ * the participants drawer's own "Mute" button; the drawer also keeps the live waiting-room list, since admitting
12
+ * someone is closer to "who's here" than a setting).
13
+ *
14
+ * An ordinary modal, not a docked panel like the drawer or a persistent window like diagnostics: closes on Escape,
15
+ * a click on the backdrop, or its own close button, matching `_CallControls.tsx`'s menus and this app's general
16
+ * "settings is a focused, modal task" convention.
17
+ */
18
+ import React, { useEffect, useState } from "react";
19
+
20
+ export interface SettingsModalProps {
21
+ forceMuteOnJoin: boolean;
22
+ onToggleForceMuteOnJoin: () => void;
23
+ /** Whether this meeting currently requires a password - never the password itself (see
24
+ * `PublicVideoMeeting.hasPassword`'s own doc comment). */
25
+ hasPassword: boolean;
26
+ /** Sets (a non-empty string), replaces, or removes (`null`) the join password. Rejecting lets `PasswordSection`
27
+ * show its own inline error without this modal needing to know anything about it. */
28
+ onSetPassword: (password: string | null) => Promise<void>;
29
+ waitingRoomEnabled: boolean;
30
+ onToggleWaitingRoomEnabled: () => void;
31
+ onClose: () => void;
32
+ }
33
+
34
+ /** The password form: one text input plus a Set/Change button, and a Remove button once one is set. Owns its own
35
+ * input/error/busy state locally - the modal only needs to know whether a password exists, not what a half-typed
36
+ * one currently says. */
37
+ function PasswordSection({ hasPassword, onSetPassword }: { hasPassword: boolean; onSetPassword: (password: string | null) => Promise<void> }) {
38
+ const [input, setInput] = useState("");
39
+ const [error, setError] = useState<string | null>(null);
40
+ const [busy, setBusy] = useState(false);
41
+
42
+ async function submit(password: string | null) {
43
+ setBusy(true);
44
+ setError(null);
45
+ try {
46
+ await onSetPassword(password);
47
+ setInput("");
48
+ } catch {
49
+ setError("Could not save - try again.");
50
+ } finally {
51
+ setBusy(false);
52
+ }
53
+ }
54
+
55
+ return (
56
+ <div className="py-3 border-b border-white/10 text-sm">
57
+ <p className="text-white/70 mb-1.5">{hasPassword ? "Password protection is on." : "No password required to join."}</p>
58
+ <form
59
+ className="flex gap-2"
60
+ onSubmit={(event) => {
61
+ event.preventDefault();
62
+ if (input) {
63
+ void submit(input);
64
+ }
65
+ }}
66
+ >
67
+ <input
68
+ type="password"
69
+ aria-label={hasPassword ? "New password" : "Set a password"}
70
+ placeholder={hasPassword ? "New password" : "Set a password"}
71
+ className="flex-1 min-w-0 px-2 py-1 rounded bg-white/10 text-white placeholder:text-white/50 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80"
72
+ value={input}
73
+ onChange={(event) => setInput(event.target.value)}
74
+ disabled={busy}
75
+ />
76
+ <button
77
+ type="submit"
78
+ className="px-2 py-1 rounded text-xs whitespace-nowrap bg-white/10 hover:bg-white/20 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80 disabled:opacity-50"
79
+ disabled={busy || !input}
80
+ >
81
+ {hasPassword ? "Change" : "Set"}
82
+ </button>
83
+ </form>
84
+ {hasPassword && (
85
+ <button
86
+ type="button"
87
+ className="mt-1.5 text-xs text-[#f2b8b5] underline focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80 disabled:opacity-50"
88
+ disabled={busy}
89
+ onClick={() => void submit(null)}
90
+ >
91
+ Remove password
92
+ </button>
93
+ )}
94
+ {error && <p className="text-[#f2b8b5] text-xs mt-1.5">{error}</p>}
95
+ </div>
96
+ );
97
+ }
98
+
99
+ export default function SettingsModal({
100
+ forceMuteOnJoin,
101
+ onToggleForceMuteOnJoin,
102
+ hasPassword,
103
+ onSetPassword,
104
+ waitingRoomEnabled,
105
+ onToggleWaitingRoomEnabled,
106
+ onClose,
107
+ }: SettingsModalProps) {
108
+ useEffect(() => {
109
+ const onKeyDown = (event: KeyboardEvent) => {
110
+ if (event.key === "Escape") {
111
+ onClose();
112
+ }
113
+ };
114
+ document.addEventListener("keydown", onKeyDown);
115
+ return () => document.removeEventListener("keydown", onKeyDown);
116
+ }, [onClose]);
117
+
118
+ return (
119
+ <>
120
+ <div className="fixed inset-0 z-30 bg-black/40" onClick={onClose} aria-hidden="true" />
121
+ <div
122
+ role="dialog"
123
+ aria-label="Meeting settings"
124
+ className="fixed z-30 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-80 max-w-[calc(100vw-1.5rem)] max-h-[80vh] overflow-y-auto flex flex-col rounded-2xl bg-[#2b2d30] text-white shadow-xl"
125
+ onClick={(event) => event.stopPropagation()}
126
+ >
127
+ <div className="flex items-center justify-between gap-2 px-4 py-3 border-b border-white/10">
128
+ <h2 className="text-sm font-medium">Meeting settings</h2>
129
+ <button
130
+ type="button"
131
+ 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"
132
+ aria-label="Close meeting settings"
133
+ onClick={onClose}
134
+ >
135
+ ✕
136
+ </button>
137
+ </div>
138
+ <div className="px-4">
139
+ <label className="flex items-center gap-2 py-3 border-b border-white/10 text-sm">
140
+ <input type="checkbox" checked={forceMuteOnJoin} onChange={onToggleForceMuteOnJoin} className="w-4 h-4" />
141
+ Mute new participants on join
142
+ </label>
143
+ <PasswordSection hasPassword={hasPassword} onSetPassword={onSetPassword} />
144
+ <label className="flex items-center gap-2 py-3 text-sm">
145
+ <input type="checkbox" checked={waitingRoomEnabled} onChange={onToggleWaitingRoomEnabled} className="w-4 h-4" />
146
+ Require the host to admit participants
147
+ </label>
148
+ </div>
149
+ </div>
150
+ </>
151
+ );
152
+ }
@@ -1,82 +1,80 @@
1
- ///////////////////////////////////////////////////////////////////////////////
2
- // Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
3
- // SPDX-License-Identifier: MPL-2.0
4
- ///////////////////////////////////////////////////////////////////////////////
5
- /** The in-call control icons, as inline SVG (24x24, Material Design paths, `currentColor`) so this plugin needs no
6
- * icon package of its own. Decorative - every button that uses one carries its own `aria-label`. */
7
- import React from "react";
8
-
9
- function Icon({ path, className = "w-6 h-6" }: { path: string; className?: string }) {
10
- return (
11
- <svg viewBox="0 0 24 24" className={className} fill="currentColor" aria-hidden="true" focusable="false">
12
- <path d={path} />
13
- </svg>
14
- );
15
- }
16
-
17
- export const MicIcon = () => (
18
- <Icon path="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5.3-3c0 3-2.54 5.1-5.3 5.1S6.7 14 6.7 11H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c3.28-.48 6-3.3 6-6.72h-1.7z" />
19
- );
20
-
21
- export const MicOffIcon = () => (
22
- <Icon path="M19 11h-1.7c0 .74-.16 1.43-.43 2.05l1.23 1.23c.56-.98.9-2.09.9-3.28zm-4.02.17c0-.06.02-.11.02-.17V5c0-1.66-1.34-3-3-3S9 3.34 9 5v.18l5.98 5.99zM4.27 3L3 4.27l6.01 6.01V11c0 1.66 1.33 3 2.99 3 .22 0 .44-.03.65-.08l1.66 1.66c-.71.33-1.5.52-2.31.52-2.76 0-5.3-2.1-5.3-5.1H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c.91-.13 1.77-.45 2.54-.9L19.73 21 21 19.73 4.27 3z" />
23
- );
24
-
25
- export const VideoIcon = () => <Icon path="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z" />;
26
-
27
- export const VideoOffIcon = () => (
28
- <Icon path="M21 6.5l-4 4V7c0-.55-.45-1-1-1H9.82L21 17.18V6.5zM3.27 2L2 3.27 4.73 6H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.21 0 .39-.08.54-.18L19.73 21 21 19.73 3.27 2z" />
29
- );
30
-
31
- export const ScreenShareIcon = () => (
32
- <Icon path="M21 3H3c-1.11 0-2 .89-2 2v14c0 1.11.89 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.11-.9-2-2-2zm0 16.02H3V4.98h18v14.04zM10 12H8l4-4 4 4h-2v4h-4v-4z" />
33
- );
34
-
35
- export const EmojiIcon = () => (
36
- <Icon path="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm3.5-9c.83 0 1.5-.67 1.5-1.5S16.33 8 15.5 8 14 8.67 14 9.5s.67 1.5 1.5 1.5zm-7 0c.83 0 1.5-.67 1.5-1.5S9.33 8 8.5 8 7 8.67 7 9.5 7.67 11 8.5 11zm3.5 6.5c2.33 0 4.31-1.46 5.11-3.5H6.89c.8 2.04 2.78 3.5 5.11 3.5z" />
37
- );
38
-
39
- export const GridIcon = () => <Icon path="M3 3h8v8H3V3zm10 0h8v8h-8V3zM3 13h8v8H3v-8zm10 0h8v8h-8v-8z" />;
40
-
41
- export const FocusIcon = () => <Icon path="M3 3h18v12H3V3zm0 14h5v4H3v-4zm6.5 0h5v4h-5v-4zM16 17h5v4h-5v-4z" />;
42
-
43
- export const LeaveIcon = () => (
44
- <Icon path="M12 9c-1.6 0-3.15.25-4.6.72v3.1c0 .39-.23.74-.56.9-.98.49-1.87 1.12-2.66 1.85-.18.18-.43.28-.7.28-.28 0-.53-.11-.71-.29L.29 13.08c-.18-.17-.29-.42-.29-.7 0-.28.11-.53.29-.71C3.34 8.78 7.46 7 12 7s8.66 1.78 11.71 4.67c.18.18.29.43.29.71 0 .28-.11.53-.29.71l-2.48 2.48c-.18.18-.43.29-.71.29-.27 0-.52-.11-.7-.28-.79-.74-1.69-1.36-2.67-1.85-.33-.16-.56-.5-.56-.9v-3.1C15.15 9.25 13.6 9 12 9z" />
45
- );
46
-
47
- export const ChevronUpIcon = () => <Icon path="M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z" className="w-5 h-5" />;
48
-
49
- /** A raised hand, as an emoji glyph - Material's own "back hand" path isn't among the ones bundled here. */
50
- export const HandIcon = () => (
51
- <span className="text-xl leading-none" aria-hidden="true">
52
- ✋
53
- </span>
54
- );
55
-
56
- export const EffectsIcon = () => (
57
- <Icon path="M19 9l1.25-2.75L23 5l-2.75-1.25L19 1l-1.25 2.75L15 5l2.75 1.25L19 9zm-7.5.5L9 4 6.5 9.5 1 12l5.5 2.5L9 20l2.5-5.5L17 12l-5.5-2.5zM19 15l-1.25 2.75L15 19l2.75 1.25L19 23l1.25-2.75L23 19l-2.75-1.25L19 15z" />
58
- );
59
-
60
- export const RotateIcon = () => (
61
- <Icon path="M12 6V2L7 7l5 5V8c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z" />
62
- );
63
-
64
- export const FlipIcon = () => (
65
- <Icon path="M15 21h2v-2h-2v2zm4-12h2V7h-2v2zM3 5v14c0 1.1.9 2 2 2h4v-2H5V5h4V3H5c-1.1 0-2 .9-2 2zm16-2v2h2c0-1.1-.9-2-2-2zm-8 20h2V1h-2v22zm8-6h2v-2h-2v2zM15 5h2V3h-2v2zm4 8h2v-2h-2v2zm0 8c1.1 0 2-.9 2-2h-2v2z" />
66
- );
67
-
68
- export const DiagnosticsIcon = () => (
69
- <Icon path="M22 12h-4l-3 9L9 3l-3 9H2v2h5.6l1.4-4.2L13.4 21 17 12h5v-2z" />
70
- );
71
-
72
- /** A talking stick: a rounded shaft with a grip knob at one end, drawn (not a Material path - there isn't one for
73
- * this) as a diagonal bar with a circle, rather than reusing `HandIcon`'s emoji-glyph fallback, since this one
74
- * needs to light up (`currentColor`) the same way every other control icon here does. */
75
- export const BatonIcon = () => (
76
- <svg viewBox="0 0 24 24" className="w-6 h-6" fill="currentColor" aria-hidden="true" focusable="false">
77
- <g transform="rotate(45 12 12)">
78
- <rect x="5" y="10.5" width="13" height="3" rx="1.5" />
79
- <circle cx="19" cy="12" r="3" />
80
- </g>
81
- </svg>
82
- );
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ /** The in-call control icons, as inline SVG (24x24, Material Design paths, `currentColor`) so this plugin needs no
6
+ * icon package of its own. Decorative - every button that uses one carries its own `aria-label`. */
7
+ import React from "react";
8
+
9
+ function Icon({ path, className = "w-6 h-6" }: { path: string; className?: string }) {
10
+ return (
11
+ <svg viewBox="0 0 24 24" className={className} fill="currentColor" aria-hidden="true" focusable="false">
12
+ <path d={path} />
13
+ </svg>
14
+ );
15
+ }
16
+
17
+ export const MicIcon = () => (
18
+ <Icon path="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5.3-3c0 3-2.54 5.1-5.3 5.1S6.7 14 6.7 11H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c3.28-.48 6-3.3 6-6.72h-1.7z" />
19
+ );
20
+
21
+ export const MicOffIcon = () => (
22
+ <Icon path="M19 11h-1.7c0 .74-.16 1.43-.43 2.05l1.23 1.23c.56-.98.9-2.09.9-3.28zm-4.02.17c0-.06.02-.11.02-.17V5c0-1.66-1.34-3-3-3S9 3.34 9 5v.18l5.98 5.99zM4.27 3L3 4.27l6.01 6.01V11c0 1.66 1.33 3 2.99 3 .22 0 .44-.03.65-.08l1.66 1.66c-.71.33-1.5.52-2.31.52-2.76 0-5.3-2.1-5.3-5.1H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c.91-.13 1.77-.45 2.54-.9L19.73 21 21 19.73 4.27 3z" />
23
+ );
24
+
25
+ export const VideoIcon = () => <Icon path="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z" />;
26
+
27
+ export const VideoOffIcon = () => (
28
+ <Icon path="M21 6.5l-4 4V7c0-.55-.45-1-1-1H9.82L21 17.18V6.5zM3.27 2L2 3.27 4.73 6H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.21 0 .39-.08.54-.18L19.73 21 21 19.73 3.27 2z" />
29
+ );
30
+
31
+ export const ScreenShareIcon = () => (
32
+ <Icon path="M21 3H3c-1.11 0-2 .89-2 2v14c0 1.11.89 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.11-.9-2-2-2zm0 16.02H3V4.98h18v14.04zM10 12H8l4-4 4 4h-2v4h-4v-4z" />
33
+ );
34
+
35
+ export const EmojiIcon = () => (
36
+ <Icon path="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm3.5-9c.83 0 1.5-.67 1.5-1.5S16.33 8 15.5 8 14 8.67 14 9.5s.67 1.5 1.5 1.5zm-7 0c.83 0 1.5-.67 1.5-1.5S9.33 8 8.5 8 7 8.67 7 9.5 7.67 11 8.5 11zm3.5 6.5c2.33 0 4.31-1.46 5.11-3.5H6.89c.8 2.04 2.78 3.5 5.11 3.5z" />
37
+ );
38
+
39
+ export const GridIcon = () => <Icon path="M3 3h8v8H3V3zm10 0h8v8h-8V3zM3 13h8v8H3v-8zm10 0h8v8h-8v-8z" />;
40
+
41
+ export const FocusIcon = () => <Icon path="M3 3h18v12H3V3zm0 14h5v4H3v-4zm6.5 0h5v4h-5v-4zM16 17h5v4h-5v-4z" />;
42
+
43
+ export const LeaveIcon = () => (
44
+ <Icon path="M12 9c-1.6 0-3.15.25-4.6.72v3.1c0 .39-.23.74-.56.9-.98.49-1.87 1.12-2.66 1.85-.18.18-.43.28-.7.28-.28 0-.53-.11-.71-.29L.29 13.08c-.18-.17-.29-.42-.29-.7 0-.28.11-.53.29-.71C3.34 8.78 7.46 7 12 7s8.66 1.78 11.71 4.67c.18.18.29.43.29.71 0 .28-.11.53-.29.71l-2.48 2.48c-.18.18-.43.29-.71.29-.27 0-.52-.11-.7-.28-.79-.74-1.69-1.36-2.67-1.85-.33-.16-.56-.5-.56-.9v-3.1C15.15 9.25 13.6 9 12 9z" />
45
+ );
46
+
47
+ export const ChevronUpIcon = () => <Icon path="M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z" className="w-5 h-5" />;
48
+
49
+ /** A raised hand, as an emoji glyph - Material's own "back hand" path isn't among the ones bundled here. */
50
+ export const HandIcon = () => (
51
+ <span className="text-xl leading-none" aria-hidden="true">
52
+ ✋
53
+ </span>
54
+ );
55
+
56
+ export const EffectsIcon = () => (
57
+ <Icon path="M19 9l1.25-2.75L23 5l-2.75-1.25L19 1l-1.25 2.75L15 5l2.75 1.25L19 9zm-7.5.5L9 4 6.5 9.5 1 12l5.5 2.5L9 20l2.5-5.5L17 12l-5.5-2.5zM19 15l-1.25 2.75L15 19l2.75 1.25L19 23l1.25-2.75L23 19l-2.75-1.25L19 15z" />
58
+ );
59
+
60
+ export const RotateIcon = () => (
61
+ <Icon path="M12 6V2L7 7l5 5V8c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z" />
62
+ );
63
+
64
+ export const FlipIcon = () => (
65
+ <Icon path="M15 21h2v-2h-2v2zm4-12h2V7h-2v2zM3 5v14c0 1.1.9 2 2 2h4v-2H5V5h4V3H5c-1.1 0-2 .9-2 2zm16-2v2h2c0-1.1-.9-2-2-2zm-8 20h2V1h-2v22zm8-6h2v-2h-2v2zM15 5h2V3h-2v2zm4 8h2v-2h-2v2zm0 8c1.1 0 2-.9 2-2h-2v2z" />
66
+ );
67
+
68
+ /** A talking stick: a rounded shaft with a grip knob at one end, drawn (not a Material path - there isn't one for
69
+ * this) as a diagonal bar with a circle, rather than reusing `HandIcon`'s emoji-glyph fallback, since this one
70
+ * needs to light up (`currentColor`) the same way every other control icon here does. */
71
+ export const BatonIcon = () => (
72
+ <svg viewBox="0 0 24 24" className="w-6 h-6" fill="currentColor" aria-hidden="true" focusable="false">
73
+ <g transform="rotate(45 12 12)">
74
+ <rect x="5" y="10.5" width="13" height="3" rx="1.5" />
75
+ <circle cx="19" cy="12" r="3" />
76
+ </g>
77
+ </svg>
78
+ );
79
+
80
+ export const OverflowIcon = () => <Icon path="M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" />;