Created
February 1, 2026 05:20
-
-
Save jalehman/4b64e77bec33c3169eff2d64756bfda5 to your computer and use it in GitHub Desktop.
Webdrop skill code review
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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 | |
| <script src="https://gist.githack.com/jalehman/ANNOTATION_GIST_ID/raw/annotate.js"></script> | |
| ``` | |
| 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 | |
| > The selected text appears here | |
| User's comment about this section | |
| --- | |
| ### ❓ Annotation 2 | |
| > 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