@dsn-starter-kit/components-react 3.5.0 → 4.1.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 (77) 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/DateInput2.mjs +1 -1
  8. package/dist/DateInput/DateInput2.mjs.map +1 -1
  9. package/dist/DateInputGroup/DateInputGroup2.mjs +3 -3
  10. package/dist/DateInputGroup/DateInputGroup2.mjs.map +1 -1
  11. package/dist/Drawer/Drawer.d.mts +5 -2
  12. package/dist/Drawer/Drawer.d.mts.map +1 -1
  13. package/dist/Drawer/Drawer2.mjs +18 -3
  14. package/dist/Drawer/Drawer2.mjs.map +1 -1
  15. package/dist/EmailInput/EmailInput.d.mts +10 -5
  16. package/dist/EmailInput/EmailInput.d.mts.map +1 -1
  17. package/dist/EmailInput/EmailInput2.mjs +2 -2
  18. package/dist/EmailInput/EmailInput2.mjs.map +1 -1
  19. package/dist/ModalDialog/ModalDialog.d.mts +13 -3
  20. package/dist/ModalDialog/ModalDialog.d.mts.map +1 -1
  21. package/dist/ModalDialog/ModalDialog2.mjs +10 -3
  22. package/dist/ModalDialog/ModalDialog2.mjs.map +1 -1
  23. package/dist/NumberInput/NumberInput.d.mts +4 -4
  24. package/dist/NumberInput/NumberInput.d.mts.map +1 -1
  25. package/dist/NumberInput/NumberInput2.mjs +2 -2
  26. package/dist/NumberInput/NumberInput2.mjs.map +1 -1
  27. package/dist/PageHeader/PageHeader.d.mts.map +1 -1
  28. package/dist/PageHeader/PageHeader2.mjs +19 -2
  29. package/dist/PageHeader/PageHeader2.mjs.map +1 -1
  30. package/dist/PasswordInput/PasswordInput.d.mts +10 -5
  31. package/dist/PasswordInput/PasswordInput.d.mts.map +1 -1
  32. package/dist/PasswordInput/PasswordInput2.mjs +2 -2
  33. package/dist/PasswordInput/PasswordInput2.mjs.map +1 -1
  34. package/dist/Popover/Popover.d.mts +16 -23
  35. package/dist/Popover/Popover.d.mts.map +1 -1
  36. package/dist/Popover/Popover2.mjs +33 -119
  37. package/dist/Popover/Popover2.mjs.map +1 -1
  38. package/dist/PopoverMenu/PopoverMenu.d.mts +62 -0
  39. package/dist/PopoverMenu/PopoverMenu.d.mts.map +1 -0
  40. package/dist/PopoverMenu/PopoverMenu2.mjs +66 -0
  41. package/dist/PopoverMenu/PopoverMenu2.mjs.map +1 -0
  42. package/dist/PopoverMenu/index.d.mts +2 -0
  43. package/dist/SearchInput/SearchInput.d.mts +10 -5
  44. package/dist/SearchInput/SearchInput.d.mts.map +1 -1
  45. package/dist/SearchInput/SearchInput2.mjs +2 -2
  46. package/dist/SearchInput/SearchInput2.mjs.map +1 -1
  47. package/dist/Select/Select.d.mts +6 -6
  48. package/dist/Select/Select.d.mts.map +1 -1
  49. package/dist/Select/Select2.mjs +4 -4
  50. package/dist/Select/Select2.mjs.map +1 -1
  51. package/dist/TelephoneInput/TelephoneInput.d.mts +10 -5
  52. package/dist/TelephoneInput/TelephoneInput.d.mts.map +1 -1
  53. package/dist/TelephoneInput/TelephoneInput2.mjs +2 -2
  54. package/dist/TelephoneInput/TelephoneInput2.mjs.map +1 -1
  55. package/dist/TextArea/TextArea.d.mts +10 -5
  56. package/dist/TextArea/TextArea.d.mts.map +1 -1
  57. package/dist/TextArea/TextArea2.mjs +2 -2
  58. package/dist/TextArea/TextArea2.mjs.map +1 -1
  59. package/dist/TextInput/TextInput.d.mts +4 -4
  60. package/dist/TextInput/TextInput.d.mts.map +1 -1
  61. package/dist/TextInput/TextInput2.mjs +2 -2
  62. package/dist/TextInput/TextInput2.mjs.map +1 -1
  63. package/dist/TimeInput/TimeInput.d.mts +1 -1
  64. package/dist/TimeInput/TimeInput2.mjs +1 -1
  65. package/dist/TimeInput/TimeInput2.mjs.map +1 -1
  66. package/dist/index.cjs +315 -79
  67. package/dist/index.cjs.map +1 -1
  68. package/dist/index.css +5950 -5217
  69. package/dist/index.d.cts +243 -66
  70. package/dist/index.d.cts.map +1 -1
  71. package/dist/index.d.mts +4 -2
  72. package/dist/index.mjs +3 -2
  73. package/dist/utils/popover.d.mts +27 -0
  74. package/dist/utils/popover.d.mts.map +1 -0
  75. package/dist/utils/popover.mjs +142 -0
  76. package/dist/utils/popover.mjs.map +1 -0
  77. package/package.json +3 -3
@@ -1 +1 @@
1
- {"version":3,"file":"Drawer2.mjs","names":[],"sources":["../../src/Drawer/Drawer.tsx"],"sourcesContent":["import React from 'react';\nimport { classNames } from '@dsn-starter-kit/core';\nimport { Button } from '../Button';\nimport { Icon } from '../Icon';\nimport { useFocusTrap } from '../utils/focusTrap';\nimport './Drawer.css';\n\n// =============================================================================\n// Context\n// =============================================================================\n\ninterface DrawerContextValue {\n headingId: string;\n headingRef: React.MutableRefObject<HTMLHeadingElement | null>;\n onClose?: () => void;\n}\n\nconst DrawerContext = React.createContext<DrawerContextValue>({\n headingId: '',\n headingRef: { current: null },\n});\n\n// =============================================================================\n// Drawer (root)\n// =============================================================================\n\nexport interface DrawerProps extends Omit<\n React.HTMLAttributes<HTMLDialogElement>,\n 'children'\n> {\n /**\n * Bepaalt of het zijpaneel getoond wordt.\n */\n isOpen: boolean;\n\n /**\n * Callback die wordt aangeroepen wanneer het zijpaneel sluit\n * (sluitknop, Escape-toets).\n */\n onClose?: () => void;\n\n /**\n * Modaal of non-modaal gedrag.\n * - `true` (standaard): opent via `.showModal()` — achtergrond geblokkeerd,\n * focus blijft binnen het paneel, Escape sluit via `cancel`-event.\n * - `false`: opent via `.show()` — achtergrond blijft interactief en de\n * gebruiker kan er met Tab naartoe, Escape sluit via `keydown`-listener.\n * @default true\n */\n modal?: boolean;\n\n /**\n * Ref naar het element dat het zijpaneel opent. Wanneer je die meegeeft,\n * houdt het zijpaneel `aria-expanded` op dat element synchroon met de\n * open-staat: `false` wanneer het paneel gesloten is, `true` wanneer het\n * open staat.\n */\n triggerRef?: React.RefObject<HTMLElement | null>;\n\n /**\n * De kant van de viewport vanwaar het zijpaneel inschuift.\n * @default \"right\"\n */\n side?: 'right' | 'left';\n\n /**\n * De subcomponenten van het zijpaneel:\n * `DrawerHeader`, `DrawerBody`, `DrawerFooter`\n */\n children?: React.ReactNode;\n}\n\n/**\n * Drawer component\n * Zijpaneel dat vanuit links of rechts de viewport inschuift.\n *\n * Gebruik `modal={true}` (standaard) voor gefocuste taken waarbij de achtergrond\n * geblokkeerd moet worden. Gebruik `modal={false}` als de gebruiker de achtergrondpagina\n * nodig heeft voor context.\n *\n * @example\n * ```tsx\n * <Drawer isOpen={isOpen} onClose={() => setIsOpen(false)}>\n * <DrawerHeader>\n * <DrawerHeading>Filteropties</DrawerHeading>\n * </DrawerHeader>\n * <DrawerBody>\n * <Paragraph>Inhoud van het zijpaneel</Paragraph>\n * </DrawerBody>\n * <DrawerFooter>\n * <ActionGroup>\n * <Button variant=\"strong\" onClick={() => setIsOpen(false)}>Toepassen</Button>\n * <Button variant=\"default\" onClick={() => setIsOpen(false)}>Annuleren</Button>\n * </ActionGroup>\n * </DrawerFooter>\n * </Drawer>\n * ```\n */\nexport const Drawer = React.forwardRef<HTMLDialogElement, DrawerProps>(\n (\n {\n className,\n isOpen,\n onClose,\n modal = true,\n triggerRef,\n side = 'right',\n children,\n ...props\n },\n ref\n ) => {\n const internalRef = React.useRef<HTMLDialogElement>(null);\n const dialogRef =\n (ref as React.RefObject<HTMLDialogElement>) ?? internalRef;\n const headingId = React.useId();\n const headingRef = React.useRef<HTMLHeadingElement | null>(null);\n\n React.useEffect(() => {\n const dialog = dialogRef.current;\n if (!dialog) return;\n\n if (isOpen && !dialog.open) {\n if (modal) {\n dialog.showModal();\n } else {\n dialog.show();\n }\n\n /*\n * Zet de focus op de heading, niet op de sluitknop. De browser zet de\n * focus bij het openen zelf op het eerste focusbare element, en dat is\n * de sluitknop. VoiceOver in Safari leest die knop dan wel voor, maar\n * niet de titel uit `aria-labelledby`, en omdat de sluitknop na de\n * titel staat kom je de titel bij verder lezen ook niet meer tegen.\n *\n * Met de focus op de heading (die `tabindex=\"-1\"` heeft) wordt de\n * titel als eerste voorgelezen en loopt de leesvolgorde daarna door\n * naar de sluitknop en de inhoud. Dit is het patroon uit de ARIA\n * Authoring Practices, en het houdt DOM-volgorde en visuele volgorde\n * gelijk.\n */\n (headingRef.current ?? dialog).focus();\n } else if (!isOpen && dialog.open) {\n dialog.close();\n }\n }, [isOpen, modal, dialogRef]);\n\n /*\n * Expliciete focus-trap bovenop de native trap van `.showModal()`.\n * Zie `utils/focusTrap.ts` voor waarom de native trap niet volstaat.\n * Non-modaal krijgt bewust geen trap: daar hoort de gebruiker juist\n * tussen paneel en achtergrondpagina te kunnen tabben.\n */\n useFocusTrap(dialogRef, isOpen && modal);\n\n // Synchroniseer aria-expanded op het triggerelement\n React.useEffect(() => {\n const trigger = triggerRef?.current;\n if (!trigger) return;\n trigger.setAttribute('aria-expanded', isOpen ? 'true' : 'false');\n return () => {\n trigger.setAttribute('aria-expanded', 'false');\n };\n }, [isOpen, triggerRef]);\n\n // Non-modaal: handmatige Escape-afhandeling via keydown\n React.useEffect(() => {\n if (modal) return;\n const dialog = dialogRef.current;\n if (!dialog || !isOpen) return;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n onClose?.();\n }\n };\n\n dialog.addEventListener('keydown', handleKeyDown);\n return () => dialog.removeEventListener('keydown', handleKeyDown);\n }, [modal, isOpen, onClose, dialogRef]);\n\n // Modaal: native cancel-event (Escape via browser)\n const handleCancel = (event: React.SyntheticEvent<HTMLDialogElement>) => {\n event.preventDefault();\n onClose?.();\n };\n\n const classes = classNames(\n 'dsn-drawer',\n side === 'left' ? 'dsn-drawer--side-left' : 'dsn-drawer--side-right',\n className\n );\n\n return (\n <DrawerContext.Provider value={{ headingId, headingRef, onClose }}>\n <dialog\n ref={dialogRef}\n className={classes}\n aria-labelledby={headingId}\n onCancel={handleCancel}\n tabIndex={-1}\n {...props}\n >\n {children}\n </dialog>\n </DrawerContext.Provider>\n );\n }\n);\n\nDrawer.displayName = 'Drawer';\n\n// =============================================================================\n// DrawerHeader\n// =============================================================================\n\nexport interface DrawerHeaderProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * Inhoud van de header — doorgaans een `DrawerHeading`\n */\n children?: React.ReactNode;\n}\n\n/**\n * DrawerHeader\n * De headerstrook van het zijpaneel met heading en sluitknop.\n */\nexport const DrawerHeader = React.forwardRef<HTMLDivElement, DrawerHeaderProps>(\n ({ className, children, ...props }, ref) => {\n const { onClose } = React.useContext(DrawerContext);\n\n return (\n <div\n ref={ref}\n className={classNames('dsn-drawer__header', className)}\n {...props}\n >\n {children}\n <Button\n variant=\"subtle\"\n size=\"small\"\n iconOnly\n onClick={onClose}\n iconStart={<Icon name=\"x\" aria-hidden />}\n >\n Sluiten\n </Button>\n </div>\n );\n }\n);\n\nDrawerHeader.displayName = 'DrawerHeader';\n\n// =============================================================================\n// DrawerHeading\n// =============================================================================\n\nexport interface DrawerHeadingProps extends React.HTMLAttributes<HTMLHeadingElement> {\n /**\n * Semantisch heading-niveau. Kies op basis van documenthiërarchie, niet visuele grootte.\n * @default 2\n */\n level?: 1 | 2 | 3 | 4 | 5 | 6;\n\n /**\n * De zichtbare heading-tekst\n */\n children?: React.ReactNode;\n}\n\n/**\n * DrawerHeading\n * De heading van het zijpaneel. ID wordt automatisch gegenereerd voor aria-labelledby.\n * Krijgt `tabindex=\"-1\"` zodat de Drawer er bij openen de focus op kan zetten.\n */\nexport const DrawerHeading = React.forwardRef<\n HTMLHeadingElement,\n DrawerHeadingProps\n>(({ className, level = 2, children, ...props }, ref) => {\n const { headingId, headingRef } = React.useContext(DrawerContext);\n const Tag = `h${level}` as React.ElementType;\n\n // De Drawer heeft de heading nodig om er bij openen de focus op te zetten;\n // een eventuele meegegeven ref blijft daarnaast gewoon werken.\n const setRef = React.useCallback(\n (node: HTMLHeadingElement | null) => {\n headingRef.current = node;\n if (typeof ref === 'function') {\n ref(node);\n } else if (ref) {\n (ref as React.MutableRefObject<HTMLHeadingElement | null>).current =\n node;\n }\n },\n [headingRef, ref]\n );\n\n return (\n <Tag\n ref={setRef}\n id={headingId}\n className={classNames('dsn-drawer-heading', className)}\n tabIndex={-1}\n {...props}\n >\n {children}\n </Tag>\n );\n});\n\nDrawerHeading.displayName = 'DrawerHeading';\n\n// =============================================================================\n// DrawerBody\n// =============================================================================\n\nexport interface DrawerBodyProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * De hoofdinhoud van het zijpaneel\n */\n children?: React.ReactNode;\n}\n\n/**\n * DrawerBody\n * De scrollbare inhoudssectie van het zijpaneel met scroll-affordance schaduw.\n */\nexport const DrawerBody = React.forwardRef<HTMLDivElement, DrawerBodyProps>(\n ({ className, children, ...props }, ref) => {\n return (\n <div\n ref={ref}\n className={classNames('dsn-drawer__body', className)}\n {...props}\n >\n {children}\n </div>\n );\n }\n);\n\nDrawerBody.displayName = 'DrawerBody';\n\n// =============================================================================\n// DrawerFooter\n// =============================================================================\n\nexport interface DrawerFooterProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * De acties van het zijpaneel — doorgaans een `ActionGroup` met knoppen\n */\n children?: React.ReactNode;\n}\n\n/**\n * DrawerFooter\n * De voettekst van het zijpaneel met actieknoppen.\n */\nexport const DrawerFooter = React.forwardRef<HTMLDivElement, DrawerFooterProps>(\n ({ className, children, ...props }, ref) => {\n return (\n <div\n ref={ref}\n className={classNames('dsn-drawer__footer', className)}\n {...props}\n >\n {children}\n </div>\n );\n }\n);\n\nDrawerFooter.displayName = 'DrawerFooter';\n"],"mappings":"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"}
1
+ {"version":3,"file":"Drawer2.mjs","names":[],"sources":["../../src/Drawer/Drawer.tsx"],"sourcesContent":["import React from 'react';\nimport { classNames } from '@dsn-starter-kit/core';\nimport { Button } from '../Button';\nimport { Icon } from '../Icon';\nimport { useFocusTrap } from '../utils/focusTrap';\nimport './Drawer.css';\n\n// =============================================================================\n// Context\n// =============================================================================\n\ninterface DrawerContextValue {\n headingId: string;\n headingRef: React.MutableRefObject<HTMLHeadingElement | null>;\n onClose?: () => void;\n}\n\nconst DrawerContext = React.createContext<DrawerContextValue>({\n headingId: '',\n headingRef: { current: null },\n});\n\n// =============================================================================\n// Drawer (root)\n// =============================================================================\n\nexport interface DrawerProps extends Omit<\n React.HTMLAttributes<HTMLDialogElement>,\n 'children'\n> {\n /**\n * Bepaalt of het zijpaneel getoond wordt.\n */\n isOpen: boolean;\n\n /**\n * Callback die wordt aangeroepen wanneer het zijpaneel sluit\n * (sluitknop, Escape-toets).\n */\n onClose?: () => void;\n\n /**\n * Modaal of non-modaal gedrag.\n * - `true` (standaard): opent via `.showModal()` — achtergrond geblokkeerd,\n * focus blijft binnen het paneel, Escape sluit via `cancel`-event.\n * - `false`: `<dialog popover=\"manual\">`, opent via `.showPopover()` —\n * achtergrond blijft interactief en de gebruiker kan er met Tab naartoe,\n * Escape sluit via `keydown`-listener. Het paneel staat in de top layer,\n * dus een sticky header of een voorouder met `transform` valt er niet\n * overheen.\n * @default true\n */\n modal?: boolean;\n\n /**\n * Ref naar het element dat het zijpaneel opent. Wanneer je die meegeeft,\n * houdt het zijpaneel `aria-expanded` op dat element synchroon met de\n * open-staat: `false` wanneer het paneel gesloten is, `true` wanneer het\n * open staat.\n */\n triggerRef?: React.RefObject<HTMLElement | null>;\n\n /**\n * De kant van de viewport vanwaar het zijpaneel inschuift.\n * @default \"right\"\n */\n side?: 'right' | 'left';\n\n /**\n * De subcomponenten van het zijpaneel:\n * `DrawerHeader`, `DrawerBody`, `DrawerFooter`\n */\n children?: React.ReactNode;\n}\n\n/**\n * Drawer component\n * Zijpaneel dat vanuit links of rechts de viewport inschuift.\n *\n * Gebruik `modal={true}` (standaard) voor gefocuste taken waarbij de achtergrond\n * geblokkeerd moet worden. Gebruik `modal={false}` als de gebruiker de achtergrondpagina\n * nodig heeft voor context.\n *\n * @example\n * ```tsx\n * <Drawer isOpen={isOpen} onClose={() => setIsOpen(false)}>\n * <DrawerHeader>\n * <DrawerHeading>Filteropties</DrawerHeading>\n * </DrawerHeader>\n * <DrawerBody>\n * <Paragraph>Inhoud van het zijpaneel</Paragraph>\n * </DrawerBody>\n * <DrawerFooter>\n * <ActionGroup>\n * <Button variant=\"strong\" onClick={() => setIsOpen(false)}>Toepassen</Button>\n * <Button variant=\"default\" onClick={() => setIsOpen(false)}>Annuleren</Button>\n * </ActionGroup>\n * </DrawerFooter>\n * </Drawer>\n * ```\n */\nexport const Drawer = React.forwardRef<HTMLDialogElement, DrawerProps>(\n (\n {\n className,\n isOpen,\n onClose,\n modal = true,\n triggerRef,\n side = 'right',\n children,\n ...props\n },\n ref\n ) => {\n const internalRef = React.useRef<HTMLDialogElement>(null);\n const dialogRef =\n (ref as React.RefObject<HTMLDialogElement>) ?? internalRef;\n const headingId = React.useId();\n const headingRef = React.useRef<HTMLHeadingElement | null>(null);\n\n /*\n * De non-modale variant is een popover (`popover=\"manual\"`) en geen via\n * `.show()` geopende dialog. `.show()` laat het paneel in de gewone\n * stapelvolgorde staan, waar een sticky PageHeader of een voorouder met\n * `transform` het kan afdekken; `.showPopover()` zet het in de top layer\n * zonder de rest van de pagina inert te maken.\n *\n * Een popover zet geen `open`-attribuut, dus die staat houden we zelf bij.\n * En anders dan `dialog.close()` herstelt `hidePopover()` de focus hier\n * niet: die blijft op een verborgen element staan. Daarom onthouden we bij\n * openen waar de focus stond, en zetten hem daar bij sluiten terug.\n */\n const popoverShownRef = React.useRef(false);\n const previousFocusRef = React.useRef<HTMLElement | null>(null);\n\n React.useEffect(() => {\n const dialog = dialogRef.current;\n if (!dialog) return;\n\n const isShown = dialog.open || popoverShownRef.current;\n\n if (isOpen && !isShown) {\n if (modal) {\n dialog.showModal();\n } else {\n previousFocusRef.current =\n document.activeElement instanceof HTMLElement\n ? document.activeElement\n : null;\n dialog.showPopover();\n popoverShownRef.current = true;\n }\n\n /*\n * Zet de focus op de heading, niet op de sluitknop. De browser zet de\n * focus bij het openen zelf op het eerste focusbare element, en dat is\n * de sluitknop. VoiceOver in Safari leest die knop dan wel voor, maar\n * niet de titel uit `aria-labelledby`, en omdat de sluitknop na de\n * titel staat kom je de titel bij verder lezen ook niet meer tegen.\n *\n * Met de focus op de heading (die `tabindex=\"-1\"` heeft) wordt de\n * titel als eerste voorgelezen en loopt de leesvolgorde daarna door\n * naar de sluitknop en de inhoud. Dit is het patroon uit de ARIA\n * Authoring Practices, en het houdt DOM-volgorde en visuele volgorde\n * gelijk.\n *\n * Waarom niet het eerste veld of de minst destructieve knop: zie\n * docs/decisions/DR-2026-12-dialogs-focus-bij-openen-op-de-heading.md.\n */\n (headingRef.current ?? dialog).focus();\n } else if (!isOpen && isShown) {\n if (popoverShownRef.current) {\n // Alleen terugzetten als de focus nog in het paneel stond; heeft de\n // gebruiker intussen op de pagina ernaast geklikt, dan blijft hij daar.\n const focusWasInside = dialog.contains(document.activeElement);\n dialog.hidePopover();\n popoverShownRef.current = false;\n if (focusWasInside) {\n previousFocusRef.current?.focus();\n }\n } else {\n dialog.close();\n }\n }\n }, [isOpen, modal, dialogRef]);\n\n /*\n * Expliciete focus-trap bovenop de native trap van `.showModal()`.\n * Zie `utils/focusTrap.ts` voor waarom de native trap niet volstaat.\n * Non-modaal krijgt bewust geen trap: daar hoort de gebruiker juist\n * tussen paneel en achtergrondpagina te kunnen tabben.\n */\n useFocusTrap(dialogRef, isOpen && modal);\n\n // Synchroniseer aria-expanded op het triggerelement\n React.useEffect(() => {\n const trigger = triggerRef?.current;\n if (!trigger) return;\n trigger.setAttribute('aria-expanded', isOpen ? 'true' : 'false');\n return () => {\n trigger.setAttribute('aria-expanded', 'false');\n };\n }, [isOpen, triggerRef]);\n\n // Non-modaal: handmatige Escape-afhandeling via keydown\n React.useEffect(() => {\n if (modal) return;\n const dialog = dialogRef.current;\n if (!dialog || !isOpen) return;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n onClose?.();\n }\n };\n\n dialog.addEventListener('keydown', handleKeyDown);\n return () => dialog.removeEventListener('keydown', handleKeyDown);\n }, [modal, isOpen, onClose, dialogRef]);\n\n // Modaal: native cancel-event (Escape via browser)\n const handleCancel = (event: React.SyntheticEvent<HTMLDialogElement>) => {\n event.preventDefault();\n onClose?.();\n };\n\n const classes = classNames(\n 'dsn-drawer',\n side === 'left' ? 'dsn-drawer--side-left' : 'dsn-drawer--side-right',\n className\n );\n\n return (\n <DrawerContext.Provider value={{ headingId, headingRef, onClose }}>\n <dialog\n ref={dialogRef}\n className={classes}\n aria-labelledby={headingId}\n onCancel={handleCancel}\n tabIndex={-1}\n popover={modal ? undefined : 'manual'}\n {...props}\n >\n {children}\n </dialog>\n </DrawerContext.Provider>\n );\n }\n);\n\nDrawer.displayName = 'Drawer';\n\n// =============================================================================\n// DrawerHeader\n// =============================================================================\n\nexport interface DrawerHeaderProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * Inhoud van de header — doorgaans een `DrawerHeading`\n */\n children?: React.ReactNode;\n}\n\n/**\n * DrawerHeader\n * De headerstrook van het zijpaneel met heading en sluitknop.\n */\nexport const DrawerHeader = React.forwardRef<HTMLDivElement, DrawerHeaderProps>(\n ({ className, children, ...props }, ref) => {\n const { onClose } = React.useContext(DrawerContext);\n\n return (\n <div\n ref={ref}\n className={classNames('dsn-drawer__header', className)}\n {...props}\n >\n {children}\n <Button\n variant=\"subtle\"\n size=\"small\"\n iconOnly\n onClick={onClose}\n iconStart={<Icon name=\"x\" aria-hidden />}\n >\n Sluiten\n </Button>\n </div>\n );\n }\n);\n\nDrawerHeader.displayName = 'DrawerHeader';\n\n// =============================================================================\n// DrawerHeading\n// =============================================================================\n\nexport interface DrawerHeadingProps extends React.HTMLAttributes<HTMLHeadingElement> {\n /**\n * Semantisch heading-niveau. Kies op basis van documenthiërarchie, niet visuele grootte.\n * @default 2\n */\n level?: 1 | 2 | 3 | 4 | 5 | 6;\n\n /**\n * De zichtbare heading-tekst\n */\n children?: React.ReactNode;\n}\n\n/**\n * DrawerHeading\n * De heading van het zijpaneel. ID wordt automatisch gegenereerd voor aria-labelledby.\n * Krijgt `tabindex=\"-1\"` zodat de Drawer er bij openen de focus op kan zetten.\n */\nexport const DrawerHeading = React.forwardRef<\n HTMLHeadingElement,\n DrawerHeadingProps\n>(({ className, level = 2, children, ...props }, ref) => {\n const { headingId, headingRef } = React.useContext(DrawerContext);\n const Tag = `h${level}` as React.ElementType;\n\n // De Drawer heeft de heading nodig om er bij openen de focus op te zetten;\n // een eventuele meegegeven ref blijft daarnaast gewoon werken.\n const setRef = React.useCallback(\n (node: HTMLHeadingElement | null) => {\n headingRef.current = node;\n if (typeof ref === 'function') {\n ref(node);\n } else if (ref) {\n (ref as React.MutableRefObject<HTMLHeadingElement | null>).current =\n node;\n }\n },\n [headingRef, ref]\n );\n\n return (\n <Tag\n ref={setRef}\n id={headingId}\n className={classNames('dsn-drawer-heading', className)}\n tabIndex={-1}\n {...props}\n >\n {children}\n </Tag>\n );\n});\n\nDrawerHeading.displayName = 'DrawerHeading';\n\n// =============================================================================\n// DrawerBody\n// =============================================================================\n\nexport interface DrawerBodyProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * De hoofdinhoud van het zijpaneel\n */\n children?: React.ReactNode;\n}\n\n/**\n * DrawerBody\n * De scrollbare inhoudssectie van het zijpaneel met scroll-affordance schaduw.\n */\nexport const DrawerBody = React.forwardRef<HTMLDivElement, DrawerBodyProps>(\n ({ className, children, ...props }, ref) => {\n return (\n <div\n ref={ref}\n className={classNames('dsn-drawer__body', className)}\n {...props}\n >\n {children}\n </div>\n );\n }\n);\n\nDrawerBody.displayName = 'DrawerBody';\n\n// =============================================================================\n// DrawerFooter\n// =============================================================================\n\nexport interface DrawerFooterProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * De acties van het zijpaneel — doorgaans een `ActionGroup` met knoppen\n */\n children?: React.ReactNode;\n}\n\n/**\n * DrawerFooter\n * De voettekst van het zijpaneel met actieknoppen.\n */\nexport const DrawerFooter = React.forwardRef<HTMLDivElement, DrawerFooterProps>(\n ({ className, children, ...props }, ref) => {\n return (\n <div\n ref={ref}\n className={classNames('dsn-drawer__footer', className)}\n {...props}\n >\n {children}\n </div>\n );\n }\n);\n\nDrawerFooter.displayName = 'DrawerFooter';\n"],"mappings":"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"}
@@ -1,6 +1,11 @@
1
1
  import React from "react";
2
- import { FormControlWidth } from "@dsn-starter-kit/core";
2
+ import { FormControlInlineSize } from "@dsn-starter-kit/core";
3
3
  //#region src/EmailInput/EmailInput.d.ts
4
+ /**
5
+ * Inline-size variants offered by EmailInput. xs and sm are left out: they
6
+ * are too narrow for the content this field is meant for.
7
+ */
8
+ type EmailInputInlineSize = Exclude<FormControlInlineSize, 'xs' | 'sm'>;
4
9
  interface EmailInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
5
10
  /**
6
11
  * Whether the input is in an invalid state
@@ -8,10 +13,10 @@ interface EmailInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElemen
8
13
  */
9
14
  invalid?: boolean;
10
15
  /**
11
- * Width variant for the input
12
- * @default undefined (uses default max-width from form-control)
16
+ * Inline-size variant for the input (md, lg, xl or full)
17
+ * @default undefined (uses default max-inline-size from form-control)
13
18
  */
14
- width?: FormControlWidth;
19
+ inlineSize?: EmailInputInlineSize;
15
20
  /**
16
21
  * Additional CSS class names
17
22
  */
@@ -36,5 +41,5 @@ interface EmailInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElemen
36
41
  */
37
42
  declare const EmailInput: React.ForwardRefExoticComponent<EmailInputProps & React.RefAttributes<HTMLInputElement>>;
38
43
  //#endregion
39
- export { EmailInput, EmailInputProps };
44
+ export { EmailInput, EmailInputInlineSize, EmailInputProps };
40
45
  //# sourceMappingURL=EmailInput.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"EmailInput.d.mts","names":[],"sources":["../../src/EmailInput/EmailInput.tsx"],"mappings":";;;UAIiB,wBAAwB,KACvC,MAAM,oBAAoB;;;;;EAO1B;;;;;EAMA,QAAQ;;;;EAKR;;;;;;;;;;;;;;;;;;;cAoBW,YAAU,MAAA,0BAAA,kBAAA,MAAA,cAAA"}
1
+ {"version":3,"file":"EmailInput.d.mts","names":[],"sources":["../../src/EmailInput/EmailInput.tsx"],"mappings":";;;;;;;KAQY,uBAAuB,QAAQ;UAE1B,wBAAwB,KACvC,MAAM,oBAAoB;;;;;EAO1B;;;;;EAMA,aAAa;;;;EAKb;;;;;;;;;;;;;;;;;;;cAoBW,YAAU,MAAA,0BAAA,kBAAA,MAAA,cAAA"}
@@ -20,8 +20,8 @@ import { jsx } from "react/jsx-runtime";
20
20
  * <EmailInput invalid aria-invalid="true" aria-describedby="error" />
21
21
  * ```
22
22
  */
23
- const EmailInput = React.forwardRef(({ className, invalid, width, autoComplete, ...props }, ref) => {
24
- const classes = classNames("dsn-text-input", width && `dsn-text-input--width-${width}`, className);
23
+ const EmailInput = React.forwardRef(({ className, invalid, inlineSize, autoComplete, ...props }, ref) => {
24
+ const classes = classNames("dsn-text-input", inlineSize && `dsn-text-input--inline-size-${inlineSize}`, className);
25
25
  return /* @__PURE__ */ jsx("input", {
26
26
  ref,
27
27
  type: "email",
@@ -1 +1 @@
1
- {"version":3,"file":"EmailInput2.mjs","names":[],"sources":["../../src/EmailInput/EmailInput.tsx"],"sourcesContent":["import React from 'react';\nimport { classNames, FormControlWidth } from '@dsn-starter-kit/core';\nimport './EmailInput.css';\n\nexport interface EmailInputProps extends Omit<\n React.InputHTMLAttributes<HTMLInputElement>,\n 'type'\n> {\n /**\n * Whether the input is in an invalid state\n * @default false\n */\n invalid?: boolean;\n\n /**\n * Width variant for the input\n * @default undefined (uses default max-width from form-control)\n */\n width?: FormControlWidth;\n\n /**\n * Additional CSS class names\n */\n className?: string;\n}\n\n/**\n * Email Input component\n * Email input with appropriate inputmode and autocomplete\n *\n * @example\n * ```tsx\n * // Basic usage\n * <EmailInput placeholder=\"naam@voorbeeld.nl\" />\n *\n * // With label\n * <FormFieldLabel htmlFor=\"email\">E-mailadres</FormFieldLabel>\n * <EmailInput id=\"email\" />\n *\n * // Invalid state\n * <EmailInput invalid aria-invalid=\"true\" aria-describedby=\"error\" />\n * ```\n */\nexport const EmailInput = React.forwardRef<HTMLInputElement, EmailInputProps>(\n ({ className, invalid, width, autoComplete, ...props }, ref) => {\n const classes = classNames(\n 'dsn-text-input',\n width && `dsn-text-input--width-${width}`,\n className\n );\n\n return (\n <input\n ref={ref}\n type=\"email\"\n inputMode=\"email\"\n className={classes}\n autoComplete={autoComplete || 'email'}\n aria-invalid={invalid || undefined}\n {...props}\n />\n );\n }\n);\n\nEmailInput.displayName = 'EmailInput';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA2CA,MAAa,aAAa,MAAM,YAC7B,EAAE,WAAW,SAAS,OAAO,cAAc,GAAG,SAAS,QAAQ;CAC9D,MAAM,UAAU,WACd,kBACA,SAAS,yBAAyB,SAClC,SACF;CAEA,OACE,oBAAC,SAAD;EACO;EACL,MAAK;EACL,WAAU;EACV,WAAW;EACX,cAAc,gBAAgB;EAC9B,gBAAc,WAAW,KAAA;EACzB,GAAI;CACL,CAAA;AAEL,CACF;AAEA,WAAW,cAAc"}
1
+ {"version":3,"file":"EmailInput2.mjs","names":[],"sources":["../../src/EmailInput/EmailInput.tsx"],"sourcesContent":["import React from 'react';\nimport { classNames, FormControlInlineSize } from '@dsn-starter-kit/core';\nimport './EmailInput.css';\n\n/**\n * Inline-size variants offered by EmailInput. xs and sm are left out: they\n * are too narrow for the content this field is meant for.\n */\nexport type EmailInputInlineSize = Exclude<FormControlInlineSize, 'xs' | 'sm'>;\n\nexport interface EmailInputProps extends Omit<\n React.InputHTMLAttributes<HTMLInputElement>,\n 'type'\n> {\n /**\n * Whether the input is in an invalid state\n * @default false\n */\n invalid?: boolean;\n\n /**\n * Inline-size variant for the input (md, lg, xl or full)\n * @default undefined (uses default max-inline-size from form-control)\n */\n inlineSize?: EmailInputInlineSize;\n\n /**\n * Additional CSS class names\n */\n className?: string;\n}\n\n/**\n * Email Input component\n * Email input with appropriate inputmode and autocomplete\n *\n * @example\n * ```tsx\n * // Basic usage\n * <EmailInput placeholder=\"naam@voorbeeld.nl\" />\n *\n * // With label\n * <FormFieldLabel htmlFor=\"email\">E-mailadres</FormFieldLabel>\n * <EmailInput id=\"email\" />\n *\n * // Invalid state\n * <EmailInput invalid aria-invalid=\"true\" aria-describedby=\"error\" />\n * ```\n */\nexport const EmailInput = React.forwardRef<HTMLInputElement, EmailInputProps>(\n ({ className, invalid, inlineSize, autoComplete, ...props }, ref) => {\n const classes = classNames(\n 'dsn-text-input',\n inlineSize && `dsn-text-input--inline-size-${inlineSize}`,\n className\n );\n\n return (\n <input\n ref={ref}\n type=\"email\"\n inputMode=\"email\"\n className={classes}\n autoComplete={autoComplete || 'email'}\n aria-invalid={invalid || undefined}\n {...props}\n />\n );\n }\n);\n\nEmailInput.displayName = 'EmailInput';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAiDA,MAAa,aAAa,MAAM,YAC7B,EAAE,WAAW,SAAS,YAAY,cAAc,GAAG,SAAS,QAAQ;CACnE,MAAM,UAAU,WACd,kBACA,cAAc,+BAA+B,cAC7C,SACF;CAEA,OACE,oBAAC,SAAD;EACO;EACL,MAAK;EACL,WAAU;EACV,WAAW;EACX,cAAc,gBAAgB;EAC9B,gBAAc,WAAW,KAAA;EACzB,GAAI;CACL,CAAA;AAEL,CACF;AAEA,WAAW,cAAc"}
@@ -8,7 +8,8 @@ interface ModalDialogProps extends Omit<React.HTMLAttributes<HTMLDialogElement>,
8
8
  isOpen: boolean;
9
9
  /**
10
10
  * Callback die wordt aangeroepen wanneer het dialoogvenster sluit
11
- * (sluitknop, Escape-toets of klik buiten het venster).
11
+ * (sluitknop of Escape-toets). Een klik op de backdrop sluit het venster
12
+ * bewust niet.
12
13
  */
13
14
  onClose?: () => void;
14
15
  /**
@@ -18,6 +19,15 @@ interface ModalDialogProps extends Omit<React.HTMLAttributes<HTMLDialogElement>,
18
19
  * open staat.
19
20
  */
20
21
  triggerRef?: React.RefObject<HTMLElement | null>;
22
+ /**
23
+ * Markeert het venster als alertdialog: een dringende melding die een
24
+ * reactie vraagt, zoals de bevestiging van een destructieve actie. Zet
25
+ * `role="alertdialog"` op het `<dialog>` en koppelt de `ModalDialogBody`
26
+ * via `aria-describedby`, zodat de boodschap bij openen wordt voorgelezen.
27
+ * De focus komt net als anders op de heading.
28
+ * @default false
29
+ */
30
+ alert?: boolean;
21
31
  /**
22
32
  * De subcomponenten van het dialoogvenster:
23
33
  * `ModalDialogHeader`, `ModalDialogBody`, `ModalDialogFooter`
@@ -35,7 +45,7 @@ interface ModalDialogProps extends Omit<React.HTMLAttributes<HTMLDialogElement>,
35
45
  *
36
46
  * @example
37
47
  * ```tsx
38
- * <ModalDialog isOpen={isOpen} onClose={() => setIsOpen(false)}>
48
+ * <ModalDialog alert isOpen={isOpen} onClose={() => setIsOpen(false)}>
39
49
  * <ModalDialogHeader>
40
50
  * <ModalDialogHeading>Bevestig verwijderen</ModalDialogHeading>
41
51
  * </ModalDialogHeader>
@@ -44,7 +54,7 @@ interface ModalDialogProps extends Omit<React.HTMLAttributes<HTMLDialogElement>,
44
54
  * </ModalDialogBody>
45
55
  * <ModalDialogFooter>
46
56
  * <ActionGroup>
47
- * <Button variant="negative" onClick={() => setIsOpen(false)}>Verwijderen</Button>
57
+ * <Button variant="strong-negative" onClick={() => setIsOpen(false)}>Verwijderen</Button>
48
58
  * <Button variant="default" onClick={() => setIsOpen(false)}>Annuleren</Button>
49
59
  * </ActionGroup>
50
60
  * </ModalDialogFooter>
@@ -1 +1 @@
1
- {"version":3,"file":"ModalDialog.d.mts","names":[],"sources":["../../src/ModalDialog/ModalDialog.tsx"],"mappings":";;UA0BiB,yBAAyB,KACxC,MAAM,eAAe;;;;;EAOrB;;;;;EAMA;;;;;;;EAQA,aAAa,MAAM,UAAU;;;;;EAM7B,WAAW,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA8BN,aAAW,MAAA,0BAAA,mBAAA,MAAA,cAAA;UA+EP,+BAA+B,MAAM,eAAe;;;;EAInE,WAAW,MAAM;;;;;;cAON,mBAAiB,MAAA,0BAAA,yBAAA,MAAA,cAAA;UAgCb,gCAAgC,MAAM,eAAe;;;;;EAKpE;;;;EAKA,WAAW,MAAM;;;;;;;cAQN,oBAAkB,MAAA,0BAAA,0BAAA,MAAA,cAAA;UAyCd,6BAA6B,MAAM,eAAe;;;;EAIjE,WAAW,MAAM;;;;;;cAON,iBAAe,MAAA,0BAAA,uBAAA,MAAA,cAAA;UAqBX,+BAA+B,MAAM,eAAe;;;;EAInE,WAAW,MAAM;;;;;;cAON,mBAAiB,MAAA,0BAAA,yBAAA,MAAA,cAAA"}
1
+ {"version":3,"file":"ModalDialog.d.mts","names":[],"sources":["../../src/ModalDialog/ModalDialog.tsx"],"mappings":";;UA4BiB,yBAAyB,KACxC,MAAM,eAAe;;;;;EAOrB;;;;;;EAOA;;;;;;;EAQA,aAAa,MAAM,UAAU;;;;;;;;;EAU7B;;;;;EAMA,WAAW,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA8BN,aAAW,MAAA,0BAAA,mBAAA,MAAA,cAAA;UAqGP,+BAA+B,MAAM,eAAe;;;;EAInE,WAAW,MAAM;;;;;;cAON,mBAAiB,MAAA,0BAAA,yBAAA,MAAA,cAAA;UAgCb,gCAAgC,MAAM,eAAe;;;;;EAKpE;;;;EAKA,WAAW,MAAM;;;;;;;cAQN,oBAAkB,MAAA,0BAAA,0BAAA,MAAA,cAAA;UAyCd,6BAA6B,MAAM,eAAe;;;;EAIjE,WAAW,MAAM;;;;;;cAON,iBAAe,MAAA,0BAAA,uBAAA,MAAA,cAAA;UAwBX,+BAA+B,MAAM,eAAe;;;;EAInE,WAAW,MAAM;;;;;;cAON,mBAAiB,MAAA,0BAAA,yBAAA,MAAA,cAAA"}
@@ -8,6 +8,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
8
8
  //#region src/ModalDialog/ModalDialog.tsx
9
9
  const ModalDialogContext = React.createContext({
10
10
  headingId: "",
11
+ bodyId: "",
11
12
  headingRef: { current: null }
12
13
  });
13
14
  /**
@@ -21,7 +22,7 @@ const ModalDialogContext = React.createContext({
21
22
  *
22
23
  * @example
23
24
  * ```tsx
24
- * <ModalDialog isOpen={isOpen} onClose={() => setIsOpen(false)}>
25
+ * <ModalDialog alert isOpen={isOpen} onClose={() => setIsOpen(false)}>
25
26
  * <ModalDialogHeader>
26
27
  * <ModalDialogHeading>Bevestig verwijderen</ModalDialogHeading>
27
28
  * </ModalDialogHeader>
@@ -30,17 +31,18 @@ const ModalDialogContext = React.createContext({
30
31
  * </ModalDialogBody>
31
32
  * <ModalDialogFooter>
32
33
  * <ActionGroup>
33
- * <Button variant="negative" onClick={() => setIsOpen(false)}>Verwijderen</Button>
34
+ * <Button variant="strong-negative" onClick={() => setIsOpen(false)}>Verwijderen</Button>
34
35
  * <Button variant="default" onClick={() => setIsOpen(false)}>Annuleren</Button>
35
36
  * </ActionGroup>
36
37
  * </ModalDialogFooter>
37
38
  * </ModalDialog>
38
39
  * ```
39
40
  */
40
- const ModalDialog = React.forwardRef(({ className, isOpen, onClose, triggerRef, children, ...props }, ref) => {
41
+ const ModalDialog = React.forwardRef(({ className, isOpen, onClose, triggerRef, alert = false, children, ...props }, ref) => {
41
42
  const internalRef = React.useRef(null);
42
43
  const dialogRef = ref ?? internalRef;
43
44
  const headingId = React.useId();
45
+ const bodyId = React.useId();
44
46
  const headingRef = React.useRef(null);
45
47
  React.useEffect(() => {
46
48
  const dialog = dialogRef.current;
@@ -67,13 +69,16 @@ const ModalDialog = React.forwardRef(({ className, isOpen, onClose, triggerRef,
67
69
  return /* @__PURE__ */ jsx(ModalDialogContext.Provider, {
68
70
  value: {
69
71
  headingId,
72
+ bodyId,
70
73
  headingRef,
71
74
  onClose
72
75
  },
73
76
  children: /* @__PURE__ */ jsx("dialog", {
74
77
  ref: dialogRef,
75
78
  className: classes,
79
+ role: alert ? "alertdialog" : void 0,
76
80
  "aria-labelledby": headingId,
81
+ "aria-describedby": alert ? bodyId : void 0,
77
82
  onCancel: handleCancel,
78
83
  tabIndex: -1,
79
84
  ...props,
@@ -134,8 +139,10 @@ ModalDialogHeading.displayName = "ModalDialogHeading";
134
139
  * De scrollbare inhoudssectie van het dialoogvenster met scroll-affordance schaduw.
135
140
  */
136
141
  const ModalDialogBody = React.forwardRef(({ className, children, ...props }, ref) => {
142
+ const { bodyId } = React.useContext(ModalDialogContext);
137
143
  return /* @__PURE__ */ jsx("div", {
138
144
  ref,
145
+ id: bodyId,
139
146
  className: classNames("dsn-modal-dialog__body", className),
140
147
  ...props,
141
148
  children
@@ -1 +1 @@
1
- {"version":3,"file":"ModalDialog2.mjs","names":[],"sources":["../../src/ModalDialog/ModalDialog.tsx"],"sourcesContent":["import React from 'react';\nimport { classNames } from '@dsn-starter-kit/core';\nimport { Button } from '../Button';\nimport { Icon } from '../Icon';\nimport { useFocusTrap } from '../utils/focusTrap';\nimport './ModalDialog.css';\n\n// =============================================================================\n// Context\n// =============================================================================\n\ninterface ModalDialogContextValue {\n headingId: string;\n headingRef: React.MutableRefObject<HTMLHeadingElement | null>;\n onClose?: () => void;\n}\n\nconst ModalDialogContext = React.createContext<ModalDialogContextValue>({\n headingId: '',\n headingRef: { current: null },\n});\n\n// =============================================================================\n// ModalDialog (root)\n// =============================================================================\n\nexport interface ModalDialogProps extends Omit<\n React.HTMLAttributes<HTMLDialogElement>,\n 'children'\n> {\n /**\n * Bepaalt of het dialoogvenster getoond wordt.\n * Het dialoogvenster wordt via `.showModal()` geopend — nooit via `.show()`.\n */\n isOpen: boolean;\n\n /**\n * Callback die wordt aangeroepen wanneer het dialoogvenster sluit\n * (sluitknop, Escape-toets of klik buiten het venster).\n */\n onClose?: () => void;\n\n /**\n * Ref naar het element dat het dialoogvenster opent. Wanneer je die meegeeft,\n * houdt het dialoogvenster `aria-expanded` op dat element synchroon met de\n * open-staat: `false` wanneer het venster gesloten is, `true` wanneer het\n * open staat.\n */\n triggerRef?: React.RefObject<HTMLElement | null>;\n\n /**\n * De subcomponenten van het dialoogvenster:\n * `ModalDialogHeader`, `ModalDialogBody`, `ModalDialogFooter`\n */\n children?: React.ReactNode;\n}\n\n/**\n * ModalDialog component\n * Modaal dialoogvenster gebaseerd op het native `<dialog>` element.\n *\n * Gebruik altijd `.showModal()` (intern afgehandeld via `isOpen` prop), nooit `.show()`.\n * Dit garandeert aria-modal semantiek en het inert-attribuut op de achtergrond.\n * De focus blijft binnen het venster via de native focus-trap plus een\n * expliciete trap, en komt bij openen op de heading te staan.\n *\n * @example\n * ```tsx\n * <ModalDialog isOpen={isOpen} onClose={() => setIsOpen(false)}>\n * <ModalDialogHeader>\n * <ModalDialogHeading>Bevestig verwijderen</ModalDialogHeading>\n * </ModalDialogHeader>\n * <ModalDialogBody>\n * <Paragraph>Weet u zeker dat u dit item wilt verwijderen?</Paragraph>\n * </ModalDialogBody>\n * <ModalDialogFooter>\n * <ActionGroup>\n * <Button variant=\"negative\" onClick={() => setIsOpen(false)}>Verwijderen</Button>\n * <Button variant=\"default\" onClick={() => setIsOpen(false)}>Annuleren</Button>\n * </ActionGroup>\n * </ModalDialogFooter>\n * </ModalDialog>\n * ```\n */\nexport const ModalDialog = React.forwardRef<\n HTMLDialogElement,\n ModalDialogProps\n>(({ className, isOpen, onClose, triggerRef, children, ...props }, ref) => {\n const internalRef = React.useRef<HTMLDialogElement>(null);\n const dialogRef = (ref as React.RefObject<HTMLDialogElement>) ?? internalRef;\n const headingId = React.useId();\n const headingRef = React.useRef<HTMLHeadingElement | null>(null);\n\n React.useEffect(() => {\n const dialog = dialogRef.current;\n if (!dialog) return;\n\n if (isOpen && !dialog.open) {\n dialog.showModal();\n\n /*\n * Zet de focus op de heading, niet op de sluitknop. De browser zet de\n * focus bij het openen zelf op het eerste focusbare element, en dat is\n * de sluitknop. VoiceOver in Safari leest die knop dan wel voor, maar\n * niet de titel uit `aria-labelledby`, en omdat de sluitknop na de\n * titel staat kom je de titel bij verder lezen ook niet meer tegen.\n *\n * Met de focus op de heading (die `tabindex=\"-1\"` heeft) wordt de titel\n * als eerste voorgelezen en loopt de leesvolgorde daarna door naar de\n * sluitknop en de inhoud. Dit is het patroon uit de ARIA Authoring\n * Practices, en het houdt DOM-volgorde en visuele volgorde gelijk.\n */\n (headingRef.current ?? dialog).focus();\n } else if (!isOpen && dialog.open) {\n dialog.close();\n }\n }, [isOpen, dialogRef]);\n\n /*\n * Expliciete focus-trap bovenop de native trap van `.showModal()`.\n * Zie `utils/focusTrap.ts` voor waarom de native trap niet volstaat.\n */\n useFocusTrap(dialogRef, isOpen);\n\n // Synchroniseer aria-expanded op het triggerelement\n React.useEffect(() => {\n const trigger = triggerRef?.current;\n if (!trigger) return;\n trigger.setAttribute('aria-expanded', isOpen ? 'true' : 'false');\n return () => {\n trigger.setAttribute('aria-expanded', 'false');\n };\n }, [isOpen, triggerRef]);\n\n const handleCancel = (event: React.SyntheticEvent<HTMLDialogElement>) => {\n event.preventDefault();\n onClose?.();\n };\n\n const classes = classNames('dsn-modal-dialog', className);\n\n return (\n <ModalDialogContext.Provider value={{ headingId, headingRef, onClose }}>\n <dialog\n ref={dialogRef}\n className={classes}\n aria-labelledby={headingId}\n onCancel={handleCancel}\n tabIndex={-1}\n {...props}\n >\n {children}\n </dialog>\n </ModalDialogContext.Provider>\n );\n});\n\nModalDialog.displayName = 'ModalDialog';\n\n// =============================================================================\n// ModalDialogHeader\n// =============================================================================\n\nexport interface ModalDialogHeaderProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * Inhoud van de header — doorgaans een `ModalDialogHeading`\n */\n children?: React.ReactNode;\n}\n\n/**\n * ModalDialogHeader\n * De headerstrook van het dialoogvenster met heading en sluitknop.\n */\nexport const ModalDialogHeader = React.forwardRef<\n HTMLDivElement,\n ModalDialogHeaderProps\n>(({ className, children, ...props }, ref) => {\n const { onClose } = React.useContext(ModalDialogContext);\n\n return (\n <div\n ref={ref}\n className={classNames('dsn-modal-dialog__header', className)}\n {...props}\n >\n {children}\n <Button\n variant=\"subtle\"\n size=\"small\"\n iconOnly\n onClick={onClose}\n iconStart={<Icon name=\"x\" aria-hidden />}\n >\n Sluiten\n </Button>\n </div>\n );\n});\n\nModalDialogHeader.displayName = 'ModalDialogHeader';\n\n// =============================================================================\n// ModalDialogHeading\n// =============================================================================\n\nexport interface ModalDialogHeadingProps extends React.HTMLAttributes<HTMLHeadingElement> {\n /**\n * Semantisch heading-niveau. Kies op basis van documenthiërarchie, niet visuele grootte.\n * @default 2\n */\n level?: 1 | 2 | 3 | 4 | 5 | 6;\n\n /**\n * De zichtbare heading-tekst\n */\n children?: React.ReactNode;\n}\n\n/**\n * ModalDialogHeading\n * De heading van het dialoogvenster. ID wordt automatisch gegenereerd voor aria-labelledby.\n * Krijgt `tabindex=\"-1\"` zodat het ModalDialog er bij openen de focus op kan zetten.\n */\nexport const ModalDialogHeading = React.forwardRef<\n HTMLHeadingElement,\n ModalDialogHeadingProps\n>(({ className, level = 2, children, ...props }, ref) => {\n const { headingId, headingRef } = React.useContext(ModalDialogContext);\n const Tag = `h${level}` as React.ElementType;\n\n // Het ModalDialog heeft de heading nodig om er bij openen de focus op te\n // zetten; een eventuele meegegeven ref blijft daarnaast gewoon werken.\n const setRef = React.useCallback(\n (node: HTMLHeadingElement | null) => {\n headingRef.current = node;\n if (typeof ref === 'function') {\n ref(node);\n } else if (ref) {\n (ref as React.MutableRefObject<HTMLHeadingElement | null>).current =\n node;\n }\n },\n [headingRef, ref]\n );\n\n return (\n <Tag\n ref={setRef}\n id={headingId}\n className={classNames('dsn-modal-dialog-heading', className)}\n tabIndex={-1}\n {...props}\n >\n {children}\n </Tag>\n );\n});\n\nModalDialogHeading.displayName = 'ModalDialogHeading';\n\n// =============================================================================\n// ModalDialogBody\n// =============================================================================\n\nexport interface ModalDialogBodyProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * De hoofdinhoud van het dialoogvenster\n */\n children?: React.ReactNode;\n}\n\n/**\n * ModalDialogBody\n * De scrollbare inhoudssectie van het dialoogvenster met scroll-affordance schaduw.\n */\nexport const ModalDialogBody = React.forwardRef<\n HTMLDivElement,\n ModalDialogBodyProps\n>(({ className, children, ...props }, ref) => {\n return (\n <div\n ref={ref}\n className={classNames('dsn-modal-dialog__body', className)}\n {...props}\n >\n {children}\n </div>\n );\n});\n\nModalDialogBody.displayName = 'ModalDialogBody';\n\n// =============================================================================\n// ModalDialogFooter\n// =============================================================================\n\nexport interface ModalDialogFooterProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * De acties van het dialoogvenster — doorgaans een `ActionGroup` met knoppen\n */\n children?: React.ReactNode;\n}\n\n/**\n * ModalDialogFooter\n * De voettekst van het dialoogvenster met actieknoppen.\n */\nexport const ModalDialogFooter = React.forwardRef<\n HTMLDivElement,\n ModalDialogFooterProps\n>(({ className, children, ...props }, ref) => {\n return (\n <div\n ref={ref}\n className={classNames('dsn-modal-dialog__footer', className)}\n {...props}\n >\n {children}\n </div>\n );\n});\n\nModalDialogFooter.displayName = 'ModalDialogFooter';\n"],"mappings":"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"}
1
+ {"version":3,"file":"ModalDialog2.mjs","names":[],"sources":["../../src/ModalDialog/ModalDialog.tsx"],"sourcesContent":["import React from 'react';\nimport { classNames } from '@dsn-starter-kit/core';\nimport { Button } from '../Button';\nimport { Icon } from '../Icon';\nimport { useFocusTrap } from '../utils/focusTrap';\nimport './ModalDialog.css';\n\n// =============================================================================\n// Context\n// =============================================================================\n\ninterface ModalDialogContextValue {\n headingId: string;\n bodyId: string;\n headingRef: React.MutableRefObject<HTMLHeadingElement | null>;\n onClose?: () => void;\n}\n\nconst ModalDialogContext = React.createContext<ModalDialogContextValue>({\n headingId: '',\n bodyId: '',\n headingRef: { current: null },\n});\n\n// =============================================================================\n// ModalDialog (root)\n// =============================================================================\n\nexport interface ModalDialogProps extends Omit<\n React.HTMLAttributes<HTMLDialogElement>,\n 'children'\n> {\n /**\n * Bepaalt of het dialoogvenster getoond wordt.\n * Het dialoogvenster wordt via `.showModal()` geopend — nooit via `.show()`.\n */\n isOpen: boolean;\n\n /**\n * Callback die wordt aangeroepen wanneer het dialoogvenster sluit\n * (sluitknop of Escape-toets). Een klik op de backdrop sluit het venster\n * bewust niet.\n */\n onClose?: () => void;\n\n /**\n * Ref naar het element dat het dialoogvenster opent. Wanneer je die meegeeft,\n * houdt het dialoogvenster `aria-expanded` op dat element synchroon met de\n * open-staat: `false` wanneer het venster gesloten is, `true` wanneer het\n * open staat.\n */\n triggerRef?: React.RefObject<HTMLElement | null>;\n\n /**\n * Markeert het venster als alertdialog: een dringende melding die een\n * reactie vraagt, zoals de bevestiging van een destructieve actie. Zet\n * `role=\"alertdialog\"` op het `<dialog>` en koppelt de `ModalDialogBody`\n * via `aria-describedby`, zodat de boodschap bij openen wordt voorgelezen.\n * De focus komt net als anders op de heading.\n * @default false\n */\n alert?: boolean;\n\n /**\n * De subcomponenten van het dialoogvenster:\n * `ModalDialogHeader`, `ModalDialogBody`, `ModalDialogFooter`\n */\n children?: React.ReactNode;\n}\n\n/**\n * ModalDialog component\n * Modaal dialoogvenster gebaseerd op het native `<dialog>` element.\n *\n * Gebruik altijd `.showModal()` (intern afgehandeld via `isOpen` prop), nooit `.show()`.\n * Dit garandeert aria-modal semantiek en het inert-attribuut op de achtergrond.\n * De focus blijft binnen het venster via de native focus-trap plus een\n * expliciete trap, en komt bij openen op de heading te staan.\n *\n * @example\n * ```tsx\n * <ModalDialog alert isOpen={isOpen} onClose={() => setIsOpen(false)}>\n * <ModalDialogHeader>\n * <ModalDialogHeading>Bevestig verwijderen</ModalDialogHeading>\n * </ModalDialogHeader>\n * <ModalDialogBody>\n * <Paragraph>Weet u zeker dat u dit item wilt verwijderen?</Paragraph>\n * </ModalDialogBody>\n * <ModalDialogFooter>\n * <ActionGroup>\n * <Button variant=\"strong-negative\" onClick={() => setIsOpen(false)}>Verwijderen</Button>\n * <Button variant=\"default\" onClick={() => setIsOpen(false)}>Annuleren</Button>\n * </ActionGroup>\n * </ModalDialogFooter>\n * </ModalDialog>\n * ```\n */\nexport const ModalDialog = React.forwardRef<\n HTMLDialogElement,\n ModalDialogProps\n>(\n (\n {\n className,\n isOpen,\n onClose,\n triggerRef,\n alert = false,\n children,\n ...props\n },\n ref\n ) => {\n const internalRef = React.useRef<HTMLDialogElement>(null);\n const dialogRef =\n (ref as React.RefObject<HTMLDialogElement>) ?? internalRef;\n const headingId = React.useId();\n const bodyId = React.useId();\n const headingRef = React.useRef<HTMLHeadingElement | null>(null);\n\n React.useEffect(() => {\n const dialog = dialogRef.current;\n if (!dialog) return;\n\n if (isOpen && !dialog.open) {\n dialog.showModal();\n\n /*\n * Zet de focus op de heading, niet op de sluitknop. De browser zet de\n * focus bij het openen zelf op het eerste focusbare element, en dat is\n * de sluitknop. VoiceOver in Safari leest die knop dan wel voor, maar\n * niet de titel uit `aria-labelledby`, en omdat de sluitknop na de\n * titel staat kom je de titel bij verder lezen ook niet meer tegen.\n *\n * Met de focus op de heading (die `tabindex=\"-1\"` heeft) wordt de titel\n * als eerste voorgelezen en loopt de leesvolgorde daarna door naar de\n * sluitknop en de inhoud. Dit is het patroon uit de ARIA Authoring\n * Practices, en het houdt DOM-volgorde en visuele volgorde gelijk.\n *\n * Waarom niet het eerste veld of de minst destructieve knop: zie\n * docs/decisions/DR-2026-12-dialogs-focus-bij-openen-op-de-heading.md.\n */\n (headingRef.current ?? dialog).focus();\n } else if (!isOpen && dialog.open) {\n dialog.close();\n }\n }, [isOpen, dialogRef]);\n\n /*\n * Expliciete focus-trap bovenop de native trap van `.showModal()`.\n * Zie `utils/focusTrap.ts` voor waarom de native trap niet volstaat.\n */\n useFocusTrap(dialogRef, isOpen);\n\n // Synchroniseer aria-expanded op het triggerelement\n React.useEffect(() => {\n const trigger = triggerRef?.current;\n if (!trigger) return;\n trigger.setAttribute('aria-expanded', isOpen ? 'true' : 'false');\n return () => {\n trigger.setAttribute('aria-expanded', 'false');\n };\n }, [isOpen, triggerRef]);\n\n const handleCancel = (event: React.SyntheticEvent<HTMLDialogElement>) => {\n event.preventDefault();\n onClose?.();\n };\n\n const classes = classNames('dsn-modal-dialog', className);\n\n return (\n <ModalDialogContext.Provider\n value={{ headingId, bodyId, headingRef, onClose }}\n >\n <dialog\n ref={dialogRef}\n className={classes}\n role={alert ? 'alertdialog' : undefined}\n aria-labelledby={headingId}\n aria-describedby={alert ? bodyId : undefined}\n onCancel={handleCancel}\n tabIndex={-1}\n {...props}\n >\n {children}\n </dialog>\n </ModalDialogContext.Provider>\n );\n }\n);\n\nModalDialog.displayName = 'ModalDialog';\n\n// =============================================================================\n// ModalDialogHeader\n// =============================================================================\n\nexport interface ModalDialogHeaderProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * Inhoud van de header — doorgaans een `ModalDialogHeading`\n */\n children?: React.ReactNode;\n}\n\n/**\n * ModalDialogHeader\n * De headerstrook van het dialoogvenster met heading en sluitknop.\n */\nexport const ModalDialogHeader = React.forwardRef<\n HTMLDivElement,\n ModalDialogHeaderProps\n>(({ className, children, ...props }, ref) => {\n const { onClose } = React.useContext(ModalDialogContext);\n\n return (\n <div\n ref={ref}\n className={classNames('dsn-modal-dialog__header', className)}\n {...props}\n >\n {children}\n <Button\n variant=\"subtle\"\n size=\"small\"\n iconOnly\n onClick={onClose}\n iconStart={<Icon name=\"x\" aria-hidden />}\n >\n Sluiten\n </Button>\n </div>\n );\n});\n\nModalDialogHeader.displayName = 'ModalDialogHeader';\n\n// =============================================================================\n// ModalDialogHeading\n// =============================================================================\n\nexport interface ModalDialogHeadingProps extends React.HTMLAttributes<HTMLHeadingElement> {\n /**\n * Semantisch heading-niveau. Kies op basis van documenthiërarchie, niet visuele grootte.\n * @default 2\n */\n level?: 1 | 2 | 3 | 4 | 5 | 6;\n\n /**\n * De zichtbare heading-tekst\n */\n children?: React.ReactNode;\n}\n\n/**\n * ModalDialogHeading\n * De heading van het dialoogvenster. ID wordt automatisch gegenereerd voor aria-labelledby.\n * Krijgt `tabindex=\"-1\"` zodat het ModalDialog er bij openen de focus op kan zetten.\n */\nexport const ModalDialogHeading = React.forwardRef<\n HTMLHeadingElement,\n ModalDialogHeadingProps\n>(({ className, level = 2, children, ...props }, ref) => {\n const { headingId, headingRef } = React.useContext(ModalDialogContext);\n const Tag = `h${level}` as React.ElementType;\n\n // Het ModalDialog heeft de heading nodig om er bij openen de focus op te\n // zetten; een eventuele meegegeven ref blijft daarnaast gewoon werken.\n const setRef = React.useCallback(\n (node: HTMLHeadingElement | null) => {\n headingRef.current = node;\n if (typeof ref === 'function') {\n ref(node);\n } else if (ref) {\n (ref as React.MutableRefObject<HTMLHeadingElement | null>).current =\n node;\n }\n },\n [headingRef, ref]\n );\n\n return (\n <Tag\n ref={setRef}\n id={headingId}\n className={classNames('dsn-modal-dialog-heading', className)}\n tabIndex={-1}\n {...props}\n >\n {children}\n </Tag>\n );\n});\n\nModalDialogHeading.displayName = 'ModalDialogHeading';\n\n// =============================================================================\n// ModalDialogBody\n// =============================================================================\n\nexport interface ModalDialogBodyProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * De hoofdinhoud van het dialoogvenster\n */\n children?: React.ReactNode;\n}\n\n/**\n * ModalDialogBody\n * De scrollbare inhoudssectie van het dialoogvenster met scroll-affordance schaduw.\n */\nexport const ModalDialogBody = React.forwardRef<\n HTMLDivElement,\n ModalDialogBodyProps\n>(({ className, children, ...props }, ref) => {\n const { bodyId } = React.useContext(ModalDialogContext);\n\n return (\n <div\n ref={ref}\n id={bodyId}\n className={classNames('dsn-modal-dialog__body', className)}\n {...props}\n >\n {children}\n </div>\n );\n});\n\nModalDialogBody.displayName = 'ModalDialogBody';\n\n// =============================================================================\n// ModalDialogFooter\n// =============================================================================\n\nexport interface ModalDialogFooterProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * De acties van het dialoogvenster — doorgaans een `ActionGroup` met knoppen\n */\n children?: React.ReactNode;\n}\n\n/**\n * ModalDialogFooter\n * De voettekst van het dialoogvenster met actieknoppen.\n */\nexport const ModalDialogFooter = React.forwardRef<\n HTMLDivElement,\n ModalDialogFooterProps\n>(({ className, children, ...props }, ref) => {\n return (\n <div\n ref={ref}\n className={classNames('dsn-modal-dialog__footer', className)}\n {...props}\n >\n {children}\n </div>\n );\n});\n\nModalDialogFooter.displayName = 'ModalDialogFooter';\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { FormControlWidth } from "@dsn-starter-kit/core";
2
+ import { FormControlInlineSize } from "@dsn-starter-kit/core";
3
3
  //#region src/NumberInput/NumberInput.d.ts
4
4
  interface NumberInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
5
5
  /**
@@ -8,10 +8,10 @@ interface NumberInputProps extends Omit<React.InputHTMLAttributes<HTMLInputEleme
8
8
  */
9
9
  invalid?: boolean;
10
10
  /**
11
- * Width variant for the input
12
- * @default undefined (uses default max-width from form-control)
11
+ * Inline-size variant for the input
12
+ * @default undefined (uses default max-inline-size from form-control)
13
13
  */
14
- width?: FormControlWidth;
14
+ inlineSize?: FormControlInlineSize;
15
15
  /**
16
16
  * Allow decimal input (e.g. amounts). When false, uses GOV.UK pattern:
17
17
  * inputmode="numeric" + pattern="[0-9]*" for a numeric-only keyboard.
@@ -1 +1 @@
1
- {"version":3,"file":"NumberInput.d.mts","names":[],"sources":["../../src/NumberInput/NumberInput.tsx"],"mappings":";;;UAIiB,yBAAyB,KACxC,MAAM,oBAAoB;;;;;EAO1B;;;;;EAMA,QAAQ;;;;;;;EAQR;;;;EAKA;;;;;;;;;;;;;;;;;;;cAoBW,aAAW,MAAA,0BAAA,mBAAA,MAAA,cAAA"}
1
+ {"version":3,"file":"NumberInput.d.mts","names":[],"sources":["../../src/NumberInput/NumberInput.tsx"],"mappings":";;;UAIiB,yBAAyB,KACxC,MAAM,oBAAoB;;;;;EAO1B;;;;;EAMA,aAAa;;;;;;;EAQb;;;;EAKA;;;;;;;;;;;;;;;;;;;cAoBW,aAAW,MAAA,0BAAA,mBAAA,MAAA,cAAA"}
@@ -20,8 +20,8 @@ import { jsx } from "react/jsx-runtime";
20
20
  * <NumberInput invalid aria-invalid="true" aria-describedby="error" />
21
21
  * ```
22
22
  */
23
- const NumberInput = React.forwardRef(({ className, invalid, width, allowDecimals = false, ...props }, ref) => {
24
- const classes = classNames("dsn-text-input", width && `dsn-text-input--width-${width}`, className);
23
+ const NumberInput = React.forwardRef(({ className, invalid, inlineSize, allowDecimals = false, ...props }, ref) => {
24
+ const classes = classNames("dsn-text-input", inlineSize && `dsn-text-input--inline-size-${inlineSize}`, className);
25
25
  return /* @__PURE__ */ jsx("input", {
26
26
  ref,
27
27
  type: "text",
@@ -1 +1 @@
1
- {"version":3,"file":"NumberInput2.mjs","names":[],"sources":["../../src/NumberInput/NumberInput.tsx"],"sourcesContent":["import React from 'react';\nimport { classNames, FormControlWidth } from '@dsn-starter-kit/core';\nimport './NumberInput.css';\n\nexport interface NumberInputProps extends Omit<\n React.InputHTMLAttributes<HTMLInputElement>,\n 'type'\n> {\n /**\n * Whether the input is in an invalid state\n * @default false\n */\n invalid?: boolean;\n\n /**\n * Width variant for the input\n * @default undefined (uses default max-width from form-control)\n */\n width?: FormControlWidth;\n\n /**\n * Allow decimal input (e.g. amounts). When false, uses GOV.UK pattern:\n * inputmode=\"numeric\" + pattern=\"[0-9]*\" for a numeric-only keyboard.\n * When true, uses inputmode=\"decimal\" without pattern restriction.\n * @default false\n */\n allowDecimals?: boolean;\n\n /**\n * Additional CSS class names\n */\n className?: string;\n}\n\n/**\n * Number Input component\n * Uses type=\"text\" with inputmode=\"decimal\" for better mobile UX (no spinner buttons)\n *\n * @example\n * ```tsx\n * // Basic usage\n * <NumberInput placeholder=\"0,00\" />\n *\n * // With label\n * <FormFieldLabel htmlFor=\"amount\">Bedrag</FormFieldLabel>\n * <NumberInput id=\"amount\" />\n *\n * // Invalid state\n * <NumberInput invalid aria-invalid=\"true\" aria-describedby=\"error\" />\n * ```\n */\nexport const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(\n ({ className, invalid, width, allowDecimals = false, ...props }, ref) => {\n const classes = classNames(\n 'dsn-text-input',\n width && `dsn-text-input--width-${width}`,\n className\n );\n\n return (\n <input\n ref={ref}\n type=\"text\"\n inputMode={allowDecimals ? 'decimal' : 'numeric'}\n pattern={allowDecimals ? undefined : '[0-9]*'}\n className={classes}\n aria-invalid={invalid || undefined}\n autoComplete=\"off\"\n {...props}\n />\n );\n }\n);\n\nNumberInput.displayName = 'NumberInput';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAmDA,MAAa,cAAc,MAAM,YAC9B,EAAE,WAAW,SAAS,OAAO,gBAAgB,OAAO,GAAG,SAAS,QAAQ;CACvE,MAAM,UAAU,WACd,kBACA,SAAS,yBAAyB,SAClC,SACF;CAEA,OACE,oBAAC,SAAD;EACO;EACL,MAAK;EACL,WAAW,gBAAgB,YAAY;EACvC,SAAS,gBAAgB,KAAA,IAAY;EACrC,WAAW;EACX,gBAAc,WAAW,KAAA;EACzB,cAAa;EACb,GAAI;CACL,CAAA;AAEL,CACF;AAEA,YAAY,cAAc"}
1
+ {"version":3,"file":"NumberInput2.mjs","names":[],"sources":["../../src/NumberInput/NumberInput.tsx"],"sourcesContent":["import React from 'react';\nimport { classNames, FormControlInlineSize } from '@dsn-starter-kit/core';\nimport './NumberInput.css';\n\nexport interface NumberInputProps extends Omit<\n React.InputHTMLAttributes<HTMLInputElement>,\n 'type'\n> {\n /**\n * Whether the input is in an invalid state\n * @default false\n */\n invalid?: boolean;\n\n /**\n * Inline-size variant for the input\n * @default undefined (uses default max-inline-size from form-control)\n */\n inlineSize?: FormControlInlineSize;\n\n /**\n * Allow decimal input (e.g. amounts). When false, uses GOV.UK pattern:\n * inputmode=\"numeric\" + pattern=\"[0-9]*\" for a numeric-only keyboard.\n * When true, uses inputmode=\"decimal\" without pattern restriction.\n * @default false\n */\n allowDecimals?: boolean;\n\n /**\n * Additional CSS class names\n */\n className?: string;\n}\n\n/**\n * Number Input component\n * Uses type=\"text\" with inputmode=\"decimal\" for better mobile UX (no spinner buttons)\n *\n * @example\n * ```tsx\n * // Basic usage\n * <NumberInput placeholder=\"0,00\" />\n *\n * // With label\n * <FormFieldLabel htmlFor=\"amount\">Bedrag</FormFieldLabel>\n * <NumberInput id=\"amount\" />\n *\n * // Invalid state\n * <NumberInput invalid aria-invalid=\"true\" aria-describedby=\"error\" />\n * ```\n */\nexport const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(\n (\n { className, invalid, inlineSize, allowDecimals = false, ...props },\n ref\n ) => {\n const classes = classNames(\n 'dsn-text-input',\n inlineSize && `dsn-text-input--inline-size-${inlineSize}`,\n className\n );\n\n return (\n <input\n ref={ref}\n type=\"text\"\n inputMode={allowDecimals ? 'decimal' : 'numeric'}\n pattern={allowDecimals ? undefined : '[0-9]*'}\n className={classes}\n aria-invalid={invalid || undefined}\n autoComplete=\"off\"\n {...props}\n />\n );\n }\n);\n\nNumberInput.displayName = 'NumberInput';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAmDA,MAAa,cAAc,MAAM,YAE7B,EAAE,WAAW,SAAS,YAAY,gBAAgB,OAAO,GAAG,SAC5D,QACG;CACH,MAAM,UAAU,WACd,kBACA,cAAc,+BAA+B,cAC7C,SACF;CAEA,OACE,oBAAC,SAAD;EACO;EACL,MAAK;EACL,WAAW,gBAAgB,YAAY;EACvC,SAAS,gBAAgB,KAAA,IAAY;EACrC,WAAW;EACX,gBAAc,WAAW,KAAA;EACzB,cAAa;EACb,GAAI;CACL,CAAA;AAEL,CACF;AAEA,YAAY,cAAc"}
@@ -1 +1 @@
1
- {"version":3,"file":"PageHeader.d.mts","names":[],"sources":["../../src/PageHeader/PageHeader.tsx"],"mappings":";;KASY;KACA;KACA;UAEK,wBAAwB,KACvC,MAAM,eAAe;;;;;EAOrB,UAAU,MAAM;;;;;;;;EAShB,SAAS;;;;;;;;;;;EAYT,SAAS;;;;;;;;;EAUT,cAAc;;;;;;;EAQd;;;;;EAMA,oBAAoB,MAAM;;;;;;EAO1B,yBAAyB,MAAM;;;;EAK/B,sBAAsB,MAAM;;;;;EAM5B,2BAA2B,MAAM;;;;;EAMjC,aAAa,MAAM;;;;;;EAOnB,kBAAkB,MAAM;;;;;;;EAQxB;;;;EAKA;;;;EAKA;;;;EAKA;;;;EAKA;;;;;EAQA;;;;;EAMA;;;;;EAMA;;;;;EAMA;;;;;EAMA;;;;;EAMA;;;;;;EAOA;;;;;;EAOA;;;;;;EAOA;;;;;;EAOA;EAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAmCW,YAAU,MAAA,0BAAA,kBAAA,MAAA,cAAA"}
1
+ {"version":3,"file":"PageHeader.d.mts","names":[],"sources":["../../src/PageHeader/PageHeader.tsx"],"mappings":";;KASY;KAOA;KACA;UAEK,wBAAwB,KACvC,MAAM,eAAe;;;;;EAOrB,UAAU,MAAM;;;;;;;;EAShB,SAAS;;;;;;;;;;;EAYT,SAAS;;;;;;;;;EAUT,cAAc;;;;;;;EAQd;;;;;EAMA,oBAAoB,MAAM;;;;;;EAO1B,yBAAyB,MAAM;;;;EAK/B,sBAAsB,MAAM;;;;;EAM5B,2BAA2B,MAAM;;;;;EAMjC,aAAa,MAAM;;;;;;EAOnB,kBAAkB,MAAM;;;;;;;EAQxB;;;;EAKA;;;;EAKA;;;;EAKA;;;;EAKA;;;;;EAQA;;;;;EAMA;;;;;EAMA;;;;;EAMA;;;;;EAMA;;;;;EAMA;;;;;;EAOA;;;;;;EAOA;;;;;;EAOA;;;;;;EAOA;EAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAmCW,YAAU,MAAA,0BAAA,kBAAA,MAAA,cAAA"}
@@ -8,6 +8,10 @@ import React from "react";
8
8
  import { classNames } from "@dsn-starter-kit/core";
9
9
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
10
  //#region src/PageHeader/PageHeader.tsx
11
+ /** Auto-hide: boven deze scrollpositie (px) blijft de header altijd zichtbaar */
12
+ const AUTO_HIDE_OFFSET = 100;
13
+ /** Auto-hide: zoveel px omhoog scrollen voordat de header terugkomt */
14
+ const AUTO_HIDE_REVEAL_DISTANCE = 64;
11
15
  /**
12
16
  * PageHeader component
13
17
  * Primaire navigatieheader voor een pagina. Mobile-first implementatie.
@@ -55,11 +59,24 @@ const PageHeader = React.forwardRef(({ className, logoSlot, sticky = "none", lay
55
59
  React.useEffect(() => {
56
60
  if (sticky !== "auto-hide") return;
57
61
  let lastScrollY = window.scrollY;
62
+ let scrolledUp = 0;
58
63
  const handleScroll = () => {
59
64
  const currentScrollY = window.scrollY;
60
- const isScrollingDown = currentScrollY > lastScrollY && currentScrollY > 100;
61
- combinedRef.current?.setAttribute("data-hidden", String(isScrollingDown));
65
+ const delta = currentScrollY - lastScrollY;
62
66
  lastScrollY = currentScrollY;
67
+ let hidden;
68
+ if (currentScrollY <= AUTO_HIDE_OFFSET) {
69
+ hidden = false;
70
+ scrolledUp = 0;
71
+ } else if (delta > 0) {
72
+ hidden = true;
73
+ scrolledUp = 0;
74
+ } else if (delta < 0) {
75
+ scrolledUp -= delta;
76
+ if (scrolledUp < AUTO_HIDE_REVEAL_DISTANCE) return;
77
+ hidden = false;
78
+ } else return;
79
+ combinedRef.current?.setAttribute("data-hidden", String(hidden));
63
80
  };
64
81
  window.addEventListener("scroll", handleScroll, { passive: true });
65
82
  return () => window.removeEventListener("scroll", handleScroll);
@@ -1 +1 @@
1
- {"version":3,"file":"PageHeader2.mjs","names":[],"sources":["../../src/PageHeader/PageHeader.tsx"],"sourcesContent":["import React from 'react';\nimport { classNames } from '@dsn-starter-kit/core';\nimport { Button } from '../Button';\nimport { Icon } from '../Icon';\nimport { SearchInput } from '../SearchInput';\nimport { Drawer, DrawerBody, DrawerHeader, DrawerHeading } from '../Drawer';\nimport { Stack } from '../Stack';\nimport './PageHeader.css';\n\nexport type PageHeaderSticky = 'none' | 'sticky' | 'auto-hide';\nexport type PageHeaderLayout = 'default' | 'compact';\nexport type PageHeaderColorScheme = 'default' | 'inverse';\n\nexport interface PageHeaderProps extends Omit<\n React.HTMLAttributes<HTMLElement>,\n 'children'\n> {\n /**\n * Logo-inhoud — `<svg>`, `<img>`, of een `<a>` die een logo omhult.\n * De CSS past automatisch `max-block-size` toe op de directe child.\n */\n logoSlot: React.ReactNode;\n\n /**\n * Scrollgedrag van de header.\n * - `none` (standaard): header scrollt mee met de pagina\n * - `sticky`: header blijft bovenaan de viewport vastgeplakt\n * - `auto-hide`: sticky + verbergt bij scroll-down, toont bij scroll-up\n * @default 'none'\n */\n sticky?: PageHeaderSticky;\n\n /**\n * Lay-out van de header op large viewport (≥ 64em).\n * - `default`: twee horizontale banden — Masthead (logo + servicemenu + zoekveld)\n * en Navigatiebalk (primaire navigatie op accent-1 achtergrond)\n * - `compact`: één enkele rij — logo (inline-start), primaire navigatie (gecentreerd),\n * servicemenu + zoekknop (inline-end). Geeft `primaryNavigationLarge` voorrang\n * boven `primaryNavigation` (en idem voor secondary) zodat de Drawer altijd\n * de verticale variant ontvangt.\n * @default 'default'\n */\n layout?: PageHeaderLayout;\n\n /**\n * Kleurschema van de header.\n * - `default`: neutrale achtergrond met accent-1 navbar\n * - `inverse`: sterke accent-1-inverse achtergrond op navbar en compact balk\n * voor prominente branding. Het masthead blijft altijd neutraal.\n * Logo-kleuren passen zich automatisch aan via CSS context overrides.\n * @default 'default'\n */\n colorScheme?: PageHeaderColorScheme;\n\n /**\n * Initiële open-staat van het zoekpaneel (small viewport).\n * Handig voor Storybook en tests — het paneel kan daarna nog steeds\n * worden geopend/gesloten via de knop.\n * @default false\n */\n initialSearchOpen?: boolean;\n\n /**\n * Primaire navigatie-inhoud in de Drawer (small viewport) — doorgaans een verticale `<Menu>` met\n * `<MenuLink>`-items inclusief uitklapmogelijkheden voor sub-niveaus.\n */\n primaryNavigation?: React.ReactNode;\n\n /**\n * Primaire navigatie-inhoud in de Navigatiebalk (large viewport) — doorgaans een horizontale\n * `<Menu>` met alleen Level 1 `<MenuLink>`-items. Als niet meegegeven valt de component\n * terug op `primaryNavigation`.\n */\n primaryNavigationLarge?: React.ReactNode;\n\n /**\n * Servicemenu-inhoud in de Drawer (small viewport) — doorgaans een verticale `<Menu>`.\n */\n secondaryNavigation?: React.ReactNode;\n\n /**\n * Servicemenu-inhoud in de Masthead (large viewport) — doorgaans een horizontale `<Menu>`.\n * Als niet meegegeven valt de component terug op `secondaryNavigation`.\n */\n secondaryNavigationLarge?: React.ReactNode;\n\n /**\n * Inline zoekveld voor de Masthead op large viewport (SearchInput + zoekknop).\n * Optioneel — toont geen zoekveld als weggelaten.\n */\n searchSlot?: React.ReactNode;\n\n /**\n * Visuele badge-indicator bij de menuknop (small viewport), doorgaans een `<DotBadge>`.\n * Gebruik dit om ongelezen meldingen te signaleren. De badge is altijd `aria-hidden` —\n * geef de toegankelijke tekst mee via `menuButtonBadgeLabel`.\n */\n menuButtonBadge?: React.ReactNode;\n\n /**\n * Toegankelijke tekst die wordt toegevoegd aan het label van de menuknop als er een badge\n * aanwezig is, bijvoorbeeld `\"2 nieuwe berichten\"`. Wordt visueel verborgen getoond zodat\n * screenreadergebruikers de melding horen: \"Menu, 2 nieuwe berichten\".\n * Vereist als `menuButtonBadge` is meegegeven.\n */\n menuButtonBadgeLabel?: string;\n\n /**\n * Callback wanneer de navigatielade opent.\n */\n onMenuOpen?: () => void;\n\n /**\n * Callback wanneer de navigatielade sluit.\n */\n onMenuClose?: () => void;\n\n /**\n * Callback wanneer het zoekpaneel opent.\n */\n onSearchOpen?: () => void;\n\n /**\n * Callback wanneer het zoekpaneel sluit.\n */\n onSearchClose?: () => void;\n\n // ── i18n labels ────────────────────────────────────────────────────────────\n\n /**\n * Label van de menuknop en de Drawer-heading.\n * @default 'Menu'\n */\n menuButtonLabel?: string;\n\n /**\n * Label van de zoekknop wanneer het zoekpaneel gesloten is.\n * @default 'Zoeken'\n */\n searchButtonLabel?: string;\n\n /**\n * Label van de zoekknop wanneer het zoekpaneel open is.\n * @default 'Sluiten'\n */\n closeButtonLabel?: string;\n\n /**\n * Placeholder van het zoekveld in het zoekpaneel (small viewport en compact layout).\n * @default 'Zoeken…'\n */\n searchInputPlaceholder?: string;\n\n /**\n * `aria-label` van het zoekveld in het zoekpaneel.\n * @default 'Zoekopdracht'\n */\n searchInputAriaLabel?: string;\n\n /**\n * Label van de zoek-submitknop in het zoekpaneel.\n * @default 'Zoeken'\n */\n searchSubmitLabel?: string;\n\n /**\n * `aria-label` van de primaire navigatie-`<nav>` (large viewport) en\n * visueel verborgen heading in de Drawer.\n * @default 'Hoofd-navigatie'\n */\n primaryNavAriaLabel?: string;\n\n /**\n * `aria-label` van het servicemenu-`<nav>` (large viewport) en\n * visueel verborgen heading in de Drawer.\n * @default 'Service-navigatie'\n */\n secondaryNavAriaLabel?: string;\n\n /**\n * Verbergt de menuknop in de small viewport layout.\n * Gebruik dit wanneer er geen navigatie aanwezig is (bijv. formulierpagina's).\n * @default false\n */\n hideMenuButton?: boolean;\n\n /**\n * Verbergt de zoekknop in zowel de small viewport als compact layout.\n * Gebruik dit wanneer zoeken niet relevant is (bijv. formulierpagina's).\n * @default false\n */\n hideSearchButton?: boolean;\n\n className?: string;\n}\n\n/**\n * PageHeader component\n * Primaire navigatieheader voor een pagina. Mobile-first implementatie.\n *\n * Op mobile toont de header drie elementen:\n * - Menuknop (inline-start) — opent een `Drawer` met primaire en service-navigatie\n * - Logo (gecentreerd, onafhankelijk van knopbreedte via CSS-grid 1fr auto 1fr)\n * - Zoekknop (inline-end) — opent een zoekpaneel direct onder de header\n *\n * @example\n * ```tsx\n * <PageHeader\n * logoSlot={\n * <a href=\"/\">\n * <Logo aria-hidden={true} />\n * <span className=\"dsn-visually-hidden\">Starter Kit — terug naar homepage</span>\n * </a>\n * }\n * primaryNavigation={\n * <Menu orientation=\"vertical\">\n * <MenuLink href=\"/home\" level={1}>Home</MenuLink>\n * <MenuLink href=\"/over\" level={1}>Over ons</MenuLink>\n * </Menu>\n * }\n * secondaryNavigation={\n * <Menu orientation=\"vertical\">\n * <MenuLink href=\"/contact\" level={1}>Contact</MenuLink>\n * </Menu>\n * }\n * />\n * ```\n */\nexport const PageHeader = React.forwardRef<HTMLElement, PageHeaderProps>(\n (\n {\n className,\n logoSlot,\n sticky = 'none',\n layout = 'default',\n colorScheme = 'default',\n initialSearchOpen = false,\n primaryNavigation,\n primaryNavigationLarge,\n secondaryNavigation,\n secondaryNavigationLarge,\n searchSlot,\n menuButtonBadge,\n menuButtonBadgeLabel,\n onMenuOpen,\n onMenuClose,\n onSearchOpen,\n onSearchClose,\n menuButtonLabel = 'Menu',\n searchButtonLabel = 'Zoeken',\n closeButtonLabel = 'Sluiten',\n searchInputPlaceholder = 'Zoeken…',\n searchInputAriaLabel = 'Zoekopdracht',\n searchSubmitLabel = 'Zoeken',\n primaryNavAriaLabel = 'Hoofd-navigatie',\n secondaryNavAriaLabel = 'Service-navigatie',\n hideMenuButton = false,\n hideSearchButton = false,\n ...props\n },\n ref\n ) => {\n const [isMenuOpen, setIsMenuOpen] = React.useState(false);\n const [isSearchOpen, setIsSearchOpen] = React.useState(initialSearchOpen);\n\n const headerRef = React.useRef<HTMLElement>(null);\n const combinedRef = (ref as React.RefObject<HTMLElement>) ?? headerRef;\n\n const menuButtonRef = React.useRef<HTMLButtonElement>(null);\n const searchButtonRef = React.useRef<HTMLButtonElement>(null);\n const searchInputRef = React.useRef<HTMLInputElement>(null);\n const compactSearchButtonRef = React.useRef<HTMLButtonElement>(null);\n const compactSearchInputRef = React.useRef<HTMLInputElement>(null);\n\n const searchPanelId = React.useId();\n const compactSearchPanelId = React.useId();\n\n // Auto-hide: detecteer scrollrichting en toggle data-hidden attribuut\n React.useEffect(() => {\n if (sticky !== 'auto-hide') return;\n\n let lastScrollY = window.scrollY;\n\n const handleScroll = () => {\n const currentScrollY = window.scrollY;\n const isScrollingDown =\n currentScrollY > lastScrollY && currentScrollY > 100;\n combinedRef.current?.setAttribute(\n 'data-hidden',\n String(isScrollingDown)\n );\n lastScrollY = currentScrollY;\n };\n\n window.addEventListener('scroll', handleScroll, { passive: true });\n return () => window.removeEventListener('scroll', handleScroll);\n }, [sticky, combinedRef]);\n\n // Drop-shadow: zet data-scrolled op \"true\" zodra de pagina gescrolled is.\n // Werkt voor zowel sticky als auto-hide — ongeacht scrollrichting.\n React.useEffect(() => {\n if (sticky === 'none') return;\n\n const updateScrolled = () => {\n combinedRef.current?.setAttribute(\n 'data-scrolled',\n String(window.scrollY > 0)\n );\n };\n\n // Direct bij mount instellen (pagina kan al gescrolled zijn)\n updateScrolled();\n\n window.addEventListener('scroll', updateScrolled, { passive: true });\n return () => window.removeEventListener('scroll', updateScrolled);\n }, [sticky, combinedRef]);\n\n // Focus management: bij openen zoekpaneel → focus naar input (klein of compact)\n React.useEffect(() => {\n if (isSearchOpen) {\n searchInputRef.current?.focus();\n compactSearchInputRef.current?.focus();\n }\n }, [isSearchOpen]);\n\n const handleMenuOpen = () => {\n setIsMenuOpen(true);\n onMenuOpen?.();\n };\n\n const handleMenuClose = () => {\n setIsMenuOpen(false);\n onMenuClose?.();\n // Focus terug naar menuknop\n menuButtonRef.current?.focus();\n };\n\n const handleSearchToggle = () => {\n const opening = !isSearchOpen;\n setIsSearchOpen(opening);\n if (opening) {\n onSearchOpen?.();\n } else {\n onSearchClose?.();\n // Focus terug naar zoek/sluit-knop — afhankelijk van actieve layout\n searchButtonRef.current?.focus();\n compactSearchButtonRef.current?.focus();\n }\n };\n\n const classes = classNames(\n 'dsn-page-header',\n sticky === 'sticky' && 'dsn-page-header--sticky',\n sticky === 'auto-hide' && 'dsn-page-header--auto-hide',\n layout === 'compact' && 'dsn-page-header--compact',\n colorScheme === 'inverse' && 'dsn-page-header--inverse',\n className\n );\n\n return (\n <>\n <header\n ref={combinedRef}\n className={classes}\n {...(sticky === 'auto-hide' ? { 'data-hidden': 'false' } : {})}\n {...props}\n >\n {/* ----------------------------------------------------------------\n Small viewport layout (verborgen boven 64em via CSS display:none)\n ---------------------------------------------------------------- */}\n <div className=\"dsn-page-header__small-layout\">\n <div className=\"dsn-page-header__inner\">\n {/* Inline-start: menuknop */}\n <div className=\"dsn-page-header__start\">\n {!hideMenuButton && (\n <Button\n ref={menuButtonRef}\n variant=\"subtle\"\n onClick={handleMenuOpen}\n iconStart={<Icon name=\"menu\" aria-hidden />}\n >\n {menuButtonLabel}\n {menuButtonBadgeLabel && (\n <span className=\"dsn-visually-hidden\">\n , {menuButtonBadgeLabel}\n </span>\n )}\n {menuButtonBadge}\n </Button>\n )}\n </div>\n\n {/* Gecentreerd logo */}\n <div className=\"dsn-page-header__logo\">{logoSlot}</div>\n\n {/* Inline-end: zoekknop / sluitknop */}\n <div className=\"dsn-page-header__end\">\n {!hideSearchButton && (\n <Button\n ref={searchButtonRef}\n variant=\"subtle\"\n aria-expanded={isSearchOpen}\n aria-controls={searchPanelId}\n onClick={handleSearchToggle}\n iconStart={\n <Icon name={isSearchOpen ? 'x' : 'search'} aria-hidden />\n }\n >\n {isSearchOpen ? closeButtonLabel : searchButtonLabel}\n </Button>\n )}\n </div>\n </div>\n\n {/* Zoekpaneel */}\n <div\n id={searchPanelId}\n className=\"dsn-page-header__search-panel\"\n hidden={!isSearchOpen}\n >\n <div className=\"dsn-page-header__search-inner\">\n <SearchInput\n ref={searchInputRef}\n placeholder={searchInputPlaceholder}\n aria-label={searchInputAriaLabel}\n />\n <Button variant=\"strong\">{searchSubmitLabel}</Button>\n </div>\n </div>\n </div>\n\n {/* ----------------------------------------------------------------\n Large viewport layout (zichtbaar boven 64em, alleen bij layout=\"default\")\n ---------------------------------------------------------------- */}\n {layout === 'default' && (\n <div className=\"dsn-page-header__large-layout\">\n {/* Masthead: neutrale achtergrond — logo + servicemenu + zoek */}\n <div className=\"dsn-page-header__masthead\">\n <div className=\"dsn-page-header__masthead-inner\">\n <div className=\"dsn-page-header__logo\">{logoSlot}</div>\n\n <div className=\"dsn-page-header__secondary-nav\">\n {(secondaryNavigationLarge ?? secondaryNavigation) && (\n <nav aria-label={secondaryNavAriaLabel}>\n {secondaryNavigationLarge ?? secondaryNavigation}\n </nav>\n )}\n {searchSlot && (\n <div className=\"dsn-page-header__searchbox\">\n {searchSlot}\n </div>\n )}\n </div>\n </div>\n </div>\n\n {/* Navigatiebalk: accent-1 achtergrond — primaire navigatie (large viewport) */}\n <div className=\"dsn-page-header__navbar\">\n <div className=\"dsn-page-header__navbar-inner\">\n {(primaryNavigationLarge ?? primaryNavigation) && (\n <nav aria-label={primaryNavAriaLabel}>\n {primaryNavigationLarge ?? primaryNavigation}\n </nav>\n )}\n </div>\n </div>\n </div>\n )}\n\n {/* ----------------------------------------------------------------\n Compact layout (zichtbaar boven 64em wanneer layout=\"compact\")\n Eén rij: logo (1fr) | primaire navigatie (auto) | servicemenu + zoek (1fr)\n ---------------------------------------------------------------- */}\n {layout === 'compact' && (\n <div className=\"dsn-page-header__compact-layout\">\n <div className=\"dsn-page-header__compact-inner\">\n {/* Logo — inline-start (eerste gridkolom) */}\n <div className=\"dsn-page-header__logo\">{logoSlot}</div>\n\n {/* Primaire navigatie — gecentreerd (middelste gridkolom)\n Gebruikt primaryNavigationLarge als aanwezig, anders primaryNavigation.\n Dit garandeert dat de Drawer altijd de verticale variant ontvangt. */}\n <div className=\"dsn-page-header__compact-primary-nav\">\n {(primaryNavigationLarge ?? primaryNavigation) && (\n <nav aria-label={primaryNavAriaLabel}>\n {primaryNavigationLarge ?? primaryNavigation}\n </nav>\n )}\n </div>\n\n {/* Servicemenu + zoekknop (icon-only) — inline-end (derde gridkolom) */}\n <div className=\"dsn-page-header__compact-secondary\">\n {(secondaryNavigationLarge ?? secondaryNavigation) && (\n <nav aria-label={secondaryNavAriaLabel}>\n {secondaryNavigationLarge ?? secondaryNavigation}\n </nav>\n )}\n {!hideSearchButton && (\n <Button\n ref={compactSearchButtonRef}\n variant=\"subtle\"\n iconOnly\n aria-expanded={isSearchOpen}\n aria-controls={compactSearchPanelId}\n onClick={handleSearchToggle}\n iconStart={\n <Icon\n name={isSearchOpen ? 'x' : 'search'}\n aria-hidden\n />\n }\n >\n {isSearchOpen ? closeButtonLabel : searchButtonLabel}\n </Button>\n )}\n </div>\n </div>\n\n {/* Zoekpaneel compact layout */}\n <div\n id={compactSearchPanelId}\n className=\"dsn-page-header__search-panel\"\n hidden={!isSearchOpen}\n >\n <div className=\"dsn-page-header__search-inner\">\n <SearchInput\n ref={compactSearchInputRef}\n placeholder={searchInputPlaceholder}\n aria-label={searchInputAriaLabel}\n />\n <Button variant=\"strong\">{searchSubmitLabel}</Button>\n </div>\n </div>\n </div>\n )}\n </header>\n\n {/* Navigatielade (sibling aan PageHeader, altijd in DOM) */}\n <Drawer\n isOpen={isMenuOpen}\n onClose={handleMenuClose}\n side=\"left\"\n triggerRef={menuButtonRef}\n >\n <DrawerHeader>\n <DrawerHeading>{menuButtonLabel}</DrawerHeading>\n </DrawerHeader>\n <DrawerBody>\n <Stack space=\"5xl\">\n {primaryNavigation && (\n <nav aria-label={primaryNavAriaLabel}>{primaryNavigation}</nav>\n )}\n {secondaryNavigation && (\n <nav aria-label={secondaryNavAriaLabel}>\n {secondaryNavigation}\n </nav>\n )}\n </Stack>\n </DrawerBody>\n </Drawer>\n </>\n );\n }\n);\n\nPageHeader.displayName = 'PageHeader';\n"],"mappings":"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"}
1
+ {"version":3,"file":"PageHeader2.mjs","names":[],"sources":["../../src/PageHeader/PageHeader.tsx"],"sourcesContent":["import React from 'react';\nimport { classNames } from '@dsn-starter-kit/core';\nimport { Button } from '../Button';\nimport { Icon } from '../Icon';\nimport { SearchInput } from '../SearchInput';\nimport { Drawer, DrawerBody, DrawerHeader, DrawerHeading } from '../Drawer';\nimport { Stack } from '../Stack';\nimport './PageHeader.css';\n\nexport type PageHeaderSticky = 'none' | 'sticky' | 'auto-hide';\n\n/** Auto-hide: boven deze scrollpositie (px) blijft de header altijd zichtbaar */\nconst AUTO_HIDE_OFFSET = 100;\n\n/** Auto-hide: zoveel px omhoog scrollen voordat de header terugkomt */\nconst AUTO_HIDE_REVEAL_DISTANCE = 64;\nexport type PageHeaderLayout = 'default' | 'compact';\nexport type PageHeaderColorScheme = 'default' | 'inverse';\n\nexport interface PageHeaderProps extends Omit<\n React.HTMLAttributes<HTMLElement>,\n 'children'\n> {\n /**\n * Logo-inhoud — `<svg>`, `<img>`, of een `<a>` die een logo omhult.\n * De CSS past automatisch `max-block-size` toe op de directe child.\n */\n logoSlot: React.ReactNode;\n\n /**\n * Scrollgedrag van de header.\n * - `none` (standaard): header scrollt mee met de pagina\n * - `sticky`: header blijft bovenaan de viewport vastgeplakt\n * - `auto-hide`: sticky + verbergt bij scroll-down, toont bij scroll-up\n * @default 'none'\n */\n sticky?: PageHeaderSticky;\n\n /**\n * Lay-out van de header op large viewport (≥ 64em).\n * - `default`: twee horizontale banden — Masthead (logo + servicemenu + zoekveld)\n * en Navigatiebalk (primaire navigatie op accent-1 achtergrond)\n * - `compact`: één enkele rij — logo (inline-start), primaire navigatie (gecentreerd),\n * servicemenu + zoekknop (inline-end). Geeft `primaryNavigationLarge` voorrang\n * boven `primaryNavigation` (en idem voor secondary) zodat de Drawer altijd\n * de verticale variant ontvangt.\n * @default 'default'\n */\n layout?: PageHeaderLayout;\n\n /**\n * Kleurschema van de header.\n * - `default`: neutrale achtergrond met accent-1 navbar\n * - `inverse`: sterke accent-1-inverse achtergrond op navbar en compact balk\n * voor prominente branding. Het masthead blijft altijd neutraal.\n * Logo-kleuren passen zich automatisch aan via CSS context overrides.\n * @default 'default'\n */\n colorScheme?: PageHeaderColorScheme;\n\n /**\n * Initiële open-staat van het zoekpaneel (small viewport).\n * Handig voor Storybook en tests — het paneel kan daarna nog steeds\n * worden geopend/gesloten via de knop.\n * @default false\n */\n initialSearchOpen?: boolean;\n\n /**\n * Primaire navigatie-inhoud in de Drawer (small viewport) — doorgaans een verticale `<Menu>` met\n * `<MenuLink>`-items inclusief uitklapmogelijkheden voor sub-niveaus.\n */\n primaryNavigation?: React.ReactNode;\n\n /**\n * Primaire navigatie-inhoud in de Navigatiebalk (large viewport) — doorgaans een horizontale\n * `<Menu>` met alleen Level 1 `<MenuLink>`-items. Als niet meegegeven valt de component\n * terug op `primaryNavigation`.\n */\n primaryNavigationLarge?: React.ReactNode;\n\n /**\n * Servicemenu-inhoud in de Drawer (small viewport) — doorgaans een verticale `<Menu>`.\n */\n secondaryNavigation?: React.ReactNode;\n\n /**\n * Servicemenu-inhoud in de Masthead (large viewport) — doorgaans een horizontale `<Menu>`.\n * Als niet meegegeven valt de component terug op `secondaryNavigation`.\n */\n secondaryNavigationLarge?: React.ReactNode;\n\n /**\n * Inline zoekveld voor de Masthead op large viewport (SearchInput + zoekknop).\n * Optioneel — toont geen zoekveld als weggelaten.\n */\n searchSlot?: React.ReactNode;\n\n /**\n * Visuele badge-indicator bij de menuknop (small viewport), doorgaans een `<DotBadge>`.\n * Gebruik dit om ongelezen meldingen te signaleren. De badge is altijd `aria-hidden` —\n * geef de toegankelijke tekst mee via `menuButtonBadgeLabel`.\n */\n menuButtonBadge?: React.ReactNode;\n\n /**\n * Toegankelijke tekst die wordt toegevoegd aan het label van de menuknop als er een badge\n * aanwezig is, bijvoorbeeld `\"2 nieuwe berichten\"`. Wordt visueel verborgen getoond zodat\n * screenreadergebruikers de melding horen: \"Menu, 2 nieuwe berichten\".\n * Vereist als `menuButtonBadge` is meegegeven.\n */\n menuButtonBadgeLabel?: string;\n\n /**\n * Callback wanneer de navigatielade opent.\n */\n onMenuOpen?: () => void;\n\n /**\n * Callback wanneer de navigatielade sluit.\n */\n onMenuClose?: () => void;\n\n /**\n * Callback wanneer het zoekpaneel opent.\n */\n onSearchOpen?: () => void;\n\n /**\n * Callback wanneer het zoekpaneel sluit.\n */\n onSearchClose?: () => void;\n\n // ── i18n labels ────────────────────────────────────────────────────────────\n\n /**\n * Label van de menuknop en de Drawer-heading.\n * @default 'Menu'\n */\n menuButtonLabel?: string;\n\n /**\n * Label van de zoekknop wanneer het zoekpaneel gesloten is.\n * @default 'Zoeken'\n */\n searchButtonLabel?: string;\n\n /**\n * Label van de zoekknop wanneer het zoekpaneel open is.\n * @default 'Sluiten'\n */\n closeButtonLabel?: string;\n\n /**\n * Placeholder van het zoekveld in het zoekpaneel (small viewport en compact layout).\n * @default 'Zoeken…'\n */\n searchInputPlaceholder?: string;\n\n /**\n * `aria-label` van het zoekveld in het zoekpaneel.\n * @default 'Zoekopdracht'\n */\n searchInputAriaLabel?: string;\n\n /**\n * Label van de zoek-submitknop in het zoekpaneel.\n * @default 'Zoeken'\n */\n searchSubmitLabel?: string;\n\n /**\n * `aria-label` van de primaire navigatie-`<nav>` (large viewport) en\n * visueel verborgen heading in de Drawer.\n * @default 'Hoofd-navigatie'\n */\n primaryNavAriaLabel?: string;\n\n /**\n * `aria-label` van het servicemenu-`<nav>` (large viewport) en\n * visueel verborgen heading in de Drawer.\n * @default 'Service-navigatie'\n */\n secondaryNavAriaLabel?: string;\n\n /**\n * Verbergt de menuknop in de small viewport layout.\n * Gebruik dit wanneer er geen navigatie aanwezig is (bijv. formulierpagina's).\n * @default false\n */\n hideMenuButton?: boolean;\n\n /**\n * Verbergt de zoekknop in zowel de small viewport als compact layout.\n * Gebruik dit wanneer zoeken niet relevant is (bijv. formulierpagina's).\n * @default false\n */\n hideSearchButton?: boolean;\n\n className?: string;\n}\n\n/**\n * PageHeader component\n * Primaire navigatieheader voor een pagina. Mobile-first implementatie.\n *\n * Op mobile toont de header drie elementen:\n * - Menuknop (inline-start) — opent een `Drawer` met primaire en service-navigatie\n * - Logo (gecentreerd, onafhankelijk van knopbreedte via CSS-grid 1fr auto 1fr)\n * - Zoekknop (inline-end) — opent een zoekpaneel direct onder de header\n *\n * @example\n * ```tsx\n * <PageHeader\n * logoSlot={\n * <a href=\"/\">\n * <Logo aria-hidden={true} />\n * <span className=\"dsn-visually-hidden\">Starter Kit — terug naar homepage</span>\n * </a>\n * }\n * primaryNavigation={\n * <Menu orientation=\"vertical\">\n * <MenuLink href=\"/home\" level={1}>Home</MenuLink>\n * <MenuLink href=\"/over\" level={1}>Over ons</MenuLink>\n * </Menu>\n * }\n * secondaryNavigation={\n * <Menu orientation=\"vertical\">\n * <MenuLink href=\"/contact\" level={1}>Contact</MenuLink>\n * </Menu>\n * }\n * />\n * ```\n */\nexport const PageHeader = React.forwardRef<HTMLElement, PageHeaderProps>(\n (\n {\n className,\n logoSlot,\n sticky = 'none',\n layout = 'default',\n colorScheme = 'default',\n initialSearchOpen = false,\n primaryNavigation,\n primaryNavigationLarge,\n secondaryNavigation,\n secondaryNavigationLarge,\n searchSlot,\n menuButtonBadge,\n menuButtonBadgeLabel,\n onMenuOpen,\n onMenuClose,\n onSearchOpen,\n onSearchClose,\n menuButtonLabel = 'Menu',\n searchButtonLabel = 'Zoeken',\n closeButtonLabel = 'Sluiten',\n searchInputPlaceholder = 'Zoeken…',\n searchInputAriaLabel = 'Zoekopdracht',\n searchSubmitLabel = 'Zoeken',\n primaryNavAriaLabel = 'Hoofd-navigatie',\n secondaryNavAriaLabel = 'Service-navigatie',\n hideMenuButton = false,\n hideSearchButton = false,\n ...props\n },\n ref\n ) => {\n const [isMenuOpen, setIsMenuOpen] = React.useState(false);\n const [isSearchOpen, setIsSearchOpen] = React.useState(initialSearchOpen);\n\n const headerRef = React.useRef<HTMLElement>(null);\n const combinedRef = (ref as React.RefObject<HTMLElement>) ?? headerRef;\n\n const menuButtonRef = React.useRef<HTMLButtonElement>(null);\n const searchButtonRef = React.useRef<HTMLButtonElement>(null);\n const searchInputRef = React.useRef<HTMLInputElement>(null);\n const compactSearchButtonRef = React.useRef<HTMLButtonElement>(null);\n const compactSearchInputRef = React.useRef<HTMLInputElement>(null);\n\n const searchPanelId = React.useId();\n const compactSearchPanelId = React.useId();\n\n // Auto-hide: detecteer scrollrichting en toggle data-hidden attribuut\n React.useEffect(() => {\n if (sticky !== 'auto-hide') return;\n\n let lastScrollY = window.scrollY;\n // Afgelegde afstand omhoog sinds de laatste scroll-down. De header komt\n // pas terug na AUTO_HIDE_REVEAL_DISTANCE, zodat een kleine beweging\n // omhoog (of een terugverende swipe) hem niet direct in beeld schuift.\n let scrolledUp = 0;\n\n const handleScroll = () => {\n const currentScrollY = window.scrollY;\n const delta = currentScrollY - lastScrollY;\n lastScrollY = currentScrollY;\n\n let hidden: boolean;\n if (currentScrollY <= AUTO_HIDE_OFFSET) {\n hidden = false;\n scrolledUp = 0;\n } else if (delta > 0) {\n hidden = true;\n scrolledUp = 0;\n } else if (delta < 0) {\n scrolledUp -= delta;\n if (scrolledUp < AUTO_HIDE_REVEAL_DISTANCE) return;\n hidden = false;\n } else {\n return;\n }\n\n combinedRef.current?.setAttribute('data-hidden', String(hidden));\n };\n\n window.addEventListener('scroll', handleScroll, { passive: true });\n return () => window.removeEventListener('scroll', handleScroll);\n }, [sticky, combinedRef]);\n\n // Drop-shadow: zet data-scrolled op \"true\" zodra de pagina gescrolled is.\n // Werkt voor zowel sticky als auto-hide — ongeacht scrollrichting.\n React.useEffect(() => {\n if (sticky === 'none') return;\n\n const updateScrolled = () => {\n combinedRef.current?.setAttribute(\n 'data-scrolled',\n String(window.scrollY > 0)\n );\n };\n\n // Direct bij mount instellen (pagina kan al gescrolled zijn)\n updateScrolled();\n\n window.addEventListener('scroll', updateScrolled, { passive: true });\n return () => window.removeEventListener('scroll', updateScrolled);\n }, [sticky, combinedRef]);\n\n // Focus management: bij openen zoekpaneel → focus naar input (klein of compact)\n React.useEffect(() => {\n if (isSearchOpen) {\n searchInputRef.current?.focus();\n compactSearchInputRef.current?.focus();\n }\n }, [isSearchOpen]);\n\n const handleMenuOpen = () => {\n setIsMenuOpen(true);\n onMenuOpen?.();\n };\n\n const handleMenuClose = () => {\n setIsMenuOpen(false);\n onMenuClose?.();\n // Focus terug naar menuknop\n menuButtonRef.current?.focus();\n };\n\n const handleSearchToggle = () => {\n const opening = !isSearchOpen;\n setIsSearchOpen(opening);\n if (opening) {\n onSearchOpen?.();\n } else {\n onSearchClose?.();\n // Focus terug naar zoek/sluit-knop — afhankelijk van actieve layout\n searchButtonRef.current?.focus();\n compactSearchButtonRef.current?.focus();\n }\n };\n\n const classes = classNames(\n 'dsn-page-header',\n sticky === 'sticky' && 'dsn-page-header--sticky',\n sticky === 'auto-hide' && 'dsn-page-header--auto-hide',\n layout === 'compact' && 'dsn-page-header--compact',\n colorScheme === 'inverse' && 'dsn-page-header--inverse',\n className\n );\n\n return (\n <>\n <header\n ref={combinedRef}\n className={classes}\n {...(sticky === 'auto-hide' ? { 'data-hidden': 'false' } : {})}\n {...props}\n >\n {/* ----------------------------------------------------------------\n Small viewport layout (verborgen boven 64em via CSS display:none)\n ---------------------------------------------------------------- */}\n <div className=\"dsn-page-header__small-layout\">\n <div className=\"dsn-page-header__inner\">\n {/* Inline-start: menuknop */}\n <div className=\"dsn-page-header__start\">\n {!hideMenuButton && (\n <Button\n ref={menuButtonRef}\n variant=\"subtle\"\n onClick={handleMenuOpen}\n iconStart={<Icon name=\"menu\" aria-hidden />}\n >\n {menuButtonLabel}\n {menuButtonBadgeLabel && (\n <span className=\"dsn-visually-hidden\">\n , {menuButtonBadgeLabel}\n </span>\n )}\n {menuButtonBadge}\n </Button>\n )}\n </div>\n\n {/* Gecentreerd logo */}\n <div className=\"dsn-page-header__logo\">{logoSlot}</div>\n\n {/* Inline-end: zoekknop / sluitknop */}\n <div className=\"dsn-page-header__end\">\n {!hideSearchButton && (\n <Button\n ref={searchButtonRef}\n variant=\"subtle\"\n aria-expanded={isSearchOpen}\n aria-controls={searchPanelId}\n onClick={handleSearchToggle}\n iconStart={\n <Icon name={isSearchOpen ? 'x' : 'search'} aria-hidden />\n }\n >\n {isSearchOpen ? closeButtonLabel : searchButtonLabel}\n </Button>\n )}\n </div>\n </div>\n\n {/* Zoekpaneel */}\n <div\n id={searchPanelId}\n className=\"dsn-page-header__search-panel\"\n hidden={!isSearchOpen}\n >\n <div className=\"dsn-page-header__search-inner\">\n <SearchInput\n ref={searchInputRef}\n placeholder={searchInputPlaceholder}\n aria-label={searchInputAriaLabel}\n />\n <Button variant=\"strong\">{searchSubmitLabel}</Button>\n </div>\n </div>\n </div>\n\n {/* ----------------------------------------------------------------\n Large viewport layout (zichtbaar boven 64em, alleen bij layout=\"default\")\n ---------------------------------------------------------------- */}\n {layout === 'default' && (\n <div className=\"dsn-page-header__large-layout\">\n {/* Masthead: neutrale achtergrond — logo + servicemenu + zoek */}\n <div className=\"dsn-page-header__masthead\">\n <div className=\"dsn-page-header__masthead-inner\">\n <div className=\"dsn-page-header__logo\">{logoSlot}</div>\n\n <div className=\"dsn-page-header__secondary-nav\">\n {(secondaryNavigationLarge ?? secondaryNavigation) && (\n <nav aria-label={secondaryNavAriaLabel}>\n {secondaryNavigationLarge ?? secondaryNavigation}\n </nav>\n )}\n {searchSlot && (\n <div className=\"dsn-page-header__searchbox\">\n {searchSlot}\n </div>\n )}\n </div>\n </div>\n </div>\n\n {/* Navigatiebalk: accent-1 achtergrond — primaire navigatie (large viewport) */}\n <div className=\"dsn-page-header__navbar\">\n <div className=\"dsn-page-header__navbar-inner\">\n {(primaryNavigationLarge ?? primaryNavigation) && (\n <nav aria-label={primaryNavAriaLabel}>\n {primaryNavigationLarge ?? primaryNavigation}\n </nav>\n )}\n </div>\n </div>\n </div>\n )}\n\n {/* ----------------------------------------------------------------\n Compact layout (zichtbaar boven 64em wanneer layout=\"compact\")\n Eén rij: logo (1fr) | primaire navigatie (auto) | servicemenu + zoek (1fr)\n ---------------------------------------------------------------- */}\n {layout === 'compact' && (\n <div className=\"dsn-page-header__compact-layout\">\n <div className=\"dsn-page-header__compact-inner\">\n {/* Logo — inline-start (eerste gridkolom) */}\n <div className=\"dsn-page-header__logo\">{logoSlot}</div>\n\n {/* Primaire navigatie — gecentreerd (middelste gridkolom)\n Gebruikt primaryNavigationLarge als aanwezig, anders primaryNavigation.\n Dit garandeert dat de Drawer altijd de verticale variant ontvangt. */}\n <div className=\"dsn-page-header__compact-primary-nav\">\n {(primaryNavigationLarge ?? primaryNavigation) && (\n <nav aria-label={primaryNavAriaLabel}>\n {primaryNavigationLarge ?? primaryNavigation}\n </nav>\n )}\n </div>\n\n {/* Servicemenu + zoekknop (icon-only) — inline-end (derde gridkolom) */}\n <div className=\"dsn-page-header__compact-secondary\">\n {(secondaryNavigationLarge ?? secondaryNavigation) && (\n <nav aria-label={secondaryNavAriaLabel}>\n {secondaryNavigationLarge ?? secondaryNavigation}\n </nav>\n )}\n {!hideSearchButton && (\n <Button\n ref={compactSearchButtonRef}\n variant=\"subtle\"\n iconOnly\n aria-expanded={isSearchOpen}\n aria-controls={compactSearchPanelId}\n onClick={handleSearchToggle}\n iconStart={\n <Icon\n name={isSearchOpen ? 'x' : 'search'}\n aria-hidden\n />\n }\n >\n {isSearchOpen ? closeButtonLabel : searchButtonLabel}\n </Button>\n )}\n </div>\n </div>\n\n {/* Zoekpaneel compact layout */}\n <div\n id={compactSearchPanelId}\n className=\"dsn-page-header__search-panel\"\n hidden={!isSearchOpen}\n >\n <div className=\"dsn-page-header__search-inner\">\n <SearchInput\n ref={compactSearchInputRef}\n placeholder={searchInputPlaceholder}\n aria-label={searchInputAriaLabel}\n />\n <Button variant=\"strong\">{searchSubmitLabel}</Button>\n </div>\n </div>\n </div>\n )}\n </header>\n\n {/* Navigatielade (sibling aan PageHeader, altijd in DOM) */}\n <Drawer\n isOpen={isMenuOpen}\n onClose={handleMenuClose}\n side=\"left\"\n triggerRef={menuButtonRef}\n >\n <DrawerHeader>\n <DrawerHeading>{menuButtonLabel}</DrawerHeading>\n </DrawerHeader>\n <DrawerBody>\n <Stack space=\"5xl\">\n {primaryNavigation && (\n <nav aria-label={primaryNavAriaLabel}>{primaryNavigation}</nav>\n )}\n {secondaryNavigation && (\n <nav aria-label={secondaryNavAriaLabel}>\n {secondaryNavigation}\n </nav>\n )}\n </Stack>\n </DrawerBody>\n </Drawer>\n </>\n );\n }\n);\n\nPageHeader.displayName = 'PageHeader';\n"],"mappings":"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"}
@@ -1,6 +1,11 @@
1
1
  import React from "react";
2
- import { FormControlWidth } from "@dsn-starter-kit/core";
2
+ import { FormControlInlineSize } from "@dsn-starter-kit/core";
3
3
  //#region src/PasswordInput/PasswordInput.d.ts
4
+ /**
5
+ * Inline-size variants offered by PasswordInput. xs and sm are left out: they
6
+ * are too narrow for the content this field is meant for.
7
+ */
8
+ type PasswordInputInlineSize = Exclude<FormControlInlineSize, 'xs' | 'sm'>;
4
9
  type PasswordAutocomplete = 'current-password' | 'new-password' | 'off';
5
10
  interface PasswordInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
6
11
  /**
@@ -17,10 +22,10 @@ interface PasswordInputProps extends Omit<React.InputHTMLAttributes<HTMLInputEle
17
22
  */
18
23
  invalid?: boolean;
19
24
  /**
20
- * Width variant for the input
21
- * @default undefined (uses default max-width from form-control)
25
+ * Inline-size variant for the input (md, lg, xl or full)
26
+ * @default undefined (uses default max-inline-size from form-control)
22
27
  */
23
- width?: FormControlWidth;
28
+ inlineSize?: PasswordInputInlineSize;
24
29
  /**
25
30
  * Additional CSS class names
26
31
  */
@@ -50,5 +55,5 @@ interface PasswordInputProps extends Omit<React.InputHTMLAttributes<HTMLInputEle
50
55
  */
51
56
  declare const PasswordInput: React.ForwardRefExoticComponent<PasswordInputProps & React.RefAttributes<HTMLInputElement>>;
52
57
  //#endregion
53
- export { PasswordAutocomplete, PasswordInput, PasswordInputProps };
58
+ export { PasswordAutocomplete, PasswordInput, PasswordInputInlineSize, PasswordInputProps };
54
59
  //# sourceMappingURL=PasswordInput.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"PasswordInput.d.mts","names":[],"sources":["../../src/PasswordInput/PasswordInput.tsx"],"mappings":";;;KAIY;UAEK,2BAA2B,KAC1C,MAAM,oBAAoB;;;;;;;;EAU1B,uBAAuB;;;;;EAMvB;;;;;EAMA,QAAQ;;;;EAKR;;;;;;;;;;;;;;;;;;;;;;;;cAyBW,eAAa,MAAA,0BAAA,qBAAA,MAAA,cAAA"}
1
+ {"version":3,"file":"PasswordInput.d.mts","names":[],"sources":["../../src/PasswordInput/PasswordInput.tsx"],"mappings":";;;;;;;KAQY,0BAA0B,QACpC;KAIU;UAEK,2BAA2B,KAC1C,MAAM,oBAAoB;;;;;;;;EAU1B,uBAAuB;;;;;EAMvB;;;;;EAMA,aAAa;;;;EAKb;;;;;;;;;;;;;;;;;;;;;;;;cAyBW,eAAa,MAAA,0BAAA,qBAAA,MAAA,cAAA"}