Skip to content

Instantly share code, notes, and snippets.

@ryan-lingle
Created March 19, 2026 17:10
Show Gist options
  • Select an option

  • Save ryan-lingle/bcb2486593f3d1d35f24f3370ca8ffc6 to your computer and use it in GitHub Desktop.

Select an option

Save ryan-lingle/bcb2486593f3d1d35f24f3370ca8ffc6 to your computer and use it in GitHub Desktop.
Claude Code /annotate skill — click-to-annotate any HTML planning doc
<!-- ══════════ 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 ══════════ -->
name annotate
description Add annotation capabilities to any HTML planning doc. Inject click-to-annotate UI so the user can leave feedback on any element (text, images, tables, code blocks). User pastes copied annotations back to Claude for review.
allowed-tools Read, Edit, Bash
version 1.0.0

Annotate Skill

Add inline annotation capabilities to any HTML document so the user can click on any element (paragraphs, images, headings, table cells, code blocks) to leave feedback, then paste all annotations back to Claude.

Usage

/annotate <path-to-html-file>

Examples:

  • /annotate docs/venue-onboarding-plan.html
  • /annotate docs/sms-architecture-review.html

What It Does

  1. Reads the snippet from ~/.claude/skills/annotate/annotation-snippet.html
  2. Injects the annotation CSS + JS + UI into the target HTML file, right before </body>
  3. Opens the file in the browser

The annotation system provides:

  • Click any element (paragraphs, headings, images, tables, code blocks, list items) to add a note
  • Amber highlight on annotated elements with an "annotated" badge
  • Floating "Annotations (N)" button — opens a side panel listing all annotations
  • "Copy All" button — copies all annotations to clipboard in a structured format Claude can parse
  • localStorage persistence — annotations survive page refreshes (keyed by filename)

How the User Gives Feedback

  1. User opens the HTML doc and clicks elements to annotate
  2. User clicks "Annotations" button → opens panel → clicks "Copy All"
  3. User pastes the copied text into Claude chat
  4. Claude reads the structured annotations with section context and processes feedback

Copied Format

# Annotations on Venue Onboarding — Architecture Plan
# 3 annotations — 3/18/2026

[1] Section: Phase 2: SMS Integration
    Context: "[Image: SMS onboarding flow diagram]"
    Note: should we support WhatsApp as a fallback channel too?

[2] Section: Phase 3: Stripe Connect
    Context: "OAuth token refresh handling for venue payment accounts"
    Note: this is the hardest part, let's time-box it

Implementation

When this skill is invoked:

  1. Read the snippet file at ~/.claude/skills/annotate/annotation-snippet.html
  2. Read the target HTML file
  3. Check if the annotation system is already injected (look for ANNOTATION SYSTEM comment)
    • If already present, just open the file — don't double-inject
  4. If not present, inject the snippet content right before </body> using the Edit tool
  5. Open the file: open <path>
# Step 5: open in browser
open "$TARGET_FILE"

Removing Annotations

To remove the annotation system from a file, delete everything between:

<!-- ANNOTATION SYSTEM -->

and

<!-- END ANNOTATION SYSTEM -->

Notes

  • Annotations are stored in localStorage keyed by filename — they won't collide between different docs
  • Images get an amber outline instead of a left border when annotated
  • Image annotations show the alt text or filename as context in the copied output
  • The annotation UI is hidden when printing (@media print)
  • The snippet uses hardcoded colors (not CSS variables) so it works with any HTML doc's styling
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment