@khanglvm/relay 0.14.0 → 0.14.1

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.14.0",
3
+ "version": "0.14.1",
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",
package/src/ui/app.js CHANGED
@@ -53,15 +53,19 @@
53
53
  // live connection dropped — used to tailor the post-submit message. The board
54
54
  // stays fully usable either way; we never disable Submit.
55
55
  let handedBack = false;
56
- // Calm, persistent status note (reuses the #banner bar). tone 'info' is the
56
+ // Calm, persistent status notes (the #banner stack). tone 'info' is the
57
57
  // default neutral style; 'warn' is a softer amber, NOT the old red error.
58
- // Shown at most once per message so the heartbeat can call it every tick.
58
+ // Each message is shown at most once so the heartbeat can call it every tick.
59
59
  const notesShown = new Set();
60
60
  function showNotice(message, tone) {
61
61
  if (notesShown.has(message)) return;
62
62
  notesShown.add(message);
63
- banner.textContent = message;
64
- banner.className = 'banner ' + (tone === 'warn' ? 'warn' : 'info');
63
+ if (banner.dataset.ready !== '1') {
64
+ banner.replaceChildren();
65
+ banner.dataset.ready = '1';
66
+ }
67
+ banner.className = 'banner-stack';
68
+ banner.append(el('div', { class: 'banner-item ' + (tone === 'warn' ? 'warn' : 'info') }, message));
65
69
  banner.style.display = 'block';
66
70
  }
67
71
 
@@ -1036,6 +1040,24 @@
1036
1040
  fsUp.addEventListener('click', () => setFontScale(fontScale + FS_STEP));
1037
1041
  const fsCtrl = el('div', { class: 'fs-ctrl' }, fsDown, fsUp);
1038
1042
 
1043
+ function buildShareControl() {
1044
+ if (!access.canShare) return null;
1045
+ const wrap = el('span', { class: 'share-wrap' });
1046
+ const shareBtn = el('button', { class: 'share-btn icon-btn', type: 'button', title: 'Share board', 'aria-label': 'Share board' });
1047
+ shareBtn.innerHTML =
1048
+ '<svg viewBox="0 0 16 16" width="15" height="15" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">' +
1049
+ '<path d="M6.5 9.5 9.5 11.2M9.5 4.8 6.5 6.5"/>' +
1050
+ '<circle cx="4.5" cy="8" r="2"/><circle cx="11.5" cy="4" r="2"/><circle cx="11.5" cy="12" r="2"/>' +
1051
+ '</svg>';
1052
+ shareBtn.addEventListener('click', () => {
1053
+ const existing = wrap.querySelector('.share-panel');
1054
+ if (existing) existing.remove();
1055
+ else openSharePanel(wrap);
1056
+ });
1057
+ wrap.append(shareBtn);
1058
+ return wrap;
1059
+ }
1060
+
1039
1061
  // Just reloaded after a live `rly update`? Flag set before reload below.
1040
1062
  try {
1041
1063
  if (sessionStorage.getItem('relay-updated') === '1') {
@@ -1051,7 +1073,7 @@
1051
1073
  }
1052
1074
 
1053
1075
  const titleEl = el('h1', {}, spec.title);
1054
- app.append(el('header', { class: 'qb-header' }, titleEl, el('div', { class: 'qb-controls' }, fsCtrl, themeBtn)));
1076
+ app.append(el('header', { class: 'qb-header' }, titleEl, el('div', { class: 'qb-controls' }, fsCtrl, buildShareControl(), themeBtn)));
1055
1077
  // The board title is commentable too: hovering it shows the pin, like the
1056
1078
  // intro and every other content element. (themeBtn stays out of it.)
1057
1079
  if (spec.title) {
@@ -1153,7 +1175,7 @@
1153
1175
  function openSharePanel(host) {
1154
1176
  const panel = el('div', { class: 'share-panel' },
1155
1177
  el('div', { class: 'share-title' }, 'Share this board'),
1156
- el('div', { class: 'share-desc' }, 'Choose a permission. The link starts working only after you confirm.'),
1178
+ el('div', { class: 'share-desc' }, 'Choose a permission. The link starts working immediately.'),
1157
1179
  el('button', { class: 'share-choice', type: 'button', 'data-role': 'collab' },
1158
1180
  el('span', { class: 'share-choice-title' }, 'Collaborator'),
1159
1181
  el('span', { class: 'share-choice-sub' }, 'Can edit answers, comment, and submit as owner.')
@@ -1172,8 +1194,6 @@
1172
1194
  for (const btn of panel.querySelectorAll('.share-choice')) {
1173
1195
  btn.addEventListener('click', async () => {
1174
1196
  const role = btn.getAttribute('data-role');
1175
- const label = role === 'collab' ? 'collaborator' : 'reviewer';
1176
- if (!window.confirm(`Activate a ${label} link for same-Wi-Fi devices?`)) return;
1177
1197
  btn.disabled = true;
1178
1198
  result.textContent = 'Activating...';
1179
1199
  try {
@@ -1203,27 +1223,14 @@
1203
1223
 
1204
1224
  function buildFooter() {
1205
1225
  const footer = el('footer', { class: 'qb-footer' }, el('span', {}, `relay · ${boot.boardId}`));
1206
- if (access.canShare) {
1207
- const wrap = el('span', { class: 'share-wrap' });
1208
- const shareBtn = el('button', { class: 'share-btn', type: 'button' }, 'Share');
1209
- shareBtn.addEventListener('click', () => {
1210
- const existing = wrap.querySelector('.share-panel');
1211
- if (existing) existing.remove();
1212
- else openSharePanel(wrap);
1213
- });
1214
- wrap.append(shareBtn);
1215
- footer.append(wrap);
1216
- } else if (access.role === 'review') {
1217
- footer.append(el('span', { class: 'share-mode' }, 'reviewer · comments only'));
1218
- } else if (access.role === 'collab') {
1219
- footer.append(el('span', { class: 'share-mode' }, 'collaborator'));
1220
- }
1221
1226
  return footer;
1222
1227
  }
1223
1228
 
1224
1229
  if (!access.canSubmit) {
1225
1230
  submitbar.style.display = 'none';
1226
- app.append(el('div', { class: 'access-note' }, 'Reviewer mode: comments are saved live. Answers and submission are disabled.'));
1231
+ showNotice('Reviewer mode: comments are saved live. Answers and submission are disabled.', 'info');
1232
+ } else if (access.role === 'collab') {
1233
+ showNotice('Editor mode: answers, comments, and submit are enabled for this shared board.', 'info');
1227
1234
  }
1228
1235
  if (!access.canEditAnswers) {
1229
1236
  document.documentElement.classList.add('relay-review');
package/src/ui/style.css CHANGED
@@ -96,14 +96,21 @@ h1 {
96
96
  .intro.blk-markdown .md { color: var(--fg-2); }
97
97
  .intro.blk-markdown .md > :first-child { margin-top: 0; }
98
98
  .intro.blk-markdown .md > :last-child { margin-bottom: 0; }
99
- .theme-btn {
99
+ .theme-btn,
100
+ .icon-btn {
100
101
  background: transparent; color: var(--fg-2);
101
102
  border: 1px solid var(--border-strong); border-radius: 999px;
102
- padding: 6px 14px; cursor: pointer; font-size: 0.8rem; flex: none;
103
+ cursor: pointer; font-size: 0.8rem; flex: none;
103
104
  font-family: var(--sans);
104
105
  transition: border-color 150ms var(--ease), color 150ms var(--ease);
105
106
  }
106
- .theme-btn:hover { border-color: var(--accent); color: var(--accent); }
107
+ .theme-btn { padding: 6px 14px; }
108
+ .icon-btn {
109
+ width: 32px; height: 32px; padding: 0;
110
+ display: inline-flex; align-items: center; justify-content: center;
111
+ }
112
+ .theme-btn:hover,
113
+ .icon-btn:hover { border-color: var(--accent); color: var(--accent); }
107
114
  /* header controls: font-size knob + theme toggle, grouped at the right */
108
115
  .qb-controls { display: flex; align-items: center; gap: 8px; flex: none; }
109
116
  .fs-ctrl { display: inline-flex; border: 1px solid var(--border-strong); border-radius: 999px; overflow: hidden; }
@@ -317,10 +324,22 @@ textarea { min-height: 90px; resize: vertical; }
317
324
  z-index: 10; display: none;
318
325
  border-bottom: 1px solid transparent;
319
326
  }
327
+ .banner-stack {
328
+ position: sticky; top: 0;
329
+ display: flex; flex-direction: column; gap: 0;
330
+ z-index: 10;
331
+ }
332
+ .banner-item {
333
+ text-align: center; padding: 9px 16px; font-size: 0.9rem;
334
+ line-height: 1.45;
335
+ border-bottom: 1px solid transparent;
336
+ }
320
337
  /* Calm status notes (timeout hand-back / lost connection). Never the red error
321
338
  state — the board stays usable, so these only inform. */
322
- .banner.info { background: var(--accent-soft); color: var(--accent); }
323
- .banner.warn { background: var(--bg-sunken); color: var(--fg-2); border-bottom-color: var(--border-strong); }
339
+ .banner.info,
340
+ .banner-item.info { background: var(--accent-soft); color: var(--accent); }
341
+ .banner.warn,
342
+ .banner-item.warn { background: var(--bg-sunken); color: var(--fg-2); border-bottom-color: var(--border-strong); }
324
343
 
325
344
  /* Live-update toast: flagged after a `rly update` reload (see app.js).
326
345
  Fixed top-center, accent-soft bg, accent text; JS auto-removes it. */
@@ -349,14 +368,9 @@ textarea { min-height: 90px; resize: vertical; }
349
368
  .share-btn, .share-copy, .share-choice, .share-close {
350
369
  font: inherit;
351
370
  }
352
- .share-btn {
353
- border: 1px solid var(--border-strong); border-radius: 999px;
354
- background: var(--card); color: var(--fg-2);
355
- padding: 5px 12px; cursor: pointer;
356
- }
357
- .share-btn:hover { border-color: var(--accent); color: var(--accent); }
371
+ .share-btn { background: transparent; }
358
372
  .share-panel {
359
- position: absolute; left: 50%; bottom: calc(100% + 10px); transform: translateX(-50%);
373
+ position: absolute; right: 0; top: calc(100% + 10px);
360
374
  width: min(360px, calc(100vw - 28px));
361
375
  background: var(--card); color: var(--fg);
362
376
  border: 1px solid var(--border-strong); border-radius: 12px;
@@ -386,20 +400,19 @@ textarea { min-height: 90px; resize: vertical; }
386
400
  border: 0; border-radius: 8px; background: var(--accent); color: var(--accent-fg);
387
401
  padding: 7px 12px; cursor: pointer; font-weight: 650;
388
402
  }
389
- .share-mode { color: var(--fg-2); }
390
- .access-note {
391
- color: var(--fg-2); background: var(--accent-soft);
392
- border-radius: 10px; padding: 10px 14px; font-size: 0.86rem;
393
- }
394
403
  .relay-review .ann-del,
395
404
  .relay-review .ann-edit { display: none !important; }
396
405
 
397
406
  @media (max-width: 480px) {
398
407
  .wrap { padding: 18px 12px 18px; }
408
+ .qb-header { align-items: flex-start; }
409
+ .qb-controls { gap: 6px; }
399
410
  .card { padding: 16px; border-radius: 10px; }
400
411
  h1 { font-size: 1.35rem; }
401
412
  .seg button { padding: 10px 18px; }
402
413
  .scale button { width: 40px; height: 40px; }
414
+ .theme-btn { padding: 6px 10px; }
415
+ .share-panel { right: -46px; }
403
416
  }
404
417
 
405
418
  /* Persistence-lost notice: when the board can no longer save the user's input