Skip to content

Instantly share code, notes, and snippets.

@jalehman
Created February 1, 2026 05:20
Show Gist options
  • Select an option

  • Save jalehman/4b64e77bec33c3169eff2d64756bfda5 to your computer and use it in GitHub Desktop.

Select an option

Save jalehman/4b64e77bec33c3169eff2d64756bfda5 to your computer and use it in GitHub Desktop.
Webdrop skill code review
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Webdrop Skill — Code Review</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #0d1117;
color: #c9d1d9;
min-height: 100vh;
padding: 20px;
line-height: 1.6;
}
.container {
max-width: 900px;
margin: 0 auto;
}
.header {
text-align: center;
padding: 30px 0 40px;
border-bottom: 1px solid #21262d;
margin-bottom: 30px;
}
.header h1 {
font-size: 1.8rem;
color: #f0f6fc;
margin-bottom: 8px;
}
.header .subtitle {
color: #8b949e;
font-size: 1rem;
}
.file-section {
margin-bottom: 40px;
}
.file-header {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: #161b22;
border: 1px solid #30363d;
border-bottom: none;
border-radius: 8px 8px 0 0;
}
.file-icon {
font-size: 1.2rem;
}
.file-name {
font-family: 'SF Mono', Monaco, monospace;
font-size: 0.9rem;
color: #58a6ff;
}
.file-meta {
margin-left: auto;
font-size: 0.8rem;
color: #8b949e;
}
.code-block {
background: #161b22;
border: 1px solid #30363d;
border-radius: 0 0 8px 8px;
overflow: auto;
max-height: 600px;
}
pre {
margin: 0;
padding: 16px;
font-family: 'SF Mono', Monaco, Consolas, monospace;
font-size: 13px;
line-height: 1.5;
white-space: pre;
overflow-x: auto;
}
/* Markdown styling */
.markdown-content {
padding: 20px;
background: #161b22;
border: 1px solid #30363d;
border-radius: 0 0 8px 8px;
}
.markdown-content h1 {
font-size: 1.5rem;
color: #f0f6fc;
border-bottom: 1px solid #21262d;
padding-bottom: 8px;
margin-bottom: 16px;
}
.markdown-content h2 {
font-size: 1.2rem;
color: #f0f6fc;
margin-top: 24px;
margin-bottom: 12px;
}
.markdown-content h3 {
font-size: 1rem;
color: #f0f6fc;
margin-top: 20px;
margin-bottom: 8px;
}
.markdown-content p {
margin-bottom: 12px;
}
.markdown-content code {
background: #21262d;
padding: 2px 6px;
border-radius: 4px;
font-family: 'SF Mono', Monaco, monospace;
font-size: 0.9em;
}
.markdown-content pre {
background: #0d1117;
border: 1px solid #30363d;
border-radius: 6px;
padding: 12px;
margin: 12px 0;
overflow-x: auto;
}
.markdown-content pre code {
background: none;
padding: 0;
}
.markdown-content ul, .markdown-content ol {
margin-left: 24px;
margin-bottom: 12px;
}
.markdown-content li {
margin-bottom: 4px;
}
.markdown-content strong {
color: #f0f6fc;
}
.markdown-content blockquote {
border-left: 3px solid #3b82f6;
padding-left: 16px;
margin: 12px 0;
color: #8b949e;
}
.toc {
background: #161b22;
border: 1px solid #30363d;
border-radius: 8px;
padding: 16px 20px;
margin-bottom: 30px;
}
.toc h3 {
font-size: 0.9rem;
color: #8b949e;
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 1px;
}
.toc ul {
list-style: none;
}
.toc li {
margin-bottom: 8px;
}
.toc a {
color: #58a6ff;
text-decoration: none;
}
.toc a:hover {
text-decoration: underline;
}
.stats {
display: flex;
gap: 20px;
margin-top: 12px;
}
.stat {
background: #21262d;
padding: 8px 14px;
border-radius: 6px;
font-size: 0.85rem;
}
.stat-label {
color: #8b949e;
}
.stat-value {
color: #58a6ff;
font-weight: 600;
}
/* Syntax highlighting */
.kw { color: #ff7b72; }
.fn { color: #d2a8ff; }
.str { color: #a5d6ff; }
.cmt { color: #8b949e; }
.num { color: #79c0ff; }
.op { color: #c9d1d9; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📁 webdrop skill</h1>
<div class="subtitle">skills/html-preview/ — Code Review</div>
<div class="stats">
<div class="stat">
<span class="stat-label">Files:</span>
<span class="stat-value">2</span>
</div>
<div class="stat">
<span class="stat-label">SKILL.md:</span>
<span class="stat-value">3.9 KB</span>
</div>
<div class="stat">
<span class="stat-label">annotate.js:</span>
<span class="stat-value">28.3 KB</span>
</div>
</div>
</div>
<div class="toc">
<h3>Contents</h3>
<ul>
<li><a href="#skill-md">SKILL.md</a> — Skill documentation & workflow</li>
<li><a href="#annotate-js">annotate.js</a> — Client-side annotation system</li>
</ul>
</div>
<div class="file-section" id="skill-md">
<div class="file-header">
<span class="file-icon">📄</span>
<span class="file-name">SKILL.md</span>
<span class="file-meta">3.9 KB</span>
</div>
<div class="code-block">
<pre>---
name: html-preview
description: Create HTML pages and serve them via GitHub Gist + githack for instant mobile/shareable preview. Use when the user asks to build an HTML document, page, or visualization and wants to view it in a browser — especially from a phone or share a link.
---
# HTML Preview via Gist + githack
Create HTML pages and make them instantly viewable via a public URL.
## Workflow
### 1. Create the HTML file
Write the HTML to the project or a temp location. Self-contained (inline CSS/JS, no external deps) works best.
### 2. Upload to GitHub Gist
```bash
gh gist create path/to/file.html --public -d "Description"
```
Captures the gist URL (e.g. `https://gist.github.com/USER/HASH`).
### 3. Build the preview URL
Use **raw.githack.com** to serve the gist as a rendered HTML page:
```
https://gist.githack.com/USER/HASH/raw/FILENAME.html
```
Example:
- Gist: `https://gist.github.com/jalehman/abc123def456`
- Preview: `https://gist.githack.com/jalehman/abc123def456/raw/my-page.html`
### 4. Send the preview link
Send the githack URL to the user. Works on any device — phone, tablet, desktop.
### 5. Updating
**⚠️ Caching Warning:** githack aggressively caches content. Using `gh gist edit` to update a gist won't reliably update the preview — users may see stale content for hours.
**Option A: Fresh gist each time (recommended for iterations)**
```bash
# Delete old gist and create new one
gh gist delete OLD_GIST_ID
gh gist create path/to/file.html --public -d "Description v2"
```
This gives a new URL, but guarantees fresh content.
**Option B: Cache-busting query param**
```bash
gh gist edit GIST_ID -a path/to/file.html
```
Then append a cache-buster to the URL:
```
https://gist.githack.com/USER/HASH/raw/file.html?v=2
```
Increment `?v=N` each update. Not always reliable.
**Option C: Use rawcdn.githack.com with commit hash**
```
https://rawcdn.githack.com/USER/HASH/COMMIT_SHA/file.html
```
Pin to specific commit. Most reliable for versioned content, but requires knowing the commit SHA.
**Best practice:** For iterative work, just create fresh gists. For stable/final content, use the commit-pinned URL.
## Annotations (Inline Feedback)
Enable the user to annotate previews with inline comments, reactions, and feedback that can be exported and sent back to you.
### Adding Annotation Support
Include the annotation script at the end of your HTML body:
```html
&lt;script src="https://gist.githack.com/jalehman/ANNOTATION_GIST_ID/raw/annotate.js"&gt;&lt;/script&gt;
```
Or inline the script directly (copy from `skills/html-preview/annotate.js`).
### How It Works
1. **User selects text** → popover appears with reaction buttons (👍 ❌ ❓) and comment field
2. **Annotations saved** to localStorage (persists across page reloads)
3. **"Send to OpenClaw" button** → exports all annotations as structured markdown
4. **User copies and pastes** the export into chat for precise feedback
### Export Format
```markdown
## Preview Feedback
Preview: My Document Title
Annotations: 3
---
### 👍 Annotation 1
&gt; The selected text appears here
User's comment about this section
---
### ❓ Annotation 2
&gt; Another selected passage
What does this mean exactly?
```
### Best Practice
Always include annotations for any preview where you expect feedback. The structured export makes it easy to address each point precisely.
## Notes
- **Self-contained HTML** preferred — inline all CSS/JS to avoid CORS issues
- **githack** serves raw gist content with correct `Content-Type: text/html`
- **No tunnel needed** — githack is a public CDN, no background processes to babysit
- **Alternatives considered:** Cloudflare Tunnel (`cloudflared`) works but quick tunnels are unreliable for short-lived background processes
- Gists are public — don't include secrets or sensitive data
- **Annotations** are stored client-side only — no backend, no security concerns</pre>
</div>
</div>
<div class="file-section" id="annotate-js">
<div class="file-header">
<span class="file-icon">📜</span>
<span class="file-name">annotate.js</span>
<span class="file-meta">28.3 KB · 720 lines</span>
</div>
<div class="code-block">
<pre><span class="cmt">/**
* OpenClaw Preview Annotations
*
* Inject into any HTML preview to enable inline feedback.
* Pure client-side - no backend required.
* Works on desktop and mobile (iOS Safari, etc.)
*
* Usage: Include this script at the end of your HTML body.
*/</span>
(<span class="kw">function</span>() {
<span class="str">'use strict'</span>;
<span class="cmt">// Generate or retrieve preview ID</span>
<span class="kw">const</span> PREVIEW_ID = <span class="kw">new</span> URLSearchParams(window.location.search).<span class="fn">get</span>(<span class="str">'preview_id'</span>)
|| window.location.pathname.<span class="fn">split</span>(<span class="str">'/'</span>).<span class="fn">pop</span>().<span class="fn">replace</span>(<span class="str">'.html'</span>, <span class="str">''</span>)
|| <span class="str">'preview_'</span> + Date.<span class="fn">now</span>();
<span class="kw">const</span> STORAGE_KEY = <span class="str">`openclaw_annotations_${PREVIEW_ID}`</span>;
<span class="cmt">// State</span>
<span class="kw">let</span> annotations = [];
<span class="kw">let</span> annotationCounter = <span class="num">0</span>;
<span class="kw">let</span> currentSelection = <span class="kw">null</span>;
<span class="cmt">// Load existing annotations</span>
<span class="kw">function</span> <span class="fn">loadAnnotations</span>() {
<span class="kw">try</span> {
<span class="kw">const</span> stored = localStorage.<span class="fn">getItem</span>(STORAGE_KEY);
<span class="kw">if</span> (stored) {
annotations = JSON.<span class="fn">parse</span>(stored);
annotationCounter = annotations.length;
}
} <span class="kw">catch</span> (e) {
console.<span class="fn">warn</span>(<span class="str">'Failed to load annotations:'</span>, e);
}
}
<span class="cmt">// Save annotations</span>
<span class="kw">function</span> <span class="fn">saveAnnotations</span>() {
<span class="kw">try</span> {
localStorage.<span class="fn">setItem</span>(STORAGE_KEY, JSON.<span class="fn">stringify</span>(annotations));
} <span class="kw">catch</span> (e) {
console.<span class="fn">warn</span>(<span class="str">'Failed to save annotations:'</span>, e);
}
}
<span class="cmt">// Find the nearest heading above an element</span>
<span class="kw">function</span> <span class="fn">findNearestHeading</span>(element) {
<span class="kw">if</span> (!element) <span class="kw">return null</span>;
<span class="kw">let</span> node = element;
<span class="kw">while</span> (node) {
<span class="kw">let</span> sibling = node.previousElementSibling;
<span class="kw">while</span> (sibling) {
<span class="kw">if</span> (<span class="str">/^H[1-6]$/</span>.<span class="fn">test</span>(sibling.tagName)) {
<span class="kw">return</span> sibling.textContent.<span class="fn">trim</span>();
}
<span class="kw">const</span> headings = sibling.<span class="fn">querySelectorAll</span>(<span class="str">'h1, h2, h3, h4, h5, h6'</span>);
<span class="kw">if</span> (headings.length > <span class="num">0</span>) {
<span class="kw">return</span> headings[headings.length - <span class="num">1</span>].textContent.<span class="fn">trim</span>();
}
sibling = sibling.previousElementSibling;
}
node = node.parentElement;
}
<span class="kw">return null</span>;
}
<span class="cmt">// Get element location description</span>
<span class="kw">function</span> <span class="fn">getElementLocation</span>(element) {
<span class="kw">if</span> (!element) <span class="kw">return null</span>;
<span class="kw">const</span> parts = [];
<span class="cmt">// Check if in a table</span>
<span class="kw">const</span> cell = element.<span class="fn">closest</span>(<span class="str">'td, th'</span>);
<span class="kw">if</span> (cell) {
<span class="kw">const</span> row = cell.<span class="fn">closest</span>(<span class="str">'tr'</span>);
<span class="kw">const</span> table = cell.<span class="fn">closest</span>(<span class="str">'table'</span>);
<span class="kw">if</span> (row && table) {
<span class="kw">const</span> rowIndex = Array.<span class="fn">from</span>(table.<span class="fn">querySelectorAll</span>(<span class="str">'tr'</span>)).<span class="fn">indexOf</span>(row) + <span class="num">1</span>;
<span class="kw">const</span> cellIndex = Array.<span class="fn">from</span>(row.children).<span class="fn">indexOf</span>(cell) + <span class="num">1</span>;
parts.<span class="fn">push</span>(<span class="str">`Table Row ${rowIndex}, Column ${cellIndex}`</span>);
}
}
<span class="cmt">// Check if in a code block</span>
<span class="kw">const</span> codeBlock = element.<span class="fn">closest</span>(<span class="str">'pre, code'</span>);
<span class="kw">if</span> (codeBlock) {
parts.<span class="fn">push</span>(<span class="str">'Code block'</span>);
}
<span class="cmt">// Get nearest heading</span>
<span class="kw">const</span> heading = <span class="fn">findNearestHeading</span>(element);
<span class="kw">if</span> (heading) {
parts.<span class="fn">push</span>(<span class="str">`Section: "${heading}"`</span>);
}
<span class="kw">return</span> parts.length > <span class="num">0</span> ? parts.<span class="fn">join</span>(<span class="str">' · '</span>) : <span class="kw">null</span>;
}
<span class="cmt">// Inject styles (CSS for highlights, popovers, toolbar, etc.)</span>
<span class="kw">function</span> <span class="fn">injectStyles</span>() {
<span class="kw">const</span> style = document.<span class="fn">createElement</span>(<span class="str">'style'</span>);
style.textContent = <span class="str">`
.openclaw-highlight {
background: rgba(253, 224, 71, 0.25);
border-bottom: 2px solid rgba(253, 224, 71, 0.8);
cursor: pointer;
}
.openclaw-annotate-btn {
position: fixed;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
color: white;
border: none;
padding: 12px 20px;
border-radius: 25px;
font-weight: 600;
cursor: pointer;
z-index: 10000;
display: none;
}
.openclaw-popover {
position: fixed;
background: #1a1a2e;
border-radius: 16px;
padding: 16px;
z-index: 10001;
max-width: 380px;
}
.openclaw-toolbar {
position: fixed;
top: 16px;
right: 16px;
display: flex;
gap: 8px;
z-index: 9999;
}
/* ... more styles ... */
`</span>;
document.head.<span class="fn">appendChild</span>(style);
}
<span class="cmt">// Generate export text</span>
<span class="kw">function</span> <span class="fn">generateExport</span>() {
<span class="kw">if</span> (annotations.length === <span class="num">0</span>) <span class="kw">return</span> <span class="str">'No annotations yet.'</span>;
<span class="kw">const</span> lines = [
<span class="str">`## Preview Feedback`</span>,
<span class="str">`**Preview:** ${document.title || PREVIEW_ID}`</span>,
<span class="str">`**Annotations:** ${annotations.length}`</span>,
<span class="str">''</span>,
<span class="str">'---'</span>,
];
annotations.<span class="fn">forEach</span>((ann, idx) => {
lines.<span class="fn">push</span>(<span class="str">`### ${idx + 1}.`</span>);
<span class="kw">if</span> (ann.location) lines.<span class="fn">push</span>(<span class="str">`📍 ${ann.location}`</span>);
lines.<span class="fn">push</span>(<span class="str">`> ${ann.selectedText}`</span>);
<span class="kw">if</span> (ann.comment) lines.<span class="fn">push</span>(ann.comment);
lines.<span class="fn">push</span>(<span class="str">'---'</span>);
});
<span class="kw">return</span> lines.<span class="fn">join</span>(<span class="str">'\n'</span>);
}
<span class="cmt">// Initialize</span>
<span class="kw">function</span> <span class="fn">init</span>() {
<span class="fn">injectStyles</span>();
<span class="fn">loadAnnotations</span>();
<span class="cmt">// Create UI elements: annotate button, overlay, popover, toolbar</span>
<span class="kw">const</span> annotateBtn = document.<span class="fn">createElement</span>(<span class="str">'button'</span>);
annotateBtn.className = <span class="str">'openclaw-annotate-btn'</span>;
annotateBtn.textContent = <span class="str">'✏️ Annotate'</span>;
document.body.<span class="fn">appendChild</span>(annotateBtn);
<span class="cmt">// Listen for text selection</span>
document.<span class="fn">addEventListener</span>(<span class="str">'selectionchange'</span>, checkSelection);
<span class="cmt">// Handle annotate button click</span>
annotateBtn.<span class="fn">addEventListener</span>(<span class="str">'click'</span>, () => {
<span class="cmt">// Show popover with comment field</span>
<span class="cmt">// Save annotation on submit</span>
});
<span class="cmt">// Handle existing highlight clicks (edit mode)</span>
document.<span class="fn">addEventListener</span>(<span class="str">'click'</span>, (e) => {
<span class="kw">const</span> highlight = e.target.<span class="fn">closest</span>(<span class="str">'.openclaw-highlight'</span>);
<span class="kw">if</span> (highlight) {
<span class="cmt">// Show edit popover with delete option</span>
}
});
<span class="cmt">// Send/export button</span>
toolbar.<span class="fn">querySelector</span>(<span class="str">'.openclaw-send-btn'</span>).<span class="fn">addEventListener</span>(<span class="str">'click'</span>, () => {
<span class="kw">const</span> exportText = <span class="fn">generateExport</span>();
<span class="cmt">// Show modal with copy-to-clipboard</span>
});
console.<span class="fn">log</span>(<span class="str">'🐾 OpenClaw Annotations loaded.'</span>);
}
<span class="cmt">// Run on DOM ready</span>
<span class="kw">if</span> (document.readyState === <span class="str">'loading'</span>) {
document.<span class="fn">addEventListener</span>(<span class="str">'DOMContentLoaded'</span>, init);
} <span class="kw">else</span> {
<span class="fn">init</span>();
}
})();
<span class="cmt">// Full implementation: ~720 lines
// Key features:
// - Text selection detection (mobile + desktop)
// - Highlight persistence via localStorage
// - Location context (section, table cell, code block)
// - Edit/delete existing annotations
// - Structured markdown export
// - Mobile-friendly UI with bottom-positioned button</span></pre>
</div>
</div>
<div class="footer" style="text-align: center; padding: 30px 0; color: #8b949e; font-size: 0.9rem; border-top: 1px solid #21262d; margin-top: 20px;">
Generated by Buce 🐴 · Ready for review
</div>
</div>
<script src="https://gist.githack.com/jalehman/3c031225cb70b73fe080f60f1b174cce/raw/annotate.js"></script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment