@khanglvm/relay 0.12.2 → 0.12.3

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": "@khanglvm/relay",
3
- "version": "0.12.2",
3
+ "version": "0.12.3",
4
4
  "description": "Question boards with rich blocks (markdown, charts, mermaid, tables, code, diffs, video, sandboxed HTML), clickable local file-links, and element-level annotations for AI coding agents (Claude Code, Codex, …): ask users structured questions, present interactive visuals, collect inline comments, read answers as JSON — in a local browser board OR rendered INLINE inside the Claude & Codex apps as an MCP App (SEP-1865).",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -48,12 +48,22 @@
48
48
  padding: 12px 14px;
49
49
  font: 14px/1.5 var(--sans);
50
50
  }
51
+ .ann-pop-head { display: flex; align-items: flex-start; gap: 8px; margin-bottom: 8px; }
51
52
  .ann-pop-label {
53
+ flex: 1; min-width: 0;
52
54
  color: var(--muted);
53
55
  font-size: 0.78rem;
54
- margin-bottom: 8px;
55
56
  overflow-wrap: anywhere;
56
57
  }
58
+ .ann-pop-close {
59
+ flex: none; margin: -4px -4px 0 0;
60
+ background: transparent; color: var(--muted);
61
+ border: none; border-radius: 7px;
62
+ width: 26px; height: 26px;
63
+ font-size: 1.25rem; line-height: 1; cursor: pointer;
64
+ transition: color 150ms var(--ease), background 150ms var(--ease);
65
+ }
66
+ .ann-pop-close:hover { color: var(--fg); background: var(--bg-sunken); }
57
67
  .ann-pop-existing { margin-bottom: 8px; max-height: min(50vh, 420px); overflow-y: auto; }
58
68
  .ann-pop-existing:empty { display: none; margin: 0; }
59
69
 
@@ -146,16 +156,49 @@
146
156
  .ann-cancel:hover { border-color: var(--accent); color: var(--accent); }
147
157
 
148
158
  /* delete button (popover items + summary rows): muted, danger on hover */
159
+ /* Delete = a trash icon button, visually distinct from the close ×. */
149
160
  .ann-del {
150
161
  flex: none;
162
+ display: inline-flex; align-items: center; justify-content: center;
151
163
  background: transparent; color: var(--muted);
152
- border: none; border-radius: 6px;
153
- padding: 2px 6px;
154
- font-size: 1.05rem; line-height: 1;
164
+ border: none; border-radius: 7px;
165
+ width: 26px; height: 26px;
155
166
  cursor: pointer;
156
- transition: color 150ms var(--ease);
167
+ transition: color 150ms var(--ease), background 150ms var(--ease);
157
168
  }
158
- .ann-del:hover { color: var(--danger); }
169
+ .ann-del:hover { color: var(--danger); background: var(--accent-soft); }
170
+
171
+ /* ---------- delete confirmation modal ---------- */
172
+ .ann-confirm-scrim {
173
+ position: fixed; inset: 0; z-index: 80;
174
+ background: rgba(28, 27, 25, 0.4);
175
+ display: flex; align-items: center; justify-content: center;
176
+ padding: 20px;
177
+ }
178
+ .ann-confirm {
179
+ width: min(360px, 92vw);
180
+ background: var(--card); color: var(--fg);
181
+ border: 1px solid var(--border); border-radius: 14px;
182
+ box-shadow: var(--shadow-lift);
183
+ padding: 20px;
184
+ font: 14px/1.5 var(--sans);
185
+ }
186
+ .ann-confirm-title { font: 600 1.05rem var(--sans); margin-bottom: 6px; }
187
+ .ann-confirm-msg { color: var(--fg-2); font-size: 0.9rem; }
188
+ .ann-confirm-skip {
189
+ display: flex; align-items: center; gap: 8px;
190
+ margin: 14px 0 4px; color: var(--fg-2); font-size: 0.85rem; cursor: pointer;
191
+ }
192
+ .ann-confirm-skip input { width: 15px; height: 15px; accent-color: var(--accent); cursor: pointer; }
193
+ .ann-confirm-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
194
+ .ann-confirm-cancel, .ann-confirm-del {
195
+ border-radius: 9px; padding: 8px 16px; font: 600 0.88rem var(--sans); cursor: pointer;
196
+ border: 1px solid var(--border-strong); transition: all 150ms var(--ease);
197
+ }
198
+ .ann-confirm-cancel { background: var(--card); color: var(--fg-2); }
199
+ .ann-confirm-cancel:hover { background: var(--bg-sunken); color: var(--fg); }
200
+ .ann-confirm-del { background: var(--danger); color: var(--danger-fg); border-color: var(--danger); }
201
+ .ann-confirm-del:hover { filter: brightness(1.06); }
159
202
 
160
203
  /* ---------- floating selection button ---------- */
161
204
  .ann-selbtn {
@@ -274,9 +317,8 @@
274
317
  .ann-rail-list .ann-sum-row + .ann-sum-row { border-top: 1px solid var(--border); }
275
318
  .ann-rail-list .ann-sum-row:hover { border-color: var(--accent); box-shadow: var(--shadow-card); }
276
319
 
277
- /* The rail always overlays the board (scrim dims, content never moves) — opening
278
- it must NOT shift layout, on any width. It's reached only via the floating
279
- Comments button, which flashes when a new comment lands. */
320
+ /* The rail NEVER auto-opens (that shifted the board unexpectedly) — it's reached
321
+ only via the floating Comments button, which flashes when a new comment lands. */
280
322
  @keyframes ann-toggle-pulse {
281
323
  0% { transform: scale(1); }
282
324
  35% { transform: scale(1.12); box-shadow: 0 0 0 4px var(--accent-soft); }
@@ -284,6 +326,15 @@
284
326
  }
285
327
  .ann-rail-toggle.pulse { animation: ann-toggle-pulse 600ms var(--ease); border-color: var(--accent); color: var(--accent); }
286
328
 
329
+ /* When opened on a roomy screen, the rail DOCKS and consumes page space (pushes
330
+ content) instead of covering it — so the board stays visible beside it and
331
+ clicking a comment reveals the highlighted item in the page. No scrim while
332
+ docked. Narrow screens fall back to an overlay + scrim (no room to dock). */
333
+ @media (min-width: 980px) {
334
+ .ann-rail-scrim { display: none !important; }
335
+ body.ann-rail-open { padding-right: 340px; }
336
+ }
337
+
287
338
  /* ---------- jump-to flash ---------- */
288
339
  .ann-flash { animation: ann-flash 1s var(--ease); }
289
340
  @keyframes ann-flash {
@@ -494,13 +494,57 @@
494
494
  dom.pop.replaceChildren();
495
495
  }
496
496
 
497
+ // ---------- delete a comment (with confirm) ----------
498
+ // The × that used to delete a comment read as "close the popup". Delete is now
499
+ // a distinct trash button that opens a confirm modal; "Don't ask again"
500
+ // (default OFF) suppresses future confirms for the session (persisted best-effort).
501
+ const ICON_TRASH =
502
+ '<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
503
+ '<path d="M2.5 4h11M6 4V2.5h4V4M5 4l.5 9h5l.5-9"/></svg>';
504
+ const DEL_SKIP_KEY = 'relay-ann-del-skip';
505
+ let delConfirmSkip = false;
506
+ try { delConfirmSkip = localStorage.getItem(DEL_SKIP_KEY) === '1'; } catch { /* sandbox: no storage */ }
507
+
508
+ function requestDelete(id, after) {
509
+ const done = () => { removeAnnotation(id); if (after) after(); };
510
+ if (delConfirmSkip) { done(); return; }
511
+ showDeleteConfirm(done);
512
+ }
513
+ function showDeleteConfirm(onConfirm) {
514
+ const scrim = el('div', { class: 'ann-confirm-scrim' });
515
+ const skip = el('input', { type: 'checkbox' });
516
+ const cancel = el('button', { class: 'ann-confirm-cancel', type: 'button' }, 'Cancel');
517
+ const del = el('button', { class: 'ann-confirm-del', type: 'button' }, 'Delete');
518
+ const close = () => { scrim.remove(); document.removeEventListener('keydown', onKey, true); };
519
+ const onKey = (e) => { if (e.key === 'Escape') { e.preventDefault(); close(); } };
520
+ cancel.addEventListener('click', close);
521
+ scrim.addEventListener('mousedown', (e) => { if (e.target === scrim) close(); });
522
+ del.addEventListener('click', () => {
523
+ if (skip.checked) { delConfirmSkip = true; try { localStorage.setItem(DEL_SKIP_KEY, '1'); } catch { /* sandbox */ } }
524
+ close();
525
+ onConfirm();
526
+ });
527
+ scrim.append(el('div', { class: 'ann-confirm', role: 'alertdialog', 'aria-label': 'Delete comment' },
528
+ el('div', { class: 'ann-confirm-title' }, 'Delete this comment?'),
529
+ el('div', { class: 'ann-confirm-msg' }, 'This removes the comment and its replies. It can’t be undone.'),
530
+ el('label', { class: 'ann-confirm-skip' }, skip, el('span', {}, 'Don’t ask again')),
531
+ el('div', { class: 'ann-confirm-actions' }, cancel, del)
532
+ ));
533
+ document.body.append(scrim);
534
+ document.addEventListener('keydown', onKey, true);
535
+ setTimeout(() => { try { cancel.focus(); } catch { /* ignore */ } }, 0);
536
+ }
537
+
497
538
  function openPopover(info, anchorEl) {
498
539
  if (disabled) return;
499
540
  ensureDom();
500
541
  closePopover();
501
542
  hidePin();
502
543
  const pop = dom.pop;
503
- pop.replaceChildren(el('div', { class: 'ann-pop-label' }, humanize(info.target)));
544
+ // Header: target label + an explicit CLOSE button, so × always means "close".
545
+ const popClose = el('button', { class: 'ann-pop-close', type: 'button', title: 'Close', 'aria-label': 'Close' }, '×');
546
+ popClose.addEventListener('click', closePopover);
547
+ pop.replaceChildren(el('div', { class: 'ann-pop-head' }, el('div', { class: 'ann-pop-label' }, humanize(info.target)), popClose));
504
548
 
505
549
  // Existing comments on this exact target, rendered as threads: author
506
550
  // chip + time + delete, the comment text, its replies indented below,
@@ -510,11 +554,9 @@
510
554
  const renderExisting = () => {
511
555
  existingWrap.replaceChildren();
512
556
  for (const a of matching(info)) {
513
- const del = el('button', { class: 'ann-del', type: 'button', title: 'Delete comment', 'aria-label': 'Delete comment' }, '×');
514
- del.addEventListener('click', () => {
515
- removeAnnotation(a.id);
516
- renderExisting();
517
- });
557
+ const del = el('button', { class: 'ann-del', type: 'button', title: 'Delete comment', 'aria-label': 'Delete comment' });
558
+ del.innerHTML = ICON_TRASH;
559
+ del.addEventListener('click', () => requestDelete(a.id, renderExisting));
518
560
  const thread = el('div', { class: 'ann-thread' },
519
561
  el('div', { class: 'ann-thread-head' }, chip(a.author), timeEl(a.createdAt), del),
520
562
  el('div', { class: 'ann-pop-text' }, a.text)
@@ -719,11 +761,9 @@
719
761
  // The rail has its own header chrome; standalone summaries get a heading.
720
762
  if (!isRail) target.append(el('h3', { class: 'ann-sum-head' }, `Comments (${annotations.length})`));
721
763
  for (const a of annotations) {
722
- const del = el('button', { class: 'ann-del', type: 'button', title: 'Delete comment', 'aria-label': 'Delete comment' }, '×');
723
- del.addEventListener('click', (e) => {
724
- e.stopPropagation();
725
- removeAnnotation(a.id);
726
- });
764
+ const del = el('button', { class: 'ann-del', type: 'button', title: 'Delete comment', 'aria-label': 'Delete comment' });
765
+ del.innerHTML = ICON_TRASH;
766
+ del.addEventListener('click', (e) => { e.stopPropagation(); requestDelete(a.id); });
727
767
  // Thread meta: reply count, plus an "agent" chip when the latest entry
728
768
  // in the thread (last reply, or the comment itself) is agent-authored.
729
769
  const replyCount = Array.isArray(a.replies) ? a.replies.length : 0;