// Miroir Search Web Component (plan ยง13.21) // // (function() { 'use strict'; const TEMPLATE = document.createElement('template'); TEMPLATE.innerHTML = `
`; class MiroirSearch extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); this.shadowRoot.appendChild(TEMPLATE.content.cloneNode(true)); this._index = null; this._accent = null; this._origin = null; this._sessionToken = null; this._debounceTimer = null; this._sessionId = crypto.randomUUID(); } static get observedAttributes() { return ['index', 'accent', 'origin', 'dark-mode']; } attributeChangedCallback(name, oldValue, newValue) { if (oldValue === newValue) return; switch (name) { case 'index': this._index = newValue; this._loadSession(); break; case 'accent': this._accent = newValue; this.style.setProperty('--miroir-accent', newValue); break; case 'origin': this._origin = newValue; break; case 'dark-mode': // Handled by CSS :host([dark-mode]) selector break; } } connectedCallback() { // Get attributes this._index = this.getAttribute('index') || 'default'; this._accent = this.getAttribute('accent'); this._origin = this.getAttribute('origin') || window.location.origin; // Set accent color if provided if (this._accent) { this.style.setProperty('--miroir-accent', this._accent); } // Set up event listeners this._input = this.shadowRoot.querySelector('.miroir-widget-input'); this._button = this.shadowRoot.querySelector('.miroir-widget-button'); this._resultsContainer = this.shadowRoot.querySelector('.miroir-widget-results'); this._button.addEventListener('click', () => this._performSearch()); this._input.addEventListener('input', () => { clearTimeout(this._debounceTimer); this._debounceTimer = setTimeout(() => this._performSearch(), 150); }); this._input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { this._performSearch(); } }); // Load session this._loadSession(); // Dispatch custom event when ready this.dispatchEvent(new CustomEvent('miroir-ready', { bubbles: true, composed: true, detail: { index: this._index } })); } async _loadSession() { if (!this._index) return; try { const response = await fetch(`${this._origin}/_miroir/ui/search/${this._index}/session`); if (!response.ok) { throw new Error('Failed to get session'); } const data = await response.json(); this._sessionToken = data.token; } catch (error) { this._showError('Failed to initialize: ' + error.message); } } async _performSearch() { if (!this._sessionToken) { this._showError('Session not loaded'); return; } const query = this._input.value.trim(); if (!query) { this._resultsContainer.innerHTML = ''; return; } this._showLoading(); try { const response = await fetch(`${this._origin}/indexes/${this._index}/search`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this._sessionToken}` }, body: JSON.stringify({ q: query, limit: 10, attributesToRetrieve: ['*'], attributesToHighlight: ['*'] }) }); if (!response.ok) { throw new Error(`Search failed: ${response.status}`); } const data = await response.json(); this._renderResults(data); // Dispatch custom event with results this.dispatchEvent(new CustomEvent('miroir-results', { bubbles: true, composed: true, detail: { query, count: data.estimatedTotalHits || 0, hits: data.hits || [] } })); } catch (error) { this._showError(error.message); } } _renderResults(data) { if (!data.hits || data.hits.length === 0) { this._resultsContainer.innerHTML = '
No results found
'; return; } const html = data.hits.map((hit, index) => { const formatted = hit._formatted || {}; const titleAttr = this._getAttributeValue(formatted, hit, 'title') || this._getAttributeValue(formatted, hit, 'name') || hit.id || 'Untitled'; const snippet = this._getAttributeValue(formatted, hit, 'description') || ''; return `
${this._escapeHtml(titleAttr)}
${snippet ? `
${this._escapeHtml(snippet)}
` : ''}
`; }).join(''); this._resultsContainer.innerHTML = html; // Add click listeners this._resultsContainer.querySelectorAll('.miroir-widget-result').forEach(result => { result.addEventListener('click', () => { const index = parseInt(result.dataset.index, 10); const hit = data.hits[index]; this.dispatchEvent(new CustomEvent('miroir-result-click', { bubbles: true, composed: true, detail: { hit, index, query: this._input.value } })); }); }); } _getAttributeValue(formatted, hit, attr) { return formatted[attr] || hit[attr]; } _escapeHtml(text) { if (typeof text !== 'string') return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } _showLoading() { this._resultsContainer.innerHTML = '
Searching...
'; } _showError(message) { this._resultsContainer.innerHTML = `
${this._escapeHtml(message)}
`; } // Public API methods /** Perform a search with the given query */ search(query) { this._input.value = query; this._performSearch(); } /** Clear the search input and results */ clear() { this._input.value = ''; this._resultsContainer.innerHTML = ''; } } // Register the custom element if (!customElements.get('miroir-search')) { customElements.define('miroir-search', MiroirSearch); } })();