Skip to content

Commit c4550c3

Browse files
Merge pull request #135 from jeffreylauwers/feature/menu-link
feat(Navigation): MenuLink + MenuButton + gedeelde menu-item tokens (v5.18.0)
2 parents 50721d9 + 84476f9 commit c4550c3

18 files changed

Lines changed: 1038 additions & 118 deletions

File tree

README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ pnpm --filter @dsn/design-tokens watch
6565
# Start Storybook in development mode
6666
pnpm dev
6767

68-
# Run tests (1187 tests across 58 test suites)
68+
# Run tests (1225 tests across 60 test suites)
6969
pnpm test
7070

7171
# Run tests in watch mode
@@ -201,6 +201,14 @@ All components are fully typed with TypeScript and include comprehensive JSDoc d
201201
| **StatusBadge** | Yes | Yes ||
202202
| **Table** | Yes | Yes ||
203203

204+
**Navigation Components (3)**
205+
206+
| Component | HTML/CSS | React | Web Component |
207+
| ------------------------ | -------- | ----- | ------------- |
208+
| **BreadcrumbNavigation** | Yes | Yes ||
209+
| **MenuButton** | Yes | Yes ||
210+
| **MenuLink** | Yes | Yes ||
211+
204212
**Form Components (25)**
205213

206214
| Component | HTML/CSS | React | Web Component |
@@ -374,7 +382,7 @@ Comprehensive documentation is available in the `/docs` folder:
374382

375383
- **Pre-commit hooks** via Husky + lint-staged (ESLint + Prettier)
376384
- **Type checking** across all packages (`pnpm type-check`)
377-
- **1187 tests** covering React components, Web Components, and utilities
385+
- **1225 tests** covering React components, Web Components, and utilities
378386
- **CI/CD** via GitHub Actions (lint, type-check, test, build)
379387

380388
## Tech Stack

docs/03-components.md

Lines changed: 144 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Components
22

3-
**Last Updated:** April 3, 2026
3+
**Last Updated:** April 4, 2026
44

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

@@ -1586,7 +1586,7 @@ const [isOpen, setIsOpen] = React.useState(false);
15861586

15871587
## Navigation Components
15881588

1589-
**Status:** Complete (HTML/CSS, React) — 1 component total
1589+
**Status:** Complete (HTML/CSS, React) — 3 components total
15901590

15911591
### BreadcrumbNavigation
15921592

@@ -1682,6 +1682,148 @@ const [isOpen, setIsOpen] = React.useState(false);
16821682

16831683
---
16841684

1685+
### MenuLink
1686+
1687+
**Status:** Complete (HTML/CSS, React)
1688+
1689+
**Location:** `packages/components-{html|react}/src/MenuLink/`
1690+
1691+
**Tokens:** `tokens/components/menu-item.json` (gedeeld) + `tokens/components/menu-link.json` (MenuLink-specifiek)
1692+
1693+
**Props:** `href`, `level` (1–4), `current`, `iconStart`, `iconEnd`, `numberBadge`, `subItems`, `expanded`, `onExpandToggle`, `children`, `className`
1694+
1695+
**Features:**
1696+
1697+
- Semantisch een `<a>`, visueel consistent met MenuButton — gebruik voor URL-navigatie
1698+
- Rendeert als `<li class="dsn-menu-link">` — altijd in een `<ul>` plaatsen
1699+
- `level` prop (1–4) stelt hiërarchische inspringing in via `margin-inline-start` op de link
1700+
- `current` prop voegt `aria-current="page"` toe en toont een `border-inline-start` indicator (3px)
1701+
- `subItems` prop toont een uitklapknop naast de link; `expanded` beheert de open/dichte staat
1702+
- `numberBadge` slot voor een `<NumberBadge>` rechts van het label
1703+
- Gedeelde visuele stijl via `--dsn-menu-item-*` tokens; current-staat via `--dsn-menu-link-current-*`
1704+
1705+
**CSS-klassen:**
1706+
1707+
| Klasse | Element | Beschrijving |
1708+
| ------------------------------ | ---------- | --------------------------------------------------------- |
1709+
| `dsn-menu-link` | `<li>` | Basiscomponent — altijd aanwezig |
1710+
| `dsn-menu-link--level-2` | `<li>` | Inspringing: 1× `level-indent` |
1711+
| `dsn-menu-link--level-3` | `<li>` | Inspringing: 2× `level-indent` |
1712+
| `dsn-menu-link--level-4` | `<li>` | Inspringing: 3× `level-indent` |
1713+
| `dsn-menu-link__link` | `<a>` | De navigatielink — bevat icoon, label, badge |
1714+
| `dsn-menu-link__label` | `<span>` | Zichtbare linktekst |
1715+
| `dsn-menu-link__divider` | `<span>` | Decoratieve scheidingslijn tussen link en uitklapknop |
1716+
| `dsn-menu-link__expand-button` | `<button>` | Uitklapknop; `aria-expanded` toggle; chevron roteert 180° |
1717+
1718+
**Usage:**
1719+
1720+
```html
1721+
<!-- HTML/CSS — level 1, standaard -->
1722+
<ul style="list-style: none; margin: 0; padding: 0;">
1723+
<li class="dsn-menu-link">
1724+
<a class="dsn-menu-link__link" href="/dashboard">
1725+
<svg class="dsn-icon" aria-hidden="true"><!-- home --></svg>
1726+
<span class="dsn-menu-link__label">Dashboard</span>
1727+
</a>
1728+
</li>
1729+
<!-- level 2, actieve pagina -->
1730+
<li class="dsn-menu-link dsn-menu-link--level-2">
1731+
<a class="dsn-menu-link__link" href="/rapporten" aria-current="page">
1732+
<span class="dsn-menu-link__label">Rapporten</span>
1733+
</a>
1734+
</li>
1735+
</ul>
1736+
```
1737+
1738+
```tsx
1739+
// React
1740+
<MenuLink href="/dashboard" iconStart={<Icon name="home" aria-hidden />}>
1741+
Dashboard
1742+
</MenuLink>
1743+
<MenuLink href="/rapporten" level={2} current>
1744+
Rapporten
1745+
</MenuLink>
1746+
<MenuLink href="/inbox" numberBadge={<NumberBadge variant="negative">5</NumberBadge>}>
1747+
Inbox
1748+
</MenuLink>
1749+
```
1750+
1751+
**Tests:** React (27 tests)
1752+
1753+
---
1754+
1755+
### MenuButton
1756+
1757+
**Status:** Complete (HTML/CSS, React)
1758+
1759+
**Location:** `packages/components-{html|react}/src/MenuButton/`
1760+
1761+
**Tokens:** `tokens/components/menu-item.json` (gedeeld met MenuLink)
1762+
1763+
**Props:** `iconStart`, `iconEnd`, `dotBadge`, `children`, `className` + alle native `<button>` attributen
1764+
1765+
**Features:**
1766+
1767+
- Semantisch een `<button>`, visueel consistent met MenuLink — gebruik voor JS-acties (uitloggen, modal openen, etc.)
1768+
- Rendeert als `<li class="dsn-menu-button">` — altijd in een `<ul>` plaatsen
1769+
- `dotBadge` slot voor een `<DotBadge>` die rechtsboven de tekst zweeft (gerenderd in de label-span, `position: relative`)
1770+
- Geen disabled state — niet van toepassing in navigatiecontext
1771+
- Volledig gedeelde visuele stijl via `--dsn-menu-item-*` tokens
1772+
1773+
**CSS-klassen:**
1774+
1775+
| Klasse | Element | Beschrijving |
1776+
| ------------------------- | ---------- | ------------------------------------------------------------------------ |
1777+
| `dsn-menu-button` | `<li>` | Basiscomponent — altijd aanwezig |
1778+
| `dsn-menu-button__button` | `<button>` | De knop — button-reset + volledige breedte, flexbox layout |
1779+
| `dsn-menu-button__label` | `<span>` | Zichtbare knoptekst; `flex: 1`; `position: relative` voor dotBadge anker |
1780+
1781+
**Usage:**
1782+
1783+
```html
1784+
<!-- HTML/CSS -->
1785+
<ul style="list-style: none; margin: 0; padding: 0;">
1786+
<li class="dsn-menu-button">
1787+
<button type="button" class="dsn-menu-button__button">
1788+
<svg class="dsn-icon" aria-hidden="true"><!-- settings --></svg>
1789+
<span class="dsn-menu-button__label">Instellingen</span>
1790+
</button>
1791+
</li>
1792+
<!-- met DotBadge -->
1793+
<li class="dsn-menu-button">
1794+
<button type="button" class="dsn-menu-button__button">
1795+
<svg class="dsn-icon" aria-hidden="true"><!-- bell --></svg>
1796+
<span class="dsn-menu-button__label">
1797+
Meldingen
1798+
<span class="dsn-visually-hidden">, nieuwe meldingen beschikbaar</span>
1799+
<span
1800+
class="dsn-dot-badge dsn-dot-badge--negative"
1801+
aria-hidden="true"
1802+
></span>
1803+
</span>
1804+
</button>
1805+
</li>
1806+
</ul>
1807+
```
1808+
1809+
```tsx
1810+
// React
1811+
<MenuButton iconStart={<Icon name="settings" aria-hidden />}>
1812+
Instellingen
1813+
</MenuButton>
1814+
<MenuButton
1815+
iconStart={<Icon name="bell" aria-hidden />}
1816+
dotBadge={<DotBadge variant="negative" />}
1817+
>
1818+
Meldingen
1819+
<span className="dsn-visually-hidden">, nieuwe meldingen beschikbaar</span>
1820+
</MenuButton>
1821+
```
1822+
1823+
**Tests:** React (13 tests)
1824+
1825+
---
1826+
16851827
## Form Components
16861828

16871829
**Status:** Complete (HTML/CSS, React) — 25 components total

docs/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Design System Documentation
22

3-
**Version:** 5.16.0
4-
**Last Updated:** March 27, 2026
3+
**Version:** 5.18.0
4+
**Last Updated:** April 4, 2026
55

66
Complete documentation voor het Design System Starter Kit.
77

@@ -91,8 +91,8 @@ Complete documentation voor het Design System Starter Kit.
9191

9292
- **Tokens per configuration:** ~1100 (400 semantic + 700 component)
9393
- **Configurations:** 8 (2 themes × 2 modes × 2 project types)
94-
- **Components:** 46 (4 layout + 9 content + 8 display/feedback + 25 form + Drawer; HTML/CSS + React)
95-
- **Tests:** 1169 across 57 test suites
94+
- **Components:** 49 (5 layout + 10 content + 9 display/feedback + 3 navigation + 25 form; HTML/CSS + React)
95+
- **Tests:** 1225 across 60 test suites
9696
- **Storybook stories:** 130+
9797

9898
---

docs/changelog.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,39 @@ All notable changes to this project are documented in this file.
66

77
---
88

9+
## Version 5.18.0 (April 4, 2026)
10+
11+
### MenuButton component + gedeelde menu-item tokens
12+
13+
#### Added
14+
15+
- **MenuButton** component — navigatieknop voor JavaScript-acties (uitloggen, modal openen, etc.); semantisch een `<button>`, visueel identiek aan MenuLink
16+
- `iconStart`, `iconEnd`, `dotBadge` props; `dotBadge` zweeft rechtsboven de tekst via `position: relative` op de label-span
17+
- 13 React tests
18+
19+
#### Changed
20+
21+
- **Tokens gerefactored:** gedeelde visuele stijl van MenuLink en MenuButton samengebracht in nieuw bestand `tokens/components/menu-item.json` (`dsn.menu-item.*` namespace)
22+
- `tokens/components/menu-link.json` bevat nu alleen nog MenuLink-specifieke tokens (`current.*` en `level-indent`)
23+
- `tokens/components/menu-button.json` verwijderd — vervangen door de gedeelde `menu-item.json`
24+
- Kleurverwijzingen van MenuLink en MenuButton gewijzigd van `action-1` naar `action-2` — consistent met `Link`
25+
26+
---
27+
28+
### MenuLink component
29+
30+
#### Added
31+
32+
- **MenuLink** component — navigatielink met niveau-hiërarchie, actieve pagina-staat en uitklapbare subnavigatie
33+
- `level` prop (1–4): toenemende `padding-inline-start` inspringing via `margin-inline-start` op de link
34+
- `current` prop: `aria-current="page"` + visuele `border-inline-start` indicator (3px, `action-2` kleur)
35+
- `subItems` + `expanded` + `onExpandToggle`: uitklapknop naast de link met `aria-expanded` en roterende chevron
36+
- `iconStart`, `iconEnd`, `numberBadge` props
37+
- Storybook stories: Default, Current, Met icoon start/end, Met NumberBadge, Met uitklapknop, Niveauhiërarchie, Volledig navigatiemenu, Alle staten, RTL, RTL long text
38+
- 27 React tests
39+
40+
---
41+
942
## Version 5.17.0 (April 3, 2026)
1043

1144
### NumberBadge component (issue #130)
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/**
2+
* MenuButton Component
3+
* Navigatieknop voor JS-acties met icoon- en badge-ondersteuning.
4+
* Semantisch een `<button>`, visueel consistent met MenuLink.
5+
*
6+
* Gedeelde visuele stijl via --dsn-menu-item-* tokens (menu-item.json).
7+
*
8+
* Structuur:
9+
* <li class="dsn-menu-button">
10+
* <button type="button" class="dsn-menu-button__button">
11+
* <svg class="dsn-icon" aria-hidden="true"><!-- icoon-start --></svg>
12+
* <span class="dsn-menu-button__label">Label</span>
13+
* <span class="dsn-dot-badge dsn-dot-badge--negative" aria-hidden="true"></span>
14+
* <svg class="dsn-icon" aria-hidden="true"><!-- icoon-end --></svg>
15+
* </button>
16+
* </li>
17+
*/
18+
19+
/* =============================================================================
20+
List item
21+
============================================================================= */
22+
23+
.dsn-menu-button {
24+
list-style: none;
25+
margin: 0;
26+
padding: 0;
27+
}
28+
29+
/* =============================================================================
30+
Button
31+
============================================================================= */
32+
33+
.dsn-menu-button__button {
34+
/* Button reset */
35+
appearance: none;
36+
background: none;
37+
border: 0;
38+
cursor: pointer;
39+
40+
/* Layout */
41+
display: inline-flex;
42+
align-items: center;
43+
width: 100%;
44+
gap: var(--dsn-menu-item-gap);
45+
padding-block: var(--dsn-menu-item-padding-block);
46+
padding-inline: var(--dsn-menu-item-padding-inline);
47+
48+
/* Typography */
49+
font-family: inherit;
50+
font-size: var(--dsn-menu-item-font-size);
51+
font-weight: var(--dsn-menu-item-font-weight);
52+
line-height: var(--dsn-menu-item-line-height);
53+
text-align: start;
54+
55+
/* Colors */
56+
color: var(--dsn-menu-item-color);
57+
background-color: var(--dsn-menu-item-background-color);
58+
59+
/* Transitions */
60+
transition:
61+
background-color var(--dsn-transition-duration-normal)
62+
var(--dsn-transition-easing-default),
63+
color var(--dsn-transition-duration-normal)
64+
var(--dsn-transition-easing-default);
65+
}
66+
67+
.dsn-menu-button__button > .dsn-icon {
68+
width: var(--dsn-menu-item-icon-size);
69+
height: var(--dsn-menu-item-icon-size);
70+
flex-shrink: 0;
71+
}
72+
73+
.dsn-menu-button__label {
74+
/* De <button> zelf is full-width via width: 100%.
75+
Het label vult de resterende ruimte zodat iconEnd rechts uitlijnt.
76+
position: relative zodat de dotBadge zich absoluut rechtsboven de tekst positioneert. */
77+
flex: 1;
78+
text-align: start;
79+
position: relative;
80+
}
81+
82+
/* =============================================================================
83+
States
84+
============================================================================= */
85+
86+
.dsn-menu-button__button:hover {
87+
color: var(--dsn-menu-item-hover-color);
88+
background-color: var(--dsn-menu-item-hover-background-color);
89+
}
90+
91+
.dsn-menu-button__button:active {
92+
color: var(--dsn-menu-item-active-color);
93+
background-color: var(--dsn-menu-item-active-background-color);
94+
}
95+
96+
.dsn-menu-button__button:focus-visible {
97+
background-color: var(--dsn-focus-background-color);
98+
color: var(--dsn-focus-color);
99+
outline: var(--dsn-focus-outline-width) var(--dsn-focus-outline-style)
100+
var(--dsn-focus-outline-color);
101+
outline-offset: var(--dsn-focus-outline-offset);
102+
box-shadow: 0 0 0
103+
calc(var(--dsn-focus-outline-offset) + var(--dsn-focus-outline-width))
104+
var(--dsn-focus-inverse-outline-color);
105+
}

0 commit comments

Comments
 (0)