My agentic slop goes here. Not intended for anyone else!
1/** 2 * Arod Search Modal Component 3 * An expandable search interface with modal overlay 4 * 5 * Usage: 6 * <arod-search placeholder="Search docs..."></arod-search> 7 * 8 * Events: 9 * - search: Fired when user performs a search 10 * - clear: Fired when search is cleared 11 */ 12 13// ============================================ 14// AROD-SEARCH - Search Modal Component 15// ============================================ 16class ArodSearch extends HTMLElement { 17 constructor() { 18 super(); 19 this.attachShadow({ mode: 'open' }); 20 this.isOpen = false; 21 this.searchDebounce = null; 22 } 23 24 connectedCallback() { 25 this.render(); 26 this.setupEventListeners(); 27 this.setupKeyboardShortcuts(); 28 } 29 30 disconnectedCallback() { 31 this.removeKeyboardShortcuts(); 32 } 33 34 render() { 35 const placeholder = this.getAttribute('placeholder') || 'Search...'; 36 37 this.shadowRoot.innerHTML = ` 38 <style> 39 :host { 40 --search-bg: #fffffc; 41 --search-border: #e0e0e0; 42 --search-text: #1a1a1a; 43 --search-muted: #666; 44 --search-accent: #090c8d; 45 --search-overlay: rgba(0, 0, 0, 0.5); 46 } 47 48 @media (prefers-color-scheme: dark) { 49 :host { 50 --search-bg: #1a1a1a; 51 --search-border: #444; 52 --search-text: #e0e0e0; 53 --search-muted: #999; 54 --search-accent: #4db8ff; 55 --search-overlay: rgba(0, 0, 0, 0.8); 56 } 57 } 58 59 /* Explicit light theme */ 60 :host-context([data-theme="light"]) { 61 --search-bg: #fffffc; 62 --search-border: #e0e0e0; 63 --search-text: #1a1a1a; 64 --search-muted: #666; 65 --search-accent: #090c8d; 66 --search-overlay: rgba(0, 0, 0, 0.5); 67 } 68 69 /* Explicit dark theme */ 70 :host-context([data-theme="dark"]) { 71 --search-bg: #1a1a1a; 72 --search-border: #444; 73 --search-text: #e0e0e0; 74 --search-muted: #999; 75 --search-accent: #4db8ff; 76 --search-overlay: rgba(0, 0, 0, 0.8); 77 } 78 79 .search-trigger { 80 display: flex; 81 align-items: center; 82 gap: 0.5rem; 83 padding: 0.5rem 0.75rem; 84 background: transparent; 85 border: 1px solid var(--search-border); 86 border-radius: 4px; 87 color: var(--search-text); 88 cursor: pointer; 89 transition: all 0.2s ease; 90 } 91 92 .search-trigger:hover { 93 border-color: var(--search-accent); 94 background: rgba(9, 12, 141, 0.05); 95 } 96 97 .search-icon { 98 width: 18px; 99 height: 18px; 100 } 101 102 .search-shortcut { 103 font-size: 0.75rem; 104 padding: 0.125rem 0.375rem; 105 background: rgba(0, 0, 0, 0.08); 106 border-radius: 3px; 107 color: var(--search-muted); 108 font-family: monospace; 109 } 110 111 .modal-overlay { 112 display: none; 113 position: fixed; 114 top: 0; 115 left: 0; 116 right: 0; 117 bottom: 0; 118 background: var(--search-overlay); 119 z-index: 9999; 120 animation: fadeIn 0.2s ease; 121 } 122 123 .modal-overlay.open { 124 display: block; 125 } 126 127 .modal-container { 128 position: fixed; 129 top: 10vh; 130 left: 50%; 131 transform: translateX(-50%); 132 width: 90%; 133 max-width: 600px; 134 background: var(--search-bg); 135 border-radius: 8px; 136 box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); 137 animation: slideDown 0.3s ease; 138 } 139 140 .search-header { 141 position: relative; 142 padding: 1rem; 143 border-bottom: 1px solid var(--search-border); 144 } 145 146 .search-input-wrapper { 147 display: flex; 148 align-items: center; 149 gap: 0.75rem; 150 } 151 152 .search-input { 153 flex: 1; 154 padding: 0.75rem; 155 font-size: 1.125rem; 156 background: transparent; 157 border: none; 158 color: var(--search-text); 159 outline: none; 160 } 161 162 .search-input::placeholder { 163 color: var(--search-muted); 164 } 165 166 .search-close { 167 padding: 0.5rem; 168 background: transparent; 169 border: none; 170 color: var(--search-muted); 171 cursor: pointer; 172 border-radius: 4px; 173 transition: all 0.2s ease; 174 } 175 176 .search-close:hover { 177 background: rgba(0, 0, 0, 0.08); 178 } 179 180 .search-results { 181 max-height: 60vh; 182 overflow-y: auto; 183 padding: 0.5rem; 184 } 185 186 .search-results:empty::after { 187 content: "Type to search..."; 188 display: block; 189 padding: 2rem; 190 text-align: center; 191 color: var(--search-muted); 192 } 193 194 .search-result-item { 195 padding: 0.75rem 1rem; 196 border-radius: 4px; 197 cursor: pointer; 198 transition: all 0.2s ease; 199 } 200 201 .search-result-item:hover { 202 background: rgba(9, 12, 141, 0.1); 203 } 204 205 .result-title { 206 font-weight: 500; 207 color: var(--search-text); 208 margin-bottom: 0.25rem; 209 } 210 211 .result-snippet { 212 font-size: 0.875rem; 213 color: var(--search-muted); 214 line-height: 1.4; 215 } 216 217 .result-snippet mark { 218 background: rgba(255, 200, 0, 0.3); 219 color: inherit; 220 padding: 0.1em 0.2em; 221 border-radius: 2px; 222 } 223 224 .search-footer { 225 padding: 0.75rem 1rem; 226 border-top: 1px solid var(--search-border); 227 display: flex; 228 justify-content: space-between; 229 align-items: center; 230 font-size: 0.875rem; 231 color: var(--search-muted); 232 } 233 234 .search-hints { 235 display: flex; 236 gap: 1rem; 237 } 238 239 .search-hint { 240 display: flex; 241 align-items: center; 242 gap: 0.25rem; 243 } 244 245 .key { 246 padding: 0.125rem 0.375rem; 247 background: rgba(0, 0, 0, 0.08); 248 border-radius: 3px; 249 font-family: monospace; 250 } 251 252 /* Selected result highlighting */ 253 .search-result-item.selected { 254 background: rgba(9, 12, 141, 0.15); 255 } 256 257 /* Dark mode adjustments for backgrounds */ 258 @media (prefers-color-scheme: dark) { 259 .search-shortcut, 260 .key { 261 background: rgba(255, 255, 255, 0.1); 262 } 263 264 .search-close:hover { 265 background: rgba(255, 255, 255, 0.1); 266 } 267 268 .search-result-item:hover { 269 background: rgba(77, 184, 255, 0.1); 270 } 271 272 .search-result-item.selected { 273 background: rgba(77, 184, 255, 0.15); 274 } 275 } 276 277 @keyframes fadeIn { 278 from { opacity: 0; } 279 to { opacity: 1; } 280 } 281 282 @keyframes slideDown { 283 from { 284 opacity: 0; 285 transform: translateX(-50%) translateY(-20px); 286 } 287 to { 288 opacity: 1; 289 transform: translateX(-50%) translateY(0); 290 } 291 } 292 </style> 293 294 <button class="search-trigger" aria-label="Search"> 295 <svg class="search-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 296 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path> 297 </svg> 298 <span class="search-shortcut">⌘K</span> 299 </button> 300 301 <div class="modal-overlay"> 302 <div class="modal-container"> 303 <div class="search-header"> 304 <div class="search-input-wrapper"> 305 <svg class="search-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 306 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path> 307 </svg> 308 <input type="text" class="search-input" placeholder="${placeholder}" autocomplete="off"> 309 <button class="search-close" aria-label="Close"> 310 <svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 311 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path> 312 </svg> 313 </button> 314 </div> 315 </div> 316 317 <div class="search-results"></div> 318 319 <div class="search-footer"> 320 <div class="search-hints"> 321 <span class="search-hint"> 322 <span class="key">↑↓</span> navigate 323 </span> 324 <span class="search-hint"> 325 <span class="key">↵</span> select 326 </span> 327 <span class="search-hint"> 328 <span class="key">esc</span> close 329 </span> 330 </div> 331 <div class="search-powered"> 332 <slot name="powered-by"></slot> 333 </div> 334 </div> 335 </div> 336 </div> 337 `; 338 } 339 340 setupEventListeners() { 341 const trigger = this.shadowRoot.querySelector('.search-trigger'); 342 const overlay = this.shadowRoot.querySelector('.modal-overlay'); 343 const close = this.shadowRoot.querySelector('.search-close'); 344 const input = this.shadowRoot.querySelector('.search-input'); 345 346 trigger.addEventListener('click', () => this.open()); 347 close.addEventListener('click', () => this.close()); 348 349 overlay.addEventListener('click', (e) => { 350 if (e.target === overlay) this.close(); 351 }); 352 353 input.addEventListener('input', (e) => { 354 clearTimeout(this.searchDebounce); 355 this.searchDebounce = setTimeout(() => { 356 this.handleSearch(e.target.value); 357 }, 200); 358 }); 359 360 input.addEventListener('keydown', (e) => { 361 if (e.key === 'Escape') this.close(); 362 else if (e.key === 'ArrowDown') this.navigateResults(1); 363 else if (e.key === 'ArrowUp') this.navigateResults(-1); 364 else if (e.key === 'Enter') this.selectResult(); 365 }); 366 } 367 368 setupKeyboardShortcuts() { 369 this.keyHandler = (e) => { 370 // Cmd+K or Ctrl+K to open search 371 if ((e.metaKey || e.ctrlKey) && e.key === 'k') { 372 e.preventDefault(); 373 this.open(); 374 } 375 }; 376 document.addEventListener('keydown', this.keyHandler); 377 } 378 379 removeKeyboardShortcuts() { 380 if (this.keyHandler) { 381 document.removeEventListener('keydown', this.keyHandler); 382 } 383 } 384 385 open() { 386 this.isOpen = true; 387 const overlay = this.shadowRoot.querySelector('.modal-overlay'); 388 const input = this.shadowRoot.querySelector('.search-input'); 389 390 overlay.classList.add('open'); 391 setTimeout(() => input.focus(), 100); 392 } 393 394 close() { 395 this.isOpen = false; 396 const overlay = this.shadowRoot.querySelector('.modal-overlay'); 397 const input = this.shadowRoot.querySelector('.search-input'); 398 const results = this.shadowRoot.querySelector('.search-results'); 399 400 overlay.classList.remove('open'); 401 input.value = ''; 402 results.innerHTML = ''; 403 } 404 405 handleSearch(query) { 406 if (!query) { 407 this.clearResults(); 408 return; 409 } 410 411 // Dispatch search event for external handling 412 this.dispatchEvent(new CustomEvent('search', { 413 detail: { query }, 414 bubbles: true 415 })); 416 } 417 418 // Public method to display search results 419 displayResults(results) { 420 const container = this.shadowRoot.querySelector('.search-results'); 421 container.innerHTML = ''; 422 423 if (!results || results.length === 0) { 424 container.innerHTML = '<div style="padding: 2rem; text-align: center; color: var(--search-muted);">No results found</div>'; 425 return; 426 } 427 428 results.forEach((result, index) => { 429 const item = document.createElement('div'); 430 item.className = 'search-result-item'; 431 item.dataset.index = index; 432 item.innerHTML = ` 433 <div class="result-title">${result.title}</div> 434 <div class="result-snippet">${result.snippet || ''}</div> 435 `; 436 item.addEventListener('click', () => { 437 if (result.url) window.location.href = result.url; 438 this.close(); 439 }); 440 container.appendChild(item); 441 }); 442 } 443 444 clearResults() { 445 const container = this.shadowRoot.querySelector('.search-results'); 446 container.innerHTML = ''; 447 448 this.dispatchEvent(new CustomEvent('clear', { bubbles: true })); 449 } 450 451 navigateResults(direction) { 452 const results = this.shadowRoot.querySelectorAll('.search-result-item'); 453 if (!results.length) return; 454 455 const current = this.shadowRoot.querySelector('.search-result-item.selected'); 456 let index = current ? parseInt(current.dataset.index) : -1; 457 458 index += direction; 459 if (index < 0) index = results.length - 1; 460 if (index >= results.length) index = 0; 461 462 results.forEach(r => r.classList.remove('selected')); 463 results[index].classList.add('selected'); 464 results[index].scrollIntoView({ block: 'nearest' }); 465 } 466 467 selectResult() { 468 const selected = this.shadowRoot.querySelector('.search-result-item.selected'); 469 if (selected) selected.click(); 470 } 471} 472 473// Register component 474customElements.define('arod-search', ArodSearch);