experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(extension): simplify command bar popup UI

- Minimal UI matching desktop cmd panel style (dark, transparent input)
- Commands: tag, note, search with inline autocomplete
- UI closes immediately on Enter (fire-and-forget)
- Backend saves async via datastore, decoupled from UI
- Removed dropdown/results for now - just input field
- Uses Playwright for dev launch (Google Chrome blocks --load-extension)

Usage: yarn extension:chrome
Then Option+P or click extension icon
Type: "tag foo, bar" or "note my note" and Enter

+152 -670
-301
backend/extension/popup.css
··· 1 - * { 2 - box-sizing: border-box; 3 - margin: 0; 4 - padding: 0; 5 - } 6 - 7 - body { 8 - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 9 - font-size: 14px; 10 - line-height: 1.5; 11 - color: #333; 12 - background: #fff; 13 - width: 320px; 14 - min-height: 100px; 15 - } 16 - 17 - /* Command bar input */ 18 - .command-bar { 19 - position: relative; 20 - padding: 12px; 21 - border-bottom: 1px solid #eee; 22 - } 23 - 24 - #command-input { 25 - width: 100%; 26 - padding: 10px 12px; 27 - border: 1px solid #ddd; 28 - border-radius: 6px; 29 - font-size: 15px; 30 - font-family: inherit; 31 - background: #fafafa; 32 - } 33 - 34 - #command-input:focus { 35 - outline: none; 36 - border-color: #4a90d9; 37 - background: #fff; 38 - box-shadow: 0 0 0 2px rgba(74, 144, 217, 0.15); 39 - } 40 - 41 - .command-hint { 42 - position: absolute; 43 - top: 22px; 44 - left: 24px; 45 - right: 24px; 46 - pointer-events: none; 47 - color: #999; 48 - font-size: 15px; 49 - white-space: nowrap; 50 - overflow: hidden; 51 - } 52 - 53 - /* Results list */ 54 - .results { 55 - max-height: 200px; 56 - overflow-y: auto; 57 - } 58 - 59 - .results:empty { 60 - display: none; 61 - } 62 - 63 - .result-item { 64 - padding: 10px 12px; 65 - cursor: pointer; 66 - display: flex; 67 - align-items: center; 68 - gap: 10px; 69 - border-bottom: 1px solid #f0f0f0; 70 - } 71 - 72 - .result-item:last-child { 73 - border-bottom: none; 74 - } 75 - 76 - .result-item:hover { 77 - background: #f5f5f5; 78 - } 79 - 80 - .result-item.selected { 81 - background: #e8f0fa; 82 - } 83 - 84 - .result-icon { 85 - width: 20px; 86 - height: 20px; 87 - display: flex; 88 - align-items: center; 89 - justify-content: center; 90 - color: #666; 91 - font-size: 14px; 92 - } 93 - 94 - .result-text { 95 - flex: 1; 96 - min-width: 0; 97 - } 98 - 99 - .result-title { 100 - font-weight: 500; 101 - white-space: nowrap; 102 - overflow: hidden; 103 - text-overflow: ellipsis; 104 - } 105 - 106 - .result-description { 107 - font-size: 12px; 108 - color: #888; 109 - white-space: nowrap; 110 - overflow: hidden; 111 - text-overflow: ellipsis; 112 - } 113 - 114 - /* Input area for note/tag entry */ 115 - .input-area { 116 - padding: 12px; 117 - border-top: 1px solid #eee; 118 - } 119 - 120 - .input-label { 121 - font-weight: 500; 122 - margin-bottom: 8px; 123 - color: #333; 124 - } 125 - 126 - #input-text { 127 - width: 100%; 128 - padding: 8px 10px; 129 - border: 1px solid #ddd; 130 - border-radius: 4px; 131 - font-size: 14px; 132 - font-family: inherit; 133 - resize: vertical; 134 - min-height: 60px; 135 - } 136 - 137 - #input-text:focus { 138 - outline: none; 139 - border-color: #4a90d9; 140 - box-shadow: 0 0 0 2px rgba(74, 144, 217, 0.15); 141 - } 142 - 143 - .input-actions { 144 - display: flex; 145 - justify-content: flex-end; 146 - gap: 8px; 147 - margin-top: 10px; 148 - } 149 - 150 - /* Buttons */ 151 - .btn { 152 - padding: 6px 14px; 153 - border: 1px solid #ccc; 154 - border-radius: 4px; 155 - background: #fff; 156 - font-size: 13px; 157 - font-family: inherit; 158 - cursor: pointer; 159 - } 160 - 161 - .btn:hover { 162 - background: #f5f5f5; 163 - } 164 - 165 - .btn.primary { 166 - background: #4a90d9; 167 - color: #fff; 168 - border-color: #4a90d9; 169 - } 170 - 171 - .btn.primary:hover { 172 - background: #3a7bc8; 173 - } 174 - 175 - /* Status message */ 176 - .status { 177 - padding: 10px 12px; 178 - font-size: 13px; 179 - color: #666; 180 - text-align: center; 181 - } 182 - 183 - .status:empty { 184 - display: none; 185 - } 186 - 187 - .status.error { 188 - color: #c0392b; 189 - background: #fdf2f2; 190 - } 191 - 192 - .status.success { 193 - color: #27ae60; 194 - background: #f0fdf4; 195 - } 196 - 197 - /* Help text at bottom */ 198 - .help-text { 199 - padding: 10px 12px; 200 - font-size: 11px; 201 - color: #999; 202 - text-align: center; 203 - border-top: 1px solid #f0f0f0; 204 - background: #fafafa; 205 - } 206 - 207 - .shortcut { 208 - display: inline-block; 209 - background: #e8e8e8; 210 - padding: 2px 6px; 211 - border-radius: 3px; 212 - font-family: monospace; 213 - font-size: 10px; 214 - margin-right: 2px; 215 - } 216 - 217 - /* Dark mode */ 218 - @media (prefers-color-scheme: dark) { 219 - body { 220 - background: #1e1e1e; 221 - color: #ddd; 222 - } 223 - 224 - .command-bar { 225 - border-bottom-color: #333; 226 - } 227 - 228 - #command-input { 229 - background: #2a2a2a; 230 - border-color: #444; 231 - color: #ddd; 232 - } 233 - 234 - #command-input:focus { 235 - background: #333; 236 - border-color: #4a90d9; 237 - } 238 - 239 - #command-input::placeholder { 240 - color: #777; 241 - } 242 - 243 - .command-hint { 244 - color: #666; 245 - } 246 - 247 - .result-item { 248 - border-bottom-color: #333; 249 - } 250 - 251 - .result-item:hover { 252 - background: #2a2a2a; 253 - } 254 - 255 - .result-item.selected { 256 - background: #2a3a4a; 257 - } 258 - 259 - .result-description { 260 - color: #777; 261 - } 262 - 263 - .input-area { 264 - border-top-color: #333; 265 - } 266 - 267 - #input-text { 268 - background: #2a2a2a; 269 - border-color: #444; 270 - color: #ddd; 271 - } 272 - 273 - .btn { 274 - background: #333; 275 - border-color: #555; 276 - color: #ddd; 277 - } 278 - 279 - .btn:hover { 280 - background: #444; 281 - } 282 - 283 - .help-text { 284 - background: #1a1a1a; 285 - border-top-color: #333; 286 - color: #777; 287 - } 288 - 289 - .shortcut { 290 - background: #333; 291 - color: #aaa; 292 - } 293 - 294 - .status.error { 295 - background: #3a2020; 296 - } 297 - 298 - .status.success { 299 - background: #1a3020; 300 - } 301 - }
+79 -28
backend/extension/popup.html
··· 4 4 <meta charset="UTF-8"> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 6 <title>Peek</title> 7 - <link rel="stylesheet" href="popup.css"> 8 - </head> 9 - <body> 10 - <div class="command-bar"> 11 - <input 12 - type="text" 13 - id="command-input" 14 - placeholder="Type a command..." 15 - autofocus 16 - spellcheck="false" 17 - > 18 - <div id="command-hint" class="command-hint"></div> 19 - </div> 7 + <style> 8 + * { 9 + box-sizing: border-box; 10 + margin: 0; 11 + padding: 0; 12 + } 20 13 21 - <div id="results" class="results"></div> 14 + html, body { 15 + width: 320px; 16 + height: 48px; 17 + background-color: rgba(40, 44, 52, 0.95); 18 + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, "Helvetica Neue", sans-serif; 19 + color: white; 20 + overflow: hidden; 21 + } 22 + 23 + .center-wrapper { 24 + width: 100%; 25 + height: 100%; 26 + display: flex; 27 + align-items: center; 28 + padding: 0 12px; 29 + } 30 + 31 + .command-display { 32 + flex: 1; 33 + min-width: 0; 34 + height: 40px; 35 + position: relative; 36 + } 37 + 38 + #command-input { 39 + position: absolute; 40 + top: 0; 41 + left: 0; 42 + width: 100%; 43 + height: 40px; 44 + line-height: 40px; 45 + border: none; 46 + outline: none; 47 + background: transparent; 48 + color: transparent; 49 + caret-color: white; 50 + font-family: inherit; 51 + font-size: 18px; 52 + font-weight: 500; 53 + padding: 0; 54 + z-index: 2; 55 + } 22 56 23 - <div id="input-area" class="input-area" hidden> 24 - <div id="input-label" class="input-label"></div> 25 - <textarea id="input-text" rows="3" placeholder=""></textarea> 26 - <div class="input-actions"> 27 - <button id="input-cancel" class="btn">Cancel</button> 28 - <button id="input-submit" class="btn primary">Save</button> 29 - </div> 30 - </div> 57 + #command-text { 58 + position: absolute; 59 + top: 0; 60 + left: 0; 61 + width: 100%; 62 + height: 40px; 63 + line-height: 40px; 64 + font-family: inherit; 65 + font-size: 18px; 66 + font-weight: 500; 67 + color: rgba(255, 255, 255, 0.4); 68 + white-space: pre; 69 + overflow: hidden; 70 + pointer-events: none; 71 + z-index: 1; 72 + } 31 73 32 - <div id="status" class="status"></div> 74 + #command-text .typed { 75 + color: white; 76 + } 33 77 34 - <div class="help-text"> 35 - <span class="shortcut">tag</span> Tag this page 36 - <span class="shortcut">note</span> Save a note 37 - <span class="shortcut">search</span> Search items 78 + #command-text.placeholder { 79 + color: rgba(255, 255, 255, 0.3); 80 + } 81 + </style> 82 + </head> 83 + <body> 84 + <div class="center-wrapper"> 85 + <div class="command-display"> 86 + <input id="command-input" type="text" autofocus spellcheck="false" /> 87 + <div id="command-text" class="placeholder">tag, note, or search...</div> 88 + </div> 38 89 </div> 39 90 40 - <script type="module" src="popup.js"></script> 91 + <script src="popup.js"></script> 41 92 </body> 42 93 </html>
+73 -341
backend/extension/popup.js
··· 1 1 /** 2 2 * Peek Browser Extension - Command Bar Popup 3 - * 4 - * Simple command bar for quick actions: 5 - * - tag: Add tags to the current page 6 - * - note: Save a text note about the page 7 - * - search: Search saved items 3 + * Simple command bar: tag, note, search 4 + * UI closes immediately - backend saves async 8 5 */ 9 6 10 - // Available commands 11 - const COMMANDS = { 12 - tag: { 13 - name: 'tag', 14 - title: 'Tag this page', 15 - description: 'Add tags to the current page', 16 - icon: '#', 17 - action: 'input', 18 - inputLabel: 'Enter tags (comma-separated)', 19 - inputPlaceholder: 'work, reference, read-later' 20 - }, 21 - note: { 22 - name: 'note', 23 - title: 'Save a note', 24 - description: 'Write a note about this page', 25 - icon: '📝', 26 - action: 'input', 27 - inputLabel: 'Enter your note', 28 - inputPlaceholder: 'Add a note about this page...' 29 - }, 30 - search: { 31 - name: 'search', 32 - title: 'Search items', 33 - description: 'Search your saved items', 34 - icon: '🔍', 35 - action: 'search' 36 - } 37 - }; 7 + const COMMANDS = ['tag', 'note', 'search']; 38 8 39 - // State 40 9 let state = { 41 10 typed: '', 42 - matches: [], 43 - selectedIndex: 0, 44 - currentCommand: null, 11 + match: null, 45 12 currentTab: null 46 13 }; 47 14 48 - // DOM elements 49 15 const commandInput = document.getElementById('command-input'); 50 - const commandHint = document.getElementById('command-hint'); 51 - const resultsEl = document.getElementById('results'); 52 - const inputArea = document.getElementById('input-area'); 53 - const inputLabel = document.getElementById('input-label'); 54 - const inputText = document.getElementById('input-text'); 55 - const inputCancel = document.getElementById('input-cancel'); 56 - const inputSubmit = document.getElementById('input-submit'); 57 - const statusEl = document.getElementById('status'); 16 + const commandText = document.getElementById('command-text'); 58 17 59 - // Initialize 60 18 async function init() { 61 - // Get current tab info 62 19 try { 63 20 const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); 64 21 state.currentTab = tab; ··· 66 23 console.error('Failed to get current tab:', err); 67 24 } 68 25 69 - // Set up event listeners 70 26 commandInput.addEventListener('input', onInput); 71 27 commandInput.addEventListener('keydown', onKeyDown); 72 - inputCancel.addEventListener('click', cancelInput); 73 - inputSubmit.addEventListener('click', submitInput); 74 - inputText.addEventListener('keydown', onInputTextKeyDown); 75 - 76 - // Focus the command input 77 28 commandInput.focus(); 78 - 79 - // Show all commands initially 80 - updateMatches(''); 81 - renderResults(); 82 29 } 83 30 84 - // Handle input changes 85 31 function onInput(e) { 86 32 state.typed = e.target.value; 87 - state.selectedIndex = 0; 88 - updateMatches(state.typed); 89 - renderResults(); 90 - updateHint(); 33 + state.match = findMatch(state.typed); 34 + updateDisplay(); 91 35 } 92 36 93 - // Handle keyboard navigation 94 37 function onKeyDown(e) { 95 - if (e.key === 'ArrowDown') { 96 - e.preventDefault(); 97 - if (state.selectedIndex < state.matches.length - 1) { 98 - state.selectedIndex++; 99 - renderResults(); 100 - } 101 - } else if (e.key === 'ArrowUp') { 38 + if (e.key === 'Enter') { 102 39 e.preventDefault(); 103 - if (state.selectedIndex > 0) { 104 - state.selectedIndex--; 105 - renderResults(); 106 - } 107 - } else if (e.key === 'Enter') { 108 - e.preventDefault(); 109 - executeSelected(); 40 + executeCommand(); 110 41 } else if (e.key === 'Escape') { 111 42 e.preventDefault(); 112 43 window.close(); 113 - } else if (e.key === 'Tab') { 44 + } else if (e.key === 'Tab' && state.match) { 114 45 e.preventDefault(); 115 - // Autocomplete the selected command 116 - if (state.matches.length > 0) { 117 - const selected = state.matches[state.selectedIndex]; 118 - commandInput.value = selected.name + ' '; 119 - state.typed = commandInput.value; 120 - updateMatches(state.typed); 121 - renderResults(); 122 - } 46 + commandInput.value = state.match + ' '; 47 + state.typed = commandInput.value; 48 + updateDisplay(); 123 49 } 124 50 } 125 51 126 - // Handle Enter in input text area 127 - function onInputTextKeyDown(e) { 128 - if (e.key === 'Escape') { 129 - e.preventDefault(); 130 - cancelInput(); 131 - } else if (e.key === 'Enter' && e.metaKey) { 132 - e.preventDefault(); 133 - submitInput(); 134 - } 52 + function findMatch(text) { 53 + if (!text) return null; 54 + const lower = text.toLowerCase().split(' ')[0]; 55 + return COMMANDS.find(cmd => cmd.startsWith(lower)) || null; 135 56 } 136 57 137 - // Find matching commands 138 - function updateMatches(text) { 139 - const query = text.trim().toLowerCase(); 140 - 141 - if (!query) { 142 - // Show all commands 143 - state.matches = Object.values(COMMANDS); 144 - return; 145 - } 58 + function updateDisplay() { 59 + commandText.innerHTML = ''; 60 + commandText.classList.remove('placeholder'); 146 61 147 - // Check if text starts with a command name (for search queries) 148 - const parts = query.split(' '); 149 - const cmdName = parts[0]; 150 - 151 - if (COMMANDS[cmdName] && parts.length > 1) { 152 - // User typed a full command plus query 153 - state.matches = [COMMANDS[cmdName]]; 62 + if (!state.typed) { 63 + commandText.classList.add('placeholder'); 64 + commandText.textContent = 'tag, note, or search...'; 154 65 return; 155 66 } 156 67 157 - // Filter commands by prefix/substring match 158 - state.matches = Object.values(COMMANDS).filter(cmd => { 159 - return cmd.name.includes(query) || 160 - cmd.title.toLowerCase().includes(query); 161 - }); 162 - } 68 + const typedSpan = document.createElement('span'); 69 + typedSpan.className = 'typed'; 70 + typedSpan.textContent = state.typed; 71 + commandText.appendChild(typedSpan); 163 72 164 - // Update hint text 165 - function updateHint() { 166 - if (state.matches.length > 0 && state.typed) { 167 - const match = state.matches[state.selectedIndex]; 168 - if (match.name.startsWith(state.typed.toLowerCase())) { 169 - commandHint.textContent = match.name; 170 - } else { 171 - commandHint.textContent = ''; 73 + if (state.match && state.match.startsWith(state.typed.toLowerCase().split(' ')[0])) { 74 + const cmdPart = state.typed.split(' ')[0]; 75 + if (state.match.length > cmdPart.length && !state.typed.includes(' ')) { 76 + const restSpan = document.createElement('span'); 77 + restSpan.textContent = state.match.substring(cmdPart.length); 78 + commandText.appendChild(restSpan); 172 79 } 173 - } else { 174 - commandHint.textContent = ''; 175 80 } 176 81 } 177 82 178 - // Render results list 179 - function renderResults() { 180 - resultsEl.innerHTML = ''; 181 - 182 - state.matches.forEach((cmd, index) => { 183 - const item = document.createElement('div'); 184 - item.className = 'result-item' + (index === state.selectedIndex ? ' selected' : ''); 185 - item.innerHTML = ` 186 - <div class="result-icon">${cmd.icon}</div> 187 - <div class="result-text"> 188 - <div class="result-title">${cmd.title}</div> 189 - <div class="result-description">${cmd.description}</div> 190 - </div> 191 - `; 192 - item.addEventListener('click', () => { 193 - state.selectedIndex = index; 194 - executeSelected(); 195 - }); 196 - resultsEl.appendChild(item); 197 - }); 198 - } 199 - 200 - // Execute the selected command 201 - function executeSelected() { 202 - if (state.matches.length === 0) return; 203 - 204 - const cmd = state.matches[state.selectedIndex]; 205 - state.currentCommand = cmd; 206 - 207 - if (cmd.action === 'input') { 208 - showInputArea(cmd); 209 - } else if (cmd.action === 'search') { 210 - performSearch(); 211 - } 212 - } 213 - 214 - // Show input area for tag/note entry 215 - function showInputArea(cmd) { 216 - inputLabel.textContent = cmd.inputLabel; 217 - inputText.placeholder = cmd.inputPlaceholder; 218 - inputText.value = ''; 219 - inputArea.hidden = false; 220 - resultsEl.innerHTML = ''; 221 - commandInput.disabled = true; 222 - inputText.focus(); 223 - } 224 - 225 - // Cancel input and go back 226 - function cancelInput() { 227 - inputArea.hidden = true; 228 - commandInput.disabled = false; 229 - commandInput.value = ''; 230 - state.typed = ''; 231 - state.currentCommand = null; 232 - updateMatches(''); 233 - renderResults(); 234 - commandInput.focus(); 235 - } 236 - 237 - // Submit the input 238 - async function submitInput() { 239 - const value = inputText.value.trim(); 240 - if (!value) { 241 - showStatus('Please enter a value', 'error'); 242 - return; 243 - } 244 - 245 - const cmd = state.currentCommand; 83 + function executeCommand() { 84 + const parts = state.typed.trim().split(/\s+/); 85 + const cmd = parts[0]?.toLowerCase(); 86 + const args = parts.slice(1).join(' '); 246 87 247 - if (cmd.name === 'tag') { 248 - await saveTag(value); 249 - } else if (cmd.name === 'note') { 250 - await saveNote(value); 88 + if (cmd === 'tag' && args) { 89 + saveTag(args); 90 + window.close(); 91 + } else if (cmd === 'note' && args) { 92 + saveNote(args); 93 + window.close(); 94 + } else if (cmd === 'search' && args) { 95 + performSearch(args); 96 + } else if (state.match && !args) { 97 + commandInput.value = state.match + ' '; 98 + state.typed = commandInput.value; 99 + updateDisplay(); 251 100 } 252 101 } 253 102 254 - // Save tags for current page 255 - async function saveTag(tagString) { 103 + function saveTag(tagString) { 256 104 const tags = tagString.split(',').map(t => t.trim()).filter(t => t); 257 - 258 - if (tags.length === 0) { 259 - showStatus('Please enter at least one tag', 'error'); 260 - return; 261 - } 262 - 263 - try { 264 - // Send message to background script 265 - const response = await chrome.runtime.sendMessage({ 266 - type: 'save-tags', 267 - data: { 268 - url: state.currentTab?.url, 269 - title: state.currentTab?.title, 270 - tags: tags 271 - } 272 - }); 105 + if (tags.length === 0) return; 273 106 274 - if (response?.success) { 275 - showStatus(`Tagged with: ${tags.join(', ')}`, 'success'); 276 - setTimeout(() => window.close(), 1000); 277 - } else { 278 - showStatus(response?.error || 'Failed to save tags', 'error'); 107 + chrome.runtime.sendMessage({ 108 + type: 'save-tags', 109 + data: { 110 + url: state.currentTab?.url, 111 + title: state.currentTab?.title, 112 + tags: tags 279 113 } 280 - } catch (err) { 281 - console.error('Failed to save tags:', err); 282 - showStatus('Failed to save tags', 'error'); 283 - } 114 + }).catch(err => console.error('Failed to save tags:', err)); 284 115 } 285 116 286 - // Save note for current page 287 - async function saveNote(noteText) { 288 - try { 289 - const response = await chrome.runtime.sendMessage({ 290 - type: 'save-note', 291 - data: { 292 - url: state.currentTab?.url, 293 - title: state.currentTab?.title, 294 - note: noteText 295 - } 296 - }); 297 - 298 - if (response?.success) { 299 - showStatus('Note saved', 'success'); 300 - setTimeout(() => window.close(), 1000); 301 - } else { 302 - showStatus(response?.error || 'Failed to save note', 'error'); 117 + function saveNote(noteText) { 118 + chrome.runtime.sendMessage({ 119 + type: 'save-note', 120 + data: { 121 + url: state.currentTab?.url, 122 + title: state.currentTab?.title, 123 + note: noteText 303 124 } 304 - } catch (err) { 305 - console.error('Failed to save note:', err); 306 - showStatus('Failed to save note', 'error'); 307 - } 125 + }).catch(err => console.error('Failed to save note:', err)); 308 126 } 309 127 310 - // Perform search 311 - async function performSearch() { 312 - const query = state.typed.replace(/^search\s*/i, '').trim(); 313 - 314 - if (!query) { 315 - // Show search input 316 - showInputArea({ 317 - inputLabel: 'Search your items', 318 - inputPlaceholder: 'Enter search query...', 319 - name: 'search-query' 320 - }); 321 - state.currentCommand = { name: 'search-query' }; 322 - return; 323 - } 324 - 325 - try { 326 - const response = await chrome.runtime.sendMessage({ 327 - type: 'search-items', 328 - data: { query } 329 - }); 330 - 331 - if (response?.success && response.data?.length > 0) { 332 - renderSearchResults(response.data); 333 - } else if (response?.success) { 334 - showStatus('No results found', ''); 335 - } else { 336 - showStatus(response?.error || 'Search failed', 'error'); 337 - } 338 - } catch (err) { 339 - console.error('Search failed:', err); 340 - showStatus('Search failed', 'error'); 341 - } 128 + function performSearch(query) { 129 + chrome.runtime.sendMessage({ 130 + type: 'search-items', 131 + data: { query } 132 + }).then(response => { 133 + console.log('Search results:', response); 134 + }).catch(err => console.error('Search failed:', err)); 342 135 } 343 136 344 - // Render search results 345 - function renderSearchResults(items) { 346 - resultsEl.innerHTML = ''; 347 - 348 - items.forEach(item => { 349 - const el = document.createElement('div'); 350 - el.className = 'result-item'; 351 - el.innerHTML = ` 352 - <div class="result-icon">${item.type === 'note' ? '📝' : '🔗'}</div> 353 - <div class="result-text"> 354 - <div class="result-title">${escapeHtml(item.title || item.url)}</div> 355 - <div class="result-description">${escapeHtml(item.snippet || item.url || '')}</div> 356 - </div> 357 - `; 358 - el.addEventListener('click', () => { 359 - if (item.url) { 360 - chrome.tabs.create({ url: item.url }); 361 - window.close(); 362 - } 363 - }); 364 - resultsEl.appendChild(el); 365 - }); 366 - } 367 - 368 - // Show status message 369 - function showStatus(message, type) { 370 - statusEl.textContent = message; 371 - statusEl.className = 'status' + (type ? ' ' + type : ''); 372 - 373 - if (type === 'success') { 374 - inputArea.hidden = true; 375 - } 376 - } 377 - 378 - // Escape HTML to prevent XSS 379 - function escapeHtml(str) { 380 - if (!str) return ''; 381 - return str 382 - .replace(/&/g, '&amp;') 383 - .replace(/</g, '&lt;') 384 - .replace(/>/g, '&gt;') 385 - .replace(/"/g, '&quot;'); 386 - } 387 - 388 - // Handle search-query submit differently 389 - const originalSubmitInput = submitInput; 390 - submitInput = async function() { 391 - if (state.currentCommand?.name === 'search-query') { 392 - const query = inputText.value.trim(); 393 - if (query) { 394 - commandInput.value = 'search ' + query; 395 - state.typed = commandInput.value; 396 - inputArea.hidden = true; 397 - await performSearch(); 398 - } 399 - return; 400 - } 401 - await originalSubmitInput(); 402 - }; 403 - 404 - // Initialize when DOM is ready 405 137 init();