zero-query 1.2.13 → 1.2.15

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.
@@ -7,5 +7,5 @@ $.ready(() => {
7
7
  // Instantiate every registered <component-tag> already in the DOM.
8
8
  // (No $.router() is wiring it up for us in this single-page scaffold.)
9
9
  $.mountAll();
10
- console.log('{{NAME}} - zQuery v' + $.version + ' WebRTC demo');
10
+ console.log('{{NAME}} · zQuery v' + $.version + ' WebRTC demo');
11
11
  });
@@ -121,7 +121,11 @@ $.component('video-room', {
121
121
  },
122
122
 
123
123
  pickRoom(e) {
124
- const btn = e && e.currentTarget;
124
+ // zero-query @click is delegated (one listener on the component root),
125
+ // so e.currentTarget is that root - NOT the room-card button. Walk up
126
+ // from the actual click target to find the button carrying data-room.
127
+ const target = e && e.target;
128
+ const btn = target && target.closest && target.closest('[data-room]');
125
129
  const name = btn && btn.getAttribute('data-room');
126
130
  if (!name) return;
127
131
  if (this.state.joined || this.state.connecting) return;
@@ -660,64 +664,83 @@ $.component('video-room', {
660
664
  if (!hasShare) hint.push('screen sharing unavailable in this browser');
661
665
  const hintLine = hint.length ? `<div class="device-hint">${this._icon('alert', 14)}<span>${$.escapeHtml(hint.join(' · '))}</span></div>` : '';
662
666
 
667
+ const totalPeers = availableRooms.reduce((n, r) => n + (r.peerCount || 0), 0);
668
+ const liveBadge = `<span class="live-dot" aria-hidden="true"></span>
669
+ <span>${availableRooms.length} ${availableRooms.length === 1 ? 'room' : 'rooms'} · ${totalPeers} online</span>`;
670
+
663
671
  const roomsBlock = availableRooms.length > 0
664
672
  ? `<ul class="room-list">
665
673
  ${availableRooms.map((r) => {
666
- const active = r.name === roomName ? ' active' : '';
667
- const count = (r.peerCount === 1) ? '1 peer' : (r.peerCount + ' peers');
674
+ const active = r.name === roomName ? ' active' : '';
675
+ const isAlone = (r.peerCount || 0) <= 1;
676
+ const count = (r.peerCount === 1) ? '1 here' : ((r.peerCount || 0) + ' here');
677
+ const dots = Math.min(r.peerCount || 0, 4);
678
+ const dotsHtml = Array.from({ length: dots }).map((_, i) =>
679
+ `<span class="pip pip-${i % 3}"></span>`
680
+ ).join('') || '<span class="pip pip-empty"></span>';
668
681
  return `<li>
669
- <button type="button" class="room-pill${active}" data-room="${$.escapeHtml(r.name)}" @click="pickRoom" ${connecting ? 'disabled' : ''}>
670
- <span class="room-pill-name">#${$.escapeHtml(r.name)}</span>
671
- <span class="room-pill-count">${count}</span>
682
+ <button type="button" class="room-card${active}" data-room="${$.escapeHtml(r.name)}" @click="pickRoom" ${connecting ? 'disabled' : ''}>
683
+ <span class="room-card-pips">${dotsHtml}</span>
684
+ <span class="room-card-body">
685
+ <span class="room-card-name">#${$.escapeHtml(r.name)}</span>
686
+ <span class="room-card-count">${count}</span>
687
+ </span>
688
+ <span class="room-card-cta">${isAlone ? 'be the first' : 'jump in'} →</span>
672
689
  </button>
673
690
  </li>`;
674
691
  }).join('')}
675
692
  </ul>`
676
- : `<p class="room-list-empty">${loadingRooms ? 'Loading\u2026' : 'No rooms yet \u2014 type a name below to start a new one.'}</p>`;
693
+ : `<div class="room-list-empty">
694
+ ${loadingRooms ? 'Looking for live rooms\u2026' : 'No live rooms right now. Type a name below and start one.'}
695
+ </div>`;
677
696
 
678
697
  return `
679
- <div class="lobby">
680
- <h1>zQuery WebRTC Demo</h1>
681
- <p class="lead">
682
- Mesh video call powered by <code>$.webrtc.join()</code> against a
683
- <a href="https://github.com/tonywied17/zero-server" target="_blank" rel="noopener">zero-server</a>
684
- <code>SignalingHub</code>. Open this page on a second device (or in another
685
- browser window) and join the same room to see a peer appear. Each call
686
- supports up to a handful of peers in a full mesh; for larger meetings
687
- swap in an SFU.
688
- </p>
689
- <p class="lead">
690
- <strong>Mic, camera, and screen share are off by default.</strong>
691
- Join first - then enable the devices you actually want to share.
692
- Screen share will prompt you to choose a screen / window / tab and,
693
- on Chromium-based browsers, also lets you include tab or system audio.
694
- </p>
695
- <form class="join-form" @submit="join">
696
- <label>
697
- Room
698
- <input type="text" value="${$.escapeHtml(roomName)}" @input="setRoom" placeholder="lobby" ${connecting ? 'disabled' : ''} />
699
- </label>
700
- <label>
701
- Your name
702
- <input type="text" value="${$.escapeHtml(displayName)}" @input="setName" placeholder="display name" ${connecting ? 'disabled' : ''} />
703
- </label>
704
- <button type="submit" class="primary" ${connecting ? 'disabled' : ''}>
705
- ${connecting ? 'Joining...' : 'Join room'}
706
- </button>
707
- </form>
708
- <div class="rooms-section">
709
- <div class="rooms-head">
710
- <h2>Active rooms</h2>
711
- <button type="button" class="ghost" @click="refreshRooms" ${loadingRooms ? 'disabled' : ''}>
712
- ${loadingRooms ? 'Refreshing\u2026' : 'Refresh'}
698
+ <div class="lobby-shell">
699
+ <div class="lobby-bg" aria-hidden="true"></div>
700
+ <div class="lobby">
701
+ <div class="lobby-hero">
702
+ <span class="eyebrow">${liveBadge}</span>
703
+ <h1>Spin up a room. Share whatever.</h1>
704
+ <p class="lead">
705
+ A no-signup mesh call wired straight into
706
+ <code>$.webrtc.join()</code> on top of
707
+ <a href="https://github.com/tonywied17/zero-server" target="_blank" rel="noopener">zero-server</a>.
708
+ Drop a name, send the link to a friend, and you'll be on a peer-to-peer
709
+ video call in two clicks. Camera, mic, and screen share are <em>off</em>
710
+ until you say so.
711
+ </p>
712
+ </div>
713
+
714
+ <form class="join-form" @submit="join">
715
+ <label>
716
+ <span class="field-label">Room</span>
717
+ <span class="field-prefix">#</span>
718
+ <input class="has-prefix" type="text" value="${$.escapeHtml(roomName)}" @input="setRoom" placeholder="lobby" autocomplete="off" spellcheck="false" ${connecting ? 'disabled' : ''} />
719
+ </label>
720
+ <label>
721
+ <span class="field-label">Your name</span>
722
+ <input type="text" value="${$.escapeHtml(displayName)}" @input="setName" placeholder="how should peers see you?" autocomplete="off" ${connecting ? 'disabled' : ''} />
723
+ </label>
724
+ <button type="submit" class="primary join-btn" ${connecting ? 'disabled' : ''}>
725
+ ${connecting ? 'Connecting...' : 'Join room'}
713
726
  </button>
727
+ </form>
728
+
729
+ <div class="rooms-section">
730
+ <div class="rooms-head">
731
+ <h2>Live right now</h2>
732
+ <button type="button" class="ghost" @click="refreshRooms" ${loadingRooms ? 'disabled' : ''}>
733
+ ${loadingRooms ? 'Refreshing\u2026' : 'Refresh'}
734
+ </button>
735
+ </div>
736
+ ${roomsBlock}
714
737
  </div>
715
- ${roomsBlock}
738
+
739
+ ${hintLine}
740
+ <p class="status ${error ? 'error' : ''}">
741
+ ${error ? $.escapeHtml(error) : $.escapeHtml(status)}
742
+ </p>
716
743
  </div>
717
- ${hintLine}
718
- <p class="status ${error ? 'error' : ''}">
719
- ${error ? $.escapeHtml(error) : $.escapeHtml(status)}
720
- </p>
721
744
  </div>
722
745
  `;
723
746
  },
@@ -2,17 +2,20 @@
2
2
  * { box-sizing: border-box; }
3
3
 
4
4
  :root {
5
- --bg: #0b0c10;
6
- --panel: #151821;
7
- --panel-2: #1c2030;
8
- --border: #262b38;
5
+ --bg: #0a0b10;
6
+ --bg-2: #0f1018;
7
+ --panel: #14161f;
8
+ --panel-2: #1c1f2c;
9
+ --border: #242838;
9
10
  --border-hi: #3a4258;
10
- --text: #e8ecf3;
11
+ --text: #eef1f8;
11
12
  --text-muted: #8a93a6;
12
- --accent: #5b8def;
13
- --accent-2: #16a34a;
13
+ --accent: #7c5cff;
14
+ --accent-2: #22d3ee;
15
+ --accent-3: #16a34a;
14
16
  --danger: #e1455c;
15
17
  --warn: #f59e0b;
18
+ --gradient: linear-gradient(135deg, #7c5cff 0%, #22d3ee 100%);
16
19
  }
17
20
 
18
21
  html, body {
@@ -23,6 +26,7 @@ html, body {
23
26
  font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
24
27
  height: 100vh;
25
28
  overflow: hidden;
29
+ -webkit-font-smoothing: antialiased;
26
30
  }
27
31
 
28
32
  body { display: flex; flex-direction: column; }
@@ -30,64 +34,194 @@ body { display: flex; flex-direction: column; }
30
34
  .topbar {
31
35
  flex: 0 0 auto;
32
36
  display: flex;
33
- align-items: baseline;
37
+ align-items: center;
34
38
  justify-content: space-between;
35
- padding: 0.6rem 1rem;
39
+ padding: 0.65rem 1.1rem;
36
40
  border-bottom: 1px solid var(--border);
37
- background: var(--panel);
41
+ background: linear-gradient(180deg, rgba(124, 92, 255, 0.05), transparent 60%), var(--panel);
42
+ position: relative;
43
+ }
44
+ .topbar::after {
45
+ content: '';
46
+ position: absolute; left: 0; right: 0; bottom: -1px; height: 1px;
47
+ background: linear-gradient(90deg, transparent, rgba(124, 92, 255, 0.4), rgba(34, 211, 238, 0.4), transparent);
48
+ }
49
+ .topbar .brand {
50
+ display: inline-flex;
51
+ align-items: center;
52
+ gap: 0.55rem;
53
+ font-weight: 700;
54
+ font-size: 1rem;
55
+ letter-spacing: -0.01em;
38
56
  }
39
- .topbar .brand { font-weight: 700; font-size: 1rem; }
40
- .topbar small { color: var(--text-muted); }
57
+ .topbar .brand-mark { display: block; }
58
+ .topbar .brand-text { background: var(--gradient); -webkit-background-clip: text; background-clip: text; color: transparent; }
59
+ .topbar small { color: var(--text-muted); font-size: 0.78rem; letter-spacing: 0.02em; }
41
60
 
42
- main { flex: 1 1 auto; min-height: 0; }
61
+ main { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
43
62
 
44
63
  /* ---- Lobby (pre-join) -------------------------------------------------- */
45
64
 
65
+ .lobby-shell {
66
+ position: relative;
67
+ min-height: 100%;
68
+ display: flex;
69
+ align-items: flex-start;
70
+ justify-content: center;
71
+ padding: 3.5rem 1.25rem 4rem;
72
+ overflow: hidden;
73
+ }
74
+ .lobby-bg {
75
+ position: absolute; inset: 0;
76
+ pointer-events: none;
77
+ background:
78
+ radial-gradient(60rem 30rem at 15% -10%, rgba(124, 92, 255, 0.18), transparent 60%),
79
+ radial-gradient(50rem 25rem at 110% 10%, rgba(34, 211, 238, 0.14), transparent 60%),
80
+ radial-gradient(40rem 20rem at 50% 110%, rgba(22, 163, 74, 0.10), transparent 60%);
81
+ animation: bgDrift 24s ease-in-out infinite alternate;
82
+ }
83
+ @keyframes bgDrift {
84
+ 0% { transform: translate3d(0, 0, 0); }
85
+ 100% { transform: translate3d(-2%, 2%, 0); }
86
+ }
87
+
46
88
  .lobby {
47
- max-width: 620px;
48
- margin: 4rem auto;
49
- padding: 2rem;
50
- background: var(--panel);
89
+ position: relative;
90
+ width: min(680px, 100%);
91
+ padding: 2.25rem 2.25rem 2rem;
92
+ background: linear-gradient(180deg, rgba(28, 31, 44, 0.85), rgba(20, 22, 31, 0.85));
51
93
  border: 1px solid var(--border);
52
- border-radius: 0.75rem;
94
+ border-radius: 1.1rem;
95
+ backdrop-filter: blur(10px);
96
+ -webkit-backdrop-filter: blur(10px);
97
+ box-shadow: 0 25px 60px -25px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(124, 92, 255, 0.05);
98
+ }
99
+ .lobby::before {
100
+ content: '';
101
+ position: absolute; inset: -1px;
102
+ border-radius: inherit;
103
+ padding: 1px;
104
+ background: var(--gradient);
105
+ -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
106
+ -webkit-mask-composite: xor;
107
+ mask-composite: exclude;
108
+ opacity: 0.35;
109
+ pointer-events: none;
110
+ }
111
+
112
+ .lobby-hero { margin-bottom: 1.5rem; }
113
+ .lobby-hero .eyebrow {
114
+ display: inline-flex;
115
+ align-items: center;
116
+ gap: 0.45rem;
117
+ font-size: 0.72rem;
118
+ color: var(--text-muted);
119
+ background: rgba(124, 92, 255, 0.08);
120
+ border: 1px solid rgba(124, 92, 255, 0.18);
121
+ padding: 0.3rem 0.65rem;
122
+ border-radius: 999px;
123
+ letter-spacing: 0.04em;
124
+ text-transform: uppercase;
125
+ font-weight: 600;
126
+ }
127
+ .lobby-hero .live-dot {
128
+ width: 7px; height: 7px;
129
+ border-radius: 50%;
130
+ background: var(--accent-3);
131
+ box-shadow: 0 0 0 3px rgba(22, 163, 74, 0.22);
132
+ animation: livePulse 1.8s ease-in-out infinite;
133
+ }
134
+ @keyframes livePulse {
135
+ 0%, 100% { box-shadow: 0 0 0 3px rgba(22, 163, 74, 0.22); }
136
+ 50% { box-shadow: 0 0 0 5px rgba(22, 163, 74, 0.05); }
53
137
  }
54
- .lobby h1 { margin: 0 0 0.5rem; font-size: 1.4rem; }
55
- .lobby .lead { color: var(--text-muted); line-height: 1.55; margin: 0.4rem 0; }
56
- .lobby code { background: #0d101a; padding: 0.1rem 0.35rem; border-radius: 0.25rem; }
138
+
139
+ .lobby h1 {
140
+ margin: 0.8rem 0 0.5rem;
141
+ font-size: clamp(1.6rem, 3.5vw, 2rem);
142
+ font-weight: 700;
143
+ letter-spacing: -0.02em;
144
+ line-height: 1.15;
145
+ }
146
+ .lobby .lead {
147
+ color: var(--text-muted);
148
+ line-height: 1.6;
149
+ margin: 0.4rem 0;
150
+ font-size: 0.95rem;
151
+ }
152
+ .lobby .lead em { color: var(--text); font-style: normal; font-weight: 600; }
153
+ .lobby code {
154
+ background: rgba(124, 92, 255, 0.1);
155
+ color: #d4cdff;
156
+ padding: 0.1rem 0.4rem;
157
+ border-radius: 0.3rem;
158
+ font-size: 0.85em;
159
+ }
160
+ .lobby a { color: var(--accent-2); text-decoration: none; }
161
+ .lobby a:hover { text-decoration: underline; }
57
162
 
58
163
  .join-form {
59
- display: flex;
60
- flex-direction: column;
61
- gap: 0.75rem;
62
- margin: 1.25rem 0;
164
+ display: grid;
165
+ grid-template-columns: 1.4fr 1fr auto;
166
+ gap: 0.6rem;
167
+ margin: 1.5rem 0 1rem;
168
+ align-items: end;
63
169
  }
64
170
  .join-form label {
171
+ position: relative;
65
172
  display: flex;
66
173
  flex-direction: column;
67
174
  gap: 0.3rem;
68
- font-size: 0.85rem;
175
+ }
176
+ .join-form .field-label {
177
+ font-size: 0.72rem;
69
178
  color: var(--text-muted);
179
+ letter-spacing: 0.04em;
180
+ text-transform: uppercase;
181
+ font-weight: 600;
182
+ }
183
+ .join-form .field-prefix {
184
+ position: absolute;
185
+ left: 0.85rem;
186
+ bottom: 0.7rem;
187
+ color: var(--text-muted);
188
+ font-weight: 600;
189
+ pointer-events: none;
190
+ font-size: 0.95rem;
70
191
  }
71
192
  .join-form input {
72
- background: #0d101a;
193
+ background: rgba(10, 11, 16, 0.7);
73
194
  color: var(--text);
74
195
  border: 1px solid var(--border);
75
- border-radius: 0.375rem;
76
- padding: 0.55rem 0.75rem;
196
+ border-radius: 0.55rem;
197
+ padding: 0.65rem 0.85rem;
77
198
  font-family: inherit;
78
199
  font-size: 0.95rem;
200
+ transition: border-color .15s, box-shadow .15s, background .15s;
201
+ }
202
+ .join-form input.has-prefix { padding-left: 1.5rem; }
203
+ .join-form input:focus {
204
+ outline: none;
205
+ border-color: rgba(124, 92, 255, 0.55);
206
+ box-shadow: 0 0 0 3px rgba(124, 92, 255, 0.18);
207
+ background: rgba(10, 11, 16, 0.95);
208
+ }
209
+ .join-form .join-btn {
210
+ padding: 0.7rem 1.25rem;
211
+ height: fit-content;
212
+ align-self: stretch;
79
213
  }
80
214
 
81
215
  button {
82
216
  background: var(--panel-2);
83
217
  color: var(--text);
84
218
  border: 1px solid var(--border);
85
- border-radius: 0.375rem;
219
+ border-radius: 0.55rem;
86
220
  padding: 0.5rem 0.9rem;
87
221
  font-weight: 600;
88
222
  cursor: pointer;
89
223
  font-family: inherit;
90
- transition: background .15s, border-color .15s, transform .05s;
224
+ transition: background .15s, border-color .15s, transform .05s, box-shadow .15s;
91
225
  display: inline-flex;
92
226
  align-items: center;
93
227
  gap: 0.4rem;
@@ -97,24 +231,29 @@ button .icon { flex: 0 0 auto; display: block; }
97
231
  button:hover { background: #232939; border-color: var(--border-hi); }
98
232
  button:active { transform: translateY(1px); }
99
233
  button:disabled { opacity: 0.55; cursor: not-allowed; }
100
- button.primary { background: var(--accent); border-color: transparent; color: #fff; }
101
- button.primary:hover { filter: brightness(1.1); }
234
+ button.primary {
235
+ background: var(--gradient);
236
+ border-color: transparent;
237
+ color: #fff;
238
+ box-shadow: 0 8px 20px -8px rgba(124, 92, 255, 0.55);
239
+ }
240
+ button.primary:hover { filter: brightness(1.08); box-shadow: 0 10px 24px -8px rgba(124, 92, 255, 0.7); }
102
241
  button.leave { background: var(--danger); border-color: transparent; color: #fff; }
103
242
  button.off { background: #3a2030; color: #f3b6c2; }
104
- button.active { background: var(--accent-2); border-color: transparent; color: #fff; }
243
+ button.active { background: var(--accent-3); border-color: transparent; color: #fff; }
105
244
  button.ghost { background: transparent; }
106
245
 
107
- .status { color: var(--text-muted); font-size: 0.9rem; margin: 0.5rem 0 0; }
246
+ .status { color: var(--text-muted); font-size: 0.88rem; margin: 0.5rem 0 0; }
108
247
  .status.error { color: var(--danger); }
109
248
 
110
249
  .device-hint {
111
250
  color: var(--warn);
112
251
  background: rgba(245, 158, 11, 0.08);
113
252
  border: 1px solid rgba(245, 158, 11, 0.25);
114
- padding: 0.5rem 0.75rem;
115
- border-radius: 0.375rem;
253
+ padding: 0.55rem 0.8rem;
254
+ border-radius: 0.55rem;
116
255
  font-size: 0.85rem;
117
- margin-top: 0.75rem;
256
+ margin-top: 1rem;
118
257
  display: flex;
119
258
  align-items: center;
120
259
  gap: 0.5rem;
@@ -124,53 +263,93 @@ button.ghost { background: transparent; }
124
263
  /* ---- Active rooms picker --------------------------------------------- */
125
264
 
126
265
  .rooms-section {
127
- margin-top: 1.25rem;
266
+ margin-top: 1.5rem;
267
+ padding-top: 1.25rem;
128
268
  border-top: 1px solid var(--border);
129
- padding-top: 1rem;
130
269
  }
131
270
  .rooms-head {
132
271
  display: flex;
133
272
  align-items: center;
134
273
  justify-content: space-between;
135
- margin-bottom: 0.5rem;
274
+ margin-bottom: 0.75rem;
136
275
  }
137
276
  .rooms-head h2 {
138
277
  margin: 0;
139
- font-size: 0.95rem;
278
+ font-size: 0.72rem;
140
279
  color: var(--text-muted);
141
- font-weight: 600;
142
- letter-spacing: 0.01em;
280
+ font-weight: 700;
281
+ letter-spacing: 0.08em;
282
+ text-transform: uppercase;
143
283
  }
144
284
  .room-list {
145
285
  list-style: none;
146
286
  margin: 0;
147
287
  padding: 0;
148
- display: flex;
149
- flex-wrap: wrap;
150
- gap: 0.4rem;
288
+ display: grid;
289
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
290
+ gap: 0.55rem;
151
291
  }
152
292
  .room-list li { margin: 0; }
153
- .room-pill {
154
- display: inline-flex;
293
+ .room-card {
294
+ width: 100%;
295
+ display: grid;
296
+ grid-template-columns: auto 1fr auto;
155
297
  align-items: center;
156
- gap: 0.45rem;
157
- padding: 0.35rem 0.65rem;
158
- border-radius: 999px;
159
- background: #11151f;
298
+ gap: 0.7rem;
299
+ padding: 0.7rem 0.85rem;
300
+ border-radius: 0.7rem;
301
+ background: rgba(10, 11, 16, 0.5);
160
302
  border: 1px solid var(--border);
161
303
  color: var(--text);
162
- font-size: 0.85rem;
304
+ font-size: 0.88rem;
163
305
  cursor: pointer;
306
+ text-align: left;
307
+ transition: background .15s, border-color .15s, transform .05s;
308
+ }
309
+ .room-card:hover {
310
+ border-color: rgba(124, 92, 255, 0.5);
311
+ background: rgba(124, 92, 255, 0.06);
312
+ }
313
+ .room-card:hover .room-card-cta { color: var(--accent-2); }
314
+ .room-card.active {
315
+ border-color: transparent;
316
+ background: linear-gradient(135deg, rgba(124, 92, 255, 0.22), rgba(34, 211, 238, 0.18));
317
+ box-shadow: inset 0 0 0 1px rgba(124, 92, 255, 0.4);
318
+ }
319
+ .room-card-pips {
320
+ display: inline-flex;
321
+ align-items: center;
322
+ gap: 2px;
323
+ padding: 0.35rem 0.4rem;
324
+ border-radius: 0.4rem;
325
+ background: rgba(255, 255, 255, 0.03);
326
+ }
327
+ .room-card-pips .pip {
328
+ width: 6px; height: 6px;
329
+ border-radius: 50%;
330
+ display: inline-block;
331
+ }
332
+ .room-card-pips .pip-0 { background: var(--accent); }
333
+ .room-card-pips .pip-1 { background: var(--accent-2); }
334
+ .room-card-pips .pip-2 { background: var(--accent-3); }
335
+ .room-card-pips .pip-empty { background: var(--border-hi); }
336
+ .room-card-body { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
337
+ .room-card-name { font-weight: 700; letter-spacing: -0.01em; }
338
+ .room-card-count { color: var(--text-muted); font-size: 0.75rem; }
339
+ .room-card-cta {
340
+ font-size: 0.75rem;
341
+ color: var(--text-muted);
342
+ font-weight: 600;
343
+ letter-spacing: 0.02em;
344
+ transition: color .15s;
164
345
  }
165
- .room-pill:hover { border-color: var(--accent); }
166
- .room-pill.active { background: var(--accent); border-color: transparent; color: #fff; }
167
- .room-pill-name { font-weight: 600; }
168
- .room-pill-count { font-size: 0.75rem; color: var(--text-muted); }
169
- .room-pill.active .room-pill-count { color: rgba(255,255,255,0.85); }
170
346
  .room-list-empty {
171
347
  color: var(--text-muted);
172
- font-size: 0.85rem;
173
- margin: 0.25rem 0 0;
348
+ font-size: 0.88rem;
349
+ padding: 0.9rem 1rem;
350
+ border: 1px dashed var(--border);
351
+ border-radius: 0.7rem;
352
+ text-align: center;
174
353
  }
175
354
 
176
355
  /* ---- Room layout (in-call) -------------------------------------------- */
@@ -496,4 +675,8 @@ button.ghost { background: transparent; }
496
675
  .stage-main-6,
497
676
  .stage-main-9 { grid-template-columns: 1fr 1fr; grid-auto-rows: minmax(140px, 1fr); grid-template-rows: none; }
498
677
  .tile.thumb { width: 120px; }
678
+ .lobby-shell { padding: 1.5rem 0.75rem 2.5rem; }
679
+ .lobby { padding: 1.5rem 1.25rem; border-radius: 0.9rem; }
680
+ .join-form { grid-template-columns: 1fr; }
681
+ .join-form .join-btn { width: 100%; justify-content: center; }
499
682
  }
@@ -11,7 +11,20 @@
11
11
  </head>
12
12
  <body>
13
13
  <header class="topbar">
14
- <span class="brand">⚡ {{NAME}}</span>
14
+ <span class="brand">
15
+ <svg class="brand-mark" width="22" height="22" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
16
+ <defs>
17
+ <linearGradient id="zq-grad" x1="0" y1="0" x2="24" y2="24" gradientUnits="userSpaceOnUse">
18
+ <stop offset="0" stop-color="#7c5cff"/>
19
+ <stop offset="1" stop-color="#22d3ee"/>
20
+ </linearGradient>
21
+ </defs>
22
+ <rect x="2" y="4" width="20" height="16" rx="4" fill="url(#zq-grad)"/>
23
+ <path d="M9 9h6l-6 6h6" stroke="#0b0c10" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
24
+ <circle cx="19.5" cy="6.5" r="2.5" fill="#16a34a" stroke="#0b0c10" stroke-width="1.5"/>
25
+ </svg>
26
+ <span class="brand-text">{{NAME}}</span>
27
+ </span>
15
28
  <small>zQuery <span id="nav-version"></span> &middot; WebRTC demo</small>
16
29
  </header>
17
30
  <main>
@@ -184,7 +184,7 @@ async function main() {
184
184
 
185
185
  // ---- Listen ----
186
186
  app.listen(PORT, () => {
187
- console.log('\n WebRTC server → http://localhost:' + PORT);
187
+ console.log('\n WebRTC server → http://localhost:' + PORT);
188
188
  console.log(' • signaling ws://localhost:' + PORT + '/rtc');
189
189
  console.log(' • turn creds http://localhost:' + PORT + '/rtc/turn');
190
190
  if (JWT) {
Binary file
package/dist/zquery.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * zQuery (zeroQuery) v1.2.13
2
+ * zQuery (zeroQuery) v1.2.15
3
3
  * Lightweight Frontend Library
4
4
  * https://github.com/tonywied17/zero-query
5
5
  * (c) 2026 Anthony Wiedman - MIT License
@@ -1564,11 +1564,12 @@ class Peer {
1564
1564
  this.pc.onicecandidate = (event) => {
1565
1565
  if (this.closed) return;
1566
1566
  const candidate = event && event.candidate;
1567
- // End-of-candidates marker (null) -> always forward.
1568
- if (!candidate) {
1569
- this.signaling.send('ice', { target: this.id, candidate: null });
1570
- return;
1571
- }
1567
+ // End-of-candidates: the @zero-server/webrtc SignalingHub rejects
1568
+ // any ice frame whose `candidate` is not a non-empty string
1569
+ // (BAD_FRAME -> counts toward the protocol-error budget that can
1570
+ // disconnect the peer). Browsers infer end-of-candidates from
1571
+ // iceGatheringState anyway, so we simply drop the marker.
1572
+ if (!candidate) return;
1572
1573
  const cand = typeof candidate === 'string' ? candidate : candidate.candidate;
1573
1574
  if (!cand) return;
1574
1575
  // NOTE: do NOT filter mDNS (.local) candidates. Firefox emits them by
@@ -1673,12 +1674,17 @@ class Peer {
1673
1674
  }
1674
1675
  // Older zero-query clients relayed only the bare `a=candidate:` string,
1675
1676
  // which browsers reject with "missing values for both sdpMid and
1676
- // sdpMLineIndex". Newer clients send a full init dict - normalize both.
1677
+ // sdpMLineIndex". Pass sdpMLineIndex:0 and leave sdpMid null so
1678
+ // the browser binds the candidate to the first (and, under
1679
+ // bundlePolicy:'max-bundle', only) m-section. Passing sdpMid:''
1680
+ // is NOT equivalent to omitting it - the browser treats empty
1681
+ // string as a real mid and fails with "Failed to execute
1682
+ // 'addIceCandidate'" because no m-section has mid="".
1677
1683
  const init = (typeof candidate === 'string')
1678
- ? { candidate, sdpMid: '', sdpMLineIndex: 0 }
1684
+ ? { candidate, sdpMid: null, sdpMLineIndex: 0 }
1679
1685
  : {
1680
1686
  candidate: candidate.candidate,
1681
- sdpMid: candidate.sdpMid != null ? candidate.sdpMid : '',
1687
+ sdpMid: candidate.sdpMid != null ? candidate.sdpMid : null,
1682
1688
  sdpMLineIndex: candidate.sdpMLineIndex != null ? candidate.sdpMLineIndex : 0,
1683
1689
  usernameFragment: candidate.usernameFragment != null ? candidate.usernameFragment : undefined,
1684
1690
  };
@@ -10483,9 +10489,9 @@ $.TurnError = TurnError;
10483
10489
  $.E2eeError = E2eeError;
10484
10490
 
10485
10491
  // --- Meta ------------------------------------------------------------------
10486
- $.version = '1.2.13';
10492
+ $.version = '1.2.15';
10487
10493
  $.libSize = '~130 KB';
10488
- $.unitTests = {"passed":2348,"failed":0,"total":2534,"suites":620,"duration":8562,"ok":true};
10494
+ $.unitTests = {"passed":2348,"failed":0,"total":2534,"suites":620,"duration":7548,"ok":true};
10489
10495
  $.meta = {}; // populated at build time by CLI bundler
10490
10496
 
10491
10497
  // --- Environment detection -------------------------------------------------