// 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();
})();