better rum todo
Build and Publish / Metadata (push) Successful in 10s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 1m53s

This commit is contained in:
mats committed 2026-09-22 09:15:00 +02:00
1 parent cb3266bf33
commit e81b8c2fcd
6 files changed
+231 -51

No files matched your search

+143 -7
View File
@@ -1,10 +1,12 @@
/**
* otel-todo.js - file todos / issues straight from the page.
*
* Shows a small floating symbol and listens for a keyboard shortcut. Either
* opens a native <dialog> in the bottom-right corner with a text box and an
* optional component picker, and POSTs the note (with the current path and
* hash) to the collector's /v1/todos endpoint.
* Shows a small floating symbol and listens for a keyboard shortcut. Opens a
* native <dialog> in the bottom-right corner with a text box, an optional
* component picker, and an "Reference an element" toggle that lets you click
* any node on the page (with a hover outline) to attach its CSS selector and
* a short label to the note. POSTs the note (with the current path, hash and
* any picked element) to the collector's /v1/todos endpoint.
*
* <script src="https://otel.example.com/static/otel-todo.js"
* data-service-name="checkout-web"
@@ -65,7 +67,22 @@
'dialog.otel-todo textarea{min-height:90px;resize:vertical}' +
'dialog.otel-todo .row{display:flex;gap:8px;justify-content:flex-end;align-items:center}' +
'dialog.otel-todo .meta{opacity:.65;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
'dialog.otel-todo .status{margin-right:auto;font-size:12px}';
'dialog.otel-todo .status{margin-right:auto;font-size:12px}' +
'.otel-todo-pick{display:inline-flex;align-items:center;gap:5px;border:1px solid #8886;border-radius:6px;' +
'background:transparent;color:inherit;padding:5px 8px;margin:0 0 6px;cursor:pointer;font-size:12px}' +
'.otel-todo-pick:hover,.otel-todo-pick:focus-visible{background:#8882}' +
'.otel-todo-pick[aria-pressed="true"]{background:#3b82f633;border-color:#3b82f6aa;color:#3b82f6}' +
'.otel-todo-ref{display:none;align-items:center;gap:6px;margin:0 0 6px;padding:5px 7px;border:1px solid #8886;' +
'border-radius:6px;background:#8881;font:11px/1.3 ui-monospace,monospace}' +
'.otel-todo-ref.show{display:flex}' +
'.otel-todo-ref span{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
'.otel-todo-ref button{border:0;background:none;cursor:pointer;color:inherit;opacity:.6;font:12px/1 inherit;padding:0 2px}' +
'.otel-todo-ref button:hover{opacity:1}' +
'.otel-todo-hover-outline{position:fixed;z-index:2147483645;pointer-events:none;border:2px solid #3b82f6;' +
'background:#3b82f626;border-radius:3px;transition:all .05s linear}' +
'.otel-todo-pick-hint{position:fixed;left:50%;top:16px;transform:translateX(-50%);z-index:2147483647;' +
'background:#3b82f6;color:#fff;font:12px/1 system-ui,sans-serif;padding:7px 12px;border-radius:20px;' +
'box-shadow:0 4px 14px #0004}';
document.head.appendChild(css);
var dlg = document.createElement('dialog');
@@ -74,6 +91,8 @@
dlg.innerHTML =
'<form method="dialog">' +
'<div class="meta"></div>' +
'<button type="button" class="otel-todo-pick" aria-pressed="false">⌖ Reference an element</button>' +
'<div class="otel-todo-ref"><span></span><button type="button" data-clear-ref aria-label="Remove reference">✕</button></div>' +
'<textarea name="text" placeholder="What needs doing?" required maxlength="4000"></textarea>' +
'<select name="component" aria-label="Component (optional)"></select>' +
'<div class="row"><span class="status" role="status"></span>' +
@@ -84,8 +103,118 @@
var selectEl = form.elements.component;
var statusEl = form.querySelector('.status');
var metaEl = form.querySelector('.meta');
var pickBtn = form.querySelector('.otel-todo-pick');
var refEl = form.querySelector('.otel-todo-ref');
var refLabelEl = refEl.querySelector('span');
document.body.appendChild(dlg);
// --- element picker: click a node on the page to attach its selector ---
var picked = null; // { selector, label }
var hoverBox = null;
var picking = false;
var hint = null;
function cssPath(el) {
if (!(el instanceof Element)) return '';
var parts = [];
var node = el;
while (node && node.nodeType === 1 && parts.length < 6) {
if (node.id) { parts.unshift('#' + CSS.escape(node.id)); break; }
var part = node.tagName.toLowerCase();
var cls = (node.getAttribute('class') || '').trim().split(/\s+/).filter(Boolean).slice(0, 2);
if (cls.length) part += '.' + cls.map(function (c) { return CSS.escape(c); }).join('.');
var parent = node.parentElement;
if (parent) {
var siblings = [].filter.call(parent.children, function (c) { return c.tagName === node.tagName; });
if (siblings.length > 1) part += ':nth-of-type(' + (siblings.indexOf(node) + 1) + ')';
}
parts.unshift(part);
node = parent;
}
return parts.join(' > ');
}
function describeElement(el) {
var tag = el.tagName.toLowerCase();
var text = (el.getAttribute('aria-label') || el.textContent || '').replace(/\s+/g, ' ').trim();
if (text.length > 60) text = text.slice(0, 60) + '…';
return { selector: cssPath(el), label: text ? tag + ' – "' + text + '"' : tag };
}
function setRef(ref) {
picked = ref;
if (ref) {
refLabelEl.textContent = ref.label;
refLabelEl.title = ref.selector;
refEl.classList.add('show');
} else {
refEl.classList.remove('show');
}
}
function ensureHoverBox() {
if (!hoverBox) {
hoverBox = document.createElement('div');
hoverBox.className = 'otel-todo-hover-outline';
document.body.appendChild(hoverBox);
}
return hoverBox;
}
function onPickMove(e) {
var el = document.elementFromPoint(e.clientX, e.clientY);
if (!el || dlg.contains(el)) { ensureHoverBox().style.display = 'none'; return; }
var r = el.getBoundingClientRect();
var box = ensureHoverBox();
box.style.display = 'block';
box.style.left = r.left + 'px';
box.style.top = r.top + 'px';
box.style.width = r.width + 'px';
box.style.height = r.height + 'px';
}
function onPickClick(e) {
var el = document.elementFromPoint(e.clientX, e.clientY);
if (!el || dlg.contains(el)) return; // let dialog controls work normally
e.preventDefault();
e.stopPropagation();
setRef(describeElement(el));
stopPicking();
}
function onPickKey(e) {
if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); stopPicking(); }
}
function startPicking() {
if (picking) return;
picking = true;
pickBtn.setAttribute('aria-pressed', 'true');
pickBtn.textContent = '⌖ Click an element… (Esc to cancel)';
hint = document.createElement('div');
hint.className = 'otel-todo-pick-hint';
hint.textContent = 'Click the element you mean — Esc to cancel';
document.body.appendChild(hint);
document.addEventListener('mousemove', onPickMove, true);
document.addEventListener('click', onPickClick, true);
document.addEventListener('keydown', onPickKey, true);
}
function stopPicking() {
if (!picking) return;
picking = false;
pickBtn.setAttribute('aria-pressed', 'false');
pickBtn.textContent = '⌖ Reference an element';
document.removeEventListener('mousemove', onPickMove, true);
document.removeEventListener('click', onPickClick, true);
document.removeEventListener('keydown', onPickKey, true);
if (hoverBox) { hoverBox.style.display = 'none'; }
if (hint) { hint.remove(); hint = null; }
}
pickBtn.addEventListener('click', function () { picking ? stopPicking() : startPicking(); });
refEl.querySelector('[data-clear-ref]').addEventListener('click', function () { setRef(null); });
function components() {
var seen = {};
var list = [];
@@ -113,12 +242,16 @@
selectEl.hidden = list.length === 0;
metaEl.textContent = project + ' · ' + location.pathname + location.hash;
statusEl.textContent = '';
setRef(null);
// Non-modal so the page stays usable while the note is being written.
dlg.show();
textEl.focus();
}
function close() { if (dlg.open) dlg.close(); }
function close() {
stopPicking();
if (dlg.open) dlg.close();
}
form.querySelector('[data-cancel]').addEventListener('click', close);
dlg.addEventListener('keydown', function (e) {
@@ -145,11 +278,14 @@
hash: location.hash,
url: location.href,
ua: navigator.userAgent,
session: sessionId
session: sessionId,
elementSelector: picked ? picked.selector : '',
elementLabel: picked ? picked.label : ''
})
}).then(function (res) {
if (!res.ok) throw new Error('HTTP ' + res.status);
textEl.value = '';
setRef(null);
close();
}).catch(function (err) {
statusEl.textContent = 'Failed: ' + err.message;