@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 +1 -1
- package/src/ui/app.js +31 -24
- package/src/ui/style.css +30 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khanglvm/relay",
|
|
3
|
-
"version": "0.14.
|
|
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
|
|
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
|
-
//
|
|
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.
|
|
64
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
323
|
-
.banner.
|
|
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;
|
|
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
|