feat: numbered element chips for picker, multi-select support
All checks were successful
Deploy to dev.bl.pixeldev.eu / deploy (push) Successful in 3s

Replace the old single-snippet textarea append with a chip-based UI:
- Each picked element gets a monotonically increasing number [1], [2], …
- A chip appears below the textarea: [N] selector ✕
- The token [element N] is inserted at the cursor position in the textarea
- Multiple elements can be picked in one session
- Chips can be removed (number is never re-used or re-ordered)
- On submit, element snippets are appended to the issue body as
  named reference blocks so the HTML context is still captured

Closes #6
This commit is contained in:
OpenClaw Agent 2026-03-13 14:24:55 +00:00
parent 7a593d082a
commit 1afd2a0e4f

View File

@ -183,10 +183,17 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => {
.fb-picker-btn { background:#1a2a1a; border:1px solid #2a4a2a; color:#6fcf6f; font-size:.78rem; .fb-picker-btn { background:#1a2a1a; border:1px solid #2a4a2a; color:#6fcf6f; font-size:.78rem;
padding:.4rem .7rem; border-radius:6px; cursor:pointer; white-space:nowrap; flex-shrink:0; } padding:.4rem .7rem; border-radius:6px; cursor:pointer; white-space:nowrap; flex-shrink:0; }
.fb-picker-btn:hover { background:#223322; } .fb-picker-btn:hover { background:#223322; }
.fb-picker-hint { color:#444; font-size:.75rem; } /* Picked-elements chip list */
.fb-snippet-preview { background:#0a0a0a; border:1px solid #2a2a2a; border-radius:6px; #_fb-picked-list { display:flex; flex-wrap:wrap; gap:.4rem; }
padding:.5rem .7rem; font-size:.72rem; color:#7c9ef5; font-family:monospace; #_fb-picked-list:empty { display:none; }
white-space:pre-wrap; word-break:break-all; max-height:80px; overflow-y:auto; } .fb-picked-chip { display:inline-flex; align-items:center; gap:.35rem; background:#0d1f0d;
border:1px solid #2a4a2a; border-radius:6px; padding:.28rem .55rem; font-size:.75rem;
color:#6fcf6f; font-family:monospace; }
.fb-chip-num { color:#4a9f4a; font-weight:700; }
.fb-chip-label { color:#aaa; max-width:140px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.fb-chip-remove { background:none; border:none; color:#555; cursor:pointer; font-size:.7rem;
padding:0; line-height:1; }
.fb-chip-remove:hover { color:#f57c7c; }
/* Picker overlay */ /* Picker overlay */
#_fb-picker-shield { display:none; position:fixed; inset:0; z-index:199999; cursor:crosshair; } #_fb-picker-shield { display:none; position:fixed; inset:0; z-index:199999; cursor:crosshair; }
#_fb-picker-shield.active { display:block; } #_fb-picker-shield.active { display:block; }
@ -240,11 +247,8 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => {
</div> </div>
<div class="fb-picker-row"> <div class="fb-picker-row">
<button class="fb-picker-btn" id="_fb-pick-btn">🎯 Pick element</button> <button class="fb-picker-btn" id="_fb-pick-btn">🎯 Pick element</button>
<span class="fb-picker-hint" id="_fb-pick-hint">Click to reference a page element</span>
</div>
<div id="_fb-snippet-wrap" style="display:none;">
<div class="fb-snippet-preview" id="_fb-snippet"></div>
</div> </div>
<div id="_fb-picked-list"></div>
<button class="fb-btn" id="_fb-submit">Submit Issue</button> <button class="fb-btn" id="_fb-submit">Submit Issue</button>
<div class="fb-status" id="_fb-status"></div> <div class="fb-status" id="_fb-status"></div>
</div> </div>
@ -302,7 +306,8 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => {
// ── Picker mode ─────────────────────────────────────────────────────────── // ── Picker mode ───────────────────────────────────────────────────────────
var pickerActive = false; var pickerActive = false;
var pickerHighlighted = null; var pickerHighlighted = null;
var capturedSnippet = ''; var nextPickNum = 1;
var pickedElements = {}; // { [num]: { selector, snippet } }
function getSelector(el) { function getSelector(el) {
if (!el || el === document.body) return 'body'; if (!el || el === document.body) return 'body';
@ -332,7 +337,30 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => {
toast.classList.add('active'); toast.classList.add('active');
} }
function deactivatePicker(snippet) { function insertAtCursor(ta, text) {
var start = ta.selectionStart != null ? ta.selectionStart : ta.value.length;
var end = ta.selectionEnd != null ? ta.selectionEnd : ta.value.length;
ta.value = ta.value.slice(0, start) + text + ta.value.slice(end);
ta.selectionStart = ta.selectionEnd = start + text.length;
}
function addPickedChip(num, selector, snippet) {
var list = document.getElementById('_fb-picked-list');
var chip = document.createElement('span');
chip.className = 'fb-picked-chip';
chip.dataset.num = num;
chip.innerHTML =
'<span class="fb-chip-num">[' + num + ']</span>' +
'<span class="fb-chip-label">' + esc(selector) + '</span>' +
'<button class="fb-chip-remove" title="Remove">✕</button>';
chip.querySelector('.fb-chip-remove').addEventListener('click', function() {
delete pickedElements[num];
chip.remove();
});
list.appendChild(chip);
}
function deactivatePicker(snippet, selector) {
pickerActive = false; pickerActive = false;
shield.classList.remove('active'); shield.classList.remove('active');
toast.classList.remove('active'); toast.classList.remove('active');
@ -342,14 +370,14 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => {
} }
overlay.classList.add('open'); overlay.classList.add('open');
if (snippet) { if (snippet) {
capturedSnippet = snippet; var num = nextPickNum++;
document.getElementById('_fb-snippet').textContent = snippet; pickedElements[num] = { selector: selector, snippet: snippet };
document.getElementById('_fb-snippet-wrap').style.display = ''; addPickedChip(num, selector, snippet);
document.getElementById('_fb-pick-hint').textContent = '✓ Element captured — you can pick again'; // Insert reference token at cursor in the textarea
// Append to description
var desc = document.getElementById('_fb-desc'); var desc = document.getElementById('_fb-desc');
var tag = '\\n\\n**Picked element:**\\n\`\`\`html\\n' + snippet + '\\n\`\`\`'; var token = '[element ' + num + ']';
if (!desc.value.includes(tag)) desc.value += tag; insertAtCursor(desc, token);
desc.focus();
} }
} }
@ -383,15 +411,15 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => {
shield.style.display = ''; shield.style.display = '';
if (el && !el.closest('#_fb-picker-toast')) { if (el && !el.closest('#_fb-picker-toast')) {
el.classList.remove('_fb-hover-highlight'); el.classList.remove('_fb-hover-highlight');
deactivatePicker(getSnippet(el)); deactivatePicker(getSnippet(el), getSelector(el));
} else { } else {
deactivatePicker(null); deactivatePicker(null, null);
} }
}); });
// Esc cancels picker // Esc cancels picker
document.addEventListener('keydown', function(e) { document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && pickerActive) deactivatePicker(null); if (e.key === 'Escape' && pickerActive) deactivatePicker(null, null);
}); });
// ── Submit ──────────────────────────────────────────────────────────────── // ── Submit ────────────────────────────────────────────────────────────────
@ -403,9 +431,16 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => {
var btn = document.getElementById('_fb-submit'); var btn = document.getElementById('_fb-submit');
if (!title) { status.style.color='#f57c7c'; status.textContent='Title is required.'; return; } if (!title) { status.style.color='#f57c7c'; status.textContent='Title is required.'; return; }
btn.disabled = true; status.style.color='#888'; status.textContent='Submitting…'; btn.disabled = true; status.style.color='#888'; status.textContent='Submitting…';
// Append element snippets as a reference block
var snippetBlock = '';
Object.keys(pickedElements).forEach(function(num) {
var pe = pickedElements[num];
snippetBlock += '\\n\\n**[element ' + num + ']** `' + pe.selector + '`\\n\`\`\`html\\n' + pe.snippet + '\\n\`\`\`';
});
var fullBody = body + snippetBlock;
fetch(SELF + '/api/feedback', { fetch(SELF + '/api/feedback', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, body, repo: cfg.repo, source_url: window.location.href, assignee: assignee || undefined }) body: JSON.stringify({ title, body: fullBody, repo: cfg.repo, source_url: window.location.href, assignee: assignee || undefined })
}).then(function(r) { return r.json(); }).then(function(d) { }).then(function(r) { return r.json(); }).then(function(d) {
if (d.ok) { if (d.ok) {
status.style.color='#6fcf6f'; status.style.color='#6fcf6f';
@ -413,9 +448,9 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => {
document.getElementById('_fb-title').value = ''; document.getElementById('_fb-title').value = '';
document.getElementById('_fb-desc').value = ''; document.getElementById('_fb-desc').value = '';
document.getElementById('_fb-assignee').value = ''; document.getElementById('_fb-assignee').value = '';
document.getElementById('_fb-snippet-wrap').style.display = 'none'; document.getElementById('_fb-picked-list').innerHTML = '';
document.getElementById('_fb-pick-hint').textContent = 'Click to reference a page element'; nextPickNum = 1;
capturedSnippet = ''; pickedElements = {};
btn.disabled = false; btn.disabled = false;
setTimeout(function() { status.textContent=''; }, 3000); setTimeout(function() { status.textContent=''; }, 3000);
} else { } else {