@joewinke/jatui 0.2.25 → 0.2.27

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.
@@ -26,6 +26,13 @@
26
26
  controls?: MeetLocalControls;
27
27
  /** Host-action callbacks. */
28
28
  hostActions?: MeetHostActions;
29
+ /**
30
+ * Live input level of the local microphone, 0–1. When provided (and the
31
+ * mic is unmuted) the mic button renders a small 3-bar meter under the
32
+ * icon so the user can SEE that their voice is being picked up — the
33
+ * camera gives that feedback for free, the mic never did. Omit to hide.
34
+ */
35
+ micLevel?: number;
29
36
  class?: string;
30
37
  }
31
38
 
@@ -36,9 +43,19 @@
36
43
  allowScreenShare = true,
37
44
  controls = {},
38
45
  hostActions = {},
46
+ micLevel,
39
47
  class: className = ''
40
48
  }: Props = $props();
41
49
 
50
+ // Perceptual boost: raw RMS of speech sits low (~0.05–0.3); sqrt spreads
51
+ // the useful range across the meter. Each bar lights past its threshold.
52
+ const meterLevel = $derived(Math.sqrt(Math.min(1, Math.max(0, micLevel ?? 0))));
53
+ const barActive = $derived([
54
+ meterLevel > 0.15,
55
+ meterLevel > 0.4,
56
+ meterLevel > 0.65
57
+ ]);
58
+
42
59
  const recording = $derived(room.recording ?? false);
43
60
  const locked = $derived(room.locked ?? false);
44
61
  </script>
@@ -52,7 +69,7 @@
52
69
  <!-- Mic -->
53
70
  <button
54
71
  type="button"
55
- class="btn btn-circle"
72
+ class="btn btn-circle relative"
56
73
  class:btn-error={local.micMuted}
57
74
  class:btn-ghost={!local.micMuted}
58
75
  aria-pressed={local.micMuted}
@@ -67,6 +84,17 @@
67
84
  <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
68
85
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11a7 7 0 01-14 0m7 7v3m0-3a4 4 0 004-4V5a4 4 0 00-8 0v6a4 4 0 004 4z" />
69
86
  </svg>
87
+ {#if micLevel !== undefined}
88
+ <span class="jatui-mic-meter" aria-hidden="true">
89
+ {#each barActive as active, i (i)}
90
+ <span
91
+ class="jatui-mic-meter-bar"
92
+ class:is-active={active}
93
+ style="height: {0.2 + i * 0.125}rem"
94
+ ></span>
95
+ {/each}
96
+ </span>
97
+ {/if}
70
98
  {/if}
71
99
  </button>
72
100
 
@@ -174,3 +202,27 @@
174
202
  <span class="hidden sm:inline">Leave</span>
175
203
  </button>
176
204
  </div>
205
+
206
+ <style>
207
+ /* Mic level meter — 3 tiny bars docked bottom-center of the mic button.
208
+ Color/motion stay subtle: base-content at rest, success when lit. */
209
+ .jatui-mic-meter {
210
+ position: absolute;
211
+ bottom: 0.3rem;
212
+ left: 50%;
213
+ transform: translateX(-50%);
214
+ display: inline-flex;
215
+ align-items: flex-end;
216
+ gap: 2px;
217
+ pointer-events: none;
218
+ }
219
+ .jatui-mic-meter-bar {
220
+ width: 3px;
221
+ border-radius: 1px;
222
+ background: color-mix(in oklch, var(--color-base-content) 25%, transparent);
223
+ transition: background var(--motion-fast, 150ms) ease;
224
+ }
225
+ .jatui-mic-meter-bar.is-active {
226
+ background: var(--color-success);
227
+ }
228
+ </style>
@@ -24,6 +24,13 @@ interface Props {
24
24
  controls?: MeetLocalControls;
25
25
  /** Host-action callbacks. */
26
26
  hostActions?: MeetHostActions;
27
+ /**
28
+ * Live input level of the local microphone, 0–1. When provided (and the
29
+ * mic is unmuted) the mic button renders a small 3-bar meter under the
30
+ * icon so the user can SEE that their voice is being picked up — the
31
+ * camera gives that feedback for free, the mic never did. Omit to hide.
32
+ */
33
+ micLevel?: number;
27
34
  class?: string;
28
35
  }
29
36
  declare const ControlBar: import("svelte").Component<Props, {}, "">;
@@ -12,6 +12,7 @@
12
12
  * haven't installed the peer. Media preview is stopped on unmount and before
13
13
  * join (the room re-acquires tracks).
14
14
  */
15
+ import { untrack } from 'svelte';
15
16
  import type { Snippet } from 'svelte';
16
17
  import type { PreJoinResult } from './types.js';
17
18
 
@@ -175,7 +176,12 @@
175
176
  void videoDeviceId;
176
177
  void audioDeviceId;
177
178
  void videoEl;
178
- acquire();
179
+ // untrack is load-bearing: acquire() synchronously calls stopStream(),
180
+ // which READS `stream` — without untrack the effect silently depends on
181
+ // `stream`, so the `stream = next` assignment re-runs the effect, whose
182
+ // cleanup stops the just-acquired stream and re-acquires, forever. The
183
+ // observable symptom was a preview that never survived acquisition.
184
+ untrack(() => acquire());
179
185
  return () => stopStream();
180
186
  });
181
187
 
@@ -1,16 +1,3 @@
1
- /**
2
- * PreJoinLobby — the device check before joining.
3
- *
4
- * Camera/mic self-preview, display-name confirm, per-device pickers, and an
5
- * optional consent gate (checkbox slot; recording/telehealth contexts). On
6
- * "Join" it emits a {@link PreJoinResult} the route uses to exchange the join
7
- * token for a LiveKit JWT and connect.
8
- *
9
- * Uses only browser-native `getUserMedia`/`enumerateDevices` — NO
10
- * livekit-client dependency, so the lobby renders in demos and in apps that
11
- * haven't installed the peer. Media preview is stopped on unmount and before
12
- * join (the room re-acquires tracks).
13
- */
14
1
  import type { Snippet } from 'svelte';
15
2
  import type { PreJoinResult } from './types.js';
16
3
  interface Props {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joewinke/jatui",
3
- "version": "0.2.25",
3
+ "version": "0.2.27",
4
4
  "private": false,
5
5
  "description": "Shared Svelte 5 component library for JAT projects",
6
6
  "type": "module",