|
|
|
<!-- ══════════ ANNOTATION SYSTEM ══════════ --> |
|
<!-- Inject this before </body> in any HTML planning doc --> |
|
|
|
<style> |
|
/* ── Annotation System ── */ |
|
|
|
.annotatable:hover { |
|
outline: 2px dashed rgba(37,99,235,0.3); |
|
outline-offset: 4px; |
|
cursor: pointer; |
|
} |
|
|
|
pre.annotatable code { |
|
cursor: pointer; |
|
pointer-events: auto; |
|
} |
|
|
|
pre.annotatable:hover code { |
|
cursor: pointer; |
|
} |
|
|
|
img.annotated { |
|
border-left: none !important; |
|
padding-left: 0 !important; |
|
outline: 3px solid #b45309; |
|
outline-offset: 4px; |
|
} |
|
|
|
.annotated { |
|
border-left: 3px solid #b45309 !important; |
|
padding-left: 12px !important; |
|
background: rgba(180,83,9,0.04); |
|
} |
|
|
|
.annotation-badge { |
|
display: inline-block; |
|
font-family: -apple-system, BlinkMacSystemFont, 'Inter', system-ui, sans-serif; |
|
font-size: 11px; |
|
font-weight: 600; |
|
color: #b45309; |
|
background: rgba(180,83,9,0.08); |
|
padding: 2px 8px; |
|
border-radius: 3px; |
|
margin-left: 8px; |
|
cursor: pointer; |
|
vertical-align: middle; |
|
} |
|
|
|
.annotation-input-container { |
|
background: #fff; |
|
border: 2px solid #b45309; |
|
border-radius: 6px; |
|
padding: 12px 16px; |
|
margin: 8px 0 16px; |
|
font-family: -apple-system, BlinkMacSystemFont, 'Inter', system-ui, sans-serif; |
|
box-shadow: 0 4px 12px rgba(0,0,0,0.08); |
|
} |
|
|
|
.annotation-input-container textarea { |
|
width: 100%; |
|
min-height: 60px; |
|
border: 1px solid #e5e5e5; |
|
border-radius: 4px; |
|
padding: 8px 10px; |
|
font-family: -apple-system, BlinkMacSystemFont, 'Inter', system-ui, sans-serif; |
|
font-size: 13px; |
|
line-height: 1.5; |
|
resize: vertical; |
|
outline: none; |
|
} |
|
|
|
.annotation-input-container textarea:focus { |
|
border-color: #b45309; |
|
} |
|
|
|
.annotation-input-container .annotation-actions { |
|
display: flex; |
|
gap: 8px; |
|
margin-top: 8px; |
|
justify-content: flex-end; |
|
} |
|
|
|
.annotation-input-container button { |
|
font-family: -apple-system, BlinkMacSystemFont, 'Inter', system-ui, sans-serif; |
|
font-size: 12px; |
|
font-weight: 600; |
|
padding: 5px 14px; |
|
border-radius: 4px; |
|
border: none; |
|
cursor: pointer; |
|
} |
|
|
|
.annotation-input-container .btn-save { |
|
background: #b45309; |
|
color: #fff; |
|
} |
|
|
|
.annotation-input-container .btn-cancel { |
|
background: #f5f5f5; |
|
color: #4a4a4a; |
|
} |
|
|
|
.annotation-input-container .btn-delete { |
|
background: rgba(220,38,38,0.08); |
|
color: #dc2626; |
|
margin-right: auto; |
|
} |
|
|
|
#annotation-fab { |
|
position: fixed; |
|
bottom: 24px; |
|
right: 24px; |
|
font-family: -apple-system, BlinkMacSystemFont, 'Inter', system-ui, sans-serif; |
|
font-size: 13px; |
|
font-weight: 600; |
|
background: #b45309; |
|
color: #fff; |
|
border: none; |
|
padding: 10px 20px; |
|
border-radius: 24px; |
|
cursor: pointer; |
|
box-shadow: 0 4px 16px rgba(180,83,9,0.3); |
|
z-index: 1000; |
|
transition: transform 0.15s; |
|
} |
|
|
|
#annotation-fab:hover { transform: scale(1.05); } |
|
|
|
#annotation-panel { |
|
position: fixed; |
|
top: 0; |
|
right: 0; |
|
width: 400px; |
|
height: 100vh; |
|
background: #fff; |
|
border-left: 1px solid #e5e5e5; |
|
box-shadow: -4px 0 20px rgba(0,0,0,0.08); |
|
z-index: 1001; |
|
display: none; |
|
flex-direction: column; |
|
font-family: -apple-system, BlinkMacSystemFont, 'Inter', system-ui, sans-serif; |
|
} |
|
|
|
#annotation-panel.open { display: flex; } |
|
|
|
#annotation-panel-header { |
|
padding: 20px 24px 16px; |
|
border-bottom: 1px solid #e5e5e5; |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
} |
|
|
|
#annotation-panel-header h3 { |
|
font-size: 15px; |
|
font-weight: 700; |
|
margin: 0; |
|
} |
|
|
|
#annotation-panel-header .panel-actions { |
|
display: flex; |
|
gap: 8px; |
|
} |
|
|
|
#annotation-panel-header button { |
|
font-size: 12px; |
|
font-weight: 600; |
|
padding: 5px 12px; |
|
border-radius: 4px; |
|
border: 1px solid #e5e5e5; |
|
background: #fff; |
|
cursor: pointer; |
|
color: #4a4a4a; |
|
} |
|
|
|
#annotation-panel-header button:hover { |
|
background: #f5f5f5; |
|
} |
|
|
|
#annotation-panel-header .btn-copy { |
|
background: #b45309; |
|
color: #fff; |
|
border: none; |
|
} |
|
|
|
#annotation-panel-body { |
|
flex: 1; |
|
overflow-y: auto; |
|
padding: 16px 24px; |
|
} |
|
|
|
.panel-annotation { |
|
padding: 12px 0; |
|
border-bottom: 1px solid #e5e5e5; |
|
cursor: pointer; |
|
} |
|
|
|
.panel-annotation:hover { |
|
background: #f5f5f5; |
|
margin: 0 -24px; |
|
padding: 12px 24px; |
|
} |
|
|
|
.panel-annotation-section { |
|
font-size: 11px; |
|
font-weight: 600; |
|
text-transform: uppercase; |
|
letter-spacing: 0.04em; |
|
color: #b45309; |
|
margin-bottom: 4px; |
|
} |
|
|
|
.panel-annotation-context { |
|
font-size: 12px; |
|
color: #717171; |
|
margin-bottom: 6px; |
|
font-style: italic; |
|
white-space: nowrap; |
|
overflow: hidden; |
|
text-overflow: ellipsis; |
|
} |
|
|
|
.panel-annotation-note { |
|
font-size: 13px; |
|
color: #1a1a1a; |
|
line-height: 1.5; |
|
} |
|
|
|
.panel-empty { |
|
color: #717171; |
|
font-size: 14px; |
|
text-align: center; |
|
padding: 40px 0; |
|
} |
|
|
|
#copy-toast { |
|
position: fixed; |
|
bottom: 72px; |
|
right: 24px; |
|
background: #1a1a1a; |
|
color: #fff; |
|
font-family: -apple-system, BlinkMacSystemFont, 'Inter', system-ui, sans-serif; |
|
font-size: 13px; |
|
font-weight: 500; |
|
padding: 8px 16px; |
|
border-radius: 6px; |
|
z-index: 1002; |
|
display: none; |
|
} |
|
|
|
@media print { |
|
#annotation-fab, #annotation-panel, #copy-toast { display: none !important; } |
|
.annotatable:hover { outline: none; } |
|
} |
|
</style> |
|
|
|
<button id="annotation-fab" onclick="togglePanel()">Annotations (0)</button> |
|
|
|
<div id="annotation-panel"> |
|
<div id="annotation-panel-header"> |
|
<h3>Annotations</h3> |
|
<div class="panel-actions"> |
|
<button class="btn-copy" onclick="copyAnnotations()">Copy All</button> |
|
<button onclick="togglePanel()">Close</button> |
|
</div> |
|
</div> |
|
<div id="annotation-panel-body"></div> |
|
</div> |
|
|
|
<div id="copy-toast">Copied to clipboard</div> |
|
|
|
<script> |
|
(function() { |
|
// Use the page filename as the storage key so annotations don't collide |
|
const STORAGE_KEY = 'annotations-' + location.pathname.split('/').pop().replace('.html', ''); |
|
|
|
function loadAnnotations() { |
|
try { return JSON.parse(localStorage.getItem(STORAGE_KEY)) || []; } |
|
catch { return []; } |
|
} |
|
|
|
function saveAnnotations(annotations) { |
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(annotations)); |
|
updateFab(); |
|
updatePanel(); |
|
} |
|
|
|
function findPreviousHeading(el) { |
|
let node = el.previousElementSibling; |
|
while (node) { |
|
if (/^H[2-4]$/.test(node.tagName)) return node; |
|
node = node.previousElementSibling; |
|
} |
|
let parent = el.parentElement; |
|
while (parent && parent !== document.body) { |
|
node = parent.previousElementSibling; |
|
while (node) { |
|
if (/^H[2-4]$/.test(node.tagName)) return node; |
|
node = node.previousElementSibling; |
|
} |
|
parent = parent.parentElement; |
|
} |
|
return null; |
|
} |
|
|
|
function getSection(el) { |
|
const heading = el.closest('h2, h3, h4') || findPreviousHeading(el); |
|
return heading?.textContent?.trim() || 'Document'; |
|
} |
|
|
|
function getPreview(el) { |
|
if (el.tagName === 'IMG') { |
|
return '[Image: ' + (el.alt || el.src.split('/').pop()) + ']'; |
|
} |
|
return el.textContent.trim().slice(0, 100); |
|
} |
|
|
|
// Find the content container — use .page if it exists, otherwise body |
|
function getContainer() { |
|
return document.querySelector('.page') |
|
|| document.querySelector('main') |
|
|| document.querySelector('article') |
|
|| document.body; |
|
} |
|
|
|
function init() { |
|
const container = getContainer(); |
|
const targets = container.querySelectorAll('p, h2, h3, h4, li, pre, td, tr, blockquote, figcaption, dt, dd, summary, img, figure'); |
|
|
|
targets.forEach((el, idx) => { |
|
el.classList.add('annotatable'); |
|
el.dataset.annotationIdx = idx; |
|
|
|
el.addEventListener('click', (e) => { |
|
if (e.target.closest('a, button, .annotation-input-container, .annotation-badge, input, textarea, select')) return; |
|
e.stopPropagation(); |
|
openAnnotationInput(el, idx); |
|
}); |
|
}); |
|
|
|
// Make code blocks inside pre annotatable via their parent pre |
|
// Clicks on <code> inside <pre> need pointer-events to pass through |
|
container.querySelectorAll('pre > code').forEach((codeEl) => { |
|
codeEl.addEventListener('click', (e) => { |
|
if (e.target.closest('a, button, .annotation-input-container, .annotation-badge, input, textarea, select')) return; |
|
const pre = codeEl.closest('pre'); |
|
if (pre && pre.dataset.annotationIdx != null) { |
|
e.stopPropagation(); |
|
openAnnotationInput(pre, parseInt(pre.dataset.annotationIdx)); |
|
} |
|
}); |
|
}); |
|
|
|
// Also make callout/verdict/cost-block divs annotatable |
|
container.querySelectorAll('.callout, .verdict, .cost-block, .cost-line').forEach((el) => { |
|
if (!el.dataset.annotationIdx) { |
|
const idx = targets.length + Array.from(container.querySelectorAll('.callout, .verdict, .cost-block, .cost-line')).indexOf(el); |
|
el.classList.add('annotatable'); |
|
el.dataset.annotationIdx = idx; |
|
el.addEventListener('click', (e) => { |
|
if (e.target.closest('a, button, .annotation-input-container, .annotation-badge, input, textarea, select')) return; |
|
e.stopPropagation(); |
|
openAnnotationInput(el, idx); |
|
}); |
|
} |
|
}); |
|
|
|
// Restore existing annotations |
|
const annotations = loadAnnotations(); |
|
annotations.forEach(a => { |
|
const el = container.querySelector(`[data-annotation-idx="${a.idx}"]`); |
|
if (el) markAnnotated(el, a); |
|
}); |
|
|
|
updateFab(); |
|
} |
|
|
|
function markAnnotated(el, annotation) { |
|
el.classList.add('annotated'); |
|
// Remove old badge — check both inside and after the element |
|
el.querySelectorAll('.annotation-badge').forEach(b => b.remove()); |
|
if (el.nextElementSibling?.classList.contains('annotation-badge')) el.nextElementSibling.remove(); |
|
|
|
const badge = document.createElement('span'); |
|
badge.className = 'annotation-badge'; |
|
badge.textContent = 'annotated'; |
|
badge.onclick = (e) => { e.stopPropagation(); openAnnotationInput(el, annotation.idx); }; |
|
|
|
// Images are void elements — place badge after, not inside |
|
if (el.tagName === 'IMG') { |
|
el.after(badge); |
|
} else { |
|
el.appendChild(badge); |
|
} |
|
} |
|
|
|
function openAnnotationInput(el, idx) { |
|
document.querySelectorAll('.annotation-input-container').forEach(c => c.remove()); |
|
|
|
const annotations = loadAnnotations(); |
|
const existing = annotations.find(a => a.idx === idx); |
|
|
|
const container = document.createElement('div'); |
|
container.className = 'annotation-input-container'; |
|
|
|
const textarea = document.createElement('textarea'); |
|
textarea.placeholder = 'Add your annotation...'; |
|
textarea.value = existing?.note || ''; |
|
container.appendChild(textarea); |
|
|
|
const actions = document.createElement('div'); |
|
actions.className = 'annotation-actions'; |
|
|
|
if (existing) { |
|
const deleteBtn = document.createElement('button'); |
|
deleteBtn.className = 'btn-delete'; |
|
deleteBtn.textContent = 'Remove'; |
|
deleteBtn.onclick = () => { |
|
const updated = annotations.filter(a => a.idx !== idx); |
|
saveAnnotations(updated); |
|
el.classList.remove('annotated'); |
|
el.querySelectorAll('.annotation-badge').forEach(b => b.remove()); |
|
container.remove(); |
|
}; |
|
actions.appendChild(deleteBtn); |
|
} |
|
|
|
const cancelBtn = document.createElement('button'); |
|
cancelBtn.className = 'btn-cancel'; |
|
cancelBtn.textContent = 'Cancel'; |
|
cancelBtn.onclick = () => container.remove(); |
|
actions.appendChild(cancelBtn); |
|
|
|
const saveBtn = document.createElement('button'); |
|
saveBtn.className = 'btn-save'; |
|
saveBtn.textContent = 'Save'; |
|
saveBtn.onclick = () => { |
|
const note = textarea.value.trim(); |
|
if (!note) { container.remove(); return; } |
|
|
|
const updated = annotations.filter(a => a.idx !== idx); |
|
const entry = { |
|
idx, |
|
section: getSection(el), |
|
context: getPreview(el), |
|
note, |
|
timestamp: new Date().toISOString() |
|
}; |
|
updated.push(entry); |
|
updated.sort((a, b) => a.idx - b.idx); |
|
saveAnnotations(updated); |
|
markAnnotated(el, entry); |
|
container.remove(); |
|
}; |
|
actions.appendChild(saveBtn); |
|
|
|
container.appendChild(actions); |
|
el.after(container); |
|
textarea.focus(); |
|
} |
|
|
|
function updateFab() { |
|
const count = loadAnnotations().length; |
|
document.getElementById('annotation-fab').textContent = `Annotations (${count})`; |
|
} |
|
|
|
window.togglePanel = function() { |
|
const panel = document.getElementById('annotation-panel'); |
|
panel.classList.toggle('open'); |
|
if (panel.classList.contains('open')) updatePanel(); |
|
}; |
|
|
|
function updatePanel() { |
|
const body = document.getElementById('annotation-panel-body'); |
|
const annotations = loadAnnotations(); |
|
|
|
if (annotations.length === 0) { |
|
body.innerHTML = '<div class="panel-empty">No annotations yet.<br>Click any paragraph to add one.</div>'; |
|
return; |
|
} |
|
|
|
body.innerHTML = annotations.map(a => ` |
|
<div class="panel-annotation" onclick="scrollToAnnotation(${a.idx})"> |
|
<div class="panel-annotation-section">${escHtml(a.section)}</div> |
|
<div class="panel-annotation-context">"${escHtml(a.context)}"</div> |
|
<div class="panel-annotation-note">${escHtml(a.note)}</div> |
|
</div> |
|
`).join(''); |
|
} |
|
|
|
window.scrollToAnnotation = function(idx) { |
|
const el = document.querySelector(`[data-annotation-idx="${idx}"]`); |
|
if (el) { |
|
el.scrollIntoView({ behavior: 'smooth', block: 'center' }); |
|
el.style.outline = '2px solid #b45309'; |
|
setTimeout(() => el.style.outline = '', 2000); |
|
} |
|
}; |
|
|
|
window.copyAnnotations = function() { |
|
const annotations = loadAnnotations(); |
|
if (annotations.length === 0) return; |
|
|
|
const docTitle = document.title || location.pathname.split('/').pop(); |
|
const text = annotations.map((a, i) => |
|
`[${i + 1}] Section: ${a.section}\n` + |
|
` Context: "${a.context}"\n` + |
|
` Note: ${a.note}` |
|
).join('\n\n'); |
|
|
|
const header = `# Annotations on ${docTitle}\n# ${annotations.length} annotations — ${new Date().toLocaleDateString()}\n\n`; |
|
|
|
navigator.clipboard.writeText(header + text).then(() => { |
|
const toast = document.getElementById('copy-toast'); |
|
toast.style.display = 'block'; |
|
setTimeout(() => toast.style.display = 'none', 2000); |
|
}); |
|
}; |
|
|
|
function escHtml(s) { |
|
const d = document.createElement('div'); |
|
d.textContent = s; |
|
return d.innerHTML; |
|
} |
|
|
|
if (document.readyState === 'loading') { |
|
document.addEventListener('DOMContentLoaded', init); |
|
} else { |
|
init(); |
|
} |
|
})(); |
|
</script> |
|
<!-- ══════════ END ANNOTATION SYSTEM ══════════ --> |