diff --git a/css/styles.css b/css/styles.css index 1c4aa15..288ee32 100644 --- a/css/styles.css +++ b/css/styles.css @@ -277,6 +277,7 @@ top-bar .logo { top-bar .actions { display: flex; align-items: center; + gap: var(--spacing-md); } /* ========================================================================== diff --git a/index.html b/index.html index 180b4d1..8b988ca 100644 --- a/index.html +++ b/index.html @@ -11,7 +11,7 @@ diff --git a/js/components/horizontal-scroll-nav.js b/js/components/horizontal-scroll-nav.js index 08a731c..a5190af 100644 --- a/js/components/horizontal-scroll-nav.js +++ b/js/components/horizontal-scroll-nav.js @@ -59,11 +59,13 @@ class HorizontalScrollNav extends HTMLElement { .nav-container { display: flex; - gap: var(--spacing-sm, 0.5rem); + align-items: center; + height: 80px; + gap: 24px; overflow-x: auto; scrollbar-width: none; -ms-overflow-style: none; - padding: var(--spacing-xs, 0.25rem) var(--spacing-sm, 0.5rem); + padding: 0 16px; } .nav-container::-webkit-scrollbar { @@ -72,29 +74,37 @@ class HorizontalScrollNav extends HTMLElement { .nav-pill { flex-shrink: 0; - padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem); - font-size: var(--font-size-sm, 0.875rem); + height: 32px; + padding: 0; + font-size: 16px; font-weight: var(--font-weight-medium, 500); - color: var(--color-text-light, #64748b); - background-color: var(--color-background-tertiary, #f1f5f9); + line-height: 1.5; + color: #000000; + background-color: transparent; border: none; - border-radius: var(--radius-full, 9999px); + border-radius: 0; cursor: pointer; transition: all var(--transition-fast, 150ms ease); white-space: nowrap; + text-decoration: underline; + display: flex; + align-items: center; } .nav-pill:hover { - background-color: var(--color-border, #e2e8f0); + color: #333333; } .nav-pill.active { - color: var(--color-text-inverse, #ffffff); - background-color: var(--color-primary, #2563eb); + color: #ffffff; + background-color: #951D51; + text-decoration: none; + border-radius: 9999px; + padding: 0 16px; } .nav-pill.active:hover { - background-color: var(--color-primary-dark, #1d4ed8); + background-color: #7a1843; } diff --git a/js/components/search-bar.js b/js/components/search-bar.js index 6b1dc13..e232126 100644 --- a/js/components/search-bar.js +++ b/js/components/search-bar.js @@ -1,21 +1,119 @@ /** * Search Bar Component - * Search input with icon + * Search input with mic and search icons on the right + * Includes speech recognition functionality */ +import { micIcon } from "../icons/mic.js"; +import { searchIcon } from "../icons/search.js"; + class SearchBar extends HTMLElement { constructor() { super(); this.attachShadow({ mode: "open" }); + this.recognition = null; + this.isListening = false; } connectedCallback() { this.render(); this.addEventListeners(); + this.initSpeechRecognition(); + } + + disconnectedCallback() { + if (this.recognition) { + this.recognition.stop(); + } + } + + initSpeechRecognition() { + // Check for browser support + const SpeechRecognition = + window.SpeechRecognition || window.webkitSpeechRecognition; + + if (!SpeechRecognition) { + console.warn("Speech recognition is not supported in this browser"); + const micButton = this.shadowRoot.querySelector(".mic-button"); + if (micButton) { + micButton.style.display = "none"; + } + return; + } + + this.recognition = new SpeechRecognition(); + this.recognition.continuous = false; + this.recognition.interimResults = true; + this.recognition.lang = "nl-NL"; + + this.recognition.onstart = () => { + this.isListening = true; + this.updateMicButtonState(); + }; + + this.recognition.onend = () => { + this.isListening = false; + this.updateMicButtonState(); + }; + + this.recognition.onresult = (event) => { + const input = this.shadowRoot.querySelector(".search-input"); + const transcript = Array.from(event.results) + .map((result) => result[0].transcript) + .join(""); + + input.value = transcript; + + // Dispatch input event for live updates + this.dispatchEvent( + new CustomEvent("search-input", { + detail: { query: transcript }, + bubbles: true, + composed: true, + }) + ); + + // If final result, also dispatch search event + if (event.results[0].isFinal) { + this.dispatchEvent( + new CustomEvent("search", { + detail: { query: transcript }, + bubbles: true, + composed: true, + }) + ); + } + }; + + this.recognition.onerror = (event) => { + console.error("Speech recognition error:", event.error); + this.isListening = false; + this.updateMicButtonState(); + }; + } + + updateMicButtonState() { + const micButton = this.shadowRoot.querySelector(".mic-button"); + if (micButton) { + micButton.classList.toggle("listening", this.isListening); + micButton.setAttribute("aria-pressed", this.isListening.toString()); + } + } + + toggleSpeechRecognition() { + if (!this.recognition) return; + + if (this.isListening) { + this.recognition.stop(); + } else { + this.recognition.start(); + } } addEventListeners() { const input = this.shadowRoot.querySelector(".search-input"); const form = this.shadowRoot.querySelector(".search-form"); + const micButton = this.shadowRoot.querySelector(".mic-button"); + const searchButton = this.shadowRoot.querySelector(".search-button"); form.addEventListener("submit", (e) => { e.preventDefault(); @@ -37,9 +135,25 @@ class SearchBar extends HTMLElement { }) ); }); + + micButton.addEventListener("click", () => { + this.toggleSpeechRecognition(); + }); + + searchButton.addEventListener("click", () => { + this.dispatchEvent( + new CustomEvent("search", { + detail: { query: input.value }, + bubbles: true, + composed: true, + }) + ); + }); } render() { + const iconColor = "#951D51"; + this.shadowRoot.innerHTML = ` - - - - + + + + + ${micIcon({ size: 20, color: iconColor })} + + + ${searchIcon({ size: 20, color: iconColor })} + + + `; } diff --git a/js/components/site-header.js b/js/components/site-header.js index fea9913..e32b45c 100644 --- a/js/components/site-header.js +++ b/js/components/site-header.js @@ -26,7 +26,6 @@ class SiteHeader extends HTMLElement { .header { display: flex; flex-direction: column; - gap: var(--spacing-sm, 0.5rem); border-bottom: 1px solid var(--color-border, #e2e8f0); } diff --git a/js/icons/index.js b/js/icons/index.js new file mode 100644 index 0000000..6d17b7d --- /dev/null +++ b/js/icons/index.js @@ -0,0 +1,9 @@ +/** + * Lucide Icons Index + * Re-exports all icons for easy importing + */ +export { micIcon } from "./mic.js"; +export { searchIcon } from "./search.js"; +export { menuIcon } from "./menu.js"; +export { userIcon } from "./user.js"; +export { shoppingBagIcon } from "./shopping-bag.js"; diff --git a/js/icons/menu.js b/js/icons/menu.js new file mode 100644 index 0000000..404e988 --- /dev/null +++ b/js/icons/menu.js @@ -0,0 +1,31 @@ +/** + * Menu Icon (Lucide) + * @param {Object} props - Icon properties + * @param {number} props.size - Icon size (default: 24) + * @param {string} props.color - Icon color (default: currentColor) + * @param {number} props.strokeWidth - Stroke width (default: 2) + * @returns {string} SVG string + */ +export function menuIcon({ + size = 24, + color = "currentColor", + strokeWidth = 2, +} = {}) { + return ` + + + + + + `; +} diff --git a/js/icons/mic.js b/js/icons/mic.js new file mode 100644 index 0000000..2c96392 --- /dev/null +++ b/js/icons/mic.js @@ -0,0 +1,31 @@ +/** + * Mic Icon (Lucide) + * @param {Object} props - Icon properties + * @param {number} props.size - Icon size (default: 24) + * @param {string} props.color - Icon color (default: currentColor) + * @param {number} props.strokeWidth - Stroke width (default: 2) + * @returns {string} SVG string + */ +export function micIcon({ + size = 24, + color = "currentColor", + strokeWidth = 2, +} = {}) { + return ` + + + + + + `; +} diff --git a/js/icons/search.js b/js/icons/search.js new file mode 100644 index 0000000..ff5921d --- /dev/null +++ b/js/icons/search.js @@ -0,0 +1,30 @@ +/** + * Search Icon (Lucide) + * @param {Object} props - Icon properties + * @param {number} props.size - Icon size (default: 24) + * @param {string} props.color - Icon color (default: currentColor) + * @param {number} props.strokeWidth - Stroke width (default: 2) + * @returns {string} SVG string + */ +export function searchIcon({ + size = 24, + color = "currentColor", + strokeWidth = 2, +} = {}) { + return ` + + + + + `; +} diff --git a/js/icons/shopping-bag.js b/js/icons/shopping-bag.js new file mode 100644 index 0000000..a5a70d3 --- /dev/null +++ b/js/icons/shopping-bag.js @@ -0,0 +1,31 @@ +/** + * Shopping Bag Icon (Lucide) + * @param {Object} props - Icon properties + * @param {number} props.size - Icon size (default: 24) + * @param {string} props.color - Icon color (default: currentColor) + * @param {number} props.strokeWidth - Stroke width (default: 2) + * @returns {string} SVG string + */ +export function shoppingBagIcon({ + size = 24, + color = "currentColor", + strokeWidth = 2, +} = {}) { + return ` + + + + + + `; +} diff --git a/js/icons/user.js b/js/icons/user.js new file mode 100644 index 0000000..dda51d5 --- /dev/null +++ b/js/icons/user.js @@ -0,0 +1,30 @@ +/** + * User Icon (Lucide) + * @param {Object} props - Icon properties + * @param {number} props.size - Icon size (default: 24) + * @param {string} props.color - Icon color (default: currentColor) + * @param {number} props.strokeWidth - Stroke width (default: 2) + * @returns {string} SVG string + */ +export function userIcon({ + size = 24, + color = "currentColor", + strokeWidth = 2, +} = {}) { + return ` + + + + + `; +}