anentrypoint-design 0.0.144 → 0.0.145

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anentrypoint-design",
3
- "version": "0.0.144",
3
+ "version": "0.0.145",
4
4
  "description": "247420 design system SDK — webjsx + modified ripple-ui, single-file ESM bundle for reproducible use of the AnEntrypoint design.",
5
5
  "type": "module",
6
6
  "main": "./dist/247420.js",
@@ -237,6 +237,48 @@ export function VoiceStrip({ channelName, status, muted, deafened, onMute, onDea
237
237
  );
238
238
  }
239
239
 
240
+ export function MobileHeader({ title, onMenu, onMembers } = {}) {
241
+ return h('div', { class: 'cm-mobile-header', role: 'banner' },
242
+ h('button', {
243
+ class: 'cm-mh-btn', type: 'button', onclick: onMenu,
244
+ title: 'Menu', 'aria-label': 'open navigation menu'
245
+ }, '☰'),
246
+ h('span', { class: 'cm-mh-title' }, title || ''),
247
+ h('button', {
248
+ class: 'cm-mh-btn', type: 'button', onclick: onMembers,
249
+ title: 'Members', 'aria-label': 'show members'
250
+ }, '👥')
251
+ );
252
+ }
253
+
254
+ export function ReplyBar({ quotedMessage, quotedAuthor, onCancel } = {}) {
255
+ return h('div', { class: 'cm-reply-bar', role: 'status' },
256
+ h('span', { class: 'cm-rb-label' }, 'Replying to ',
257
+ h('strong', { class: 'cm-rb-author' }, quotedAuthor || 'unknown')
258
+ ),
259
+ h('span', { class: 'cm-rb-preview', title: quotedMessage || '' }, quotedMessage || ''),
260
+ h('button', {
261
+ class: 'cm-rb-cancel', type: 'button', onclick: onCancel,
262
+ title: 'Cancel reply', 'aria-label': 'cancel reply'
263
+ }, '✕')
264
+ );
265
+ }
266
+
267
+ export function Banner({ tone = 'info', message, visible, actionLabel, onAction, onClick } = {}) {
268
+ if (!visible || !message) return null;
269
+ return h('div', {
270
+ class: 'cm-banner tone-' + tone + (onClick ? ' clickable' : ''),
271
+ role: tone === 'error' || tone === 'warning' ? 'alert' : 'status',
272
+ onclick: onClick || null
273
+ },
274
+ h('span', { class: 'cm-banner-msg' }, message),
275
+ actionLabel ? h('button', {
276
+ class: 'cm-banner-action', type: 'button',
277
+ onclick: (e) => { e.stopPropagation(); onAction && onAction(e); }
278
+ }, actionLabel) : null
279
+ );
280
+ }
281
+
240
282
  export function CommunityShell({ serverRailProps, sidebarProps, children, memberListProps, voiceStripProps } = {}) {
241
283
  return h('div', { class: 'cm-shell' },
242
284
  serverRailProps ? ServerRail(serverRailProps) : null,
@@ -103,7 +103,17 @@ export function PromptDialog({ title = 'name', value = '', placeholder = '', con
103
103
  ),
104
104
  h('div', { class: 'ds-modal-actions' },
105
105
  Btn({ onClick: onCancel, children: cancelLabel }),
106
- h('button', { class: 'btn-primary', onclick: () => onConfirm && onConfirm(value) }, confirmLabel)
106
+ h('button', {
107
+ class: 'btn-primary',
108
+ // Read the live input value, not the closed-over `value` prop:
109
+ // consumers update their state in oninput without re-rendering
110
+ // (to avoid caret jump), so the prop is stale at click time.
111
+ onclick: (e) => {
112
+ if (!onConfirm) return;
113
+ const inp = e.currentTarget.closest('.ds-modal')?.querySelector('.ds-modal-input');
114
+ onConfirm(inp ? inp.value : value);
115
+ }
116
+ }, confirmLabel)
107
117
  )
108
118
  ]
109
119
  });
@@ -217,3 +217,248 @@ export function Dropdown({ trigger, items = [], onSelect, placement = 'bottom-st
217
217
  ? webjsx.createElement(child.type, { ...(child.props || {}), ref: refFn }, ...(child.children || []))
218
218
  : h('button', { type: 'button', class: 'ds-dropdown-trigger', ref: refFn }, child || 'Menu');
219
219
  }
220
+
221
+ // Clamp a fixed-position box to the viewport given desired top-left coords.
222
+ function _clampToViewport(x, y, w, h, margin = 8) {
223
+ const vw = (typeof window !== 'undefined' ? window.innerWidth : 1024);
224
+ const vh = (typeof window !== 'undefined' ? window.innerHeight : 768);
225
+ return {
226
+ left: Math.max(margin, Math.min(vw - w - margin, x)),
227
+ top: Math.max(margin, Math.min(vh - h - margin, y)),
228
+ };
229
+ }
230
+
231
+ // CommandPalette — centered Cmd+K palette with live filter + keyboard nav.
232
+ export function CommandPalette({ open, items = [], onSelect, onClose } = {}) {
233
+ if (!open) return null;
234
+ const list = Array.isArray(items) ? items : [];
235
+ const labelOf = (it) => String(it.label || it.title || it.name || '');
236
+ let active = 0, filterText = '';
237
+
238
+ const matches = () => {
239
+ const q = filterText.trim().toLowerCase();
240
+ return q ? list.filter(it => labelOf(it).toLowerCase().includes(q)) : list.slice();
241
+ };
242
+
243
+ const rowsFor = (filtered) => {
244
+ const out = [];
245
+ let lastGroup = null, flatIdx = 0;
246
+ for (const it of filtered) {
247
+ const grp = it.group != null ? String(it.group) : null;
248
+ if (grp && grp !== lastGroup) {
249
+ out.push(h('div', { class: 'ov-cmd-group', role: 'presentation' }, grp));
250
+ lastGroup = grp;
251
+ }
252
+ const idx = flatIdx++;
253
+ const glyph = it.icon != null ? it.icon : (it.glyph != null ? it.glyph : null);
254
+ const hint = it.hint != null ? it.hint : (it.shortcut != null ? it.shortcut : null);
255
+ out.push(h('button', {
256
+ type: 'button', role: 'option',
257
+ 'data-idx': String(idx),
258
+ 'aria-selected': idx === active ? 'true' : 'false',
259
+ class: 'ov-cmd-item' + (idx === active ? ' is-active' : ''),
260
+ onclick: () => choose(it),
261
+ onmousemove: () => { if (active !== idx) { active = idx; renderInner(); } },
262
+ },
263
+ glyph != null ? h('span', { class: 'ov-cmd-glyph', 'aria-hidden': 'true' }, glyph) : null,
264
+ h('span', { class: 'ov-cmd-label' }, labelOf(it)),
265
+ hint != null ? h('span', { class: 'ov-cmd-hint' }, hint) : null
266
+ ));
267
+ }
268
+ return out;
269
+ };
270
+
271
+ let rootEl = null, inputEl = null, listEl = null, flat = [];
272
+ const close = () => onClose && onClose();
273
+ const choose = (it) => { if (it && onSelect) onSelect(it); };
274
+
275
+ const renderInner = () => {
276
+ if (!listEl) return;
277
+ const filtered = matches();
278
+ flat = filtered;
279
+ if (active >= filtered.length) active = Math.max(0, filtered.length - 1);
280
+ webjsx.applyDiff(listEl, h('div', { class: 'ov-cmd-list-inner' },
281
+ filtered.length ? rowsFor(filtered) : h('div', { class: 'ov-cmd-empty' }, 'No results')));
282
+ const sel = listEl.querySelector('.ov-cmd-item.is-active');
283
+ if (sel && sel.scrollIntoView) sel.scrollIntoView({ block: 'nearest' });
284
+ };
285
+
286
+ const onKey = (e) => {
287
+ if (e.key === 'Escape') { e.preventDefault(); close(); }
288
+ else if (e.key === 'ArrowDown') { e.preventDefault(); if (flat.length) { active = (active + 1) % flat.length; renderInner(); } }
289
+ else if (e.key === 'ArrowUp') { e.preventDefault(); if (flat.length) { active = (active - 1 + flat.length) % flat.length; renderInner(); } }
290
+ else if (e.key === 'Enter') { e.preventDefault(); if (flat[active]) choose(flat[active]); }
291
+ };
292
+
293
+ return h('div', {
294
+ class: 'ov-cmd-backdrop', role: 'presentation',
295
+ ref: (el) => {
296
+ if (!el || el._ovCmd) return; el._ovCmd = true; rootEl = el;
297
+ el.addEventListener('mousedown', (e) => {
298
+ const panel = el.querySelector('.ov-cmd-panel');
299
+ if (panel && !panel.contains(e.target)) close();
300
+ });
301
+ },
302
+ },
303
+ h('div', { class: 'ov-cmd-panel', role: 'dialog', 'aria-label': 'Command palette', onkeydown: onKey },
304
+ h('input', {
305
+ type: 'text', class: 'ov-cmd-input', placeholder: 'Type a command…',
306
+ 'aria-label': 'Filter commands',
307
+ oninput: (e) => { filterText = e.target.value; active = 0; renderInner(); },
308
+ ref: (el) => { if (!el || el._ovCmdIn) return; el._ovCmdIn = true; inputEl = el; queueMicrotask(() => el.focus()); },
309
+ }),
310
+ h('div', { class: 'ov-cmd-list', role: 'listbox',
311
+ ref: (el) => { if (!el) return; listEl = el; queueMicrotask(renderInner); } })
312
+ )
313
+ );
314
+ }
315
+
316
+ const EMOJI_CATEGORIES = [
317
+ { id: 'smileys', label: '😀', emoji: ['😀','😁','😂','🤣','😊','😍','😘','😎','🤔','😅','😉','🙂','😇','🥳','😴','🤩','😜','😢','😭','😡','😱','🥺','😤','😬'] },
318
+ { id: 'gestures', label: '👍', emoji: ['👍','👎','👌','✌️','🤞','🙏','👏','🙌','💪','👀','🤝','✋','🤙','👋','🤟','☝️'] },
319
+ { id: 'hearts', label: '❤️', emoji: ['❤️','🧡','💛','💚','💙','💜','🖤','🤍','💔','💕','💖','💗'] },
320
+ { id: 'symbols', label: '✅', emoji: ['🔥','💯','✅','❌','⭐','🎉','🎊','✨','💡','⚡','💢','💀','🚀','🏆'] },
321
+ ];
322
+
323
+ // EmojiPicker — fixed popover near (anchorX, anchorY) with category tabs + grid.
324
+ export function EmojiPicker({ open, anchorX = 0, anchorY = 0, onSelect, onClose } = {}) {
325
+ if (!open) return null;
326
+ let cat = EMOJI_CATEGORIES[0].id;
327
+ let rootEl = null, gridEl = null;
328
+ const close = () => onClose && onClose();
329
+
330
+ const renderGrid = () => {
331
+ if (!gridEl) return;
332
+ const c = EMOJI_CATEGORIES.find(x => x.id === cat) || EMOJI_CATEGORIES[0];
333
+ webjsx.applyDiff(gridEl, h('div', { class: 'ov-emoji-grid-inner' },
334
+ ...c.emoji.map((ch) => h('button', {
335
+ type: 'button', class: 'ov-emoji-cell', 'aria-label': ch,
336
+ onclick: () => { if (onSelect) onSelect(ch); },
337
+ }, ch))));
338
+ };
339
+
340
+ return h('div', {
341
+ class: 'ov-emoji-root', role: 'dialog', 'aria-label': 'Emoji picker',
342
+ tabindex: '-1',
343
+ onkeydown: (e) => { if (e.key === 'Escape') { e.preventDefault(); close(); } },
344
+ ref: (el) => {
345
+ if (!el || el._ovEmoji) return; el._ovEmoji = true; rootEl = el;
346
+ const place = () => {
347
+ const r = el.getBoundingClientRect();
348
+ const { left, top } = _clampToViewport(anchorX, anchorY, r.width || 260, r.height || 240);
349
+ el.style.left = left + 'px'; el.style.top = top + 'px';
350
+ };
351
+ queueMicrotask(() => { place(); el.focus(); });
352
+ const onDown = (e) => { if (!el.contains(e.target)) close(); };
353
+ queueMicrotask(() => document.addEventListener('mousedown', onDown, true));
354
+ el._ovEmojiCleanup = () => document.removeEventListener('mousedown', onDown, true);
355
+ },
356
+ },
357
+ h('div', { class: 'ov-emoji-tabs', role: 'tablist' },
358
+ ...EMOJI_CATEGORIES.map((c) => h('button', {
359
+ type: 'button', class: 'ov-emoji-tab', role: 'tab',
360
+ 'aria-selected': c.id === cat ? 'true' : 'false',
361
+ onclick: (e) => {
362
+ cat = c.id;
363
+ const tabs = rootEl.querySelectorAll('.ov-emoji-tab');
364
+ tabs.forEach(t => t.setAttribute('aria-selected', 'false'));
365
+ e.currentTarget.setAttribute('aria-selected', 'true');
366
+ renderGrid();
367
+ },
368
+ }, c.label))),
369
+ h('div', { class: 'ov-emoji-grid',
370
+ ref: (el) => { if (!el) return; gridEl = el; queueMicrotask(renderGrid); } })
371
+ );
372
+ }
373
+
374
+ // BootOverlay — full-screen brand/progress overlay with error state.
375
+ export function BootOverlay({ progress = 0, phase = '', errored = false, visible = false } = {}) {
376
+ if (!visible) return null;
377
+ let pct = Number(progress) || 0;
378
+ if (pct <= 1) pct = pct * 100;
379
+ pct = Math.max(0, Math.min(100, pct));
380
+ return h('div', { class: 'ov-boot' + (errored ? ' is-error' : ''), role: errored ? 'alert' : 'status', 'aria-live': 'polite' },
381
+ h('div', { class: 'ov-boot-inner' },
382
+ errored
383
+ ? h('div', { class: 'ov-boot-mark ov-boot-mark-error', 'aria-hidden': 'true' }, '⚠')
384
+ : h('div', { class: 'ov-boot-spinner', 'aria-hidden': 'true' }),
385
+ !errored ? h('div', { class: 'ov-boot-bar', role: 'progressbar',
386
+ 'aria-valuenow': String(Math.round(pct)), 'aria-valuemin': '0', 'aria-valuemax': '100' },
387
+ h('div', { class: 'ov-boot-bar-fill', style: 'width:' + pct + '%' })) : null,
388
+ h('div', { class: 'ov-boot-phase' }, String(phase || (errored ? 'Error' : 'Loading…')))
389
+ )
390
+ );
391
+ }
392
+
393
+ // SettingsPopover — fixed popover with generic section/row control rendering.
394
+ export function SettingsPopover({ title = 'Settings', open, anchorX = 0, anchorY = 0, sections = [], onClose } = {}) {
395
+ if (!open) return null;
396
+ const close = () => onClose && onClose();
397
+ const secs = Array.isArray(sections) ? sections : [];
398
+
399
+ const renderRow = (row, i) => {
400
+ const label = row.label != null ? row.label : (row.title != null ? row.title : '');
401
+ const kind = row.kind;
402
+ const labelNode = h('span', { class: 'ov-set-row-label' }, String(label));
403
+ let control = null;
404
+ if (kind === 'select') {
405
+ const opts = Array.isArray(row.options) ? row.options : [];
406
+ control = h('select', {
407
+ class: 'ov-set-control', value: row.value != null ? String(row.value) : undefined,
408
+ onchange: (e) => row.onChange && row.onChange(e.target.value),
409
+ }, ...opts.map(o => {
410
+ const v = (o && typeof o === 'object') ? o.value : o;
411
+ const l = (o && typeof o === 'object') ? (o.label != null ? o.label : o.value) : o;
412
+ return h('option', { value: String(v), selected: String(v) === String(row.value) ? 'selected' : undefined }, String(l));
413
+ }));
414
+ } else if (kind === 'toggle') {
415
+ control = h('input', {
416
+ type: 'checkbox', class: 'ov-set-toggle',
417
+ checked: row.value ? 'checked' : undefined,
418
+ onchange: (e) => row.onChange && row.onChange(e.target.checked),
419
+ });
420
+ } else if (kind === 'range') {
421
+ control = h('input', {
422
+ type: 'range', class: 'ov-set-control',
423
+ min: String(row.min != null ? row.min : 0),
424
+ max: String(row.max != null ? row.max : 100),
425
+ step: String(row.step != null ? row.step : 1),
426
+ value: String(row.value != null ? row.value : 0),
427
+ oninput: (e) => row.onChange && row.onChange(Number(e.target.value)),
428
+ });
429
+ } else if (kind === 'button') {
430
+ control = h('button', { type: 'button', class: 'ov-set-btn',
431
+ onclick: () => row.onClick && row.onClick() }, String(label || 'Action'));
432
+ return h('div', { class: 'ov-set-row', key: i }, control);
433
+ } else {
434
+ control = h('span', { class: 'ov-set-row-value' }, String(row.value != null ? row.value : ''));
435
+ }
436
+ return h('div', { class: 'ov-set-row', key: i }, labelNode, control);
437
+ };
438
+
439
+ return h('div', {
440
+ class: 'ov-set-root', role: 'dialog', 'aria-label': String(title), tabindex: '-1',
441
+ onkeydown: (e) => { if (e.key === 'Escape') { e.preventDefault(); close(); } },
442
+ ref: (el) => {
443
+ if (!el || el._ovSet) return; el._ovSet = true;
444
+ const place = () => {
445
+ const r = el.getBoundingClientRect();
446
+ const { left, top } = _clampToViewport(anchorX, anchorY, r.width || 280, r.height || 200);
447
+ el.style.left = left + 'px'; el.style.top = top + 'px';
448
+ };
449
+ queueMicrotask(() => { place(); el.focus(); });
450
+ const onDown = (e) => { if (!el.contains(e.target)) close(); };
451
+ queueMicrotask(() => document.addEventListener('mousedown', onDown, true));
452
+ },
453
+ },
454
+ h('div', { class: 'ov-set-head' }, String(title)),
455
+ h('div', { class: 'ov-set-body' },
456
+ ...secs.map((sec, si) => {
457
+ const slabel = sec.label != null ? sec.label : (sec.title != null ? sec.title : '');
458
+ const rows = Array.isArray(sec.rows) ? sec.rows : (Array.isArray(sec.items) ? sec.items : []);
459
+ return h('div', { class: 'ov-set-section', key: si },
460
+ slabel ? h('div', { class: 'ov-set-section-head' }, String(slabel)) : null,
461
+ ...rows.map((r, ri) => renderRow(r, ri)));
462
+ }))
463
+ );
464
+ }
@@ -0,0 +1,228 @@
1
+ // Voice surfaces — PTT, VAD meter, webcam preview, voice settings, audio queue.
2
+ // Pure factories returning webjsx vnodes. Class prefix: vx-*.
3
+
4
+ import * as webjsx from '../../vendor/webjsx/index.js';
5
+ const h = webjsx.createElement;
6
+
7
+ function fmtDur(s) {
8
+ s = Math.max(0, Math.round(Number(s) || 0));
9
+ if (s < 60) return s + 's';
10
+ const m = Math.floor(s / 60);
11
+ const r = s % 60;
12
+ return m + ':' + String(r).padStart(2, '0');
13
+ }
14
+
15
+ export function PttButton({ state = 'idle', mode = 'ptt', onHoldStart, onHoldEnd, onClick, label = 'Hold to talk' } = {}) {
16
+ const active = state === 'live' || state === 'recording' || state === 'vad';
17
+ const start = (e) => { onHoldStart && onHoldStart(e); };
18
+ const end = (e) => { onHoldEnd && onHoldEnd(e); };
19
+ return h('button', {
20
+ type: 'button',
21
+ class: 'vx-ptt vx-ptt-' + state + ' vx-ptt-mode-' + mode,
22
+ 'data-state': state,
23
+ 'data-mode': mode,
24
+ 'aria-pressed': active ? 'true' : 'false',
25
+ 'aria-label': label,
26
+ onclick: onClick ? (e) => onClick(e) : null,
27
+ onpointerdown: (e) => { e.preventDefault(); start(e); },
28
+ onpointerup: (e) => { e.preventDefault(); end(e); },
29
+ onpointerleave: (e) => end(e),
30
+ oncontextmenu: (e) => e.preventDefault(),
31
+ ontouchstart: (e) => start(e),
32
+ ontouchend: (e) => { e.preventDefault(); end(e); }
33
+ },
34
+ h('span', { class: 'vx-ptt-glow', 'aria-hidden': 'true' }),
35
+ h('span', { class: 'vx-ptt-icon', 'aria-hidden': 'true' }, state === 'idle' ? '🎙' : '●'),
36
+ h('span', { class: 'vx-ptt-label' }, label)
37
+ );
38
+ }
39
+
40
+ export function VadMeter({ level = 0, threshold = 0.5, onThresholdChange } = {}) {
41
+ const lvl = Math.max(0, Math.min(1, Number(level) || 0));
42
+ const thr = Math.max(0, Math.min(1, Number(threshold) || 0));
43
+ const over = lvl >= thr;
44
+ return h('div', { class: 'vx-vad', role: 'group', 'aria-label': 'voice activity meter' },
45
+ h('div', { class: 'vx-vad-track' },
46
+ h('div', { class: 'vx-vad-fill' + (over ? ' vx-vad-fill-over' : ''), style: 'width:' + (lvl * 100).toFixed(1) + '%' }),
47
+ h('div', { class: 'vx-vad-marker', style: 'left:' + (thr * 100).toFixed(1) + '%', 'aria-hidden': 'true' }),
48
+ h('input', {
49
+ class: 'vx-vad-range',
50
+ type: 'range', min: '0', max: '1', step: '0.01',
51
+ value: String(thr),
52
+ 'aria-label': 'VAD threshold',
53
+ oninput: onThresholdChange ? (e) => onThresholdChange(parseFloat(e.target.value)) : null
54
+ })
55
+ ),
56
+ h('div', { class: 'vx-vad-readout' },
57
+ h('span', {}, 'lvl ' + Math.round(lvl * 100)),
58
+ h('span', {}, 'thr ' + Math.round(thr * 100))
59
+ )
60
+ );
61
+ }
62
+
63
+ export function WebcamPreview({ videoStream = null, resolution = '640x480', fps = 30, enabled = true, resolutions = [], fpsOptions = [], onResolutionChange, onFpsChange, onToggle } = {}) {
64
+ const videoRef = (el) => {
65
+ if (!el) return;
66
+ if (el.srcObject !== videoStream) el.srcObject = videoStream || null;
67
+ };
68
+ const resOpts = (resolutions.length ? resolutions : [resolution]).map(r =>
69
+ h('option', { key: 'r-' + r, value: r, selected: r === resolution }, r));
70
+ const fpsList = fpsOptions.length ? fpsOptions : [fps];
71
+ const fpsOpts = fpsList.map(f =>
72
+ h('option', { key: 'f-' + f, value: String(f), selected: Number(f) === Number(fps) }, f + ' fps'));
73
+ return h('div', { class: 'vx-cam' + (enabled ? '' : ' vx-cam-off') },
74
+ h('div', { class: 'vx-cam-stage' },
75
+ enabled
76
+ ? h('video', { class: 'vx-cam-video', ref: videoRef, autoplay: true, muted: true, playsinline: true })
77
+ : h('div', { class: 'vx-cam-placeholder' }, h('span', {}, '📷'), h('span', {}, 'Camera off'))
78
+ ),
79
+ h('div', { class: 'vx-cam-controls' },
80
+ h('select', {
81
+ class: 'vx-select', 'aria-label': 'resolution',
82
+ onchange: onResolutionChange ? (e) => onResolutionChange(e.target.value) : null
83
+ }, ...resOpts),
84
+ h('select', {
85
+ class: 'vx-select', 'aria-label': 'frame rate',
86
+ onchange: onFpsChange ? (e) => onFpsChange(Number(e.target.value)) : null
87
+ }, ...fpsOpts),
88
+ h('button', {
89
+ type: 'button',
90
+ class: 'vx-btn' + (enabled ? ' vx-btn-on' : ''),
91
+ 'aria-pressed': enabled ? 'true' : 'false',
92
+ onclick: onToggle ? () => onToggle() : null
93
+ }, enabled ? 'Disable' : 'Enable')
94
+ )
95
+ );
96
+ }
97
+
98
+ function seg({ label, children, className = '' }) {
99
+ return h('div', { class: 'vx-section ' + className },
100
+ label != null ? h('div', { class: 'vx-section-label' }, label) : null,
101
+ ...(Array.isArray(children) ? children : [children])
102
+ );
103
+ }
104
+
105
+ function devSelect(value, devices, onChange, aria) {
106
+ return h('select', {
107
+ class: 'vx-select', 'aria-label': aria,
108
+ onchange: onChange ? (e) => onChange(e.target.value) : null
109
+ }, ...(devices || []).map(d =>
110
+ h('option', { key: 'd-' + d.value, value: d.value, selected: d.value === value }, d.label)));
111
+ }
112
+
113
+ function toggleRow(label, checked, onToggle) {
114
+ return h('label', { class: 'vx-toggle-row' },
115
+ h('span', {}, label),
116
+ h('input', {
117
+ type: 'checkbox', class: 'vx-toggle',
118
+ checked: checked ? true : null,
119
+ onchange: onToggle ? (e) => onToggle(e.target.checked) : null
120
+ })
121
+ );
122
+ }
123
+
124
+ export function VoiceSettingsModal({ open = false, mode = 'ptt', inputId, outputId, inputDevices = [], outputDevices = [], vadThreshold = 0.5, rnnoise = false, autoGain = false, forceTurn = false, bitrate = 64, volume, onChange, onSave, onCancel, onClose } = {}) {
125
+ if (!open) return null;
126
+ const patch = (p) => onChange && onChange(p);
127
+ const modes = ['ptt', 'vad', 'live'];
128
+ const vol = volume == null ? 1 : volume;
129
+ return h('div', {
130
+ class: 'vx-modal-backdrop',
131
+ onclick: (e) => { if (e.target === e.currentTarget) onClose && onClose(); },
132
+ onkeydown: (e) => { if (e.key === 'Escape') { e.preventDefault(); onClose && onClose(); } }
133
+ },
134
+ h('div', { class: 'vx-modal', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Voice settings' },
135
+ h('div', { class: 'vx-modal-head' },
136
+ h('h2', { class: 'vx-modal-title' }, 'Voice settings'),
137
+ h('button', { type: 'button', class: 'vx-modal-x', 'aria-label': 'close', onclick: () => onClose && onClose() }, '×')
138
+ ),
139
+ h('div', { class: 'vx-modal-body' },
140
+ seg({ label: 'Mode', children:
141
+ h('div', { class: 'vx-segmented', role: 'group', 'aria-label': 'mode' },
142
+ ...modes.map(m => h('button', {
143
+ key: 'm-' + m, type: 'button',
144
+ class: 'vx-seg' + (m === mode ? ' vx-seg-on' : ''),
145
+ 'aria-pressed': m === mode ? 'true' : 'false',
146
+ onclick: () => patch({ mode: m })
147
+ }, m.toUpperCase())))
148
+ }),
149
+ seg({ label: 'Input device', children: devSelect(inputId, inputDevices, (v) => patch({ inputId: v }), 'input device') }),
150
+ seg({ label: 'Output device', children: devSelect(outputId, outputDevices, (v) => patch({ outputId: v }), 'output device') }),
151
+ mode === 'vad' ? seg({ label: 'VAD threshold', children:
152
+ h('div', { class: 'vx-range-row' },
153
+ h('input', {
154
+ type: 'range', class: 'vx-range', min: '0', max: '1', step: '0.01',
155
+ value: String(vadThreshold), 'aria-label': 'VAD threshold',
156
+ oninput: (e) => patch({ vadThreshold: parseFloat(e.target.value) })
157
+ }),
158
+ h('span', { class: 'vx-range-val' }, Math.round((Number(vadThreshold) || 0) * 100) + '%')
159
+ )
160
+ }) : null,
161
+ seg({ label: 'Processing', children: [
162
+ toggleRow('RNNoise', rnnoise, (v) => patch({ rnnoise: v })),
163
+ toggleRow('Auto gain', autoGain, (v) => patch({ autoGain: v })),
164
+ toggleRow('Force TURN', forceTurn, (v) => patch({ forceTurn: v }))
165
+ ]}),
166
+ seg({ label: 'Bitrate', children:
167
+ h('div', { class: 'vx-range-row' },
168
+ h('input', {
169
+ type: 'range', class: 'vx-range', min: '8', max: '256', step: '8',
170
+ value: String(bitrate), 'aria-label': 'bitrate',
171
+ oninput: (e) => patch({ bitrate: parseInt(e.target.value, 10) })
172
+ }),
173
+ h('span', { class: 'vx-range-val' }, (Number(bitrate) || 0) + ' kbps')
174
+ )
175
+ }),
176
+ seg({ label: 'Master volume', children:
177
+ h('div', { class: 'vx-range-row' },
178
+ h('input', {
179
+ type: 'range', class: 'vx-range', min: '0', max: '1', step: '0.01',
180
+ value: String(vol), 'aria-label': 'master volume',
181
+ oninput: (e) => patch({ volume: parseFloat(e.target.value) })
182
+ }),
183
+ h('span', { class: 'vx-range-val' }, Math.round(vol * 100) + '%')
184
+ )
185
+ })
186
+ ),
187
+ h('div', { class: 'vx-modal-foot' },
188
+ h('button', { type: 'button', class: 'vx-btn', onclick: () => onCancel && onCancel() }, 'Cancel'),
189
+ h('button', { type: 'button', class: 'vx-btn vx-btn-primary', onclick: () => onSave && onSave() }, 'Save')
190
+ )
191
+ )
192
+ );
193
+ }
194
+
195
+ export function AudioQueue({ segments = [], currentSegmentId = null, paused = false, onReplay, onSkip, onResume, onPause } = {}) {
196
+ if (!segments || !segments.length) {
197
+ return h('div', { class: 'vx-queue vx-queue-empty' },
198
+ h('span', { class: 'vx-queue-empty-text' }, 'No audio queued'));
199
+ }
200
+ return h('div', { class: 'vx-queue', role: 'group', 'aria-label': 'audio queue' },
201
+ h('div', { class: 'vx-queue-ctrls' },
202
+ h('button', {
203
+ type: 'button', class: 'vx-queue-btn',
204
+ 'aria-label': paused ? 'resume' : 'pause',
205
+ onclick: () => paused ? (onResume && onResume()) : (onPause && onPause())
206
+ }, paused ? '▶' : '⏸'),
207
+ h('button', {
208
+ type: 'button', class: 'vx-queue-btn',
209
+ 'aria-label': 'skip', onclick: () => onSkip && onSkip()
210
+ }, '⏭')
211
+ ),
212
+ h('div', { class: 'vx-queue-strip' },
213
+ ...segments.map(s => h('button', {
214
+ key: 'q-' + s.id,
215
+ type: 'button',
216
+ class: 'vx-chip' + (s.id === currentSegmentId ? ' vx-chip-current' : '') + (s.isLive ? ' vx-chip-live' : ''),
217
+ 'data-id': s.id,
218
+ onclick: () => onReplay && onReplay(s.id)
219
+ },
220
+ h('span', { class: 'vx-chip-dot', style: s.color ? 'background:' + s.color : null, 'aria-hidden': 'true' }),
221
+ h('span', { class: 'vx-chip-name' }, s.speaker || '—'),
222
+ s.isLive
223
+ ? h('span', { class: 'vx-chip-tag' }, 'LIVE')
224
+ : h('span', { class: 'vx-chip-dur' }, fmtDur(s.duration))
225
+ ))
226
+ )
227
+ );
228
+ }
package/src/components.js CHANGED
@@ -40,9 +40,14 @@ export {
40
40
  ChannelItem, ChannelCategory,
41
41
  VoiceUser, UserPanel, ChannelSidebar,
42
42
  MemberItem, MemberList,
43
- ChatHeader, VoiceStrip, CommunityShell
43
+ ChatHeader, VoiceStrip, CommunityShell,
44
+ MobileHeader, ReplyBar, Banner
44
45
  } from './components/community.js';
45
46
 
47
+ export {
48
+ PttButton, VadMeter, WebcamPreview, VoiceSettingsModal, AudioQueue
49
+ } from './components/voice.js';
50
+
46
51
  export { ThemeToggle } from './components/theme-toggle.js';
47
52
 
48
53
  export {
@@ -69,7 +74,8 @@ export {
69
74
  } from './components/editor-primitives.js';
70
75
 
71
76
  export {
72
- Tooltip, Popover, Dropdown, useLongPress, useFloating
77
+ Tooltip, Popover, Dropdown, useLongPress, useFloating,
78
+ CommandPalette, EmojiPicker, BootOverlay, SettingsPopover
73
79
  } from './components/overlay-primitives.js';
74
80
 
75
81
  export {
@@ -34,7 +34,7 @@ export function renderAboutApp(opts = {}) {
34
34
  ul.appendChild(li);
35
35
  }
36
36
  const foot = document.createElement('p');
37
- foot.innerHTML = footer;
37
+ foot.textContent = footer;
38
38
  const meta = document.createElement('p');
39
39
  meta.className = 'meta';
40
40
  links.forEach((l, i) => {