anentrypoint-design 0.0.121 → 0.0.122

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.121",
3
+ "version": "0.0.122",
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",
@@ -0,0 +1,399 @@
1
+ // Surfaces — thread panel, forum view, page view, auth modal, settings popover, voice settings modal.
2
+ // Pure factories: props → vnode. Render nothing when modals are not open.
3
+
4
+ import * as webjsx from '../../vendor/webjsx/index.js';
5
+ const h = webjsx.createElement;
6
+
7
+ function timeAgo(t) {
8
+ if (!t) return '';
9
+ const d = typeof t === 'number' ? t : Date.parse(t);
10
+ if (!Number.isFinite(d)) return String(t);
11
+ const s = Math.max(0, Math.floor((Date.now() - d) / 1000));
12
+ if (s < 60) return s + 's';
13
+ if (s < 3600) return Math.floor(s / 60) + 'm';
14
+ if (s < 86400) return Math.floor(s / 3600) + 'h';
15
+ return Math.floor(s / 86400) + 'd';
16
+ }
17
+
18
+ export function ThreadPanel({ threads = [], activeId, onSelect, onCreate, onClose, open, title = 'Threads' } = {}) {
19
+ if (open !== true) return null;
20
+ return h('aside', { class: 'cm-thread-panel', role: 'complementary' },
21
+ h('div', { class: 'cm-tp-header' },
22
+ h('span', { class: 'cm-tp-title' }, title),
23
+ h('div', { class: 'cm-tp-header-actions' },
24
+ onCreate ? h('button', { class: 'cm-tp-new', onclick: onCreate, title: 'New thread' }, '+') : null,
25
+ onClose ? h('button', { class: 'cm-tp-close', onclick: onClose, title: 'Close' }, '✕') : null
26
+ )
27
+ ),
28
+ h('div', { class: 'cm-tp-list' },
29
+ ...threads.map(t => h('div', {
30
+ class: 'cm-tp-item' + (t.id === activeId ? ' active' : '') + (t.unread ? ' unread' : ''),
31
+ onclick: () => onSelect && onSelect(t.id),
32
+ 'data-id': t.id
33
+ },
34
+ h('div', { class: 'cm-tp-item-row' },
35
+ h('span', { class: 'cm-tp-item-title' }, t.title || '(untitled)'),
36
+ h('span', { class: 'cm-tp-item-time' }, timeAgo(t.time))
37
+ ),
38
+ t.lastMessage ? h('div', { class: 'cm-tp-item-msg' },
39
+ t.author ? h('span', { class: 'cm-tp-item-author' }, t.author + ': ') : null,
40
+ t.lastMessage
41
+ ) : null,
42
+ t.unread ? h('span', { class: 'cm-tp-item-dot' }) : null
43
+ ))
44
+ )
45
+ );
46
+ }
47
+
48
+ export function ForumView({ posts = [], onSelect, onSearch, onNewPost, query, sortBy = 'recent', onSort } = {}) {
49
+ const sorts = [
50
+ { id: 'recent', label: 'Recent' },
51
+ { id: 'popular', label: 'Popular' },
52
+ { id: 'unanswered', label: 'Unanswered' }
53
+ ];
54
+ return h('div', { class: 'cm-forum-view' },
55
+ h('div', { class: 'cm-fv-toolbar' },
56
+ h('input', {
57
+ class: 'cm-fv-search',
58
+ type: 'search',
59
+ placeholder: 'Search posts…',
60
+ value: query || '',
61
+ oninput: (e) => onSearch && onSearch(e.target.value)
62
+ }),
63
+ h('select', {
64
+ class: 'cm-fv-sort',
65
+ value: sortBy,
66
+ onchange: (e) => onSort && onSort(e.target.value)
67
+ }, ...sorts.map(s => h('option', { value: s.id, selected: s.id === sortBy ? 'selected' : null }, s.label))),
68
+ onNewPost ? h('button', { class: 'cm-fv-new', onclick: onNewPost, title: 'New post' }, '+ New') : null
69
+ ),
70
+ h('div', { class: 'cm-fv-list' },
71
+ ...posts.map(p => h('div', {
72
+ class: 'cm-fv-post',
73
+ onclick: () => onSelect && onSelect(p.id),
74
+ 'data-id': p.id
75
+ },
76
+ h('div', { class: 'cm-fv-post-head' },
77
+ h('span', { class: 'cm-fv-post-title' }, p.title || '(untitled)'),
78
+ h('span', { class: 'cm-fv-post-meta' },
79
+ p.author ? h('span', { class: 'cm-fv-post-author' }, p.author) : null,
80
+ h('span', { class: 'cm-fv-post-time' }, timeAgo(p.time)),
81
+ h('span', { class: 'cm-fv-post-replies' }, (p.replyCount || 0) + ' replies')
82
+ )
83
+ ),
84
+ p.snippet ? h('div', { class: 'cm-fv-post-snippet' }, p.snippet) : null,
85
+ p.tags && p.tags.length ? h('div', { class: 'cm-fv-post-tags' },
86
+ ...p.tags.map(tg => h('span', { class: 'cm-fv-post-tag' }, tg))
87
+ ) : null
88
+ ))
89
+ )
90
+ );
91
+ }
92
+
93
+ export function PageView({ content, html, title, onEdit, isAdmin } = {}) {
94
+ const bodyAttrs = { class: 'cm-pv-content' };
95
+ if (html != null) {
96
+ bodyAttrs.ref = (el) => { if (el) el.innerHTML = String(html); };
97
+ }
98
+ return h('div', { class: 'cm-page-view' },
99
+ h('div', { class: 'cm-pv-head' },
100
+ title ? h('h1', { class: 'cm-pv-title' }, title) : null,
101
+ isAdmin && onEdit ? h('button', { class: 'cm-pv-edit', onclick: onEdit, title: 'Edit' }, 'Edit') : null
102
+ ),
103
+ html != null
104
+ ? h('div', bodyAttrs)
105
+ : h('div', bodyAttrs, content || null)
106
+ );
107
+ }
108
+
109
+ export function AuthModal({ mode = 'extension', error, onConnectExtension, onGenerate, onImport, onModeChange, open, onClose, busy } = {}) {
110
+ if (open !== true) return null;
111
+ const tabs = [
112
+ { id: 'extension', label: 'Extension' },
113
+ { id: 'generate', label: 'Generate' },
114
+ { id: 'import', label: 'Import' }
115
+ ];
116
+ let importRef = null;
117
+ return h('div', { class: 'cm-modal-backdrop', onclick: onClose },
118
+ h('div', {
119
+ class: 'cm-auth-modal',
120
+ onclick: (e) => e.stopPropagation(),
121
+ role: 'dialog'
122
+ },
123
+ h('div', { class: 'cm-am-head' },
124
+ h('span', { class: 'cm-am-title' }, 'Sign in'),
125
+ onClose ? h('button', { class: 'cm-am-close', onclick: onClose, title: 'Close' }, '✕') : null
126
+ ),
127
+ h('div', { class: 'cm-am-tabs', role: 'tablist' },
128
+ ...tabs.map(t => h('button', {
129
+ class: 'cm-am-tab' + (mode === t.id ? ' active' : ''),
130
+ onclick: () => onModeChange && onModeChange(t.id),
131
+ 'data-mode': t.id
132
+ }, t.label))
133
+ ),
134
+ mode === 'extension' ? h('div', { class: 'cm-am-pane' },
135
+ h('p', { class: 'cm-am-text' }, 'Connect using a NIP-07 browser extension (e.g. nos2x, Alby).'),
136
+ h('button', {
137
+ class: 'cm-am-btn',
138
+ onclick: onConnectExtension,
139
+ disabled: busy ? 'disabled' : null
140
+ }, busy ? 'Connecting…' : 'Connect extension')
141
+ ) : null,
142
+ mode === 'generate' ? h('div', { class: 'cm-am-pane' },
143
+ h('p', { class: 'cm-am-text' }, 'Generate a fresh keypair. Keep your secret key safe — losing it means losing your identity.'),
144
+ h('div', { class: 'cm-am-warn' }, 'Warning: store the generated nsec somewhere safe before continuing.'),
145
+ h('button', {
146
+ class: 'cm-am-btn',
147
+ onclick: onGenerate,
148
+ disabled: busy ? 'disabled' : null
149
+ }, busy ? 'Generating…' : 'Generate keypair')
150
+ ) : null,
151
+ mode === 'import' ? h('div', { class: 'cm-am-pane' },
152
+ h('p', { class: 'cm-am-text' }, 'Paste your nsec secret key to import an existing identity.'),
153
+ h('textarea', {
154
+ class: 'cm-am-textarea',
155
+ placeholder: 'nsec1…',
156
+ rows: 3,
157
+ ref: (el) => { importRef = el; }
158
+ }),
159
+ h('button', {
160
+ class: 'cm-am-btn',
161
+ onclick: () => onImport && onImport(importRef ? importRef.value.trim() : ''),
162
+ disabled: busy ? 'disabled' : null
163
+ }, busy ? 'Importing…' : 'Import')
164
+ ) : null,
165
+ error ? h('div', { class: 'cm-am-error', role: 'alert' }, String(error)) : null
166
+ )
167
+ );
168
+ }
169
+
170
+ function renderSettingsItem(item) {
171
+ if (!item) return null;
172
+ if (item.kind === 'header') return h('div', { class: 'cm-sp-header' }, item.label || '');
173
+ if (item.kind === 'text') return h('div', { class: 'cm-sp-text' }, item.label || '');
174
+ if (item.kind === 'toggle') {
175
+ return h('label', { class: 'cm-sp-row cm-sp-toggle' },
176
+ h('span', { class: 'cm-sp-row-label' }, item.label || ''),
177
+ h('input', {
178
+ type: 'checkbox',
179
+ checked: item.value ? 'checked' : null,
180
+ onchange: (e) => item.onChange && item.onChange(e.target.checked)
181
+ })
182
+ );
183
+ }
184
+ if (item.kind === 'select') {
185
+ return h('label', { class: 'cm-sp-row cm-sp-select' },
186
+ h('span', { class: 'cm-sp-row-label' }, item.label || ''),
187
+ h('select', {
188
+ value: item.value,
189
+ onchange: (e) => item.onChange && item.onChange(e.target.value)
190
+ }, ...(item.options || []).map(o => h('option', {
191
+ value: o.value != null ? o.value : o.id,
192
+ selected: (o.value != null ? o.value : o.id) === item.value ? 'selected' : null
193
+ }, o.label)))
194
+ );
195
+ }
196
+ if (item.kind === 'button') {
197
+ return h('div', { class: 'cm-sp-row' },
198
+ h('button', {
199
+ class: 'cm-sp-btn' + (item.danger ? ' danger' : ''),
200
+ onclick: item.onClick
201
+ }, item.label || '')
202
+ );
203
+ }
204
+ return null;
205
+ }
206
+
207
+ export function SettingsPopover({ sections = [], open, onClose, anchorX, anchorY, title = 'Settings' } = {}) {
208
+ if (open !== true) return null;
209
+ const style = (anchorX != null && anchorY != null)
210
+ ? `left:${anchorX}px;top:${anchorY}px`
211
+ : null;
212
+ return h('div', { class: 'cm-modal-backdrop transparent', onclick: onClose },
213
+ h('div', {
214
+ class: 'cm-settings-popover',
215
+ style,
216
+ onclick: (e) => e.stopPropagation(),
217
+ role: 'dialog'
218
+ },
219
+ h('div', { class: 'cm-sp-head' },
220
+ h('span', { class: 'cm-sp-title' }, title),
221
+ onClose ? h('button', { class: 'cm-sp-close', onclick: onClose, title: 'Close' }, '✕') : null
222
+ ),
223
+ ...sections.map(sec => h('div', { class: 'cm-sp-section', 'data-id': sec.id },
224
+ sec.label ? h('div', { class: 'cm-sp-section-label' }, sec.label) : null,
225
+ ...(sec.items || []).map(renderSettingsItem)
226
+ ))
227
+ )
228
+ );
229
+ }
230
+
231
+ export function VoiceSettingsModal({
232
+ mode = 'ptt',
233
+ devices = { input: [], output: [] },
234
+ inputId,
235
+ outputId,
236
+ volume = 0.7,
237
+ bitrate = 64,
238
+ vadThreshold = 0.15,
239
+ rnnoise = true,
240
+ autoGain = true,
241
+ forceTurn = false,
242
+ isAdmin,
243
+ channelMode,
244
+ allowedRoles,
245
+ onModeChange,
246
+ onInputChange,
247
+ onOutputChange,
248
+ onVolumeChange,
249
+ onBitrateChange,
250
+ onVadChange,
251
+ onRnnoiseToggle,
252
+ onAutoGainToggle,
253
+ onForceTurnToggle,
254
+ onChannelModeChange,
255
+ onSave,
256
+ onCancel,
257
+ open
258
+ } = {}) {
259
+ if (open !== true) return null;
260
+ const modes = [
261
+ { id: 'ptt', label: 'Push-to-talk' },
262
+ { id: 'realtime', label: 'Open mic (realtime)' }
263
+ ];
264
+ const channelModes = [
265
+ { id: 'ptt', label: 'PTT only' },
266
+ { id: 'realtime', label: 'Realtime' },
267
+ { id: 'free', label: 'User choice' }
268
+ ];
269
+ return h('div', { class: 'cm-modal-backdrop', onclick: onCancel },
270
+ h('div', {
271
+ class: 'cm-voice-settings-modal',
272
+ onclick: (e) => e.stopPropagation(),
273
+ role: 'dialog'
274
+ },
275
+ h('div', { class: 'cm-vsm-head' },
276
+ h('span', { class: 'cm-vsm-title' }, 'Voice settings'),
277
+ onCancel ? h('button', { class: 'cm-vsm-close', onclick: onCancel, title: 'Close' }, '✕') : null
278
+ ),
279
+
280
+ h('div', { class: 'cm-vsm-section' },
281
+ h('div', { class: 'cm-vsm-section-label' }, 'Audio devices'),
282
+ h('label', { class: 'cm-vsm-row' },
283
+ h('span', { class: 'cm-vsm-row-label' }, 'Input'),
284
+ h('select', {
285
+ value: inputId,
286
+ onchange: (e) => onInputChange && onInputChange(e.target.value)
287
+ }, ...(devices.input || []).map(d => h('option', {
288
+ value: d.deviceId || d.id,
289
+ selected: (d.deviceId || d.id) === inputId ? 'selected' : null
290
+ }, d.label || d.name || 'Unknown')))
291
+ ),
292
+ h('label', { class: 'cm-vsm-row' },
293
+ h('span', { class: 'cm-vsm-row-label' }, 'Output'),
294
+ h('select', {
295
+ value: outputId,
296
+ onchange: (e) => onOutputChange && onOutputChange(e.target.value)
297
+ }, ...(devices.output || []).map(d => h('option', {
298
+ value: d.deviceId || d.id,
299
+ selected: (d.deviceId || d.id) === outputId ? 'selected' : null
300
+ }, d.label || d.name || 'Unknown')))
301
+ ),
302
+ h('label', { class: 'cm-vsm-row' },
303
+ h('span', { class: 'cm-vsm-row-label' }, 'Output volume'),
304
+ h('input', {
305
+ type: 'range', min: '0', max: '1', step: '0.01', value: String(volume),
306
+ oninput: (e) => onVolumeChange && onVolumeChange(parseFloat(e.target.value))
307
+ }),
308
+ h('span', { class: 'cm-vsm-row-val' }, Math.round(volume * 100) + '%')
309
+ )
310
+ ),
311
+
312
+ h('div', { class: 'cm-vsm-section' },
313
+ h('div', { class: 'cm-vsm-section-label' }, 'Voice mode'),
314
+ h('div', { class: 'cm-vsm-radio' },
315
+ ...modes.map(m => h('button', {
316
+ class: 'cm-vsm-radio-btn' + (mode === m.id ? ' active' : ''),
317
+ onclick: () => onModeChange && onModeChange(m.id),
318
+ 'data-mode': m.id
319
+ }, m.label))
320
+ )
321
+ ),
322
+
323
+ h('div', { class: 'cm-vsm-section' },
324
+ h('div', { class: 'cm-vsm-section-label' }, 'Audio processing'),
325
+ h('label', { class: 'cm-vsm-row cm-vsm-toggle' },
326
+ h('span', { class: 'cm-vsm-row-label' }, 'Noise suppression (RNNoise)'),
327
+ h('input', {
328
+ type: 'checkbox',
329
+ checked: rnnoise ? 'checked' : null,
330
+ onchange: (e) => onRnnoiseToggle && onRnnoiseToggle(e.target.checked)
331
+ })
332
+ ),
333
+ h('label', { class: 'cm-vsm-row cm-vsm-toggle' },
334
+ h('span', { class: 'cm-vsm-row-label' }, 'Auto gain control'),
335
+ h('input', {
336
+ type: 'checkbox',
337
+ checked: autoGain ? 'checked' : null,
338
+ onchange: (e) => onAutoGainToggle && onAutoGainToggle(e.target.checked)
339
+ })
340
+ ),
341
+ h('label', { class: 'cm-vsm-row' },
342
+ h('span', { class: 'cm-vsm-row-label' }, 'VAD threshold'),
343
+ h('input', {
344
+ type: 'range', min: '0', max: '1', step: '0.01', value: String(vadThreshold),
345
+ oninput: (e) => onVadChange && onVadChange(parseFloat(e.target.value))
346
+ }),
347
+ h('span', { class: 'cm-vsm-row-val' }, vadThreshold.toFixed(2))
348
+ )
349
+ ),
350
+
351
+ h('div', { class: 'cm-vsm-section' },
352
+ h('div', { class: 'cm-vsm-section-label' }, 'Quality'),
353
+ h('label', { class: 'cm-vsm-row' },
354
+ h('span', { class: 'cm-vsm-row-label' }, 'Bitrate'),
355
+ h('input', {
356
+ type: 'range', min: '8', max: '128', step: '1', value: String(bitrate),
357
+ oninput: (e) => onBitrateChange && onBitrateChange(parseInt(e.target.value, 10))
358
+ }),
359
+ h('span', { class: 'cm-vsm-row-val' }, bitrate + ' kbps')
360
+ )
361
+ ),
362
+
363
+ h('div', { class: 'cm-vsm-section' },
364
+ h('div', { class: 'cm-vsm-section-label' }, 'Network'),
365
+ h('label', { class: 'cm-vsm-row cm-vsm-toggle' },
366
+ h('span', { class: 'cm-vsm-row-label' }, 'Force TURN relay'),
367
+ h('input', {
368
+ type: 'checkbox',
369
+ checked: forceTurn ? 'checked' : null,
370
+ onchange: (e) => onForceTurnToggle && onForceTurnToggle(e.target.checked)
371
+ })
372
+ )
373
+ ),
374
+
375
+ isAdmin ? h('div', { class: 'cm-vsm-section cm-vsm-admin' },
376
+ h('div', { class: 'cm-vsm-section-label' }, 'Channel admin'),
377
+ h('div', { class: 'cm-vsm-row' },
378
+ h('span', { class: 'cm-vsm-row-label' }, 'Channel mode'),
379
+ h('div', { class: 'cm-vsm-radio' },
380
+ ...channelModes.map(m => h('button', {
381
+ class: 'cm-vsm-radio-btn' + (channelMode === m.id ? ' active' : ''),
382
+ onclick: () => onChannelModeChange && onChannelModeChange(m.id),
383
+ 'data-mode': m.id
384
+ }, m.label))
385
+ )
386
+ ),
387
+ allowedRoles != null ? h('div', { class: 'cm-vsm-row' },
388
+ h('span', { class: 'cm-vsm-row-label' }, 'Allowed roles'),
389
+ h('span', { class: 'cm-vsm-row-val' }, Array.isArray(allowedRoles) ? allowedRoles.join(', ') || '(any)' : String(allowedRoles))
390
+ ) : null
391
+ ) : null,
392
+
393
+ h('div', { class: 'cm-vsm-actions' },
394
+ h('button', { class: 'cm-vsm-cancel', onclick: onCancel }, 'Cancel'),
395
+ h('button', { class: 'cm-vsm-save', onclick: onSave }, 'Save')
396
+ )
397
+ )
398
+ );
399
+ }
@@ -0,0 +1,132 @@
1
+ // Voice / audio / video surface — pure factories.
2
+
3
+ import * as webjsx from '../../vendor/webjsx/index.js';
4
+ const h = webjsx.createElement;
5
+
6
+ export function PttButton({ state = 'idle', mode = 'ptt', onHoldStart, onHoldEnd, onClick, label } = {}) {
7
+ const cls = 'cm-ptt-button mode-' + mode + ' ' + state;
8
+ const hold = (fn) => fn ? (e) => { e.preventDefault(); fn(e); } : null;
9
+ return h('button', {
10
+ class: cls,
11
+ 'data-state': state,
12
+ 'data-mode': mode,
13
+ onmousedown: hold(onHoldStart),
14
+ onmouseup: hold(onHoldEnd),
15
+ onmouseleave: hold(onHoldEnd),
16
+ ontouchstart: hold(onHoldStart),
17
+ ontouchend: hold(onHoldEnd),
18
+ ontouchcancel: hold(onHoldEnd),
19
+ onclick: onClick
20
+ },
21
+ h('span', { class: 'cm-ptt-dot' }),
22
+ h('span', { class: 'cm-ptt-label' }, label || (mode === 'realtime' ? (state === 'realtime' ? 'Live' : 'Go live') : (state === 'live' ? 'Talking' : 'Hold to talk')))
23
+ );
24
+ }
25
+
26
+ export function VadMeter({ level = 0, threshold = 0.15, onThresholdChange, height = 24, width = 200 } = {}) {
27
+ const clamp = (n) => Math.max(0, Math.min(1, n));
28
+ const lv = clamp(level);
29
+ const th = clamp(threshold);
30
+ const onPick = onThresholdChange ? (e) => {
31
+ const r = e.currentTarget.getBoundingClientRect();
32
+ const x = (e.clientX - r.left) / r.width;
33
+ onThresholdChange(clamp(x));
34
+ } : null;
35
+ return h('div', { class: 'cm-vad-meter', style: `width:${width}px;height:${height}px;` },
36
+ h('div', { class: 'cm-vad-track' },
37
+ h('div', { class: 'cm-vad-bar' + (lv >= th ? ' active' : ''), style: `width:${lv * 100}%;` }),
38
+ h('div', { class: 'cm-vad-threshold', style: `left:${th * 100}%;` }),
39
+ h('div', { class: 'cm-vad-hit', onclick: onPick, onmousedown: onPick })
40
+ )
41
+ );
42
+ }
43
+
44
+ export function WebcamPreview({ videoStream, resolution = '320x240', fps = 15, resolutions = [], fpsOptions = [], onResolutionChange, onFpsChange, onToggle, enabled } = {}) {
45
+ const ref = (el) => { if (el && videoStream && el.srcObject !== videoStream) el.srcObject = videoStream; };
46
+ return h('div', { class: 'cm-webcam-preview' + (enabled ? ' enabled' : ' disabled') },
47
+ h('video', { class: 'cm-wc-video', autoplay: true, muted: true, playsinline: true, ref }),
48
+ h('div', { class: 'cm-wc-controls' },
49
+ h('select', {
50
+ class: 'cm-wc-select',
51
+ value: resolution,
52
+ onchange: (e) => onResolutionChange && onResolutionChange(e.currentTarget.value)
53
+ }, ...resolutions.map(r => h('option', { value: r, selected: r === resolution }, r))),
54
+ h('select', {
55
+ class: 'cm-wc-select',
56
+ value: String(fps),
57
+ onchange: (e) => onFpsChange && onFpsChange(Number(e.currentTarget.value))
58
+ }, ...fpsOptions.map(f => h('option', { value: String(f), selected: Number(f) === Number(fps) }, `${f} fps`))),
59
+ h('button', {
60
+ class: 'cm-wc-toggle' + (enabled ? ' on' : ''),
61
+ onclick: onToggle,
62
+ title: enabled ? 'Disable camera' : 'Enable camera'
63
+ }, enabled ? '📷 On' : '📷 Off')
64
+ )
65
+ );
66
+ }
67
+
68
+ export function AudioQueue({ segments = [], currentSegmentId, onReplay, onSkip, onResume, onPause, paused } = {}) {
69
+ return h('div', { class: 'cm-audio-queue' },
70
+ h('div', { class: 'cm-aq-header' },
71
+ h('span', { class: 'cm-aq-title' }, `Queue (${segments.length})`),
72
+ h('button', {
73
+ class: 'cm-aq-pause',
74
+ onclick: paused ? onResume : onPause,
75
+ title: paused ? 'Resume' : 'Pause'
76
+ }, paused ? '▶' : '❚❚')
77
+ ),
78
+ h('div', { class: 'cm-aq-list' },
79
+ ...segments.map(seg => h('div', {
80
+ class: 'cm-aq-item' + (seg.id === currentSegmentId ? ' current' : '') + (seg.isLive ? ' live' : ''),
81
+ key: seg.id,
82
+ 'data-id': seg.id
83
+ },
84
+ h('span', { class: 'cm-aq-dot', style: seg.color ? `background:${seg.color}` : '' }),
85
+ h('span', { class: 'cm-aq-speaker' }, seg.speaker || '?'),
86
+ h('span', { class: 'cm-aq-dur' }, seg.duration != null ? `${Number(seg.duration).toFixed(1)}s` : ''),
87
+ seg.isLive ? h('span', { class: 'cm-aq-live-tag' }, 'LIVE') : null,
88
+ h('button', { class: 'cm-aq-replay', onclick: () => onReplay && onReplay(seg.id), title: 'Replay' }, '↺'),
89
+ h('button', { class: 'cm-aq-skip', onclick: () => onSkip && onSkip(seg.id), title: 'Skip' }, '⏭')
90
+ ))
91
+ )
92
+ );
93
+ }
94
+
95
+ export function VoiceControls({ muted, deafened, cameraOn, screenShareOn, onMic, onDeafen, onCamera, onScreenShare, onSettings, onLeave } = {}) {
96
+ return h('div', { class: 'cm-voice-controls' },
97
+ h('button', { class: 'cm-vc-btn' + (muted ? ' muted' : ''), onclick: onMic, title: muted ? 'Unmute' : 'Mute' }, muted ? '🔇' : '🎤'),
98
+ h('button', { class: 'cm-vc-btn' + (deafened ? ' deafened' : ''), onclick: onDeafen, title: deafened ? 'Undeafen' : 'Deafen' }, deafened ? '🔕' : '🎧'),
99
+ h('button', { class: 'cm-vc-btn' + (cameraOn ? ' active' : ''), onclick: onCamera, title: cameraOn ? 'Camera off' : 'Camera on' }, '📷'),
100
+ h('button', { class: 'cm-vc-btn' + (screenShareOn ? ' active' : ''), onclick: onScreenShare, title: screenShareOn ? 'Stop share' : 'Share screen' }, '🖥'),
101
+ h('button', { class: 'cm-vc-btn', onclick: onSettings, title: 'Voice settings' }, '⚙'),
102
+ h('button', { class: 'cm-vc-btn danger', onclick: onLeave, title: 'Leave voice' }, '✕')
103
+ );
104
+ }
105
+
106
+ export function MicMonitor({ rawLevel = 0, processedLevel = 0, label } = {}) {
107
+ const clamp = (n) => Math.max(0, Math.min(1, n));
108
+ const r = clamp(rawLevel), p = clamp(processedLevel);
109
+ return h('div', { class: 'cm-mic-monitor' },
110
+ label ? h('div', { class: 'cm-mm-label' }, label) : null,
111
+ h('div', { class: 'cm-mm-row' },
112
+ h('span', { class: 'cm-mm-cap' }, 'raw'),
113
+ h('div', { class: 'cm-mm-bar raw', style: `width:${r * 100}%;` })
114
+ ),
115
+ h('div', { class: 'cm-mm-row' },
116
+ h('span', { class: 'cm-mm-cap' }, 'proc'),
117
+ h('div', { class: 'cm-mm-bar processed', style: `width:${p * 100}%;` })
118
+ )
119
+ );
120
+ }
121
+
122
+ export function VideoLightbox({ src, label, open, onClose } = {}) {
123
+ if (!open) return null;
124
+ return h('div', { class: 'cm-video-lightbox' },
125
+ h('div', { class: 'cm-vl-backdrop', onclick: onClose }),
126
+ h('div', { class: 'cm-vl-content' },
127
+ h('button', { class: 'cm-vl-close', onclick: onClose, title: 'Close' }, '✕'),
128
+ label ? h('div', { class: 'cm-vl-label' }, label) : null,
129
+ h('video', { class: 'cm-vl-video', src, controls: true, autoplay: true, playsinline: true })
130
+ )
131
+ );
132
+ }
package/src/components.js CHANGED
@@ -42,6 +42,11 @@ export {
42
42
  ChatHeader, VoiceStrip, CommunityShell
43
43
  } from './components/community.js';
44
44
 
45
+ export {
46
+ PttButton, VadMeter, WebcamPreview, AudioQueue,
47
+ VoiceControls, MicMonitor, VideoLightbox
48
+ } from './components/voice.js';
49
+
45
50
  export { ThemeToggle } from './components/theme-toggle.js';
46
51
 
47
52
  export {
@@ -49,6 +54,11 @@ export {
49
54
  ContextMenu, CommandPalette, EmojiPicker, ReplyBar
50
55
  } from './components/overlays.js';
51
56
 
57
+ export {
58
+ ThreadPanel, ForumView, PageView,
59
+ AuthModal, SettingsPopover, VoiceSettingsModal
60
+ } from './components/surfaces.js';
61
+
52
62
  export {
53
63
  FREDDIE_PAGES,
54
64
  home, chat, voice, sessions, projects, agents, analytics,