@ahrowe/ui 0.37.0 → 0.39.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 (123) hide show
  1. package/README.md +5 -2
  2. package/dist/cjs-types/common/commandPalette/commandPalette.d.ts +4 -0
  3. package/dist/cjs-types/common/commandPalette/commandPalette.types.d.ts +42 -0
  4. package/dist/cjs-types/common/commandPalette/filterCommands.d.ts +15 -0
  5. package/dist/cjs-types/common/commandPalette/index.d.ts +2 -0
  6. package/dist/cjs-types/common/configProvider/configProvider.types.d.ts +6 -0
  7. package/dist/cjs-types/common/copyButton/copyButton.d.ts +4 -0
  8. package/dist/cjs-types/common/copyButton/copyButton.types.d.ts +36 -0
  9. package/dist/cjs-types/common/copyButton/index.d.ts +2 -0
  10. package/dist/cjs-types/common/datePicker/components/datePickerMonth/datePickerMonth.types.d.ts +6 -6
  11. package/dist/cjs-types/common/datePicker/dateFunctions.d.ts +10 -19
  12. package/dist/cjs-types/common/datePicker/datePicker.types.d.ts +20 -14
  13. package/dist/cjs-types/common/datePicker/index.d.ts +1 -0
  14. package/dist/cjs-types/common/errorBoundary/errorBoundary.d.ts +0 -3
  15. package/dist/cjs-types/common/errorBoundary/errorBoundary.types.d.ts +2 -3
  16. package/dist/cjs-types/common/hooks/index.d.ts +2 -0
  17. package/dist/cjs-types/common/hooks/useClipboard.d.ts +16 -0
  18. package/dist/cjs-types/common/inputDropdown/inputDropdown.types.d.ts +7 -1
  19. package/dist/cjs-types/common/menu/menu.types.d.ts +2 -0
  20. package/dist/cjs-types/common/popover/popover.types.d.ts +5 -0
  21. package/dist/cjs-types/common/themeProvider/themeProvider.d.ts +8 -7
  22. package/dist/cjs-types/common/utils/day.d.ts +16 -0
  23. package/dist/cjs-types/common/weatherIcon/index.d.ts +3 -0
  24. package/dist/cjs-types/common/weatherIcon/parts.d.ts +26 -0
  25. package/dist/cjs-types/common/weatherIcon/weatherIcon.d.ts +4 -0
  26. package/dist/cjs-types/common/weatherIcon/weatherIcon.types.d.ts +44 -0
  27. package/dist/cjs-types/index.d.ts +7 -1
  28. package/dist/esm/common/buttonGroup/buttonGroup.module.mjs.map +1 -1
  29. package/dist/esm/common/commandPalette/commandPalette.mjs +2 -0
  30. package/dist/esm/common/commandPalette/commandPalette.mjs.map +1 -0
  31. package/dist/esm/common/commandPalette/commandPalette.module.mjs +2 -0
  32. package/dist/esm/common/commandPalette/commandPalette.module.mjs.map +1 -0
  33. package/dist/esm/common/commandPalette/filterCommands.mjs +2 -0
  34. package/dist/esm/common/commandPalette/filterCommands.mjs.map +1 -0
  35. package/dist/esm/common/copyButton/copyButton.mjs +2 -0
  36. package/dist/esm/common/copyButton/copyButton.mjs.map +1 -0
  37. package/dist/esm/common/copyButton/copyButton.module.mjs +2 -0
  38. package/dist/esm/common/copyButton/copyButton.module.mjs.map +1 -0
  39. package/dist/esm/common/datePicker/components/datePickerMonth/datePickerMonth.mjs +1 -1
  40. package/dist/esm/common/datePicker/components/datePickerMonth/datePickerMonth.mjs.map +1 -1
  41. package/dist/esm/common/datePicker/dateFunctions.mjs +1 -1
  42. package/dist/esm/common/datePicker/dateFunctions.mjs.map +1 -1
  43. package/dist/esm/common/datePicker/datePicker.mjs +1 -1
  44. package/dist/esm/common/datePicker/datePicker.mjs.map +1 -1
  45. package/dist/esm/common/dropdown/dropdown.mjs +1 -1
  46. package/dist/esm/common/dropdown/dropdown.module.mjs.map +1 -1
  47. package/dist/esm/common/errorBoundary/errorBoundary.mjs +2 -2
  48. package/dist/esm/common/errorBoundary/errorBoundary.mjs.map +1 -1
  49. package/dist/esm/common/errorBoundary/errorBoundary.module.mjs +1 -1
  50. package/dist/esm/common/errorBoundary/errorBoundary.module.mjs.map +1 -1
  51. package/dist/esm/common/hooks/useClipboard.mjs +2 -0
  52. package/dist/esm/common/hooks/useClipboard.mjs.map +1 -0
  53. package/dist/esm/common/hooks/useFocusBoundary.mjs +1 -1
  54. package/dist/esm/common/hooks/useFocusBoundary.mjs.map +1 -1
  55. package/dist/esm/common/inputDropdown/inputDropdown.mjs +1 -1
  56. package/dist/esm/common/inputDropdown/inputDropdown.mjs.map +1 -1
  57. package/dist/esm/common/inputDropdown/inputDropdown.module.mjs +1 -1
  58. package/dist/esm/common/inputDropdown/inputDropdown.module.mjs.map +1 -1
  59. package/dist/esm/common/menu/menu.mjs +1 -1
  60. package/dist/esm/common/menu/menu.mjs.map +1 -1
  61. package/dist/esm/common/menu/menu.types.mjs.map +1 -1
  62. package/dist/esm/common/popover/popover.mjs +1 -1
  63. package/dist/esm/common/popover/popover.mjs.map +1 -1
  64. package/dist/esm/common/popover/popover.types.mjs.map +1 -1
  65. package/dist/esm/common/themeProvider/themeProvider.mjs +1 -1
  66. package/dist/esm/common/themeProvider/themeProvider.mjs.map +1 -1
  67. package/dist/esm/common/timer/timer.mjs +1 -1
  68. package/dist/esm/common/timer/timer.mjs.map +1 -1
  69. package/dist/esm/common/utils/day.mjs +2 -0
  70. package/dist/esm/common/utils/day.mjs.map +1 -0
  71. package/dist/esm/common/weatherIcon/parts.mjs +2 -0
  72. package/dist/esm/common/weatherIcon/parts.mjs.map +1 -0
  73. package/dist/esm/common/weatherIcon/weatherIcon.mjs +2 -0
  74. package/dist/esm/common/weatherIcon/weatherIcon.mjs.map +1 -0
  75. package/dist/esm/common/weatherIcon/weatherIcon.module.mjs +2 -0
  76. package/dist/esm/common/weatherIcon/weatherIcon.module.mjs.map +1 -0
  77. package/dist/esm/common/weatherIcon/weatherIcon.types.mjs +2 -0
  78. package/dist/esm/common/weatherIcon/weatherIcon.types.mjs.map +1 -0
  79. package/dist/esm/index.mjs +1 -1
  80. package/dist/index.cjs +3 -3
  81. package/dist/index.cjs.map +1 -1
  82. package/dist/style.css +1 -1
  83. package/dist/types/common/commandPalette/commandPalette.d.ts +4 -0
  84. package/dist/types/common/commandPalette/commandPalette.types.d.ts +42 -0
  85. package/dist/types/common/commandPalette/filterCommands.d.ts +15 -0
  86. package/dist/types/common/commandPalette/index.d.ts +2 -0
  87. package/dist/types/common/configProvider/configProvider.types.d.ts +6 -0
  88. package/dist/types/common/copyButton/copyButton.d.ts +4 -0
  89. package/dist/types/common/copyButton/copyButton.types.d.ts +36 -0
  90. package/dist/types/common/copyButton/index.d.ts +2 -0
  91. package/dist/types/common/datePicker/components/datePickerMonth/datePickerMonth.types.d.ts +6 -6
  92. package/dist/types/common/datePicker/dateFunctions.d.ts +10 -19
  93. package/dist/types/common/datePicker/datePicker.types.d.ts +20 -14
  94. package/dist/types/common/datePicker/index.d.ts +1 -0
  95. package/dist/types/common/errorBoundary/errorBoundary.d.ts +0 -3
  96. package/dist/types/common/errorBoundary/errorBoundary.types.d.ts +2 -3
  97. package/dist/types/common/hooks/index.d.ts +2 -0
  98. package/dist/types/common/hooks/useClipboard.d.ts +16 -0
  99. package/dist/types/common/inputDropdown/inputDropdown.types.d.ts +7 -1
  100. package/dist/types/common/menu/menu.types.d.ts +2 -0
  101. package/dist/types/common/popover/popover.types.d.ts +5 -0
  102. package/dist/types/common/themeProvider/themeProvider.d.ts +8 -7
  103. package/dist/types/common/utils/day.d.ts +16 -0
  104. package/dist/types/common/weatherIcon/index.d.ts +3 -0
  105. package/dist/types/common/weatherIcon/parts.d.ts +26 -0
  106. package/dist/types/common/weatherIcon/weatherIcon.d.ts +4 -0
  107. package/dist/types/common/weatherIcon/weatherIcon.types.d.ts +44 -0
  108. package/dist/types/index.d.ts +7 -1
  109. package/docs/ButtonGroup.md +2 -0
  110. package/docs/CLAUDE.md +9 -2
  111. package/docs/CommandPalette.md +84 -0
  112. package/docs/ConfigProvider.md +2 -2
  113. package/docs/CopyButton.md +45 -0
  114. package/docs/DatePicker.md +72 -45
  115. package/docs/ErrorBoundary.md +4 -4
  116. package/docs/Fab.md +1 -2
  117. package/docs/FormValidator.md +3 -3
  118. package/docs/Hooks.md +39 -3
  119. package/docs/InputDropdown.md +9 -4
  120. package/docs/Menu.md +5 -3
  121. package/docs/Popover.md +15 -0
  122. package/docs/WeatherIcon.md +49 -0
  123. package/package.json +2 -3
@@ -0,0 +1,2 @@
1
+ "use client";import{useCallback as e,useEffect as t,useRef as n,useState as r}from"react";function i({timeout:i=2e3}={}){let[a,o]=r(!1),[s,c]=r(null),l=n(null),u=e(()=>{l.current&&clearTimeout(l.current),l.current=null},[]);t(()=>u,[u]);let d=e(()=>{u(),o(!1),c(null)},[u]);return{copy:e(async e=>{u();try{if(!navigator.clipboard?.writeText)throw Error(`Clipboard API unavailable`);return await navigator.clipboard.writeText(e),c(null),o(!0),l.current=setTimeout(()=>o(!1),i),!0}catch(e){return o(!1),c(e instanceof Error?e:Error(String(e))),!1}},[u,i]),copied:a,error:s,reset:d}}export{i as default,i as useClipboard};
2
+ //# sourceMappingURL=useClipboard.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useClipboard.mjs","names":[],"sources":["../../../../package/common/hooks/useClipboard.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\n\nexport interface UseClipboardOptions {\n /** How long `copied` stays `true` after a successful copy, in ms. Default: 2000. */\n timeout?: number;\n}\n\nexport interface UseClipboardResult {\n /** Writes `text` to the clipboard. Resolves `true` on success, `false` on failure. */\n copy: (text: string) => Promise<boolean>;\n /** `true` for `timeout` ms after a successful copy. */\n copied: boolean;\n /** Why the last copy failed, until the next attempt. */\n error: Error | null;\n /** Clears `copied` and `error` early. */\n reset: () => void;\n}\n\n// No `execCommand('copy')` fallback: it is deprecated, and where the async API is missing the\n// caller is better served by `error` than by a copy that may silently do nothing.\nexport function useClipboard({ timeout = 2000 }: UseClipboardOptions = {}): UseClipboardResult {\n const [copied, setCopied] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const clearTimer = useCallback(() => {\n if (timerRef.current) clearTimeout(timerRef.current);\n timerRef.current = null;\n }, []);\n\n useEffect(() => clearTimer, [clearTimer]);\n\n const reset = useCallback(() => {\n clearTimer();\n setCopied(false);\n setError(null);\n }, [clearTimer]);\n\n const copy = useCallback(\n async (text: string) => {\n clearTimer();\n try {\n if (!navigator.clipboard?.writeText) throw new Error('Clipboard API unavailable');\n await navigator.clipboard.writeText(text);\n setError(null);\n setCopied(true);\n timerRef.current = setTimeout(() => setCopied(false), timeout);\n return true;\n } catch (e) {\n setCopied(false);\n setError(e instanceof Error ? e : new Error(String(e)));\n return false;\n }\n },\n [clearTimer, timeout],\n );\n\n return { copy, copied, error, reset };\n}\n\nexport default useClipboard;\n"],"mappings":"0FAoBA,SAAgB,EAAa,CAAE,UAAU,KAA8B,CAAC,EAAuB,CAC7F,GAAM,CAAC,EAAQ,GAAa,EAAS,EAAK,EACpC,CAAC,EAAO,GAAY,EAAuB,IAAI,EAC/C,EAAW,EAA6C,IAAI,EAE5D,EAAa,MAAkB,CAC/B,EAAS,SAAS,aAAa,EAAS,OAAO,EACnD,EAAS,QAAU,IACrB,EAAG,CAAC,CAAC,EAEL,MAAgB,EAAY,CAAC,CAAU,CAAC,EAExC,IAAM,EAAQ,MAAkB,CAC9B,EAAW,EACX,EAAU,EAAK,EACf,EAAS,IAAI,CACf,EAAG,CAAC,CAAU,CAAC,EAqBf,MAAO,CAAE,KAnBI,EACX,KAAO,IAAiB,CACtB,EAAW,EACX,GAAI,CACF,GAAI,CAAC,UAAU,WAAW,UAAW,MAAU,MAAM,2BAA2B,EAKhF,OAJA,MAAM,UAAU,UAAU,UAAU,CAAI,EACxC,EAAS,IAAI,EACb,EAAU,EAAI,EACd,EAAS,QAAU,eAAiB,EAAU,EAAK,EAAG,CAAO,EACtD,EACT,OAAS,EAAG,CAGV,OAFA,EAAU,EAAK,EACf,EAAS,aAAa,MAAQ,EAAQ,MAAM,OAAO,CAAC,CAAC,CAAC,EAC/C,EACT,CACF,EACA,CAAC,EAAY,CAAO,CAGb,EAAM,SAAQ,QAAO,OAAM,CACtC"}
@@ -1,2 +1,2 @@
1
- "use client";var e=[`a[href]`,`area[href]`,`button:not([disabled])`,`input:not([disabled])`,`select:not([disabled])`,`textarea:not([disabled])`,`[tabindex]:not([tabindex="-1"])`,`[contenteditable="true"]`].join(`,`);function t(t){return t?Array.from(t.querySelectorAll(e)).filter(e=>e.getAttribute(`tabindex`)!==`-1`):[]}function n({triggerRef:e,contentRef:n}){function r(){return t(e.current)[0]??e.current}function i(){r()?.focus()}function a(){let e=t(document.body).filter(e=>!n.current?.contains(e)),i=r(),a=i?e.indexOf(i):-1;a>=0&&e[a+1]?.focus()}return{firstTriggerFocusable:r,restoreFocusToTrigger:i,focusAfterTrigger:a}}export{t as tabbablesIn,n as useFocusBoundary};
1
+ "use client";var e=[`a[href]`,`area[href]`,`button:not([disabled])`,`input:not([disabled])`,`select:not([disabled])`,`textarea:not([disabled])`,`[tabindex]:not([tabindex="-1"])`,`[contenteditable="true"]`].join(`,`);function t(t){return t?Array.from(t.querySelectorAll(e)).filter(e=>e.getAttribute(`tabindex`)!==`-1`):[]}function n(t){return t.matches(e)&&t.getAttribute(`tabindex`)!==`-1`}function r({triggerRef:e,contentRef:r}){function i(){let r=e.current;return r&&n(r)?r:t(r)[0]??r}function a(){i()?.focus()}function o(){let e=t(document.body).filter(e=>!r.current?.contains(e)),n=i(),a=n?e.indexOf(n):-1;a>=0&&e[a+1]?.focus()}return{firstTriggerFocusable:i,restoreFocusToTrigger:a,focusAfterTrigger:o}}export{t as tabbablesIn,r as useFocusBoundary};
2
2
  //# sourceMappingURL=useFocusBoundary.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"useFocusBoundary.mjs","names":[],"sources":["../../../../package/common/hooks/useFocusBoundary.ts"],"sourcesContent":["import type { RefObject } from 'react';\n\nconst FOCUSABLE_SELECTOR = [\n 'a[href]',\n 'area[href]',\n 'button:not([disabled])',\n 'input:not([disabled])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]:not([tabindex=\"-1\"])',\n '[contenteditable=\"true\"]',\n].join(',');\n\n/** Tabbable descendants of `root`, in DOM order. */\nexport function tabbablesIn(root: ParentNode | null): HTMLElement[] {\n if (!root) return [];\n return (\n Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR))\n // A button or a link can be taken out of the tab order too, which the\n // selector above only catches for elements whose focusability comes from\n // the attribute. A roving tabindex, where one item of a list is the tab\n // stop and the rest are reachable by arrow key, otherwise reads as a list\n // of tab stops, and the Tab boundary lands on the wrong entry.\n .filter((el) => el.getAttribute('tabindex') !== '-1')\n );\n}\n\nexport interface UseFocusBoundaryArgs {\n /** The trigger element focus returns to on Escape / Shift+Tab out of the content's top. */\n triggerRef: RefObject<HTMLElement | null>;\n /** The portaled content root — tabbables inside it define the Tab boundary. */\n contentRef: RefObject<HTMLElement | null>;\n}\n\n/**\n * Focus helpers for content portaled to the end of the DOM (e.g. via BodyEnd), where Tab can't\n * reach it in visual document order on its own. Used by Popover and by Menu, which both need to\n * hand focus back to the trigger on Escape / Shift+Tab out of the top, and continue focus\n * naturally past the trigger when Tab exits the bottom.\n */\nexport function useFocusBoundary({ triggerRef, contentRef }: UseFocusBoundaryArgs) {\n function firstTriggerFocusable(): HTMLElement | null {\n return tabbablesIn(triggerRef.current)[0] ?? triggerRef.current;\n }\n\n function restoreFocusToTrigger() {\n firstTriggerFocusable()?.focus();\n }\n\n // Move focus to the tabbable element that follows the trigger in the document, skipping the\n // portaled content — the \"natural next\" stop when Tab exits the bottom of the content.\n function focusAfterTrigger() {\n const all = tabbablesIn(document.body).filter((el) => !contentRef.current?.contains(el));\n const trig = firstTriggerFocusable();\n const idx = trig ? all.indexOf(trig) : -1;\n if (idx >= 0) all[idx + 1]?.focus();\n }\n\n return { firstTriggerFocusable, restoreFocusToTrigger, focusAfterTrigger };\n}\n"],"mappings":"aAEA,IAAM,EAAqB,CACzB,UACA,aACA,yBACA,wBACA,yBACA,2BACA,kCACA,0BACF,CAAC,CAAC,KAAK,GAAG,EAGV,SAAgB,EAAY,EAAwC,CAElE,OADK,EAEH,MAAM,KAAK,EAAK,iBAA8B,CAAkB,CAAC,CAAC,CAM/D,OAAQ,GAAO,EAAG,aAAa,UAAU,IAAM,IAAI,EARtC,CAAC,CAUrB,CAeA,SAAgB,EAAiB,CAAE,aAAY,cAAoC,CACjF,SAAS,GAA4C,CACnD,OAAO,EAAY,EAAW,OAAO,CAAC,CAAC,IAAM,EAAW,OAC1D,CAEA,SAAS,GAAwB,CAC/B,EAAsB,CAAC,EAAE,MAAM,CACjC,CAIA,SAAS,GAAoB,CAC3B,IAAM,EAAM,EAAY,SAAS,IAAI,CAAC,CAAC,OAAQ,GAAO,CAAC,EAAW,SAAS,SAAS,CAAE,CAAC,EACjF,EAAO,EAAsB,EAC7B,EAAM,EAAO,EAAI,QAAQ,CAAI,EAAI,GACnC,GAAO,GAAG,EAAI,EAAM,EAAE,EAAE,MAAM,CACpC,CAEA,MAAO,CAAE,wBAAuB,wBAAuB,mBAAkB,CAC3E"}
1
+ {"version":3,"file":"useFocusBoundary.mjs","names":[],"sources":["../../../../package/common/hooks/useFocusBoundary.ts"],"sourcesContent":["import type { RefObject } from 'react';\n\nconst FOCUSABLE_SELECTOR = [\n 'a[href]',\n 'area[href]',\n 'button:not([disabled])',\n 'input:not([disabled])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]:not([tabindex=\"-1\"])',\n '[contenteditable=\"true\"]',\n].join(',');\n\n/** Tabbable descendants of `root`, in DOM order. */\nexport function tabbablesIn(root: ParentNode | null): HTMLElement[] {\n if (!root) return [];\n return (\n Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR))\n // A button or a link can be taken out of the tab order too, which the\n // selector above only catches for elements whose focusability comes from\n // the attribute. A roving tabindex, where one item of a list is the tab\n // stop and the rest are reachable by arrow key, otherwise reads as a list\n // of tab stops, and the Tab boundary lands on the wrong entry.\n .filter((el) => el.getAttribute('tabindex') !== '-1')\n );\n}\n\nfunction isTabbable(element: HTMLElement): boolean {\n return element.matches(FOCUSABLE_SELECTOR) && element.getAttribute('tabindex') !== '-1';\n}\n\nexport interface UseFocusBoundaryArgs {\n /** The trigger element focus returns to on Escape / Shift+Tab out of the content's top. */\n triggerRef: RefObject<HTMLElement | null>;\n /** The portaled content root — tabbables inside it define the Tab boundary. */\n contentRef: RefObject<HTMLElement | null>;\n}\n\n/**\n * Focus helpers for content portaled to the end of the DOM (e.g. via BodyEnd), where Tab can't\n * reach it in visual document order on its own. Used by Popover and by Menu, which both need to\n * hand focus back to the trigger on Escape / Shift+Tab out of the top, and continue focus\n * naturally past the trigger when Tab exits the bottom.\n */\nexport function useFocusBoundary({ triggerRef, contentRef }: UseFocusBoundaryArgs) {\n function firstTriggerFocusable(): HTMLElement | null {\n const trigger = triggerRef.current;\n // The trigger itself where it takes focus, as an element handed to Popover's `anchor` can;\n // else the first element inside it that does, as inside Popover's and Menu's own wrappers.\n if (trigger && isTabbable(trigger)) return trigger;\n return tabbablesIn(trigger)[0] ?? trigger;\n }\n\n function restoreFocusToTrigger() {\n firstTriggerFocusable()?.focus();\n }\n\n // Move focus to the tabbable element that follows the trigger in the document, skipping the\n // portaled content — the \"natural next\" stop when Tab exits the bottom of the content.\n function focusAfterTrigger() {\n const all = tabbablesIn(document.body).filter((el) => !contentRef.current?.contains(el));\n const trig = firstTriggerFocusable();\n const idx = trig ? all.indexOf(trig) : -1;\n if (idx >= 0) all[idx + 1]?.focus();\n }\n\n return { firstTriggerFocusable, restoreFocusToTrigger, focusAfterTrigger };\n}\n"],"mappings":"aAEA,IAAM,EAAqB,CACzB,UACA,aACA,yBACA,wBACA,yBACA,2BACA,kCACA,0BACF,CAAC,CAAC,KAAK,GAAG,EAGV,SAAgB,EAAY,EAAwC,CAElE,OADK,EAEH,MAAM,KAAK,EAAK,iBAA8B,CAAkB,CAAC,CAAC,CAM/D,OAAQ,GAAO,EAAG,aAAa,UAAU,IAAM,IAAI,EARtC,CAAC,CAUrB,CAEA,SAAS,EAAW,EAA+B,CACjD,OAAO,EAAQ,QAAQ,CAAkB,GAAK,EAAQ,aAAa,UAAU,IAAM,IACrF,CAeA,SAAgB,EAAiB,CAAE,aAAY,cAAoC,CACjF,SAAS,GAA4C,CACnD,IAAM,EAAU,EAAW,QAI3B,OADI,GAAW,EAAW,CAAO,EAAU,EACpC,EAAY,CAAO,CAAC,CAAC,IAAM,CACpC,CAEA,SAAS,GAAwB,CAC/B,EAAsB,CAAC,EAAE,MAAM,CACjC,CAIA,SAAS,GAAoB,CAC3B,IAAM,EAAM,EAAY,SAAS,IAAI,CAAC,CAAC,OAAQ,GAAO,CAAC,EAAW,SAAS,SAAS,CAAE,CAAC,EACjF,EAAO,EAAsB,EAC7B,EAAM,EAAO,EAAI,QAAQ,CAAI,EAAI,GACnC,GAAO,GAAG,EAAI,EAAM,EAAE,EAAE,MAAM,CACpC,CAEA,MAAO,CAAE,wBAAuB,wBAAuB,mBAAkB,CAC3E"}
@@ -1,2 +1,2 @@
1
- "use client";import{useComponentDefaults as e}from"../configProvider/useComponentDefaults.mjs";import{PopoverAlign as t,PopoverPlacement as n}from"../popover/popover.types.mjs";import ee from"../popover/popover.mjs";import r from"../input/input.mjs";import{nextIndex as i,scrollOptionIntoView as a}from"../utils/listNavigation.mjs";import o from"./inputDropdown.module.mjs";import{useCallback as s,useEffect as c,useLayoutEffect as l,useMemo as u,useRef as d,useState as f}from"react";import p from"classnames";import{jsx as m}from"react/jsx-runtime";function h(e,t){return t?t.value??``:e??``}function g(e,t){let n=t.find(t=>t.value===e);return n?n.label??n.value:e}var te={wrap:!1};function _(_){let{label:v=``,value:y=``,formValidator:b=null,error:x,isRequired:S=!1,items:C=[],allowCustomValue:w=!1,maxItems:T=100,className:E,style:D,classNames:O,styles:k,onSelect:A=()=>null,onChange:j,placeholder:M=``,readOnly:N=!1,...P}=e(`InputDropdown`,_),F=h(y,b),[,I]=f(0),L=d(b);c(()=>{if(L.current=b,!b)return;let e=()=>I(e=>e+1);return b.registerOnUpdateListener(e),()=>{b.removeOnUpdateListener(e)}},[b]);let R=S||!!b?.validators.find(e=>e.validatorId===`required`),[z,B]=f(()=>g(F,C)),[V,H]=f(F),[U,W]=f(!1),[G,K]=f(null),q=d(null),J=d(null),Y=d(null),X=d(null),Z=k?.dropdown?.fontSize;if(l(()=>{let e=J.current,t=Y.current;U&&e&&t&&Z==null&&(t.style.fontSize=window.getComputedStyle(e).fontSize)},[U,Z]),F!==V){H(F);let e=C.find(e=>e.value===F);e?B(e.label??e.value):w&&B(F)}let Q=u(()=>{let e=z.toLowerCase(),t=[];for(let n of C)if((n.label??n.value).toLowerCase().includes(e)&&(t.push(n),T>0&&t.length>=T))break;return t},[C,z,T]),ne=()=>{N||(q.current&&=(clearTimeout(q.current),null),W(!0))},re=()=>{L.current&&(L.current.touched=!0),q.current=setTimeout(()=>{W(!1),q.current=null,w||B(g(F,C))},100)};c(()=>{U&&G!==null&&a(X.current,X.current?.children[G])},[U,G]);let $=s(e=>{B(e.label??e.value),W(!1),K(null),b&&b.set(e.value),j&&j(e.value),A(e.value)},[b,j,A]),ie=s(e=>{if(!N){if(e.key===`ArrowUp`||e.key===`ArrowDown`){e.preventDefault();let t=e.key===`ArrowDown`?1:-1;K(e=>e!==null&&e===(t===1?Q.length-1:0)?null:i(e,t,Q.length,te))}else e.key===`Enter`&&(G!==null&&Q[G]?(e.preventDefault(),$(Q[G])):w&&(W(!1),K(null)))}},[Q,G,$,w,N]),ae=m(`div`,{ref:Y,className:p(o.dropdown,O?.dropdown),style:k?.dropdown,children:m(`div`,{ref:X,className:o.dropdownScroll,children:Q.map((e,t)=>m(`div`,{className:p(o.dropdownItem,G===t&&o.dropdownItemHighlighted,O?.item),style:k?.item,onMouseDown:e=>e.preventDefault(),onClick:()=>$(e),children:e.label??e.value},`${t}_${e.label}_${e.value}`))})});return m(`div`,{...P,ref:J,className:p(o.inputDropdown,O?.root,E),style:{...k?.root,...D},onFocus:ne,onBlur:re,onKeyDown:ie,children:m(ee,{isOpen:U&&!N,onOpenChange:e=>{e||W(!1)},placement:n.Bottom,align:t.Start,offset:0,withArrow:!1,constrainWidth:!1,closeOnTriggerClick:!1,focusOnOpen:!1,classNames:{panel:o.inputDropdownPanel},content:ae,children:m(r,{autoComplete:`off`,label:v,value:z,readOnly:N,formValidator:null,isRequired:R,isValid:b?!b.touched||!b.hasError():!x,errorMessage:b?b.touched?b.getCurrentErrorMessage()??``:``:x??``,onChange:e=>{if(B(e),K(null),W(!0),w)b&&b.set(e),j&&j(e);else if(e===``)b&&b.set(``),j&&j(``),A(``);else{let t=C.find(t=>(t.label??t.value).toLowerCase()===e.toLowerCase());t&&(b&&b.set(t.value),j&&j(t.value),A(t.value))}},placeholder:M})})})}export{_ as default};
1
+ "use client";import{useComponentDefaults as e}from"../configProvider/useComponentDefaults.mjs";import{useLabels as t}from"../configProvider/useLabels.mjs";import{Presentation as n}from"../configProvider/presentation.types.mjs";import{PopoverAlign as r,PopoverPlacement as i}from"../popover/popover.types.mjs";import ee from"../popover/popover.mjs";import a from"../input/input.mjs";import{nextIndex as o,scrollOptionIntoView as s}from"../utils/listNavigation.mjs";import c from"./inputDropdown.module.mjs";import{useCallback as l,useEffect as u,useMemo as te,useRef as d,useState as f}from"react";import p from"classnames";import{jsx as m,jsxs as h}from"react/jsx-runtime";function ne(e,t){return t?t.value??``:e??``}function g(e,t){let n=t.find(t=>t.value===e);return n?n.label??n.value:e}var re={wrap:!1},ie={empty:`No matches`};function _(_){let{label:ae=``,value:v=``,formValidator:y=null,error:b,isRequired:x=!1,items:S=[],allowCustomValue:C=!1,maxItems:w=100,className:T,style:E,classNames:D,styles:O,onSelect:k=()=>null,onChange:A,placeholder:oe=``,readOnly:j=!1,labels:M,...se}=e(`InputDropdown`,_),N=t(`InputDropdown`,M,ie),P=ne(v,y),[,F]=f(0),I=d(y);u(()=>{if(I.current=y,!y)return;let e=()=>F(e=>e+1);return y.registerOnUpdateListener(e),()=>{y.removeOnUpdateListener(e)}},[y]);let L=x||!!y?.validators.find(e=>e.validatorId===`required`),[R,z]=f(()=>g(P,S)),[B,V]=f(P),[H,U]=f(!1),[W,G]=f(null),K=d(null),q=d(null),J=d(null);if(P!==B){V(P);let e=S.find(e=>e.value===P);e?z(e.label??e.value):C&&z(P)}if(!H&&!C&&R===P){let e=g(P,S);e!==R&&z(e)}let Y=te(()=>{let e=R.toLowerCase(),t=[];for(let n of S)if((n.label??n.value).toLowerCase().includes(e)&&(t.push(n),w>0&&t.length>=w))break;return t},[S,R,w]),ce=!C&&R!==``&&R!==g(P,S),X=Y.length===0&&ce,Z=H&&!j&&(Y.length>0||X),Q=O?.dropdown?.fontSize,le=l(e=>{let t=q.current;e&&t&&Q==null&&(e.style.fontSize=window.getComputedStyle(t).fontSize)},[Q]),ue=()=>{j||(K.current&&=(clearTimeout(K.current),null),U(!0))},de=()=>{I.current&&(I.current.touched=!0),K.current=setTimeout(()=>{U(!1),G(null),K.current=null,C||z(g(P,S))},100)};u(()=>{Z&&W!==null&&s(J.current,J.current?.children[W])},[Z,W]);let $=l(e=>{z(e.label??e.value),U(!1),G(null),y&&y.set(e.value),A&&A(e.value),k(e.value)},[y,A,k]),fe=l(e=>{if(j)return;let t=e.key===`ArrowUp`||e.key===`ArrowDown`;if(!Z){t&&(e.preventDefault(),U(!0));return}if(t){e.preventDefault();let t=e.key===`ArrowDown`?1:-1;G(e=>e!==null&&e===(t===1?Y.length-1:0)?null:o(e,t,Y.length,re))}else e.key===`Enter`&&(W!==null&&Y[W]?(e.preventDefault(),$(Y[W])):C&&(U(!1),G(null)))},[Y,W,$,C,j,Z]),pe=h(`div`,{ref:le,className:p(c.dropdown,D?.dropdown),style:O?.dropdown,children:[X&&m(`div`,{className:p(c.dropdownEmpty,D?.empty),style:O?.empty,onMouseDown:e=>e.preventDefault(),children:N.empty}),m(`div`,{ref:J,className:c.dropdownScroll,children:Y.map((e,t)=>m(`div`,{className:p(c.dropdownItem,W===t&&c.dropdownItemHighlighted,D?.item),style:O?.item,onMouseDown:e=>e.preventDefault(),onClick:()=>$(e),children:e.label??e.value},`${t}_${e.label}_${e.value}`))})]});return h(`div`,{...se,ref:q,className:p(c.inputDropdown,D?.root,T),style:{...O?.root,...E},onFocus:ue,onBlur:de,onKeyDown:fe,children:[m(ee,{isOpen:Z,onOpenChange:e=>{e||(U(!1),G(null))},presentation:n.Default,placement:i.Bottom,align:r.Start,offset:0,withArrow:!1,constrainWidth:!1,closeOnTriggerClick:!1,focusOnOpen:!1,classNames:{panel:c.inputDropdownPanel},content:pe,children:m(a,{autoComplete:`off`,label:ae,value:R,readOnly:j,formValidator:null,isRequired:L,isValid:y?!y.touched||!y.hasError():!b,errorMessage:y?y.touched?y.getCurrentErrorMessage()??``:``:b??``,onChange:e=>{if(z(e),G(null),U(!0),C)y&&y.set(e),A&&A(e);else if(e===``)y&&y.set(``),A&&A(``),k(``);else{let t=S.find(t=>(t.label??t.value).toLowerCase()===e.toLowerCase());t&&(y&&y.set(t.value),A&&A(t.value),k(t.value))}},placeholder:oe})}),m(`span`,{className:c.inputDropdownStatus,role:`status`,"aria-live":`polite`,children:Z&&X?N.empty:``})]})}export{_ as default};
2
2
  //# sourceMappingURL=inputDropdown.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"inputDropdown.mjs","names":[],"sources":["../../../../package/common/inputDropdown/inputDropdown.tsx"],"sourcesContent":["import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport cx from 'classnames';\nimport styles from './inputDropdown.module.pcss';\nimport Input from '../input';\nimport Popover from '../popover';\nimport { PopoverAlign, PopoverPlacement } from '../popover/popover.types';\nimport { useComponentDefaults } from '../configProvider';\nimport type { InputDropdownItem, InputDropdownProps } from './inputDropdown.types';\nimport { nextIndex, scrollOptionIntoView } from '../utils/listNavigation';\n\nfunction getKeyFromProps(value: string | undefined, formValidator: InputDropdownProps['formValidator']): string {\n return formValidator ? ((formValidator.value as string) ?? '') : (value ?? '');\n}\n\nfunction labelForKey(key: string, items: InputDropdownItem[]): string {\n const match = items.find((item) => item.value === key);\n return match ? (match.label ?? match.value) : key;\n}\n\n/**\n * The suggestions do not wrap: the list is filtered by what is being typed, so running off one\n * end and reappearing at the other reads as the list having changed under the caret.\n */\nconst NO_WRAP = { wrap: false };\n\nexport default function InputDropdown(props: InputDropdownProps) {\n const {\n label = '',\n value = '',\n formValidator = null,\n error,\n isRequired = false,\n items = [],\n allowCustomValue = false,\n maxItems = 100,\n className,\n style,\n classNames,\n styles: slotStyles,\n onSelect = () => null,\n onChange,\n placeholder = '',\n readOnly = false,\n ...rest\n } = useComponentDefaults('InputDropdown', props);\n const currentKey = getKeyFromProps(value, formValidator);\n const [, forceUpdate] = useState(0);\n const fvRef = useRef(formValidator);\n\n useEffect(() => {\n fvRef.current = formValidator;\n if (!formValidator) return;\n const listener = () => forceUpdate((n) => n + 1);\n formValidator.registerOnUpdateListener(listener);\n return () => {\n formValidator.removeOnUpdateListener(listener);\n };\n }, [formValidator]);\n\n const computedIsRequired =\n isRequired || !!formValidator?.validators.find((v) => (v as { validatorId?: string }).validatorId === 'required');\n\n const [inputText, setInputText] = useState(() => labelForKey(currentKey, items));\n const [prevKey, setPrevKey] = useState(currentKey);\n const [isMenuShown, setIsMenuShown] = useState(false);\n const [highlightedIndex, setHighlightedIndex] = useState<number | null>(null);\n const hideTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const triggerRef = useRef<HTMLDivElement>(null);\n const anchorRef = useRef<HTMLDivElement>(null);\n const scrollRef = useRef<HTMLDivElement>(null);\n\n const menuFontSizeOverride = slotStyles?.dropdown?.fontSize;\n useLayoutEffect(() => {\n const trigger = triggerRef.current;\n const menu = anchorRef.current;\n if (!isMenuShown || !trigger || !menu || menuFontSizeOverride != null) return;\n menu.style.fontSize = window.getComputedStyle(trigger).fontSize;\n }, [isMenuShown, menuFontSizeOverride]);\n\n // Sync display text when the controlled key changes externally (e.g. parent reset).\n if (currentKey !== prevKey) {\n setPrevKey(currentKey);\n const match = items.find((item) => item.value === currentKey);\n if (match) setInputText(match.label ?? match.value);\n else if (allowCustomValue) setInputText(currentKey);\n }\n\n const currItems = useMemo(() => {\n const needle = inputText.toLowerCase();\n const matches: InputDropdownItem[] = [];\n for (const item of items) {\n if (!(item.label ?? item.value).toLowerCase().includes(needle)) continue;\n matches.push(item);\n if (maxItems > 0 && matches.length >= maxItems) break;\n }\n return matches;\n }, [items, inputText, maxItems]);\n\n const handleFocus = () => {\n if (readOnly) return;\n if (hideTimeoutRef.current) {\n clearTimeout(hideTimeoutRef.current);\n hideTimeoutRef.current = null;\n }\n setIsMenuShown(true);\n };\n\n const handleBlur = () => {\n if (fvRef.current) fvRef.current.touched = true;\n hideTimeoutRef.current = setTimeout(() => {\n setIsMenuShown(false);\n hideTimeoutRef.current = null;\n // Without custom values, uncommitted typed text is invalid — snap the\n // display back to the currently selected item's label.\n if (!allowCustomValue) setInputText(labelForKey(currentKey, items));\n }, 100);\n };\n\n useEffect(() => {\n if (!isMenuShown || highlightedIndex === null) return;\n scrollOptionIntoView(scrollRef.current, scrollRef.current?.children[highlightedIndex]);\n }, [isMenuShown, highlightedIndex]);\n\n const handleSelect = useCallback(\n (item: InputDropdownItem) => {\n setInputText(item.label ?? item.value);\n setIsMenuShown(false);\n setHighlightedIndex(null);\n if (formValidator) formValidator.set(item.value);\n if (onChange) onChange(item.value);\n onSelect(item.value);\n },\n [formValidator, onChange, onSelect],\n );\n\n const onKeyDown = useCallback(\n (event: React.KeyboardEvent) => {\n if (readOnly) return;\n if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {\n event.preventDefault();\n const step = event.key === 'ArrowDown' ? 1 : -1;\n setHighlightedIndex((idx) => {\n // The text being typed is part of the cycle, so both ends lead back to it and on round\n // to the far end: last → text → first, first → text → last. That is how a browser's own\n // address bar behaves, and it is why this list does not wrap straight around the way\n // Dropdown and MultiSelect do — they have no typed text to pass through.\n const atFarEnd = idx !== null && idx === (step === 1 ? currItems.length - 1 : 0);\n return atFarEnd ? null : nextIndex(idx, step, currItems.length, NO_WRAP);\n });\n } else if (event.key === 'Enter') {\n if (highlightedIndex !== null && currItems[highlightedIndex]) {\n // This Enter picked a suggestion, so it must not also reach the form around the field.\n // Without this the browser's implicit submission runs as well, and choosing from the\n // list submitted the form underneath the user. The blank state deliberately does not\n // do this: Enter there means the text as typed, and submitting is a fair reading of it.\n event.preventDefault();\n handleSelect(currItems[highlightedIndex]);\n } else if (allowCustomValue) {\n setIsMenuShown(false);\n setHighlightedIndex(null);\n }\n }\n },\n [currItems, highlightedIndex, handleSelect, allowCustomValue, readOnly],\n );\n\n const menu = (\n <div ref={anchorRef} className={cx(styles.dropdown, classNames?.dropdown)} style={slotStyles?.dropdown}>\n <div ref={scrollRef} className={styles.dropdownScroll}>\n {currItems.map((item, key) => (\n <div\n key={`${key}_${item.label}_${item.value}`}\n className={cx(\n styles.dropdownItem,\n highlightedIndex === key && styles.dropdownItemHighlighted,\n classNames?.item,\n )}\n style={slotStyles?.item}\n // Prevent the input from blurring before the click resolves.\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => handleSelect(item)}\n >\n {item.label ?? item.value}\n </div>\n ))}\n </div>\n </div>\n );\n\n return (\n <div\n {...rest}\n ref={triggerRef}\n className={cx(styles.inputDropdown, classNames?.root, className)}\n style={{ ...slotStyles?.root, ...style }}\n onFocus={handleFocus}\n onBlur={handleBlur}\n onKeyDown={onKeyDown}\n >\n <Popover\n // The field owns the open state: it opens on typing and closes on blur, on\n // a pick, or on Escape. Only a close is taken from Popover, which is the\n // outside click its portaled panel would otherwise not see.\n isOpen={isMenuShown && !readOnly}\n onOpenChange={(next) => {\n if (!next) setIsMenuShown(false);\n }}\n placement={PopoverPlacement.Bottom}\n align={PopoverAlign.Start}\n offset={0}\n withArrow={false}\n constrainWidth={false}\n closeOnTriggerClick={false}\n focusOnOpen={false}\n classNames={{ panel: styles.inputDropdownPanel }}\n content={menu}\n >\n <Input\n // A combobox renders its own suggestion list; the browser's autofill dropdown\n // opens on top of it, so two lists compete for the same space.\n autoComplete='off'\n label={label}\n value={inputText}\n readOnly={readOnly}\n formValidator={null}\n isRequired={computedIsRequired}\n isValid={formValidator ? (formValidator.touched ? !formValidator.hasError() : true) : !error}\n errorMessage={\n formValidator\n ? formValidator.touched\n ? (formValidator.getCurrentErrorMessage() ?? '')\n : ''\n : (error ?? '')\n }\n onChange={(text) => {\n setInputText(text);\n setHighlightedIndex(null);\n setIsMenuShown(true);\n if (allowCustomValue) {\n if (formValidator) formValidator.set(text);\n if (onChange) onChange(text);\n } else if (text === '') {\n if (formValidator) formValidator.set('');\n if (onChange) onChange('');\n onSelect('');\n } else {\n // Emit when the typed text exactly matches an item's label/value.\n const match = items.find((item) => (item.label ?? item.value).toLowerCase() === text.toLowerCase());\n if (match) {\n if (formValidator) formValidator.set(match.value);\n if (onChange) onChange(match.value);\n onSelect(match.value);\n }\n }\n }}\n placeholder={placeholder}\n />\n </Popover>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"inputDropdown.mjs","names":[],"sources":["../../../../package/common/inputDropdown/inputDropdown.tsx"],"sourcesContent":["import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport cx from 'classnames';\nimport styles from './inputDropdown.module.pcss';\nimport Input from '../input';\nimport Popover from '../popover';\nimport { PopoverAlign, PopoverPlacement } from '../popover/popover.types';\nimport { useComponentDefaults, useLabels } from '../configProvider';\nimport { Presentation } from '../configProvider/presentation.types';\nimport type { InputDropdownItem, InputDropdownLabels, InputDropdownProps } from './inputDropdown.types';\nimport { nextIndex, scrollOptionIntoView } from '../utils/listNavigation';\n\nfunction getKeyFromProps(value: string | undefined, formValidator: InputDropdownProps['formValidator']): string {\n return formValidator ? ((formValidator.value as string) ?? '') : (value ?? '');\n}\n\nfunction labelForKey(key: string, items: InputDropdownItem[]): string {\n const match = items.find((item) => item.value === key);\n return match ? (match.label ?? match.value) : key;\n}\n\n/**\n * The suggestions do not wrap: the list is filtered by what is being typed, so running off one\n * end and reappearing at the other reads as the list having changed under the caret.\n */\nconst NO_WRAP = { wrap: false };\n\nconst DEFAULT_LABELS: Required<InputDropdownLabels> = { empty: 'No matches' };\n\nexport default function InputDropdown(props: InputDropdownProps) {\n const {\n label = '',\n value = '',\n formValidator = null,\n error,\n isRequired = false,\n items = [],\n allowCustomValue = false,\n maxItems = 100,\n className,\n style,\n classNames,\n styles: slotStyles,\n onSelect = () => null,\n onChange,\n placeholder = '',\n readOnly = false,\n labels,\n ...rest\n } = useComponentDefaults('InputDropdown', props);\n const text = useLabels('InputDropdown', labels, DEFAULT_LABELS);\n const currentKey = getKeyFromProps(value, formValidator);\n const [, forceUpdate] = useState(0);\n const fvRef = useRef(formValidator);\n\n useEffect(() => {\n fvRef.current = formValidator;\n if (!formValidator) return;\n const listener = () => forceUpdate((n) => n + 1);\n formValidator.registerOnUpdateListener(listener);\n return () => {\n formValidator.removeOnUpdateListener(listener);\n };\n }, [formValidator]);\n\n const computedIsRequired =\n isRequired || !!formValidator?.validators.find((v) => (v as { validatorId?: string }).validatorId === 'required');\n\n const [inputText, setInputText] = useState(() => labelForKey(currentKey, items));\n const [prevKey, setPrevKey] = useState(currentKey);\n const [isMenuShown, setIsMenuShown] = useState(false);\n const [highlightedIndex, setHighlightedIndex] = useState<number | null>(null);\n const hideTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const triggerRef = useRef<HTMLDivElement>(null);\n const scrollRef = useRef<HTMLDivElement>(null);\n\n // Sync display text when the controlled key changes externally (e.g. parent reset).\n if (currentKey !== prevKey) {\n setPrevKey(currentKey);\n const match = items.find((item) => item.value === currentKey);\n if (match) setInputText(match.label ?? match.value);\n else if (allowCustomValue) setInputText(currentKey);\n }\n\n // `items` often arrives after the value, leaving the raw key on display. Not while open, or a key\n // being typed would turn into its label under the caret.\n if (!isMenuShown && !allowCustomValue && inputText === currentKey) {\n const selectedLabel = labelForKey(currentKey, items);\n if (selectedLabel !== inputText) setInputText(selectedLabel);\n }\n\n const currItems = useMemo(() => {\n const needle = inputText.toLowerCase();\n const matches: InputDropdownItem[] = [];\n for (const item of items) {\n if (!(item.label ?? item.value).toLowerCase().includes(needle)) continue;\n matches.push(item);\n if (maxItems > 0 && matches.length >= maxItems) break;\n }\n return matches;\n }, [items, inputText, maxItems]);\n\n // Only text that blur swaps for the selection's label needs a warning. Comparing with the label\n // rather than '' keeps a preset value missing from `items` from showing \"No matches\" unprompted.\n const isDiscardedOnBlur = !allowCustomValue && inputText !== '' && inputText !== labelForKey(currentKey, items);\n const showEmpty = currItems.length === 0 && isDiscardedOnBlur;\n // Closed rather than hidden when empty: a mounted panel stays on the overlay stack, so inside a\n // Modal an invisible list would take the Escape meant for the modal.\n const isOpen = isMenuShown && !readOnly && (currItems.length > 0 || showEmpty);\n\n // A ref callback rather than a layout effect: the list is portaled out of the field's font size,\n // and Popover positions it in its own layout effect, which runs before this component's would.\n const menuFontSizeOverride = slotStyles?.dropdown?.fontSize;\n const menuRef = useCallback(\n (menu: HTMLDivElement | null) => {\n const trigger = triggerRef.current;\n if (!menu || !trigger || menuFontSizeOverride != null) return;\n menu.style.fontSize = window.getComputedStyle(trigger).fontSize;\n },\n [menuFontSizeOverride],\n );\n\n const handleFocus = () => {\n if (readOnly) return;\n if (hideTimeoutRef.current) {\n clearTimeout(hideTimeoutRef.current);\n hideTimeoutRef.current = null;\n }\n setIsMenuShown(true);\n };\n\n const handleBlur = () => {\n if (fvRef.current) fvRef.current.touched = true;\n hideTimeoutRef.current = setTimeout(() => {\n setIsMenuShown(false);\n setHighlightedIndex(null);\n hideTimeoutRef.current = null;\n // Without custom values, uncommitted typed text is invalid — snap the\n // display back to the currently selected item's label.\n if (!allowCustomValue) setInputText(labelForKey(currentKey, items));\n }, 100);\n };\n\n useEffect(() => {\n if (!isOpen || highlightedIndex === null) return;\n scrollOptionIntoView(scrollRef.current, scrollRef.current?.children[highlightedIndex]);\n }, [isOpen, highlightedIndex]);\n\n const handleSelect = useCallback(\n (item: InputDropdownItem) => {\n setInputText(item.label ?? item.value);\n setIsMenuShown(false);\n setHighlightedIndex(null);\n if (formValidator) formValidator.set(item.value);\n if (onChange) onChange(item.value);\n onSelect(item.value);\n },\n [formValidator, onChange, onSelect],\n );\n\n const onKeyDown = useCallback(\n (event: React.KeyboardEvent) => {\n if (readOnly) return;\n const isArrow = event.key === 'ArrowUp' || event.key === 'ArrowDown';\n // Enter belongs to the form while closed, or it would pick a suggestion the user never saw.\n if (!isOpen) {\n if (isArrow) {\n event.preventDefault();\n setIsMenuShown(true);\n }\n return;\n }\n if (isArrow) {\n event.preventDefault();\n const step = event.key === 'ArrowDown' ? 1 : -1;\n setHighlightedIndex((idx) => {\n // The text being typed is part of the cycle, so both ends lead back to it and on round\n // to the far end: last → text → first, first → text → last. That is how a browser's own\n // address bar behaves, and it is why this list does not wrap straight around the way\n // Dropdown and MultiSelect do — they have no typed text to pass through.\n const atFarEnd = idx !== null && idx === (step === 1 ? currItems.length - 1 : 0);\n return atFarEnd ? null : nextIndex(idx, step, currItems.length, NO_WRAP);\n });\n } else if (event.key === 'Enter') {\n if (highlightedIndex !== null && currItems[highlightedIndex]) {\n // This Enter picked a suggestion, so it must not also reach the form around the field.\n // Without this the browser's implicit submission runs as well, and choosing from the\n // list submitted the form underneath the user. The blank state deliberately does not\n // do this: Enter there means the text as typed, and submitting is a fair reading of it.\n event.preventDefault();\n handleSelect(currItems[highlightedIndex]);\n } else if (allowCustomValue) {\n setIsMenuShown(false);\n setHighlightedIndex(null);\n }\n }\n },\n [currItems, highlightedIndex, handleSelect, allowCustomValue, readOnly, isOpen],\n );\n\n const menu = (\n <div ref={menuRef} className={cx(styles.dropdown, classNames?.dropdown)} style={slotStyles?.dropdown}>\n {showEmpty && (\n <div\n className={cx(styles.dropdownEmpty, classNames?.empty)}\n style={slotStyles?.empty}\n // A blur would throw the typed text away.\n onMouseDown={(e) => e.preventDefault()}\n >\n {text.empty}\n </div>\n )}\n <div ref={scrollRef} className={styles.dropdownScroll}>\n {currItems.map((item, key) => (\n <div\n key={`${key}_${item.label}_${item.value}`}\n className={cx(\n styles.dropdownItem,\n highlightedIndex === key && styles.dropdownItemHighlighted,\n classNames?.item,\n )}\n style={slotStyles?.item}\n // Prevent the input from blurring before the click resolves.\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => handleSelect(item)}\n >\n {item.label ?? item.value}\n </div>\n ))}\n </div>\n </div>\n );\n\n return (\n <div\n {...rest}\n ref={triggerRef}\n className={cx(styles.inputDropdown, classNames?.root, className)}\n style={{ ...slotStyles?.root, ...style }}\n onFocus={handleFocus}\n onBlur={handleBlur}\n onKeyDown={onKeyDown}\n >\n <Popover\n // The field owns the open state: it opens on typing and closes on blur, on\n // a pick, or on Escape. Only a close is taken from Popover, which is the\n // outside click its portaled panel would otherwise not see.\n isOpen={isOpen}\n onOpenChange={(next) => {\n if (next) return;\n setIsMenuShown(false);\n setHighlightedIndex(null);\n }}\n // Never a sheet, whatever the app-wide `presentation`: its backdrop would cover the field\n // being typed into.\n presentation={Presentation.Default}\n placement={PopoverPlacement.Bottom}\n align={PopoverAlign.Start}\n offset={0}\n withArrow={false}\n constrainWidth={false}\n closeOnTriggerClick={false}\n focusOnOpen={false}\n classNames={{ panel: styles.inputDropdownPanel }}\n content={menu}\n >\n <Input\n // A combobox renders its own suggestion list; the browser's autofill dropdown\n // opens on top of it, so two lists compete for the same space.\n autoComplete='off'\n label={label}\n value={inputText}\n readOnly={readOnly}\n formValidator={null}\n isRequired={computedIsRequired}\n isValid={formValidator ? (formValidator.touched ? !formValidator.hasError() : true) : !error}\n errorMessage={\n formValidator\n ? formValidator.touched\n ? (formValidator.getCurrentErrorMessage() ?? '')\n : ''\n : (error ?? '')\n }\n onChange={(typed) => {\n setInputText(typed);\n setHighlightedIndex(null);\n setIsMenuShown(true);\n if (allowCustomValue) {\n if (formValidator) formValidator.set(typed);\n if (onChange) onChange(typed);\n } else if (typed === '') {\n if (formValidator) formValidator.set('');\n if (onChange) onChange('');\n onSelect('');\n } else {\n // Emit when the typed text exactly matches an item's label/value.\n const match = items.find((item) => (item.label ?? item.value).toLowerCase() === typed.toLowerCase());\n if (match) {\n if (formValidator) formValidator.set(match.value);\n if (onChange) onChange(match.value);\n onSelect(match.value);\n }\n }\n }}\n placeholder={placeholder}\n />\n </Popover>\n {/* Outside the portal and always mounted: a live region that mounts already filled is not\n reliably announced. */}\n <span className={styles.inputDropdownStatus} role='status' aria-live='polite'>\n {isOpen && showEmpty ? text.empty : ''}\n </span>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- "use client";var e=`inputDropdown_UdYMa`,t=`inputDropdownPanel_TAURL`,n=`dropdown_-M5-x`,r=`dropdown-scroll_XMZs-`,i=`dropdown-item_JRgbt`,a=`dropdown-item-highlighted_1ZENP`,o={inputDropdown:e,inputDropdownPanel:t,dropdown:n,"dropdown-scroll":`dropdown-scroll_XMZs-`,dropdownScroll:r,"dropdown-item":`dropdown-item_JRgbt`,dropdownItem:i,"dropdown-item-highlighted":`dropdown-item-highlighted_1ZENP`,dropdownItemHighlighted:a};export{o as default,n as dropdown,i as dropdownItem,a as dropdownItemHighlighted,r as dropdownScroll,e as inputDropdown,t as inputDropdownPanel};
1
+ "use client";var e=`inputDropdown_UdYMa`,t=`inputDropdownStatus_ORHlt`,n=`inputDropdownPanel_TAURL`,r=`dropdown_-M5-x`,i=`dropdown-scroll_XMZs-`,a=`dropdown-empty_Kx---`,o=`dropdown-item_JRgbt`,s=`dropdown-item-highlighted_1ZENP`,c={inputDropdown:e,inputDropdownStatus:t,inputDropdownPanel:n,dropdown:r,"dropdown-scroll":`dropdown-scroll_XMZs-`,dropdownScroll:i,"dropdown-empty":`dropdown-empty_Kx---`,dropdownEmpty:a,"dropdown-item":`dropdown-item_JRgbt`,dropdownItem:o,"dropdown-item-highlighted":`dropdown-item-highlighted_1ZENP`,dropdownItemHighlighted:s};export{c as default,r as dropdown,a as dropdownEmpty,o as dropdownItem,s as dropdownItemHighlighted,i as dropdownScroll,e as inputDropdown,n as inputDropdownPanel,t as inputDropdownStatus};
2
2
  //# sourceMappingURL=inputDropdown.module.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"inputDropdown.module.mjs","names":[],"sources":["../../../../package/common/inputDropdown/inputDropdown.module.pcss"],"sourcesContent":[".inputDropdown {\n position: relative;\n}\n\n/* The list brings its own padding and its rows reach the panel edge. */\n.inputDropdownPanel {\n padding: 0;\n}\n\n/* Chrome, placement and the portal come from the Popover this renders inside; the\n width is this component's own, since the list sizes to the longest suggestion\n rather than to the field. */\n.dropdown {\n width: fit-content;\n max-width: 15em;\n box-sizing: border-box;\n overflow: hidden;\n border-radius: inherit;\n\n &-scroll {\n overflow: auto;\n max-height: 12.5em;\n }\n\n &-item {\n padding: 0.5em;\n z-index: 0;\n outline: none;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n\n &:hover,\n &:focus {\n background: var(--background-accent);\n }\n\n &-highlighted {\n background: var(--background-accent);\n }\n }\n}\n"],"mappings":""}
1
+ {"version":3,"file":"inputDropdown.module.mjs","names":[],"sources":["../../../../package/common/inputDropdown/inputDropdown.module.pcss"],"sourcesContent":[".inputDropdown {\n position: relative;\n}\n\n.inputDropdownStatus {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n border: 0;\n clip-path: inset(50%);\n overflow: hidden;\n white-space: nowrap;\n}\n\n/* The list brings its own padding and its rows reach the panel edge. */\n.inputDropdownPanel {\n padding: 0;\n}\n\n/* Chrome, placement and the portal come from the Popover this renders inside; the\n width is this component's own, since the list sizes to the longest suggestion\n rather than to the field. */\n.dropdown {\n width: fit-content;\n max-width: 15em;\n box-sizing: border-box;\n overflow: hidden;\n border-radius: inherit;\n\n &-scroll {\n overflow: auto;\n max-height: 12.5em;\n }\n\n &-empty {\n padding: 0.5em;\n color: var(--text-dark);\n }\n\n &-item {\n padding: 0.5em;\n z-index: 0;\n outline: none;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n\n &:hover,\n &:focus {\n background: var(--background-accent);\n }\n\n &-highlighted {\n background: var(--background-accent);\n }\n }\n}\n"],"mappings":""}
@@ -1,2 +1,2 @@
1
- "use client";import{renderIcon as e}from"../utils/renderIcon.mjs";import{PopoverAlign as t,PopoverPlacement as n}from"../popover/popover.types.mjs";import{useFocusBoundary as r}from"../hooks/useFocusBoundary.mjs";import i from"../popover/popover.mjs";import a from"./menu.module.mjs";import{formatHotkey as o}from"../hooks/useHotkeys.mjs";import{isMenuAction as s,isMenuHeading as c,isMenuSeparator as l}from"./menu.types.mjs";import{cloneElement as u,isValidElement as d,useEffect as f,useId as p,useRef as m,useState as h}from"react";import g from"classnames";import{jsx as _,jsxs as v}from"react/jsx-runtime";var y=600;function b(e,t,n){for(let r=1;r<=e.length;r++){let i=(t+n*r+e.length*r)%e.length,a=e[i];if(s(a)&&!a.disabled)return i}return-1}function x(e){return e.findIndex(e=>s(e)&&!e.disabled)}function S(e){for(let t=e.length-1;t>=0;t--){let n=e[t];if(s(n)&&!n.disabled)return t}return-1}function C({items:C,children:w,onAction:T,isOpen:E,onOpenChange:D,placement:O=n.Bottom,align:k=t.Start,presentation:A,className:j,style:M,classNames:N,styles:P,...F}){let I=E!==void 0,[ee,L]=h(!1),R=I?E:ee,[z,B]=h(-1),V=m([]),H=m(null),U=m(null),{restoreFocusToTrigger:W,focusAfterTrigger:G}=r({triggerRef:H,contentRef:U}),K=p(),q=m(`first`),J=m({query:``,timer:void 0}),Y=m(!1);function X(e,t=!1){e&&(Y.current=t),I||L(e),D?.(e)}let Z=m(C);f(()=>{Z.current=C}),f(()=>{if(!R)return;if(!Y.current){B(-1);return}let e=Z.current,t=q.current===`last`?S(e):x(e);B(t);let n=requestAnimationFrame(()=>{V.current[t]?.focus()});return()=>{cancelAnimationFrame(n),q.current=`first`}},[R]),f(()=>()=>clearTimeout(J.current.timer),[]);function Q(e){e<0||(B(e),V.current[e]?.focus())}function $(e){X(!1),e&&W()}function te(e){if(e.length!==1||!e.trim())return!1;clearTimeout(J.current.timer),J.current.query+=e.toLowerCase(),J.current.timer=setTimeout(()=>{J.current.query=``},y);let t=J.current.query,n=t.length>1&&[...t].every(e=>e===t[0]),r=n?t[0]:t,i=n||t.length===1?z:z-1;for(let e=1;e<=C.length;e++){let t=(i+e+C.length)%C.length,n=C[t];if(s(n)&&!n.disabled&&(V.current[t]?.textContent?.trim().toLowerCase()??``).startsWith(r))return Q(t),!0}return!1}function ne(e){if(e.key===`ArrowDown`||e.key===`ArrowUp`){if(e.preventDefault(),e.stopPropagation(),R){Q(e.key===`ArrowUp`?S(C):x(C));return}q.current=e.key===`ArrowUp`?`last`:`first`,X(!0,!0)}}function re(e){switch(e.key){case`ArrowDown`:Q(b(C,z,1));break;case`ArrowUp`:Q(z<0?S(C):b(C,z,-1));break;case`Home`:Q(x(C));break;case`End`:Q(S(C));break;case`Escape`:$(!0);break;case`Tab`:$(!1),e.shiftKey?W():G();break;default:if(!te(e.key))return}e.preventDefault(),e.stopPropagation()}function ie(e){let t=C[e];s(t)&&!t.disabled&&(t.onClick?.(),T?.(t.id),$(!0))}let ae=d(w)?u(w,{"aria-haspopup":w.props[`aria-haspopup`]??`menu`,"aria-expanded":w.props[`aria-expanded`]??R,"aria-controls":R?w.props[`aria-controls`]??K:w.props[`aria-controls`]}):w,oe=_(`div`,{ref:U,id:K,role:`menu`,className:g(a.menu,N?.menu),style:P?.menu,onKeyDown:re,children:C.map((t,n)=>l(t)?_(`div`,{role:`separator`,className:g(a.menuSeparator,N?.separator),style:P?.separator},`separator-${n}`):c(t)?_(`div`,{role:`presentation`,className:g(a.menuHeading,N?.heading),style:P?.heading,children:t.heading},`heading-${n}`):v(`button`,{ref:e=>{V.current[n]=e},type:`button`,role:`menuitem`,disabled:t.disabled,tabIndex:n===z?0:-1,className:g(a.menuItem,t.danger&&a.menuItemDanger,N?.item),style:P?.item,onClick:()=>ie(n),onMouseEnter:()=>B(n),children:[t.icon!=null&&e(t.icon,{className:g(a.menuItemIcon,N?.itemIcon),style:P?.itemIcon}),_(`span`,{className:g(a.menuItemLabel,N?.itemLabel),style:P?.itemLabel,children:t.label}),t.shortcut!=null&&_(`span`,{className:g(a.menuItemShortcut,N?.itemShortcut),style:P?.itemShortcut,children:o(t.shortcut)})]},t.id))});return _(i,{...F,isOpen:R,onOpenChange:(e,{fromKeyboard:t})=>X(e,t),placement:O,align:k,presentation:A,content:oe,focusOnOpen:!1,constrainWidth:!1,classNames:{panel:a.menuPanel},className:g(j,N?.root),style:{...M,...P?.root},children:_(`span`,{ref:H,className:g(a.menuTrigger,N?.trigger),style:P?.trigger,onKeyDown:ne,children:ae})})}export{C as default};
1
+ "use client";import{renderIcon as e}from"../utils/renderIcon.mjs";import{PopoverAlign as t,PopoverPlacement as n}from"../popover/popover.types.mjs";import{useFocusBoundary as r}from"../hooks/useFocusBoundary.mjs";import i from"../popover/popover.mjs";import{formatHotkey as a}from"../hooks/useHotkeys.mjs";import{isMenuAction as o,isMenuHeading as s,isMenuSeparator as c}from"./menu.types.mjs";import l from"./menu.module.mjs";import{cloneElement as u,isValidElement as d,useEffect as f,useId as ee,useRef as p,useState as m}from"react";import h from"classnames";import{jsx as g,jsxs as _}from"react/jsx-runtime";var v=600;function y(e,t,n){for(let r=1;r<=e.length;r++){let i=(t+n*r+e.length*r)%e.length,a=e[i];if(o(a)&&!a.disabled)return i}return-1}function b(e){return e.findIndex(e=>o(e)&&!e.disabled)}function x(e){for(let t=e.length-1;t>=0;t--){let n=e[t];if(o(n)&&!n.disabled)return t}return-1}function S({items:S,children:C,onAction:w,isOpen:T,onOpenChange:E,placement:D=n.Bottom,align:O=t.Start,presentation:k,className:A,style:j,classNames:M,styles:N,...P}){let F=T!==void 0,[I,L]=m(!1),R=F?T:I,[z,B]=m(-1),V=p([]),H=p(null),U=p(null),{restoreFocusToTrigger:W,focusAfterTrigger:G}=r({triggerRef:H,contentRef:U}),K=ee(),q=p(`first`),J=p({query:``,timer:void 0}),Y=p(!1);function X(e,t=!1){e&&(Y.current=t),F||L(e),E?.(e)}let Z=p(S);f(()=>{Z.current=S}),f(()=>{if(!R)return;if(!Y.current){B(-1);return}let e=Z.current,t=q.current===`last`?x(e):b(e);B(t);let n=requestAnimationFrame(()=>{V.current[t]?.focus()});return()=>{cancelAnimationFrame(n),q.current=`first`}},[R]),f(()=>()=>clearTimeout(J.current.timer),[]);function Q(e){e<0||(B(e),V.current[e]?.focus())}function $(e){X(!1),e&&W()}function te(e){if(e.length!==1||!e.trim())return!1;clearTimeout(J.current.timer),J.current.query+=e.toLowerCase(),J.current.timer=setTimeout(()=>{J.current.query=``},v);let t=J.current.query,n=t.length>1&&[...t].every(e=>e===t[0]),r=n?t[0]:t,i=n||t.length===1?z:z-1;for(let e=1;e<=S.length;e++){let t=(i+e+S.length)%S.length,n=S[t];if(o(n)&&!n.disabled&&(V.current[t]?.textContent?.trim().toLowerCase()??``).startsWith(r))return Q(t),!0}return!1}function ne(e){if(e.key===`ArrowDown`||e.key===`ArrowUp`){if(e.preventDefault(),e.stopPropagation(),R){Q(e.key===`ArrowUp`?x(S):b(S));return}q.current=e.key===`ArrowUp`?`last`:`first`,X(!0,!0)}}function re(e){switch(e.key){case`ArrowDown`:Q(y(S,z,1));break;case`ArrowUp`:Q(z<0?x(S):y(S,z,-1));break;case`Home`:Q(b(S));break;case`End`:Q(x(S));break;case`Escape`:$(!0);break;case`Tab`:$(!1),e.shiftKey?W():G();break;default:if(!te(e.key))return}e.preventDefault(),e.stopPropagation()}function ie(e){let t=S[e];o(t)&&!t.disabled&&(t.onClick?.(),w?.(t.id),$(!0))}let ae=d(C)?u(C,{"aria-haspopup":C.props[`aria-haspopup`]??`menu`,"aria-expanded":C.props[`aria-expanded`]??R,"aria-controls":R?C.props[`aria-controls`]??K:C.props[`aria-controls`]}):C,oe=g(`div`,{ref:U,id:K,role:`menu`,className:h(l.menu,M?.menu),style:N?.menu,onKeyDown:re,children:S.map((t,n)=>c(t)?g(`div`,{role:`separator`,className:h(l.menuSeparator,M?.separator),style:N?.separator},`separator-${n}`):s(t)?g(`div`,{role:`presentation`,className:h(l.menuHeading,M?.heading),style:N?.heading,children:t.heading},`heading-${n}`):_(`button`,{ref:e=>{V.current[n]=e},type:`button`,role:`menuitem`,disabled:t.disabled,tabIndex:n===z?0:-1,className:h(l.menuItem,t.danger&&l.menuItemDanger,M?.item),style:N?.item,onClick:()=>ie(n),onMouseEnter:()=>B(n),children:[t.icon!=null&&e(t.icon,{className:h(l.menuItemIcon,M?.itemIcon),style:N?.itemIcon}),g(`span`,{className:h(l.menuItemLabel,M?.itemLabel),style:N?.itemLabel,children:t.label}),t.shortcut!=null&&g(`span`,{className:h(l.menuItemShortcut,M?.itemShortcut),style:N?.itemShortcut,children:a(t.shortcut)})]},t.id))});return g(i,{...P,isOpen:R,onOpenChange:(e,{fromKeyboard:t})=>X(e,t),placement:D,align:O,presentation:k,content:oe,focusOnOpen:!1,constrainWidth:!1,classNames:{panel:l.menuPanel},className:h(A,M?.root),style:{...j,...N?.root},children:g(`span`,{ref:H,className:h(l.menuTrigger,M?.trigger),style:N?.trigger,onKeyDown:ne,children:ae})})}export{S as default};
2
2
  //# sourceMappingURL=menu.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"menu.mjs","names":[],"sources":["../../../../package/common/menu/menu.tsx"],"sourcesContent":["import { cloneElement, isValidElement, useEffect, useId, useRef, useState, type ReactElement } from 'react';\nimport cx from 'classnames';\n\nimport styles from './menu.module.pcss';\nimport Popover from '../popover';\nimport { PopoverAlign, PopoverPlacement } from '../popover/popover.types';\nimport { renderIcon } from '../utils/renderIcon';\nimport { useFocusBoundary } from '../hooks/useFocusBoundary';\nimport { formatHotkey } from '../hooks/useHotkeys';\nimport { isMenuAction, isMenuHeading, isMenuSeparator, type MenuEntry, type MenuProps } from './menu.types';\n\n/** How long a typed run stays one search before it starts over. */\nconst TYPEAHEAD_RESET_MS = 600;\n\n/** Index of the next action that can be focused, wrapping, or -1 if there is none. */\nfunction nextEnabled(items: MenuEntry[], from: number, step: 1 | -1): number {\n for (let i = 1; i <= items.length; i++) {\n // The `items.length * i` term only keeps the value positive for a backwards\n // step; JavaScript's `%` would otherwise return a negative index.\n const index = (from + step * i + items.length * i) % items.length;\n const entry = items[index];\n if (isMenuAction(entry) && !entry.disabled) return index;\n }\n return -1;\n}\n\nfunction firstEnabled(items: MenuEntry[]): number {\n return items.findIndex((entry) => isMenuAction(entry) && !entry.disabled);\n}\n\nfunction lastEnabled(items: MenuEntry[]): number {\n for (let i = items.length - 1; i >= 0; i--) {\n const entry = items[i];\n if (isMenuAction(entry) && !entry.disabled) return i;\n }\n return -1;\n}\n\n/**\n * A menu of actions on a trigger of your choosing.\n *\n * `Popover` does the anchoring, portalling and outside-click work but takes\n * arbitrary `content`, which leaves every consumer to write their own `ul`/`li`\n * and, in practice, to leave out the roles and the arrow keys. This is that list,\n * done once: `role=\"menu\"`, a roving focus that skips disabled entries, Escape\n * back to the trigger, and separators and headings that stay out of the keyboard\n * order.\n */\nfunction Menu({\n items,\n children,\n onAction,\n isOpen: controlledOpen,\n onOpenChange,\n placement = PopoverPlacement.Bottom,\n align = PopoverAlign.Start,\n presentation,\n className,\n style,\n classNames,\n styles: slotStyles,\n ...rest\n}: MenuProps): ReactElement {\n const isControlled = controlledOpen !== undefined;\n const [internalOpen, setInternalOpen] = useState(false);\n const isOpen = isControlled ? controlledOpen : internalOpen;\n\n const [activeIndex, setActiveIndex] = useState(-1);\n const itemRefs = useRef<(HTMLButtonElement | null)[]>([]);\n const triggerRef = useRef<HTMLSpanElement>(null);\n const menuRef = useRef<HTMLDivElement>(null);\n // Tab leaves the panel in both directions, so the menu needs the same boundary\n // Popover uses: back to the trigger, or on to whatever follows it.\n const { restoreFocusToTrigger, focusAfterTrigger } = useFocusBoundary({ triggerRef, contentRef: menuRef });\n const menuId = useId();\n // Which end to land on when the menu opens: `ArrowUp` on the trigger opens at\n // the bottom, everything else at the top.\n const openAtRef = useRef<'first' | 'last'>('first');\n const typeahead = useRef<{ query: string; timer: ReturnType<typeof setTimeout> | undefined }>({\n query: '',\n timer: undefined,\n });\n\n // Whether a key opened the menu, which decides between landing the focus on an\n // entry and leaving it on the trigger. Set from Popover, which is what sees the\n // trigger's events, or here when an arrow key did the opening.\n const openedByKeyboardRef = useRef(false);\n\n function setOpen(next: boolean, fromKeyboard = false) {\n if (next) openedByKeyboardRef.current = fromKeyboard;\n if (!isControlled) setInternalOpen(next);\n onOpenChange?.(next);\n }\n\n // The effect below needs the entries, but must not re-run when the array is rebuilt. A consumer\n // writes `items={[…]}` inline, so it is a new identity on every render of *their* component:\n // keyed off it, any parent re-render while the menu was open threw the focus back to the first\n // entry, mid-navigation. Declared above the effect that reads it, so it has already run.\n const itemsRef = useRef(items);\n useEffect(() => {\n itemsRef.current = items;\n });\n\n // Focus follows the active entry rather than the menu keeping focus and\n // pointing at it: the items are real buttons, so Enter and Space are theirs.\n // Popover's own `focusOnOpen` would take the first focusable and stop there;\n // the active index has to move with it, so the menu does it itself.\n useEffect(() => {\n if (!isOpen) return;\n // A pointer open leaves the focus where the pointer put it. A highlight on an\n // entry nobody pointed at reads as a selection, and the mouse is about to land\n // somewhere else anyway; the first arrow key moves in, see the trigger's keys.\n if (!openedByKeyboardRef.current) {\n setActiveIndex(-1);\n return;\n }\n const entries = itemsRef.current;\n const target = openAtRef.current === 'last' ? lastEnabled(entries) : firstEnabled(entries);\n setActiveIndex(target);\n // The panel is portalled and animated in, so its buttons exist a frame later.\n const frame = requestAnimationFrame(() => {\n itemRefs.current[target]?.focus();\n });\n return () => {\n cancelAnimationFrame(frame);\n openAtRef.current = 'first';\n };\n }, [isOpen]);\n\n useEffect(() => () => clearTimeout(typeahead.current.timer), []);\n\n function moveTo(index: number) {\n if (index < 0) return;\n setActiveIndex(index);\n itemRefs.current[index]?.focus();\n }\n\n function close(returnFocus: boolean) {\n setOpen(false);\n if (returnFocus) restoreFocusToTrigger();\n }\n\n /**\n * Jumps to the next entry whose label starts with what has been typed. The\n * text comes off the rendered button rather than the `label` prop, which is a\n * `ReactNode` and may be an icon, a badge or anything else.\n */\n function handleTypeahead(key: string): boolean {\n if (key.length !== 1 || !key.trim()) return false;\n clearTimeout(typeahead.current.timer);\n typeahead.current.query += key.toLowerCase();\n typeahead.current.timer = setTimeout(() => {\n typeahead.current.query = '';\n }, TYPEAHEAD_RESET_MS);\n\n const query = typeahead.current.query;\n // A repeated single character walks through the entries starting with it,\n // rather than sticking to the first one.\n const repeated = query.length > 1 && [...query].every((c) => c === query[0]);\n const needle = repeated ? query[0] : query;\n const from = repeated || query.length === 1 ? activeIndex : activeIndex - 1;\n\n for (let i = 1; i <= items.length; i++) {\n const index = (from + i + items.length) % items.length;\n const entry = items[index];\n if (!isMenuAction(entry) || entry.disabled) continue;\n const text = itemRefs.current[index]?.textContent?.trim().toLowerCase() ?? '';\n if (text.startsWith(needle)) {\n moveTo(index);\n return true;\n }\n }\n return false;\n }\n\n /** The trigger's own keys, which decide where the menu opens. */\n function handleTriggerKeyDown(event: React.KeyboardEvent) {\n if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;\n // Popover opens on these keys too; letting both act would be harmless but\n // this keeps the decision about where to land in one place.\n event.preventDefault();\n event.stopPropagation();\n // Already open means a pointer opened it and the focus never left the trigger,\n // since an entry's own keys never reach here. This is the key that moves in.\n if (isOpen) {\n moveTo(event.key === 'ArrowUp' ? lastEnabled(items) : firstEnabled(items));\n return;\n }\n openAtRef.current = event.key === 'ArrowUp' ? 'last' : 'first';\n setOpen(true, true);\n }\n\n function handleKeyDown(event: React.KeyboardEvent) {\n switch (event.key) {\n case 'ArrowDown':\n moveTo(nextEnabled(items, activeIndex, 1));\n break;\n case 'ArrowUp':\n // Stepping back from \"nothing active\" lands two short of the end, so the end is named.\n moveTo(activeIndex < 0 ? lastEnabled(items) : nextEnabled(items, activeIndex, -1));\n break;\n case 'Home':\n moveTo(firstEnabled(items));\n break;\n case 'End':\n moveTo(lastEnabled(items));\n break;\n // Popover handles both of these too, but only once the focus has actually\n // reached the panel: its Escape restores focus on that condition, and its\n // Tab handling acts at the first and last focusable. A menu focuses its\n // first entry a frame after opening, so a key pressed in between would\n // fall through. Here they are unconditional, which is also what a menu\n // wants from Tab: leave, whatever is focused.\n case 'Escape':\n close(true);\n break;\n case 'Tab':\n // Carry on from the trigger, as if the panel had never been in the tab\n // order. Closing and letting the browser take it from here would move on\n // from an entry that is about to be removed, which lands at the top of\n // the document.\n close(false);\n if (event.shiftKey) restoreFocusToTrigger();\n else focusAfterTrigger();\n break;\n default:\n if (!handleTypeahead(event.key)) return;\n }\n event.preventDefault();\n event.stopPropagation();\n }\n\n function handleAction(index: number) {\n const entry = items[index];\n if (!isMenuAction(entry) || entry.disabled) return;\n entry.onClick?.();\n onAction?.(entry.id);\n close(true);\n }\n\n // A screen reader has no way to tell that this button opens a menu unless the\n // button itself says so. Only filled in where the consumer has not, so a\n // trigger that describes itself keeps doing that.\n const triggerWithMenuSemantics = isValidElement<Record<string, unknown>>(children)\n ? cloneElement(children, {\n 'aria-haspopup': children.props['aria-haspopup'] ?? 'menu',\n 'aria-expanded': children.props['aria-expanded'] ?? isOpen,\n 'aria-controls': isOpen ? (children.props['aria-controls'] ?? menuId) : children.props['aria-controls'],\n })\n : children;\n\n const content = (\n <div\n ref={menuRef}\n id={menuId}\n role='menu'\n className={cx(styles.menu, classNames?.menu)}\n style={slotStyles?.menu}\n onKeyDown={handleKeyDown}\n >\n {items.map((entry, index) => {\n if (isMenuSeparator(entry))\n return (\n <div\n // Separators and headings carry no id of their own, and reordering\n // the list re-renders it wholesale anyway.\n key={`separator-${index}`}\n role='separator'\n className={cx(styles.menuSeparator, classNames?.separator)}\n style={slotStyles?.separator}\n />\n );\n\n if (isMenuHeading(entry))\n return (\n <div\n key={`heading-${index}`}\n role='presentation'\n className={cx(styles.menuHeading, classNames?.heading)}\n style={slotStyles?.heading}\n >\n {entry.heading}\n </div>\n );\n\n return (\n <button\n key={entry.id}\n ref={(el) => {\n itemRefs.current[index] = el;\n }}\n type='button'\n role='menuitem'\n disabled={entry.disabled}\n // One tab stop for the whole menu: the arrow keys move between items.\n tabIndex={index === activeIndex ? 0 : -1}\n className={cx(styles.menuItem, entry.danger && styles.menuItemDanger, classNames?.item)}\n style={slotStyles?.item}\n onClick={() => handleAction(index)}\n onMouseEnter={() => setActiveIndex(index)}\n >\n {entry.icon != null &&\n renderIcon(entry.icon, {\n className: cx(styles.menuItemIcon, classNames?.itemIcon),\n style: slotStyles?.itemIcon,\n })}\n <span className={cx(styles.menuItemLabel, classNames?.itemLabel)} style={slotStyles?.itemLabel}>\n {entry.label}\n </span>\n {entry.shortcut != null && (\n <span className={cx(styles.menuItemShortcut, classNames?.itemShortcut)} style={slotStyles?.itemShortcut}>\n {formatHotkey(entry.shortcut)}\n </span>\n )}\n </button>\n );\n })}\n </div>\n );\n\n return (\n <Popover\n {...rest}\n isOpen={isOpen}\n onOpenChange={(next, { fromKeyboard }) => setOpen(next, fromKeyboard)}\n placement={placement}\n align={align}\n presentation={presentation}\n content={content}\n // The list pads its own rows, so a hover highlight reaches the panel edge,\n // and it sizes itself. Focus is the menu's own business, see above.\n focusOnOpen={false}\n constrainWidth={false}\n classNames={{ panel: styles.menuPanel }}\n className={cx(className, classNames?.root)}\n style={{ ...style, ...slotStyles?.root }}\n >\n <span\n ref={triggerRef}\n className={cx(styles.menuTrigger, classNames?.trigger)}\n style={slotStyles?.trigger}\n onKeyDown={handleTriggerKeyDown}\n >\n {triggerWithMenuSemantics}\n </span>\n </Popover>\n );\n}\n\nexport default Menu;\n"],"mappings":"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"}
1
+ {"version":3,"file":"menu.mjs","names":[],"sources":["../../../../package/common/menu/menu.tsx"],"sourcesContent":["import { cloneElement, isValidElement, useEffect, useId, useRef, useState, type ReactElement } from 'react';\nimport cx from 'classnames';\n\nimport styles from './menu.module.pcss';\nimport Popover from '../popover';\nimport { PopoverAlign, PopoverPlacement } from '../popover/popover.types';\nimport { renderIcon } from '../utils/renderIcon';\nimport { useFocusBoundary } from '../hooks/useFocusBoundary';\nimport { formatHotkey } from '../hooks/useHotkeys';\nimport { isMenuAction, isMenuHeading, isMenuSeparator, type MenuEntry, type MenuProps } from './menu.types';\n\n/** How long a typed run stays one search before it starts over. */\nconst TYPEAHEAD_RESET_MS = 600;\n\n/** Index of the next action that can be focused, wrapping, or -1 if there is none. */\nfunction nextEnabled(items: MenuEntry[], from: number, step: 1 | -1): number {\n for (let i = 1; i <= items.length; i++) {\n // The `items.length * i` term only keeps the value positive for a backwards\n // step; JavaScript's `%` would otherwise return a negative index.\n const index = (from + step * i + items.length * i) % items.length;\n const entry = items[index];\n if (isMenuAction(entry) && !entry.disabled) return index;\n }\n return -1;\n}\n\nfunction firstEnabled(items: MenuEntry[]): number {\n return items.findIndex((entry) => isMenuAction(entry) && !entry.disabled);\n}\n\nfunction lastEnabled(items: MenuEntry[]): number {\n for (let i = items.length - 1; i >= 0; i--) {\n const entry = items[i];\n if (isMenuAction(entry) && !entry.disabled) return i;\n }\n return -1;\n}\n\n/**\n * A menu of actions on a trigger of your choosing.\n *\n * `Popover` does the anchoring, portalling and outside-click work but takes\n * arbitrary `content`, which leaves every consumer to write their own `ul`/`li`\n * and, in practice, to leave out the roles and the arrow keys. This is that list,\n * done once: `role=\"menu\"`, a roving focus that skips disabled entries, Escape\n * back to the trigger, and separators and headings that stay out of the keyboard\n * order.\n */\nfunction Menu({\n items,\n children,\n onAction,\n isOpen: controlledOpen,\n onOpenChange,\n placement = PopoverPlacement.Bottom,\n align = PopoverAlign.Start,\n presentation,\n className,\n style,\n classNames,\n styles: slotStyles,\n ...rest\n}: MenuProps): ReactElement {\n const isControlled = controlledOpen !== undefined;\n const [internalOpen, setInternalOpen] = useState(false);\n const isOpen = isControlled ? controlledOpen : internalOpen;\n\n const [activeIndex, setActiveIndex] = useState(-1);\n const itemRefs = useRef<(HTMLButtonElement | null)[]>([]);\n const triggerRef = useRef<HTMLSpanElement>(null);\n const menuRef = useRef<HTMLDivElement>(null);\n // Tab leaves the panel in both directions, so the menu needs the same boundary\n // Popover uses: back to the trigger, or on to whatever follows it.\n const { restoreFocusToTrigger, focusAfterTrigger } = useFocusBoundary({ triggerRef, contentRef: menuRef });\n const menuId = useId();\n // Which end to land on when the menu opens: `ArrowUp` on the trigger opens at\n // the bottom, everything else at the top.\n const openAtRef = useRef<'first' | 'last'>('first');\n const typeahead = useRef<{ query: string; timer: ReturnType<typeof setTimeout> | undefined }>({\n query: '',\n timer: undefined,\n });\n\n // Whether a key opened the menu, which decides between landing the focus on an\n // entry and leaving it on the trigger. Set from Popover, which is what sees the\n // trigger's events, or here when an arrow key did the opening.\n const openedByKeyboardRef = useRef(false);\n\n function setOpen(next: boolean, fromKeyboard = false) {\n if (next) openedByKeyboardRef.current = fromKeyboard;\n if (!isControlled) setInternalOpen(next);\n onOpenChange?.(next);\n }\n\n // The effect below needs the entries, but must not re-run when the array is rebuilt. A consumer\n // writes `items={[…]}` inline, so it is a new identity on every render of *their* component:\n // keyed off it, any parent re-render while the menu was open threw the focus back to the first\n // entry, mid-navigation. Declared above the effect that reads it, so it has already run.\n const itemsRef = useRef(items);\n useEffect(() => {\n itemsRef.current = items;\n });\n\n // Focus follows the active entry rather than the menu keeping focus and\n // pointing at it: the items are real buttons, so Enter and Space are theirs.\n // Popover's own `focusOnOpen` would take the first focusable and stop there;\n // the active index has to move with it, so the menu does it itself.\n useEffect(() => {\n if (!isOpen) return;\n // A pointer open leaves the focus where the pointer put it. A highlight on an\n // entry nobody pointed at reads as a selection, and the mouse is about to land\n // somewhere else anyway; the first arrow key moves in, see the trigger's keys.\n if (!openedByKeyboardRef.current) {\n setActiveIndex(-1);\n return;\n }\n const entries = itemsRef.current;\n const target = openAtRef.current === 'last' ? lastEnabled(entries) : firstEnabled(entries);\n setActiveIndex(target);\n // The panel is portalled and animated in, so its buttons exist a frame later.\n const frame = requestAnimationFrame(() => {\n itemRefs.current[target]?.focus();\n });\n return () => {\n cancelAnimationFrame(frame);\n openAtRef.current = 'first';\n };\n }, [isOpen]);\n\n useEffect(() => () => clearTimeout(typeahead.current.timer), []);\n\n function moveTo(index: number) {\n if (index < 0) return;\n setActiveIndex(index);\n itemRefs.current[index]?.focus();\n }\n\n function close(returnFocus: boolean) {\n setOpen(false);\n if (returnFocus) restoreFocusToTrigger();\n }\n\n /**\n * Jumps to the next entry whose label starts with what has been typed. The\n * text comes off the rendered button rather than the `label` prop, which is a\n * `ReactNode` and may be an icon, a badge or anything else.\n */\n function handleTypeahead(key: string): boolean {\n if (key.length !== 1 || !key.trim()) return false;\n clearTimeout(typeahead.current.timer);\n typeahead.current.query += key.toLowerCase();\n typeahead.current.timer = setTimeout(() => {\n typeahead.current.query = '';\n }, TYPEAHEAD_RESET_MS);\n\n const query = typeahead.current.query;\n // A repeated single character walks through the entries starting with it,\n // rather than sticking to the first one.\n const repeated = query.length > 1 && [...query].every((c) => c === query[0]);\n const needle = repeated ? query[0] : query;\n const from = repeated || query.length === 1 ? activeIndex : activeIndex - 1;\n\n for (let i = 1; i <= items.length; i++) {\n const index = (from + i + items.length) % items.length;\n const entry = items[index];\n if (!isMenuAction(entry) || entry.disabled) continue;\n const text = itemRefs.current[index]?.textContent?.trim().toLowerCase() ?? '';\n if (text.startsWith(needle)) {\n moveTo(index);\n return true;\n }\n }\n return false;\n }\n\n /** The trigger's own keys, which decide where the menu opens. */\n function handleTriggerKeyDown(event: React.KeyboardEvent) {\n if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;\n // Popover opens on these keys too; letting both act would be harmless but\n // this keeps the decision about where to land in one place.\n event.preventDefault();\n event.stopPropagation();\n // Already open means a pointer opened it and the focus never left the trigger,\n // since an entry's own keys never reach here. This is the key that moves in.\n if (isOpen) {\n moveTo(event.key === 'ArrowUp' ? lastEnabled(items) : firstEnabled(items));\n return;\n }\n openAtRef.current = event.key === 'ArrowUp' ? 'last' : 'first';\n setOpen(true, true);\n }\n\n function handleKeyDown(event: React.KeyboardEvent) {\n switch (event.key) {\n case 'ArrowDown':\n moveTo(nextEnabled(items, activeIndex, 1));\n break;\n case 'ArrowUp':\n // Stepping back from \"nothing active\" lands two short of the end, so the end is named.\n moveTo(activeIndex < 0 ? lastEnabled(items) : nextEnabled(items, activeIndex, -1));\n break;\n case 'Home':\n moveTo(firstEnabled(items));\n break;\n case 'End':\n moveTo(lastEnabled(items));\n break;\n // Popover handles both of these too, but only once the focus has actually\n // reached the panel: its Escape restores focus on that condition, and its\n // Tab handling acts at the first and last focusable. A menu focuses its\n // first entry a frame after opening, so a key pressed in between would\n // fall through. Here they are unconditional, which is also what a menu\n // wants from Tab: leave, whatever is focused.\n case 'Escape':\n close(true);\n break;\n case 'Tab':\n // Carry on from the trigger, as if the panel had never been in the tab\n // order. Closing and letting the browser take it from here would move on\n // from an entry that is about to be removed, which lands at the top of\n // the document.\n close(false);\n if (event.shiftKey) restoreFocusToTrigger();\n else focusAfterTrigger();\n break;\n default:\n if (!handleTypeahead(event.key)) return;\n }\n event.preventDefault();\n event.stopPropagation();\n }\n\n function handleAction(index: number) {\n const entry = items[index];\n if (!isMenuAction(entry) || entry.disabled) return;\n entry.onClick?.();\n onAction?.(entry.id);\n close(true);\n }\n\n // A screen reader has no way to tell that this button opens a menu unless the\n // button itself says so. Only filled in where the consumer has not, so a\n // trigger that describes itself keeps doing that.\n const triggerWithMenuSemantics = isValidElement<Record<string, unknown>>(children)\n ? cloneElement(children, {\n 'aria-haspopup': children.props['aria-haspopup'] ?? 'menu',\n 'aria-expanded': children.props['aria-expanded'] ?? isOpen,\n 'aria-controls': isOpen ? (children.props['aria-controls'] ?? menuId) : children.props['aria-controls'],\n })\n : children;\n\n const content = (\n <div\n ref={menuRef}\n id={menuId}\n role='menu'\n className={cx(styles.menu, classNames?.menu)}\n style={slotStyles?.menu}\n onKeyDown={handleKeyDown}\n >\n {items.map((entry, index) => {\n if (isMenuSeparator(entry))\n return (\n <div\n // Separators and headings carry no id of their own, and reordering\n // the list re-renders it wholesale anyway.\n key={`separator-${index}`}\n role='separator'\n className={cx(styles.menuSeparator, classNames?.separator)}\n style={slotStyles?.separator}\n />\n );\n\n if (isMenuHeading(entry))\n return (\n <div\n key={`heading-${index}`}\n role='presentation'\n className={cx(styles.menuHeading, classNames?.heading)}\n style={slotStyles?.heading}\n >\n {entry.heading}\n </div>\n );\n\n return (\n <button\n key={entry.id}\n ref={(el) => {\n itemRefs.current[index] = el;\n }}\n type='button'\n role='menuitem'\n disabled={entry.disabled}\n // One tab stop for the whole menu: the arrow keys move between items.\n tabIndex={index === activeIndex ? 0 : -1}\n className={cx(styles.menuItem, entry.danger && styles.menuItemDanger, classNames?.item)}\n style={slotStyles?.item}\n onClick={() => handleAction(index)}\n onMouseEnter={() => setActiveIndex(index)}\n >\n {entry.icon != null &&\n renderIcon(entry.icon, {\n className: cx(styles.menuItemIcon, classNames?.itemIcon),\n style: slotStyles?.itemIcon,\n })}\n <span className={cx(styles.menuItemLabel, classNames?.itemLabel)} style={slotStyles?.itemLabel}>\n {entry.label}\n </span>\n {entry.shortcut != null && (\n <span className={cx(styles.menuItemShortcut, classNames?.itemShortcut)} style={slotStyles?.itemShortcut}>\n {formatHotkey(entry.shortcut)}\n </span>\n )}\n </button>\n );\n })}\n </div>\n );\n\n return (\n <Popover\n {...rest}\n isOpen={isOpen}\n onOpenChange={(next, { fromKeyboard }) => setOpen(next, fromKeyboard)}\n placement={placement}\n align={align}\n presentation={presentation}\n content={content}\n // The list pads its own rows, so a hover highlight reaches the panel edge,\n // and it sizes itself. Focus is the menu's own business, see above.\n focusOnOpen={false}\n constrainWidth={false}\n classNames={{ panel: styles.menuPanel }}\n className={cx(className, classNames?.root)}\n style={{ ...style, ...slotStyles?.root }}\n >\n <span\n ref={triggerRef}\n className={cx(styles.menuTrigger, classNames?.trigger)}\n style={slotStyles?.trigger}\n onKeyDown={handleTriggerKeyDown}\n >\n {triggerWithMenuSemantics}\n </span>\n </Popover>\n );\n}\n\nexport default Menu;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"menu.types.mjs","names":[],"sources":["../../../../package/common/menu/menu.types.ts"],"sourcesContent":["import type { CSSProperties, ReactNode } from 'react';\nimport type { SlotClassNames, SlotStyles, HtmlProps } from '../types/slots.types';\nimport type { IconContent } from '../types/icon.types';\nimport type { PopoverAlign, PopoverPlacement } from '../popover/popover.types';\nimport type { Presentation } from '../configProvider/presentation.types';\n\n/** One thing the menu can do. */\nexport interface MenuAction {\n /** Stable id, also what `onAction` reports. */\n id: string;\n label: ReactNode;\n icon?: IconContent;\n /**\n * A keyboard shortcut for this action, in `useHotkeys` spelling: `'mod+k'`,\n * `'shift+f2'`, `'delete'`. Shown on the trailing edge written for the\n * platform: `⌘K` on an Apple one, `Ctrl+K` elsewhere.\n *\n * The menu draws it and nothing more. Binding is yours, because only the app\n * knows what else is listening and whether the shortcut should work at all\n * times or only while the menu is open. `menuHotkeys` turns these entries\n * into a `useHotkeys` map, so the one spelling covers both ends.\n */\n shortcut?: string;\n disabled?: boolean;\n /** Renders in the destructive colour. For deleting, discarding, revoking. */\n danger?: boolean;\n onClick?: () => void;\n}\n\n/** A line between groups of actions. */\nexport interface MenuSeparator {\n separator: true;\n}\n\n/** A label over the actions that follow it. */\nexport interface MenuHeading {\n heading: ReactNode;\n}\n\nexport type MenuEntry = MenuAction | MenuSeparator | MenuHeading;\n\nexport function isMenuSeparator(entry: MenuEntry): entry is MenuSeparator {\n return 'separator' in entry;\n}\n\nexport function isMenuHeading(entry: MenuEntry): entry is MenuHeading {\n return 'heading' in entry;\n}\n\nexport function isMenuAction(entry: MenuEntry): entry is MenuAction {\n return !isMenuSeparator(entry) && !isMenuHeading(entry);\n}\n\nexport type MenuSlots =\n | 'root'\n | 'trigger'\n | 'menu'\n | 'item'\n | 'itemIcon'\n | 'itemLabel'\n | 'itemShortcut'\n | 'separator'\n | 'heading';\n\nexport interface MenuProps extends Omit<HtmlProps, 'onSelect'> {\n /** What the menu offers, in the order it is read. */\n items: MenuEntry[];\n /** The element that opens the menu. */\n children?: ReactNode;\n /** Called with the id of the action that was picked, after its own `onClick`. */\n onAction?: (id: string) => void;\n /** Controlled open state. Leave it out to let the menu manage its own. */\n isOpen?: boolean;\n onOpenChange?: (isOpen: boolean) => void;\n /** Which side of the trigger the menu opens on (default `Bottom`). */\n placement?: PopoverPlacement;\n /** Which edge it lines up with on the cross axis (default `Start`). */\n align?: PopoverAlign;\n /** Anchored to the trigger, pinned to the bottom of the screen, or the latter only on touch. */\n presentation?: Presentation;\n className?: string;\n style?: CSSProperties;\n classNames?: SlotClassNames<MenuSlots>;\n styles?: SlotStyles<MenuSlots>;\n}\n"],"mappings":"aAyCA,SAAgB,EAAgB,EAA0C,CACxE,MAAO,cAAe,CACxB,CAEA,SAAgB,EAAc,EAAwC,CACpE,MAAO,YAAa,CACtB,CAEA,SAAgB,EAAa,EAAuC,CAClE,MAAO,CAAC,EAAgB,CAAK,GAAK,CAAC,EAAc,CAAK,CACxD"}
1
+ {"version":3,"file":"menu.types.mjs","names":[],"sources":["../../../../package/common/menu/menu.types.ts"],"sourcesContent":["import type { CSSProperties, ReactNode } from 'react';\nimport type { SlotClassNames, SlotStyles, HtmlProps } from '../types/slots.types';\nimport type { IconContent } from '../types/icon.types';\nimport type { PopoverAlign, PopoverPlacement } from '../popover/popover.types';\nimport type { Presentation } from '../configProvider/presentation.types';\n\n/** One thing the menu can do. */\nexport interface MenuAction {\n /** Stable id, also what `onAction` reports. */\n id: string;\n label: ReactNode;\n icon?: IconContent;\n /**\n * A keyboard shortcut for this action, in `useHotkeys` spelling: `'mod+k'`,\n * `'shift+f2'`, `'delete'`. Shown on the trailing edge written for the\n * platform: `⌘K` on an Apple one, `Ctrl+K` elsewhere.\n *\n * The menu draws it and nothing more. Binding is yours, because only the app\n * knows what else is listening and whether the shortcut should work at all\n * times or only while the menu is open. `menuHotkeys` turns these entries\n * into a `useHotkeys` map, so the one spelling covers both ends.\n */\n shortcut?: string;\n /** Extra search terms for `CommandPalette`, which also matches a string `label`. `Menu` ignores them. */\n keywords?: string[];\n disabled?: boolean;\n /** Renders in the destructive colour. For deleting, discarding, revoking. */\n danger?: boolean;\n onClick?: () => void;\n}\n\n/** A line between groups of actions. */\nexport interface MenuSeparator {\n separator: true;\n}\n\n/** A label over the actions that follow it. */\nexport interface MenuHeading {\n heading: ReactNode;\n}\n\nexport type MenuEntry = MenuAction | MenuSeparator | MenuHeading;\n\nexport function isMenuSeparator(entry: MenuEntry): entry is MenuSeparator {\n return 'separator' in entry;\n}\n\nexport function isMenuHeading(entry: MenuEntry): entry is MenuHeading {\n return 'heading' in entry;\n}\n\nexport function isMenuAction(entry: MenuEntry): entry is MenuAction {\n return !isMenuSeparator(entry) && !isMenuHeading(entry);\n}\n\nexport type MenuSlots =\n | 'root'\n | 'trigger'\n | 'menu'\n | 'item'\n | 'itemIcon'\n | 'itemLabel'\n | 'itemShortcut'\n | 'separator'\n | 'heading';\n\nexport interface MenuProps extends Omit<HtmlProps, 'onSelect'> {\n /** What the menu offers, in the order it is read. */\n items: MenuEntry[];\n /** The element that opens the menu. */\n children?: ReactNode;\n /** Called with the id of the action that was picked, after its own `onClick`. */\n onAction?: (id: string) => void;\n /** Controlled open state. Leave it out to let the menu manage its own. */\n isOpen?: boolean;\n onOpenChange?: (isOpen: boolean) => void;\n /** Which side of the trigger the menu opens on (default `Bottom`). */\n placement?: PopoverPlacement;\n /** Which edge it lines up with on the cross axis (default `Start`). */\n align?: PopoverAlign;\n /** Anchored to the trigger, pinned to the bottom of the screen, or the latter only on touch. */\n presentation?: Presentation;\n className?: string;\n style?: CSSProperties;\n classNames?: SlotClassNames<MenuSlots>;\n styles?: SlotStyles<MenuSlots>;\n}\n"],"mappings":"aA2CA,SAAgB,EAAgB,EAA0C,CACxE,MAAO,cAAe,CACxB,CAEA,SAAgB,EAAc,EAAwC,CACpE,MAAO,YAAa,CACtB,CAEA,SAAgB,EAAa,EAAuC,CAClE,MAAO,CAAC,EAAgB,CAAK,GAAK,CAAC,EAAc,CAAK,CACxD"}
@@ -1,2 +1,2 @@
1
- "use client";import{useComponentDefaults as e}from"../configProvider/useComponentDefaults.mjs";import{usePresentationDefault as t}from"../configProvider/usePresentationDefault.mjs";import{Presentation as n}from"../configProvider/presentation.types.mjs";import r from"../bodyEnd/bodyEnd.mjs";import i from"./popover.module.mjs";import a from"../styles/sheet.module.mjs";import o from"../sheetBackdrop/sheetBackdrop.mjs";import{PopoverAlign as ee,PopoverPlacement as s,PopoverTrigger as c}from"./popover.types.mjs";import{usePopoverPosition as te}from"./usePopoverPosition.mjs";import{tabbablesIn as l,useFocusBoundary as ne}from"../hooks/useFocusBoundary.mjs";import u from"../hooks/useSheet.mjs";import{useOverlay as d}from"../hooks/useOverlay.mjs";import{useCallback as f,useEffect as p,useRef as m,useState as re}from"react";import h from"classnames";import{jsx as g,jsxs as _}from"react/jsx-runtime";function v(v){let{children:y=null,content:b=null,placement:x=s.Bottom,align:S=ee.Center,trigger:C=c.Click,isOpen:w,defaultOpen:T=!1,onOpenChange:E=()=>{},offset:ie=10,withArrow:D=!0,closeOnOutsideClick:O=!0,closeOnEscape:ae=!0,openDelay:oe=100,closeDelay:se=120,disabled:k=!1,focusOnOpen:A=`keyboard`,closeOnTriggerClick:ce=!0,width:j=`auto`,constrainWidth:M=!0,presentation:N,className:P,style:F,classNames:I,styles:L,...R}=e(`Popover`,v),z=m(null),B=m(null),V=m(null),H=m(null),U=m(void 0),W=m(!1),G=m(!1);function K(){W.current=!0}let q=w!==void 0,[le,ue]=re(T),J=q?w:le,Y=m(E);p(()=>{Y.current=E});let X=f((e,t=!1)=>{k&&e||(e&&(G.current=t),q||ue(e),Y.current(e,{fromKeyboard:t}))},[k,q]),de=t(),fe=N??de??n.Default,{isSheet:pe,sheetStyle:me}=u(fe),Z=pe&&C===c.Click;te({isOpen:!!J&&!Z,placement:x,align:S,offset:ie,triggerRef:z,floatingRef:B,panelRef:V,arrowRef:D?H:{current:null},matchTriggerWidth:j===`trigger`}),d({isActive:!!J,containerRef:V,onEscape:ae?()=>{B.current?.contains(document.activeElement)&&(l(z.current)[0]??z.current)?.focus(),X(!1,!0)}:void 0,trapFocus:!1,lockScroll:Z});let{restoreFocusToTrigger:Q,focusAfterTrigger:he}=ne({triggerRef:z,contentRef:B});p(()=>{if(!J){G.current=!1;return}if(!(A===!0||A===`keyboard`&&G.current)||C!==c.Click)return;let e=l(V.current);e.length!==0&&(B.current?.setAttribute(`role`,`dialog`),e[0].focus())},[J,C,A]);function ge(e){if(e.key!==`Tab`||C!==c.Click)return;let t=l(V.current);if(t.length===0)return;let n=document.activeElement;!e.shiftKey&&n===t[t.length-1]?(e.preventDefault(),he(),X(!1,!0)):e.shiftKey&&n===t[0]&&(e.preventDefault(),Q(),X(!1,!0))}p(()=>{if(!J)return;function e(e){let t=W.current;W.current=!1;let n=e.target,r=B.current?.contains(n),i=z.current?.contains(n);!t&&!r&&!i&&X(!1)}function t(e){let t=W.current;W.current=!1;let n=e.target,r=B.current?.contains(n),i=z.current?.contains(n);!t&&!r&&!i&&X(!1,!0)}return O&&C!==c.Hover&&(window.addEventListener(`mouseup`,e),document.addEventListener(`focusin`,t)),()=>{window.removeEventListener(`mouseup`,e),document.removeEventListener(`focusin`,t)}},[J,O,C,X]),p(()=>()=>clearTimeout(U.current),[]);function $(e){clearTimeout(U.current),U.current=setTimeout(()=>X(e),e?oe:se)}function _e(e){J||e.key!==`ArrowDown`&&e.key!==`ArrowUp`||(e.preventDefault(),X(!0,!0))}let ve=C===c.Click?{onClick:e=>{(!J||ce)&&X(!J,e.detail===0)},onKeyDown:_e}:C===c.Hover?{onMouseEnter:()=>$(!0),onMouseLeave:()=>$(!1)}:{onFocus:()=>X(!0),onBlur:()=>X(!1)},ye=C===c.Hover?{onMouseEnter:()=>$(!0),onMouseLeave:()=>$(!1)}:{};return _(`div`,{...R,className:h(i.popover,P,I?.root),style:{...F,...L?.root},children:[g(`div`,{ref:z,className:h(i.popoverTrigger,I?.trigger),style:L?.trigger,...ve,children:y}),J&&_(r,{children:[Z&&g(o,{onClose:()=>{O&&X(!1)},className:I?.backdrop,style:L?.backdrop}),_(`div`,{ref:B,className:h(Z?a.sheet:i.popoverFloating,I?.floating),style:{...Z?me:void 0,...L?.floating},"data-placement":x,onKeyDown:ge,onMouseUpCapture:K,onFocusCapture:K,...ye,children:[g(`div`,{ref:V,className:h(i.popoverPanel,M&&i.popoverPanelConstrained,Z&&a.sheetPanel,I?.panel),style:L?.panel,children:b}),D&&!Z&&g(`div`,{ref:H,className:h(i.popoverArrow,I?.arrow),style:L?.arrow})]})]})]})}export{v as default};
1
+ "use client";import{useComponentDefaults as e}from"../configProvider/useComponentDefaults.mjs";import{usePresentationDefault as t}from"../configProvider/usePresentationDefault.mjs";import{Presentation as n}from"../configProvider/presentation.types.mjs";import r from"../bodyEnd/bodyEnd.mjs";import i from"./popover.module.mjs";import a from"../styles/sheet.module.mjs";import o from"../sheetBackdrop/sheetBackdrop.mjs";import{PopoverAlign as ee,PopoverPlacement as s,PopoverTrigger as c}from"./popover.types.mjs";import{usePopoverPosition as l}from"./usePopoverPosition.mjs";import{tabbablesIn as u,useFocusBoundary as te}from"../hooks/useFocusBoundary.mjs";import ne from"../hooks/useSheet.mjs";import{useOverlay as re}from"../hooks/useOverlay.mjs";import{useCallback as ie,useEffect as d,useRef as f,useState as p}from"react";import m from"classnames";import{jsx as h,jsxs as g}from"react/jsx-runtime";var ae=1e3;function _(e,t){return e?{current:e}:t}function v(v){let{children:y=null,anchor:b,content:x=null,placement:S=s.Bottom,align:C=ee.Center,trigger:w=c.Click,isOpen:T,defaultOpen:oe=!1,onOpenChange:E=()=>{},offset:se=10,withArrow:D=!0,closeOnOutsideClick:O=!0,closeOnEscape:ce=!0,openDelay:le=100,closeDelay:ue=120,disabled:k=!1,focusOnOpen:A=`keyboard`,closeOnTriggerClick:de=!0,width:fe=`auto`,constrainWidth:pe=!0,presentation:me,className:he,style:ge,classNames:j,styles:M,...N}=e(`Popover`,v),P=f(null),F=f(null),I=f(null),L=f(null),R=f(void 0),z=f(!1),B=f(!1);function V(){z.current=!0}let[H,_e]=p(()=>({anchor:b,ref:_(b,P)}));H.anchor!==b&&_e({anchor:b,ref:_(b,P)});let U=H.ref,W=f(null),G=b!==void 0;d(()=>{if(!G)return;function e(e){W.current={target:e.target,keyboard:e.detail===0,time:performance.now()}}return document.addEventListener(`click`,e,!0),()=>document.removeEventListener(`click`,e,!0)},[G]);let K=T!==void 0,[ve,ye]=p(oe),q=K?T:ve,J=f(E);d(()=>{J.current=E});let Y=ie((e,t=!1)=>{k&&e||(e&&(B.current=t),K||ye(e),J.current(e,{fromKeyboard:t}))},[k,K]),be=t(),xe=me??be??n.Default,{isSheet:Se,sheetStyle:Ce}=ne(xe),X=Se&&w===c.Click;l({isOpen:!!q&&!X,placement:S,align:C,offset:se,triggerRef:U,floatingRef:F,panelRef:I,arrowRef:D?L:{current:null},matchTriggerWidth:fe===`trigger`});let{restoreFocusToTrigger:Z,focusAfterTrigger:we}=te({triggerRef:U,contentRef:F});re({isActive:!!q,containerRef:I,onEscape:ce?()=>{F.current?.contains(document.activeElement)&&Z(),Y(!1,!0)}:void 0,trapFocus:!1,lockScroll:X}),d(()=>{if(!q){B.current=!1;return}let e=W.current;W.current=null;let t=!!b&&!!e?.keyboard&&b.contains(e.target)&&performance.now()-e.time<ae;if(!(A===!0||A===`keyboard`&&(B.current||t))||w!==c.Click)return;let n=u(I.current);n.length!==0&&(F.current?.setAttribute(`role`,`dialog`),n[0].focus())},[q,w,A,b]);function Q(e){if(e.key!==`Tab`||w!==c.Click)return;let t=u(I.current);if(t.length===0)return;let n=document.activeElement;!e.shiftKey&&n===t[t.length-1]?(e.preventDefault(),we(),Y(!1,!0)):e.shiftKey&&n===t[0]&&(e.preventDefault(),Z(),Y(!1,!0))}d(()=>{if(!q)return;z.current=!1;function e(e){let t=z.current;z.current=!1;let n=e.target,r=F.current?.contains(n),i=P.current?.contains(n)||U.current?.contains(n);!t&&!r&&!i&&Y(!1)}function t(e){let t=z.current;z.current=!1;let n=e.target,r=F.current?.contains(n),i=P.current?.contains(n)||U.current?.contains(n);!t&&!r&&!i&&Y(!1,!0)}return O&&w!==c.Hover&&(window.addEventListener(`mouseup`,e),document.addEventListener(`focusin`,t)),()=>{window.removeEventListener(`mouseup`,e),document.removeEventListener(`focusin`,t)}},[q,O,w,Y,U]),d(()=>()=>clearTimeout(R.current),[]);function $(e){clearTimeout(R.current),R.current=setTimeout(()=>Y(e),e?le:ue)}function Te(e){q||e.key!==`ArrowDown`&&e.key!==`ArrowUp`||(e.preventDefault(),Y(!0,!0))}let Ee=w===c.Click?{onClick:e=>{(!q||de)&&Y(!q,e.detail===0)},onKeyDown:Te}:w===c.Hover?{onMouseEnter:()=>$(!0),onMouseLeave:()=>$(!1)}:{onFocus:()=>Y(!0),onBlur:()=>Y(!1)},De=w===c.Hover?{onMouseEnter:()=>$(!0),onMouseLeave:()=>$(!1)}:{};return g(`div`,{...N,className:m(i.popover,he,j?.root),style:{...ge,...M?.root},children:[h(`div`,{ref:P,className:m(i.popoverTrigger,j?.trigger),style:M?.trigger,...Ee,children:y}),q&&g(r,{children:[X&&h(o,{onClose:()=>{O&&Y(!1)},className:j?.backdrop,style:M?.backdrop}),g(`div`,{ref:F,className:m(X?a.sheet:i.popoverFloating,j?.floating),style:{...X?Ce:void 0,...M?.floating},"data-placement":S,onKeyDown:Q,onMouseUpCapture:V,onFocusCapture:V,...De,children:[h(`div`,{ref:I,className:m(i.popoverPanel,pe&&i.popoverPanelConstrained,X&&a.sheetPanel,j?.panel),style:M?.panel,children:x}),D&&!X&&h(`div`,{ref:L,className:m(i.popoverArrow,j?.arrow),style:M?.arrow})]})]})]})}export{v as default};
2
2
  //# sourceMappingURL=popover.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"popover.mjs","names":[],"sources":["../../../../package/common/popover/popover.tsx"],"sourcesContent":["import { useRef, useEffect, useState, useCallback } from 'react';\nimport cx from 'classnames';\nimport BodyEnd from 'components/common/bodyEnd';\n\nimport styles from './popover.module.pcss';\nimport sheetStyles from '../styles/sheet.module.pcss';\nimport SheetBackdrop from '../sheetBackdrop';\nimport { usePopoverPosition } from './usePopoverPosition';\nimport { useFocusBoundary, tabbablesIn } from '../hooks/useFocusBoundary';\nimport { useSheet } from '../hooks/useSheet';\nimport { useOverlay } from '../hooks/useOverlay';\nimport { useComponentDefaults, usePresentationDefault } from '../configProvider';\nimport { Presentation } from '../configProvider/presentation.types';\nimport { PopoverPlacement, PopoverAlign, PopoverTrigger, type PopoverProps } from './popover.types';\n\nfunction Popover(props: PopoverProps) {\n const {\n children = null,\n content = null,\n placement = PopoverPlacement.Bottom,\n align = PopoverAlign.Center,\n trigger = PopoverTrigger.Click,\n isOpen,\n defaultOpen = false,\n onOpenChange = () => {},\n offset = 10,\n withArrow = true,\n closeOnOutsideClick = true,\n closeOnEscape = true,\n openDelay = 100,\n closeDelay = 120,\n disabled = false,\n focusOnOpen = 'keyboard',\n closeOnTriggerClick = true,\n width = 'auto',\n constrainWidth = true,\n presentation: presentationProp,\n className,\n style,\n classNames,\n styles: slotStyles,\n ...rest\n } = useComponentDefaults('Popover', props);\n const triggerRef = useRef<HTMLDivElement>(null);\n const floatingRef = useRef<HTMLDivElement>(null);\n const panelRef = useRef<HTMLDivElement>(null);\n const arrowRef = useRef<HTMLDivElement>(null);\n const hoverTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n // Set from a capture handler on the panel, which React fires for its whole\n // React subtree — including children that portal elsewhere in the DOM, such as\n // a Dropdown's list inside a DatePicker's calendar. A DOM `contains` check\n // cannot see those, and without this the panel closes when one is used.\n const panelInteractedRef = useRef(false);\n // Whether the open that is running came from the keyboard, which is what the\n // default `focusOnOpen` follows and what `onOpenChange` reports. A click raised\n // by Enter or Space reports `detail === 0`, where a pointer press counts the\n // clicks; an open with no trigger event at all, such as a panel that appears\n // while the user types, leaves this false.\n const openedByKeyboardRef = useRef(false);\n function markPanelInteraction() {\n panelInteractedRef.current = true;\n }\n\n const isControlled = isOpen !== undefined;\n const [internalOpen, setInternalOpen] = useState(defaultOpen);\n const open = isControlled ? isOpen : internalOpen;\n\n // Keep the outside-click/Escape listeners pointed at the latest setter without re-subscribing.\n const onOpenChangeRef = useRef(onOpenChange);\n useEffect(() => {\n onOpenChangeRef.current = onOpenChange;\n });\n\n const setOpen = useCallback(\n (next: boolean, fromKeyboard = false) => {\n if (disabled && next) return;\n if (next) openedByKeyboardRef.current = fromKeyboard;\n if (!isControlled) setInternalOpen(next);\n onOpenChangeRef.current(next, { fromKeyboard });\n },\n [disabled, isControlled],\n );\n\n // Explicit prop → this component's global default → the app-wide one → anchored.\n const appPresentation = usePresentationDefault();\n const presentation = presentationProp ?? appPresentation ?? Presentation.Default;\n const { isSheet: sheetPresentation, sheetStyle } = useSheet(presentation);\n // Hover and focus are pointer affordances that open something small next to what caused them;\n // a sheet is a deliberate, screen-owning open. Only a click asks for one.\n const isSheet = sheetPresentation && trigger === PopoverTrigger.Click;\n\n // A sheet isn't anchored to anything, so there's nothing to track, flip or clamp.\n usePopoverPosition({\n isOpen: !!open && !isSheet,\n placement,\n align,\n offset,\n triggerRef,\n floatingRef,\n panelRef,\n arrowRef: withArrow ? arrowRef : { current: null },\n matchTriggerWidth: width === 'trigger',\n });\n\n // Registered for as long as it's open, not just as a sheet: this is what puts the popover on\n // the overlay stack, so an enclosing Modal knows something sits above it and neither drags\n // focus out of this (portaled, so outside the modal's panel) panel nor closes itself on the\n // Escape meant for this. Focus isn't trapped: a Popover is deliberately non-modal, and it\n // already moves focus into the panel on open by itself.\n useOverlay({\n isActive: !!open,\n containerRef: panelRef,\n onEscape: closeOnEscape\n ? () => {\n // Hand focus back to the trigger when it currently sits inside the panel.\n if (floatingRef.current?.contains(document.activeElement)) {\n (tabbablesIn(triggerRef.current)[0] ?? triggerRef.current)?.focus();\n }\n setOpen(false, true);\n }\n : undefined,\n trapFocus: false,\n lockScroll: isSheet,\n });\n\n const { restoreFocusToTrigger, focusAfterTrigger } = useFocusBoundary({\n triggerRef,\n contentRef: floatingRef,\n });\n\n // The panel is portaled to the end of the DOM, so Tab never flows into it naturally. When we\n // manage focus (click trigger + focusable content), move focus in on open and, on Tab out of\n // either end, close and continue focus as if the panel sat right after the trigger.\n useEffect(() => {\n if (!open) {\n openedByKeyboardRef.current = false;\n return;\n }\n const takeFocus = focusOnOpen === true || (focusOnOpen === 'keyboard' && openedByKeyboardRef.current);\n if (!takeFocus || trigger !== PopoverTrigger.Click) return;\n const focusables = tabbablesIn(panelRef.current);\n if (focusables.length === 0) return;\n // Only claim the dialog role when we're actually managing focus for real content.\n floatingRef.current?.setAttribute('role', 'dialog');\n focusables[0].focus();\n }, [open, trigger, focusOnOpen]);\n\n function handleFloatingKeyDown(event: React.KeyboardEvent) {\n if (event.key !== 'Tab' || trigger !== PopoverTrigger.Click) return;\n const focusables = tabbablesIn(panelRef.current);\n if (focusables.length === 0) return;\n const active = document.activeElement;\n if (!event.shiftKey && active === focusables[focusables.length - 1]) {\n event.preventDefault();\n focusAfterTrigger();\n setOpen(false, true);\n } else if (event.shiftKey && active === focusables[0]) {\n event.preventDefault();\n restoreFocusToTrigger();\n setOpen(false, true);\n }\n }\n\n // Outside click. Registered only while open and only for the relevant trigger mode; Escape is\n // handled by `useOverlay` above, which scopes it to the innermost open layer.\n useEffect(() => {\n if (!open) return;\n\n function onMouseUp(event: MouseEvent) {\n // Read and clear: the flag describes this one event, and the panel's React\n // handler has already run by the time this does.\n const inPanelSubtree = panelInteractedRef.current;\n panelInteractedRef.current = false;\n const target = event.target as Node;\n const inPanel = floatingRef.current?.contains(target);\n const inTrigger = triggerRef.current?.contains(target);\n if (!inPanelSubtree && !inPanel && !inTrigger) setOpen(false);\n }\n // Keyboard counterpart to the outside click: once focus leaves both the trigger\n // and the (portaled) panel — e.g. tabbing on to the next element — close.\n function onFocusIn(event: FocusEvent) {\n const inPanelSubtree = panelInteractedRef.current;\n panelInteractedRef.current = false;\n const target = event.target as Node;\n const inPanel = floatingRef.current?.contains(target);\n const inTrigger = triggerRef.current?.contains(target);\n if (!inPanelSubtree && !inPanel && !inTrigger) setOpen(false, true);\n }\n\n if (closeOnOutsideClick && trigger !== PopoverTrigger.Hover) {\n window.addEventListener('mouseup', onMouseUp);\n document.addEventListener('focusin', onFocusIn);\n }\n return () => {\n window.removeEventListener('mouseup', onMouseUp);\n document.removeEventListener('focusin', onFocusIn);\n };\n }, [open, closeOnOutsideClick, trigger, setOpen]);\n\n useEffect(() => () => clearTimeout(hoverTimer.current), []);\n\n function scheduleHover(next: boolean) {\n clearTimeout(hoverTimer.current);\n hoverTimer.current = setTimeout(() => setOpen(next), next ? openDelay : closeDelay);\n }\n\n // Opening from the keyboard without the trigger having to be a button that\n // happens to toggle on Enter: the arrow keys are what the menu-button pattern\n // asks for, and what a consumer reaches for first.\n function handleTriggerKeyDown(event: React.KeyboardEvent) {\n if (open || (event.key !== 'ArrowDown' && event.key !== 'ArrowUp')) return;\n event.preventDefault();\n setOpen(true, true);\n }\n\n const triggerHandlers =\n trigger === PopoverTrigger.Click\n ? {\n onClick: (event: React.MouseEvent) => {\n // A field that opens a picker keeps it open when clicked again, so\n // that placing the caret in the field does not close the panel.\n if (open && !closeOnTriggerClick) return;\n setOpen(!open, event.detail === 0);\n },\n onKeyDown: handleTriggerKeyDown,\n }\n : trigger === PopoverTrigger.Hover\n ? { onMouseEnter: () => scheduleHover(true), onMouseLeave: () => scheduleHover(false) }\n : { onFocus: () => setOpen(true), onBlur: () => setOpen(false) };\n\n const floatingHoverHandlers =\n trigger === PopoverTrigger.Hover\n ? { onMouseEnter: () => scheduleHover(true), onMouseLeave: () => scheduleHover(false) }\n : {};\n\n return (\n <div\n {...rest}\n className={cx(styles.popover, className, classNames?.root)}\n style={{ ...style, ...slotStyles?.root }}\n >\n <div\n ref={triggerRef}\n className={cx(styles.popoverTrigger, classNames?.trigger)}\n style={slotStyles?.trigger}\n {...triggerHandlers}\n >\n {children}\n </div>\n {open && (\n <BodyEnd>\n {isSheet && (\n <SheetBackdrop\n onClose={() => {\n if (closeOnOutsideClick) setOpen(false);\n }}\n className={classNames?.backdrop}\n style={slotStyles?.backdrop}\n />\n )}\n <div\n ref={floatingRef}\n className={cx(isSheet ? sheetStyles.sheet : styles.popoverFloating, classNames?.floating)}\n style={{ ...(isSheet ? sheetStyle : undefined), ...slotStyles?.floating }}\n data-placement={placement}\n onKeyDown={handleFloatingKeyDown}\n onMouseUpCapture={markPanelInteraction}\n onFocusCapture={markPanelInteraction}\n {...floatingHoverHandlers}\n >\n <div\n ref={panelRef}\n className={cx(\n styles.popoverPanel,\n constrainWidth && styles.popoverPanelConstrained,\n isSheet && sheetStyles.sheetPanel,\n classNames?.panel,\n )}\n style={slotStyles?.panel}\n >\n {content}\n </div>\n {withArrow && !isSheet && (\n <div ref={arrowRef} className={cx(styles.popoverArrow, classNames?.arrow)} style={slotStyles?.arrow} />\n )}\n </div>\n </BodyEnd>\n )}\n </div>\n );\n}\n\nexport default Popover;\n"],"mappings":"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"}
1
+ {"version":3,"file":"popover.mjs","names":[],"sources":["../../../../package/common/popover/popover.tsx"],"sourcesContent":["import { useRef, useEffect, useState, useCallback, type RefObject } from 'react';\nimport cx from 'classnames';\nimport BodyEnd from 'components/common/bodyEnd';\n\nimport styles from './popover.module.pcss';\nimport sheetStyles from '../styles/sheet.module.pcss';\nimport SheetBackdrop from '../sheetBackdrop';\nimport { usePopoverPosition } from './usePopoverPosition';\nimport { useFocusBoundary, tabbablesIn } from '../hooks/useFocusBoundary';\nimport { useSheet } from '../hooks/useSheet';\nimport { useOverlay } from '../hooks/useOverlay';\nimport { useComponentDefaults, usePresentationDefault } from '../configProvider';\nimport { Presentation } from '../configProvider/presentation.types';\nimport { PopoverPlacement, PopoverAlign, PopoverTrigger, type PopoverProps } from './popover.types';\n\n/**\n * How long after its click an anchored popover still counts as opened by it: long enough for the\n * render the click causes, short enough that an old keypress does not decide a later open.\n */\nconst ACTIVATION_WINDOW_MS = 1000;\n\nfunction toReference(\n anchor: HTMLElement | null | undefined,\n triggerRef: RefObject<HTMLElement | null>,\n): RefObject<HTMLElement | null> {\n return anchor ? { current: anchor } : triggerRef;\n}\n\nfunction Popover(props: PopoverProps) {\n const {\n children = null,\n anchor,\n content = null,\n placement = PopoverPlacement.Bottom,\n align = PopoverAlign.Center,\n trigger = PopoverTrigger.Click,\n isOpen,\n defaultOpen = false,\n onOpenChange = () => {},\n offset = 10,\n withArrow = true,\n closeOnOutsideClick = true,\n closeOnEscape = true,\n openDelay = 100,\n closeDelay = 120,\n disabled = false,\n focusOnOpen = 'keyboard',\n closeOnTriggerClick = true,\n width = 'auto',\n constrainWidth = true,\n presentation: presentationProp,\n className,\n style,\n classNames,\n styles: slotStyles,\n ...rest\n } = useComponentDefaults('Popover', props);\n const triggerRef = useRef<HTMLDivElement>(null);\n const floatingRef = useRef<HTMLDivElement>(null);\n const panelRef = useRef<HTMLDivElement>(null);\n const arrowRef = useRef<HTMLDivElement>(null);\n const hoverTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n // Set from a capture handler on the panel, which React fires for its whole\n // React subtree — including children that portal elsewhere in the DOM, such as\n // a Dropdown's list inside a DatePicker's calendar. A DOM `contains` check\n // cannot see those, and without this the panel closes when one is used.\n const panelInteractedRef = useRef(false);\n // Whether the open that is running came from the keyboard, which is what the\n // default `focusOnOpen` follows and what `onOpenChange` reports. A click raised\n // by Enter or Space reports `detail === 0`, where a pointer press counts the\n // clicks; an open with no trigger event at all, such as a panel that appears\n // while the user types, leaves this false.\n const openedByKeyboardRef = useRef(false);\n function markPanelInteraction() {\n panelInteractedRef.current = true;\n }\n\n // What the panel is placed against, and where the focus goes back to. A new object exactly when\n // `anchor` changes, which is what restarts the position tracking for the new element: state, not\n // a memo, because a memo React chose to drop would restart it for no reason.\n const [reference, setReference] = useState(() => ({ anchor, ref: toReference(anchor, triggerRef) }));\n if (reference.anchor !== anchor) setReference({ anchor, ref: toReference(anchor, triggerRef) });\n const referenceRef = reference.ref;\n\n // An anchored popover is opened by its consumer through `isOpen`, so none of the key handling\n // below sees how. The click that caused it does: Enter and Space raise one with `detail === 0`,\n // the same test the trigger uses. Not `:focus-visible`, which a text field matches after a\n // mouse click too, and which stays on a keyboard-focused button that is then clicked.\n const activationRef = useRef<{ target: Node; keyboard: boolean; time: number } | null>(null);\n const anchored = anchor !== undefined;\n useEffect(() => {\n if (!anchored) return;\n function onClick(event: MouseEvent) {\n activationRef.current = { target: event.target as Node, keyboard: event.detail === 0, time: performance.now() };\n }\n // Capturing, so it is recorded before the consumer's own handler opens the panel.\n document.addEventListener('click', onClick, true);\n return () => document.removeEventListener('click', onClick, true);\n }, [anchored]);\n\n const isControlled = isOpen !== undefined;\n const [internalOpen, setInternalOpen] = useState(defaultOpen);\n const open = isControlled ? isOpen : internalOpen;\n\n // Keep the outside-click/Escape listeners pointed at the latest setter without re-subscribing.\n const onOpenChangeRef = useRef(onOpenChange);\n useEffect(() => {\n onOpenChangeRef.current = onOpenChange;\n });\n\n const setOpen = useCallback(\n (next: boolean, fromKeyboard = false) => {\n if (disabled && next) return;\n if (next) openedByKeyboardRef.current = fromKeyboard;\n if (!isControlled) setInternalOpen(next);\n onOpenChangeRef.current(next, { fromKeyboard });\n },\n [disabled, isControlled],\n );\n\n // Explicit prop → this component's global default → the app-wide one → anchored.\n const appPresentation = usePresentationDefault();\n const presentation = presentationProp ?? appPresentation ?? Presentation.Default;\n const { isSheet: sheetPresentation, sheetStyle } = useSheet(presentation);\n // Hover and focus are pointer affordances that open something small next to what caused them;\n // a sheet is a deliberate, screen-owning open. Only a click asks for one.\n const isSheet = sheetPresentation && trigger === PopoverTrigger.Click;\n\n // A sheet isn't anchored to anything, so there's nothing to track, flip or clamp.\n usePopoverPosition({\n isOpen: !!open && !isSheet,\n placement,\n align,\n offset,\n triggerRef: referenceRef,\n floatingRef,\n panelRef,\n arrowRef: withArrow ? arrowRef : { current: null },\n matchTriggerWidth: width === 'trigger',\n });\n\n // Registered for as long as it's open, not just as a sheet: this is what puts the popover on\n // the overlay stack, so an enclosing Modal knows something sits above it and neither drags\n // focus out of this (portaled, so outside the modal's panel) panel nor closes itself on the\n // Escape meant for this. Focus isn't trapped: a Popover is deliberately non-modal, and it\n // already moves focus into the panel on open by itself.\n const { restoreFocusToTrigger, focusAfterTrigger } = useFocusBoundary({\n triggerRef: referenceRef,\n contentRef: floatingRef,\n });\n\n useOverlay({\n isActive: !!open,\n containerRef: panelRef,\n onEscape: closeOnEscape\n ? () => {\n // Hand focus back to the trigger when it currently sits inside the panel.\n if (floatingRef.current?.contains(document.activeElement)) restoreFocusToTrigger();\n setOpen(false, true);\n }\n : undefined,\n trapFocus: false,\n lockScroll: isSheet,\n });\n\n // The panel is portaled to the end of the DOM, so Tab never flows into it naturally. When we\n // manage focus (click trigger + focusable content), move focus in on open and, on Tab out of\n // either end, close and continue focus as if the panel sat right after the trigger.\n useEffect(() => {\n if (!open) {\n openedByKeyboardRef.current = false;\n return;\n }\n const activation = activationRef.current;\n activationRef.current = null;\n const anchorKeyed =\n !!anchor &&\n !!activation?.keyboard &&\n anchor.contains(activation.target) &&\n performance.now() - activation.time < ACTIVATION_WINDOW_MS;\n const takeFocus =\n focusOnOpen === true || (focusOnOpen === 'keyboard' && (openedByKeyboardRef.current || anchorKeyed));\n if (!takeFocus || trigger !== PopoverTrigger.Click) return;\n const focusables = tabbablesIn(panelRef.current);\n if (focusables.length === 0) return;\n // Only claim the dialog role when we're actually managing focus for real content.\n floatingRef.current?.setAttribute('role', 'dialog');\n focusables[0].focus();\n }, [open, trigger, focusOnOpen, anchor]);\n\n function handleFloatingKeyDown(event: React.KeyboardEvent) {\n if (event.key !== 'Tab' || trigger !== PopoverTrigger.Click) return;\n const focusables = tabbablesIn(panelRef.current);\n if (focusables.length === 0) return;\n const active = document.activeElement;\n if (!event.shiftKey && active === focusables[focusables.length - 1]) {\n event.preventDefault();\n focusAfterTrigger();\n setOpen(false, true);\n } else if (event.shiftKey && active === focusables[0]) {\n event.preventDefault();\n restoreFocusToTrigger();\n setOpen(false, true);\n }\n }\n\n // Outside click. Registered only while open and only for the relevant trigger mode; Escape is\n // handled by `useOverlay` above, which scopes it to the innermost open layer.\n useEffect(() => {\n if (!open) return;\n // Focus that landed in the panel before these listeners existed (ours on open, or content\n // that focuses itself on mount) set the flag with nothing to clear it, and the first\n // click outside was swallowed.\n panelInteractedRef.current = false;\n\n function onMouseUp(event: MouseEvent) {\n // Read and clear: the flag describes this one event, and the panel's React\n // handler has already run by the time this does.\n const inPanelSubtree = panelInteractedRef.current;\n panelInteractedRef.current = false;\n const target = event.target as Node;\n const inPanel = floatingRef.current?.contains(target);\n // Both: with an anchor, `children` still toggle the panel, and a press on them is theirs.\n const inTrigger = triggerRef.current?.contains(target) || referenceRef.current?.contains(target);\n if (!inPanelSubtree && !inPanel && !inTrigger) setOpen(false);\n }\n // Keyboard counterpart to the outside click: once focus leaves both the trigger\n // and the (portaled) panel — e.g. tabbing on to the next element — close.\n function onFocusIn(event: FocusEvent) {\n const inPanelSubtree = panelInteractedRef.current;\n panelInteractedRef.current = false;\n const target = event.target as Node;\n const inPanel = floatingRef.current?.contains(target);\n const inTrigger = triggerRef.current?.contains(target) || referenceRef.current?.contains(target);\n if (!inPanelSubtree && !inPanel && !inTrigger) setOpen(false, true);\n }\n\n if (closeOnOutsideClick && trigger !== PopoverTrigger.Hover) {\n window.addEventListener('mouseup', onMouseUp);\n document.addEventListener('focusin', onFocusIn);\n }\n return () => {\n window.removeEventListener('mouseup', onMouseUp);\n document.removeEventListener('focusin', onFocusIn);\n };\n }, [open, closeOnOutsideClick, trigger, setOpen, referenceRef]);\n\n useEffect(() => () => clearTimeout(hoverTimer.current), []);\n\n function scheduleHover(next: boolean) {\n clearTimeout(hoverTimer.current);\n hoverTimer.current = setTimeout(() => setOpen(next), next ? openDelay : closeDelay);\n }\n\n // Opening from the keyboard without the trigger having to be a button that\n // happens to toggle on Enter: the arrow keys are what the menu-button pattern\n // asks for, and what a consumer reaches for first.\n function handleTriggerKeyDown(event: React.KeyboardEvent) {\n if (open || (event.key !== 'ArrowDown' && event.key !== 'ArrowUp')) return;\n event.preventDefault();\n setOpen(true, true);\n }\n\n const triggerHandlers =\n trigger === PopoverTrigger.Click\n ? {\n onClick: (event: React.MouseEvent) => {\n // A field that opens a picker keeps it open when clicked again, so\n // that placing the caret in the field does not close the panel.\n if (open && !closeOnTriggerClick) return;\n setOpen(!open, event.detail === 0);\n },\n onKeyDown: handleTriggerKeyDown,\n }\n : trigger === PopoverTrigger.Hover\n ? { onMouseEnter: () => scheduleHover(true), onMouseLeave: () => scheduleHover(false) }\n : { onFocus: () => setOpen(true), onBlur: () => setOpen(false) };\n\n const floatingHoverHandlers =\n trigger === PopoverTrigger.Hover\n ? { onMouseEnter: () => scheduleHover(true), onMouseLeave: () => scheduleHover(false) }\n : {};\n\n return (\n <div\n {...rest}\n className={cx(styles.popover, className, classNames?.root)}\n style={{ ...style, ...slotStyles?.root }}\n >\n <div\n ref={triggerRef}\n className={cx(styles.popoverTrigger, classNames?.trigger)}\n style={slotStyles?.trigger}\n {...triggerHandlers}\n >\n {children}\n </div>\n {open && (\n <BodyEnd>\n {isSheet && (\n <SheetBackdrop\n onClose={() => {\n if (closeOnOutsideClick) setOpen(false);\n }}\n className={classNames?.backdrop}\n style={slotStyles?.backdrop}\n />\n )}\n <div\n ref={floatingRef}\n className={cx(isSheet ? sheetStyles.sheet : styles.popoverFloating, classNames?.floating)}\n style={{ ...(isSheet ? sheetStyle : undefined), ...slotStyles?.floating }}\n data-placement={placement}\n onKeyDown={handleFloatingKeyDown}\n onMouseUpCapture={markPanelInteraction}\n onFocusCapture={markPanelInteraction}\n {...floatingHoverHandlers}\n >\n <div\n ref={panelRef}\n className={cx(\n styles.popoverPanel,\n constrainWidth && styles.popoverPanelConstrained,\n isSheet && sheetStyles.sheetPanel,\n classNames?.panel,\n )}\n style={slotStyles?.panel}\n >\n {content}\n </div>\n {withArrow && !isSheet && (\n <div ref={arrowRef} className={cx(styles.popoverArrow, classNames?.arrow)} style={slotStyles?.arrow} />\n )}\n </div>\n </BodyEnd>\n )}\n </div>\n );\n}\n\nexport default Popover;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"popover.types.mjs","names":[],"sources":["../../../../package/common/popover/popover.types.ts"],"sourcesContent":["import type { CSSProperties, ReactNode } from 'react';\nimport type { SlotClassNames, SlotStyles, HtmlProps } from '../types/slots.types';\nimport type { Presentation } from '../configProvider/presentation.types';\n\n/** Preferred side of the trigger the panel opens on. Flips to the opposite side when there's no room. */\nexport enum PopoverPlacement {\n Top = 'top',\n Bottom = 'bottom',\n Left = 'left',\n Right = 'right',\n}\n\n/** Alignment along the trigger's cross axis (horizontal for top/bottom, vertical for left/right). */\nexport enum PopoverAlign {\n Start = 'start',\n Center = 'center',\n End = 'end',\n}\n\n/** What opens the popover. */\nexport enum PopoverTrigger {\n Click = 'click',\n Hover = 'hover',\n Focus = 'focus',\n}\n\nexport type PopoverSlots = 'root' | 'trigger' | 'floating' | 'panel' | 'arrow' | 'backdrop';\n\nexport interface PopoverProps extends Omit<HtmlProps, 'content'> {\n /** The trigger element. */\n children?: ReactNode;\n /** The popover body. */\n content?: ReactNode;\n /** Preferred side (default `Bottom`). */\n placement?: PopoverPlacement;\n /** Cross-axis alignment (default `Center`). */\n align?: PopoverAlign;\n /** What opens the popover (default `Click`). */\n trigger?: PopoverTrigger;\n /** Controlled open state. Omit for uncontrolled. */\n isOpen?: boolean;\n /** Initial open state when uncontrolled (default `false`). */\n defaultOpen?: boolean;\n /**\n * Fires whenever the popover wants to open or close. `fromKeyboard` says\n * whether a key caused it, which is what a menu needs in order to put the\n * focus on an entry only when the pointer was not what opened it.\n */\n onOpenChange?: (isOpen: boolean, meta: { fromKeyboard: boolean }) => void;\n /** Gap in px between trigger and panel (default `10`). */\n offset?: number;\n /** Render the arrow pointing at the trigger (default `true`). */\n withArrow?: boolean;\n /** Close when clicking outside the panel (default `true`). */\n closeOnOutsideClick?: boolean;\n /** Close when pressing Escape (default `true`). */\n closeOnEscape?: boolean;\n /** Delay in ms before a hover-triggered popover opens (default `100`). */\n openDelay?: number;\n /** Delay in ms before a hover-triggered popover closes (default `120`). */\n closeDelay?: number;\n /** Prevent the popover from opening. */\n disabled?: boolean;\n /**\n * Move focus into the panel when it opens, and claim `role=\"dialog\"` with it.\n * `'keyboard'` (the default) does it only when the open came from the\n * keyboard: the panel is portaled to the end of the document, so Tab never\n * reaches it on its own, while a click leaves the focus where the pointer\n * already put it. `true` always, `false` never.\n */\n focusOnOpen?: boolean | 'keyboard';\n /**\n * Whether clicking the trigger while the panel is open closes it (default\n * `true`). A menu button should toggle; a text field that opens a picker\n * should not, or clicking into it to place the caret would close the panel.\n */\n closeOnTriggerClick?: boolean;\n /**\n * `'trigger'` gives the panel at least the trigger's width, for a list that\n * should line up with the field it belongs to. `'auto'` (the default) lets it\n * size to its own content.\n */\n width?: 'auto' | 'trigger';\n /**\n * Cap the panel at a readable measure (default `true`). Off for a panel with\n * its own width, a calendar or a media grid.\n */\n constrainWidth?: boolean;\n /** Where the panel opens: anchored to the trigger (`Default`), pinned to the bottom of the\n * screen, or the latter only on a small touch screen. Only a `Click` trigger presents as a\n * sheet. */\n presentation?: Presentation;\n\n className?: string;\n style?: CSSProperties;\n classNames?: SlotClassNames<PopoverSlots>;\n styles?: SlotStyles<PopoverSlots>;\n}\n"],"mappings":"aAKA,IAAY,EAAL,SAAA,EAAA,OACL,GAAA,IAAA,MACA,EAAA,OAAA,SACA,EAAA,KAAA,OACA,EAAA,MAAA,SACF,EAAA,CAAA,CAAA,EAGY,EAAL,SAAA,EAAA,OACL,GAAA,MAAA,QACA,EAAA,OAAA,SACA,EAAA,IAAA,OACF,EAAA,CAAA,CAAA,EAGY,EAAL,SAAA,EAAA,OACL,GAAA,MAAA,QACA,EAAA,MAAA,QACA,EAAA,MAAA,SACF,EAAA,CAAA,CAAA"}
1
+ {"version":3,"file":"popover.types.mjs","names":[],"sources":["../../../../package/common/popover/popover.types.ts"],"sourcesContent":["import type { CSSProperties, ReactNode } from 'react';\nimport type { SlotClassNames, SlotStyles, HtmlProps } from '../types/slots.types';\nimport type { Presentation } from '../configProvider/presentation.types';\n\n/** Preferred side of the trigger the panel opens on. Flips to the opposite side when there's no room. */\nexport enum PopoverPlacement {\n Top = 'top',\n Bottom = 'bottom',\n Left = 'left',\n Right = 'right',\n}\n\n/** Alignment along the trigger's cross axis (horizontal for top/bottom, vertical for left/right). */\nexport enum PopoverAlign {\n Start = 'start',\n Center = 'center',\n End = 'end',\n}\n\n/** What opens the popover. */\nexport enum PopoverTrigger {\n Click = 'click',\n Hover = 'hover',\n Focus = 'focus',\n}\n\nexport type PopoverSlots = 'root' | 'trigger' | 'floating' | 'panel' | 'arrow' | 'backdrop';\n\nexport interface PopoverProps extends Omit<HtmlProps, 'content'> {\n /** The trigger element. */\n children?: ReactNode;\n /**\n * Place the panel against this element instead of `children`, for one Popover does not wrap.\n * Open it with `isOpen`; `null` falls back to `children`.\n */\n anchor?: HTMLElement | null;\n /** The popover body. */\n content?: ReactNode;\n /** Preferred side (default `Bottom`). */\n placement?: PopoverPlacement;\n /** Cross-axis alignment (default `Center`). */\n align?: PopoverAlign;\n /** What opens the popover (default `Click`). */\n trigger?: PopoverTrigger;\n /** Controlled open state. Omit for uncontrolled. */\n isOpen?: boolean;\n /** Initial open state when uncontrolled (default `false`). */\n defaultOpen?: boolean;\n /**\n * Fires whenever the popover wants to open or close. `fromKeyboard` says\n * whether a key caused it, which is what a menu needs in order to put the\n * focus on an entry only when the pointer was not what opened it.\n */\n onOpenChange?: (isOpen: boolean, meta: { fromKeyboard: boolean }) => void;\n /** Gap in px between trigger and panel (default `10`). */\n offset?: number;\n /** Render the arrow pointing at the trigger (default `true`). */\n withArrow?: boolean;\n /** Close when clicking outside the panel (default `true`). */\n closeOnOutsideClick?: boolean;\n /** Close when pressing Escape (default `true`). */\n closeOnEscape?: boolean;\n /** Delay in ms before a hover-triggered popover opens (default `100`). */\n openDelay?: number;\n /** Delay in ms before a hover-triggered popover closes (default `120`). */\n closeDelay?: number;\n /** Prevent the popover from opening. */\n disabled?: boolean;\n /**\n * Move focus into the panel when it opens, and claim `role=\"dialog\"` with it.\n * `'keyboard'` (the default) does it only when the open came from the\n * keyboard: the panel is portaled to the end of the document, so Tab never\n * reaches it on its own, while a click leaves the focus where the pointer\n * already put it. `true` always, `false` never.\n */\n focusOnOpen?: boolean | 'keyboard';\n /**\n * Whether clicking the trigger while the panel is open closes it (default\n * `true`). A menu button should toggle; a text field that opens a picker\n * should not, or clicking into it to place the caret would close the panel.\n */\n closeOnTriggerClick?: boolean;\n /**\n * `'trigger'` gives the panel at least the trigger's width, for a list that\n * should line up with the field it belongs to. `'auto'` (the default) lets it\n * size to its own content.\n */\n width?: 'auto' | 'trigger';\n /**\n * Cap the panel at a readable measure (default `true`). Off for a panel with\n * its own width, a calendar or a media grid.\n */\n constrainWidth?: boolean;\n /** Where the panel opens: anchored to the trigger (`Default`), pinned to the bottom of the\n * screen, or the latter only on a small touch screen. Only a `Click` trigger presents as a\n * sheet. */\n presentation?: Presentation;\n\n className?: string;\n style?: CSSProperties;\n classNames?: SlotClassNames<PopoverSlots>;\n styles?: SlotStyles<PopoverSlots>;\n}\n"],"mappings":"aAKA,IAAY,EAAL,SAAA,EAAA,OACL,GAAA,IAAA,MACA,EAAA,OAAA,SACA,EAAA,KAAA,OACA,EAAA,MAAA,SACF,EAAA,CAAA,CAAA,EAGY,EAAL,SAAA,EAAA,OACL,GAAA,MAAA,QACA,EAAA,OAAA,SACA,EAAA,IAAA,OACF,EAAA,CAAA,CAAA,EAGY,EAAL,SAAA,EAAA,OACL,GAAA,MAAA,QACA,EAAA,MAAA,QACA,EAAA,MAAA,SACF,EAAA,CAAA,CAAA"}
@@ -1,2 +1,2 @@
1
- "use client";import{ThemeVariablesContext as e}from"./themeVariablesContext.mjs";import{defaultTheme as t}from"./defaultTheme.mjs";import n from"./theme.module.mjs";import r,{Component as i}from"react";import a from"classnames";import{jsx as o}from"react/jsx-runtime";var s=class extends i{constructor(...e){super(...e),this.state={resolvedVariables:{},colorFadeActive:!1,currentThemeColor:null},this.colorFadeTimeout=null,this.wrapperRef=r.createRef(),this.themeColorMeta=null,this.createdThemeColorMeta=!1,this.applyTheme=e=>{let n=this.props.themes??[t],r=n.find(e=>!e.baseThemeId)??n[0],i=r?r.variables:{},a=n.find(t=>t.id===e),o=a?this.resolveChain(a.id):[],s=Object.assign({},i,...o);this.setState({resolvedVariables:s,colorFadeActive:!0}),this.colorFadeTimeout&&clearTimeout(this.colorFadeTimeout),this.colorFadeTimeout=setTimeout(()=>{this.setState({colorFadeActive:!1}),this.colorFadeTimeout=null},500),setTimeout(()=>{this.wrapperRef.current&&this.setState({currentThemeColor:getComputedStyle(this.wrapperRef.current).getPropertyValue(`--primary-color`).trim()})},1)}}componentDidMount(){this.applyTheme(this.props.currentThemeId),this.syncThemeColorMeta(this.state.currentThemeColor??``)}componentDidUpdate(e,t){t.currentThemeColor!==this.state.currentThemeColor&&this.syncThemeColorMeta(this.state.currentThemeColor??``),(e.currentThemeId!==this.props.currentThemeId||e.themes!==this.props.themes)&&this.applyTheme(this.props.currentThemeId)}componentWillUnmount(){this.colorFadeTimeout&&clearTimeout(this.colorFadeTimeout),this.createdThemeColorMeta&&=(this.themeColorMeta?.remove(),this.themeColorMeta=null,!1)}syncThemeColorMeta(e){if(typeof document<`u`){if(!this.themeColorMeta){let e=document.head.querySelector(`meta[name="theme-color"]`);if(e)this.themeColorMeta=e;else{let e=document.createElement(`meta`);e.setAttribute(`name`,`theme-color`),document.head.appendChild(e),this.themeColorMeta=e,this.createdThemeColorMeta=!0}}this.themeColorMeta.setAttribute(`content`,e)}}resolveChain(e){let n=(this.props.themes??[t]).find(t=>t.id===e);return n?[...n.baseThemeId?this.resolveChain(n.baseThemeId):[],n.variables]:[]}render(){let{children:t}=this.props,{resolvedVariables:r,colorFadeActive:i}=this.state;return o(`div`,{ref:this.wrapperRef,id:`themeProvider`,className:a(n.themeRoot,i&&n.colorFade),style:r,children:o(e.Provider,{value:r,children:t})})}};export{s as default};
1
+ "use client";import{ThemeVariablesContext as e}from"./themeVariablesContext.mjs";import{defaultTheme as t}from"./defaultTheme.mjs";import n from"./theme.module.mjs";import r,{Component as i}from"react";import a from"classnames";import{jsx as o}from"react/jsx-runtime";function s(e,t){let n=e.find(e=>e.id===t);return n?[...n.baseThemeId?s(e,n.baseThemeId):[],n.variables]:[]}function c({themes:e=[t],currentThemeId:n}){let r=e.find(e=>!e.baseThemeId)??e[0],i=r?r.variables:{},a=n?s(e,n):[];return Object.assign({},i,...a)}var l=class extends i{constructor(...e){super(...e),this.state={resolvedVariables:c(this.props),colorFadeActive:!1,currentThemeColor:null,resolvedFrom:{themes:this.props.themes,currentThemeId:this.props.currentThemeId}},this.colorFadeTimeout=null,this.wrapperRef=r.createRef(),this.themeColorMeta=null,this.createdThemeColorMeta=!1}static getDerivedStateFromProps(e,t){let{themes:n,currentThemeId:r}=t.resolvedFrom;return e.themes===n&&e.currentThemeId===r?null:{resolvedVariables:c(e),colorFadeActive:!0,resolvedFrom:{themes:e.themes,currentThemeId:e.currentThemeId}}}componentDidMount(){this.readThemeColor()}componentDidUpdate(e,t){t.resolvedVariables!==this.state.resolvedVariables&&(this.readThemeColor(),this.colorFadeTimeout&&clearTimeout(this.colorFadeTimeout),this.colorFadeTimeout=setTimeout(()=>{this.setState({colorFadeActive:!1}),this.colorFadeTimeout=null},500)),t.currentThemeColor!==this.state.currentThemeColor&&this.syncThemeColorMeta(this.state.currentThemeColor??``)}componentWillUnmount(){this.colorFadeTimeout&&clearTimeout(this.colorFadeTimeout),this.createdThemeColorMeta&&=(this.themeColorMeta?.remove(),this.themeColorMeta=null,!1)}syncThemeColorMeta(e){if(typeof document<`u`){if(!this.themeColorMeta){let e=document.head.querySelector(`meta[name="theme-color"]`);if(e)this.themeColorMeta=e;else{let e=document.createElement(`meta`);e.setAttribute(`name`,`theme-color`),document.head.appendChild(e),this.themeColorMeta=e,this.createdThemeColorMeta=!0}}this.themeColorMeta.setAttribute(`content`,e)}}readThemeColor(){if(!this.wrapperRef.current)return;let e=getComputedStyle(this.wrapperRef.current).getPropertyValue(`--primary-color`).trim();e!==this.state.currentThemeColor&&this.setState({currentThemeColor:e})}render(){let{children:t}=this.props,{resolvedVariables:r,colorFadeActive:i}=this.state;return o(`div`,{ref:this.wrapperRef,id:`themeProvider`,className:a(n.themeRoot,i&&n.colorFade),style:r,children:o(e.Provider,{value:r,children:t})})}};export{l as default};
2
2
  //# sourceMappingURL=themeProvider.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"themeProvider.mjs","names":[],"sources":["../../../../package/common/themeProvider/themeProvider.tsx"],"sourcesContent":["import React, { Component } from 'react';\nimport cx from 'classnames';\n\nimport type { Theme } from './theme.types';\nimport { defaultTheme } from './defaultTheme';\nimport { ThemeVariablesContext } from './themeVariablesContext';\nimport styles from './theme.module.pcss';\n\ninterface Props {\n children: React.ReactNode;\n themes?: Theme[];\n currentThemeId?: string;\n}\n\ninterface State {\n resolvedVariables: Record<string, string>;\n colorFadeActive: boolean;\n currentThemeColor: string | null;\n}\n\nclass ThemeProvider extends Component<Props, State> {\n state: State = {\n resolvedVariables: {},\n colorFadeActive: false,\n currentThemeColor: null,\n };\n\n private colorFadeTimeout: ReturnType<typeof setTimeout> | null = null;\n private wrapperRef = React.createRef<HTMLDivElement>();\n private themeColorMeta: HTMLMetaElement | null = null;\n private createdThemeColorMeta = false;\n\n componentDidMount() {\n this.applyTheme(this.props.currentThemeId);\n this.syncThemeColorMeta(this.state.currentThemeColor ?? '');\n }\n\n componentDidUpdate(prevProps: Props, prevState: State) {\n if (prevState.currentThemeColor !== this.state.currentThemeColor) {\n this.syncThemeColorMeta(this.state.currentThemeColor ?? '');\n }\n if (prevProps.currentThemeId !== this.props.currentThemeId || prevProps.themes !== this.props.themes) {\n this.applyTheme(this.props.currentThemeId);\n }\n }\n\n componentWillUnmount() {\n if (this.colorFadeTimeout) clearTimeout(this.colorFadeTimeout);\n // Only remove the meta tag if we created it; leave a host-app tag intact.\n // Also clear the cached refs — StrictMode's dev-only mount/unmount/remount\n // cycle reuses this instance, and a stale ref would otherwise point at a\n // detached element on remount instead of re-inserting into <head>.\n if (this.createdThemeColorMeta) {\n this.themeColorMeta?.remove();\n this.themeColorMeta = null;\n this.createdThemeColorMeta = false;\n }\n }\n\n /**\n * Replaces the former react-helmet usage: keep a single\n * <meta name=\"theme-color\"> in sync with the active theme's primary colour.\n * Reuses a tag the host app already declares; otherwise creates its own\n * (and removes it again on unmount).\n */\n private syncThemeColorMeta(color: string) {\n if (typeof document === 'undefined') return;\n\n if (!this.themeColorMeta) {\n const existing = document.head.querySelector<HTMLMetaElement>('meta[name=\"theme-color\"]');\n if (existing) {\n this.themeColorMeta = existing;\n } else {\n const meta = document.createElement('meta');\n meta.setAttribute('name', 'theme-color');\n document.head.appendChild(meta);\n this.themeColorMeta = meta;\n this.createdThemeColorMeta = true;\n }\n }\n\n this.themeColorMeta.setAttribute('content', color);\n }\n\n /**\n * Resolves the full variable set for a given theme ID by walking up the\n * baseThemeId chain. Returns variables ordered from deepest ancestor to\n * the requested theme so callers can merge in order (later entries win).\n */\n private resolveChain(id: string): Record<string, string>[] {\n const themes = this.props.themes ?? [defaultTheme];\n const theme = themes.find((t) => t.id === id);\n if (!theme) return [];\n\n const parentVars = theme.baseThemeId ? this.resolveChain(theme.baseThemeId) : [];\n\n return [...parentVars, theme.variables as Record<string, string>];\n }\n\n private applyTheme = (currentThemeId: string | undefined) => {\n const themes = this.props.themes ?? [defaultTheme];\n\n // The root theme is the first theme without a baseThemeId.\n // It is always applied as the variable foundation.\n const rootTheme = themes.find((t) => !t.baseThemeId) ?? themes[0];\n const rootVars: Record<string, string> = rootTheme ? (rootTheme.variables as Record<string, string>) : {};\n\n // Resolve the selected theme's full inheritance chain.\n const selectedTheme = themes.find((t) => t.id === currentThemeId);\n const chainVars = selectedTheme ? this.resolveChain(selectedTheme.id) : [];\n\n // Merge: root foundation → ancestor chain → selected theme (each step overrides)\n const resolvedVariables = Object.assign({}, rootVars, ...chainVars);\n\n this.setState({ resolvedVariables, colorFadeActive: true });\n\n if (this.colorFadeTimeout) clearTimeout(this.colorFadeTimeout);\n this.colorFadeTimeout = setTimeout(() => {\n this.setState({ colorFadeActive: false });\n this.colorFadeTimeout = null;\n }, 500);\n\n // Read --primary-color after the new style is painted for the theme-color meta tag.\n setTimeout(() => {\n if (this.wrapperRef.current) {\n this.setState({\n currentThemeColor: getComputedStyle(this.wrapperRef.current).getPropertyValue('--primary-color').trim(),\n });\n }\n }, 1);\n };\n\n render() {\n const { children } = this.props;\n const { resolvedVariables, colorFadeActive } = this.state;\n\n return (\n <div\n ref={this.wrapperRef}\n id='themeProvider'\n className={cx(styles.themeRoot, colorFadeActive && styles.colorFade)}\n style={resolvedVariables as React.CSSProperties}\n >\n <ThemeVariablesContext.Provider value={resolvedVariables}>{children}</ThemeVariablesContext.Provider>\n </div>\n );\n }\n}\n\nexport default ThemeProvider;\n"],"mappings":"4QAoBA,IAAM,EAAN,cAA4B,CAAwB,+BACnC,KAAA,MAAA,CACb,kBAAmB,CAAC,EACpB,gBAAiB,GACjB,kBAAmB,IACrB,EAEiE,KAAA,iBAAA,KAC5C,KAAA,WAAA,EAAM,UAA0B,EACJ,KAAA,eAAA,KACjB,KAAA,sBAAA,GAqEV,KAAA,WAAA,GAAuC,CAC3D,IAAM,EAAS,KAAK,MAAM,QAAU,CAAC,CAAY,EAI3C,EAAY,EAAO,KAAM,GAAM,CAAC,EAAE,WAAW,GAAK,EAAO,GACzD,EAAmC,EAAa,EAAU,UAAuC,CAAC,EAGlG,EAAgB,EAAO,KAAM,GAAM,EAAE,KAAO,CAAc,EAC1D,EAAY,EAAgB,KAAK,aAAa,EAAc,EAAE,EAAI,CAAC,EAGnE,EAAoB,OAAO,OAAO,CAAC,EAAG,EAAU,GAAG,CAAS,EAElE,KAAK,SAAS,CAAE,oBAAmB,gBAAiB,EAAK,CAAC,EAEtD,KAAK,kBAAkB,aAAa,KAAK,gBAAgB,EAC7D,KAAK,iBAAmB,eAAiB,CACvC,KAAK,SAAS,CAAE,gBAAiB,EAAM,CAAC,EACxC,KAAK,iBAAmB,IAC1B,EAAG,GAAG,EAGN,eAAiB,CACX,KAAK,WAAW,SAClB,KAAK,SAAS,CACZ,kBAAmB,iBAAiB,KAAK,WAAW,OAAO,CAAC,CAAC,iBAAiB,iBAAiB,CAAC,CAAC,KAAK,CACxG,CAAC,CAEL,EAAG,CAAC,CACN,EAlGA,mBAAoB,CAClB,KAAK,WAAW,KAAK,MAAM,cAAc,EACzC,KAAK,mBAAmB,KAAK,MAAM,mBAAqB,EAAE,CAC5D,CAEA,mBAAmB,EAAkB,EAAkB,CACjD,EAAU,oBAAsB,KAAK,MAAM,mBAC7C,KAAK,mBAAmB,KAAK,MAAM,mBAAqB,EAAE,GAExD,EAAU,iBAAmB,KAAK,MAAM,gBAAkB,EAAU,SAAW,KAAK,MAAM,SAC5F,KAAK,WAAW,KAAK,MAAM,cAAc,CAE7C,CAEA,sBAAuB,CACjB,KAAK,kBAAkB,aAAa,KAAK,gBAAgB,EAK7D,AAGE,KAAK,yBAFL,KAAK,gBAAgB,OAAO,EAC5B,KAAK,eAAiB,KACO,GAEjC,CAQA,mBAA2B,EAAe,CACpC,UAAO,SAAa,IAExB,IAAI,CAAC,KAAK,eAAgB,CACxB,IAAM,EAAW,SAAS,KAAK,cAA+B,0BAA0B,EACxF,GAAI,EACF,KAAK,eAAiB,MACjB,CACL,IAAM,EAAO,SAAS,cAAc,MAAM,EAC1C,EAAK,aAAa,OAAQ,aAAa,EACvC,SAAS,KAAK,YAAY,CAAI,EAC9B,KAAK,eAAiB,EACtB,KAAK,sBAAwB,EAC/B,CACF,CAEA,KAAK,eAAe,aAAa,UAAW,CAAK,CAFjD,CAGF,CAOA,aAAqB,EAAsC,CAEzD,IAAM,GADS,KAAK,MAAM,QAAU,CAAC,CAAY,EAAA,CAC5B,KAAM,GAAM,EAAE,KAAO,CAAE,EAK5C,OAJK,EAIE,CAAC,GAFW,EAAM,YAAc,KAAK,aAAa,EAAM,WAAW,EAAI,CAAC,EAExD,EAAM,SAAmC,EAJ7C,CAAC,CAKtB,CAmCA,QAAS,CACP,GAAM,CAAE,YAAa,KAAK,MACpB,CAAE,oBAAmB,mBAAoB,KAAK,MAEpD,OACE,EAAC,MAAD,CACE,IAAK,KAAK,WACV,GAAG,gBACH,UAAW,EAAG,EAAO,UAAW,GAAmB,EAAO,SAAS,EACnE,MAAO,EAEP,SAAA,EAAC,EAAsB,SAAvB,CAAgC,MAAO,EAAoB,UAAyC,CAAA,CACjG,CAAA,CAET,CACF"}
1
+ {"version":3,"file":"themeProvider.mjs","names":[],"sources":["../../../../package/common/themeProvider/themeProvider.tsx"],"sourcesContent":["import React, { Component } from 'react';\nimport cx from 'classnames';\n\nimport type { Theme } from './theme.types';\nimport { defaultTheme } from './defaultTheme';\nimport { ThemeVariablesContext } from './themeVariablesContext';\nimport styles from './theme.module.pcss';\n\ninterface Props {\n children: React.ReactNode;\n themes?: Theme[];\n currentThemeId?: string;\n}\n\ninterface State {\n resolvedVariables: Record<string, string>;\n colorFadeActive: boolean;\n currentThemeColor: string | null;\n /** What `resolvedVariables` was resolved from, to tell a theme change from any other render. */\n resolvedFrom: { themes: Theme[] | undefined; currentThemeId: string | undefined };\n}\n\n/**\n * Resolves a theme ID's variables by walking up its baseThemeId chain, ordered from deepest\n * ancestor to the requested theme so they merge in order (later entries win).\n */\nfunction resolveChain(themes: Theme[], id: string): Record<string, string>[] {\n const theme = themes.find((t) => t.id === id);\n if (!theme) return [];\n const parentVars = theme.baseThemeId ? resolveChain(themes, theme.baseThemeId) : [];\n return [...parentVars, theme.variables as Record<string, string>];\n}\n\nfunction resolveVariables({ themes = [defaultTheme], currentThemeId }: Props): Record<string, string> {\n // The root theme, the first without a baseThemeId, is always the foundation.\n const rootTheme = themes.find((t) => !t.baseThemeId) ?? themes[0];\n const rootVars = rootTheme ? (rootTheme.variables as Record<string, string>) : {};\n const chainVars = currentThemeId ? resolveChain(themes, currentThemeId) : [];\n return Object.assign({}, rootVars, ...chainVars);\n}\n\nclass ThemeProvider extends Component<Props, State> {\n // Resolved before the first render rather than after mount: resolved in componentDidMount, every\n // component rendered and measured itself unthemed first, and on each page load some 50\n // backgrounds then faded in from nothing.\n state: State = {\n resolvedVariables: resolveVariables(this.props),\n colorFadeActive: false,\n currentThemeColor: null,\n resolvedFrom: { themes: this.props.themes, currentThemeId: this.props.currentThemeId },\n };\n\n private colorFadeTimeout: ReturnType<typeof setTimeout> | null = null;\n private wrapperRef = React.createRef<HTMLDivElement>();\n private themeColorMeta: HTMLMetaElement | null = null;\n private createdThemeColorMeta = false;\n\n // The fade and the new variables have to land in the same render: added a render later, the\n // browser may already have painted the new colours without it.\n static getDerivedStateFromProps(props: Props, state: State): Partial<State> | null {\n const { themes, currentThemeId } = state.resolvedFrom;\n if (props.themes === themes && props.currentThemeId === currentThemeId) return null;\n return {\n resolvedVariables: resolveVariables(props),\n colorFadeActive: true,\n resolvedFrom: { themes: props.themes, currentThemeId: props.currentThemeId },\n };\n }\n\n componentDidMount() {\n this.readThemeColor();\n }\n\n componentDidUpdate(prevProps: Props, prevState: State) {\n if (prevState.resolvedVariables !== this.state.resolvedVariables) {\n this.readThemeColor();\n if (this.colorFadeTimeout) clearTimeout(this.colorFadeTimeout);\n this.colorFadeTimeout = setTimeout(() => {\n this.setState({ colorFadeActive: false });\n this.colorFadeTimeout = null;\n }, 500);\n }\n if (prevState.currentThemeColor !== this.state.currentThemeColor) {\n this.syncThemeColorMeta(this.state.currentThemeColor ?? '');\n }\n }\n\n componentWillUnmount() {\n if (this.colorFadeTimeout) clearTimeout(this.colorFadeTimeout);\n // Only remove the meta tag if we created it; leave a host-app tag intact.\n // Also clear the cached refs — StrictMode's dev-only mount/unmount/remount\n // cycle reuses this instance, and a stale ref would otherwise point at a\n // detached element on remount instead of re-inserting into <head>.\n if (this.createdThemeColorMeta) {\n this.themeColorMeta?.remove();\n this.themeColorMeta = null;\n this.createdThemeColorMeta = false;\n }\n }\n\n /**\n * Replaces the former react-helmet usage: keep a single\n * <meta name=\"theme-color\"> in sync with the active theme's primary colour.\n * Reuses a tag the host app already declares; otherwise creates its own\n * (and removes it again on unmount).\n */\n private syncThemeColorMeta(color: string) {\n if (typeof document === 'undefined') return;\n\n if (!this.themeColorMeta) {\n const existing = document.head.querySelector<HTMLMetaElement>('meta[name=\"theme-color\"]');\n if (existing) {\n this.themeColorMeta = existing;\n } else {\n const meta = document.createElement('meta');\n meta.setAttribute('name', 'theme-color');\n document.head.appendChild(meta);\n this.themeColorMeta = meta;\n this.createdThemeColorMeta = true;\n }\n }\n\n this.themeColorMeta.setAttribute('content', color);\n }\n\n /** `--primary-color` as the browser resolved it, which may be a `var()` of another variable. */\n private readThemeColor() {\n if (!this.wrapperRef.current) return;\n const color = getComputedStyle(this.wrapperRef.current).getPropertyValue('--primary-color').trim();\n if (color !== this.state.currentThemeColor) this.setState({ currentThemeColor: color });\n }\n\n render() {\n const { children } = this.props;\n const { resolvedVariables, colorFadeActive } = this.state;\n\n return (\n <div\n ref={this.wrapperRef}\n id='themeProvider'\n className={cx(styles.themeRoot, colorFadeActive && styles.colorFade)}\n style={resolvedVariables as React.CSSProperties}\n >\n <ThemeVariablesContext.Provider value={resolvedVariables}>{children}</ThemeVariablesContext.Provider>\n </div>\n );\n }\n}\n\nexport default ThemeProvider;\n"],"mappings":"4QA0BA,SAAS,EAAa,EAAiB,EAAsC,CAC3E,IAAM,EAAQ,EAAO,KAAM,GAAM,EAAE,KAAO,CAAE,EAG5C,OAFK,EAEE,CAAC,GADW,EAAM,YAAc,EAAa,EAAQ,EAAM,WAAW,EAAI,CAAC,EAC3D,EAAM,SAAmC,EAF7C,CAAC,CAGtB,CAEA,SAAS,EAAiB,CAAE,SAAS,CAAC,CAAY,EAAG,kBAAiD,CAEpG,IAAM,EAAY,EAAO,KAAM,GAAM,CAAC,EAAE,WAAW,GAAK,EAAO,GACzD,EAAW,EAAa,EAAU,UAAuC,CAAC,EAC1E,EAAY,EAAiB,EAAa,EAAQ,CAAc,EAAI,CAAC,EAC3E,OAAO,OAAO,OAAO,CAAC,EAAG,EAAU,GAAG,CAAS,CACjD,CAEA,IAAM,EAAN,cAA4B,CAAwB,+BAInC,KAAA,MAAA,CACb,kBAAmB,EAAiB,KAAK,KAAK,EAC9C,gBAAiB,GACjB,kBAAmB,KACnB,aAAc,CAAE,OAAQ,KAAK,MAAM,OAAQ,eAAgB,KAAK,MAAM,cAAe,CACvF,EAEiE,KAAA,iBAAA,KAC5C,KAAA,WAAA,EAAM,UAA0B,EACJ,KAAA,eAAA,KACjB,KAAA,sBAAA,GAIhC,OAAO,yBAAyB,EAAc,EAAqC,CACjF,GAAM,CAAE,SAAQ,kBAAmB,EAAM,aAEzC,OADI,EAAM,SAAW,GAAU,EAAM,iBAAmB,EAAuB,KACxE,CACL,kBAAmB,EAAiB,CAAK,EACzC,gBAAiB,GACjB,aAAc,CAAE,OAAQ,EAAM,OAAQ,eAAgB,EAAM,cAAe,CAC7E,CACF,CAEA,mBAAoB,CAClB,KAAK,eAAe,CACtB,CAEA,mBAAmB,EAAkB,EAAkB,CACjD,EAAU,oBAAsB,KAAK,MAAM,oBAC7C,KAAK,eAAe,EAChB,KAAK,kBAAkB,aAAa,KAAK,gBAAgB,EAC7D,KAAK,iBAAmB,eAAiB,CACvC,KAAK,SAAS,CAAE,gBAAiB,EAAM,CAAC,EACxC,KAAK,iBAAmB,IAC1B,EAAG,GAAG,GAEJ,EAAU,oBAAsB,KAAK,MAAM,mBAC7C,KAAK,mBAAmB,KAAK,MAAM,mBAAqB,EAAE,CAE9D,CAEA,sBAAuB,CACjB,KAAK,kBAAkB,aAAa,KAAK,gBAAgB,EAK7D,AAGE,KAAK,yBAFL,KAAK,gBAAgB,OAAO,EAC5B,KAAK,eAAiB,KACO,GAEjC,CAQA,mBAA2B,EAAe,CACpC,UAAO,SAAa,IAExB,IAAI,CAAC,KAAK,eAAgB,CACxB,IAAM,EAAW,SAAS,KAAK,cAA+B,0BAA0B,EACxF,GAAI,EACF,KAAK,eAAiB,MACjB,CACL,IAAM,EAAO,SAAS,cAAc,MAAM,EAC1C,EAAK,aAAa,OAAQ,aAAa,EACvC,SAAS,KAAK,YAAY,CAAI,EAC9B,KAAK,eAAiB,EACtB,KAAK,sBAAwB,EAC/B,CACF,CAEA,KAAK,eAAe,aAAa,UAAW,CAAK,CAFjD,CAGF,CAGA,gBAAyB,CACvB,GAAI,CAAC,KAAK,WAAW,QAAS,OAC9B,IAAM,EAAQ,iBAAiB,KAAK,WAAW,OAAO,CAAC,CAAC,iBAAiB,iBAAiB,CAAC,CAAC,KAAK,EAC7F,IAAU,KAAK,MAAM,mBAAmB,KAAK,SAAS,CAAE,kBAAmB,CAAM,CAAC,CACxF,CAEA,QAAS,CACP,GAAM,CAAE,YAAa,KAAK,MACpB,CAAE,oBAAmB,mBAAoB,KAAK,MAEpD,OACE,EAAC,MAAD,CACE,IAAK,KAAK,WACV,GAAG,gBACH,UAAW,EAAG,EAAO,UAAW,GAAmB,EAAO,SAAS,EACnE,MAAO,EAEP,SAAA,EAAC,EAAsB,SAAvB,CAAgC,MAAO,EAAoB,UAAyC,CAAA,CACjG,CAAA,CAET,CACF"}