@dsn-starter-kit/components-react 3.4.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/Card/Card.d.mts +86 -21
  2. package/dist/Card/Card.d.mts.map +1 -1
  3. package/dist/Card/Card2.mjs +100 -17
  4. package/dist/Card/Card2.mjs.map +1 -1
  5. package/dist/Card/index.d.mts +2 -2
  6. package/dist/DateInput/DateInput.d.mts +1 -1
  7. package/dist/DateInput/DateInput.d.mts.map +1 -1
  8. package/dist/DateInput/DateInput2.mjs +1 -2
  9. package/dist/DateInput/DateInput2.mjs.map +1 -1
  10. package/dist/DateInputGroup/DateInputGroup2.mjs +3 -3
  11. package/dist/DateInputGroup/DateInputGroup2.mjs.map +1 -1
  12. package/dist/Drawer/Drawer.d.mts +11 -3
  13. package/dist/Drawer/Drawer.d.mts.map +1 -1
  14. package/dist/Drawer/Drawer2.mjs +28 -4
  15. package/dist/Drawer/Drawer2.mjs.map +1 -1
  16. package/dist/EmailInput/EmailInput.d.mts +10 -5
  17. package/dist/EmailInput/EmailInput.d.mts.map +1 -1
  18. package/dist/EmailInput/EmailInput2.mjs +2 -2
  19. package/dist/EmailInput/EmailInput2.mjs.map +1 -1
  20. package/dist/ModalDialog/ModalDialog.d.mts +12 -2
  21. package/dist/ModalDialog/ModalDialog.d.mts.map +1 -1
  22. package/dist/ModalDialog/ModalDialog2.mjs +35 -8
  23. package/dist/ModalDialog/ModalDialog2.mjs.map +1 -1
  24. package/dist/NumberInput/NumberInput.d.mts +4 -4
  25. package/dist/NumberInput/NumberInput.d.mts.map +1 -1
  26. package/dist/NumberInput/NumberInput2.mjs +2 -2
  27. package/dist/NumberInput/NumberInput2.mjs.map +1 -1
  28. package/dist/PageHeader/PageHeader.d.mts.map +1 -1
  29. package/dist/PageHeader/PageHeader2.mjs +20 -2
  30. package/dist/PageHeader/PageHeader2.mjs.map +1 -1
  31. package/dist/PasswordInput/PasswordInput.d.mts +10 -5
  32. package/dist/PasswordInput/PasswordInput.d.mts.map +1 -1
  33. package/dist/PasswordInput/PasswordInput2.mjs +2 -2
  34. package/dist/PasswordInput/PasswordInput2.mjs.map +1 -1
  35. package/dist/SearchInput/SearchInput.d.mts +10 -5
  36. package/dist/SearchInput/SearchInput.d.mts.map +1 -1
  37. package/dist/SearchInput/SearchInput2.mjs +2 -3
  38. package/dist/SearchInput/SearchInput2.mjs.map +1 -1
  39. package/dist/Select/Select.d.mts +6 -6
  40. package/dist/Select/Select.d.mts.map +1 -1
  41. package/dist/Select/Select2.mjs +4 -5
  42. package/dist/Select/Select2.mjs.map +1 -1
  43. package/dist/TelephoneInput/TelephoneInput.d.mts +10 -5
  44. package/dist/TelephoneInput/TelephoneInput.d.mts.map +1 -1
  45. package/dist/TelephoneInput/TelephoneInput2.mjs +2 -2
  46. package/dist/TelephoneInput/TelephoneInput2.mjs.map +1 -1
  47. package/dist/TextArea/TextArea.d.mts +10 -5
  48. package/dist/TextArea/TextArea.d.mts.map +1 -1
  49. package/dist/TextArea/TextArea2.mjs +2 -2
  50. package/dist/TextArea/TextArea2.mjs.map +1 -1
  51. package/dist/TextInput/TextInput.d.mts +4 -4
  52. package/dist/TextInput/TextInput.d.mts.map +1 -1
  53. package/dist/TextInput/TextInput2.mjs +2 -2
  54. package/dist/TextInput/TextInput2.mjs.map +1 -1
  55. package/dist/TimeInput/TimeInput.d.mts +1 -1
  56. package/dist/TimeInput/TimeInput.d.mts.map +1 -1
  57. package/dist/TimeInput/TimeInput2.mjs +1 -2
  58. package/dist/TimeInput/TimeInput2.mjs.map +1 -1
  59. package/dist/index.cjs +303 -53
  60. package/dist/index.cjs.map +1 -1
  61. package/dist/index.css +5439 -3838
  62. package/dist/index.d.cts +163 -55
  63. package/dist/index.d.cts.map +1 -1
  64. package/dist/index.d.mts +2 -2
  65. package/dist/index.mjs +2 -2
  66. package/dist/utils/focusTrap.mjs +99 -0
  67. package/dist/utils/focusTrap.mjs.map +1 -0
  68. package/package.json +3 -3
package/dist/index.cjs CHANGED
@@ -1630,8 +1630,8 @@ LinkButton.displayName = "LinkButton";
1630
1630
  * <TextInput readOnly value="Cannot edit this" />
1631
1631
  * ```
1632
1632
  */
1633
- const TextInput = react.default.forwardRef(({ className, invalid, width, type = "text", ...props }, ref) => {
1634
- const classes = (0, _dsn_starter_kit_core.classNames)("dsn-text-input", width && `dsn-text-input--width-${width}`, className);
1633
+ const TextInput = react.default.forwardRef(({ className, invalid, inlineSize, type = "text", ...props }, ref) => {
1634
+ const classes = (0, _dsn_starter_kit_core.classNames)("dsn-text-input", inlineSize && `dsn-text-input--inline-size-${inlineSize}`, className);
1635
1635
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1636
1636
  ref,
1637
1637
  type,
@@ -1694,8 +1694,8 @@ FileInput.displayName = "FileInput";
1694
1694
  * <EmailInput invalid aria-invalid="true" aria-describedby="error" />
1695
1695
  * ```
1696
1696
  */
1697
- const EmailInput = react.default.forwardRef(({ className, invalid, width, autoComplete, ...props }, ref) => {
1698
- const classes = (0, _dsn_starter_kit_core.classNames)("dsn-text-input", width && `dsn-text-input--width-${width}`, className);
1697
+ const EmailInput = react.default.forwardRef(({ className, invalid, inlineSize, autoComplete, ...props }, ref) => {
1698
+ const classes = (0, _dsn_starter_kit_core.classNames)("dsn-text-input", inlineSize && `dsn-text-input--inline-size-${inlineSize}`, className);
1699
1699
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1700
1700
  ref,
1701
1701
  type: "email",
@@ -1731,8 +1731,8 @@ EmailInput.displayName = "EmailInput";
1731
1731
  * <PasswordInput invalid aria-invalid="true" aria-describedby="error" />
1732
1732
  * ```
1733
1733
  */
1734
- const PasswordInput = react.default.forwardRef(({ passwordAutocomplete = "current-password", className, invalid, width, autoComplete, ...props }, ref) => {
1735
- const classes = (0, _dsn_starter_kit_core.classNames)("dsn-text-input", width && `dsn-text-input--width-${width}`, className);
1734
+ const PasswordInput = react.default.forwardRef(({ passwordAutocomplete = "current-password", className, invalid, inlineSize, autoComplete, ...props }, ref) => {
1735
+ const classes = (0, _dsn_starter_kit_core.classNames)("dsn-text-input", inlineSize && `dsn-text-input--inline-size-${inlineSize}`, className);
1736
1736
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1737
1737
  ref,
1738
1738
  type: "password",
@@ -1762,8 +1762,8 @@ PasswordInput.displayName = "PasswordInput";
1762
1762
  * <NumberInput invalid aria-invalid="true" aria-describedby="error" />
1763
1763
  * ```
1764
1764
  */
1765
- const NumberInput = react.default.forwardRef(({ className, invalid, width, allowDecimals = false, ...props }, ref) => {
1766
- const classes = (0, _dsn_starter_kit_core.classNames)("dsn-text-input", width && `dsn-text-input--width-${width}`, className);
1765
+ const NumberInput = react.default.forwardRef(({ className, invalid, inlineSize, allowDecimals = false, ...props }, ref) => {
1766
+ const classes = (0, _dsn_starter_kit_core.classNames)("dsn-text-input", inlineSize && `dsn-text-input--inline-size-${inlineSize}`, className);
1767
1767
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1768
1768
  ref,
1769
1769
  type: "text",
@@ -1795,8 +1795,8 @@ NumberInput.displayName = "NumberInput";
1795
1795
  * <TelephoneInput invalid aria-invalid="true" aria-describedby="error" />
1796
1796
  * ```
1797
1797
  */
1798
- const TelephoneInput = react.default.forwardRef(({ className, invalid, width, autoComplete, ...props }, ref) => {
1799
- const classes = (0, _dsn_starter_kit_core.classNames)("dsn-text-input", width && `dsn-text-input--width-${width}`, className);
1798
+ const TelephoneInput = react.default.forwardRef(({ className, invalid, inlineSize = "md", autoComplete, ...props }, ref) => {
1799
+ const classes = (0, _dsn_starter_kit_core.classNames)("dsn-text-input", inlineSize && `dsn-text-input--inline-size-${inlineSize}`, className);
1800
1800
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1801
1801
  ref,
1802
1802
  type: "tel",
@@ -1828,8 +1828,8 @@ TelephoneInput.displayName = "TelephoneInput";
1828
1828
  * <SearchInput invalid aria-invalid="true" aria-describedby="error" />
1829
1829
  * ```
1830
1830
  */
1831
- const SearchInput = react.default.forwardRef(({ className, invalid, width, ...props }, ref) => {
1832
- const wrapperClasses = (0, _dsn_starter_kit_core.classNames)("dsn-search-input-wrapper", width && `dsn-search-input-wrapper--width-${width}`);
1831
+ const SearchInput = react.default.forwardRef(({ className, invalid, inlineSize, ...props }, ref) => {
1832
+ const wrapperClasses = (0, _dsn_starter_kit_core.classNames)("dsn-search-input-wrapper", inlineSize && `dsn-search-input-wrapper--inline-size-${inlineSize}`);
1833
1833
  const inputClasses = (0, _dsn_starter_kit_core.classNames)("dsn-text-input", "dsn-search-input", className);
1834
1834
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1835
1835
  className: wrapperClasses,
@@ -1852,7 +1852,7 @@ SearchInput.displayName = "SearchInput";
1852
1852
  /**
1853
1853
  * Time Input component
1854
1854
  * Time input with an interactive clock button at inline-end that opens the native time picker.
1855
- * Fixed width (sm) — not configurable, as time inputs have a predictable content width.
1855
+ * Fixed inline-size (sm): not configurable, as time inputs have a predictable content length.
1856
1856
  *
1857
1857
  * @example
1858
1858
  * ```tsx
@@ -1911,7 +1911,7 @@ TimeInput.displayName = "TimeInput";
1911
1911
  /**
1912
1912
  * Date Input component
1913
1913
  * Date input with an interactive calendar button at inline-end that opens the native date picker.
1914
- * Fixed width (sm) — not configurable, as date inputs have a predictable content width.
1914
+ * Fixed inline-size (md): not configurable, as date inputs have a predictable content length.
1915
1915
  *
1916
1916
  * @example
1917
1917
  * ```tsx
@@ -2024,7 +2024,7 @@ const DateInputGroup = react.default.forwardRef(({ className, value, onChange, i
2024
2024
  children: "Dag"
2025
2025
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumberInput, {
2026
2026
  id: dayId,
2027
- width: "xs",
2027
+ inlineSize: "xs",
2028
2028
  value: value?.day ?? "",
2029
2029
  onChange: handleChange("day"),
2030
2030
  invalid,
@@ -2043,7 +2043,7 @@ const DateInputGroup = react.default.forwardRef(({ className, value, onChange, i
2043
2043
  children: "Maand"
2044
2044
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumberInput, {
2045
2045
  id: monthId,
2046
- width: "xs",
2046
+ inlineSize: "xs",
2047
2047
  value: value?.month ?? "",
2048
2048
  onChange: handleChange("month"),
2049
2049
  invalid,
@@ -2062,7 +2062,7 @@ const DateInputGroup = react.default.forwardRef(({ className, value, onChange, i
2062
2062
  children: "Jaar"
2063
2063
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumberInput, {
2064
2064
  id: yearId,
2065
- width: "sm",
2065
+ inlineSize: "sm",
2066
2066
  value: value?.year ?? "",
2067
2067
  onChange: handleChange("year"),
2068
2068
  invalid,
@@ -2102,15 +2102,15 @@ DateInputGroup.displayName = "DateInputGroup";
2102
2102
  * </Select>
2103
2103
  * </FormField>
2104
2104
  *
2105
- * // Width variant
2106
- * <Select width="lg">...</Select>
2105
+ * // Inline-size variant
2106
+ * <Select inlineSize="lg">...</Select>
2107
2107
  *
2108
2108
  * // Invalid state
2109
2109
  * <Select invalid aria-invalid="true" aria-describedby="error">...</Select>
2110
2110
  * ```
2111
2111
  */
2112
- const Select = react.default.forwardRef(({ className, invalid, width, disabled, children, ...props }, ref) => {
2113
- const wrapperClasses = (0, _dsn_starter_kit_core.classNames)("dsn-select-wrapper", width && `dsn-select-wrapper--width-${width}`);
2112
+ const Select = react.default.forwardRef(({ className, invalid, inlineSize, disabled, children, ...props }, ref) => {
2113
+ const wrapperClasses = (0, _dsn_starter_kit_core.classNames)("dsn-select-wrapper", inlineSize && `dsn-select-wrapper--inline-size-${inlineSize}`);
2114
2114
  const selectClasses = (0, _dsn_starter_kit_core.classNames)("dsn-text-input", "dsn-select", className);
2115
2115
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2116
2116
  className: wrapperClasses,
@@ -2154,8 +2154,8 @@ Select.displayName = "Select";
2154
2154
  * <TextArea readOnly value="View only" rows={3} />
2155
2155
  * ```
2156
2156
  */
2157
- const TextArea = react.default.forwardRef(({ className, invalid, width, rows = 4, ...props }, ref) => {
2158
- const classes = (0, _dsn_starter_kit_core.classNames)("dsn-text-area", width && `dsn-text-area--width-${width}`, className);
2157
+ const TextArea = react.default.forwardRef(({ className, invalid, inlineSize, rows = 4, ...props }, ref) => {
2158
+ const classes = (0, _dsn_starter_kit_core.classNames)("dsn-text-area", inlineSize && `dsn-text-area--inline-size-${inlineSize}`, className);
2159
2159
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
2160
2160
  ref,
2161
2161
  className: classes,
@@ -3431,8 +3431,106 @@ const PageFooter = react.default.forwardRef(({ className, slot1, slot2, slot3, s
3431
3431
  });
3432
3432
  PageFooter.displayName = "PageFooter";
3433
3433
  //#endregion
3434
+ //#region src/utils/focusTrap.ts
3435
+ /**
3436
+ * Focus-trap hulpfuncties voor dialoogachtige componenten.
3437
+ *
3438
+ * Gebaseerd op de techniek van Hidde de Vries:
3439
+ * https://hidde.blog/using-javascript-to-trap-focus-in-an-element/
3440
+ *
3441
+ * Het native `<dialog>` element trapt de focus zelf zodra het via `.showModal()`
3442
+ * geopend wordt. Die native trap is echter niet overal betrouwbaar: zodra het
3443
+ * dialoogvenster in een `<iframe>` staat (Storybook, embeds, previews) laat
3444
+ * Safari de focus alsnog over de iframe-grens ontsnappen naar de pagina
3445
+ * eromheen. Deze expliciete trap staat daarbovenop en maakt het gedrag
3446
+ * browseronafhankelijk.
3447
+ */
3448
+ /**
3449
+ * Selector voor elementen die standaard in de tabvolgorde staan.
3450
+ * Uitgeschakelde controls en `tabindex="-1"` vallen er bewust buiten.
3451
+ */
3452
+ const FOCUSABLE_SELECTOR = [
3453
+ "a[href]",
3454
+ "area[href]",
3455
+ "button:not(:disabled)",
3456
+ "input:not(:disabled):not([type=\"hidden\"])",
3457
+ "select:not(:disabled)",
3458
+ "textarea:not(:disabled)",
3459
+ "details > summary:first-of-type",
3460
+ "iframe",
3461
+ "audio[controls]",
3462
+ "video[controls]",
3463
+ "[contenteditable]:not([contenteditable=\"false\"])",
3464
+ "[tabindex]:not([tabindex^=\"-\"])"
3465
+ ].join(", ");
3466
+ /**
3467
+ * Elementen die de focus niet kunnen krijgen omdat ze (of een voorouder)
3468
+ * verborgen of inert zijn. `[hidden]` en `[inert]` halen een element uit de
3469
+ * tabvolgorde, `[aria-hidden="true"]` hoort geen focusbare inhoud te bevatten.
3470
+ */
3471
+ function isReachable(element) {
3472
+ return !element.closest("[hidden], [inert], [aria-hidden=\"true\"]");
3473
+ }
3474
+ /**
3475
+ * Alle bereikbare focusbare elementen binnen een container, in DOM-volgorde.
3476
+ *
3477
+ * De trap staat of valt met die volgorde: het eerste en het laatste element
3478
+ * bepalen waar Tab en Shift+Tab omslaan. Daarom lopen we alle elementen langs
3479
+ * en toetsen we ze met `matches`, in plaats van `querySelectorAll` met de hele
3480
+ * selectorlijst. Browsers geven daar documentvolgorde bij terug, maar jsdom
3481
+ * groepeert het resultaat per losse selector, en dan zou de trap in tests
3482
+ * ander gedrag vertonen dan in het echt.
3483
+ */
3484
+ function getFocusableElements(container) {
3485
+ return Array.from(container.querySelectorAll("*")).filter((element) => element.matches(FOCUSABLE_SELECTOR) && isReachable(element));
3486
+ }
3487
+ /**
3488
+ * Houdt de toetsenbordfocus binnen `containerRef` zolang `active` waar is.
3489
+ *
3490
+ * De listener hangt aan de container zelf, niet aan het document: hij grijpt
3491
+ * alleen in wanneer de focus al binnen de container zit, en laat de rest van de
3492
+ * pagina met rust.
3493
+ *
3494
+ * Elementen met `tabindex="-1"` (zoals de heading die bij openen focus krijgt)
3495
+ * staan niet in de tabvolgorde. Vanaf zo'n element wrapt Shift+Tab naar het
3496
+ * laatste focusbare element, zodat de focus ook daar niet naar buiten valt.
3497
+ */
3498
+ function useFocusTrap(containerRef, active) {
3499
+ react.default.useEffect(() => {
3500
+ if (!active) return;
3501
+ const container = containerRef.current;
3502
+ if (!container) return;
3503
+ const handleKeyDown = (event) => {
3504
+ if (event.key !== "Tab") return;
3505
+ const focusable = getFocusableElements(container);
3506
+ if (focusable.length === 0) {
3507
+ event.preventDefault();
3508
+ return;
3509
+ }
3510
+ const first = focusable[0];
3511
+ const last = focusable[focusable.length - 1];
3512
+ const current = document.activeElement;
3513
+ const isInTabOrder = current ? focusable.includes(current) : false;
3514
+ if (event.shiftKey) {
3515
+ if (current === first || !isInTabOrder) {
3516
+ event.preventDefault();
3517
+ last.focus();
3518
+ }
3519
+ } else if (current === last) {
3520
+ event.preventDefault();
3521
+ first.focus();
3522
+ }
3523
+ };
3524
+ container.addEventListener("keydown", handleKeyDown);
3525
+ return () => container.removeEventListener("keydown", handleKeyDown);
3526
+ }, [active, containerRef]);
3527
+ }
3528
+ //#endregion
3434
3529
  //#region src/Drawer/Drawer.tsx
3435
- const DrawerContext = react.default.createContext({ headingId: "" });
3530
+ const DrawerContext = react.default.createContext({
3531
+ headingId: "",
3532
+ headingRef: { current: null }
3533
+ });
3436
3534
  /**
3437
3535
  * Drawer component
3438
3536
  * Zijpaneel dat vanuit links of rechts de viewport inschuift.
@@ -3459,22 +3557,33 @@ const DrawerContext = react.default.createContext({ headingId: "" });
3459
3557
  * </Drawer>
3460
3558
  * ```
3461
3559
  */
3462
- const Drawer = react.default.forwardRef(({ className, isOpen, onClose, modal = true, side = "right", children, ...props }, ref) => {
3560
+ const Drawer = react.default.forwardRef(({ className, isOpen, onClose, modal = true, triggerRef, side = "right", children, ...props }, ref) => {
3463
3561
  const internalRef = react.default.useRef(null);
3464
3562
  const dialogRef = ref ?? internalRef;
3465
3563
  const headingId = react.default.useId();
3564
+ const headingRef = react.default.useRef(null);
3466
3565
  react.default.useEffect(() => {
3467
3566
  const dialog = dialogRef.current;
3468
3567
  if (!dialog) return;
3469
3568
  if (isOpen && !dialog.open) {
3470
3569
  if (modal) dialog.showModal();
3471
3570
  else dialog.show();
3571
+ (headingRef.current ?? dialog).focus();
3472
3572
  } else if (!isOpen && dialog.open) dialog.close();
3473
3573
  }, [
3474
3574
  isOpen,
3475
3575
  modal,
3476
3576
  dialogRef
3477
3577
  ]);
3578
+ useFocusTrap(dialogRef, isOpen && modal);
3579
+ react.default.useEffect(() => {
3580
+ const trigger = triggerRef?.current;
3581
+ if (!trigger) return;
3582
+ trigger.setAttribute("aria-expanded", isOpen ? "true" : "false");
3583
+ return () => {
3584
+ trigger.setAttribute("aria-expanded", "false");
3585
+ };
3586
+ }, [isOpen, triggerRef]);
3478
3587
  react.default.useEffect(() => {
3479
3588
  if (modal) return;
3480
3589
  const dialog = dialogRef.current;
@@ -3498,6 +3607,7 @@ const Drawer = react.default.forwardRef(({ className, isOpen, onClose, modal = t
3498
3607
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DrawerContext.Provider, {
3499
3608
  value: {
3500
3609
  headingId,
3610
+ headingRef,
3501
3611
  onClose
3502
3612
  },
3503
3613
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dialog", {
@@ -3505,6 +3615,7 @@ const Drawer = react.default.forwardRef(({ className, isOpen, onClose, modal = t
3505
3615
  className: classes,
3506
3616
  "aria-labelledby": headingId,
3507
3617
  onCancel: handleCancel,
3618
+ tabIndex: -1,
3508
3619
  ...props,
3509
3620
  children
3510
3621
  })
@@ -3538,14 +3649,21 @@ DrawerHeader.displayName = "DrawerHeader";
3538
3649
  /**
3539
3650
  * DrawerHeading
3540
3651
  * De heading van het zijpaneel. ID wordt automatisch gegenereerd voor aria-labelledby.
3652
+ * Krijgt `tabindex="-1"` zodat de Drawer er bij openen de focus op kan zetten.
3541
3653
  */
3542
3654
  const DrawerHeading = react.default.forwardRef(({ className, level = 2, children, ...props }, ref) => {
3543
- const { headingId } = react.default.useContext(DrawerContext);
3655
+ const { headingId, headingRef } = react.default.useContext(DrawerContext);
3544
3656
  const Tag = `h${level}`;
3657
+ const setRef = react.default.useCallback((node) => {
3658
+ headingRef.current = node;
3659
+ if (typeof ref === "function") ref(node);
3660
+ else if (ref) ref.current = node;
3661
+ }, [headingRef, ref]);
3545
3662
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tag, {
3546
- ref,
3663
+ ref: setRef,
3547
3664
  id: headingId,
3548
3665
  className: (0, _dsn_starter_kit_core.classNames)("dsn-drawer-heading", className),
3666
+ tabIndex: -1,
3549
3667
  ...props,
3550
3668
  children
3551
3669
  });
@@ -3579,6 +3697,10 @@ const DrawerFooter = react.default.forwardRef(({ className, children, ...props }
3579
3697
  DrawerFooter.displayName = "DrawerFooter";
3580
3698
  //#endregion
3581
3699
  //#region src/PageHeader/PageHeader.tsx
3700
+ /** Auto-hide: boven deze scrollpositie (px) blijft de header altijd zichtbaar */
3701
+ const AUTO_HIDE_OFFSET = 100;
3702
+ /** Auto-hide: zoveel px omhoog scrollen voordat de header terugkomt */
3703
+ const AUTO_HIDE_REVEAL_DISTANCE = 64;
3582
3704
  /**
3583
3705
  * PageHeader component
3584
3706
  * Primaire navigatieheader voor een pagina. Mobile-first implementatie.
@@ -3626,11 +3748,24 @@ const PageHeader = react.default.forwardRef(({ className, logoSlot, sticky = "no
3626
3748
  react.default.useEffect(() => {
3627
3749
  if (sticky !== "auto-hide") return;
3628
3750
  let lastScrollY = window.scrollY;
3751
+ let scrolledUp = 0;
3629
3752
  const handleScroll = () => {
3630
3753
  const currentScrollY = window.scrollY;
3631
- const isScrollingDown = currentScrollY > lastScrollY && currentScrollY > 100;
3632
- combinedRef.current?.setAttribute("data-hidden", String(isScrollingDown));
3754
+ const delta = currentScrollY - lastScrollY;
3633
3755
  lastScrollY = currentScrollY;
3756
+ let hidden;
3757
+ if (currentScrollY <= AUTO_HIDE_OFFSET) {
3758
+ hidden = false;
3759
+ scrolledUp = 0;
3760
+ } else if (delta > 0) {
3761
+ hidden = true;
3762
+ scrolledUp = 0;
3763
+ } else if (delta < 0) {
3764
+ scrolledUp -= delta;
3765
+ if (scrolledUp < AUTO_HIDE_REVEAL_DISTANCE) return;
3766
+ hidden = false;
3767
+ } else return;
3768
+ combinedRef.current?.setAttribute("data-hidden", String(hidden));
3634
3769
  };
3635
3770
  window.addEventListener("scroll", handleScroll, { passive: true });
3636
3771
  return () => window.removeEventListener("scroll", handleScroll);
@@ -3827,6 +3962,7 @@ const PageHeader = react.default.forwardRef(({ className, logoSlot, sticky = "no
3827
3962
  isOpen: isMenuOpen,
3828
3963
  onClose: handleMenuClose,
3829
3964
  side: "left",
3965
+ triggerRef: menuButtonRef,
3830
3966
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DrawerHeader, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DrawerHeading, { children: menuButtonLabel }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DrawerBody, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Stack, {
3831
3967
  space: "5xl",
3832
3968
  children: [primaryNavigation && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("nav", {
@@ -4375,22 +4511,29 @@ Image.displayName = "Image";
4375
4511
  const CardContext = react.default.createContext({});
4376
4512
  /**
4377
4513
  * Card component
4378
- * Configureerbare container voor gestructureerde content met header, body en footer.
4379
- * Gebruikt een stretched-link techniek: de link in `CardHeading` dekt de volledige card.
4380
- * Screenreaders lezen alleen de heading-tekst als linknaam.
4514
+ * Configureerbare container met vier optionele secties: `CardPreHeader`,
4515
+ * `CardHeader`, `CardBody` en `CardFooter` (zie DR-2026-11).
4516
+ *
4517
+ * Zet `CardHeader` in de JSX altijd vóór `CardPreHeader`: zo kondigt een
4518
+ * screenreader de heading eerst aan. De CSS zet de pre-header visueel bovenaan.
4519
+ *
4520
+ * Met `href` wordt de heading een stretched link: de link dekt de volledige
4521
+ * card, en screenreaders lezen alleen de heading-tekst als linknaam.
4381
4522
  *
4382
4523
  * @example
4383
4524
  * ```tsx
4384
4525
  * <Card href="/artikel/slug">
4385
4526
  * <CardHeader>
4386
- * <Image src="/foto.jpg" alt="" width={800} height={450} ratio="16:9" />
4527
+ * <CardHeading level={2}>Artikeltitel</CardHeading>
4387
4528
  * </CardHeader>
4529
+ * <CardPreHeader>
4530
+ * <Image src="/foto.jpg" alt="" width={800} height={450} ratio="16:9" />
4531
+ * </CardPreHeader>
4388
4532
  * <CardBody>
4389
- * <CardHeading level={2}>Artikeltitel</CardHeading>
4390
4533
  * <Paragraph>Korte beschrijving.</Paragraph>
4391
4534
  * </CardBody>
4392
4535
  * <CardFooter>
4393
- * <Link href="/artikel/slug" aria-hidden tabIndex={-1}>Lees meer</Link>
4536
+ * <CardAffordance>Lees meer</CardAffordance>
4394
4537
  * </CardFooter>
4395
4538
  * </Card>
4396
4539
  * ```
@@ -4408,13 +4551,14 @@ const Card = react.default.forwardRef(({ className, href, children, ...props },
4408
4551
  });
4409
4552
  Card.displayName = "Card";
4410
4553
  /**
4411
- * CardHeader sub-component
4554
+ * CardPreHeader sub-component
4555
+ * Staat in de DOM ná `CardHeader` en komt visueel bovenaan via `order: -1`.
4412
4556
  * Zonder children → toont automatisch een afbeeldingsplaceholder.
4413
4557
  */
4414
- const CardHeader = react.default.forwardRef(({ className, children, ...props }, ref) => {
4558
+ const CardPreHeader = react.default.forwardRef(({ className, children, ...props }, ref) => {
4415
4559
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4416
4560
  ref,
4417
- className: (0, _dsn_starter_kit_core.classNames)("dsn-card__header", className),
4561
+ className: (0, _dsn_starter_kit_core.classNames)("dsn-card__pre-header", className),
4418
4562
  ...props,
4419
4563
  children: children ? children : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4420
4564
  className: "dsn-card__image-placeholder",
@@ -4422,10 +4566,24 @@ const CardHeader = react.default.forwardRef(({ className, children, ...props },
4422
4566
  })
4423
4567
  });
4424
4568
  });
4569
+ CardPreHeader.displayName = "CardPreHeader";
4570
+ /**
4571
+ * CardHeader sub-component
4572
+ * Bevat de heading of het label van de card. Zet hem in de JSX vóór
4573
+ * `CardPreHeader`.
4574
+ */
4575
+ const CardHeader = react.default.forwardRef(({ className, children, ...props }, ref) => {
4576
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4577
+ ref,
4578
+ className: (0, _dsn_starter_kit_core.classNames)("dsn-card__header", className),
4579
+ ...props,
4580
+ children
4581
+ });
4582
+ });
4425
4583
  CardHeader.displayName = "CardHeader";
4426
4584
  /**
4427
4585
  * CardBody sub-component
4428
- * Groeit via `flex: 1` zodat de footer altijd onderaan uitlijnt.
4586
+ * Inhoudssectie onder de header.
4429
4587
  */
4430
4588
  const CardBody = react.default.forwardRef(({ className, children, ...props }, ref) => {
4431
4589
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -4439,7 +4597,7 @@ CardBody.displayName = "CardBody";
4439
4597
  /**
4440
4598
  * CardHeading sub-component
4441
4599
  * Ontvangt `href` automatisch via React context van de parent `Card`.
4442
- * Wanneer `href` beschikbaar is, wraps de heading-tekst in een `<a class="dsn-card-heading__link">`.
4600
+ * Wanneer `href` beschikbaar is, staat de heading-tekst in een `<a class="dsn-card__link">`.
4443
4601
  * Het `::before` pseudo-element dekt de volledige card (stretched link).
4444
4602
  */
4445
4603
  const CardHeading = react.default.forwardRef(({ className, level = 2, children, ...props }, ref) => {
@@ -4447,21 +4605,67 @@ const CardHeading = react.default.forwardRef(({ className, level = 2, children,
4447
4605
  const Tag = `h${level}`;
4448
4606
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tag, {
4449
4607
  ref,
4450
- className: (0, _dsn_starter_kit_core.classNames)("dsn-card-heading", className),
4608
+ className: (0, _dsn_starter_kit_core.classNames)("dsn-card__heading", className),
4451
4609
  ...props,
4452
4610
  children: href ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
4453
4611
  href,
4454
- className: "dsn-card-heading__link",
4612
+ className: "dsn-card__link",
4455
4613
  children
4456
4614
  }) : children
4457
4615
  });
4458
4616
  });
4459
4617
  CardHeading.displayName = "CardHeading";
4460
4618
  /**
4619
+ * CardLabel sub-component
4620
+ * Gebruik in plaats van `CardHeading` wanneer de card geen nieuw onderwerp
4621
+ * introduceert, zoals bij login-opties of instellingen. Ontvangt `href` via
4622
+ * context, net als `CardHeading`.
4623
+ */
4624
+ const CardLabel = react.default.forwardRef(({ className, children, ...props }, ref) => {
4625
+ const { href } = react.default.useContext(CardContext);
4626
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4627
+ ref,
4628
+ className: (0, _dsn_starter_kit_core.classNames)("dsn-card__label", className),
4629
+ ...props,
4630
+ children: href ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
4631
+ href,
4632
+ className: "dsn-card__link",
4633
+ children
4634
+ }) : children
4635
+ });
4636
+ });
4637
+ CardLabel.displayName = "CardLabel";
4638
+ /**
4639
+ * CardDescription sub-component
4640
+ * Korte omschrijving met eigen typografie-tokens.
4641
+ */
4642
+ const CardDescription = react.default.forwardRef(({ className, children, ...props }, ref) => {
4643
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4644
+ ref,
4645
+ className: (0, _dsn_starter_kit_core.classNames)("dsn-card__description", className),
4646
+ ...props,
4647
+ children
4648
+ });
4649
+ });
4650
+ CardDescription.displayName = "CardDescription";
4651
+ /**
4652
+ * CardMeta sub-component
4653
+ * Aanvullende gegevens bij de card, zoals een datum.
4654
+ */
4655
+ const CardMeta = react.default.forwardRef(({ className, children, ...props }, ref) => {
4656
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4657
+ ref,
4658
+ className: (0, _dsn_starter_kit_core.classNames)("dsn-card__meta", className),
4659
+ ...props,
4660
+ children
4661
+ });
4662
+ });
4663
+ CardMeta.displayName = "CardMeta";
4664
+ /**
4461
4665
  * CardFooter sub-component
4462
- * Directe kinderen staan boven de stretched link via `z-index: 2` in CSS.
4463
- * Gebruik `aria-hidden` + `tabIndex={-1}` op de link wanneer deze dezelfde
4464
- * bestemming heeft als de card — vermijdt een dubbele tabstop.
4666
+ * Staat altijd onderaan de card. Links en buttons staan boven de stretched
4667
+ * link en blijven zelfstandig klikbaar. Voor een visuele "Lees meer" naar
4668
+ * dezelfde bestemming als de card: gebruik `CardAffordance`.
4465
4669
  */
4466
4670
  const CardFooter = react.default.forwardRef(({ className, children, ...props }, ref) => {
4467
4671
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -4473,6 +4677,21 @@ const CardFooter = react.default.forwardRef(({ className, children, ...props },
4473
4677
  });
4474
4678
  CardFooter.displayName = "CardFooter";
4475
4679
  /**
4680
+ * CardAffordance sub-component
4681
+ * Een `<span aria-hidden="true">` die eruitziet als een link. De stretched
4682
+ * link vangt de klik al af, dus er is geen tweede tabstop nodig.
4683
+ */
4684
+ const CardAffordance = react.default.forwardRef(({ className, children, ...props }, ref) => {
4685
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4686
+ ref,
4687
+ className: (0, _dsn_starter_kit_core.classNames)("dsn-card__affordance", className),
4688
+ "aria-hidden": "true",
4689
+ ...props,
4690
+ children
4691
+ });
4692
+ });
4693
+ CardAffordance.displayName = "CardAffordance";
4694
+ /**
4476
4695
  * CardGroup sub-component
4477
4696
  * Flexbox wrapper die gelijke hoogte garandeert en de footer in elke card onderaan uitlijnt.
4478
4697
  * Rendert `role="list"` wanneer `as="ul"` — nodig omdat CSS-resets de lijstsemantiek verwijderen.
@@ -4490,13 +4709,18 @@ const CardGroup = react.default.forwardRef(({ className, as: Tag = "ul", childre
4490
4709
  CardGroup.displayName = "CardGroup";
4491
4710
  //#endregion
4492
4711
  //#region src/ModalDialog/ModalDialog.tsx
4493
- const ModalDialogContext = react.default.createContext({ headingId: "" });
4712
+ const ModalDialogContext = react.default.createContext({
4713
+ headingId: "",
4714
+ headingRef: { current: null }
4715
+ });
4494
4716
  /**
4495
4717
  * ModalDialog component
4496
4718
  * Modaal dialoogvenster gebaseerd op het native `<dialog>` element.
4497
4719
  *
4498
- * Gebruik altijd `.showModal()` (intern afgehandeld via `isOpen` prop) — nooit `.show()`.
4499
- * Dit garandeert natieve focus-trap, aria-modal semantiek en inert-attribuut op de achtergrond.
4720
+ * Gebruik altijd `.showModal()` (intern afgehandeld via `isOpen` prop), nooit `.show()`.
4721
+ * Dit garandeert aria-modal semantiek en het inert-attribuut op de achtergrond.
4722
+ * De focus blijft binnen het venster via de native focus-trap plus een
4723
+ * expliciete trap, en komt bij openen op de heading te staan.
4500
4724
  *
4501
4725
  * @example
4502
4726
  * ```tsx
@@ -4516,16 +4740,28 @@ const ModalDialogContext = react.default.createContext({ headingId: "" });
4516
4740
  * </ModalDialog>
4517
4741
  * ```
4518
4742
  */
4519
- const ModalDialog = react.default.forwardRef(({ className, isOpen, onClose, children, ...props }, ref) => {
4743
+ const ModalDialog = react.default.forwardRef(({ className, isOpen, onClose, triggerRef, children, ...props }, ref) => {
4520
4744
  const internalRef = react.default.useRef(null);
4521
4745
  const dialogRef = ref ?? internalRef;
4522
4746
  const headingId = react.default.useId();
4747
+ const headingRef = react.default.useRef(null);
4523
4748
  react.default.useEffect(() => {
4524
4749
  const dialog = dialogRef.current;
4525
4750
  if (!dialog) return;
4526
- if (isOpen && !dialog.open) dialog.showModal();
4527
- else if (!isOpen && dialog.open) dialog.close();
4751
+ if (isOpen && !dialog.open) {
4752
+ dialog.showModal();
4753
+ (headingRef.current ?? dialog).focus();
4754
+ } else if (!isOpen && dialog.open) dialog.close();
4528
4755
  }, [isOpen, dialogRef]);
4756
+ useFocusTrap(dialogRef, isOpen);
4757
+ react.default.useEffect(() => {
4758
+ const trigger = triggerRef?.current;
4759
+ if (!trigger) return;
4760
+ trigger.setAttribute("aria-expanded", isOpen ? "true" : "false");
4761
+ return () => {
4762
+ trigger.setAttribute("aria-expanded", "false");
4763
+ };
4764
+ }, [isOpen, triggerRef]);
4529
4765
  const handleCancel = (event) => {
4530
4766
  event.preventDefault();
4531
4767
  onClose?.();
@@ -4534,6 +4770,7 @@ const ModalDialog = react.default.forwardRef(({ className, isOpen, onClose, chil
4534
4770
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModalDialogContext.Provider, {
4535
4771
  value: {
4536
4772
  headingId,
4773
+ headingRef,
4537
4774
  onClose
4538
4775
  },
4539
4776
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dialog", {
@@ -4541,6 +4778,7 @@ const ModalDialog = react.default.forwardRef(({ className, isOpen, onClose, chil
4541
4778
  className: classes,
4542
4779
  "aria-labelledby": headingId,
4543
4780
  onCancel: handleCancel,
4781
+ tabIndex: -1,
4544
4782
  ...props,
4545
4783
  children
4546
4784
  })
@@ -4574,14 +4812,21 @@ ModalDialogHeader.displayName = "ModalDialogHeader";
4574
4812
  /**
4575
4813
  * ModalDialogHeading
4576
4814
  * De heading van het dialoogvenster. ID wordt automatisch gegenereerd voor aria-labelledby.
4815
+ * Krijgt `tabindex="-1"` zodat het ModalDialog er bij openen de focus op kan zetten.
4577
4816
  */
4578
4817
  const ModalDialogHeading = react.default.forwardRef(({ className, level = 2, children, ...props }, ref) => {
4579
- const { headingId } = react.default.useContext(ModalDialogContext);
4818
+ const { headingId, headingRef } = react.default.useContext(ModalDialogContext);
4580
4819
  const Tag = `h${level}`;
4820
+ const setRef = react.default.useCallback((node) => {
4821
+ headingRef.current = node;
4822
+ if (typeof ref === "function") ref(node);
4823
+ else if (ref) ref.current = node;
4824
+ }, [headingRef, ref]);
4581
4825
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tag, {
4582
- ref,
4826
+ ref: setRef,
4583
4827
  id: headingId,
4584
4828
  className: (0, _dsn_starter_kit_core.classNames)("dsn-modal-dialog-heading", className),
4829
+ tabIndex: -1,
4585
4830
  ...props,
4586
4831
  children
4587
4832
  });
@@ -5036,11 +5281,16 @@ exports.BreakoutSection = BreakoutSection;
5036
5281
  exports.Button = Button;
5037
5282
  exports.ButtonLink = ButtonLink;
5038
5283
  exports.Card = Card;
5284
+ exports.CardAffordance = CardAffordance;
5039
5285
  exports.CardBody = CardBody;
5286
+ exports.CardDescription = CardDescription;
5040
5287
  exports.CardFooter = CardFooter;
5041
5288
  exports.CardGroup = CardGroup;
5042
5289
  exports.CardHeader = CardHeader;
5043
5290
  exports.CardHeading = CardHeading;
5291
+ exports.CardLabel = CardLabel;
5292
+ exports.CardMeta = CardMeta;
5293
+ exports.CardPreHeader = CardPreHeader;
5044
5294
  exports.Checkbox = Checkbox;
5045
5295
  exports.CheckboxGroup = CheckboxGroup;
5046
5296
  exports.CheckboxOption = CheckboxOption;