mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-15 19:32:21 +02:00
* refactor(notes): decompose Notes into feature-owned modules * fix(notes): avoid HTML interpolation in reminder picker * docs(notes): drop checkpoint archives, session logs and publication drafts The N0-N6 tarballs and raw pytest/node logs are not read by any script or test, and the logs carry absolute home-directory paths. Once squashed into dev they would stay in history for good. notes-pr.md and notes-issue.md were the drafts used to open #6712 and #6713, not documentation. notes-validation.md keeps the counts, the checkpoint hashes and implementation.sha256 as the record; local interpreter paths and the local Docker image name are replaced with portable placeholders. * fix(notes): keep the state store raw and stop masonry re-observing after close - state: writes built from views (filter, spread, splice) stored nested read-only views in the store, so structuredClone on a stored note threw DataCloneError. Store writes now unwrap nested views. - layout: a save that resolves after closePanel re-renders the leaving pane and re-created the masonry ResizeObserver that teardown had disposed. Skip creating it once Notes is closed. - index.js: drop 30 imported or declared names that were never used and group the imports at the top. Import order is unchanged. - Revert unrelated punctuation edits in .gitattributes and keep the header comment and docstring first in compare/vote.js and test_notes_dom_xss_helpers.py. Both fixes have a node test that fails on the previous head. * test(notes): make script rejection assertion case-insensitive Address CodeQL js/bad-tag-filter alert #1150 in test-only assertion and refresh Notes evidence manifest checksum. --------- Co-authored-by: Léo <leograndcontact@gmail.com>
83 lines
3.6 KiB
JavaScript
83 lines
3.6 KiB
JavaScript
/** Note clipboard feedback and the Copy/Agent corner menu. */
|
|
import { _serializeNoteForCopy } from '../domain/noteRules.js';
|
|
export function createClipboard({
|
|
_agentSolveNote,
|
|
bindMenuDismiss,
|
|
dismissOrRemove,
|
|
state,
|
|
topPortalZ,
|
|
uiModule
|
|
}) {
|
|
function _openNoteCornerMenu(btn) {
|
|
document.querySelectorAll('.note-corner-menu-dropdown').forEach(dismissOrRemove);
|
|
const id = btn.dataset.noteId;
|
|
const note = state.notes.find(n => n.id === id);
|
|
if (!note) return;
|
|
const menu = document.createElement('div');
|
|
menu.className = 'note-corner-menu-dropdown';
|
|
menu.innerHTML = `
|
|
<button type="button" class="ncm-item" data-act="copy">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
<span>Copy</span>
|
|
</button>
|
|
<button type="button" class="ncm-item" data-act="agent">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8V4H8"/><rect x="4" y="8" width="16" height="12" rx="2"/><path d="M2 14h2M20 14h2M15 13v2M9 13v2"/></svg>
|
|
<span>${note.agent_session_id ? 'Re-run agent' : 'Agent: solve this'}</span>
|
|
</button>`;
|
|
document.body.appendChild(menu);
|
|
const r = btn.getBoundingClientRect();
|
|
// Right-align to the ⋯ button, clamped to the viewport.
|
|
const mw = 168;
|
|
let left = Math.min(r.right - mw, window.innerWidth - mw - 8);
|
|
left = Math.max(8, left);
|
|
// Drop down by default; flip up if there isn't room below (the button
|
|
// sits at the card's bottom edge now).
|
|
const mh = menu.offsetHeight || 96;
|
|
const below = window.innerHeight - r.bottom;
|
|
const top = (below < mh + 8 && r.top > mh + 8) ? (r.top - mh - 4) : (r.bottom + 4);
|
|
menu.style.cssText += `position:fixed;z-index:${topPortalZ()};top:${Math.round(top)}px;left:${Math.round(left)}px;`;
|
|
const close = bindMenuDismiss(menu, () => { menu.remove(); });
|
|
menu.querySelector('[data-act="copy"]').addEventListener('click', () => { close(); _copyNote(id, btn); });
|
|
menu.querySelector('[data-act="agent"]').addEventListener('click', () => { close(); _agentSolveNote(id); });
|
|
}
|
|
|
|
async function _copyNote(noteId, btnEl) {
|
|
const note = state.notes.find(n => n.id === noteId);
|
|
if (!note) return false;
|
|
const text = _serializeNoteForCopy(note);
|
|
if (!text) return false;
|
|
let ok = false;
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
ok = true;
|
|
} catch {
|
|
const ta = document.createElement('textarea');
|
|
ta.value = text;
|
|
ta.style.position = 'fixed'; ta.style.left = '-9999px';
|
|
document.body.appendChild(ta);
|
|
ta.select();
|
|
try { ok = document.execCommand('copy'); } catch { ok = false; }
|
|
ta.remove();
|
|
}
|
|
if (ok) {
|
|
if (btnEl && !btnEl._copyFlashing) {
|
|
const original = btnEl.innerHTML;
|
|
btnEl._copyFlashing = true;
|
|
btnEl.innerHTML = '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
|
|
btnEl.classList.add('copied');
|
|
setTimeout(() => {
|
|
btnEl.innerHTML = original;
|
|
btnEl.classList.remove('copied');
|
|
btnEl._copyFlashing = false;
|
|
}, 1200);
|
|
}
|
|
uiModule.showToast?.('Copied');
|
|
} else {
|
|
uiModule.showError?.('Copy failed');
|
|
}
|
|
return ok;
|
|
}
|
|
|
|
return { _openNoteCornerMenu, _copyNote };
|
|
}
|