📜
stitcher.js
Back
📝 Javascript ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
// stitcher.js — Option B: Constructable Stylesheet (with fallback) window.App = window.App || {}; (() => { /* ---------- CSS (scoped to this module) ---------- */ const stitcherCSS = ` /* Editable code textareas */ .stitcher-textarea { width: 100%; min-height: 140px; max-height: 50vh; resize: vertical; padding: 0.75rem; border-radius: 0.5rem; border: 1px solid #3f3f46; /* zinc-700-ish */ background: #0b1220; /* dark code bg */ color: #e5e7eb; font-family: 'Courier New', monospace; font-size: 0.85rem; line-height: 1.5; overflow-x: auto; overflow-wrap: anywhere; word-break: break-word; } @media (max-width: 640px) { .stitcher-textarea { min-height: 120px; font-size: 0.8rem; } } /* Stitcher card container */ .stitcher-card { border: 1px solid rgba(0,0,0,.08); border-color: rgb(228 228 231 / 1); /* zinc-200 */ background: #f8fafc; /* zinc-50 */ color: inherit; border-radius: 0.5rem; padding: 0.75rem; } .dark .stitcher-card { border-color: rgb(39 39 42 / 1); /* zinc-800 */ background: #27272a; /* zinc-800 */ } .stitcher-card__header { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; margin-bottom: 0.5rem; } .stitcher-card__title { font-size: 0.9rem; font-weight: 600; } .stitcher-btn { font-size: 0.75rem; padding: 0.25rem 0.5rem; border-radius: 0.375rem; border: 1px solid #d4d4d8; /* zinc-300 */ background: #fafafa; } .dark .stitcher-btn { border-color: #3f3f46; /* zinc-700 */ background: #3f3f46; /* zinc-700 */ color: #fff; } .stitcher-btn--danger { background: #dc2626; color: white; border-color: #b91c1c; } .stitcher-btn[disabled] { opacity: .5; cursor: not-allowed; } `; // Apply CSS: constructable stylesheet with graceful fallback (function applyStyles(cssText) { try { if ('adoptedStyleSheets' in Document.prototype && 'replaceSync' in CSSStyleSheet.prototype) { const sheet = new CSSStyleSheet(); sheet.replaceSync(cssText); document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet]; } else { const style = document.createElement('style'); style.setAttribute('data-stitcher-inline', 'true'); style.textContent = cssText; document.head.appendChild(style); } } catch { const style = document.createElement('style'); style.setAttribute('data-stitcher-inline', 'true'); style.textContent = cssText; document.head.appendChild(style); } })(stitcherCSS); /* ---------- DOM refs ---------- */ const els = { stitcherOverlay: document.getElementById('stitcherOverlay'), openStitcher: document.getElementById('openStitcher'), closeStitcher: document.getElementById('closeStitcher'), stitcherBackdrop: document.getElementById('stitcherBackdrop'), stitcherContent: document.getElementById('stitcherContent'), stitcherEmpty: document.getElementById('stitcherEmpty'), stitcherCount: document.getElementById('stitcherCount'), clearStitcher: document.getElementById('clearStitcher'), copyStitched: document.getElementById('copyStitched'), }; // Ensure state shape const initStitcherState = () => { App.state = App.state || {}; App.state.stitcher = App.state.stitcher || { chunks: [], isOpen: false }; }; initStitcherState(); /* ---------- UI helpers ---------- */ function updateBadge() { const count = App.state.stitcher.chunks.length; if (count > 0) { els.stitcherCount.textContent = count; els.stitcherCount.classList.remove('hidden'); } else { els.stitcherCount.classList.add('hidden'); } } function render() { const chunks = App.state.stitcher.chunks; updateBadge(); if (!chunks.length) { els.stitcherEmpty.classList.remove('hidden'); els.stitcherContent.innerHTML = ` <div class="text-center text-zinc-500 text-sm" id="stitcherEmpty"> No code chunks added yet. Use the "Add to Stitcher" button on code blocks to add editable chunks. </div>`; return; } els.stitcherEmpty.classList.add('hidden'); els.stitcherContent.innerHTML = ''; chunks.forEach((chunk, index) => { const card = document.createElement('div'); card.className = 'stitcher-card'; const header = document.createElement('div'); header.className = 'stitcher-card__header'; const title = document.createElement('div'); title.className = 'stitcher-card__title'; title.textContent = `Chunk ${index + 1}${chunk.language ? ` (${String(chunk.language).toUpperCase()})` : ''}`; const actions = document.createElement('div'); actions.className = 'flex gap-2'; const up = document.createElement('button'); up.className = 'stitcher-btn'; up.textContent = '↑'; const down = document.createElement('button'); down.className = 'stitcher-btn'; down.textContent = '↓'; const remove = document.createElement('button'); remove.className = 'stitcher-btn stitcher-btn--danger'; remove.textContent = '×'; up.disabled = index === 0; down.disabled = index === chunks.length - 1; actions.append(up, down, remove); header.append(title, actions); const ta = document.createElement('textarea'); ta.className = 'stitcher-textarea'; ta.value = chunk.code || ''; ta.spellcheck = false; // Wire handlers ta.addEventListener('input', () => { App.state.stitcher.chunks[index].code = ta.value; App.saveState(App.state); }); up.addEventListener('click', () => { if (index > 0) { const tmp = App.state.stitcher.chunks[index]; App.state.stitcher.chunks[index] = App.state.stitcher.chunks[index - 1]; App.state.stitcher.chunks[index - 1] = tmp; App.saveState(App.state); render(); } }); down.addEventListener('click', () => { if (index < chunks.length - 1) { const tmp = App.state.stitcher.chunks[index]; App.state.stitcher.chunks[index] = App.state.stitcher.chunks[index + 1]; App.state.stitcher.chunks[index + 1] = tmp; App.saveState(App.state); render(); } }); remove.addEventListener('click', () => { App.state.stitcher.chunks.splice(index, 1); App.saveState(App.state); render(); if (App.renderTranscript) App.renderTranscript(); // update per-message "Add to Stitcher" states }); card.append(header, ta); els.stitcherContent.appendChild(card); }); } /* ---------- Public API for chat.js ---------- */ App.addToStitcher = function addToStitcher(code, language) { const i = App.state.stitcher.chunks.findIndex(c => c.code === code); if (i >= 0) { // Remove if already added App.state.stitcher.chunks.splice(i, 1); App.saveState(App.state); render(); if (App.renderTranscript) App.renderTranscript(); return false; // removed } // Add as editable block App.state.stitcher.chunks.push({ id: Date.now() + Math.random(), code: code || '', language: language || 'text', timestamp: Date.now() }); App.saveState(App.state); render(); if (App.renderTranscript) App.renderTranscript(); return true; // added }; /* ---------- Chrome wiring ---------- */ function open() { els.stitcherOverlay.classList.remove('hidden'); render(); } function close() { els.stitcherOverlay.classList.add('hidden'); } document.getElementById('openStitcher').addEventListener('click', open); document.getElementById('closeStitcher').addEventListener('click', close); document.getElementById('stitcherBackdrop').addEventListener('click', close); els.clearStitcher.addEventListener('click', () => { if (confirm('Clear all chunks?')) { App.state.stitcher.chunks = []; App.saveState(App.state); render(); if (App.renderTranscript) App.renderTranscript(); } }); els.copyStitched.addEventListener('click', () => { // Concatenate current textarea values (include edits) const vals = Array.from(els.stitcherContent.querySelectorAll('.stitcher-textarea')) .map(ta => ta.value.trim()); const combined = vals.filter(Boolean).join('\n\n'); if (!combined) return; if (navigator.clipboard?.writeText) { navigator.clipboard.writeText(combined).then(() => { els.copyStitched.textContent = 'Copied!'; setTimeout(() => (els.copyStitched.textContent = 'Copy All'), 1600); }); } else { // Fallback copy const t = document.createElement('textarea'); t.value = combined; t.style.position = 'fixed'; t.style.left = '-9999px'; t.style.top = '-9999px'; document.body.appendChild(t); t.focus(); t.select(); try { document.execCommand('copy'); } catch {} document.body.removeChild(t); els.copyStitched.textContent = 'Copied!'; setTimeout(() => (els.copyStitched.textContent = 'Copy All'), 1600); } }); window.addEventListener('keydown', (e) => { if (e.key === 'Escape') close(); }); // Initial badge on load updateBadge(); })();