anentrypoint-design 0.0.144 → 0.0.146

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.146",
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,148 @@ 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
+
282
+ function fmtRelTime(ts) {
283
+ const t = Number(ts) || 0;
284
+ if (!t) return '';
285
+ const ms = t > 1e12 ? t : t * 1000;
286
+ const d = Math.max(0, Date.now() - ms);
287
+ const m = Math.floor(d / 60000);
288
+ if (m < 1) return 'now';
289
+ if (m < 60) return m + 'm';
290
+ const hr = Math.floor(m / 60);
291
+ if (hr < 24) return hr + 'h';
292
+ return Math.floor(hr / 24) + 'd';
293
+ }
294
+
295
+ export function ThreadPanel({ threads = [], activeId = null, title = 'Threads', onSelect, onCreate, onClose } = {}) {
296
+ const list = Array.isArray(threads) ? threads : [];
297
+ return h('div', { class: 'cm-thread-panel', role: 'complementary', 'aria-label': title },
298
+ h('div', { class: 'cm-tp-head' },
299
+ h('span', { class: 'cm-tp-title' }, title),
300
+ h('div', { class: 'cm-tp-head-actions' },
301
+ onCreate ? h('button', { type: 'button', class: 'cm-tp-new', 'aria-label': 'new thread', title: 'New thread', onclick: onCreate }, '+') : null,
302
+ onClose ? h('button', { type: 'button', class: 'cm-tp-close', 'aria-label': 'close', title: 'Close', onclick: onClose }, '✕') : null
303
+ )
304
+ ),
305
+ h('div', { class: 'cm-tp-list' },
306
+ list.length
307
+ ? list.map(t => h('button', {
308
+ type: 'button', key: 'tp-' + t.id,
309
+ class: 'cm-tp-item' + (t.id === activeId ? ' is-active' : '') + (t.unread ? ' is-unread' : ''),
310
+ onclick: () => onSelect && onSelect(t.id)
311
+ },
312
+ t.unread ? h('span', { class: 'cm-tp-dot', 'aria-hidden': 'true' }) : null,
313
+ h('span', { class: 'cm-tp-item-title' }, t.title || '(untitled)'),
314
+ t.lastMessage ? h('span', { class: 'cm-tp-item-snippet' }, t.lastMessage) : null,
315
+ h('span', { class: 'cm-tp-item-meta' },
316
+ t.author ? h('span', { class: 'cm-tp-item-author' }, t.author) : null,
317
+ t.time ? h('span', { class: 'cm-tp-item-time' }, fmtRelTime(t.time)) : null
318
+ )
319
+ ))
320
+ : h('div', { class: 'cm-tp-empty' }, 'No threads yet')
321
+ )
322
+ );
323
+ }
324
+
325
+ export function ForumView({ posts = [], onSearch, onSort, onSelect, onNewPost } = {}) {
326
+ const list = Array.isArray(posts) ? posts : [];
327
+ return h('div', { class: 'cm-forum', role: 'region', 'aria-label': 'forum' },
328
+ h('div', { class: 'cm-forum-toolbar' },
329
+ h('input', {
330
+ type: 'search', class: 'cm-forum-search', placeholder: 'Search posts…',
331
+ 'aria-label': 'search posts',
332
+ oninput: onSearch ? (e) => onSearch(e.target.value) : null
333
+ }),
334
+ h('select', {
335
+ class: 'cm-forum-sort', 'aria-label': 'sort posts',
336
+ onchange: onSort ? (e) => onSort(e.target.value) : null
337
+ },
338
+ h('option', { value: 'recent' }, 'Recent'),
339
+ h('option', { value: 'replies' }, 'Most replies'),
340
+ h('option', { value: 'oldest' }, 'Oldest')
341
+ ),
342
+ onNewPost ? h('button', { type: 'button', class: 'cm-forum-new', onclick: onNewPost }, 'New post') : null
343
+ ),
344
+ h('div', { class: 'cm-forum-list' },
345
+ list.length
346
+ ? list.map(p => h('button', {
347
+ type: 'button', key: 'fp-' + p.id, class: 'cm-forum-item',
348
+ onclick: () => onSelect && onSelect(p.id)
349
+ },
350
+ h('div', { class: 'cm-forum-item-head' },
351
+ h('span', { class: 'cm-forum-item-title' }, p.title || '(untitled)'),
352
+ h('span', { class: 'cm-forum-item-replies' }, (Number(p.replyCount) || 0) + ' ▸')
353
+ ),
354
+ p.snippet ? h('div', { class: 'cm-forum-item-snippet' }, p.snippet) : null,
355
+ h('div', { class: 'cm-forum-item-meta' },
356
+ p.author ? h('span', { class: 'cm-forum-item-author' }, p.author) : null,
357
+ p.time ? h('span', { class: 'cm-forum-item-time' }, fmtRelTime(p.time)) : null,
358
+ Array.isArray(p.tags) && p.tags.length
359
+ ? h('span', { class: 'cm-forum-item-tags' }, ...p.tags.map((tag, i) =>
360
+ h('span', { class: 'cm-forum-tag', key: 'tg-' + i }, tag)))
361
+ : null
362
+ )
363
+ ))
364
+ : h('div', { class: 'cm-forum-empty' }, 'No posts yet')
365
+ )
366
+ );
367
+ }
368
+
369
+ export function PageView({ title = '', html = '', isAdmin = false, onEdit } = {}) {
370
+ return h('div', { class: 'cm-page', role: 'document' },
371
+ h('div', { class: 'cm-page-head' },
372
+ h('h1', { class: 'cm-page-title' }, title || ''),
373
+ isAdmin && onEdit ? h('button', { type: 'button', class: 'cm-page-edit', onclick: onEdit }, 'Edit') : null
374
+ ),
375
+ h('div', {
376
+ class: 'cm-page-body',
377
+ ref: (el) => { if (el) el.innerHTML = html || '<p class="cm-page-empty">This page is empty.</p>'; }
378
+ })
379
+ );
380
+ }
381
+
240
382
  export function CommunityShell({ serverRailProps, sidebarProps, children, memberListProps, voiceStripProps } = {}) {
241
383
  return h('div', { class: 'cm-shell' },
242
384
  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,336 @@ 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
+ }
465
+
466
+ // AuthModal — centered login dialog: extension / generate / import (nsec) modes.
467
+ export function AuthModal({ mode = 'extension', error = '', busy = false, open = false, onModeChange, onConnectExtension, onGenerate, onImport, onClose } = {}) {
468
+ if (!open) return null;
469
+ const close = () => onClose && onClose();
470
+ const modes = [
471
+ { id: 'extension', label: 'Extension' },
472
+ { id: 'generate', label: 'Generate' },
473
+ { id: 'import', label: 'Import key' },
474
+ ];
475
+ let nsec = '';
476
+ const body = () => {
477
+ if (mode === 'generate') {
478
+ return [
479
+ h('p', { class: 'ov-auth-hint' }, 'Create a fresh Nostr identity. Back up the key after.'),
480
+ h('button', { type: 'button', class: 'ov-auth-primary', disabled: busy ? true : null,
481
+ onclick: () => onGenerate && onGenerate() }, busy ? 'Working…' : 'Generate new key'),
482
+ ];
483
+ }
484
+ if (mode === 'import') {
485
+ return [
486
+ h('p', { class: 'ov-auth-hint' }, 'Paste an existing nsec / hex secret key.'),
487
+ h('input', {
488
+ type: 'password', class: 'ov-auth-input', placeholder: 'nsec1…',
489
+ 'aria-label': 'secret key', disabled: busy ? true : null,
490
+ oninput: (e) => { nsec = e.target.value; },
491
+ onkeydown: (e) => { if (e.key === 'Enter') { e.preventDefault(); onImport && onImport(nsec); } },
492
+ }),
493
+ h('button', { type: 'button', class: 'ov-auth-primary', disabled: busy ? true : null,
494
+ onclick: () => onImport && onImport(nsec) }, busy ? 'Working…' : 'Import'),
495
+ ];
496
+ }
497
+ return [
498
+ h('p', { class: 'ov-auth-hint' }, 'Connect a NIP-07 browser extension (Alby, nos2x…).'),
499
+ h('button', { type: 'button', class: 'ov-auth-primary', disabled: busy ? true : null,
500
+ onclick: () => onConnectExtension && onConnectExtension() }, busy ? 'Connecting…' : 'Connect extension'),
501
+ ];
502
+ };
503
+ return h('div', {
504
+ class: 'ov-auth-backdrop', role: 'presentation',
505
+ ref: (el) => {
506
+ if (!el || el._ovAuth) return; el._ovAuth = true;
507
+ el.addEventListener('mousedown', (e) => {
508
+ const panel = el.querySelector('.ov-auth-panel');
509
+ if (panel && !panel.contains(e.target)) close();
510
+ });
511
+ },
512
+ },
513
+ h('div', {
514
+ class: 'ov-auth-panel', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Sign in',
515
+ onkeydown: (e) => { if (e.key === 'Escape') { e.preventDefault(); close(); } },
516
+ },
517
+ h('div', { class: 'ov-auth-head' },
518
+ h('h2', { class: 'ov-auth-title' }, 'Sign in'),
519
+ h('button', { type: 'button', class: 'ov-auth-x', 'aria-label': 'close', onclick: close }, '×')
520
+ ),
521
+ h('div', { class: 'ov-auth-tabs', role: 'tablist' },
522
+ ...modes.map(m => h('button', {
523
+ type: 'button', role: 'tab', key: 'am-' + m.id,
524
+ class: 'ov-auth-tab' + (m.id === mode ? ' is-active' : ''),
525
+ 'aria-selected': m.id === mode ? 'true' : 'false',
526
+ onclick: () => onModeChange && onModeChange(m.id),
527
+ }, m.label))
528
+ ),
529
+ h('div', { class: 'ov-auth-body' }, ...body()),
530
+ error ? h('div', { class: 'ov-auth-error', role: 'alert' }, String(error)) : null
531
+ )
532
+ );
533
+ }
534
+
535
+ // VideoLightbox — fullscreen video player overlay with backdrop dismiss.
536
+ export function VideoLightbox({ src, label = '', open = false, onClose } = {}) {
537
+ if (!open || !src) return null;
538
+ const close = () => onClose && onClose();
539
+ return h('div', {
540
+ class: 'ov-lightbox-backdrop', role: 'dialog', 'aria-modal': 'true', 'aria-label': label || 'Video',
541
+ tabindex: '-1',
542
+ onkeydown: (e) => { if (e.key === 'Escape') { e.preventDefault(); close(); } },
543
+ ref: (el) => { if (el && !el._ovLb) { el._ovLb = true; queueMicrotask(() => el.focus()); } },
544
+ onmousedown: (e) => { if (e.target === e.currentTarget) close(); },
545
+ },
546
+ h('button', { type: 'button', class: 'ov-lightbox-x', 'aria-label': 'close', onclick: close }, '×'),
547
+ h('div', { class: 'ov-lightbox-stage' },
548
+ h('video', { class: 'ov-lightbox-video', src, controls: true, autoplay: true, playsinline: true }),
549
+ label ? h('div', { class: 'ov-lightbox-label' }, label) : null
550
+ )
551
+ );
552
+ }