feature/site-header #1
@@ -8,12 +8,7 @@
|
|||||||
content="The Midnight Library - Between life and death there is a library"
|
content="The Midnight Library - Between life and death there is a library"
|
||||||
/>
|
/>
|
||||||
<title>The Midnight Library - BookStore</title>
|
<title>The Midnight Library - BookStore</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<!-- Fonts are loaded via @font-face in styles.css -->
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
||||||
<link
|
|
||||||
href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&display=swap"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
<link rel="stylesheet" href="css/styles.css" />
|
<link rel="stylesheet" href="css/styles.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1,3 +1,25 @@
|
|||||||
|
/* ==========================================================================
|
||||||
|
Font Definitions
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Outline";
|
||||||
|
src: url("../fonts/Outline-Regular.woff2") format("woff2"),
|
||||||
|
url("../fonts/Outline-Regular.woff") format("woff");
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Outline";
|
||||||
|
src: url("../fonts/Outline-Light.woff2") format("woff2"),
|
||||||
|
url("../fonts/Outline-Light.woff") format("woff");
|
||||||
|
font-weight: 300;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
CSS Reset & Normalize (Modern Best Practices)
|
CSS Reset & Normalize (Modern Best Practices)
|
||||||
Based on normalize.css v8.0.1 + modern resets
|
Based on normalize.css v8.0.1 + modern resets
|
||||||
@@ -175,15 +197,19 @@ table {
|
|||||||
--color-border: #e2e8f0;
|
--color-border: #e2e8f0;
|
||||||
--color-border-light: #f1f5f9;
|
--color-border-light: #f1f5f9;
|
||||||
|
|
||||||
|
--color-push-box-bg: #EBEEF4;
|
||||||
|
|
||||||
--color-success: #22c55e;
|
--color-success: #22c55e;
|
||||||
--color-error: #ef4444;
|
--color-error: #ef4444;
|
||||||
--color-warning: #f59e0b;
|
--color-warning: #f59e0b;
|
||||||
|
|
||||||
/* Typography */
|
/* Typography */
|
||||||
--font-family-base: "Outfit", system-ui, -apple-system, BlinkMacSystemFont,
|
--font-family-base: "Outline", system-ui, -apple-system, BlinkMacSystemFont,
|
||||||
"Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue",
|
"Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue",
|
||||||
sans-serif;
|
sans-serif;
|
||||||
--font-family-heading: var(--font-family-base);
|
--font-family-heading: var(--font-family-base);
|
||||||
|
--font-family-outfit: "Outfit", system-ui, -apple-system, BlinkMacSystemFont,
|
||||||
|
"Segoe UI", Roboto, sans-serif;
|
||||||
|
|
||||||
--font-size-xs: 0.75rem; /* 12px */
|
--font-size-xs: 0.75rem; /* 12px */
|
||||||
--font-size-sm: 0.875rem; /* 14px */
|
--font-size-sm: 0.875rem; /* 14px */
|
||||||
@@ -193,14 +219,16 @@ table {
|
|||||||
--font-size-2xl: 1.5rem; /* 24px */
|
--font-size-2xl: 1.5rem; /* 24px */
|
||||||
--font-size-3xl: 1.875rem; /* 30px */
|
--font-size-3xl: 1.875rem; /* 30px */
|
||||||
|
|
||||||
|
--font-weight-light: 300;
|
||||||
--font-weight-normal: 400;
|
--font-weight-normal: 400;
|
||||||
--font-weight-medium: 500;
|
--font-weight-medium: 500;
|
||||||
--font-weight-semibold: 600;
|
--font-weight-semibold: 600;
|
||||||
--font-weight-bold: 700;
|
--font-weight-bold: 700;
|
||||||
|
|
||||||
--line-height-tight: 1.25;
|
--line-height-tight: 1.25;
|
||||||
--line-height-normal: 1.5;
|
--line-height-normal: 1.5; /* 24px at 16px base */
|
||||||
--line-height-relaxed: 1.75;
|
--line-height-relaxed: 1.75;
|
||||||
|
--line-height-24: 24px;
|
||||||
|
|
||||||
/* Spacing */
|
/* Spacing */
|
||||||
--spacing-xs: 0.25rem; /* 4px */
|
--spacing-xs: 0.25rem; /* 4px */
|
||||||
@@ -268,8 +296,8 @@ top-bar .icon-button svg {
|
|||||||
|
|
||||||
top-bar .logo {
|
top-bar .logo {
|
||||||
font-family: var(--font-family-base);
|
font-family: var(--font-family-base);
|
||||||
font-size: 1.25rem;
|
font-size: var(--font-size-xl);
|
||||||
font-weight: 700;
|
font-weight: var(--font-weight-bold);
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
@@ -277,6 +305,7 @@ top-bar .logo {
|
|||||||
top-bar .actions {
|
top-bar .actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: var(--spacing-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
|
|||||||
BIN
images/logo-asoka.png
Normal file
BIN
images/logo-asoka.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
12
index.html
12
index.html
@@ -8,10 +8,11 @@
|
|||||||
content="Milinda - Discover and buy your next favorite book"
|
content="Milinda - Discover and buy your next favorite book"
|
||||||
/>
|
/>
|
||||||
<title>Milinda - Home</title>
|
<title>Milinda - Home</title>
|
||||||
|
<!-- Fonts are loaded via @font-face in styles.css -->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;700&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
<link rel="stylesheet" href="css/styles.css" />
|
<link rel="stylesheet" href="css/styles.css" />
|
||||||
@@ -81,6 +82,15 @@
|
|||||||
</site-header>
|
</site-header>
|
||||||
|
|
||||||
<site-content>
|
<site-content>
|
||||||
|
<push-box>
|
||||||
|
<img slot="logo" src="images/logo-asoka.png" alt="Asoka Logo" />
|
||||||
|
<h2 slot="title">
|
||||||
|
Gespecialiseerd op het vlak van boeddhisme en aanverwante Oost-West
|
||||||
|
thema's
|
||||||
|
</h2>
|
||||||
|
<arrow-button slot="cta" href="#">Meer over Asoka</arrow-button>
|
||||||
|
</push-box>
|
||||||
|
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<h2 class="section-title">Featured Books</h2>
|
<h2 class="section-title">Featured Books</h2>
|
||||||
<div class="book-grid">
|
<div class="book-grid">
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import './components/search-bar.js';
|
|||||||
import './components/site-content.js';
|
import './components/site-content.js';
|
||||||
import './components/site-footer.js';
|
import './components/site-footer.js';
|
||||||
import './components/book-card.js';
|
import './components/book-card.js';
|
||||||
|
import './components/push-box.js';
|
||||||
|
import './components/arrow-button.js';
|
||||||
|
|
||||||
// App initialization (if needed)
|
// App initialization (if needed)
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
|||||||
88
js/components/arrow-button.js
Normal file
88
js/components/arrow-button.js
Normal file
@@ -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 = `
|
||||||
|
<style>
|
||||||
|
:host {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
transition: opacity var(--transition-fast, 150ms ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-button:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: 1.5px solid currentColor;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-icon svg {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
stroke: currentColor;
|
||||||
|
fill: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-text {
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 3px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<a class="arrow-button" href="${this.href}">
|
||||||
|
<span class="arrow-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M5 12h14"></path>
|
||||||
|
<path d="m12 5 7 7-7 7"></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="button-text">
|
||||||
|
<slot></slot>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("arrow-button", ArrowButton);
|
||||||
@@ -59,11 +59,13 @@ class HorizontalScrollNav extends HTMLElement {
|
|||||||
|
|
||||||
.nav-container {
|
.nav-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing-sm, 0.5rem);
|
align-items: center;
|
||||||
|
height: 80px;
|
||||||
|
gap: 24px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none;
|
||||||
padding: var(--spacing-xs, 0.25rem) var(--spacing-sm, 0.5rem);
|
padding: 0 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-container::-webkit-scrollbar {
|
.nav-container::-webkit-scrollbar {
|
||||||
@@ -72,29 +74,39 @@ class HorizontalScrollNav extends HTMLElement {
|
|||||||
|
|
||||||
.nav-pill {
|
.nav-pill {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
|
height: 32px;
|
||||||
font-size: var(--font-size-sm, 0.875rem);
|
padding: 0;
|
||||||
|
font-family: var(--font-family-base);
|
||||||
|
font-size: var(--font-size-base, 16px);
|
||||||
font-weight: var(--font-weight-medium, 500);
|
font-weight: var(--font-weight-medium, 500);
|
||||||
color: var(--color-text-light, #64748b);
|
line-height: var(--line-height-24, 24px);
|
||||||
background-color: var(--color-background-tertiary, #f1f5f9);
|
color: #000000;
|
||||||
|
background-color: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--radius-full, 9999px);
|
border-radius: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all var(--transition-fast, 150ms ease);
|
transition: all var(--transition-fast, 150ms ease);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-pill:hover {
|
.nav-pill:hover {
|
||||||
background-color: var(--color-border, #e2e8f0);
|
color: #333333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-pill.active {
|
.nav-pill.active {
|
||||||
color: var(--color-text-inverse, #ffffff);
|
color: #ffffff;
|
||||||
background-color: var(--color-primary, #2563eb);
|
background-color: #951D51;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 9999px;
|
||||||
|
padding: 0 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-pill.active:hover {
|
.nav-pill.active:hover {
|
||||||
background-color: var(--color-primary-dark, #1d4ed8);
|
background-color: #7a1843;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<nav class="nav-container" role="navigation" aria-label="Book categories">
|
<nav class="nav-container" role="navigation" aria-label="Book categories">
|
||||||
|
|||||||
93
js/components/push-box.js
Normal file
93
js/components/push-box.js
Normal file
@@ -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 = `
|
||||||
|
<style>
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.push-box {
|
||||||
|
background-color: ${this.backgroundColor};
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 48px 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-wrapper {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-wrapper ::slotted(img) {
|
||||||
|
width: 104px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-wrapper ::slotted(*) {
|
||||||
|
font-family: var(--font-family-outfit, "Outfit", sans-serif);
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 34px;
|
||||||
|
color: #000000;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-wrapper {
|
||||||
|
color: #951D51;
|
||||||
|
font-family: var(--font-family-outfit, "Outfit", sans-serif);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="push-box">
|
||||||
|
<div class="logo-wrapper">
|
||||||
|
<slot name="logo"></slot>
|
||||||
|
</div>
|
||||||
|
<div class="title-wrapper">
|
||||||
|
<slot name="title"></slot>
|
||||||
|
</div>
|
||||||
|
<div class="cta-wrapper">
|
||||||
|
<slot name="cta"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("push-box", PushBox);
|
||||||
@@ -1,21 +1,119 @@
|
|||||||
/**
|
/**
|
||||||
* Search Bar Component
|
* 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 {
|
class SearchBar extends HTMLElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.attachShadow({ mode: "open" });
|
this.attachShadow({ mode: "open" });
|
||||||
|
this.recognition = null;
|
||||||
|
this.isListening = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.render();
|
this.render();
|
||||||
this.addEventListeners();
|
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() {
|
addEventListeners() {
|
||||||
const input = this.shadowRoot.querySelector(".search-input");
|
const input = this.shadowRoot.querySelector(".search-input");
|
||||||
const form = this.shadowRoot.querySelector(".search-form");
|
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) => {
|
form.addEventListener("submit", (e) => {
|
||||||
e.preventDefault();
|
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() {
|
render() {
|
||||||
|
const iconColor = "#951D51";
|
||||||
|
|
||||||
this.shadowRoot.innerHTML = `
|
this.shadowRoot.innerHTML = `
|
||||||
<style>
|
<style>
|
||||||
:host {
|
:host {
|
||||||
@@ -49,53 +163,115 @@ class SearchBar extends HTMLElement {
|
|||||||
.search-form {
|
.search-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
position: relative;
|
height: 50px;
|
||||||
padding: var(--spacing-sm, 0.5rem) var(--spacing-sm, 0.5rem);
|
padding: 8px 16px;
|
||||||
background-color: #951D51;
|
background-color: #951D51;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-icon {
|
.search-container {
|
||||||
position: absolute;
|
display: flex;
|
||||||
left: var(--spacing-md, 1rem);
|
align-items: center;
|
||||||
width: 20px;
|
width: 100%;
|
||||||
height: 20px;
|
height: 40px;
|
||||||
color: var(--color-text-light, #64748b);
|
background-color: #FFF;
|
||||||
pointer-events: none;
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input {
|
.search-input {
|
||||||
width: 100%;
|
flex: 1;
|
||||||
padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
|
height: 100%;
|
||||||
padding-left: calc(var(--spacing-md, 1rem) + 20px + var(--spacing-sm, 0.5rem));
|
padding: 0 16px;
|
||||||
font-size: var(--font-size-sm, 0.875rem);
|
font-family: var(--font-family-base);
|
||||||
color: var(--color-text, #1e293b);
|
font-size: var(--font-size-base, 16px);
|
||||||
background-color: var(--color-background-tertiary, #f1f5f9);
|
font-weight: var(--font-weight-light, 300);
|
||||||
border: 1px solid transparent;
|
color: #383838;
|
||||||
border-radius: var(--radius-lg, 0.75rem);
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: all var(--transition-fast, 150ms ease);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input::placeholder {
|
.search-input::placeholder {
|
||||||
color: var(--color-text-light, #64748b);
|
font-family: var(--font-family-base);
|
||||||
|
font-size: var(--font-size-base, 16px);
|
||||||
|
font-weight: var(--font-weight-light, 300);
|
||||||
|
color: rgba(56, 56, 56, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input:focus {
|
.icon-buttons {
|
||||||
background-color: var(--color-background, #ffffff);
|
display: flex;
|
||||||
border-color: var(--color-primary, #2563eb);
|
align-items: center;
|
||||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
gap: 4px;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button:hover {
|
||||||
|
background-color: rgba(149, 29, 81, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button:active {
|
||||||
|
background-color: rgba(149, 29, 81, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button.listening {
|
||||||
|
background-color: rgba(149, 29, 81, 0.2);
|
||||||
|
animation: pulse 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<form class="search-form" role="search">
|
<form class="search-form" role="search">
|
||||||
<svg class="search-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
<div class="search-container">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
|
|
||||||
</svg>
|
|
||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
class="search-input"
|
class="search-input"
|
||||||
placeholder="Search books, authors..."
|
placeholder="Waar ben je naar op zoek?"
|
||||||
aria-label="Search books and authors"
|
aria-label="Zoek boeken en auteurs"
|
||||||
>
|
>
|
||||||
|
<div class="icon-buttons">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="icon-button mic-button"
|
||||||
|
aria-label="Zoeken met spraak"
|
||||||
|
aria-pressed="false"
|
||||||
|
>
|
||||||
|
${micIcon({ size: 20, color: iconColor })}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="icon-button search-button"
|
||||||
|
aria-label="Zoeken"
|
||||||
|
>
|
||||||
|
${searchIcon({ size: 20, color: iconColor })}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ class SiteHeader extends HTMLElement {
|
|||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--spacing-sm, 0.5rem);
|
|
||||||
border-bottom: 1px solid var(--color-border, #e2e8f0);
|
border-bottom: 1px solid var(--color-border, #e2e8f0);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -65,9 +65,9 @@ class TopBar extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::slotted([slot="logo"]) {
|
::slotted([slot="logo"]) {
|
||||||
font-family: 'Outfit', system-ui, sans-serif;
|
font-family: var(--font-family-base);
|
||||||
font-size: 1.25rem;
|
font-size: var(--font-size-xl, 1.25rem);
|
||||||
font-weight: 700;
|
font-weight: var(--font-weight-bold, 700);
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|||||||
9
js/icons/index.js
Normal file
9
js/icons/index.js
Normal file
@@ -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";
|
||||||
31
js/icons/menu.js
Normal file
31
js/icons/menu.js
Normal file
@@ -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 `
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="${size}"
|
||||||
|
height="${size}"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="${color}"
|
||||||
|
stroke-width="${strokeWidth}"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<line x1="4" x2="20" y1="12" y2="12"/>
|
||||||
|
<line x1="4" x2="20" y1="6" y2="6"/>
|
||||||
|
<line x1="4" x2="20" y1="18" y2="18"/>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
}
|
||||||
31
js/icons/mic.js
Normal file
31
js/icons/mic.js
Normal file
@@ -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 `
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="${size}"
|
||||||
|
height="${size}"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="${color}"
|
||||||
|
stroke-width="${strokeWidth}"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/>
|
||||||
|
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
|
||||||
|
<line x1="12" x2="12" y1="19" y2="22"/>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
}
|
||||||
30
js/icons/search.js
Normal file
30
js/icons/search.js
Normal file
@@ -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 `
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="${size}"
|
||||||
|
height="${size}"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="${color}"
|
||||||
|
stroke-width="${strokeWidth}"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="11" cy="11" r="8"/>
|
||||||
|
<path d="m21 21-4.3-4.3"/>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
}
|
||||||
31
js/icons/shopping-bag.js
Normal file
31
js/icons/shopping-bag.js
Normal file
@@ -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 `
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="${size}"
|
||||||
|
height="${size}"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="${color}"
|
||||||
|
stroke-width="${strokeWidth}"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z"/>
|
||||||
|
<path d="M3 6h18"/>
|
||||||
|
<path d="M16 10a4 4 0 0 1-8 0"/>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
}
|
||||||
30
js/icons/user.js
Normal file
30
js/icons/user.js
Normal file
@@ -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 `
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="${size}"
|
||||||
|
height="${size}"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="${color}"
|
||||||
|
stroke-width="${strokeWidth}"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="8" r="5"/>
|
||||||
|
<path d="M20 21a8 8 0 0 0-16 0"/>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user