zero-query 1.2.12 → 1.2.14

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
  });
@@ -660,64 +660,83 @@ $.component('video-room', {
660
660
  if (!hasShare) hint.push('screen sharing unavailable in this browser');
661
661
  const hintLine = hint.length ? `<div class="device-hint">${this._icon('alert', 14)}<span>${$.escapeHtml(hint.join(' · '))}</span></div>` : '';
662
662
 
663
+ const totalPeers = availableRooms.reduce((n, r) => n + (r.peerCount || 0), 0);
664
+ const liveBadge = `<span class="live-dot" aria-hidden="true"></span>
665
+ <span>${availableRooms.length} ${availableRooms.length === 1 ? 'room' : 'rooms'} · ${totalPeers} online</span>`;
666
+
663
667
  const roomsBlock = availableRooms.length > 0
664
668
  ? `<ul class="room-list">
665
669
  ${availableRooms.map((r) => {
666
- const active = r.name === roomName ? ' active' : '';
667
- const count = (r.peerCount === 1) ? '1 peer' : (r.peerCount + ' peers');
670
+ const active = r.name === roomName ? ' active' : '';
671
+ const isAlone = (r.peerCount || 0) <= 1;
672
+ const count = (r.peerCount === 1) ? '1 here' : ((r.peerCount || 0) + ' here');
673
+ const dots = Math.min(r.peerCount || 0, 4);
674
+ const dotsHtml = Array.from({ length: dots }).map((_, i) =>
675
+ `<span class="pip pip-${i % 3}"></span>`
676
+ ).join('') || '<span class="pip pip-empty"></span>';
668
677
  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>
678
+ <button type="button" class="room-card${active}" data-room="${$.escapeHtml(r.name)}" @click="pickRoom" ${connecting ? 'disabled' : ''}>
679
+ <span class="room-card-pips">${dotsHtml}</span>
680
+ <span class="room-card-body">
681
+ <span class="room-card-name">#${$.escapeHtml(r.name)}</span>
682
+ <span class="room-card-count">${count}</span>
683
+ </span>
684
+ <span class="room-card-cta">${isAlone ? 'be the first' : 'jump in'} →</span>
672
685
  </button>
673
686
  </li>`;
674
687
  }).join('')}
675
688
  </ul>`
676
- : `<p class="room-list-empty">${loadingRooms ? 'Loading\u2026' : 'No rooms yet \u2014 type a name below to start a new one.'}</p>`;
689
+ : `<div class="room-list-empty">
690
+ ${loadingRooms ? 'Looking for live rooms\u2026' : 'No live rooms right now. Type a name below and start one.'}
691
+ </div>`;
677
692
 
678
693
  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'}
694
+ <div class="lobby-shell">
695
+ <div class="lobby-bg" aria-hidden="true"></div>
696
+ <div class="lobby">
697
+ <div class="lobby-hero">
698
+ <span class="eyebrow">${liveBadge}</span>
699
+ <h1>Spin up a room. Share whatever.</h1>
700
+ <p class="lead">
701
+ A no-signup mesh call wired straight into
702
+ <code>$.webrtc.join()</code> on top of
703
+ <a href="https://github.com/tonywied17/zero-server" target="_blank" rel="noopener">zero-server</a>.
704
+ Drop a name, send the link to a friend, and you'll be on a peer-to-peer
705
+ video call in two clicks. Camera, mic, and screen share are <em>off</em>
706
+ until you say so.
707
+ </p>
708
+ </div>
709
+
710
+ <form class="join-form" @submit="join">
711
+ <label>
712
+ <span class="field-label">Room</span>
713
+ <span class="field-prefix">#</span>
714
+ <input class="has-prefix" type="text" value="${$.escapeHtml(roomName)}" @input="setRoom" placeholder="lobby" autocomplete="off" spellcheck="false" ${connecting ? 'disabled' : ''} />
715
+ </label>
716
+ <label>
717
+ <span class="field-label">Your name</span>
718
+ <input type="text" value="${$.escapeHtml(displayName)}" @input="setName" placeholder="how should peers see you?" autocomplete="off" ${connecting ? 'disabled' : ''} />
719
+ </label>
720
+ <button type="submit" class="primary join-btn" ${connecting ? 'disabled' : ''}>
721
+ ${connecting ? 'Connecting...' : 'Join room'}
713
722
  </button>
723
+ </form>
724
+
725
+ <div class="rooms-section">
726
+ <div class="rooms-head">
727
+ <h2>Live right now</h2>
728
+ <button type="button" class="ghost" @click="refreshRooms" ${loadingRooms ? 'disabled' : ''}>
729
+ ${loadingRooms ? 'Refreshing\u2026' : 'Refresh'}
730
+ </button>
731
+ </div>
732
+ ${roomsBlock}
714
733
  </div>
715
- ${roomsBlock}
734
+
735
+ ${hintLine}
736
+ <p class="status ${error ? 'error' : ''}">
737
+ ${error ? $.escapeHtml(error) : $.escapeHtml(status)}
738
+ </p>
716
739
  </div>
717
- ${hintLine}
718
- <p class="status ${error ? 'error' : ''}">
719
- ${error ? $.escapeHtml(error) : $.escapeHtml(status)}
720
- </p>
721
740
  </div>
722
741
  `;
723
742
  },
@@ -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.12
2
+ * zQuery (zeroQuery) v1.2.14
3
3
  * Lightweight Frontend Library
4
4
  * https://github.com/tonywied17/zero-query
5
5
  * (c) 2026 Anthony Wiedman - MIT License
@@ -1415,6 +1415,11 @@ class Peer {
1415
1415
  this._maxIceCandidates = options.maxIceCandidates || DEFAULT_MAX_ICE_CANDIDATES;
1416
1416
  this._sentCandidates = 0;
1417
1417
  this._sigUnsub = [];
1418
+ // Serialize incoming remote events (offer/answer/ice) so an ICE frame
1419
+ // can never call addIceCandidate() while a setRemoteDescription() is
1420
+ // still in flight - that race throws "The remote description was null"
1421
+ // and leaves the PeerConnection stuck in `have-local-offer` forever.
1422
+ this._opChain = Promise.resolve();
1418
1423
 
1419
1424
  this._attachPc();
1420
1425
  this._attachSignaling();
@@ -1605,7 +1610,9 @@ class Peer {
1605
1610
  const guard = (cb) => (msg) => {
1606
1611
  if (this.closed) return;
1607
1612
  if (!msg || msg.from !== this.id) return;
1608
- cb(msg);
1613
+ // Chain every remote event behind any prior in-flight op so SDP
1614
+ // and ICE never race against each other.
1615
+ this._opChain = this._opChain.then(() => cb(msg)).catch(() => {});
1609
1616
  };
1610
1617
  this._sigUnsub.push(
1611
1618
  this.signaling.on('offer', guard((m) => this._onRemoteDescription('offer', m.sdp))),
@@ -10476,9 +10483,9 @@ $.TurnError = TurnError;
10476
10483
  $.E2eeError = E2eeError;
10477
10484
 
10478
10485
  // --- Meta ------------------------------------------------------------------
10479
- $.version = '1.2.12';
10486
+ $.version = '1.2.14';
10480
10487
  $.libSize = '~130 KB';
10481
- $.unitTests = {"passed":2348,"failed":0,"total":2534,"suites":620,"duration":8873,"ok":true};
10488
+ $.unitTests = {"passed":2348,"failed":0,"total":2534,"suites":620,"duration":8121,"ok":true};
10482
10489
  $.meta = {}; // populated at build time by CLI bundler
10483
10490
 
10484
10491
  // --- Environment detection -------------------------------------------------