Compare commits

..

3 Commits

Author SHA1 Message Date
Tim Rijkse
5755d43cfc fix: add action list 2026-01-16 09:23:37 +01:00
Tim Rijkse
337f5dbf5b fix: add book details 2026-01-16 08:51:28 +01:00
Tim Rijkse
7925172039 fix: externalise icons 2026-01-16 08:20:19 +01:00
20 changed files with 282 additions and 2310 deletions

492
README.md
View File

@@ -1,492 +0,0 @@
# Milinda Uitgevers — Herontwerp Mobiele Webshop
> **Pitch voor het herontwerp van de webshop van Milinda Uitgevers**
---
## 📖 Over dit project
Dit project is een pitch voor het herontwerp van de webshop van Milinda Uitgevers. De huidige website is ruim vijftien jaar oud en voldoet niet meer aan de eisen van vandaag op het gebied van responsive design en toegankelijkheid.
### Doelstelling
Het doel van dit herontwerp is om:
1. **Gebruiksgemak op alle platforms** — Een volledig responsieve mobiele ervaring die naadloos werkt op smartphones, tablets en desktops
2. **Toegankelijkheid** — Volledige ondersteuning voor gebruikers met een beperking (WCAG-richtlijnen)
3. **Behoud van de "feel"** — De oorspronkelijke sfeer en identiteit van de website behouden terwijl de visuele presentatie wordt gemoderniseerd
### Waarom deze aanpak?
We hebben gekozen voor een **Web Components-architectuur** om de volgende redenen:
- **Geen externe frameworks** — Werkt met vanilla JavaScript, geen React/Vue/Angular nodig
- **Eenvoudige integratie** — De componenten kunnen gemakkelijk worden geïntegreerd met de bestaande back-end
- **Toekomstbestendig** — Web Components zijn een webstandaard en werken in alle moderne browsers
- **Modulair** — Elk component is zelfstandig en herbruikbaar
- **Geen server-side libraries** — Volledig client-side, zoals gevraagd in de opdracht
---
## 🎯 UX Onderbouwing
### Paginastructuur
De pagina's zijn opgebouwd volgens een bewuste hiërarchie die de gebruiker begeleidt van oriëntatie naar actie:
```
┌─────────────────────────────────┐
│ Header (sticky) │ ← Altijd toegankelijk
│ ├── Logo + Menu + Acties │
│ ├── Categorie navigatie │
│ └── Zoekbalk │
├─────────────────────────────────┤
│ Hoofdinhoud │ ← Scrollbare content
│ ├── Promotieblok (push-box) │
│ ├── Boekensecties │
│ ├── Categorieën │
│ └── Nieuwsbrief │
├─────────────────────────────────┤
│ Footer │ ← Service informatie
│ └── Accordion navigatie │
└─────────────────────────────────┘
```
### Ontwerpkeuzes per element
#### 🔍 Zoekbalk (prominent in header)
**Waarom:** De zoekfunctie staat bovenaan omdat:
- **Directe toegang** — Terugkerende klanten weten vaak al wat ze zoeken
- **Tijdsbesparing** — Sneller dan door categorieën navigeren
- **Spraakherkenning** — Extra toegankelijkheid voor gebruikers met beperkingen of onderweg
- **Verwachting** — Gebruikers verwachten een zoekfunctie bij een webshop
#### 📱 Mobile Drawer (slide-in navigatie)
**Waarom een drawer in plaats van dropdown:**
- **Meer ruimte** — Volledige schermhoogte voor uitgebreide navigatie
- **Touch-vriendelijk** — Grote klikgebieden, swipe om te sluiten
- **Focus** — Backdrop voorkomt interactie met achterliggende content
- **Overzicht** — Alle categorieën en imprints in één overzicht
- **Authenticatie** — Plek voor inlog/registratie knoppen
#### 📚 Boekkaarten (horizontale layout)
**Waarom horizontale kaarten:**
- **Scanbaarheid** — Cover links trekt de aandacht, tekst rechts geeft context
- **Efficiënt** — Meer informatie zichtbaar per kaart dan bij verticale layout
- **Touch targets** — Grote klikbare gebieden voor mobiel gebruik
- **Consistentie** — Herkenbare structuur door hele site
**Informatiehiërarchie:**
1. **Titel** (vetgedrukt, onderstreept) — Primair identificatie
2. **Beschrijving** — Context over de inhoud
3. **Auteur** (paars, onderstreept) — Secundair, maar klikbaar
4. **Prijs** (vetgedrukt) — Koopbeslissing informatie
5. **Winkelwagen knop** — Directe actie
#### 📦 Push Boxes (promotieblokken)
**Waarom push boxes:**
- **Visuele onderbreking** — Doorbreekt de lijst met boeken
- **Merkidentiteit** — Ruimte voor imprint logos (Asoka)
- **Call-to-action** — Stuurt naar belangrijke pagina's
- **Flexibiliteit** — Twee varianten (default/paars) voor verschillende contexten
**Plaatsing:**
- **Bovenaan homepage** — Introductie van het merk/imprint
- **Onder boekdetails** — Klantenservice toegang na productinfo
- **Strategisch** — Voorkomt "scroll fatigue" bij lange pagina's
#### 🏷️ Categoriekaarten (grid layout)
**Waarom visuele categorieën:**
- **Herkenning** — Iconen zijn sneller te verwerken dan tekst
- **Uitnodigend** — Visuele elementen nodigen uit tot verkennen
- **Oriëntatie** — Helpt nieuwe bezoekers het aanbod te begrijpen
- **Twee kolommen** — Optimaal voor mobiele weergave
#### 📑 Tabbladen (boekdetailpagina)
**Waarom tabs voor Beschrijving/Inzage/Recensies:**
- **Ruimtebesparing** — Voorkomt zeer lange pagina's
- **Gebruikerskeuze** — Laat gebruiker kiezen wat relevant is
- **Focus** — Toont één type informatie tegelijk
- **Verwachting** — Bekend patroon van andere webshops
#### 📰 Nieuwsbrief sectie
**Waarom onderaan (maar boven footer):**
- **Niet opdringerig** — Staat niet in de weg van productinhoud
- **Logische plek** — Na het bekijken van content, uitnodiging om verbonden te blijven
- **Visueel onderscheid** — Border maakt het herkenbaar als apart element
#### 🦶 Footer met accordions
**Waarom accordions:**
- **Ruimtebesparing** — Veel links zonder lange scrolllijsten
- **Georganiseerd** — Duidelijke categorisering van footer content
- **Touch-vriendelijk** — Grote tap targets voor mobiel
- **Verwachting** — Standaard patroon voor mobiele footers
### Visuele hiërarchie
De kleurkeuzes ondersteunen de gebruikerservaring:
| Element | Kleur | Reden |
|---------|-------|-------|
| Primaire acties | Paars (#951D51) | Opvallend, merkidentiteit |
| Links | Paars + onderstreept | Herkenbaar als klikbaar |
| Titels | Zwart + vetgedrukt | Maximaal contrast, hiërarchie |
| Secundaire tekst | Grijs | Ondersteunend, niet afleidend |
| Achtergronden | Lichtgrijs varianten | Subtiel onderscheid tussen secties |
### Scroll-gedrag header
**Smart header die inklapt bij scrollen:**
- **Naar beneden** — Header klapt deels in, meer ruimte voor content
- **Naar boven** — Header verschijnt weer, navigatie altijd bereikbaar
- **Drempel van 100px** — Voorkomt "flikkeren" bij kleine scrollbewegingen
Dit patroon maximaliseert schermruimte terwijl navigatie toegankelijk blijft.
---
## 🚀 Project starten
### Vereisten
- Node.js 18.0.0 of hoger
### Installatie
```bash
# Installeer dependencies
npm install
# Start de development server
npm start
```
De applicatie is vervolgens beschikbaar op `http://localhost:3000`.
### Beschikbare pagina's
| Pagina | URL | Beschrijving |
|--------|-----|--------------|
| Homepage | `/index.html` | Overzicht met uitgelichte boeken, categorieën en nieuwsbrief |
| Boekdetail | `/book.html` | Detailpagina voor "Zen is opendoen" |
---
## 📁 Projectstructuur
```
milinda-pitch/
├── css/
│ └── styles.css # Globale stijlen en design tokens
├── images/ # Afbeeldingen en iconen
├── js/
│ ├── app.js # Hoofdapplicatie entry point
│ ├── components/ # Web Components
│ ├── icons/ # SVG icon components
│ └── store/ # State management (winkelwagen)
├── index.html # Homepage
├── book.html # Boekdetailpagina
└── package.json # Project configuratie
```
---
## 🧩 Componenten
### Layout Componenten
| Component | Bestand | Beschrijving |
|-----------|---------|--------------|
| `<site-header>` | `site-header.js` | Sticky header met scroll-gedrag: klapt in bij scrollen naar beneden, verschijnt bij omhoog scrollen |
| `<top-bar>` | `top-bar.js` | Bovenste balk met logo, menu-knop en acties (profiel, winkelwagen) |
| `<horizontal-scroll-nav>` | `horizontal-scroll-nav.js` | Horizontaal scrollbare categorie-navigatie met pill-vormige knoppen |
| `<search-bar>` | `search-bar.js` | Zoekbalk met spraakherkenning (Nederlands) en zoek-icoon |
| `<site-content>` | `site-content.js` | Wrapper voor de hoofdinhoud van de pagina |
| `<site-footer>` | `site-footer.js` | Footer met accordion-secties en linkkolommen |
| `<mobile-drawer>` | `mobile-drawer.js` | Slide-in navigatiemenu vanaf de linkerzijde met backdrop |
### Content Componenten
| Component | Bestand | Beschrijving |
|-----------|---------|--------------|
| `<book-card>` | `book-card.js` | Boekkaart met cover, titel, beschrijving, auteur en prijs. Ondersteunt licht/donker thema |
| `<book-details>` | `book-details.js` | Uitgebreide boekdetails met metadata, categorieën en koopknoppen |
| `<book-description>` | `book-description.js` | Gestileerde boekbeschrijving met citaten |
| `<book-reviews>` | `book-reviews.js` | Container voor boekrecensies |
| `<book-review-item>` | `book-review-item.js` | Individuele recensie met sterren, auteur en datum |
| `<category-card>` | `category-card.js` | Categoriekaart met icoon en titel |
| `<image-gallery>` | `image-gallery.js` | Afbeeldingsgalerij met thumbnails |
| `<content-tabs>` | `content-tabs.js` | Tabblad-interface voor verschillende secties (Beschrijving, Inzage, Recensies) met volledige keyboard navigatie |
### Interactieve Componenten
| Component | Bestand | Beschrijving |
|-----------|---------|--------------|
| `<push-box>` | `push-box.js` | Promotionele container met logo, titel en CTA. Ondersteunt `variant="purple"` |
| `<newsletter-signup>` | `newsletter-signup.js` | Nieuwsbrief aanmeldformulier met e-mail input |
| `<footer-accordion-item>` | `footer-accordion-item.js` | Inklapbare sectie voor de footer met ARIA-ondersteuning |
| `<add-to-cart-button>` | `add-to-cart-button.js` | Plus-knop voor toevoegen aan winkelwagen |
### Button Componenten
| Component | Bestand | Beschrijving |
|-----------|---------|--------------|
| `<arrow-button>` | `arrow-button.js` | Link met ronde pijl-icoon |
| `<cta-button>` | `cta-button.js` | Full-width call-to-action knop |
| `<icon-cta-button>` | `icon-cta-button.js` | CTA knop met icoon (winkelwagen, e-book) |
| `<icon-link-button>` | `icon-link-button.js` | Link met icoon (verlanglijstje, recensie) |
| `<action-links-list>` | `action-links-list.js` | Gegroepeerde actie-links |
### Utility Componenten
| Component | Bestand | Beschrijving |
|-----------|---------|--------------|
| `<section-title>` | `section-title.js` | Consistente sectietitel |
---
## 🎨 Iconen
Alle iconen zijn geïmplementeerd als Web Components en/of exporteerbare functies. Ze ondersteunen `size`, `color` en `stroke-width` attributen.
| Icon Component | Bestand | Beschrijving |
|----------------|---------|--------------|
| `<menu-icon>` | `menu-icon.js` | Hamburger menu icoon (3 lijnen) |
| `<user-icon>` | `user-icon.js` | Gebruikersprofiel icoon |
| `<shopping-bag-icon>` | `shopping-bag-icon.js` | Boodschappentas icoon |
| `<arrow-circle-right-icon>` | `arrow-circle-right-icon.js` | Pijl in cirkel (rechts) |
| `<book-open-icon>` | `book-open-icon.js` | Open boek icoon (placeholder) |
| `<clipboard-icon>` | `clipboard-icon.js` | Klembord icoon |
| `<chevron-down-icon>` | `chevron-down-icon.js` | Pijl naar beneden (accordion) |
| `<close-icon>` | `close-icon.js` | Sluiten/kruis icoon |
### Icon Functies
| Functie | Bestand | Beschrijving |
|---------|---------|--------------|
| `micIcon()` | `mic.js` | Microfoon icoon (spraakherkenning) |
| `searchIcon()` | `search.js` | Vergrootglas icoon |
| `sendIcon()` | `send-icon.js` | Verstuur/pijl icoon (nieuwsbrief) |
---
## 🎨 Design System
### Lettertypen
| Font | Gewichten | Gebruik |
|------|-----------|---------|
| **Outfit** | 400 (Regular), 700 (Bold) | Primair lettertype voor alle tekst, titels en UI elementen |
| System fonts | - | Fallback: system-ui, -apple-system, etc. |
Het Outfit lettertype wordt geladen via Google Fonts CDN.
### Kleuren
#### Primaire Kleuren
| Naam | Waarde | CSS Variable | Gebruik |
|------|--------|--------------|---------|
| **Purple (primair)** | `#951D51` | `--color-button-primary`, `--color-purple` | Knoppen, links, accenten |
| **Purple Dark** | `#7A1842` | `--color-purple-dark` | Hover states |
#### Tekst Kleuren
| Naam | Waarde | CSS Variable | Gebruik |
|------|--------|--------------|---------|
| **Text** | `#1E293B` | `--color-text` | Primaire tekst |
| **Text Light** | `#64748B` | `--color-text-light` | Secundaire tekst |
| **Text Inverse** | `#FFFFFF` | `--color-text-inverse` | Tekst op donkere achtergrond |
| **Black** | `#000000` | `--color-black` | Titels en koppen |
#### Achtergrond Kleuren
| Naam | Waarde | CSS Variable | Gebruik |
|------|--------|--------------|---------|
| **Background** | `#FFFFFF` | `--color-background` | Primaire achtergrond |
| **Background Secondary** | `#F8FAFC` | `--color-background-secondary` | Pagina achtergrond |
| **Background Tertiary** | `#F1F5F9` | `--color-background-tertiary` | Cards, secties |
| **Push Box BG** | `#EBEEF4` | `--color-push-box-bg` | Promotie blokken |
| **Card Dark BG** | `#EBEEF4` | `--color-card-dark-bg` | Donkere kaarten |
| **Category Card** | `#F5F4FC` | - | Categorie kaarten |
#### Overige Kleuren
| Naam | Waarde | CSS Variable | Gebruik |
|------|--------|--------------|---------|
| **Border** | `#E2E8F0` | `--color-border` | Borders en scheidingslijnen |
| **Accent** | `#F59E0B` | `--color-accent` | Sterren, favoriet icoon |
| **Success** | `#22C55E` | `--color-success` | Bevestigingen |
| **Error** | `#EF4444` | `--color-error` | Foutmeldingen |
### Spacing
| Naam | Waarde | CSS Variable |
|------|--------|--------------|
| **XS** | 4px (0.25rem) | `--spacing-xs` |
| **SM** | 8px (0.5rem) | `--spacing-sm` |
| **MD** | 16px (1rem) | `--spacing-md` |
| **LG** | 24px (1.5rem) | `--spacing-lg` |
| **XL** | 32px (2rem) | `--spacing-xl` |
| **2XL** | 48px (3rem) | `--spacing-2xl` |
### Typografie Schaal
| Naam | Waarde | CSS Variable |
|------|--------|--------------|
| **XS** | 12px (0.75rem) | `--font-size-xs` |
| **SM** | 14px (0.875rem) | `--font-size-sm` |
| **Base** | 16px (1rem) | `--font-size-base` |
| **LG** | 18px (1.125rem) | `--font-size-lg` |
| **XL** | 20px (1.25rem) | `--font-size-xl` |
| **2XL** | 24px (1.5rem) | `--font-size-2xl` |
| **3XL** | 30px (1.875rem) | `--font-size-3xl` |
### Border Radius
| Naam | Waarde | CSS Variable |
|------|--------|--------------|
| **SM** | 4px (0.25rem) | `--radius-sm` |
| **MD** | 8px (0.5rem) | `--radius-md` |
| **LG** | 12px (0.75rem) | `--radius-lg` |
| **XL** | 16px (1rem) | `--radius-xl` |
| **Full** | 9999px | `--radius-full` |
### Transities
| Naam | Waarde | CSS Variable |
|------|--------|--------------|
| **Fast** | 150ms ease | `--transition-fast` |
| **Normal** | 250ms ease | `--transition-normal` |
| **Slow** | 350ms ease | `--transition-slow` |
---
## ♿ Toegankelijkheid (WCAG)
Dit project is gebouwd met toegankelijkheid als prioriteit en volgt de WCAG 2.1 richtlijnen.
### Geïmplementeerde functies
#### Skip-to-content link
- Verborgen link die verschijnt bij keyboard focus
- Stelt gebruikers in staat direct naar de hoofdinhoud te navigeren
- Slaat herhalende navigatie-elementen over
- Geplaatst binnen de header landmark voor correcte WCAG compliance
#### Semantische HTML
- Correcte gebruik van `<header>`, `<nav>`, `<main>`, `<footer>`, `<section>`, `<article>`
- Logische heading hiërarchie (h1, h2, h3)
- `<main>` element met `id="main-content"` voor skip-link target
#### ARIA Labels en Rollen
- `aria-label` op alle icon buttons (menu, profiel, winkelwagen, zoeken, etc.)
- `aria-expanded` en `aria-controls` op accordion componenten
- `role="tablist"`, `role="tab"`, en `role="tabpanel"` voor tabbed interfaces
- `aria-selected` voor actieve tabs
- `aria-current` voor actieve navigatie-items
- `aria-pressed` voor toggle buttons
#### Keyboard Navigatie
- Alle interactieve elementen zijn bereikbaar met Tab
- Tab componenten ondersteunen pijltjestoetsen (←/→), Home, en End
- Escape sluit de mobile drawer
- Zichtbare focus indicators op alle focusable elementen
- Logische focus volgorde
#### Formulieren
- `aria-label` op input velden
- Associatie tussen labels en inputs
- Duidelijke foutmeldingen
#### Spraakherkenning
- Zoekfunctie met Nederlandse spraakherkenning (`lang="nl-NL"`)
- Visuele feedback tijdens luisteren (pulse animatie)
- Graceful degradation in browsers zonder ondersteuning
#### Taal
- `lang="nl"` attribuut op HTML element
- Correcte taalinstelling voor screenreaders
- Alle ARIA labels zijn in het Nederlands
### Focus Stijlen
Alle interactieve elementen hebben duidelijke focus indicators:
```css
/* Voorbeeld focus stijl */
element:focus {
outline: 2px solid var(--color-purple);
outline-offset: 2px;
}
```
---
## 📱 Responsive Design
> **Let op:** De huidige implementatie toont een gefixeerde mobiele container (430px) puur ter demonstratie voor deze pitch. Bij de daadwerkelijke ontwikkeling wordt de website volledig responsive gebouwd voor alle schermformaten.
### Pitch weergave
| Breakpoint | Gedrag |
|------------|--------|
| < 431px | Volledige breedte, mobiele ervaring |
| ≥ 431px | Gecentreerde container met afgeronde hoeken en schaduw (pitch preview) |
### Geplande responsive implementatie
Bij de definitieve ontwikkeling worden de volgende breakpoints toegevoegd:
| Breakpoint | Apparaat | Aanpassingen |
|------------|----------|--------------|
| < 640px | Mobiel | Huidige ontwerp, enkele kolom |
| 640px - 1024px | Tablet | Twee-koloms grid voor boeken, aangepaste navigatie |
| > 1024px | Desktop | Volledige sidebar navigatie, drie-koloms grid, uitgebreide header |
---
## 🔧 Technische details
### Browser ondersteuning
- Chrome 90+
- Firefox 90+
- Safari 14+
- Edge 90+
### Geen build stap nodig
Het project maakt gebruik van native ES modules en heeft geen build tooling nodig. Alle JavaScript wordt direct door de browser geladen.
### State management
Een eenvoudige winkelwagen store (`js/store/cart.js`) beheert de winkelwagen state met:
- `addItem()` — Voeg item toe
- `removeItem()` — Verwijder item
- `getItems()` — Haal alle items op
- `getTotal()` — Bereken totaal
- `getItemCount()` — Tel items
Events worden gedispatched voor reactieve updates.
---
## 📞 Contact
**Tim Rijkse**
Front-end Developer
---
## 📄 Licentie
Dit project is gemaakt als onderdeel van een pitch voor Milinda Uitgevers en mag uitsluitend worden gebruikt indien de opdracht wordt gegund.

100
book.html
View File

@@ -1,13 +1,13 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="nl"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta <meta
name="description" name="description"
content="Zen is opendoen - Dick Verstegen | Milinda Uitgevers" content="Milinda - Discover and buy your next favorite book"
/> />
<title>Zen is opendoen - Milinda Uitgevers</title> <title>Milinda - Home</title>
<!-- Fonts are loaded via @font-face in styles.css --> <!-- 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 />
@@ -18,27 +18,18 @@
<link rel="stylesheet" href="css/styles.css" /> <link rel="stylesheet" href="css/styles.css" />
</head> </head>
<body> <body>
<mobile-drawer></mobile-drawer>
<div class="mobile-container"> <div class="mobile-container">
<site-header> <site-header>
<a href="#main-content" class="skip-to-content" slot="skip-link"
>Ga naar inhoud</a
>
<top-bar slot="top-bar"> <top-bar slot="top-bar">
<button <button slot="menu-button" class="icon-button" aria-label="Menu">
slot="menu-button"
class="icon-button"
aria-label="Menu"
onclick="window.dispatchEvent(new CustomEvent('toggle-mobile-drawer'))"
>
<menu-icon></menu-icon> <menu-icon></menu-icon>
</button> </button>
<a slot="logo" href="index.html" class="logo">Milinda</a> <a slot="logo" href="index.html" class="logo">Milinda</a>
<div slot="actions" class="actions"> <div slot="actions" class="actions">
<button class="icon-button" aria-label="Profiel"> <button class="icon-button" aria-label="Profile">
<user-icon></user-icon> <user-icon></user-icon>
</button> </button>
<button class="icon-button" aria-label="Winkelwagen"> <button class="icon-button" aria-label="Shopping basket">
<shopping-bag-icon></shopping-bag-icon> <shopping-bag-icon></shopping-bag-icon>
</button> </button>
</div> </div>
@@ -47,7 +38,6 @@
<search-bar slot="search"></search-bar> <search-bar slot="search"></search-bar>
</site-header> </site-header>
<main id="main-content">
<site-content> <site-content>
<section class="section"> <section class="section">
<book-details <book-details
@@ -70,84 +60,13 @@
Schrijf een recensie Schrijf een recensie
</icon-link-button> </icon-link-button>
</action-links-list> </action-links-list>
<content-tabs tabs="Beschrijving,Inzage,Recensies">
<book-description slot="panel-0">
<p>
Zen is opendoen bevat een selectie van zeventig columns die
Dick Verstegen schreef tussen 2002 en 2017. Hij schreef ze als
columnist voor o.a. het boeddhistisch kwartaalblad Vorm &
Leegte, het Boeddhistisch Dagblad, Centrum Waerbeke, het Han
Fortmann Centrum en de Wijkkrant van Nijmegen-Oost. Zijn
columns wijzen zonder uitzondering naar het mysterie van het
leven en geven blijk van bewogenheid en overgave, maar de
lichte toets ontbreekt nooit. Ze gaan over zeer uiteenlopende
onderwerpen, zoals: lente, de ware stem van je hart,
compassie, ontroering, woorden, de dood van zijn vrouw, de
kathedraal van Royan, het windorgel in Vlissingen, liefde,
boeddhaschap, bedelen, emoties, relaties, management,
overgave, gedachten, Nepal, leerling zijn, theekommen, zijn
vader, stilte, licht en opendoen.
</p>
<p class="quote">
Uit zijn columns blijkt hoezeer hij zich ervan bewust is dat
zenboeddhisme geen mening is, maar een non-duale zienswijze.
Non-dualiteit is zien dat de dualiteiten geen hindernissen
zijn voor een bevrijd bestaan. (...) zo te spreken of te
schrijven dat het mysteriekarakter onaangetast blijft. Wat mij
betreft is Dick hierin volkomen geslaagd. Hoe hem dit gelukt
is, is mij een raadsel. Misschien wel dankzij zijn grote
liefde voor de taal die spreekt uit elke bladzijde van deze
verzameling literaire miniaturen.
</p>
<p class="author">—Nico Tydeman</p>
</book-description>
<image-gallery
slot="panel-1"
images="images/book-insight.jpg,images/book-insight.jpg,images/book-insight.jpg"
></image-gallery>
<book-reviews slot="panel-2">
<book-review-item
rating="5"
author="Maria van der Berg"
date="12 januari 2026"
>
Een prachtige verzameling columns die je aan het denken zet.
Dick Verstegen schrijft met zoveel warmte en wijsheid. Elk
stukje is een kleine meditatie op zich. Aanrader voor iedereen
die geïnteresseerd is in zen en het dagelijks leven.
</book-review-item>
<book-review-item
rating="5"
author="Jan Pietersen"
date="8 december 2025"
>
Dit boek heeft mijn kijk op zen volledig veranderd. De columns
zijn toegankelijk geschreven en toch diepgaand. Ik lees elke
avond een column voor het slapen gaan. Een boek om te
koesteren.
</book-review-item>
<book-review-item
rating="4"
author="Sophie de Vries"
date="23 november 2025"
>
Mooie, poëtische teksten die je uitnodigen om stil te staan
bij het leven. Soms wat abstract, maar overall een waardevolle
toevoeging aan mijn boekenplank. De quote van Nico Tydeman op
de achterkant vat het perfect samen.
</book-review-item>
</book-reviews>
</content-tabs>
</section> </section>
<div class="content-padding"> <div class="content-padding">
<push-box variant="purple"> <push-box>
<h2 slot="title"> <h2 slot="title">
Kom je er niet uit of heb je een vraag? Neem contact op met de Gespecialiseerd op het vlak van boeddhisme en aanverwante
klantenservice. Oost-West thema's
</h2> </h2>
<div slot="cta" class="cta-buttons"> <div slot="cta" class="cta-buttons">
<arrow-button href="#">Klantenservice</arrow-button> <arrow-button href="#">Klantenservice</arrow-button>
@@ -165,7 +84,6 @@
></newsletter-signup> ></newsletter-signup>
</div> </div>
</site-content> </site-content>
</main>
<site-footer> <site-footer>
<span slot="logo">MILINDA uitgevers</span> <span slot="logo">MILINDA uitgevers</span>

View File

@@ -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
@@ -88,6 +110,22 @@ textarea:not([rows]) {
scroll-margin-block: 5ex; scroll-margin-block: 5ex;
} }
/* Remove all animations, transitions and smooth scroll for people that prefer not to see them */
@media (prefers-reduced-motion: reduce) {
html:focus-within {
scroll-behavior: auto;
}
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* Remove the inner border and padding in Firefox */ /* Remove the inner border and padding in Firefox */
::-moz-focus-inner { ::-moz-focus-inner {
border-style: none; border-style: none;
@@ -174,10 +212,12 @@ table {
--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 */
@@ -327,30 +367,6 @@ site-content {
border: 0; border: 0;
} }
/* Skip to Content Link */
.skip-to-content {
position: absolute;
top: -100%;
left: 50%;
transform: translateX(-50%);
z-index: 9999;
padding: var(--spacing-sm) var(--spacing-md);
background-color: var(--color-button-primary);
color: var(--color-text-inverse);
font-family: var(--font-family-base);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-semibold);
text-decoration: none;
border-radius: var(--radius-sm);
transition: top var(--transition-fast);
}
.skip-to-content:focus {
top: var(--spacing-sm);
outline: 2px solid var(--color-text-inverse);
outline-offset: 2px;
}
.truncate { .truncate {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 550 KiB

View File

@@ -1,11 +1,11 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="nl"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta <meta
name="description" name="description"
content="Milinda Uitgevers - Boeken over boeddhisme, meditatie en mindfulness" 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 --> <!-- Fonts are loaded via @font-face in styles.css -->
@@ -18,27 +18,18 @@
<link rel="stylesheet" href="css/styles.css" /> <link rel="stylesheet" href="css/styles.css" />
</head> </head>
<body> <body>
<mobile-drawer></mobile-drawer>
<div class="mobile-container"> <div class="mobile-container">
<site-header> <site-header>
<a href="#main-content" class="skip-to-content" slot="skip-link"
>Ga naar inhoud</a
>
<top-bar slot="top-bar"> <top-bar slot="top-bar">
<button <button slot="menu-button" class="icon-button" aria-label="Menu">
slot="menu-button"
class="icon-button"
aria-label="Menu"
onclick="window.dispatchEvent(new CustomEvent('toggle-mobile-drawer'))"
>
<menu-icon></menu-icon> <menu-icon></menu-icon>
</button> </button>
<a slot="logo" href="index.html" class="logo">Milinda</a> <a slot="logo" href="index.html" class="logo">Milinda</a>
<div slot="actions" class="actions"> <div slot="actions" class="actions">
<button class="icon-button" aria-label="Profiel"> <button class="icon-button" aria-label="Profile">
<user-icon></user-icon> <user-icon></user-icon>
</button> </button>
<button class="icon-button" aria-label="Winkelwagen"> <button class="icon-button" aria-label="Shopping basket">
<shopping-bag-icon></shopping-bag-icon> <shopping-bag-icon></shopping-bag-icon>
</button> </button>
</div> </div>
@@ -47,7 +38,6 @@
<search-bar slot="search"></search-bar> <search-bar slot="search"></search-bar>
</site-header> </site-header>
<main id="main-content">
<site-content> <site-content>
<div class="content-padding"> <div class="content-padding">
<push-box> <push-box>
@@ -99,9 +89,7 @@
href="book.html" href="book.html"
theme="dark" theme="dark"
></book-card> ></book-card>
<cta-button href="#" <cta-button href="#">Toon meer recent verschenen boeken</cta-button>
>Toon meer recent verschenen boeken</cta-button
>
</div> </div>
</section> </section>
@@ -223,7 +211,6 @@
></newsletter-signup> ></newsletter-signup>
</div> </div>
</site-content> </site-content>
</main>
<site-footer> <site-footer>
<span slot="logo">MILINDA uitgevers</span> <span slot="logo">MILINDA uitgevers</span>

View File

@@ -26,12 +26,6 @@ import "./components/book-details.js";
import "./components/icon-cta-button.js"; import "./components/icon-cta-button.js";
import "./components/icon-link-button.js"; import "./components/icon-link-button.js";
import "./components/action-links-list.js"; import "./components/action-links-list.js";
import "./components/content-tabs.js";
import "./components/image-gallery.js";
import "./components/book-description.js";
import "./components/book-reviews.js";
import "./components/book-review-item.js";
import "./components/mobile-drawer.js";
// Import icon components // Import icon components
import "./icons/menu-icon.js"; import "./icons/menu-icon.js";
@@ -41,7 +35,6 @@ import "./icons/arrow-circle-right-icon.js";
import "./icons/book-open-icon.js"; import "./icons/book-open-icon.js";
import "./icons/clipboard-icon.js"; import "./icons/clipboard-icon.js";
import "./icons/chevron-down-icon.js"; import "./icons/chevron-down-icon.js";
import "./icons/close-icon.js";
// App initialization // App initialization
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {

View File

@@ -66,7 +66,7 @@ class AddToCartButton extends HTMLElement {
color: var(--color-text-inverse, #ffffff); color: var(--color-text-inverse, #ffffff);
} }
</style> </style>
<button class="add-to-cart-button" type="button" aria-label="Voeg toe aan winkelwagen"> <button class="add-to-cart-button" type="button">
${plusIcon({ size: 16, color: "#ffffff" })} ${plusIcon({ size: 16, color: "#ffffff" })}
${shoppingBagIcon({ size: 16, color: "#ffffff" })} ${shoppingBagIcon({ size: 16, color: "#ffffff" })}
</button> </button>

View File

@@ -1,53 +0,0 @@
/**
* Book Description Component
* Displays formatted book description text
*/
class BookDescription extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
}
connectedCallback() {
this.render();
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
}
.description {
font-family: var(--font-family-outfit, "Outfit", sans-serif);
font-size: 16px;
font-weight: 400;
line-height: 28px;
color: var(--color-text, #1e293b);
}
::slotted(p) {
margin: 0 0 var(--spacing-md, 1rem) 0;
}
::slotted(p:last-child) {
margin-bottom: 0;
}
::slotted(.quote) {
font-style: italic;
}
::slotted(.author) {
font-weight: 400;
}
</style>
<div class="description">
<slot></slot>
</div>
`;
}
}
customElements.define("book-description", BookDescription);

View File

@@ -331,7 +331,7 @@ class BookDetails extends HTMLElement {
<div class="header"> <div class="header">
<div class="title-row"> <div class="title-row">
<h1 class="title">${this.bookTitle}</h1> <h1 class="title">${this.bookTitle}</h1>
<button class="favorite-btn" aria-label="Toevoegen aan favorieten"> <button class="favorite-btn" aria-label="Add to favorites">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/> <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
</svg> </svg>

View File

@@ -1,128 +0,0 @@
/**
* Book Review Item Component
* Individual review with rating, author, date, and text
*/
class BookReviewItem extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
}
connectedCallback() {
this.render();
}
get rating() {
return parseInt(this.getAttribute("rating") || "5", 10);
}
get author() {
return this.getAttribute("author") || "Anoniem";
}
get date() {
return this.getAttribute("date") || "";
}
renderStars() {
const rating = Math.min(5, Math.max(0, this.rating));
const fullStars = rating;
const emptyStars = 5 - rating;
let stars = "";
for (let i = 0; i < fullStars; i++) {
stars += `<span class="star filled">★</span>`;
}
for (let i = 0; i < emptyStars; i++) {
stars += `<span class="star empty">☆</span>`;
}
return stars;
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
}
.review {
padding-bottom: var(--spacing-lg, 1.5rem);
border-bottom: 1px solid var(--color-border, #e2e8f0);
}
:host(:last-child) .review {
border-bottom: none;
padding-bottom: 0;
}
.review-header {
display: flex;
flex-direction: column;
gap: var(--spacing-xs, 0.25rem);
margin-bottom: var(--spacing-sm, 0.5rem);
}
.stars {
display: flex;
gap: 2px;
}
.star {
font-size: 16px;
line-height: 1;
}
.star.filled {
color: var(--color-purple, #951d51);
}
.star.empty {
color: var(--color-border, #e2e8f0);
}
.review-meta {
display: flex;
align-items: center;
gap: var(--spacing-sm, 0.5rem);
font-family: var(--font-family-outfit, "Outfit", sans-serif);
font-size: 14px;
line-height: 20px;
}
.author {
font-weight: 500;
color: var(--color-text, #1e293b);
}
.date {
color: var(--color-text-light, #64748b);
font-weight: 300;
}
.review-text {
font-family: var(--font-family-outfit, "Outfit", sans-serif);
font-size: 16px;
font-weight: 400;
line-height: 26px;
color: var(--color-text, #1e293b);
margin: 0;
}
</style>
<article class="review">
<div class="review-header">
<div class="stars">${this.renderStars()}</div>
<div class="review-meta">
<span class="author">${this.author}</span>
${this.date ? `<span class="date">• ${this.date}</span>` : ""}
</div>
</div>
<p class="review-text">
<slot></slot>
</p>
</article>
`;
}
}
customElements.define("book-review-item", BookReviewItem);

View File

@@ -1,47 +0,0 @@
/**
* Book Reviews Component
* Displays customer reviews for a book
*/
class BookReviews extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
}
connectedCallback() {
this.render();
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
}
.reviews-container {
display: flex;
flex-direction: column;
gap: var(--spacing-lg, 1.5rem);
}
.no-reviews {
font-family: var(--font-family-outfit, "Outfit", sans-serif);
font-size: 16px;
font-weight: 400;
line-height: 24px;
color: var(--color-text-light, #64748b);
text-align: center;
padding: var(--spacing-xl, 2rem);
}
</style>
<div class="reviews-container">
<slot>
<p class="no-reviews">Nog geen recensies. Wees de eerste om een recensie te schrijven!</p>
</slot>
</div>
`;
}
}
customElements.define("book-reviews", BookReviews);

View File

@@ -1,194 +0,0 @@
/**
* Content Tabs Component
* Tabbed interface for displaying different content sections
*/
class ContentTabs extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.activeTab = 0;
}
connectedCallback() {
this.render();
this.setupEventListeners();
}
get tabs() {
const tabsAttr = this.getAttribute("tabs");
return tabsAttr ? tabsAttr.split(",").map((t) => t.trim()) : [];
}
setupEventListeners() {
const tabButtons = this.shadowRoot.querySelectorAll(".tab-button");
tabButtons.forEach((button, index) => {
button.addEventListener("click", () => {
this.setActiveTab(index);
});
// Keyboard navigation
button.addEventListener("keydown", (e) => {
const tabCount = this.tabs.length;
let newIndex = this.activeTab;
switch (e.key) {
case "ArrowLeft":
newIndex = (this.activeTab - 1 + tabCount) % tabCount;
break;
case "ArrowRight":
newIndex = (this.activeTab + 1) % tabCount;
break;
case "Home":
newIndex = 0;
break;
case "End":
newIndex = tabCount - 1;
break;
default:
return;
}
e.preventDefault();
this.setActiveTab(newIndex);
this.shadowRoot.querySelectorAll(".tab-button")[newIndex]?.focus();
});
});
}
setActiveTab(index) {
this.activeTab = index;
this.updateTabs();
this.dispatchEvent(
new CustomEvent("tab-change", {
bubbles: true,
composed: true,
detail: { index, tab: this.tabs[index] },
})
);
}
updateTabs() {
// Update tab buttons
const tabButtons = this.shadowRoot.querySelectorAll(".tab-button");
tabButtons.forEach((button, index) => {
const isActive = index === this.activeTab;
button.classList.toggle("active", isActive);
button.setAttribute("aria-selected", isActive.toString());
button.setAttribute("tabindex", isActive ? "0" : "-1");
});
// Update panels in shadow DOM
const shadowPanels = this.shadowRoot.querySelectorAll("[role='tabpanel']");
shadowPanels.forEach((panel, index) => {
panel.style.display = index === this.activeTab ? "block" : "none";
});
// Update slotted panels
const panels = this.querySelectorAll("[slot^='panel-']");
panels.forEach((panel, index) => {
panel.style.display = index === this.activeTab ? "block" : "none";
});
}
render() {
const tabsHtml = this.tabs
.map(
(tab, index) => `
<button
class="tab-button ${index === this.activeTab ? "active" : ""}"
type="button"
role="tab"
id="tab-${index}"
aria-selected="${index === this.activeTab}"
aria-controls="panel-${index}"
tabindex="${index === this.activeTab ? "0" : "-1"}"
>
${tab}
</button>
`
)
.join("");
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
margin: var(--spacing-xl, 2rem) 0;
}
.tabs-container {
display: flex;
flex-direction: column;
}
.tab-list {
display: flex;
gap: var(--spacing-md, 1rem);
margin-top: var(--spacing-lg, 1.5rem);
border-bottom: 1px solid var(--color-border, #e2e8f0);
}
.tab-button {
padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
background: transparent;
border: none;
border-radius: 4px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
font-family: var(--font-family-outfit, "Outfit", sans-serif);
font-size: 16px;
font-weight: 400;
line-height: 24px;
color: var(--color-purple, #951d51);
text-decoration: underline;
text-underline-offset: 3px;
cursor: pointer;
transition: all var(--transition-fast, 150ms ease);
}
.tab-button.active {
background: var(--color-border, #e2e8f0);
color: var(--color-text, #1e293b);
text-decoration: none;
}
.tab-button:not(.active):hover {
opacity: 0.7;
}
.tab-button:focus {
outline: 2px solid var(--color-purple, #951d51);
outline-offset: 2px;
}
.tab-panels {
min-height: 100px;
background: var(--color-border, #e2e8f0);
padding: var(--spacing-lg, 1.5rem) var(--spacing-md, 1rem);
border-radius: var(--radius-sm, 0.25rem);
}
</style>
<div class="tabs-container">
<div class="tab-list" role="tablist">
${tabsHtml}
</div>
<div class="tab-panels">
<div id="panel-0" role="tabpanel" aria-labelledby="tab-0">
<slot name="panel-0"></slot>
</div>
<div id="panel-1" role="tabpanel" aria-labelledby="tab-1">
<slot name="panel-1"></slot>
</div>
<div id="panel-2" role="tabpanel" aria-labelledby="tab-2">
<slot name="panel-2"></slot>
</div>
</div>
</div>
`;
// Initialize panel visibility
setTimeout(() => this.updateTabs(), 0);
}
}
customElements.define("content-tabs", ContentTabs);

View File

@@ -3,13 +3,10 @@
* Expandable accordion item for footer navigation * Expandable accordion item for footer navigation
*/ */
class FooterAccordionItem extends HTMLElement { class FooterAccordionItem extends HTMLElement {
static _idCounter = 0;
constructor() { constructor() {
super(); super();
this.attachShadow({ mode: "open" }); this.attachShadow({ mode: "open" });
this._expanded = false; this._expanded = false;
this._uniqueId = `accordion-${FooterAccordionItem._idCounter++}`;
} }
static get observedAttributes() { static get observedAttributes() {
@@ -37,7 +34,6 @@ class FooterAccordionItem extends HTMLElement {
this._expanded = !this._expanded; this._expanded = !this._expanded;
const content = this.shadowRoot.querySelector(".accordion-content"); const content = this.shadowRoot.querySelector(".accordion-content");
const icon = this.shadowRoot.querySelector(".accordion-icon"); const icon = this.shadowRoot.querySelector(".accordion-icon");
const header = this.shadowRoot.querySelector(".accordion-header");
if (content) { if (content) {
content.classList.toggle("expanded", this._expanded); content.classList.toggle("expanded", this._expanded);
@@ -45,9 +41,6 @@ class FooterAccordionItem extends HTMLElement {
if (icon) { if (icon) {
icon.classList.toggle("rotated", this._expanded); icon.classList.toggle("rotated", this._expanded);
} }
if (header) {
header.setAttribute("aria-expanded", this._expanded.toString());
}
} }
render() { render() {
@@ -64,10 +57,7 @@ class FooterAccordionItem extends HTMLElement {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
width: 100%;
padding: var(--spacing-lg, 1.5rem) 0; padding: var(--spacing-lg, 1.5rem) 0;
background: none;
border: none;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
} }
@@ -76,11 +66,6 @@ class FooterAccordionItem extends HTMLElement {
opacity: 0.9; opacity: 0.9;
} }
.accordion-header:focus {
outline: 2px solid var(--color-text-inverse, #ffffff);
outline-offset: 2px;
}
.accordion-title { .accordion-title {
font-size: var(--font-size-base, 1rem); font-size: var(--font-size-base, 1rem);
font-weight: 400; font-weight: 400;
@@ -113,21 +98,11 @@ class FooterAccordionItem extends HTMLElement {
color: var(--color-text-inverse, #ffffff); color: var(--color-text-inverse, #ffffff);
} }
</style> </style>
<button <div class="accordion-header">
class="accordion-header"
type="button"
aria-expanded="${this._expanded}"
aria-controls="accordion-content-${this._uniqueId}"
>
<h3 class="accordion-title">${title}</h3> <h3 class="accordion-title">${title}</h3>
<chevron-down-icon class="accordion-icon" size="24"></chevron-down-icon> <chevron-down-icon class="accordion-icon" size="24"></chevron-down-icon>
</button> </div>
<div <div class="accordion-content">
id="accordion-content-${this._uniqueId}"
class="accordion-content"
role="region"
aria-labelledby="accordion-header-${this._uniqueId}"
>
<slot></slot> <slot></slot>
</div> </div>
`; `;

View File

@@ -7,11 +7,14 @@ class HorizontalScrollNav extends HTMLElement {
super(); super();
this.attachShadow({ mode: "open" }); this.attachShadow({ mode: "open" });
this.categories = [ this.categories = [
{ id: "asoka", label: "Asoka", active: true }, { id: "all", label: "All", active: true },
{ id: "synthese", label: "Synthese", active: false }, { id: "fiction", label: "Fiction", active: false },
{ id: "de-driehoek", label: "De Driehoek", active: false }, { id: "non-fiction", label: "Non-Fiction", active: false },
{ id: "waerbeke", label: "Waerbeke", active: false }, { id: "mystery", label: "Mystery", active: false },
{ id: "stuivenberg", label: "Stuivenberg", active: false }, { id: "romance", label: "Romance", active: false },
{ id: "sci-fi", label: "Sci-Fi", active: false },
{ id: "biography", label: "Biography", active: false },
{ id: "history", label: "History", active: false },
]; ];
} }
@@ -107,7 +110,7 @@ class HorizontalScrollNav extends HTMLElement {
background-color: #7a1843; background-color: #7a1843;
} }
</style> </style>
<nav class="nav-container" role="navigation" aria-label="Imprints"> <nav class="nav-container" role="navigation" aria-label="Book categories">
${this.categories ${this.categories
.map( .map(
(cat) => ` (cat) => `

View File

@@ -1,534 +0,0 @@
/**
* Image Gallery Component
* Grid of images that open in a fullscreen modal on click
*/
class ImageGallery extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
// Zoom and pan state
this.currentZoom = 1;
this.translateX = 0;
this.translateY = 0;
this.isDragging = false;
this.startX = 0;
this.startY = 0;
// Pinch zoom state
this.initialPinchDistance = 0;
this.initialZoom = 1;
this.pinchCenterX = 0;
this.pinchCenterY = 0;
this.initialTranslateX = 0;
this.initialTranslateY = 0;
}
connectedCallback() {
this.render();
this.setupEventListeners();
}
get images() {
const imagesAttr = this.getAttribute("images");
return imagesAttr ? imagesAttr.split(",").map((img) => img.trim()) : [];
}
setupEventListeners() {
const imageItems = this.shadowRoot.querySelectorAll(".gallery-image");
imageItems.forEach((img, index) => {
img.addEventListener("click", () => {
this.openModal(index);
});
});
// Modal controls
const closeBtn = this.shadowRoot.querySelector(".modal-close");
const modal = this.shadowRoot.querySelector(".modal-overlay");
if (closeBtn) {
closeBtn.addEventListener("click", () => this.closeModal());
}
if (modal) {
modal.addEventListener("click", (e) => {
if (e.target === modal) {
this.closeModal();
}
});
}
// Keyboard support
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
this.closeModal();
}
});
// Zoom controls
const zoomIn = this.shadowRoot.querySelector(".zoom-in");
const zoomOut = this.shadowRoot.querySelector(".zoom-out");
const resetZoom = this.shadowRoot.querySelector(".zoom-reset");
if (zoomIn) {
zoomIn.addEventListener("click", () => this.zoom(0.25));
}
if (zoomOut) {
zoomOut.addEventListener("click", () => this.zoom(-0.25));
}
if (resetZoom) {
resetZoom.addEventListener("click", () => this.resetZoom());
}
// Drag/pan functionality
const modalImage = this.shadowRoot.querySelector(".modal-image");
if (modalImage) {
// Mouse events
modalImage.addEventListener("mousedown", (e) => this.startDrag(e));
modalImage.addEventListener("mousemove", (e) => this.drag(e));
modalImage.addEventListener("mouseup", () => this.endDrag());
modalImage.addEventListener("mouseleave", () => this.endDrag());
// Touch events for mobile
modalImage.addEventListener("touchstart", (e) => this.startDrag(e), { passive: false });
modalImage.addEventListener("touchmove", (e) => this.drag(e), { passive: false });
modalImage.addEventListener("touchend", () => this.endDrag());
// Prevent default drag behavior
modalImage.addEventListener("dragstart", (e) => e.preventDefault());
// Mouse wheel zoom
modalImage.addEventListener("wheel", (e) => this.handleWheel(e), { passive: false });
}
// Pinch zoom for touch devices on modal content
const modalContent = this.shadowRoot.querySelector(".modal-content");
if (modalContent) {
modalContent.addEventListener("touchstart", (e) => this.handleTouchStart(e), { passive: false });
modalContent.addEventListener("touchmove", (e) => this.handleTouchMove(e), { passive: false });
modalContent.addEventListener("touchend", (e) => this.handleTouchEnd(e));
// Prevent default browser gestures on the modal
modalContent.addEventListener("gesturestart", (e) => e.preventDefault());
modalContent.addEventListener("gesturechange", (e) => e.preventDefault());
modalContent.addEventListener("gestureend", (e) => e.preventDefault());
}
}
handleWheel(e) {
e.preventDefault();
e.stopPropagation();
const delta = e.deltaY > 0 ? -0.15 : 0.15;
// Get mouse position relative to modal content center
const modalContent = this.shadowRoot.querySelector(".modal-content");
const rect = modalContent.getBoundingClientRect();
const mouseX = e.clientX - rect.left - rect.width / 2;
const mouseY = e.clientY - rect.top - rect.height / 2;
this.zoomToPoint(delta, mouseX, mouseY);
}
zoomToPoint(delta, pointX, pointY) {
const modalImage = this.shadowRoot.querySelector(".modal-image");
if (!modalImage) return;
const prevZoom = this.currentZoom;
const newZoom = Math.max(1, Math.min(4, this.currentZoom + delta));
if (newZoom === prevZoom) return;
// Calculate new translate to zoom towards point
if (newZoom === 1) {
this.translateX = 0;
this.translateY = 0;
} else {
const zoomRatio = newZoom / prevZoom;
this.translateX = pointX - (pointX - this.translateX) * zoomRatio;
this.translateY = pointY - (pointY - this.translateY) * zoomRatio;
}
this.currentZoom = newZoom;
this.updateImageTransform(false);
modalImage.style.cursor = this.currentZoom > 1 ? "grab" : "default";
}
handleTouchStart(e) {
if (e.touches.length === 2) {
// Pinch zoom start
e.preventDefault();
this.isDragging = false; // Stop any ongoing drag
this.initialPinchDistance = this.getPinchDistance(e.touches);
this.initialZoom = this.currentZoom;
this.initialTranslateX = this.translateX;
this.initialTranslateY = this.translateY;
// Get pinch center relative to viewport
const modalContent = this.shadowRoot.querySelector(".modal-content");
const rect = modalContent.getBoundingClientRect();
this.pinchCenterX = ((e.touches[0].clientX + e.touches[1].clientX) / 2) - rect.left - rect.width / 2;
this.pinchCenterY = ((e.touches[0].clientY + e.touches[1].clientY) / 2) - rect.top - rect.height / 2;
} else if (e.touches.length === 1 && this.currentZoom > 1) {
// Single touch drag when zoomed
this.startDrag(e);
}
}
handleTouchMove(e) {
if (e.touches.length === 2 && this.initialPinchDistance > 0) {
// Pinch zoom with zoom-to-point
e.preventDefault();
const currentDistance = this.getPinchDistance(e.touches);
const scale = currentDistance / this.initialPinchDistance;
const newZoom = Math.max(1, Math.min(4, this.initialZoom * scale));
const prevZoom = this.currentZoom;
this.currentZoom = newZoom;
if (this.currentZoom === 1) {
this.translateX = 0;
this.translateY = 0;
} else {
// Adjust translate to zoom towards pinch center
const zoomRatio = this.currentZoom / this.initialZoom;
this.translateX = this.pinchCenterX - (this.pinchCenterX - this.initialTranslateX) * zoomRatio;
this.translateY = this.pinchCenterY - (this.pinchCenterY - this.initialTranslateY) * zoomRatio;
}
this.updateImageTransform(false); // No transition during pinch
const modalImage = this.shadowRoot.querySelector(".modal-image");
if (modalImage) {
modalImage.style.cursor = this.currentZoom > 1 ? "grab" : "default";
}
} else if (e.touches.length === 1 && this.isDragging) {
// Single touch drag
this.drag(e);
}
}
handleTouchEnd(e) {
this.initialPinchDistance = 0;
this.endDrag();
}
getPinchDistance(touches) {
const dx = touches[0].clientX - touches[1].clientX;
const dy = touches[0].clientY - touches[1].clientY;
return Math.sqrt(dx * dx + dy * dy);
}
startDrag(e) {
// Only enable drag when zoomed in
if (this.currentZoom <= 1) return;
this.isDragging = true;
if (e.type === "touchstart") {
this.startX = e.touches[0].clientX - this.translateX;
this.startY = e.touches[0].clientY - this.translateY;
} else {
this.startX = e.clientX - this.translateX;
this.startY = e.clientY - this.translateY;
}
const modalImage = this.shadowRoot.querySelector(".modal-image");
if (modalImage) {
modalImage.style.cursor = "grabbing";
modalImage.style.transition = "none";
}
}
drag(e) {
if (!this.isDragging) return;
e.preventDefault();
let clientX, clientY;
if (e.type === "touchmove") {
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
} else {
clientX = e.clientX;
clientY = e.clientY;
}
this.translateX = clientX - this.startX;
this.translateY = clientY - this.startY;
// Use no transition during drag for smooth movement
const modalImage = this.shadowRoot.querySelector(".modal-image");
if (modalImage) {
modalImage.style.transition = "none";
modalImage.style.transform = `scale(${this.currentZoom}) translate(${this.translateX / this.currentZoom}px, ${this.translateY / this.currentZoom}px)`;
}
}
endDrag() {
if (!this.isDragging) return;
this.isDragging = false;
const modalImage = this.shadowRoot.querySelector(".modal-image");
if (modalImage) {
modalImage.style.cursor = this.currentZoom > 1 ? "grab" : "default";
modalImage.style.transition = "transform 0.2s ease";
}
}
updateImageTransform(useTransition = true) {
const modalImage = this.shadowRoot.querySelector(".modal-image");
if (modalImage) {
if (!useTransition) {
modalImage.style.transition = "none";
}
modalImage.style.transform = `scale(${this.currentZoom}) translate(${this.translateX / this.currentZoom}px, ${this.translateY / this.currentZoom}px)`;
// Force reflow and restore transition
if (!useTransition) {
modalImage.offsetHeight; // Force reflow
modalImage.style.transition = "transform 0.2s ease";
}
}
}
openModal(index) {
const modal = this.shadowRoot.querySelector(".modal-overlay");
const modalImage = this.shadowRoot.querySelector(".modal-image");
if (modal && modalImage && this.images[index]) {
modalImage.src = this.images[index];
// Reset all state
this.currentZoom = 1;
this.translateX = 0;
this.translateY = 0;
this.isDragging = false;
this.initialPinchDistance = 0;
this.initialZoom = 1;
this.initialTranslateX = 0;
this.initialTranslateY = 0;
// Reset transform without transition
modalImage.style.transition = "none";
modalImage.style.transform = "scale(1) translate(0px, 0px)";
modalImage.style.cursor = "default";
// Force reflow then restore transition
modalImage.offsetHeight;
modalImage.style.transition = "transform 0.2s ease";
modal.classList.add("open");
document.body.style.overflow = "hidden";
}
}
closeModal() {
const modal = this.shadowRoot.querySelector(".modal-overlay");
if (modal) {
modal.classList.remove("open");
document.body.style.overflow = "";
}
}
zoom(delta) {
const modalImage = this.shadowRoot.querySelector(".modal-image");
if (modalImage) {
const prevZoom = this.currentZoom;
this.currentZoom = Math.max(1, Math.min(4, this.currentZoom + delta));
// If zooming out to 1x, reset position
if (this.currentZoom === 1) {
this.translateX = 0;
this.translateY = 0;
}
this.updateImageTransform();
modalImage.style.cursor = this.currentZoom > 1 ? "grab" : "default";
}
}
resetZoom() {
const modalImage = this.shadowRoot.querySelector(".modal-image");
if (modalImage) {
// Reset all state
this.currentZoom = 1;
this.translateX = 0;
this.translateY = 0;
this.isDragging = false;
this.initialPinchDistance = 0;
this.initialZoom = 1;
this.initialTranslateX = 0;
this.initialTranslateY = 0;
// Ensure transition is enabled for smooth reset animation
modalImage.style.transition = "transform 0.3s ease";
modalImage.style.transform = "scale(1) translate(0px, 0px)";
modalImage.style.cursor = "default";
}
}
render() {
const imagesHtml = this.images
.map(
(img, index) => `
<button class="gallery-item" type="button" aria-label="Bekijk afbeelding ${index + 1}">
<img src="${img}" alt="Boek voorvertoning ${index + 1}" class="gallery-image" />
</button>
`
)
.join("");
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--spacing-sm, 0.5rem);
}
.gallery-item {
aspect-ratio: 3/4;
overflow: hidden;
border-radius: var(--radius-sm, 0.25rem);
background: none;
border: none;
padding: 0;
cursor: pointer;
transition: opacity var(--transition-fast, 150ms ease);
}
.gallery-item:hover {
opacity: 0.8;
}
.gallery-image {
width: 100%;
height: 100%;
object-fit: cover;
}
/* Modal Styles */
.modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.95);
z-index: 1000;
flex-direction: column;
touch-action: none;
-webkit-touch-callout: none;
}
.modal-overlay.open {
display: flex;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-md, 1rem);
background-color: rgba(0, 0, 0, 0.5);
}
.modal-close {
width: 40px;
height: 40px;
background: none;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
margin-left: auto;
}
.modal-close svg {
width: 32px;
height: 32px;
color: #ffffff;
}
.zoom-controls {
display: flex;
gap: var(--spacing-sm, 0.5rem);
}
.zoom-btn {
width: 40px;
height: 40px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: var(--radius-sm, 0.25rem);
color: #ffffff;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
transition: background var(--transition-fast, 150ms ease);
}
.zoom-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
.modal-content {
flex: 1;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
padding: var(--spacing-md, 1rem);
touch-action: none;
-webkit-touch-callout: none;
}
.modal-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
transition: transform 0.2s ease;
cursor: default;
user-select: none;
-webkit-user-drag: none;
touch-action: none;
}
</style>
<div class="gallery-grid">
${imagesHtml}
</div>
<div class="modal-overlay">
<div class="modal-header">
<div class="zoom-controls">
<button class="zoom-btn zoom-out" type="button" aria-label="Uitzoomen"></button>
<button class="zoom-btn zoom-reset" type="button" aria-label="Zoom resetten">⟲</button>
<button class="zoom-btn zoom-in" type="button" aria-label="Inzoomen">+</button>
</div>
<button class="modal-close" type="button" aria-label="Sluiten">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="modal-content">
<img class="modal-image" src="" alt="Volledige afbeelding" />
</div>
</div>
`;
}
}
customElements.define("image-gallery", ImageGallery);

View File

@@ -1,384 +0,0 @@
/**
* Mobile Drawer Component
* Slide-in navigation drawer from the left with backdrop
*/
class MobileDrawer extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.isOpen = false;
this.handleBackdropClick = this.handleBackdropClick.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
}
connectedCallback() {
this.render();
this.setupEventListeners();
// Set initial aria-hidden state
this.setAttribute("aria-hidden", "true");
}
disconnectedCallback() {
document.removeEventListener("keydown", this.handleKeyDown);
}
setupEventListeners() {
// Close button
const closeBtn = this.shadowRoot.querySelector(".close-button");
closeBtn?.addEventListener("click", () => this.close());
// Backdrop click
const backdrop = this.shadowRoot.querySelector(".backdrop");
backdrop?.addEventListener("click", this.handleBackdropClick);
// Escape key
document.addEventListener("keydown", this.handleKeyDown);
// Listen for global open event
window.addEventListener("open-mobile-drawer", () => this.open());
window.addEventListener("close-mobile-drawer", () => this.close());
window.addEventListener("toggle-mobile-drawer", () => this.toggle());
}
handleBackdropClick() {
this.close();
}
handleKeyDown(e) {
if (e.key === "Escape" && this.isOpen) {
this.close();
}
}
open() {
this.isOpen = true;
this.shadowRoot.querySelector(".drawer-container").classList.add("open");
this.setAttribute("aria-hidden", "false");
document.body.style.overflow = "hidden";
// Focus trap - focus first focusable element
setTimeout(() => {
const closeBtn = this.shadowRoot.querySelector(".close-button");
closeBtn?.focus();
}, 100);
}
close() {
this.isOpen = false;
this.shadowRoot.querySelector(".drawer-container").classList.remove("open");
this.setAttribute("aria-hidden", "true");
document.body.style.overflow = "";
}
toggle() {
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
--drawer-width: 320px;
--drawer-bg: #ffffff;
--drawer-header-bg: #951D51;
--drawer-header-color: #ffffff;
--backdrop-color: rgba(0, 0, 0, 0.5);
--transition-duration: 300ms;
--section-title-color: #64748b;
--link-color: #1e293b;
--link-hover-color: #951D51;
--border-color: #e2e8f0;
}
.drawer-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000;
pointer-events: none;
}
.drawer-container.open {
pointer-events: auto;
}
.backdrop {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--backdrop-color);
opacity: 0;
transition: opacity var(--transition-duration) ease;
}
.drawer-container.open .backdrop {
opacity: 1;
}
.drawer {
position: absolute;
top: 0;
left: 0;
width: min(var(--drawer-width), 85vw);
height: 100%;
background-color: var(--drawer-bg);
transform: translateX(-100%);
transition: transform var(--transition-duration) cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
flex-direction: column;
box-shadow: 4px 0 20px rgba(0, 0, 0, 0.15);
}
.drawer-container.open .drawer {
transform: translateX(0);
}
.drawer-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background-color: var(--drawer-header-bg);
color: var(--drawer-header-color);
min-height: 56px;
}
.drawer-logo {
font-family: var(--font-family-base, system-ui);
font-size: 1.125rem;
font-weight: 700;
color: inherit;
text-decoration: none;
}
.close-button {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: #ffffff;
border: none;
border-radius: 50%;
cursor: pointer;
color: #951D51;
transition: all 150ms ease;
}
.close-button:hover {
background: #f1f5f9;
transform: scale(1.05);
}
.close-button:focus {
outline: 2px solid rgba(255, 255, 255, 0.5);
outline-offset: 2px;
}
.drawer-content {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 0;
}
.nav-section {
padding: 20px 16px;
border-bottom: 1px solid var(--border-color);
}
.nav-section:last-child {
border-bottom: none;
}
.nav-section-title {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--section-title-color);
margin-bottom: 12px;
}
.nav-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.nav-list a {
display: block;
padding: 10px 12px;
color: var(--link-color);
text-decoration: none;
font-size: 0.9375rem;
border-radius: 8px;
transition: all 150ms ease;
}
.nav-list a:hover {
background-color: #f1f5f9;
color: var(--link-hover-color);
}
.nav-list a:focus {
outline: 2px solid var(--link-hover-color);
outline-offset: -2px;
}
/* Milinda section - smaller styling */
.nav-section.milinda .nav-section-title {
font-size: 0.6875rem;
}
.nav-section.milinda .nav-list a {
font-size: 0.875rem;
padding: 8px 12px;
}
.drawer-footer {
padding: 20px 16px;
border-top: 1px solid var(--border-color);
background-color: #f8fafc;
}
.auth-buttons {
display: flex;
flex-direction: column;
gap: 10px;
}
.btn {
display: flex;
align-items: center;
justify-content: center;
padding: 12px 20px;
font-size: 0.9375rem;
font-weight: 600;
border-radius: 10px;
border: none;
cursor: pointer;
transition: all 150ms ease;
text-decoration: none;
text-align: center;
}
.btn-primary {
background-color: #951D51;
color: #ffffff;
}
.btn-primary:hover {
background-color: #7a1842;
}
.btn-secondary {
background-color: transparent;
color: #951D51;
border: 2px solid #951D51;
}
.btn-secondary:hover {
background-color: #951D51;
color: #ffffff;
}
.btn:focus {
outline: 2px solid #951D51;
outline-offset: 2px;
}
/* Scrollbar styling */
.drawer-content::-webkit-scrollbar {
width: 6px;
}
.drawer-content::-webkit-scrollbar-track {
background: transparent;
}
.drawer-content::-webkit-scrollbar-thumb {
background-color: #cbd5e1;
border-radius: 3px;
}
.drawer-content::-webkit-scrollbar-thumb:hover {
background-color: #94a3b8;
}
</style>
<div class="drawer-container">
<div class="backdrop"></div>
<nav class="drawer" aria-label="Hoofdnavigatie">
<div class="drawer-header">
<a href="index.html" class="drawer-logo">Milinda</a>
<button class="close-button" aria-label="Sluit menu">
<close-icon size="18" color="#951D51"></close-icon>
</button>
</div>
<div class="drawer-content">
<div class="nav-section">
<h2 class="nav-section-title">Onderwerpen</h2>
<ul class="nav-list">
<li><a href="#">Boeddhisme (algemeen)</a></li>
<li><a href="#">Theravada</a></li>
<li><a href="#">Zen</a></li>
<li><a href="#">Tibetaans boeddhisme</a></li>
<li><a href="#">Meditatie</a></li>
<li><a href="#">Mindfulness</a></li>
<li><a href="#">Vipassana</a></li>
<li><a href="#">Pali-canon</a></li>
<li><a href="#">Biografie / autobiografie</a></li>
<li><a href="#">Kinderboeken</a></li>
<li><a href="#">Alle onderwerpen bekijken</a></li>
</ul>
</div>
<div class="nav-section">
<h2 class="nav-section-title">Imprints</h2>
<ul class="nav-list">
<li><a href="#">Asoka</a></li>
<li><a href="#">Synthese</a></li>
<li><a href="#">De Driehoek</a></li>
<li><a href="#">Waerbeke</a></li>
<li><a href="#">Stuivenberg</a></li>
</ul>
</div>
<div class="nav-section milinda">
<h2 class="nav-section-title">Milinda</h2>
<ul class="nav-list">
<li><a href="#">Over ons</a></li>
<li><a href="#">Klantenservice</a></li>
<li><a href="#">Verzending & retourneren</a></li>
<li><a href="#">Nieuwsbrief</a></li>
<li><a href="#">Contact</a></li>
</ul>
</div>
</div>
<div class="drawer-footer">
<div class="auth-buttons">
<a href="#" class="btn btn-primary">Inloggen</a>
<a href="#" class="btn btn-secondary">Registreren</a>
</div>
</div>
</nav>
</div>
`;
}
}
customElements.define("mobile-drawer", MobileDrawer);

View File

@@ -2,11 +2,10 @@
* Push Box Component * Push Box Component
* A promotional container with logo, title, and CTA * A promotional container with logo, title, and CTA
* Uses slots for all content to allow easy customization in HTML * Uses slots for all content to allow easy customization in HTML
* Supports variant="purple" for purple background with white content
*/ */
class PushBox extends HTMLElement { class PushBox extends HTMLElement {
static get observedAttributes() { static get observedAttributes() {
return ["background-color", "text-color", "variant"]; return ["background-color", "text-color"];
} }
constructor() { constructor() {
@@ -49,18 +48,7 @@ class PushBox extends HTMLElement {
} }
} }
get variant() {
return this.getAttribute("variant") || "default";
}
get isPurple() {
return this.variant === "purple";
}
get backgroundColor() { get backgroundColor() {
if (this.isPurple) {
return "#951D51";
}
return ( return (
this.getAttribute("background-color") || this.getAttribute("background-color") ||
"var(--color-push-box-bg, #EBEEF4)" "var(--color-push-box-bg, #EBEEF4)"
@@ -68,19 +56,9 @@ class PushBox extends HTMLElement {
} }
get textColor() { get textColor() {
if (this.isPurple) {
return "#FFFFFF";
}
return this.getAttribute("text-color") || "#951D51"; return this.getAttribute("text-color") || "#951D51";
} }
get titleColor() {
if (this.isPurple) {
return "#FFFFFF";
}
return "#000000";
}
render() { render() {
this.shadowRoot.innerHTML = ` this.shadowRoot.innerHTML = `
<style> <style>
@@ -116,12 +94,12 @@ class PushBox extends HTMLElement {
font-size: 24px; font-size: 24px;
font-weight: 700; font-weight: 700;
line-height: 34px; line-height: 34px;
color: ${this.titleColor}; color: #000000;
margin: 0; margin: 0;
} }
.cta-wrapper { .cta-wrapper {
color: ${this.textColor}; color: #951D51;
font-family: var(--font-family-outfit, "Outfit", sans-serif); font-family: var(--font-family-outfit, "Outfit", sans-serif);
font-size: 16px; font-size: 16px;
font-weight: 400; font-weight: 400;

View File

@@ -41,9 +41,9 @@ class SiteContent extends HTMLElement {
padding-bottom: var(--spacing-md, 16px); padding-bottom: var(--spacing-md, 16px);
} }
</style> </style>
<div class="content"> <main class="content">
<slot></slot> <slot></slot>
</div> </main>
`; `;
} }
} }

View File

@@ -96,7 +96,6 @@ class SiteHeader extends HTMLElement {
} }
</style> </style>
<header class="header"> <header class="header">
<slot name="skip-link"></slot>
<slot name="top-bar"></slot> <slot name="top-bar"></slot>
<div class="collapsible"> <div class="collapsible">
<slot name="nav"></slot> <slot name="nav"></slot>

View File

@@ -1,65 +0,0 @@
/**
* Close Icon Web Component
* A reusable close/X icon element
*/
class CloseIcon extends HTMLElement {
static get observedAttributes() {
return ["size", "color", "stroke-width"];
}
constructor() {
super();
this.attachShadow({ mode: "open" });
}
connectedCallback() {
this.render();
}
attributeChangedCallback() {
this.render();
}
get size() {
return this.getAttribute("size") || "24";
}
get color() {
return this.getAttribute("color") || "currentColor";
}
get strokeWidth() {
return this.getAttribute("stroke-width") || "2";
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: inline-flex;
align-items: center;
justify-content: center;
}
svg {
display: block;
}
</style>
<svg
xmlns="http://www.w3.org/2000/svg"
width="${this.size}"
height="${this.size}"
viewBox="0 0 24 24"
fill="none"
stroke="${this.color}"
stroke-width="${this.strokeWidth}"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
`;
}
}
customElements.define("close-icon", CloseIcon);