feat: structured @-mentions (notify+clickable+hover), distinct links, Ctrl-rebalance sliders

- Mentions now insert a span carrying the user id (handles names with spaces):
  the sanitizer permits <span class="mention" data-user-card="ULID">, comment
  and chat notifications resolve by id and only fire for users with access,
  and mentions render as clickable chips with the shared hover user-card.
- Messages composer inserts an atomic, non-editable mention chip so the
  trailing space no longer collapses while typing.
- Links inside chat messages and task comments are underlined/accented so they
  read as clickable.
- Atomization: hold Ctrl/Cmd while dragging an effort slider to rebalance the
  sibling coefficients proportionally (sum stays ~1.00); added a hint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
etalon
2026-06-13 12:09:17 +02:00
parent 70cb664246
commit b232b4b3d3
9 changed files with 239 additions and 59 deletions
+5 -4
View File
@@ -1,6 +1,6 @@
import { api, toast } from '/static/js/api.js';
import { subscribe } from '/static/js/ws.js';
import { attachMentions } from '/static/js/mention.js';
import { attachMentions, mentionHTML } from '/static/js/mention.js';
const root = document.getElementById('task-root');
const errorBox = document.getElementById('error');
@@ -58,7 +58,7 @@ async function render() {
const comments = await Promise.all((t.comments || []).map(async (c) => `
<div class="card" style="padding:12px">
<p class="muted" style="margin:0 0 8px">${esc(await userName(c.authorId))} · ${new Date(c.at).toLocaleString()}</p>
<div>${c.body}</div>
<div class="comment-body">${c.body}</div>
</div>`));
const timeline = await Promise.all((t.timeline || []).slice().reverse().map(async (e) => `
@@ -183,9 +183,10 @@ function wireHandlers() {
});
document.getElementById('comment-form').addEventListener('submit', async (e) => {
e.preventDefault();
const body = document.getElementById('comment-body').value;
const field = document.getElementById('comment-body');
const body = mentionHTML(field.value, field).replace(/\n/g, '<br>');
try {
await api('POST', `/api/v1/tasks/${taskId}/comments`, { body: '<p>' + esc(body).replace(/\n/g, '<br>') + '</p>' });
await api('POST', `/api/v1/tasks/${taskId}/comments`, { body: '<p>' + body + '</p>' });
load();
} catch (err) { toast(err.message, 'err'); }
});