Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,26 @@ Kernregels:
- Geen geneste element-namen: `dsn-alert__content__text` ❌
- HTML-toestanden via pseudo-klassen: `.dsn-button:disabled` ✅

### 4. TypeScript moet volledig schoon zijn
### 4. Token-hiërarchie — altijd op de juiste laag aanpassen

Tokens zijn gelaagd: `base.json` (gedeelde primitieven) → component-token JSON → CSS custom property → component CSS. Pas altijd aan op de **hoogste laag die de waarde definieert**, zodat de delegatieketen intact blijft.

```json
// ❌ Omzeilen van de delegatieketen in text-input.json
"padding-block-start": { "value": "{dsn.space.block.md}" }

// ✅ Aanpassen op de juiste laag — in base.json onder form-control
"padding-block-start": { "value": "{dsn.space.block.md}" }
// text-input.json blijft delegeren naar {dsn.form-control.padding-block-start}
```

**Werkwijze bij een token-wijziging:**

1. Zoek via `Grep` welk token de waarde _uiteindelijk_ definieert (vaak in `base.json` of een theme-bestand)
2. Pas dáár de waarde aan
3. Controleer of de delegatieketen in component-JSONs ongewijzigd blijft

### 5. TypeScript moet volledig schoon zijn

`pnpm --filter storybook exec tsc --noEmit` geeft **0 fouten en 0 warnings**. Nieuwe code mag geen nieuwe fouten of warnings introduceren.

Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ pnpm --filter @dsn/design-tokens watch
# Start Storybook in development mode
pnpm dev

# Run tests (1234 tests across 61 test suites)
# Run tests (1282 tests across 63 test suites)
pnpm test

# Run tests in watch mode
Expand Down Expand Up @@ -201,14 +201,15 @@ All components are fully typed with TypeScript and include comprehensive JSDoc d
| **StatusBadge** | Yes | Yes | — |
| **Table** | Yes | Yes | — |

**Navigation Components (4)**
**Navigation Components (5)**

| Component | HTML/CSS | React | Web Component |
| ------------------------ | -------- | ----- | ------------- |
| **BreadcrumbNavigation** | Yes | Yes | — |
| **Menu** | Yes | Yes | — |
| **MenuButton** | Yes | Yes | — |
| **MenuLink** | Yes | Yes | — |
| **PageHeader** | Yes | Yes | — |

**Form Components (25)**

Expand Down Expand Up @@ -383,7 +384,7 @@ Comprehensive documentation is available in the `/docs` folder:

- **Pre-commit hooks** via Husky + lint-staged (ESLint + Prettier)
- **Type checking** across all packages (`pnpm type-check`)
- **1248 tests** covering React components, Web Components, and utilities
- **1282 tests** covering React components, Web Components, and utilities
- **CI/CD** via GitHub Actions (lint, type-check, test, build)

## Tech Stack
Expand Down
229 changes: 223 additions & 6 deletions docs/03-components.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Components

**Last Updated:** April 5, 2026
**Last Updated:** April 6, 2026

Complete component specifications and guidelines for the Design System Starter Kit.

Expand Down Expand Up @@ -1587,7 +1587,7 @@ const [isOpen, setIsOpen] = React.useState(false);

## Navigation Components

**Status:** Complete (HTML/CSS, React) — 4 components total
**Status:** Complete (HTML/CSS, React) — 5 components total

### Menu

Expand Down Expand Up @@ -1899,6 +1899,223 @@ const [isOpen, setIsOpen] = React.useState(false);

---

### PageHeader

**Status:** Complete (HTML/CSS, React)

**Location:** `packages/components-{html|react}/src/PageHeader/`

**Tokens:** `tokens/components/page-header.json`

**Props:** `logoSlot`, `sticky` (`'none'` | `'sticky'` | `'auto-hide'`), `primaryNavigation`, `primaryNavigationLarge`, `secondaryNavigation`, `secondaryNavigationLarge`, `searchSlot`, `onMenuOpen`, `onMenuClose`, `onSearchOpen`, `onSearchClose`, `className`

**Features:**

- Mobile-first: hamburgerknop (inline-start) opent een `Drawer`, gecentreerd logo (CSS-grid `1fr auto 1fr`), zoekknop (inline-end) ontvouwt zoekpaneel direct onder de header
- Boven `64em` (~1024px): tweebandig large viewport layout via `display: none` switch
- **Masthead** — neutrale achtergrond met logo (inline-start), servicemenu en inline zoekveld (inline-end)
- **Navigatiebalk** — accent-1 achtergrond met primaire navigatie; MenuLink-items krijgen `min-block-size: 4rem` en `padding-inline: var(--dsn-space-inline-xl)` via token-overschrijving op de container
- `primaryNavigationLarge` / `secondaryNavigationLarge` — aparte slots voor large viewport; valt terug op de mobile variant wanneer weggelaten
- `sticky='sticky'`: `position: sticky; inset-block-start: 0`
- `sticky='auto-hide'`: sticky + verbergt bij scroll-down via JS `scroll`-eventlistener (`data-hidden` attribuut), CSS-transitie animeert de beweging
- Focus management: openen zoekpaneel → focus naar `<input>`; sluiten → focus terug naar zoekknop
- Unieke IDs via `useId()` voor `aria-controls` (zoekpaneel) en `aria-labelledby` (nav-elementen)
- De `Drawer` is altijd in de DOM (niet `hidden`) zodat focus management correct werkt

**CSS-klassen:**

| Klasse | Element | Beschrijving |
| --------------------------------- | ---------- | ----------------------------------------------------- |
| `dsn-page-header` | `<header>` | Basiscomponent |
| `dsn-page-header--sticky` | `<header>` | Sticky gedrag: `position: sticky` |
| `dsn-page-header--auto-hide` | `<header>` | Auto-hide sticky: CSS-transitie op `data-hidden` |
| `dsn-page-header__small-layout` | `<div>` | Zichtbaar op small viewport (`< 64em`) |
| `dsn-page-header__large-layout` | `<div>` | Zichtbaar op large viewport (`≥ 64em`) |
| `dsn-page-header__inner` | `<div>` | CSS-grid (`1fr auto 1fr`) voor gecentreerd logo |
| `dsn-page-header__start` | `<div>` | Inline-start slot (hamburgerknop) |
| `dsn-page-header__logo` | `<div>` | Logo-slot; `max-block-size` op directe child |
| `dsn-page-header__end` | `<div>` | Inline-end slot (zoekknop) |
| `dsn-page-header__search-panel` | `<div>` | Zoekpaneel (small viewport); verborgen via `[hidden]` |
| `dsn-page-header__search-inner` | `<div>` | Flex-container: zoekveld + zoekknop |
| `dsn-page-header__masthead` | `<div>` | Bovenste band large viewport (neutrale achtergrond) |
| `dsn-page-header__masthead-inner` | `<div>` | Flex-container: logo ↔ secondary-nav |
| `dsn-page-header__secondary-nav` | `<div>` | Servicemenu + zoekveld naast elkaar (inline-end) |
| `dsn-page-header__searchbox` | `<div>` | Inline zoekveld + zoekknop in masthead |
| `dsn-page-header__navbar` | `<div>` | Onderste band large viewport (accent-1 achtergrond) |

**Design tokens:**

| Token | Waarde | Beschrijving |
| ------------------------------------------------- | ------------------------------------ | ------------------------------------------- |
| `--dsn-page-header-background-color` | `{dsn.color.neutral.bg-document}` | Achtergrondkleur header |
| `--dsn-page-header-border-block-end-width` | `{dsn.border.width.thick}` | Breedte onderkantrand (4px, small viewport) |
| `--dsn-page-header-border-block-end-color` | `{dsn.color.accent-1.color-default}` | Kleur onderkantrand (merkkleur) |
| `--dsn-page-header-padding-block` | `{dsn.space.block.md}` | Verticale padding mobile binnenbalk |
| `--dsn-page-header-padding-inline` | `{dsn.space.inline.xl}` | Horizontale padding mobile binnenbalk |
| `--dsn-page-header-z-index` | `300` | Z-index sticky — onder backdrop (400) |
| `--dsn-page-header-logo-max-block-size` | `2rem` | Maximale hoogte logo (32px) |
| `--dsn-page-header-search-panel-background-color` | `{dsn.color.accent-1.bg-default}` | Achtergrond zoekpaneel (small viewport) |
| `--dsn-page-header-search-panel-padding-block` | `{dsn.space.block.md}` | Verticale padding zoekpaneel |
| `--dsn-page-header-search-panel-padding-inline` | `{dsn.space.inline.xl}` | Horizontale padding zoekpaneel |
| `--dsn-page-header-masthead-background-color` | `{dsn.color.neutral.bg-document}` | Masthead achtergrond (large viewport) |
| `--dsn-page-header-masthead-padding-block` | `{dsn.space.block.xl}` | Verticale padding masthead |
| `--dsn-page-header-masthead-padding-inline` | `{dsn.space.inline.xl}` | Horizontale padding masthead |
| `--dsn-page-header-navbar-background-color` | `{dsn.color.accent-1.bg-default}` | Navigatiebalk achtergrond |
| `--dsn-page-header-navbar-padding-inline` | `{dsn.space.inline.xl}` | Horizontale padding navigatiebalk |
| `--dsn-page-header-secondary-nav-gap` | `{dsn.space.column.3xl}` | Gap servicemenu ↔ zoekveld in masthead |

**Usage:**

```html
<!-- HTML/CSS — small viewport -->
<header class="dsn-page-header">
<div class="dsn-page-header__small-layout">
<div class="dsn-page-header__inner">
<div class="dsn-page-header__start">
<button
type="button"
class="dsn-button dsn-button--subtle"
aria-expanded="false"
aria-controls="nav-drawer"
>
<svg class="dsn-icon" aria-hidden="true"><!-- menu --></svg>
<span class="dsn-button__label">Menu</span>
</button>
</div>
<div class="dsn-page-header__logo">
<a href="/">
<svg class="dsn-logo" aria-hidden="true"><!-- logo --></svg>
<span class="dsn-visually-hidden"
>Naam organisatie — terug naar homepage</span
>
</a>
</div>
<div class="dsn-page-header__end">
<button
type="button"
class="dsn-button dsn-button--subtle"
aria-expanded="false"
aria-controls="search-panel"
>
<svg class="dsn-icon" aria-hidden="true"><!-- search --></svg>
<span class="dsn-button__label">Zoeken</span>
</button>
</div>
</div>
<div class="dsn-page-header__search-panel" id="search-panel" hidden>
<div class="dsn-page-header__search-inner">
<div class="dsn-search-input-wrapper">
<input
type="search"
class="dsn-text-input"
placeholder="Zoeken…"
aria-label="Zoekopdracht"
/>
</div>
<button type="button" class="dsn-button dsn-button--strong">
<span class="dsn-button__label">Zoeken</span>
</button>
</div>
</div>
</div>

<!-- Large viewport (zichtbaar boven 64em) -->
<div class="dsn-page-header__large-layout">
<div class="dsn-page-header__masthead">
<div class="dsn-page-header__masthead-inner">
<div class="dsn-page-header__logo">
<a href="/"
><svg class="dsn-logo" aria-hidden="true"><!-- logo --></svg></a
>
</div>
<div class="dsn-page-header__secondary-nav">
<nav aria-labelledby="service-nav-id">
<h2 id="service-nav-id" class="dsn-visually-hidden">Servicemenu</h2>
<ul class="dsn-menu dsn-menu--horizontal">
<!-- MenuLink items -->
</ul>
</nav>
<div class="dsn-page-header__searchbox">
<div class="dsn-search-input-wrapper">
<input
type="search"
class="dsn-text-input"
placeholder="Zoeken…"
aria-label="Zoekopdracht"
/>
</div>
<button type="button" class="dsn-button dsn-button--strong">
<span class="dsn-button__label">Zoeken</span>
</button>
</div>
</div>
</div>
</div>
<div class="dsn-page-header__navbar">
<nav aria-labelledby="primary-nav-id">
<h2 id="primary-nav-id" class="dsn-visually-hidden">Hoofdmenu</h2>
<ul class="dsn-menu dsn-menu--horizontal">
<!-- MenuLink items -->
</ul>
</nav>
</div>
</div>
</header>
```

```tsx
// React
<PageHeader
logoSlot={
<a href="/">
<Logo aria-hidden={true} />
<span className="dsn-visually-hidden">
Naam organisatie — terug naar homepage
</span>
</a>
}
primaryNavigation={
<Menu orientation="vertical">
<MenuLink href="/home" level={1}>
Home
</MenuLink>
</Menu>
}
primaryNavigationLarge={
<Menu orientation="horizontal">
<MenuLink href="/home" level={1} current>
Home
</MenuLink>
</Menu>
}
secondaryNavigation={
<Menu orientation="vertical">
<MenuLink href="/contact" level={1}>
Contact
</MenuLink>
</Menu>
}
secondaryNavigationLarge={
<Menu orientation="horizontal">
<MenuLink href="/contact" level={1}>
Contact
</MenuLink>
</Menu>
}
searchSlot={
<>
<SearchInput placeholder="Zoeken…" aria-label="Zoekopdracht" />
<Button variant="strong">Zoeken</Button>
</>
}
sticky="auto-hide"
/>
```

**Tests:** React (38 tests)

---

## Branding Components

**Status:** Complete (HTML/CSS, React) — 1 component total
Expand Down Expand Up @@ -2294,15 +2511,15 @@ defineButton('my-custom-button');

## Component Statistics

**Total Components:** 48
**Total Components:** 49

**Implementations:**

- **HTML/CSS:** 48 components
- **React:** 48 components (1248 tests total, 62 test suites)
- **HTML/CSS:** 49 components
- **React:** 49 components (1282 tests total, 63 test suites)
- **Web Component:** 7 components (Button, Heading, Icon, Link, OrderedList, Paragraph, UnorderedList)

**Test Coverage:** 1248 tests across 62 test suites
**Test Coverage:** 1282 tests across 63 test suites

---

Expand Down
35 changes: 35 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,41 @@ All notable changes to this project are documented in this file.

---

## Version 5.21.1 (April 6, 2026)

### Fix: PageHeader large viewport verfijningen (PR #146)

#### Fixed

- **Zoekknop padding** in de masthead (large viewport): `SearchInput` wrapper krijgt `flex: 1` + `max-inline-size: none` zodat de Zoeken-knop zijn natuurlijke breedte en padding behoudt — zelfde patroon als het mobile zoekpaneel
- **Token gecorrigeerd**: `--dsn-page-header-padding-inline` van `{dsn.space.row.md}` naar `{dsn.space.inline.xl}` — nu consistent met masthead en navbar padding
- **Docs gecorrigeerd**: `--dsn-page-header-search-panel-background-color` was gedocumenteerd als `{dsn.color.neutral.bg-subtle}`, is `{dsn.color.accent-1.bg-default}`

#### Changed

- **Navbar MenuLink-hoogte en padding**: `min-block-size: 4rem` en `padding-inline: var(--dsn-space-inline-xl)` via token-overschrijving op `.dsn-page-header__navbar`; de `calc()`-compensatie voor de current-indicator werkt automatisch mee
- **Storybook controls opgeschoond**: circulaire `DocsPage`-import verwijderd (loste crash op), `argTypes` geherstructureerd in drie categorieën: **Gedrag** / **Slots** / **Events**

---

## Version 5.21.0 (April 6, 2026)

### PageHeader large viewport layout (PR #142)

#### Added

- **Large viewport layout** (≥ 64em) voor `PageHeader` — tweebandig ontwerp boven `64em` via `display: none` switch:
- **Masthead** (`dsn-page-header__masthead`) — neutrale achtergrond met logo (inline-start), servicemenu en inline zoekveld (inline-end)
- **Navigatiebalk** (`dsn-page-header__navbar`) — accent-1 achtergrond met primaire navigatie
- `primaryNavigationLarge` prop — aparte navigatie-inhoud voor de navigatiebalk op large viewport; valt terug op `primaryNavigation` wanneer weggelaten
- `secondaryNavigationLarge` prop — aparte servicemenu-inhoud voor de masthead; valt terug op `secondaryNavigation` wanneer weggelaten
- `searchSlot` prop — inline zoekveld (SearchInput + zoekknop) in de masthead rechts van het servicemenu
- CSS-klassen: `dsn-page-header__large-layout`, `dsn-page-header__small-layout`, `dsn-page-header__masthead`, `dsn-page-header__masthead-inner`, `dsn-page-header__secondary-nav`, `dsn-page-header__searchbox`, `dsn-page-header__navbar`
- 6 nieuwe design tokens: `masthead-background-color`, `masthead-padding-block`, `masthead-padding-inline`, `navbar-background-color`, `navbar-padding-inline`, `secondary-nav-gap`
- 38 React tests

---

## Version 5.20.0 (April 5, 2026)

### Logo component (issue #126)
Expand Down
19 changes: 19 additions & 0 deletions packages/components-html/src/page-header/page-header.css
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,32 @@
gap: var(--dsn-space-inline-md);
}

/* SearchInput wrapper vult beschikbare ruimte — button behoudt zijn natuurlijke
breedte en padding. Zelfde patroon als dsn-page-header__search-inner. */
.dsn-page-header__searchbox .dsn-search-input-wrapper {
flex: 1;
min-inline-size: 0;
max-inline-size: none;
}

.dsn-page-header__searchbox .dsn-text-input {
max-inline-size: none;
inline-size: 100%;
}

/* =============================================================================
Navbar (large viewport) — accent-1 achtergrond, primaire navigatie
============================================================================= */

.dsn-page-header__navbar {
background-color: var(--dsn-page-header-navbar-background-color);
padding-inline: var(--dsn-page-header-navbar-padding-inline);

/* MenuLink-items in de navbar krijgen een grotere min-block-size en ruimere
padding-inline. Door de tokens te overschrijven op de container werkt de
calc()-compensatie voor de current-indicator automatisch mee. */
--dsn-menu-item-min-block-size: 4rem;
--dsn-menu-item-padding-inline: var(--dsn-space-inline-xl);
}

/* =============================================================================
Expand Down
Loading
Loading