diff --git a/js/app.js b/js/app.js
index d1b66f1..289184c 100644
--- a/js/app.js
+++ b/js/app.js
@@ -11,6 +11,8 @@ import './components/search-bar.js';
import './components/site-content.js';
import './components/site-footer.js';
import './components/book-card.js';
+import './components/push-box.js';
+import './components/arrow-button.js';
// App initialization (if needed)
document.addEventListener('DOMContentLoaded', () => {
diff --git a/js/components/arrow-button.js b/js/components/arrow-button.js
new file mode 100644
index 0000000..be19ff5
--- /dev/null
+++ b/js/components/arrow-button.js
@@ -0,0 +1,88 @@
+/**
+ * Arrow Button Component
+ * A CTA link with a circular arrow icon
+ * Uses a slot for the button text content
+ */
+class ArrowButton extends HTMLElement {
+ static get observedAttributes() {
+ return ["href"];
+ }
+
+ constructor() {
+ super();
+ this.attachShadow({ mode: "open" });
+ }
+
+ connectedCallback() {
+ this.render();
+ }
+
+ attributeChangedCallback() {
+ this.render();
+ }
+
+ get href() {
+ return this.getAttribute("href") || "#";
+ }
+
+ render() {
+ this.shadowRoot.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ }
+}
+
+customElements.define("arrow-button", ArrowButton);
diff --git a/js/components/horizontal-scroll-nav.js b/js/components/horizontal-scroll-nav.js
index 08a731c..4b0c2fd 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,39 @@ 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-family: var(--font-family-base);
+ font-size: var(--font-size-base, 16px);
font-weight: var(--font-weight-medium, 500);
- color: var(--color-text-light, #64748b);
- background-color: var(--color-background-tertiary, #f1f5f9);
+ line-height: var(--line-height-24, 24px);
+ 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;
+ text-underline-offset: 4px;
+ 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/push-box.js b/js/components/push-box.js
new file mode 100644
index 0000000..531bc55
--- /dev/null
+++ b/js/components/push-box.js
@@ -0,0 +1,93 @@
+/**
+ * Push Box Component
+ * A promotional container with logo, title, and CTA
+ * Uses slots for all content to allow easy customization in HTML
+ */
+class PushBox extends HTMLElement {
+ static get observedAttributes() {
+ return ["background-color", "text-color"];
+ }
+
+ constructor() {
+ super();
+ this.attachShadow({ mode: "open" });
+ }
+
+ connectedCallback() {
+ this.render();
+ }
+
+ attributeChangedCallback() {
+ this.render();
+ }
+
+ get backgroundColor() {
+ return (
+ this.getAttribute("background-color") ||
+ "var(--color-push-box-bg, #EBEEF4)"
+ );
+ }
+
+ get textColor() {
+ return this.getAttribute("text-color") || "#951D51";
+ }
+
+ render() {
+ this.shadowRoot.innerHTML = `
+
+
+ `;
+ }
+}
+
+customElements.define("push-box", PushBox);
diff --git a/js/components/search-bar.js b/js/components/search-bar.js
index 6b1dc13..34d9eea 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 = `
`;
}
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/components/top-bar.js b/js/components/top-bar.js
index bdaf489..b93d207 100644
--- a/js/components/top-bar.js
+++ b/js/components/top-bar.js
@@ -65,9 +65,9 @@ class TopBar extends HTMLElement {
}
::slotted([slot="logo"]) {
- font-family: 'Outfit', system-ui, sans-serif;
- font-size: 1.25rem;
- font-weight: 700;
+ font-family: var(--font-family-base);
+ font-size: var(--font-size-xl, 1.25rem);
+ font-weight: var(--font-weight-bold, 700);
color: #ffffff;
text-decoration: none;
}
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 `
+
+
+
+
+ `;
+}