@ahrowe/ui 0.36.0 → 0.37.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 (100) hide show
  1. package/README.md +3 -2
  2. package/dist/cjs-types/common/hooks/index.d.ts +13 -0
  3. package/dist/cjs-types/common/hooks/useHotkeys.d.ts +45 -0
  4. package/dist/cjs-types/common/hooks/useSwipeDismiss.d.ts +6 -2
  5. package/dist/cjs-types/common/loading/index.d.ts +0 -1
  6. package/dist/cjs-types/common/menu/index.d.ts +1 -0
  7. package/dist/cjs-types/common/menu/menu.types.d.ts +9 -3
  8. package/dist/cjs-types/common/menu/menuHotkeys.d.ts +20 -0
  9. package/dist/cjs-types/common/popover/usePopoverPosition.d.ts +1 -0
  10. package/dist/cjs-types/common/sticky/sticky.d.ts +1 -1
  11. package/dist/cjs-types/common/sticky/sticky.types.d.ts +15 -0
  12. package/dist/cjs-types/common/sticky/stickyStack.d.ts +5 -0
  13. package/dist/cjs-types/common/utils/scrollAncestors.d.ts +10 -0
  14. package/dist/cjs-types/common/virtualList/scroller.d.ts +33 -0
  15. package/dist/cjs-types/common/virtualList/useRowDrag.d.ts +6 -1
  16. package/dist/cjs-types/common/virtualList/useRowNavigation.d.ts +11 -1
  17. package/dist/cjs-types/common/virtualList/useVirtualWindow.d.ts +22 -3
  18. package/dist/cjs-types/common/virtualList/virtualList.d.ts +1 -1
  19. package/dist/cjs-types/common/virtualList/virtualList.types.d.ts +43 -3
  20. package/dist/cjs-types/index.d.ts +1 -0
  21. package/dist/esm/common/dropdown/dropdown.module.mjs.map +1 -1
  22. package/dist/esm/common/hooks/useHotkeys.mjs +2 -0
  23. package/dist/esm/common/hooks/useHotkeys.mjs.map +1 -0
  24. package/dist/esm/common/hooks/useSwipeDismiss.mjs +1 -1
  25. package/dist/esm/common/hooks/useSwipeDismiss.mjs.map +1 -1
  26. package/dist/esm/common/input/input.mjs +1 -1
  27. package/dist/esm/common/input/input.mjs.map +1 -1
  28. package/dist/esm/common/input/input.module.mjs +1 -1
  29. package/dist/esm/common/input/input.module.mjs.map +1 -1
  30. package/dist/esm/common/menu/menu.mjs +1 -1
  31. package/dist/esm/common/menu/menu.mjs.map +1 -1
  32. package/dist/esm/common/menu/menu.types.mjs.map +1 -1
  33. package/dist/esm/common/menu/menuHotkeys.mjs +2 -0
  34. package/dist/esm/common/menu/menuHotkeys.mjs.map +1 -0
  35. package/dist/esm/common/multiSelect/multiSelect.mjs +1 -1
  36. package/dist/esm/common/multiSelect/multiSelect.mjs.map +1 -1
  37. package/dist/esm/common/multiSelect/multiSelect.module.mjs +1 -1
  38. package/dist/esm/common/multiSelect/multiSelect.module.mjs.map +1 -1
  39. package/dist/esm/common/otpInput/otpInput.module.mjs.map +1 -1
  40. package/dist/esm/common/popover/usePopoverPosition.mjs +1 -1
  41. package/dist/esm/common/popover/usePopoverPosition.mjs.map +1 -1
  42. package/dist/esm/common/sticky/sticky.mjs +1 -1
  43. package/dist/esm/common/sticky/sticky.mjs.map +1 -1
  44. package/dist/esm/common/sticky/stickyStack.mjs +1 -1
  45. package/dist/esm/common/sticky/stickyStack.mjs.map +1 -1
  46. package/dist/esm/common/utils/scrollAncestors.mjs +1 -1
  47. package/dist/esm/common/utils/scrollAncestors.mjs.map +1 -1
  48. package/dist/esm/common/virtualList/scroller.mjs +2 -0
  49. package/dist/esm/common/virtualList/scroller.mjs.map +1 -0
  50. package/dist/esm/common/virtualList/useRowDrag.mjs +1 -1
  51. package/dist/esm/common/virtualList/useRowDrag.mjs.map +1 -1
  52. package/dist/esm/common/virtualList/useRowNavigation.mjs +1 -1
  53. package/dist/esm/common/virtualList/useRowNavigation.mjs.map +1 -1
  54. package/dist/esm/common/virtualList/useVirtualWindow.mjs +1 -1
  55. package/dist/esm/common/virtualList/useVirtualWindow.mjs.map +1 -1
  56. package/dist/esm/common/virtualList/virtualList.mjs +1 -1
  57. package/dist/esm/common/virtualList/virtualList.mjs.map +1 -1
  58. package/dist/esm/common/virtualList/virtualList.module.mjs +1 -1
  59. package/dist/esm/common/virtualList/virtualList.module.mjs.map +1 -1
  60. package/dist/esm/index.mjs +1 -1
  61. package/dist/index.cjs +3 -3
  62. package/dist/index.cjs.map +1 -1
  63. package/dist/style.css +1 -1
  64. package/dist/types/common/hooks/index.d.ts +13 -0
  65. package/dist/types/common/hooks/useHotkeys.d.ts +45 -0
  66. package/dist/types/common/hooks/useSwipeDismiss.d.ts +6 -2
  67. package/dist/types/common/loading/index.d.ts +0 -1
  68. package/dist/types/common/menu/index.d.ts +1 -0
  69. package/dist/types/common/menu/menu.types.d.ts +9 -3
  70. package/dist/types/common/menu/menuHotkeys.d.ts +20 -0
  71. package/dist/types/common/popover/usePopoverPosition.d.ts +1 -0
  72. package/dist/types/common/sticky/sticky.d.ts +1 -1
  73. package/dist/types/common/sticky/sticky.types.d.ts +15 -0
  74. package/dist/types/common/sticky/stickyStack.d.ts +5 -0
  75. package/dist/types/common/utils/scrollAncestors.d.ts +10 -0
  76. package/dist/types/common/virtualList/scroller.d.ts +33 -0
  77. package/dist/types/common/virtualList/useRowDrag.d.ts +6 -1
  78. package/dist/types/common/virtualList/useRowNavigation.d.ts +11 -1
  79. package/dist/types/common/virtualList/useVirtualWindow.d.ts +22 -3
  80. package/dist/types/common/virtualList/virtualList.d.ts +1 -1
  81. package/dist/types/common/virtualList/virtualList.types.d.ts +43 -3
  82. package/dist/types/index.d.ts +1 -0
  83. package/docs/CLAUDE.md +3 -2
  84. package/docs/Hooks.md +232 -0
  85. package/docs/Loading.md +56 -15
  86. package/docs/Menu.md +25 -3
  87. package/docs/Popover.md +2 -0
  88. package/docs/Sticky.md +22 -1
  89. package/docs/VirtualList.md +44 -4
  90. package/package.json +1 -1
  91. package/dist/cjs-types/common/loading/iconLoading/iconLoading.d.ts +0 -5
  92. package/dist/cjs-types/common/loading/iconLoading/index.d.ts +0 -2
  93. package/dist/esm/common/loading/iconLoading/iconLoading.mjs +0 -2
  94. package/dist/esm/common/loading/iconLoading/iconLoading.mjs.map +0 -1
  95. package/dist/esm/common/loading/iconLoading/iconLoading.module.mjs +0 -2
  96. package/dist/esm/common/loading/iconLoading/iconLoading.module.mjs.map +0 -1
  97. package/dist/esm/common/loading/iconLoading/index.mjs +0 -2
  98. package/dist/esm/common/loading/iconLoading/index.mjs.map +0 -1
  99. package/dist/types/common/loading/iconLoading/iconLoading.d.ts +0 -5
  100. package/dist/types/common/loading/iconLoading/index.d.ts +0 -2
@@ -1 +1 @@
1
- {"version":3,"file":"multiSelect.module.mjs","names":[],"sources":["../../../../package/common/multiSelect/multiSelect.module.pcss"],"sourcesContent":[".multiSelect {\n position: relative;\n width: 100%;\n\n /* Input's notch geometry, so both labels float to the same height in a form row. */\n --multi-select-notch-height: 1.75em;\n\n &-label {\n position: absolute;\n left: 7px;\n right: 0;\n top: 50%;\n transform: translateY(-50%);\n font-size: 1em;\n color: var(--input-label-resting-color, var(--text-light));\n pointer-events: none;\n user-select: none;\n transition: all 0.3s;\n\n &-floating {\n top: 0;\n bottom: auto;\n left: 14px;\n font-size: 0.8em;\n color: var(--input-label-floating-color, var(--text-color));\n }\n }\n\n /* The field's border lives here, not on the control: the legend has to cut the notch out of it. */\n &-fieldset {\n position: absolute;\n inset: calc(var(--multi-select-notch-height) / -2) 0 0;\n width: 100%;\n margin: 0;\n padding: 0 0 0 8px;\n min-width: 0;\n box-sizing: border-box;\n border: 1px solid var(--border-color);\n border-radius: var(--default-border-radius);\n font-size: 0.8em;\n pointer-events: none;\n user-select: none;\n transition: border-color 0.15s;\n\n &-noNotch {\n /* Resetting this everywhere would leave a label-less field shorter than its neighbours.\n Only Firefox renders the reserved space as an overlap, so scope it there, as Input does. */\n @supports (-moz-appearance: none) {\n inset: 0;\n }\n }\n\n &-focused {\n border-color: var(--focus-border-color, var(--primary-color));\n }\n\n &-error {\n border-color: var(--error-border-color, var(--error-color));\n }\n\n &-legend {\n display: block;\n max-width: 0;\n padding: 0;\n white-space: nowrap;\n transition: max-width 0.2s;\n\n &-active {\n max-width: 300px;\n }\n\n span {\n line-height: var(--multi-select-notch-height);\n padding: 0 5px;\n opacity: 0;\n }\n\n span:empty {\n padding: 0.000001px;\n }\n }\n }\n\n &-control {\n position: relative;\n display: flex;\n align-items: center;\n gap: 6px;\n box-sizing: border-box;\n width: 100%;\n\n /* Input and Dropdown both resolve to this: a 1.25 line-height plus 14px of padding. Kept as\n the formula rather than a flat 34px so it still matches them at another font size. */\n min-height: var(--multi-select-min-height, calc(1.25em + 14px));\n padding: 2px 8px;\n border-radius: var(--default-border-radius);\n background: var(--input-background, var(--background));\n color: var(--text-color);\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n\n /* An outline here would draw a second, un-notched rectangle over the fieldset's border. */\n &:focus-visible {\n outline: none;\n\n .multiSelect-fieldset {\n border-color: var(--focus-border-color, var(--primary-color));\n }\n }\n }\n\n &-values {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 4px;\n flex: 1;\n min-width: 0;\n }\n\n /* At full size a Chip is nearly as tall as the field, so one row would set the height instead\n of fitting inside it. Shrinking the font shrinks the text and the 1em remove icon together. */\n &-chip {\n max-width: 100%;\n min-width: 0;\n font-size: 0.8em;\n\n /* FontAwesome's `vertical-align: -0.125em` makes the inline remove icon reserve 1.6px of\n descender space: the difference between a 34px field and a 34.4px one. */\n svg {\n display: block;\n }\n }\n\n /* Faded in on a delay rather than swapped in: the label takes 0.3s to float clear, and showing\n the placeholder as it starts moving puts the two on top of each other. As Input does it. */\n &-placeholder {\n position: absolute;\n left: 8px;\n right: 1.75em;\n top: 50%;\n transform: translateY(-50%);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n opacity: 0;\n color: var(--input-label-resting-color, var(--text-light));\n pointer-events: none;\n user-select: none;\n\n &-shown {\n animation: 0.2s multiSelectFadeIn 0.3s forwards;\n }\n\n /* Nothing to wait for without a label. Must stay after `-shown` to win the specificity tie. */\n &-instant {\n animation-delay: 0s;\n }\n }\n\n @keyframes multiSelectFadeIn {\n from {\n opacity: 0;\n }\n\n to {\n opacity: 1;\n }\n }\n\n &-clearButton {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n width: 22px;\n height: 22px;\n padding: 0;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: var(--text-dark);\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n\n &:hover {\n background: var(--background-accent);\n color: var(--text-color);\n }\n }\n\n &-icon {\n flex: none;\n color: var(--input-icon-color, var(--text-dark));\n transition: transform 0.2s;\n\n &-open {\n transform: rotate(180deg);\n }\n }\n\n &-disabled {\n opacity: 0.5;\n\n .multiSelect-control {\n cursor: default;\n }\n }\n}\n\n/* Portalled, so a sibling of the field rather than a descendant, and not nestable above. */\n.multiSelect-panel {\n display: flex;\n flex-direction: column;\n gap: 6px;\n min-width: 180px;\n}\n\n.multiSelect-search {\n flex: none;\n}\n\n.multiSelect-options {\n display: flex;\n flex-direction: column;\n max-height: var(--multi-select-panel-height, 260px);\n overflow-y: auto;\n}\n\n.multiSelect-option {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 10px;\n border-radius: var(--default-border-radius);\n color: var(--text-color);\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n\n &-active {\n background: var(--background-accent);\n }\n\n &-selected {\n color: var(--primary-color);\n font-weight: 600;\n }\n\n &-disabled {\n opacity: 0.45;\n cursor: default;\n pointer-events: none;\n }\n}\n\n.multiSelect-optionCheck {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n width: 1em;\n color: var(--primary-color);\n}\n\n.multiSelect-optionLabel {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.multiSelect-empty {\n padding: 12px 10px;\n color: var(--text-dark);\n text-align: center;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .multiSelect-label,\n .multiSelect-icon {\n transition: none;\n }\n}\n"],"mappings":""}
1
+ {"version":3,"file":"multiSelect.module.mjs","names":[],"sources":["../../../../package/common/multiSelect/multiSelect.module.pcss"],"sourcesContent":[".multiSelect {\n position: relative;\n width: 100%;\n\n /* Input's notch geometry, so both labels float to the same height in a form row. */\n --multi-select-notch-height: 1.75em;\n\n &-label {\n position: absolute;\n left: 7px;\n right: 0;\n top: 50%;\n transform: translateY(-50%);\n font-size: 1em;\n color: var(--input-label-resting-color, var(--text-light));\n pointer-events: none;\n user-select: none;\n transition: all 0.3s;\n\n &-floating {\n top: 0;\n bottom: auto;\n left: 14px;\n font-size: 0.8em;\n color: var(--input-label-floating-color, var(--text-color));\n }\n }\n\n /* The field's border lives here, not on the control: the legend has to cut the notch out of it. */\n &-fieldset {\n position: absolute;\n inset: calc(var(--multi-select-notch-height) / -2) 0 0;\n width: 100%;\n margin: 0;\n padding: 0 0 0 8px;\n min-width: 0;\n box-sizing: border-box;\n border: 1px solid var(--border-color);\n border-radius: var(--default-border-radius);\n font-size: 0.8em;\n pointer-events: none;\n user-select: none;\n transition: border-color 0.15s;\n\n &-focused {\n border-color: var(--focus-border-color, var(--primary-color));\n }\n\n &-error {\n border-color: var(--error-border-color, var(--error-color));\n }\n\n &-legend {\n display: block;\n\n /* Not taken from the text: an empty span gets no line box, which raised a label-less\n field's border half a notch. */\n height: var(--multi-select-notch-height);\n max-width: 0;\n padding: 0;\n white-space: nowrap;\n transition: max-width 0.2s;\n\n &-active {\n max-width: 300px;\n }\n\n span {\n line-height: var(--multi-select-notch-height);\n padding: 0 5px;\n opacity: 0;\n }\n\n span:empty {\n padding: 0;\n }\n }\n }\n\n &-control {\n position: relative;\n display: flex;\n align-items: center;\n gap: 6px;\n box-sizing: border-box;\n width: 100%;\n\n /* Input and Dropdown both resolve to this: a 1.25 line-height plus 14px of padding. Kept as\n the formula rather than a flat 34px so it still matches them at another font size. */\n min-height: var(--multi-select-min-height, calc(1.25em + 14px));\n padding: 2px 8px;\n border-radius: var(--default-border-radius);\n background: var(--input-background, var(--background));\n color: var(--text-color);\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n\n /* An outline here would draw a second, un-notched rectangle over the fieldset's border. */\n &:focus-visible {\n outline: none;\n\n .multiSelect-fieldset {\n border-color: var(--focus-border-color, var(--primary-color));\n }\n }\n }\n\n &-values {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 4px;\n flex: 1;\n min-width: 0;\n }\n\n /* At full size a Chip is nearly as tall as the field, so one row would set the height instead\n of fitting inside it. Shrinking the font shrinks the text and the 1em remove icon together. */\n &-chip {\n max-width: 100%;\n min-width: 0;\n font-size: 0.8em;\n\n /* FontAwesome's `vertical-align: -0.125em` makes the inline remove icon reserve 1.6px of\n descender space: the difference between a 34px field and a 34.4px one. */\n svg {\n display: block;\n }\n }\n\n /* Faded in on a delay rather than swapped in: the label takes 0.3s to float clear, and showing\n the placeholder as it starts moving puts the two on top of each other. As Input does it. */\n &-placeholder {\n position: absolute;\n left: 8px;\n right: 1.75em;\n top: 50%;\n transform: translateY(-50%);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n opacity: 0;\n color: var(--input-label-resting-color, var(--text-light));\n pointer-events: none;\n user-select: none;\n\n &-shown {\n animation: 0.2s multiSelectFadeIn 0.3s forwards;\n }\n\n /* Nothing to wait for without a label. Must stay after `-shown` to win the specificity tie. */\n &-instant {\n animation-delay: 0s;\n }\n }\n\n @keyframes multiSelectFadeIn {\n from {\n opacity: 0;\n }\n\n to {\n opacity: 1;\n }\n }\n\n &-clearButton {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n width: 22px;\n height: 22px;\n padding: 0;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: var(--text-dark);\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n\n &:hover {\n background: var(--background-accent);\n color: var(--text-color);\n }\n }\n\n &-icon {\n flex: none;\n color: var(--input-icon-color, var(--text-dark));\n transition: transform 0.2s;\n\n &-open {\n transform: rotate(180deg);\n }\n }\n\n &-disabled {\n opacity: 0.5;\n\n .multiSelect-control {\n cursor: default;\n }\n }\n}\n\n/* Portalled, so a sibling of the field rather than a descendant, and not nestable above. */\n.multiSelect-panel {\n display: flex;\n flex-direction: column;\n gap: 6px;\n min-width: 180px;\n}\n\n.multiSelect-search {\n flex: none;\n}\n\n.multiSelect-options {\n display: flex;\n flex-direction: column;\n max-height: var(--multi-select-panel-height, 260px);\n overflow-y: auto;\n}\n\n.multiSelect-option {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 10px;\n border-radius: var(--default-border-radius);\n color: var(--text-color);\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n\n &-active {\n background: var(--background-accent);\n }\n\n &-selected {\n color: var(--primary-color);\n font-weight: 600;\n }\n\n &-disabled {\n opacity: 0.45;\n cursor: default;\n pointer-events: none;\n }\n}\n\n.multiSelect-optionCheck {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n width: 1em;\n color: var(--primary-color);\n}\n\n.multiSelect-optionLabel {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.multiSelect-empty {\n padding: 12px 10px;\n color: var(--text-dark);\n text-align: center;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .multiSelect-label,\n .multiSelect-icon {\n transition: none;\n }\n}\n"],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"otpInput.module.mjs","names":[],"sources":["../../../../package/common/otpInput/otpInput.module.pcss"],"sourcesContent":["@keyframes otpInputShake {\n 10%,\n 90% {\n transform: translateX(-1px);\n }\n\n 20%,\n 80% {\n transform: translateX(2px);\n }\n\n 30%,\n 50%,\n 70% {\n transform: translateX(-4px);\n }\n\n 40%,\n 60% {\n transform: translateX(4px);\n }\n}\n\n.otpInput {\n display: inline-flex;\n flex-direction: column;\n row-gap: 0.5em;\n\n &-label {\n font-size: var(--default-font-size);\n color: var(--text-color);\n }\n\n &-requiredMark {\n color: var(--required-mark-color, var(--error-color));\n }\n\n &-boxes {\n display: flex;\n align-items: center;\n column-gap: 0.5em;\n\n &-shake {\n animation: otpInputShake 0.4s ease-in-out;\n\n @media (prefers-reduced-motion: reduce) {\n animation: none;\n }\n }\n }\n\n &-boxWrapper {\n display: flex;\n align-items: center;\n column-gap: 0.5em;\n }\n\n /* These em are against this box's OWN 1.25em font, not the root's: 40px and 48px of 20px. */\n &-box {\n width: 2em;\n height: 2.4em;\n text-align: center;\n font-size: 1.25em;\n font-weight: 600;\n font-family: var(--themed-font, inherit);\n color: var(--text-color);\n background: var(--input-background, var(--background));\n border: 0.075em solid var(--border-color);\n border-radius: var(--default-border-radius);\n outline: none;\n transition:\n border-color 0.15s,\n box-shadow 0.15s;\n\n &:focus {\n border-color: var(--focus-border-color, var(--primary-color));\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--focus-border-color, var(--primary-color)) 20%, transparent);\n }\n\n &-filled {\n border-color: var(--primary-lighter, var(--border-color));\n }\n\n &-error {\n border-color: var(--error-color);\n\n &:focus {\n border-color: var(--error-color);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--error-color) 20%, transparent);\n }\n }\n\n &:disabled {\n color: var(--disabled-font-color);\n background: var(--disabled-input-color);\n cursor: not-allowed;\n }\n }\n\n &-separator {\n width: 0.625em;\n height: 0.09375em;\n background: var(--border-color);\n flex-shrink: 0;\n }\n\n &-disabled &-label {\n color: var(--disabled-font-color);\n }\n}\n"],"mappings":""}
1
+ {"version":3,"file":"otpInput.module.mjs","names":[],"sources":["../../../../package/common/otpInput/otpInput.module.pcss"],"sourcesContent":["@keyframes otpInputShake {\n 10%,\n 90% {\n transform: translateX(-1px);\n }\n\n 20%,\n 80% {\n transform: translateX(2px);\n }\n\n 30%,\n 50%,\n 70% {\n transform: translateX(-4px);\n }\n\n 40%,\n 60% {\n transform: translateX(4px);\n }\n}\n\n.otpInput {\n display: inline-flex;\n flex-direction: column;\n row-gap: 0.5em;\n\n &-label {\n font-size: var(--default-font-size);\n color: var(--text-color);\n }\n\n &-requiredMark {\n color: var(--required-mark-color, var(--error-color));\n }\n\n &-boxes {\n display: flex;\n align-items: center;\n column-gap: 0.5em;\n\n &-shake {\n animation: otpInputShake 0.4s ease-in-out;\n\n @media (prefers-reduced-motion: reduce) {\n animation: none;\n }\n }\n }\n\n &-boxWrapper {\n display: flex;\n align-items: center;\n column-gap: 0.5em;\n }\n\n /* These em are against this box's OWN 1.25em font, not the root's: 40px and 48px of 20px. */\n &-box {\n width: 2em;\n height: 2.4em;\n text-align: center;\n font-size: 1.25em;\n font-weight: 600;\n font-family: var(--themed-font, inherit);\n color: var(--text-color);\n background: var(--input-background, var(--background));\n border: 0.075em solid var(--border-color);\n border-radius: var(--default-border-radius);\n outline: none;\n transition:\n border-color 0.15s,\n box-shadow 0.15s;\n\n &:focus {\n border-color: var(--focus-border-color, var(--primary-color));\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--focus-border-color, var(--primary-color)) 20%, transparent);\n }\n\n &-filled {\n border-color: var(--primary-lighter, var(--border-color));\n }\n\n &-error {\n border-color: var(--error-color);\n\n &:focus {\n border-color: var(--error-color);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--error-color) 20%, transparent);\n }\n }\n\n &:disabled {\n color: var(--disabled-font-color);\n background: var(--disabled-input-color);\n cursor: not-allowed;\n }\n }\n\n &-separator {\n width: 0.625em;\n height: calc(1.5em / 16);\n background: var(--border-color);\n flex-shrink: 0;\n }\n\n &-disabled &-label {\n color: var(--disabled-font-color);\n }\n}\n"],"mappings":""}
@@ -1,2 +1,2 @@
1
- "use client";import{isHiddenByAnyAncestor as e}from"../utils/scrollAncestors.mjs";import{getVisualViewportRect as t}from"../utils/visualViewport.mjs";import{useAnchorTracking as n}from"../hooks/useAnchorTracking.mjs";import{PopoverAlign as r,PopoverPlacement as i}from"./popover.types.mjs";var a={[i.Top]:`bottom`,[i.Bottom]:`top`,[i.Left]:`right`,[i.Right]:`left`},o=8,s=160,c=3;function l(e){let t=e;for(let e=0;e<=c;e++){let e=t.getBoundingClientRect();if(e.width>0||e.height>0)return e;let n=t.firstElementChild;if(!n)break;t=n}return e.getBoundingClientRect()}function u({isOpen:c,placement:u,align:d,offset:f,triggerRef:p,floatingRef:m,panelRef:h,arrowRef:g,matchTriggerWidth:_=!1,shrinkToFit:v=!1}){n({isOpen:c,triggerRef:p,elementRefs:[m,h],resizeRefs:[h],deps:[u,d,f,m,h,g,_,v],update:(n,c)=>{let p=m.current,y=h.current;if(!p||!y)return;let b=l(n);_?y.style.minWidth=`${b.width}px`:y.style.removeProperty(`min-width`),v&&y.style.removeProperty(`max-width`);let{width:x,height:S}=p.getBoundingClientRect(),C=t(),w=u===i.Top||u===i.Bottom;if(v&&w){let e=d===r.End?b.right-(C.left+o):d===r.Start?C.left+C.width-o-b.left:C.width-16,t=Math.min(e,C.width-16);x>t&&t>=s&&(y.style.maxWidth=`${t}px`,{width:x,height:S}=p.getBoundingClientRect())}let T={top:b.top-C.top,bottom:C.top+C.height-b.bottom,left:b.left-C.left,right:C.left+C.width-b.right},E=u===i.Top||u===i.Bottom?S:x,D=u,O=a[u];T[D]<E+f&&T[O]>T[D]&&(D=O);let k=D===`top`||D===`bottom`,A,j;D===`bottom`?A=b.bottom+f:D===`top`?A=b.top-f-S:j=D===`right`?b.right+f:b.left-f-x,k?(j=d===r.Start?b.left:d===r.End?b.right-x:b.left+b.width/2-x/2,j=Math.max(C.left+o,Math.min(j,C.left+C.width-x-o))):(A=d===r.Start?b.top:d===r.End?b.bottom-S:b.top+b.height/2-S/2,A=Math.max(C.top+o,Math.min(A,C.top+C.height-S-o))),p.style.top=`${A}px`,p.style.left=`${j}px`,p.style.visibility=e(b,c)?`hidden`:`visible`,p.dataset.placement=D;let M=g.current;if(M){let e=M.offsetWidth/2||5;if(k){let t=b.left+b.width/2-j;M.style.setProperty(`--arrow-x`,`${Math.max(e+2,Math.min(t,x-e-2))}px`),M.style.removeProperty(`--arrow-y`)}else{let t=b.top+b.height/2-A;M.style.setProperty(`--arrow-y`,`${Math.max(e+2,Math.min(t,S-e-2))}px`),M.style.removeProperty(`--arrow-x`)}}},teardown:()=>{let e=m.current;if(!e)return;e.style.top=``,e.style.left=``,e.style.visibility=``;let t=h.current;t?.style.removeProperty(`min-width`),t?.style.removeProperty(`max-width`)}})}export{u as usePopoverPosition};
1
+ "use client";import{isHiddenByAnyAncestor as e}from"../utils/scrollAncestors.mjs";import{getVisualViewportRect as t}from"../utils/visualViewport.mjs";import{useAnchorTracking as n}from"../hooks/useAnchorTracking.mjs";import{PopoverAlign as r,PopoverPlacement as i}from"./popover.types.mjs";import{useId as a,useRef as o}from"react";var s={[i.Top]:`bottom`,[i.Bottom]:`top`,[i.Left]:`right`,[i.Right]:`left`},c=8,l=160,u=3;function d(e){let t=e;for(let e=0;e<=u;e++){let e=t.getBoundingClientRect();if(e.width>0||e.height>0)return{element:t,rect:e};let n=t.firstElementChild;if(!n)break;t=n}return{element:null,rect:e.getBoundingClientRect()}}var f;function p(){if(f!==void 0)return f;if(typeof CSS>`u`||!CSS.supports?.(`top: anchor(top, 0px)`))return f=!1;let e=document.createElement(`div`);e.style.cssText=`position: fixed; top: 0; left: 0; transform: translateY(100px); visibility: hidden`;let t=document.createElement(`div`);t.style.cssText=`anchor-name: --popover-probe; width: 1px; height: 1px`;let n=document.createElement(`div`);return n.style.cssText=`position: fixed; position-anchor: --popover-probe; top: anchor(top, 0px); left: 0; visibility: hidden`,e.append(t),document.body.append(e,n),f=Math.abs(n.getBoundingClientRect().top-t.getBoundingClientRect().top)<1,e.remove(),n.remove(),f}function m(e){return e.style.getPropertyValue(`anchor-name`).split(`,`).map(e=>e.trim()).filter(e=>e&&e!==`none`)}function h(e,t){let n=m(e);n.includes(t)||e.style.setProperty(`anchor-name`,[...n,t].join(`, `))}function g(e,t){let n=m(e).filter(e=>e!==t);n.length>0?e.style.setProperty(`anchor-name`,n.join(`, `)):e.style.removeProperty(`anchor-name`)}function _(e,t,n,r){return r?`calc(anchor(${e}, ${n}px) + ${t-n}px)`:`${t}px`}function v({isOpen:u,placement:f,align:m,offset:v,triggerRef:y,floatingRef:b,panelRef:x,arrowRef:S,matchTriggerWidth:C=!1,shrinkToFit:w=!1}){let T=`--popover-${a().replace(/[^\w-]/g,``)}`,E=o(null);function D(e){E.current!==e&&(E.current&&g(E.current,T),e&&h(e,T),E.current=e)}n({isOpen:u,triggerRef:y,elementRefs:[b,x],resizeRefs:[x],deps:[f,m,v,b,x,S,C,w],update:(n,a)=>{let o=b.current,u=x.current;if(!o||!u)return;let{element:h,rect:g}=d(n),y=h!==null&&p();D(y?h:null),y?o.style.setProperty(`position-anchor`,T):o.style.removeProperty(`position-anchor`),C?u.style.minWidth=`${g.width}px`:u.style.removeProperty(`min-width`),w&&u.style.removeProperty(`max-width`);let{width:E,height:O}=o.getBoundingClientRect(),k=t(),A=f===i.Top||f===i.Bottom;if(w&&A){let e=m===r.End?g.right-(k.left+c):m===r.Start?k.left+k.width-c-g.left:k.width-16,t=Math.min(e,k.width-16);E>t&&t>=l&&(u.style.maxWidth=`${t}px`,{width:E,height:O}=o.getBoundingClientRect())}let j={top:g.top-k.top,bottom:k.top+k.height-g.bottom,left:g.left-k.left,right:k.left+k.width-g.right},M=f===i.Top||f===i.Bottom?O:E,N=f,P=s[f];j[N]<M+v&&j[P]>j[N]&&(N=P);let F=N===`top`||N===`bottom`,I,L;N===`bottom`?I=g.bottom+v:N===`top`?I=g.top-v-O:L=N===`right`?g.right+v:g.left-v-E;let R=!1,z=!1;if(F){L=m===r.Start?g.left:m===r.End?g.right-E:g.left+g.width/2-E/2;let e=Math.max(k.left+c,Math.min(L,k.left+k.width-E-c));R=e!==L,L=e}else{I=m===r.Start?g.top:m===r.End?g.bottom-O:g.top+g.height/2-O/2;let e=Math.max(k.top+c,Math.min(I,k.top+k.height-O-c));z=e!==I,I=e}o.style.top=_(`top`,I,g.top,y&&!z),o.style.left=_(`left`,L,g.left,y&&!R),o.style.visibility=e(g,a)?`hidden`:`visible`,o.dataset.placement=N;let B=S.current;if(B){let e=B.offsetWidth/2||5;if(F){let t=g.left+g.width/2-L;B.style.setProperty(`--arrow-x`,`${Math.max(e+2,Math.min(t,E-e-2))}px`),B.style.removeProperty(`--arrow-y`)}else{let t=g.top+g.height/2-I;B.style.setProperty(`--arrow-y`,`${Math.max(e+2,Math.min(t,O-e-2))}px`),B.style.removeProperty(`--arrow-x`)}}},teardown:()=>{D(null);let e=b.current;if(!e)return;e.style.top=``,e.style.left=``,e.style.visibility=``,e.style.removeProperty(`position-anchor`);let t=x.current;t?.style.removeProperty(`min-width`),t?.style.removeProperty(`max-width`)}})}export{p as supportsAnchoring,v as usePopoverPosition};
2
2
  //# sourceMappingURL=usePopoverPosition.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"usePopoverPosition.mjs","names":[],"sources":["../../../../package/common/popover/usePopoverPosition.ts"],"sourcesContent":["import { type RefObject } from 'react';\nimport { isHiddenByAnyAncestor } from 'components/common/utils/scrollAncestors';\nimport { getVisualViewportRect } from 'components/common/utils/visualViewport';\nimport { useAnchorTracking } from '../hooks/useAnchorTracking';\nimport { PopoverPlacement, PopoverAlign } from './popover.types';\n\ntype Axis = 'top' | 'bottom' | 'left' | 'right';\nconst OPPOSITE: Record<PopoverPlacement, Axis> = {\n [PopoverPlacement.Top]: 'bottom',\n [PopoverPlacement.Bottom]: 'top',\n [PopoverPlacement.Left]: 'right',\n [PopoverPlacement.Right]: 'left',\n};\n\nconst MARGIN = 8; // viewport gutter kept clear when shifting a panel back on-screen\n\n/**\n * Narrower than this and `shrinkToFit` gives up: a panel this size reads worse than\n * one that overhangs. Around four words a line for an error bubble, which is the\n * narrowest caller. A constant rather than an option, because the one caller that\n * shrinks has no reason to want a different floor; give it one the day a second\n * caller disagrees, as `shrinkToFit: boolean | number`.\n */\nconst MIN_SHRINK_WIDTH = 160;\n\n/** How far down to look for a box when the wrappers in between have none. */\nconst MAX_ANCHOR_DEPTH = 3;\n\n/**\n * The rect to anchor to. A trigger with a box of its own is that box, which is the\n * case for a caller that hands over a real element (`Tooltip`).\n *\n * Popover's own trigger wrapper generates no box, deliberately, so that the\n * trigger keeps the place it would have had without Popover around it. Then the\n * real trigger is inside it, and this descends to find it, past any further\n * boxless element a consumer wrapped its trigger in: such an element has a zero\n * rect at the origin, which would otherwise send the panel to the top-left corner\n * of the screen.\n */\nfunction resolveAnchorRect(trigger: HTMLElement): DOMRect {\n let el: HTMLElement = trigger;\n for (let depth = 0; depth <= MAX_ANCHOR_DEPTH; depth++) {\n const rect = el.getBoundingClientRect();\n if (rect.width > 0 || rect.height > 0) return rect;\n const child = el.firstElementChild as HTMLElement | null;\n if (!child) break;\n el = child;\n }\n return trigger.getBoundingClientRect();\n}\n\nexport interface UsePopoverPositionArgs {\n isOpen: boolean;\n placement: PopoverPlacement;\n align: PopoverAlign;\n offset: number;\n triggerRef: RefObject<HTMLElement | null>;\n /** `position: fixed` wrapper that receives top/left/visibility and the resolved-placement data attribute. */\n floatingRef: RefObject<HTMLElement | null>;\n /** Visible panel, measured to decide flip and shift. */\n panelRef: RefObject<HTMLElement | null>;\n /** Arrow element; gets `--arrow-x` / `--arrow-y` written to it. Pass a null ref when `withArrow` is false. */\n arrowRef: RefObject<HTMLElement | null>;\n /**\n * Give the panel at least the trigger's width, for a list that lines up with\n * the field it belongs to. A minimum rather than a fixed width: a menu on a\n * narrow icon button still sizes to its own content.\n */\n matchTriggerWidth?: boolean;\n /**\n * When the panel does not fit on the side its alignment puts it, narrow it\n * rather than sliding it away from the trigger. Sliding keeps the panel\n * on-screen but detaches it: an end-aligned bubble stops ending where its\n * trigger ends, and its arrow stops pointing at anything. Off by default, since\n * a panel whose width is its content, a menu or a list, would rather overhang\n * its trigger than wrap.\n *\n * Only down to `MIN_SHRINK_WIDTH`. Past that the panel is narrower than it is\n * readable, and a trigger that close to the screen edge is better served by a\n * panel that overhangs it.\n */\n shrinkToFit?: boolean;\n}\n\n/**\n * Positions a portaled, `position: fixed` popover on any of the four sides of its trigger with\n * start/center/end cross-axis alignment. Flips to the opposite side when the preferred one has no\n * room, shifts back on-screen along the cross axis, points the arrow at the trigger centre, and\n * hides (not closes) the panel once the trigger scrolls behind a clipping ancestor.\n *\n * `useAnchorTracking` supplies the lifecycle around this: mount wait, ancestor resolution, and\n * re-running on scroll, viewport resize and panel size changes.\n */\nexport function usePopoverPosition({\n isOpen,\n placement,\n align,\n offset,\n triggerRef,\n floatingRef,\n panelRef,\n arrowRef,\n matchTriggerWidth = false,\n shrinkToFit = false,\n}: UsePopoverPositionArgs) {\n useAnchorTracking({\n isOpen,\n triggerRef,\n elementRefs: [floatingRef, panelRef],\n // The panel's own size feeds the flip decision and, on the `top`/`left` sides, the coordinate\n // itself — so content that resizes while open has to be re-measured.\n resizeRefs: [panelRef],\n deps: [placement, align, offset, floatingRef, panelRef, arrowRef, matchTriggerWidth, shrinkToFit],\n\n update: (trigger, ancestors) => {\n const floating = floatingRef.current;\n const panel = panelRef.current;\n if (!floating || !panel) return;\n\n const t = resolveAnchorRect(trigger);\n // Written before the panel is measured, so `pw` below already accounts for it,\n // and taken back off when it is no longer wanted: the panel outlives the\n // switch, and an inline width would outrank whatever it switched to.\n if (matchTriggerWidth) panel.style.minWidth = `${t.width}px`;\n else panel.style.removeProperty('min-width');\n // The wrapper's box, not the panel's: `offsetWidth` rounds to whole pixels, so\n // aligning a panel 165.4px wide against a trigger edge at 406.47 left it 0.4px\n // past that edge. The panel's own rect is fractional but carries the entrance\n // animation's scale; the wrapper is the same layout box without the transform.\n // Cleared before measuring, or a cap left over from a narrower screen would be\n // read back as the panel's own width and never widen again.\n if (shrinkToFit) panel.style.removeProperty('max-width');\n let { width: pw, height: ph } = floating.getBoundingClientRect();\n // The visible area, not the full layout viewport — shrinks on mobile when a keyboard is\n // open, so the panel flips/clamps against what's actually on screen instead of ending up\n // hidden behind the keyboard.\n const vv = getVisualViewportRect();\n\n // Flipping only swaps a side for its opposite, so which axis the alignment runs\n // along is decided by the preferred placement alone, before any of that.\n const alignsHorizontally = placement === PopoverPlacement.Top || placement === PopoverPlacement.Bottom;\n\n if (shrinkToFit && alignsHorizontally) {\n const alignedSide =\n align === PopoverAlign.End\n ? t.right - (vv.left + MARGIN)\n : align === PopoverAlign.Start\n ? vv.left + vv.width - MARGIN - t.left\n : vv.width - 2 * MARGIN;\n const room = Math.min(alignedSide, vv.width - 2 * MARGIN);\n if (pw > room && room >= MIN_SHRINK_WIDTH) {\n panel.style.maxWidth = `${room}px`;\n // Wrapping makes it taller, and the height is what the flip below is decided on.\n ({ width: pw, height: ph } = floating.getBoundingClientRect());\n }\n }\n\n // Resolve the main-axis side, flipping when the preferred side lacks room and the\n // opposite side has more.\n const room: Record<Axis, number> = {\n top: t.top - vv.top,\n bottom: vv.top + vv.height - t.bottom,\n left: t.left - vv.left,\n right: vv.left + vv.width - t.right,\n };\n const need = placement === PopoverPlacement.Top || placement === PopoverPlacement.Bottom ? ph : pw;\n let side: Axis = placement as unknown as Axis;\n const opp = OPPOSITE[placement];\n if (room[side] < need + offset && room[opp] > room[side]) side = opp;\n\n const vertical = side === 'top' || side === 'bottom';\n\n // Main axis: place the panel just outside the trigger on the resolved side.\n let top: number;\n let left: number;\n if (side === 'bottom') {\n top = t.bottom + offset;\n } else if (side === 'top') {\n top = t.top - offset - ph;\n } else if (side === 'right') {\n left = t.right + offset;\n } else {\n left = t.left - offset - pw;\n }\n\n // Cross axis: align start/center/end against the trigger, then shift on-screen.\n if (vertical) {\n if (align === PopoverAlign.Start) left = t.left;\n else if (align === PopoverAlign.End) left = t.right - pw;\n else left = t.left + t.width / 2 - pw / 2;\n left = Math.max(vv.left + MARGIN, Math.min(left!, vv.left + vv.width - pw - MARGIN));\n } else {\n if (align === PopoverAlign.Start) top = t.top;\n else if (align === PopoverAlign.End) top = t.bottom - ph;\n else top = t.top + t.height / 2 - ph / 2;\n top = Math.max(vv.top + MARGIN, Math.min(top!, vv.top + vv.height - ph - MARGIN));\n }\n\n floating.style.top = `${top!}px`;\n floating.style.left = `${left!}px`;\n floating.style.visibility = isHiddenByAnyAncestor(t, ancestors) ? 'hidden' : 'visible';\n floating.dataset.placement = side;\n\n // Arrow: centre it on the trigger along the cross axis, clamped inside the panel edges.\n const arrow = arrowRef.current;\n if (arrow) {\n const half = arrow.offsetWidth / 2 || 5;\n if (vertical) {\n const x = t.left + t.width / 2 - left!;\n arrow.style.setProperty('--arrow-x', `${Math.max(half + 2, Math.min(x, pw - half - 2))}px`);\n arrow.style.removeProperty('--arrow-y');\n } else {\n const y = t.top + t.height / 2 - top!;\n arrow.style.setProperty('--arrow-y', `${Math.max(half + 2, Math.min(y, ph - half - 2))}px`);\n arrow.style.removeProperty('--arrow-x');\n }\n }\n },\n\n teardown: () => {\n // Positioning is written as inline styles, which outrank any class the panel might\n // switch to while it stays mounted — a popover turning into a bottom sheet (`useSheet`)\n // would keep the coordinates computed for its anchored self.\n const floating = floatingRef.current;\n if (!floating) return;\n floating.style.top = '';\n floating.style.left = '';\n floating.style.visibility = '';\n const panel = panelRef.current;\n panel?.style.removeProperty('min-width');\n panel?.style.removeProperty('max-width');\n },\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"usePopoverPosition.mjs","names":[],"sources":["../../../../package/common/popover/usePopoverPosition.ts"],"sourcesContent":["import { useId, useRef, type RefObject } from 'react';\nimport { isHiddenByAnyAncestor } from 'components/common/utils/scrollAncestors';\nimport { getVisualViewportRect } from 'components/common/utils/visualViewport';\nimport { useAnchorTracking } from '../hooks/useAnchorTracking';\nimport { PopoverPlacement, PopoverAlign } from './popover.types';\n\ntype Axis = 'top' | 'bottom' | 'left' | 'right';\nconst OPPOSITE: Record<PopoverPlacement, Axis> = {\n [PopoverPlacement.Top]: 'bottom',\n [PopoverPlacement.Bottom]: 'top',\n [PopoverPlacement.Left]: 'right',\n [PopoverPlacement.Right]: 'left',\n};\n\nconst MARGIN = 8; // viewport gutter kept clear when shifting a panel back on-screen\n\n/**\n * Narrower than this and `shrinkToFit` gives up: a panel this size reads worse than\n * one that overhangs. Around four words a line for an error bubble, which is the\n * narrowest caller. A constant rather than an option, because the one caller that\n * shrinks has no reason to want a different floor; give it one the day a second\n * caller disagrees, as `shrinkToFit: boolean | number`.\n */\nconst MIN_SHRINK_WIDTH = 160;\n\n/** How far down to look for a box when the wrappers in between have none. */\nconst MAX_ANCHOR_DEPTH = 3;\n\n/**\n * The element to anchor to. A trigger with a box of its own is that element, which\n * is the case for a caller that hands over a real element (`Tooltip`).\n *\n * Popover's own trigger wrapper generates no box, deliberately, so that the\n * trigger keeps the place it would have had without Popover around it. Then the\n * real trigger is inside it, and this descends to find it, past any further\n * boxless element a consumer wrapped its trigger in: such an element has a zero\n * rect at the origin, which would otherwise send the panel to the top-left corner\n * of the screen.\n */\nfunction resolveAnchor(trigger: HTMLElement): { element: HTMLElement | null; rect: DOMRect } {\n let el: HTMLElement = trigger;\n for (let depth = 0; depth <= MAX_ANCHOR_DEPTH; depth++) {\n const rect = el.getBoundingClientRect();\n if (rect.width > 0 || rect.height > 0) return { element: el, rect };\n const child = el.firstElementChild as HTMLElement | null;\n if (!child) break;\n el = child;\n }\n return { element: null, rect: trigger.getBoundingClientRect() };\n}\n\nlet anchorSupport: boolean | undefined;\n\n// Probed, not just parsed: Firefox 155 resolves `anchor()` without the anchor's transforms, which\n// put a Dropdown in a VirtualList row, placed by `translateY`, above the list.\nexport function supportsAnchoring(): boolean {\n if (anchorSupport !== undefined) return anchorSupport;\n if (typeof CSS === 'undefined' || !CSS.supports?.('top: anchor(top, 0px)')) return (anchorSupport = false);\n const host = document.createElement('div');\n host.style.cssText = 'position: fixed; top: 0; left: 0; transform: translateY(100px); visibility: hidden';\n const anchor = document.createElement('div');\n anchor.style.cssText = 'anchor-name: --popover-probe; width: 1px; height: 1px';\n const probe = document.createElement('div');\n probe.style.cssText =\n 'position: fixed; position-anchor: --popover-probe; top: anchor(top, 0px); left: 0; visibility: hidden';\n host.append(anchor);\n document.body.append(host, probe);\n anchorSupport = Math.abs(probe.getBoundingClientRect().top - anchor.getBoundingClientRect().top) < 1;\n host.remove();\n probe.remove();\n return anchorSupport;\n}\n\nfunction anchorNames(el: HTMLElement): string[] {\n return el.style\n .getPropertyValue('anchor-name')\n .split(',')\n .map((name) => name.trim())\n .filter((name) => name && name !== 'none');\n}\n\n// Merged, not replaced: one trigger can carry a Tooltip and a Popover at once.\nfunction addAnchorName(el: HTMLElement, name: string) {\n const names = anchorNames(el);\n if (!names.includes(name)) el.style.setProperty('anchor-name', [...names, name].join(', '));\n}\n\nfunction removeAnchorName(el: HTMLElement, name: string) {\n const names = anchorNames(el).filter((n) => n !== name);\n if (names.length > 0) el.style.setProperty('anchor-name', names.join(', '));\n else el.style.removeProperty('anchor-name');\n}\n\n// Anchored, the browser moves the panel with the scroll in the same frame; the scroll listener\n// lands a frame or more later. The pixel fallback covers an anchor the browser refuses.\nfunction inset(side: 'top' | 'left', value: number, anchorEdge: number, anchored: boolean): string {\n if (!anchored) return `${value}px`;\n return `calc(anchor(${side}, ${anchorEdge}px) + ${value - anchorEdge}px)`;\n}\n\nexport interface UsePopoverPositionArgs {\n isOpen: boolean;\n placement: PopoverPlacement;\n align: PopoverAlign;\n offset: number;\n triggerRef: RefObject<HTMLElement | null>;\n /** `position: fixed` wrapper that receives top/left/visibility and the resolved-placement data attribute. */\n floatingRef: RefObject<HTMLElement | null>;\n /** Visible panel, measured to decide flip and shift. */\n panelRef: RefObject<HTMLElement | null>;\n /** Arrow element; gets `--arrow-x` / `--arrow-y` written to it. Pass a null ref when `withArrow` is false. */\n arrowRef: RefObject<HTMLElement | null>;\n /**\n * Give the panel at least the trigger's width, for a list that lines up with\n * the field it belongs to. A minimum rather than a fixed width: a menu on a\n * narrow icon button still sizes to its own content.\n */\n matchTriggerWidth?: boolean;\n /**\n * When the panel does not fit on the side its alignment puts it, narrow it\n * rather than sliding it away from the trigger. Sliding keeps the panel\n * on-screen but detaches it: an end-aligned bubble stops ending where its\n * trigger ends, and its arrow stops pointing at anything. Off by default, since\n * a panel whose width is its content, a menu or a list, would rather overhang\n * its trigger than wrap.\n *\n * Only down to `MIN_SHRINK_WIDTH`. Past that the panel is narrower than it is\n * readable, and a trigger that close to the screen edge is better served by a\n * panel that overhangs it.\n */\n shrinkToFit?: boolean;\n}\n\n/**\n * Positions a portaled, `position: fixed` popover on any of the four sides of its trigger with\n * start/center/end cross-axis alignment. Flips to the opposite side when the preferred one has no\n * room, shifts back on-screen along the cross axis, points the arrow at the trigger centre, and\n * hides (not closes) the panel once the trigger scrolls behind a clipping ancestor.\n *\n * `useAnchorTracking` supplies the lifecycle around this: mount wait, ancestor resolution, and\n * re-running on scroll, viewport resize and panel size changes.\n */\nexport function usePopoverPosition({\n isOpen,\n placement,\n align,\n offset,\n triggerRef,\n floatingRef,\n panelRef,\n arrowRef,\n matchTriggerWidth = false,\n shrinkToFit = false,\n}: UsePopoverPositionArgs) {\n const anchorName = `--popover-${useId().replace(/[^\\w-]/g, '')}`;\n const anchorRef = useRef<HTMLElement | null>(null);\n\n function setAnchor(next: HTMLElement | null) {\n if (anchorRef.current === next) return;\n if (anchorRef.current) removeAnchorName(anchorRef.current, anchorName);\n if (next) addAnchorName(next, anchorName);\n anchorRef.current = next;\n }\n\n useAnchorTracking({\n isOpen,\n triggerRef,\n elementRefs: [floatingRef, panelRef],\n // The panel's own size feeds the flip decision and, on the `top`/`left` sides, the coordinate\n // itself — so content that resizes while open has to be re-measured.\n resizeRefs: [panelRef],\n deps: [placement, align, offset, floatingRef, panelRef, arrowRef, matchTriggerWidth, shrinkToFit],\n\n update: (trigger, ancestors) => {\n const floating = floatingRef.current;\n const panel = panelRef.current;\n if (!floating || !panel) return;\n\n const { element: anchorElement, rect: t } = resolveAnchor(trigger);\n const anchored = anchorElement !== null && supportsAnchoring();\n setAnchor(anchored ? anchorElement : null);\n if (anchored) floating.style.setProperty('position-anchor', anchorName);\n else floating.style.removeProperty('position-anchor');\n // Written before the panel is measured, so `pw` below already accounts for it,\n // and taken back off when it is no longer wanted: the panel outlives the\n // switch, and an inline width would outrank whatever it switched to.\n if (matchTriggerWidth) panel.style.minWidth = `${t.width}px`;\n else panel.style.removeProperty('min-width');\n // The wrapper's box, not the panel's: `offsetWidth` rounds to whole pixels, so\n // aligning a panel 165.4px wide against a trigger edge at 406.47 left it 0.4px\n // past that edge. The panel's own rect is fractional but carries the entrance\n // animation's scale; the wrapper is the same layout box without the transform.\n // Cleared before measuring, or a cap left over from a narrower screen would be\n // read back as the panel's own width and never widen again.\n if (shrinkToFit) panel.style.removeProperty('max-width');\n let { width: pw, height: ph } = floating.getBoundingClientRect();\n // The visible area, not the full layout viewport — shrinks on mobile when a keyboard is\n // open, so the panel flips/clamps against what's actually on screen instead of ending up\n // hidden behind the keyboard.\n const vv = getVisualViewportRect();\n\n // Flipping only swaps a side for its opposite, so which axis the alignment runs\n // along is decided by the preferred placement alone, before any of that.\n const alignsHorizontally = placement === PopoverPlacement.Top || placement === PopoverPlacement.Bottom;\n\n if (shrinkToFit && alignsHorizontally) {\n const alignedSide =\n align === PopoverAlign.End\n ? t.right - (vv.left + MARGIN)\n : align === PopoverAlign.Start\n ? vv.left + vv.width - MARGIN - t.left\n : vv.width - 2 * MARGIN;\n const room = Math.min(alignedSide, vv.width - 2 * MARGIN);\n if (pw > room && room >= MIN_SHRINK_WIDTH) {\n panel.style.maxWidth = `${room}px`;\n // Wrapping makes it taller, and the height is what the flip below is decided on.\n ({ width: pw, height: ph } = floating.getBoundingClientRect());\n }\n }\n\n // Resolve the main-axis side, flipping when the preferred side lacks room and the\n // opposite side has more.\n const room: Record<Axis, number> = {\n top: t.top - vv.top,\n bottom: vv.top + vv.height - t.bottom,\n left: t.left - vv.left,\n right: vv.left + vv.width - t.right,\n };\n const need = placement === PopoverPlacement.Top || placement === PopoverPlacement.Bottom ? ph : pw;\n let side: Axis = placement as unknown as Axis;\n const opp = OPPOSITE[placement];\n if (room[side] < need + offset && room[opp] > room[side]) side = opp;\n\n const vertical = side === 'top' || side === 'bottom';\n\n // Main axis: place the panel just outside the trigger on the resolved side.\n let top: number;\n let left: number;\n if (side === 'bottom') {\n top = t.bottom + offset;\n } else if (side === 'top') {\n top = t.top - offset - ph;\n } else if (side === 'right') {\n left = t.right + offset;\n } else {\n left = t.left - offset - pw;\n }\n\n // Cross axis: align start/center/end against the trigger, then shift on-screen.\n let shiftedX = false;\n let shiftedY = false;\n if (vertical) {\n if (align === PopoverAlign.Start) left = t.left;\n else if (align === PopoverAlign.End) left = t.right - pw;\n else left = t.left + t.width / 2 - pw / 2;\n const clamped = Math.max(vv.left + MARGIN, Math.min(left!, vv.left + vv.width - pw - MARGIN));\n shiftedX = clamped !== left!;\n left = clamped;\n } else {\n if (align === PopoverAlign.Start) top = t.top;\n else if (align === PopoverAlign.End) top = t.bottom - ph;\n else top = t.top + t.height / 2 - ph / 2;\n const clamped = Math.max(vv.top + MARGIN, Math.min(top!, vv.top + vv.height - ph - MARGIN));\n shiftedY = clamped !== top!;\n top = clamped;\n }\n\n // A shifted axis is pinned to the viewport edge, so it stays in pixels: anchored, the scroll\n // would carry it off the edge until the next update pulled it back, which reads as shaking.\n floating.style.top = inset('top', top!, t.top, anchored && !shiftedY);\n floating.style.left = inset('left', left!, t.left, anchored && !shiftedX);\n floating.style.visibility = isHiddenByAnyAncestor(t, ancestors) ? 'hidden' : 'visible';\n floating.dataset.placement = side;\n\n // Arrow: centre it on the trigger along the cross axis, clamped inside the panel edges.\n const arrow = arrowRef.current;\n if (arrow) {\n const half = arrow.offsetWidth / 2 || 5;\n if (vertical) {\n const x = t.left + t.width / 2 - left!;\n arrow.style.setProperty('--arrow-x', `${Math.max(half + 2, Math.min(x, pw - half - 2))}px`);\n arrow.style.removeProperty('--arrow-y');\n } else {\n const y = t.top + t.height / 2 - top!;\n arrow.style.setProperty('--arrow-y', `${Math.max(half + 2, Math.min(y, ph - half - 2))}px`);\n arrow.style.removeProperty('--arrow-x');\n }\n }\n },\n\n teardown: () => {\n // Positioning is written as inline styles, which outrank any class the panel might\n // switch to while it stays mounted — a popover turning into a bottom sheet (`useSheet`)\n // would keep the coordinates computed for its anchored self.\n setAnchor(null);\n const floating = floatingRef.current;\n if (!floating) return;\n floating.style.top = '';\n floating.style.left = '';\n floating.style.visibility = '';\n floating.style.removeProperty('position-anchor');\n const panel = panelRef.current;\n panel?.style.removeProperty('min-width');\n panel?.style.removeProperty('max-width');\n },\n });\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- "use client";import{stickyStack as e}from"./stickyStack.mjs";import t from"./sticky.module.mjs";import{useCallback as n,useEffect as r,useLayoutEffect as i,useRef as a,useState as o}from"react";import s from"classnames";import{jsx as c,jsxs as l}from"react/jsx-runtime";function u(e){let t=e?.parentElement??null;for(;t&&t!==document.body;){let{overflowY:e}=getComputedStyle(t);if(e===`auto`||e===`scroll`||e===`overlay`)return t;t=t.parentElement}return null}function d({children:d,offsetTop:f=0,offsetElement:p,stickToScrollParent:m=!1,zIndex:h=20,disabled:g=!1,onStuckChange:_,className:v,style:y,classNames:b,styles:x,...S}){let C=a(null),w=a(null),T=a(null),E=a(null),[D,O]=o({width:0,left:0,height:0}),[k,A]=o(null),[j,M]=o(0),N=p?.current?k??p.current.offsetHeight:f,[P,F]=o({isStuck:!1,top:N}),I=a(_);r(()=>{I.current=_},[_]);let L=n(()=>{let t=C.current,n=w.current;if(!t||!n)return;let r=t.getBoundingClientRect();O({width:t.clientWidth,left:r.left,height:n.offsetHeight}),e.schedule()},[]);r(()=>{let e=p?.current;if(!e)return;let t=()=>A(e.offsetHeight);t();let n=new ResizeObserver(t);return n.observe(e),()=>n.disconnect()},[p]),i(()=>{E.current=m?u(C.current):null},[m]),i(()=>{let t=e.register({node:C.current,offsetTop:N,disabled:g,getMetrics:()=>{let e=C.current,t=w.current;if(!e||!t)return null;let n=e.getBoundingClientRect(),r=E.current?.getBoundingClientRect(),i=r?.top??0;return M(e=>e===i?e:i),{top:r?n.top-r.top:n.top,left:r?n.left-r.left:n.left,width:e.clientWidth,contentHeight:t.offsetHeight,container:E.current}}},F);return T.current=t,()=>{e.unregister(t),T.current=null}},[]),r(()=>{T.current&&e.updateEntry(T.current,{offsetTop:N,disabled:g}),e.schedule()},[N,g]),i(()=>{L()},[L,d]),r(()=>{let e=new ResizeObserver(L);return C.current&&e.observe(C.current),w.current&&e.observe(w.current),E.current&&e.observe(E.current),()=>e.disconnect()},[L,m]);let R=a(!0);r(()=>{if(R.current){R.current=!1;return}I.current?.(P.isStuck)},[P.isStuck]);let z=P.isStuck?{position:`fixed`,top:j+P.top,left:D.left,width:D.width,zIndex:h,...x?.content}:x?.content;return l(`div`,{ref:C,className:s(t.sticky,v,b?.root),style:{...x?.root,...y},...S,children:[P.isStuck&&c(`div`,{"aria-hidden":!0,className:s(t.stickyPlaceholder,b?.placeholder),style:{height:D.height,...x?.placeholder}}),c(`div`,{ref:w,className:s(t.stickyContent,P.isStuck&&t.stickyContentStuck,b?.content),style:z,children:d})]})}export{d as default};
1
+ "use client";import{getScrollParent as e}from"../utils/scrollAncestors.mjs";import{stickyStack as t}from"./stickyStack.mjs";import n from"./sticky.module.mjs";import{useCallback as r,useEffect as i,useLayoutEffect as a,useRef as o,useState as s}from"react";import c from"classnames";import{jsx as l,jsxs as u}from"react/jsx-runtime";function d({children:d,offsetTop:f=0,offsetElement:p,stickToScrollParent:m=!1,boundary:h,boundaryInset:g=0,zIndex:_=20,disabled:v=!1,onStuckChange:y,className:b,style:x,classNames:S,styles:C,...w}){let T=o(null),E=o(null),D=o(null),O=o(null),[k,A]=s({width:0,left:0,height:0}),[j,M]=s(null),[N,P]=s(0),F=p?.current?j??p.current.offsetHeight:f,[I,L]=s({isStuck:!1,top:F}),R=o(y);i(()=>{R.current=y},[y]);let z=o(h),B=o(g);a(()=>{z.current=h,B.current=g});let V=r(()=>{let e=T.current,n=E.current;if(!e||!n)return;let r=e.getBoundingClientRect();A({width:e.clientWidth,left:r.left,height:n.offsetHeight}),t.schedule()},[]);i(()=>{let e=p?.current;if(!e)return;let t=()=>M(e.offsetHeight);t();let n=new ResizeObserver(t);return n.observe(e),()=>n.disconnect()},[p]),a(()=>{O.current=m?e(T.current):null},[m]),a(()=>{let e=t.register({node:T.current,offsetTop:F,disabled:v,getMetrics:()=>{let e=T.current,t=E.current;if(!e||!t)return null;let n=e.getBoundingClientRect(),r=O.current?.getBoundingClientRect(),i=r?.top??0;P(e=>e===i?e:i);let a=r?n.top-r.top:n.top,o=r?n.left-r.left:n.left,s=z.current?.current??null,c=s?.getBoundingClientRect();return{top:a,left:o,width:e.clientWidth,contentHeight:t.offsetHeight,container:O.current,boundary:s,boundaryBottom:c?c.bottom-i-B.current:null}}},L);return D.current=e,()=>{t.unregister(e),D.current=null}},[]),i(()=>{D.current&&t.updateEntry(D.current,{offsetTop:F,disabled:v}),t.schedule()},[F,v]),a(()=>{V()},[V,d]),i(()=>{let e=new ResizeObserver(V);return T.current&&e.observe(T.current),E.current&&e.observe(E.current),O.current&&e.observe(O.current),()=>e.disconnect()},[V,m]);let H=o(!0);i(()=>{if(H.current){H.current=!1;return}R.current?.(I.isStuck)},[I.isStuck]);let U=I.isStuck?{position:`fixed`,top:N+I.top,left:k.left,width:k.width,zIndex:_,...C?.content}:C?.content;return u(`div`,{ref:T,className:c(n.sticky,b,S?.root),style:{...C?.root,...x},...w,children:[I.isStuck&&l(`div`,{"aria-hidden":!0,className:c(n.stickyPlaceholder,S?.placeholder),style:{height:k.height,...C?.placeholder}}),l(`div`,{ref:E,className:c(n.stickyContent,I.isStuck&&n.stickyContentStuck,S?.content),style:U,children:d})]})}export{d as default};
2
2
  //# sourceMappingURL=sticky.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"sticky.mjs","names":[],"sources":["../../../../package/common/sticky/sticky.tsx"],"sourcesContent":["import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';\nimport cx from 'classnames';\nimport type { StickyProps } from './sticky.types';\nimport { stickyStack } from './stickyStack';\nimport type { StickyStackResult } from './stickyStack';\n\nimport styles from './sticky.module.pcss';\n\ninterface Metrics {\n width: number;\n left: number;\n height: number;\n}\n\n// Nearest ancestor whose own scrollbar would carry the sticky's scroll — used so\n// `stickToScrollParent` can pin relative to that container's box instead of the viewport.\nfunction getScrollParent(el: HTMLElement | null): HTMLElement | null {\n let node = el?.parentElement ?? null;\n while (node && node !== document.body) {\n const { overflowY } = getComputedStyle(node);\n if (overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay') return node;\n node = node.parentElement;\n }\n return null;\n}\n\nexport default function Sticky({\n children,\n offsetTop = 0,\n offsetElement,\n stickToScrollParent = false,\n zIndex = 20,\n disabled = false,\n onStuckChange,\n className,\n style,\n classNames,\n styles: slotStyles,\n ...rest\n}: StickyProps) {\n const rootRef = useRef<HTMLDivElement>(null);\n const contentRef = useRef<HTMLDivElement>(null);\n const stackIdRef = useRef<symbol | null>(null);\n const scrollParentRef = useRef<HTMLElement | null>(null);\n const [metrics, setMetrics] = useState<Metrics>({ width: 0, left: 0, height: 0 });\n const [measuredOffset, setMeasuredOffset] = useState<number | null>(null);\n const [scrollParentTop, setScrollParentTop] = useState(0);\n const effectiveOffsetTop = offsetElement?.current\n ? (measuredOffset ?? offsetElement.current.offsetHeight)\n : offsetTop;\n const [stackResult, setStackResult] = useState<StickyStackResult>({ isStuck: false, top: effectiveOffsetTop });\n\n const onStuckChangeRef = useRef(onStuckChange);\n useEffect(() => {\n onStuckChangeRef.current = onStuckChange;\n }, [onStuckChange]);\n\n // Local geometry (width/left/height) doesn't depend on other Stickies — only the\n // effective `top` and `isStuck` (from the stack coordinator) do.\n const measure = useCallback(() => {\n const root = rootRef.current;\n const content = contentRef.current;\n if (!root || !content) return;\n const rootRect = root.getBoundingClientRect();\n setMetrics({ width: root.clientWidth, left: rootRect.left, height: content.offsetHeight });\n stickyStack.schedule();\n }, []);\n\n useEffect(() => {\n const el = offsetElement?.current;\n if (!el) return;\n const updateFromElement = () => setMeasuredOffset(el.offsetHeight);\n updateFromElement();\n const ro = new ResizeObserver(updateFromElement);\n ro.observe(el);\n return () => ro.disconnect();\n }, [offsetElement]);\n\n useLayoutEffect(() => {\n scrollParentRef.current = stickToScrollParent ? getScrollParent(rootRef.current) : null;\n }, [stickToScrollParent]);\n\n useLayoutEffect(() => {\n const id = stickyStack.register(\n {\n node: rootRef.current as HTMLElement,\n offsetTop: effectiveOffsetTop,\n disabled,\n getMetrics: () => {\n const root = rootRef.current;\n const content = contentRef.current;\n if (!root || !content) return null;\n const rect = root.getBoundingClientRect();\n const parentRect = scrollParentRef.current?.getBoundingClientRect();\n const newParentTop = parentRect?.top ?? 0;\n setScrollParentTop((prev) => (prev === newParentTop ? prev : newParentTop));\n // Report top/left relative to the scroll parent's box so the stack's stuck/stacking\n // math stays in the same coordinate space regardless of what it's pinned relative to.\n const top = parentRect ? rect.top - parentRect.top : rect.top;\n const left = parentRect ? rect.left - parentRect.left : rect.left;\n return {\n top,\n left,\n width: root.clientWidth,\n contentHeight: content.offsetHeight,\n container: scrollParentRef.current,\n };\n },\n },\n setStackResult,\n );\n stackIdRef.current = id;\n return () => {\n stickyStack.unregister(id);\n stackIdRef.current = null;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n if (stackIdRef.current) {\n stickyStack.updateEntry(stackIdRef.current, { offsetTop: effectiveOffsetTop, disabled });\n }\n stickyStack.schedule();\n }, [effectiveOffsetTop, disabled]);\n\n useLayoutEffect(() => {\n measure();\n }, [measure, children]);\n\n useEffect(() => {\n const ro = new ResizeObserver(measure);\n if (rootRef.current) ro.observe(rootRef.current);\n if (contentRef.current) ro.observe(contentRef.current);\n if (scrollParentRef.current) ro.observe(scrollParentRef.current);\n return () => ro.disconnect();\n }, [measure, stickToScrollParent]);\n\n const firstRun = useRef(true);\n useEffect(() => {\n if (firstRun.current) {\n firstRun.current = false;\n return;\n }\n onStuckChangeRef.current?.(stackResult.isStuck);\n }, [stackResult.isStuck]);\n\n // stackResult.top is relative to the scroll parent's box (or the viewport if there isn't\n // one) — convert back to a viewport-absolute value for the fixed `top`.\n const contentStyle: React.CSSProperties | undefined = stackResult.isStuck\n ? {\n position: 'fixed',\n top: scrollParentTop + stackResult.top,\n left: metrics.left,\n width: metrics.width,\n zIndex,\n ...slotStyles?.content,\n }\n : slotStyles?.content;\n\n return (\n <div\n ref={rootRef}\n className={cx(styles.sticky, className, classNames?.root)}\n style={{ ...slotStyles?.root, ...style }}\n {...rest}\n >\n {stackResult.isStuck && (\n <div\n aria-hidden\n className={cx(styles.stickyPlaceholder, classNames?.placeholder)}\n style={{ height: metrics.height, ...slotStyles?.placeholder }}\n />\n )}\n <div\n ref={contentRef}\n className={cx(styles.stickyContent, stackResult.isStuck && styles.stickyContentStuck, classNames?.content)}\n style={contentStyle}\n >\n {children}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sticky.mjs","names":[],"sources":["../../../../package/common/sticky/sticky.tsx"],"sourcesContent":["import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';\nimport cx from 'classnames';\nimport type { StickyProps } from './sticky.types';\nimport { stickyStack } from './stickyStack';\nimport type { StickyStackResult } from './stickyStack';\nimport { getScrollParent } from '../utils/scrollAncestors';\n\nimport styles from './sticky.module.pcss';\n\ninterface Metrics {\n width: number;\n left: number;\n height: number;\n}\n\nexport default function Sticky({\n children,\n offsetTop = 0,\n offsetElement,\n stickToScrollParent = false,\n boundary,\n boundaryInset = 0,\n zIndex = 20,\n disabled = false,\n onStuckChange,\n className,\n style,\n classNames,\n styles: slotStyles,\n ...rest\n}: StickyProps) {\n const rootRef = useRef<HTMLDivElement>(null);\n const contentRef = useRef<HTMLDivElement>(null);\n const stackIdRef = useRef<symbol | null>(null);\n const scrollParentRef = useRef<HTMLElement | null>(null);\n const [metrics, setMetrics] = useState<Metrics>({ width: 0, left: 0, height: 0 });\n const [measuredOffset, setMeasuredOffset] = useState<number | null>(null);\n const [scrollParentTop, setScrollParentTop] = useState(0);\n const effectiveOffsetTop = offsetElement?.current\n ? (measuredOffset ?? offsetElement.current.offsetHeight)\n : offsetTop;\n const [stackResult, setStackResult] = useState<StickyStackResult>({ isStuck: false, top: effectiveOffsetTop });\n\n const onStuckChangeRef = useRef(onStuckChange);\n useEffect(() => {\n onStuckChangeRef.current = onStuckChange;\n }, [onStuckChange]);\n\n // The stack entry is registered once, so the boundary reaches it through a ref\n // rather than the closure: the one handed in may still be empty on first render.\n const boundaryRef = useRef(boundary);\n const boundaryInsetRef = useRef(boundaryInset);\n useLayoutEffect(() => {\n boundaryRef.current = boundary;\n boundaryInsetRef.current = boundaryInset;\n });\n\n // Local geometry (width/left/height) doesn't depend on other Stickies — only the\n // effective `top` and `isStuck` (from the stack coordinator) do.\n const measure = useCallback(() => {\n const root = rootRef.current;\n const content = contentRef.current;\n if (!root || !content) return;\n const rootRect = root.getBoundingClientRect();\n setMetrics({ width: root.clientWidth, left: rootRect.left, height: content.offsetHeight });\n stickyStack.schedule();\n }, []);\n\n useEffect(() => {\n const el = offsetElement?.current;\n if (!el) return;\n const updateFromElement = () => setMeasuredOffset(el.offsetHeight);\n updateFromElement();\n const ro = new ResizeObserver(updateFromElement);\n ro.observe(el);\n return () => ro.disconnect();\n }, [offsetElement]);\n\n useLayoutEffect(() => {\n scrollParentRef.current = stickToScrollParent ? getScrollParent(rootRef.current) : null;\n }, [stickToScrollParent]);\n\n useLayoutEffect(() => {\n const id = stickyStack.register(\n {\n node: rootRef.current as HTMLElement,\n offsetTop: effectiveOffsetTop,\n disabled,\n getMetrics: () => {\n const root = rootRef.current;\n const content = contentRef.current;\n if (!root || !content) return null;\n const rect = root.getBoundingClientRect();\n const parentRect = scrollParentRef.current?.getBoundingClientRect();\n const newParentTop = parentRect?.top ?? 0;\n setScrollParentTop((prev) => (prev === newParentTop ? prev : newParentTop));\n // Report top/left relative to the scroll parent's box so the stack's stuck/stacking\n // math stays in the same coordinate space regardless of what it's pinned relative to.\n const top = parentRect ? rect.top - parentRect.top : rect.top;\n const left = parentRect ? rect.left - parentRect.left : rect.left;\n const boundaryEl = boundaryRef.current?.current ?? null;\n const boundaryRect = boundaryEl?.getBoundingClientRect();\n return {\n top,\n left,\n width: root.clientWidth,\n contentHeight: content.offsetHeight,\n container: scrollParentRef.current,\n boundary: boundaryEl,\n boundaryBottom: boundaryRect ? boundaryRect.bottom - newParentTop - boundaryInsetRef.current : null,\n };\n },\n },\n setStackResult,\n );\n stackIdRef.current = id;\n return () => {\n stickyStack.unregister(id);\n stackIdRef.current = null;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n if (stackIdRef.current) {\n stickyStack.updateEntry(stackIdRef.current, { offsetTop: effectiveOffsetTop, disabled });\n }\n stickyStack.schedule();\n }, [effectiveOffsetTop, disabled]);\n\n useLayoutEffect(() => {\n measure();\n }, [measure, children]);\n\n useEffect(() => {\n const ro = new ResizeObserver(measure);\n if (rootRef.current) ro.observe(rootRef.current);\n if (contentRef.current) ro.observe(contentRef.current);\n if (scrollParentRef.current) ro.observe(scrollParentRef.current);\n return () => ro.disconnect();\n }, [measure, stickToScrollParent]);\n\n const firstRun = useRef(true);\n useEffect(() => {\n if (firstRun.current) {\n firstRun.current = false;\n return;\n }\n onStuckChangeRef.current?.(stackResult.isStuck);\n }, [stackResult.isStuck]);\n\n // stackResult.top is relative to the scroll parent's box (or the viewport if there isn't\n // one) — convert back to a viewport-absolute value for the fixed `top`.\n const contentStyle: React.CSSProperties | undefined = stackResult.isStuck\n ? {\n position: 'fixed',\n top: scrollParentTop + stackResult.top,\n left: metrics.left,\n width: metrics.width,\n zIndex,\n ...slotStyles?.content,\n }\n : slotStyles?.content;\n\n return (\n <div\n ref={rootRef}\n className={cx(styles.sticky, className, classNames?.root)}\n style={{ ...slotStyles?.root, ...style }}\n {...rest}\n >\n {stackResult.isStuck && (\n <div\n aria-hidden\n className={cx(styles.stickyPlaceholder, classNames?.placeholder)}\n style={{ height: metrics.height, ...slotStyles?.placeholder }}\n />\n )}\n <div\n ref={contentRef}\n className={cx(styles.stickyContent, stackResult.isStuck && styles.stickyContentStuck, classNames?.content)}\n style={contentStyle}\n >\n {children}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- "use client";function e(e,t,n,r){return e<r&&n<t}function t(e,t){if(e===t)return 0;let n=e.compareDocumentPosition(t);return n&Node.DOCUMENT_POSITION_FOLLOWING?-1:n&Node.DOCUMENT_POSITION_PRECEDING?1:0}var n=new class{constructor(){this.entries=new Map,this.listeners=new Map,this.results=new Map,this.rafId=0,this.listenerCount=0,this.schedule=()=>{cancelAnimationFrame(this.rafId),this.rafId=requestAnimationFrame(this.recompute)},this.recompute=()=>{let n=[...this.entries.entries()].sort(([,e],[,n])=>t(e.node,n.node)),r=new Map;for(let[t,i]of n){let n=i.getMetrics();if(!n)continue;let a=r.get(n.container);a||(a=[],r.set(n.container,a));let o=a.filter(t=>e(t.left,t.right,n.left,n.left+n.width)).reduce((e,t)=>Math.max(e,t.bottom),i.offsetTop),s=!i.disabled&&n.top<=o,c={isStuck:s,top:s?o:i.offsetTop};s&&a.push({left:n.left,right:n.left+n.width,bottom:o+n.contentHeight});let l=this.results.get(t);(!l||l.isStuck!==c.isStuck||l.top!==c.top)&&(this.results.set(t,c),this.listeners.get(t)?.(c))}}}register(e,t){let n=Symbol(`sticky`);return this.entries.set(n,e),this.listeners.set(n,t),this.startListening(),n}updateEntry(e,t){let n=this.entries.get(e);n&&Object.assign(n,t)}unregister(e){this.entries.has(e)&&(this.entries.delete(e),this.listeners.delete(e),this.results.delete(e),this.stopListening())}startListening(){this.listenerCount===0&&(window.addEventListener(`scroll`,this.schedule,{passive:!0,capture:!0}),window.addEventListener(`resize`,this.schedule)),this.listenerCount++}stopListening(){this.listenerCount--,this.listenerCount===0&&(window.removeEventListener(`scroll`,this.schedule,{capture:!0}),window.removeEventListener(`resize`,this.schedule))}};export{n as stickyStack};
1
+ "use client";function e(e,t,n,r){return e<r&&n<t}function t(e,t){if(e===t)return 0;let n=e.compareDocumentPosition(t);return n&Node.DOCUMENT_POSITION_FOLLOWING?-1:n&Node.DOCUMENT_POSITION_PRECEDING?1:0}var n=new class{constructor(){this.entries=new Map,this.listeners=new Map,this.results=new Map,this.rafId=0,this.listenerCount=0,this.schedule=()=>{cancelAnimationFrame(this.rafId),this.rafId=requestAnimationFrame(this.recompute)},this.recompute=()=>{let n=[...this.entries.entries()].sort(([,e],[,n])=>t(e.node,n.node)),r=new Map,i=[];for(let[t,a]of n){let n=a.getMetrics();if(!n)continue;let o=r.get(n.container);o||(o=[],r.set(n.container,o));let s=o.filter(t=>e(t.left,t.right,n.left,n.left+n.width)).reduce((e,t)=>Math.max(e,t.bottom),a.offsetTop),c=!a.disabled&&n.top<=s;if(c){let e=s+n.contentHeight;o.push({left:n.left,right:n.left+n.width,bottom:n.boundaryBottom==null?e:Math.min(e,n.boundaryBottom)})}i.push({id:t,metrics:n,baseTop:c?s:a.offsetTop,isStuck:c})}let a=new Map;for(let{metrics:e,baseTop:t,isStuck:n}of i){let{boundary:r,boundaryBottom:i,contentHeight:o}=e;if(!n||r==null||i==null)continue;let s=Math.min(0,i-(t+o)),c=a.get(r);(c==null||s<c)&&a.set(r,s)}for(let{id:e,metrics:t,baseTop:n,isStuck:r}of i){let i={isStuck:r,top:n+(r&&t.boundary?a.get(t.boundary)??0:0)},o=this.results.get(e);o&&o.isStuck===i.isStuck&&o.top===i.top||(this.results.set(e,i),this.listeners.get(e)?.(i))}}}register(e,t){let n=Symbol(`sticky`);return this.entries.set(n,e),this.listeners.set(n,t),this.startListening(),n}updateEntry(e,t){let n=this.entries.get(e);n&&Object.assign(n,t)}unregister(e){this.entries.has(e)&&(this.entries.delete(e),this.listeners.delete(e),this.results.delete(e),this.stopListening())}startListening(){this.listenerCount===0&&(window.addEventListener(`scroll`,this.schedule,{passive:!0,capture:!0}),window.addEventListener(`resize`,this.schedule)),this.listenerCount++}stopListening(){this.listenerCount--,this.listenerCount===0&&(window.removeEventListener(`scroll`,this.schedule,{capture:!0}),window.removeEventListener(`resize`,this.schedule))}};export{n as stickyStack};
2
2
  //# sourceMappingURL=stickyStack.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"stickyStack.mjs","names":[],"sources":["../../../../package/common/sticky/stickyStack.ts"],"sourcesContent":["/**\n * Coordinates every mounted `Sticky` so ones that overlap horizontally stack\n * underneath each other (in DOM order) instead of pinning to the same spot.\n * Non-overlapping ones (e.g. side-by-side halves under a full-width one) land\n * at the same top instead of being pushed down by each other.\n */\n\nexport interface StickyStackMetrics {\n /** Current (unstuck-flow) top of the root element, from getBoundingClientRect. */\n top: number;\n left: number;\n width: number;\n contentHeight: number;\n /** The element this entry sticks relative to (its scroll parent), or null for the viewport.\n * Stacking only considers overlap between entries sharing the same container — two Stickies\n * in unrelated scroll panels should never push each other down just because they happen to\n * share horizontal DOM coordinates. */\n container: HTMLElement | null;\n}\n\nexport interface StickyStackEntry {\n node: HTMLElement;\n offsetTop: number;\n disabled: boolean;\n getMetrics: () => StickyStackMetrics | null;\n}\n\nexport interface StickyStackResult {\n isStuck: boolean;\n /** Effective `top` to pin at while stuck — offsetTop plus the height of any stuck, overlapping entries above it. */\n top: number;\n}\n\ntype ResultListener = (result: StickyStackResult) => void;\n\nfunction rangesOverlap(aLeft: number, aRight: number, bLeft: number, bRight: number): boolean {\n return aLeft < bRight && bLeft < aRight;\n}\n\nfunction compareDomOrder(a: HTMLElement, b: HTMLElement): number {\n if (a === b) return 0;\n const position = a.compareDocumentPosition(b);\n if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1;\n if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1;\n return 0;\n}\n\nclass StickyStack {\n private entries = new Map<symbol, StickyStackEntry>();\n private listeners = new Map<symbol, ResultListener>();\n private results = new Map<symbol, StickyStackResult>();\n private rafId = 0;\n private listenerCount = 0;\n\n schedule = (): void => {\n cancelAnimationFrame(this.rafId);\n this.rafId = requestAnimationFrame(this.recompute);\n };\n\n register(entry: StickyStackEntry, onChange: ResultListener): symbol {\n const id = Symbol('sticky');\n this.entries.set(id, entry);\n this.listeners.set(id, onChange);\n this.startListening();\n return id;\n }\n\n updateEntry(id: symbol, patch: Partial<Pick<StickyStackEntry, 'offsetTop' | 'disabled'>>): void {\n const entry = this.entries.get(id);\n if (entry) Object.assign(entry, patch);\n }\n\n unregister(id: symbol): void {\n if (!this.entries.has(id)) return;\n this.entries.delete(id);\n this.listeners.delete(id);\n this.results.delete(id);\n this.stopListening();\n }\n\n recompute = (): void => {\n const ordered = [...this.entries.entries()].sort(([, a], [, b]) => compareDomOrder(a.node, b.node));\n const placedByContainer = new Map<HTMLElement | null, { left: number; right: number; bottom: number }[]>();\n\n for (const [id, entry] of ordered) {\n const metrics = entry.getMetrics();\n if (!metrics) continue;\n\n let placed = placedByContainer.get(metrics.container);\n if (!placed) {\n placed = [];\n placedByContainer.set(metrics.container, placed);\n }\n\n const overlapping = placed.filter((p) =>\n rangesOverlap(p.left, p.right, metrics.left, metrics.left + metrics.width),\n );\n const baseTop = overlapping.reduce((max, p) => Math.max(max, p.bottom), entry.offsetTop);\n const isStuck = !entry.disabled && metrics.top <= baseTop;\n const result: StickyStackResult = { isStuck, top: isStuck ? baseTop : entry.offsetTop };\n\n if (isStuck) {\n placed.push({\n left: metrics.left,\n right: metrics.left + metrics.width,\n bottom: baseTop + metrics.contentHeight,\n });\n }\n\n const prev = this.results.get(id);\n if (!prev || prev.isStuck !== result.isStuck || prev.top !== result.top) {\n this.results.set(id, result);\n this.listeners.get(id)?.(result);\n }\n }\n };\n\n private startListening(): void {\n if (this.listenerCount === 0) {\n window.addEventListener('scroll', this.schedule, { passive: true, capture: true });\n window.addEventListener('resize', this.schedule);\n }\n this.listenerCount++;\n }\n\n private stopListening(): void {\n this.listenerCount--;\n if (this.listenerCount === 0) {\n window.removeEventListener('scroll', this.schedule, { capture: true } as EventListenerOptions);\n window.removeEventListener('resize', this.schedule);\n }\n }\n}\n\nexport const stickyStack = new StickyStack();\n"],"mappings":"aAmCA,SAAS,EAAc,EAAe,EAAgB,EAAe,EAAyB,CAC5F,OAAO,EAAQ,GAAU,EAAQ,CACnC,CAEA,SAAS,EAAgB,EAAgB,EAAwB,CAC/D,GAAI,IAAM,EAAG,MAAO,GACpB,IAAM,EAAW,EAAE,wBAAwB,CAAC,EAG5C,OAFI,EAAW,KAAK,4BAAoC,GACpD,EAAW,KAAK,4BAAoC,EACjD,CACT,CAyFA,IAAa,EAAc,IAAI,KAvFb,eACE,KAAA,QAAA,IAAI,IACF,KAAA,UAAA,IAAI,IACN,KAAA,QAAA,IAAI,IACN,KAAA,MAAA,EACQ,KAAA,cAAA,EAED,KAAA,aAAA,CACrB,qBAAqB,KAAK,KAAK,EAC/B,KAAK,MAAQ,sBAAsB,KAAK,SAAS,CACnD,EAuBwB,KAAA,cAAA,CACtB,IAAM,EAAU,CAAC,GAAG,KAAK,QAAQ,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAG,GAAI,EAAG,KAAO,EAAgB,EAAE,KAAM,EAAE,IAAI,CAAC,EAC5F,EAAoB,IAAI,IAE9B,IAAK,GAAM,CAAC,EAAI,KAAU,EAAS,CACjC,IAAM,EAAU,EAAM,WAAW,EACjC,GAAI,CAAC,EAAS,SAEd,IAAI,EAAS,EAAkB,IAAI,EAAQ,SAAS,EAC/C,IACH,EAAS,CAAC,EACV,EAAkB,IAAI,EAAQ,UAAW,CAAM,GAMjD,IAAM,EAHc,EAAO,OAAQ,GACjC,EAAc,EAAE,KAAM,EAAE,MAAO,EAAQ,KAAM,EAAQ,KAAO,EAAQ,KAAK,CAE3D,CAAA,CAAY,QAAQ,EAAK,IAAM,KAAK,IAAI,EAAK,EAAE,MAAM,EAAG,EAAM,SAAS,EACjF,EAAU,CAAC,EAAM,UAAY,EAAQ,KAAO,EAC5C,EAA4B,CAAE,UAAS,IAAK,EAAU,EAAU,EAAM,SAAU,EAElF,GACF,EAAO,KAAK,CACV,KAAM,EAAQ,KACd,MAAO,EAAQ,KAAO,EAAQ,MAC9B,OAAQ,EAAU,EAAQ,aAC5B,CAAC,EAGH,IAAM,EAAO,KAAK,QAAQ,IAAI,CAAE,GAC5B,CAAC,GAAQ,EAAK,UAAY,EAAO,SAAW,EAAK,MAAQ,EAAO,OAClE,KAAK,QAAQ,IAAI,EAAI,CAAM,EAC3B,KAAK,UAAU,IAAI,CAAE,CAAC,GAAG,CAAM,EAEnC,CACF,EAxDA,SAAS,EAAyB,EAAkC,CAClE,IAAM,EAAK,OAAO,QAAQ,EAI1B,OAHA,KAAK,QAAQ,IAAI,EAAI,CAAK,EAC1B,KAAK,UAAU,IAAI,EAAI,CAAQ,EAC/B,KAAK,eAAe,EACb,CACT,CAEA,YAAY,EAAY,EAAwE,CAC9F,IAAM,EAAQ,KAAK,QAAQ,IAAI,CAAE,EAC7B,GAAO,OAAO,OAAO,EAAO,CAAK,CACvC,CAEA,WAAW,EAAkB,CACtB,KAAK,QAAQ,IAAI,CAAE,IACxB,KAAK,QAAQ,OAAO,CAAE,EACtB,KAAK,UAAU,OAAO,CAAE,EACxB,KAAK,QAAQ,OAAO,CAAE,EACtB,KAAK,cAAc,EACrB,CAuCA,gBAA+B,CACzB,KAAK,gBAAkB,IACzB,OAAO,iBAAiB,SAAU,KAAK,SAAU,CAAE,QAAS,GAAM,QAAS,EAAK,CAAC,EACjF,OAAO,iBAAiB,SAAU,KAAK,QAAQ,GAEjD,KAAK,eACP,CAEA,eAA8B,CAC5B,KAAK,gBACD,KAAK,gBAAkB,IACzB,OAAO,oBAAoB,SAAU,KAAK,SAAU,CAAE,QAAS,EAAK,CAAyB,EAC7F,OAAO,oBAAoB,SAAU,KAAK,QAAQ,EAEtD,CACF"}
1
+ {"version":3,"file":"stickyStack.mjs","names":[],"sources":["../../../../package/common/sticky/stickyStack.ts"],"sourcesContent":["/**\n * Coordinates every mounted `Sticky` so ones that overlap horizontally stack\n * underneath each other (in DOM order) instead of pinning to the same spot.\n * Non-overlapping ones (e.g. side-by-side halves under a full-width one) land\n * at the same top instead of being pushed down by each other.\n */\n\nexport interface StickyStackMetrics {\n /** Current (unstuck-flow) top of the root element, from getBoundingClientRect. */\n top: number;\n left: number;\n width: number;\n contentHeight: number;\n /** The element this entry sticks relative to (its scroll parent), or null for the viewport.\n * Stacking only considers overlap between entries sharing the same container — two Stickies\n * in unrelated scroll panels should never push each other down just because they happen to\n * share horizontal DOM coordinates. */\n container: HTMLElement | null;\n /** The element the entry is confined to, or null when nothing confines it.\n * Entries sharing one leave together. */\n boundary: HTMLElement | null;\n /** Bottom edge of that element (less its inset), in the same coordinate space as `top`. */\n boundaryBottom: number | null;\n}\n\nexport interface StickyStackEntry {\n node: HTMLElement;\n offsetTop: number;\n disabled: boolean;\n getMetrics: () => StickyStackMetrics | null;\n}\n\nexport interface StickyStackResult {\n isStuck: boolean;\n /** Effective `top` to pin at while stuck — offsetTop plus the height of any stuck, overlapping entries above it. */\n top: number;\n}\n\ntype ResultListener = (result: StickyStackResult) => void;\n\nfunction rangesOverlap(aLeft: number, aRight: number, bLeft: number, bRight: number): boolean {\n return aLeft < bRight && bLeft < aRight;\n}\n\nfunction compareDomOrder(a: HTMLElement, b: HTMLElement): number {\n if (a === b) return 0;\n const position = a.compareDocumentPosition(b);\n if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1;\n if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1;\n return 0;\n}\n\nclass StickyStack {\n private entries = new Map<symbol, StickyStackEntry>();\n private listeners = new Map<symbol, ResultListener>();\n private results = new Map<symbol, StickyStackResult>();\n private rafId = 0;\n private listenerCount = 0;\n\n schedule = (): void => {\n cancelAnimationFrame(this.rafId);\n this.rafId = requestAnimationFrame(this.recompute);\n };\n\n register(entry: StickyStackEntry, onChange: ResultListener): symbol {\n const id = Symbol('sticky');\n this.entries.set(id, entry);\n this.listeners.set(id, onChange);\n this.startListening();\n return id;\n }\n\n updateEntry(id: symbol, patch: Partial<Pick<StickyStackEntry, 'offsetTop' | 'disabled'>>): void {\n const entry = this.entries.get(id);\n if (entry) Object.assign(entry, patch);\n }\n\n unregister(id: symbol): void {\n if (!this.entries.has(id)) return;\n this.entries.delete(id);\n this.listeners.delete(id);\n this.results.delete(id);\n this.stopListening();\n }\n\n recompute = (): void => {\n const ordered = [...this.entries.entries()].sort(([, a], [, b]) => compareDomOrder(a.node, b.node));\n const placedByContainer = new Map<HTMLElement | null, { left: number; right: number; bottom: number }[]>();\n const rows: { id: symbol; metrics: StickyStackMetrics; baseTop: number; isStuck: boolean }[] = [];\n\n for (const [id, entry] of ordered) {\n const metrics = entry.getMetrics();\n if (!metrics) continue;\n\n let placed = placedByContainer.get(metrics.container);\n if (!placed) {\n placed = [];\n placedByContainer.set(metrics.container, placed);\n }\n\n const overlapping = placed.filter((p) =>\n rangesOverlap(p.left, p.right, metrics.left, metrics.left + metrics.width),\n );\n const baseTop = overlapping.reduce((max, p) => Math.max(max, p.bottom), entry.offsetTop);\n const isStuck = !entry.disabled && metrics.top <= baseTop;\n\n if (isStuck) {\n // Never hold space below the boundary this entry is confined to: the\n // shared shift below has pulled it up to that edge, and an entry under it\n // must stack on where it sits, not on where it would sit unconfined.\n const bottom = baseTop + metrics.contentHeight;\n placed.push({\n left: metrics.left,\n right: metrics.left + metrics.width,\n bottom: metrics.boundaryBottom == null ? bottom : Math.min(bottom, metrics.boundaryBottom),\n });\n }\n rows.push({ id, metrics, baseTop: isStuck ? baseTop : entry.offsetTop, isStuck });\n }\n\n // A boundary pulls everything confined to it up by one shared amount, the\n // largest any of them needs. Letting each clamp its own bottom edge instead\n // would collapse a stacked group onto that edge, sliding the lower ones up\n // through the ones above.\n const shiftByBoundary = new Map<HTMLElement, number>();\n for (const { metrics, baseTop, isStuck } of rows) {\n const { boundary, boundaryBottom, contentHeight } = metrics;\n if (!isStuck || boundary == null || boundaryBottom == null) continue;\n const needed = Math.min(0, boundaryBottom - (baseTop + contentHeight));\n const current = shiftByBoundary.get(boundary);\n if (current == null || needed < current) shiftByBoundary.set(boundary, needed);\n }\n\n // An entry on its way out stays `isStuck`, so its placeholder keeps holding\n // the flow slot and the exit isn't a jump.\n for (const { id, metrics, baseTop, isStuck } of rows) {\n const shift = isStuck && metrics.boundary ? (shiftByBoundary.get(metrics.boundary) ?? 0) : 0;\n const result: StickyStackResult = { isStuck, top: baseTop + shift };\n const prev = this.results.get(id);\n if (prev && prev.isStuck === result.isStuck && prev.top === result.top) continue;\n this.results.set(id, result);\n this.listeners.get(id)?.(result);\n }\n };\n\n private startListening(): void {\n if (this.listenerCount === 0) {\n window.addEventListener('scroll', this.schedule, { passive: true, capture: true });\n window.addEventListener('resize', this.schedule);\n }\n this.listenerCount++;\n }\n\n private stopListening(): void {\n this.listenerCount--;\n if (this.listenerCount === 0) {\n window.removeEventListener('scroll', this.schedule, { capture: true } as EventListenerOptions);\n window.removeEventListener('resize', this.schedule);\n }\n }\n}\n\nexport const stickyStack = new StickyStack();\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- "use client";function e(e){let t=[],n=e.parentElement;for(;n&&n!==document.body&&n!==document.documentElement;){let e=getComputedStyle(n);(e.overflowX!==`visible`||e.overflowY!==`visible`)&&t.push(n),n=n.parentElement}return t}function t(e,t){return t.some(t=>{let n=t.getBoundingClientRect();return e.bottom<=n.top||e.top>=n.bottom||e.right<=n.left||e.left>=n.right})}export{e as getScrollableAncestors,t as isHiddenByAnyAncestor};
1
+ "use client";function e(e){let t=[],n=e.parentElement;for(;n&&n!==document.body&&n!==document.documentElement;){let e=getComputedStyle(n);(e.overflowX!==`visible`||e.overflowY!==`visible`)&&t.push(n),n=n.parentElement}return t}function t(e,t){return t.some(t=>{let n=t.getBoundingClientRect();return e.bottom<=n.top||e.top>=n.bottom||e.right<=n.left||e.left>=n.right})}function n(e){let t=e?.parentElement??null;for(;t&&t!==document.body;){let{overflowY:e}=getComputedStyle(t);if(e===`auto`||e===`scroll`||e===`overlay`)return t;t=t.parentElement}return null}export{n as getScrollParent,e as getScrollableAncestors,t as isHiddenByAnyAncestor};
2
2
  //# sourceMappingURL=scrollAncestors.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"scrollAncestors.mjs","names":[],"sources":["../../../../package/common/utils/scrollAncestors.ts"],"sourcesContent":["// Scrollable ancestors up to (not including) <body>. This is computed once per open rather\n// than on every scroll tick, since walking the tree and reading computed style forces layout,\n// which is too expensive to repeat at scroll frequency (Floating UI's autoUpdate does the same\n// thing, resolving the ancestor list once via getOverflowAncestors). Plain page/viewport scroll\n// is deliberately left out: a trigger that scrolls off the top of the page already renders its\n// fixed-position anchor off-screen for free.\nexport function getScrollableAncestors(el: HTMLElement): HTMLElement[] {\n const ancestors: HTMLElement[] = [];\n let node = el.parentElement;\n while (node && node !== document.body && node !== document.documentElement) {\n const style = getComputedStyle(node);\n if (style.overflowX !== 'visible' || style.overflowY !== 'visible') ancestors.push(node);\n node = node.parentElement;\n }\n return ancestors;\n}\n\nexport function isHiddenByAnyAncestor(rect: DOMRect, ancestors: HTMLElement[]): boolean {\n return ancestors.some((ancestor) => {\n const a = ancestor.getBoundingClientRect();\n return rect.bottom <= a.top || rect.top >= a.bottom || rect.right <= a.left || rect.left >= a.right;\n });\n}\n"],"mappings":"aAMA,SAAgB,EAAuB,EAAgC,CACrE,IAAM,EAA2B,CAAC,EAC9B,EAAO,EAAG,cACd,KAAO,GAAQ,IAAS,SAAS,MAAQ,IAAS,SAAS,iBAAiB,CAC1E,IAAM,EAAQ,iBAAiB,CAAI,GAC/B,EAAM,YAAc,WAAa,EAAM,YAAc,YAAW,EAAU,KAAK,CAAI,EACvF,EAAO,EAAK,aACd,CACA,OAAO,CACT,CAEA,SAAgB,EAAsB,EAAe,EAAmC,CACtF,OAAO,EAAU,KAAM,GAAa,CAClC,IAAM,EAAI,EAAS,sBAAsB,EACzC,OAAO,EAAK,QAAU,EAAE,KAAO,EAAK,KAAO,EAAE,QAAU,EAAK,OAAS,EAAE,MAAQ,EAAK,MAAQ,EAAE,KAChG,CAAC,CACH"}
1
+ {"version":3,"file":"scrollAncestors.mjs","names":[],"sources":["../../../../package/common/utils/scrollAncestors.ts"],"sourcesContent":["// Scrollable ancestors up to (not including) <body>. This is computed once per open rather\n// than on every scroll tick, since walking the tree and reading computed style forces layout,\n// which is too expensive to repeat at scroll frequency (Floating UI's autoUpdate does the same\n// thing, resolving the ancestor list once via getOverflowAncestors). Plain page/viewport scroll\n// is deliberately left out: a trigger that scrolls off the top of the page already renders its\n// fixed-position anchor off-screen for free.\nexport function getScrollableAncestors(el: HTMLElement): HTMLElement[] {\n const ancestors: HTMLElement[] = [];\n let node = el.parentElement;\n while (node && node !== document.body && node !== document.documentElement) {\n const style = getComputedStyle(node);\n if (style.overflowX !== 'visible' || style.overflowY !== 'visible') ancestors.push(node);\n node = node.parentElement;\n }\n return ancestors;\n}\n\nexport function isHiddenByAnyAncestor(rect: DOMRect, ancestors: HTMLElement[]): boolean {\n return ancestors.some((ancestor) => {\n const a = ancestor.getBoundingClientRect();\n return rect.bottom <= a.top || rect.top >= a.bottom || rect.right <= a.left || rect.left >= a.right;\n });\n}\n\n/**\n * The ancestor whose scrollbar carries `el`, or null when that is the page.\n * `Sticky` pins against it and `VirtualList` virtualizes against it, and the two\n * have to agree or a sticky header and the rows under it come apart.\n *\n * Narrower than `getScrollableAncestors` above, which is about who *clips* and so\n * counts `hidden` too: a `hidden` box never scrolls, so pinning to it would pin\n * to something that never moves.\n */\nexport function getScrollParent(el: HTMLElement | null): HTMLElement | null {\n let node = el?.parentElement ?? null;\n while (node && node !== document.body) {\n const { overflowY } = getComputedStyle(node);\n if (overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay') return node;\n node = node.parentElement;\n }\n return null;\n}\n"],"mappings":"aAMA,SAAgB,EAAuB,EAAgC,CACrE,IAAM,EAA2B,CAAC,EAC9B,EAAO,EAAG,cACd,KAAO,GAAQ,IAAS,SAAS,MAAQ,IAAS,SAAS,iBAAiB,CAC1E,IAAM,EAAQ,iBAAiB,CAAI,GAC/B,EAAM,YAAc,WAAa,EAAM,YAAc,YAAW,EAAU,KAAK,CAAI,EACvF,EAAO,EAAK,aACd,CACA,OAAO,CACT,CAEA,SAAgB,EAAsB,EAAe,EAAmC,CACtF,OAAO,EAAU,KAAM,GAAa,CAClC,IAAM,EAAI,EAAS,sBAAsB,EACzC,OAAO,EAAK,QAAU,EAAE,KAAO,EAAK,KAAO,EAAE,QAAU,EAAK,OAAS,EAAE,MAAQ,EAAK,MAAQ,EAAE,KAChG,CAAC,CACH,CAWA,SAAgB,EAAgB,EAA4C,CAC1E,IAAI,EAAO,GAAI,eAAiB,KAChC,KAAO,GAAQ,IAAS,SAAS,MAAM,CACrC,GAAM,CAAE,aAAc,iBAAiB,CAAI,EAC3C,GAAI,IAAc,QAAU,IAAc,UAAY,IAAc,UAAW,OAAO,EACtF,EAAO,EAAK,aACd,CACA,OAAO,IACT"}
@@ -0,0 +1,2 @@
1
+ "use client";function e(e){return!(`nodeType`in e)}function t(e){return e==null||typeof window>`u`?null:e===`window`?window:typeof e==`function`?e()??window:e}function n(t){return e(t)?{top:0,height:t.innerHeight}:{top:t.getBoundingClientRect().top+t.clientTop,height:t.clientHeight}}function r(e,t){let r=e.clientWidth;if(!t){let t=e.clientHeight;return{rel:e.scrollTop,visibleHeight:t,viewportHeight:t,distanceToEnd:e.scrollHeight-e.scrollTop-t,width:r}}let i=e.getBoundingClientRect(),{top:a,height:o}=n(t),s=a+o;return{rel:a-i.top,visibleHeight:Math.max(0,Math.min(s,i.bottom)-Math.max(a,i.top)),viewportHeight:o,distanceToEnd:i.bottom-s,width:r}}export{e as isWindowScroller,r as measureViewport,t as resolveScroller,n as scrollportBounds};
2
+ //# sourceMappingURL=scroller.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scroller.mjs","names":[],"sources":["../../../../package/common/virtualList/scroller.ts"],"sourcesContent":["import type { VirtualListScrollParent } from './virtualList.types';\n\n/**\n * Whatever carries the list's scroll, and how the list is measured against it.\n *\n * Everything here works in *list space*: offset 0 is the top of the first row,\n * which is the root's `scrollTop` when the list scrolls itself and its distance\n * above the scrollport's top edge when it doesn't. One coordinate system, so the\n * windowing, anchoring and jump math never branch.\n */\n\nexport type Scroller = HTMLElement | Window;\n\nexport function isWindowScroller(scroller: Scroller): scroller is Window {\n return !('nodeType' in scroller);\n}\n\n/** `'nearest'` is resolved by the caller, which caches the ancestor walk. */\nexport function resolveScroller(\n scrollParent: Exclude<VirtualListScrollParent, 'nearest'> | undefined,\n): Scroller | null {\n if (scrollParent == null || typeof window === 'undefined') return null;\n if (scrollParent === 'window') return window;\n // Resolved on every commit, so a ref still empty on the first render costs one\n // frame against the page rather than rendering nothing.\n if (typeof scrollParent === 'function') return scrollParent() ?? window;\n return scrollParent;\n}\n\n/** In viewport coordinates, which is what the rects below are compared against. */\nexport function scrollportBounds(scroller: Scroller): { top: number; height: number } {\n if (isWindowScroller(scroller)) return { top: 0, height: scroller.innerHeight };\n // `clientTop` is the border, which `getBoundingClientRect` includes and\n // `clientHeight` doesn't: without it a bordered scroll parent is off by it.\n return { top: scroller.getBoundingClientRect().top + scroller.clientTop, height: scroller.clientHeight };\n}\n\nexport interface Viewport {\n /** List-space scroll position, unclamped: negative while the list starts below\n * the scrollport, which is what makes a target reachable from either side. */\n rel: number;\n /** The slice of the scrollport the list occupies now. */\n visibleHeight: number;\n /** The whole scrollport, which is what a scroll target is aimed within. */\n viewportHeight: number;\n /** From the list's last row to the end of the scrollport. The browser clamps\n * `scrollTop`, so a list that scrolls itself never reports this negative;\n * riding a scrollport, it does, for the whole rest of the page. */\n distanceToEnd: number;\n width: number;\n}\n\nexport function measureViewport(root: HTMLElement, scroller: Scroller | null): Viewport {\n const width = root.clientWidth;\n if (!scroller) {\n const height = root.clientHeight;\n return {\n rel: root.scrollTop,\n visibleHeight: height,\n viewportHeight: height,\n distanceToEnd: root.scrollHeight - root.scrollTop - height,\n width,\n };\n }\n const rect = root.getBoundingClientRect();\n const { top, height } = scrollportBounds(scroller);\n const bottom = top + height;\n return {\n rel: top - rect.top,\n visibleHeight: Math.max(0, Math.min(bottom, rect.bottom) - Math.max(top, rect.top)),\n viewportHeight: height,\n distanceToEnd: rect.bottom - bottom,\n width,\n };\n}\n"],"mappings":"aAaA,SAAgB,EAAiB,EAAwC,CACvE,MAAO,EAAE,aAAc,EACzB,CAGA,SAAgB,EACd,EACiB,CAMjB,OALI,GAAgB,MAAQ,OAAO,OAAW,IAAoB,KAC9D,IAAiB,SAAiB,OAGlC,OAAO,GAAiB,WAAmB,EAAa,GAAK,OAC1D,CACT,CAGA,SAAgB,EAAiB,EAAqD,CAIpF,OAHI,EAAiB,CAAQ,EAAU,CAAE,IAAK,EAAG,OAAQ,EAAS,WAAY,EAGvE,CAAE,IAAK,EAAS,sBAAsB,CAAC,CAAC,IAAM,EAAS,UAAW,OAAQ,EAAS,YAAa,CACzG,CAiBA,SAAgB,EAAgB,EAAmB,EAAqC,CACtF,IAAM,EAAQ,EAAK,YACnB,GAAI,CAAC,EAAU,CACb,IAAM,EAAS,EAAK,aACpB,MAAO,CACL,IAAK,EAAK,UACV,cAAe,EACf,eAAgB,EAChB,cAAe,EAAK,aAAe,EAAK,UAAY,EACpD,OACF,CACF,CACA,IAAM,EAAO,EAAK,sBAAsB,EAClC,CAAE,MAAK,UAAW,EAAiB,CAAQ,EAC3C,EAAS,EAAM,EACrB,MAAO,CACL,IAAK,EAAM,EAAK,IAChB,cAAe,KAAK,IAAI,EAAG,KAAK,IAAI,EAAQ,EAAK,MAAM,EAAI,KAAK,IAAI,EAAK,EAAK,GAAG,CAAC,EAClF,eAAgB,EAChB,cAAe,EAAK,OAAS,EAC7B,OACF,CACF"}
@@ -1,2 +1,2 @@
1
- "use client";import e from"./virtualList.module.mjs";import{lastDescendantIndex as t,resolveReorderTarget as n,resolveTreeDropTarget as r}from"./virtualList.utils.mjs";import{useCallback as i,useEffect as a,useLayoutEffect as o,useRef as s,useState as c}from"react";function l({reorderable:l,treeReorder:u,onReorder:d,onTreeDrop:f,longPressDelay:ee,dragHandle:p,treeIndentPx:m,keys:h,depths:g,isGroupAt:_,rootRef:v,keysRef:y}){let b=s(null),[x,S]=c(null),[C,w]=c(null),T=s(null),E=s(d),D=s(f),O=s(u),k=s(g),A=s(_),j=s(!0),M=s(null),[N,te]=c(null),P=s(null),F=s(!1),I=s(null),L=s(0),R=s(0),z=s(null),B=s(!1),V=s(null),H=s(0),U=s(0),W=s(0),[G]=c(()=>typeof window<`u`&&typeof window.matchMedia==`function`&&window.matchMedia(`(pointer: coarse)`).matches);o(()=>{E.current=d,D.current=f,O.current=u,k.current=g,T.current=C,A.current=_});function K(e){G||M.current!==e&&(M.current=e,te(e))}function ne(e,t){t.buttons===0&&K(t.target.closest(`[data-drag-handle]`)?e:null)}function re(e){e.buttons===0&&K(null)}function ie(e,t){if(t.button!==0)return;let n=!!t.target.closest(`[data-drag-handle]`);j.current=n,n&&K(e)}function ae(e,t){if(p&&!j.current){t.preventDefault();return}b.current=e,S(e),u&&w(null)}function oe(e,n,i){let a=b.current;if(a==null)return null;let o=h.indexOf(a);if(o!==-1&&e>=o&&e<=t(o,g))return null;let s=r(n,i.top,i.height,_(e));return{key:h[e],position:s}}function se(e,t){t.preventDefault();let n=t.currentTarget.getBoundingClientRect(),r=oe(e,t.clientY,n);w(e=>e?.key===r?.key&&e?.position===r?.position?e:r)}function ce(){let e=b.current,t=T.current;e!=null&&t&&t.key!==e&&f?.(e,t)}function le(e){e.preventDefault(),ce(),w(null),b.current=null,S(null),K(null)}function ue(){w(null),b.current=null,S(null),K(null)}function de(e,t){t.preventDefault();let r=b.current;if(r==null)return;let i=h.indexOf(r),a=t.currentTarget.getBoundingClientRect(),o=n(i,e,t.clientY,a.top,a.height);o!=null&&d?.(i,o)}function fe(){b.current=null,S(null),K(null)}let q=i(e=>{let i=b.current,a=v.current;if(i==null||!a)return;let o=V.current?R.current-H.current+W.current/2:R.current,s=document.elementFromPoint(e,o),c=s&&a.contains(s)?s.closest(`[data-index]`):null;if(!c)return;let l=Number(c.dataset.index);if(Number.isNaN(l))return;let u=y.current.indexOf(i),d=c.getBoundingClientRect();if(O.current){let e=y.current,n=k.current;if(u!==-1&&l>=u&&l<=t(u,n)){w(null);return}let i=r(o,d.top,d.height,A.current(l)),a={key:e[l],position:i};w(e=>e?.key===a.key&&e?.position===a.position?e:a);return}let f=n(u,l,o,d.top,d.height);f!=null&&E.current?.(u,f)},[v,y]),J=i(()=>{let e=V.current;e&&(e.style.transform=`translate(${U.current}px, ${R.current-H.current}px)`)},[]),Y=i(()=>{let e=v.current;if(!e||!F.current){z.current=null;return}let t=e.getBoundingClientRect(),n=L.current,r=R.current,i=0;n>=t.left&&n<=t.right&&r>=t.top&&r<=t.bottom&&(r<t.top+48?i=-Math.min(18,Math.ceil((t.top+48-r)/3)):r>t.bottom-48&&(i=Math.min(18,Math.ceil((r-(t.bottom-48))/3)))),i!==0&&(e.scrollTop+=i,q(n)),z.current=requestAnimationFrame(Y)},[q,v]),X=i(()=>{if(P.current!=null&&(clearTimeout(P.current),P.current=null),window.removeEventListener(`touchmove`,Z),window.removeEventListener(`touchend`,X),window.removeEventListener(`touchcancel`,X),z.current!=null&&(cancelAnimationFrame(z.current),z.current=null),V.current&&=(V.current.remove(),null),document.body.style.touchAction=``,document.body.style.overscrollBehavior=``,F.current&&(B.current=!0,setTimeout(()=>{B.current=!1},350),O.current)){let e=b.current,t=T.current;e!=null&&t&&t.key!==e&&D.current?.(e,t)}w(null),F.current=!1,I.current=null,b.current=null,S(null)},[]),Z=i(e=>{let t=e.touches[0];if(t){if(L.current=t.clientX,R.current=t.clientY,!F.current){let e=I.current;e!=null&&Math.abs(t.clientY-e)>10&&P.current!=null&&(clearTimeout(P.current),P.current=null);return}e.cancelable&&e.preventDefault(),J(),q(t.clientX)}},[q,J]);function pe(e,t){if(!l&&!u||p&&!t.target.closest(`[data-drag-handle]`)||P.current!=null||F.current)return;let n=t.touches[0];n&&(I.current=n.clientY,L.current=n.clientX,R.current=n.clientY,window.addEventListener(`touchmove`,Z,{passive:!1}),window.addEventListener(`touchend`,X),window.addEventListener(`touchcancel`,X),P.current=window.setTimeout(()=>{P.current=null,F.current=!0,b.current=e,S(e),navigator.vibrate?.(10),document.body.style.touchAction=`none`,document.body.style.overscrollBehavior=`none`,me(e),z.current??=requestAnimationFrame(Y)},ee))}function me(t){let n=v.current;if(!n)return;let r=y.current.indexOf(t),i=n.querySelector(`[data-index="${r}"]`);if(!i)return;let a=i.getBoundingClientRect();H.current=R.current-a.top,U.current=a.left,W.current=a.height;let o=i.cloneNode(!0);o.removeAttribute(`data-index`),o.setAttribute(`data-drag-clone`,`true`),o.setAttribute(`aria-hidden`,`true`),o.classList.add(e.virtualListDragClone),Object.assign(o.style,{position:`fixed`,left:`0`,top:`0`,width:`${a.width}px`,height:`${a.height}px`,margin:`0`,opacity:`1`,pointerEvents:`none`,zIndex:`3`,background:`var(--background)`,boxShadow:`var(--card-shadow)`,borderRadius:`var(--default-border-radius)`}),n.appendChild(o),V.current=o,J()}a(()=>X,[X]);let Q=l||u,$=Q&&!G;function he(e,t){let n=u&&C?.key===t?C.position:void 0;return{reorderable:Q&&!p,mouseDragEnabled:$,draggable:$&&(!p||t===N)||void 0,isDragging:Q&&t===x,onMouseDown:Q&&p?e=>ie(t,e):void 0,onMouseOver:Q&&p?e=>ne(t,e):void 0,onMouseLeave:Q&&p?re:void 0,onDragStart:Q?e=>ae(t,e):void 0,onDragOver:u?t=>se(e,t):l?t=>de(e,t):void 0,onDragEnd:u?ue:l?fe:void 0,onDrop:u?le:void 0,onTouchStart:Q?e=>pe(t,e):void 0,dropPosition:n,dropIndentPx:n?(g[e]??0)*m:void 0}}function ge(){return B.current?(B.current=!1,!0):!1}return{draggingKey:x,dragEnabled:Q,getRowDragProps:he,consumeSuppressedClick:ge}}export{l as useRowDrag};
1
+ "use client";import e from"./virtualList.module.mjs";import{lastDescendantIndex as t,resolveReorderTarget as n,resolveTreeDropTarget as r}from"./virtualList.utils.mjs";import{useCallback as i,useEffect as a,useLayoutEffect as o,useRef as s,useState as c}from"react";function l({reorderable:l,treeReorder:u,onReorder:d,onTreeDrop:f,longPressDelay:ee,dragHandle:p,treeIndentPx:te,keys:m,depths:h,isGroupAt:g,rootRef:_,keysRef:v,getVisibleRect:y,scrollBy:b}){let x=s(null),[S,C]=c(null),[w,T]=c(null),E=s(null),D=s(d),O=s(f),k=s(u),A=s(h),j=s(g),M=s(!0),N=s(null),[ne,re]=c(null),P=s(null),F=s(!1),I=s(null),L=s(0),R=s(0),z=s(null),B=s(!1),V=s(null),H=s(0),U=s(0),W=s(0),[G]=c(()=>typeof window<`u`&&typeof window.matchMedia==`function`&&window.matchMedia(`(pointer: coarse)`).matches);o(()=>{D.current=d,O.current=f,k.current=u,A.current=h,E.current=w,j.current=g});function K(e){G||N.current!==e&&(N.current=e,re(e))}function ie(e,t){t.buttons===0&&K(t.target.closest(`[data-drag-handle]`)?e:null)}function ae(e){e.buttons===0&&K(null)}function oe(e,t){if(t.button!==0)return;let n=!!t.target.closest(`[data-drag-handle]`);M.current=n,n&&K(e)}function se(e,t){if(p&&!M.current){t.preventDefault();return}x.current=e,C(e),u&&T(null)}function ce(e,n,i){let a=x.current;if(a==null)return null;let o=m.indexOf(a);if(o!==-1&&e>=o&&e<=t(o,h))return null;let s=r(n,i.top,i.height,g(e));return{key:m[e],position:s}}function le(e,t){t.preventDefault();let n=t.currentTarget.getBoundingClientRect(),r=ce(e,t.clientY,n);T(e=>e?.key===r?.key&&e?.position===r?.position?e:r)}function ue(){let e=x.current,t=E.current;e!=null&&t&&t.key!==e&&f?.(e,t)}function de(e){e.preventDefault(),ue(),T(null),x.current=null,C(null),K(null)}function fe(){T(null),x.current=null,C(null),K(null)}function pe(e,t){t.preventDefault();let r=x.current;if(r==null)return;let i=m.indexOf(r),a=t.currentTarget.getBoundingClientRect(),o=n(i,e,t.clientY,a.top,a.height);o!=null&&d?.(i,o)}function me(){x.current=null,C(null),K(null)}let q=i(e=>{let i=x.current,a=_.current;if(i==null||!a)return;let o=V.current?R.current-H.current+W.current/2:R.current,s=document.elementFromPoint(e,o),c=s&&a.contains(s)?s.closest(`[data-index]`):null;if(!c)return;let l=Number(c.dataset.index);if(Number.isNaN(l))return;let u=v.current.indexOf(i),d=c.getBoundingClientRect();if(k.current){let e=v.current,n=A.current;if(u!==-1&&l>=u&&l<=t(u,n)){T(null);return}let i=r(o,d.top,d.height,j.current(l)),a={key:e[l],position:i};T(e=>e?.key===a.key&&e?.position===a.position?e:a);return}let f=n(u,l,o,d.top,d.height);f!=null&&D.current?.(u,f)},[_,v]),J=i(()=>{let e=V.current;e&&(e.style.transform=`translate(${U.current}px, ${R.current-H.current}px)`)},[]),Y=i(()=>{let e=F.current?y():null;if(!e){z.current=null;return}let t=L.current,n=R.current,r=0;t>=e.left&&t<=e.right&&n>=e.top&&n<=e.bottom&&(n<e.top+48?r=-Math.min(18,Math.ceil((e.top+48-n)/3)):n>e.bottom-48&&(r=Math.min(18,Math.ceil((n-(e.bottom-48))/3)))),r!==0&&(b(r),q(t)),z.current=requestAnimationFrame(Y)},[q,y,b]),X=i(()=>{if(P.current!=null&&(clearTimeout(P.current),P.current=null),window.removeEventListener(`touchmove`,Z),window.removeEventListener(`touchend`,X),window.removeEventListener(`touchcancel`,X),z.current!=null&&(cancelAnimationFrame(z.current),z.current=null),V.current&&=(V.current.remove(),null),document.body.style.touchAction=``,document.body.style.overscrollBehavior=``,F.current&&(B.current=!0,setTimeout(()=>{B.current=!1},350),k.current)){let e=x.current,t=E.current;e!=null&&t&&t.key!==e&&O.current?.(e,t)}T(null),F.current=!1,I.current=null,x.current=null,C(null)},[]),Z=i(e=>{let t=e.touches[0];if(t){if(L.current=t.clientX,R.current=t.clientY,!F.current){let e=I.current;e!=null&&Math.abs(t.clientY-e)>10&&P.current!=null&&(clearTimeout(P.current),P.current=null);return}e.cancelable&&e.preventDefault(),J(),q(t.clientX)}},[q,J]);function he(e,t){if(!l&&!u||p&&!t.target.closest(`[data-drag-handle]`)||P.current!=null||F.current)return;let n=t.touches[0];n&&(I.current=n.clientY,L.current=n.clientX,R.current=n.clientY,window.addEventListener(`touchmove`,Z,{passive:!1}),window.addEventListener(`touchend`,X),window.addEventListener(`touchcancel`,X),P.current=window.setTimeout(()=>{P.current=null,F.current=!0,x.current=e,C(e),navigator.vibrate?.(10),document.body.style.touchAction=`none`,document.body.style.overscrollBehavior=`none`,ge(e),z.current??=requestAnimationFrame(Y)},ee))}function ge(t){let n=_.current;if(!n)return;let r=v.current.indexOf(t),i=n.querySelector(`[data-index="${r}"]`);if(!i)return;let a=i.getBoundingClientRect();H.current=R.current-a.top,U.current=a.left,W.current=a.height;let o=i.cloneNode(!0);o.removeAttribute(`data-index`),o.setAttribute(`data-drag-clone`,`true`),o.setAttribute(`aria-hidden`,`true`),o.classList.add(e.virtualListDragClone),Object.assign(o.style,{position:`fixed`,left:`0`,top:`0`,width:`${a.width}px`,height:`${a.height}px`,margin:`0`,opacity:`1`,pointerEvents:`none`,zIndex:`3`,background:`var(--background)`,boxShadow:`var(--card-shadow)`,borderRadius:`var(--default-border-radius)`}),n.appendChild(o),V.current=o,J()}a(()=>X,[X]);let Q=l||u,$=Q&&!G;function _e(e,t){let n=u&&w?.key===t?w.position:void 0;return{reorderable:Q&&!p,mouseDragEnabled:$,draggable:$&&(!p||t===ne)||void 0,isDragging:Q&&t===S,onMouseDown:Q&&p?e=>oe(t,e):void 0,onMouseOver:Q&&p?e=>ie(t,e):void 0,onMouseLeave:Q&&p?ae:void 0,onDragStart:Q?e=>se(t,e):void 0,onDragOver:u?t=>le(e,t):l?t=>pe(e,t):void 0,onDragEnd:u?fe:l?me:void 0,onDrop:u?de:void 0,onTouchStart:Q?e=>he(t,e):void 0,dropPosition:n,dropIndentPx:n?(h[e]??0)*te:void 0}}function ve(){return B.current?(B.current=!1,!0):!1}return{draggingKey:S,dragEnabled:Q,getRowDragProps:_e,consumeSuppressedClick:ve}}export{l as useRowDrag};
2
2
  //# sourceMappingURL=useRowDrag.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"useRowDrag.mjs","names":[],"sources":["../../../../package/common/virtualList/useRowDrag.ts"],"sourcesContent":["import React, { useState, useRef, useEffect, useLayoutEffect, useCallback } from 'react';\n\nimport styles from './virtualList.module.pcss';\nimport { resolveReorderTarget, resolveTreeDropTarget, lastDescendantIndex } from './virtualList.utils';\nimport type { TreeDropPosition, TreeDropTarget } from './virtualList.types';\n\ninterface UseRowDragArgs {\n reorderable: boolean;\n treeReorder: boolean;\n onReorder?: (fromIndex: number, toIndex: number) => void;\n onTreeDrop?: (dragKey: string | number, target: TreeDropTarget) => void;\n longPressDelay: number;\n dragHandle: boolean;\n treeIndentPx: number;\n /** Current flat ordering and depth tags (mouse handlers read these directly). */\n keys: (string | number)[];\n depths: number[];\n isGroupAt: (index: number) => boolean;\n rootRef: React.RefObject<HTMLDivElement | null>;\n /** Latest keys for async (touch / rAF) consumers — owned by useVirtualWindow. */\n keysRef: React.RefObject<(string | number)[]>;\n}\n\n/** The drag-related props the parent spreads onto a given row. */\nexport interface RowDragProps {\n reorderable: boolean;\n /** Native mouse DnD is active for the list (independent of which row is armed). */\n mouseDragEnabled: boolean;\n draggable: boolean | undefined;\n isDragging: boolean;\n onMouseDown: ((e: React.MouseEvent<HTMLDivElement>) => void) | undefined;\n onMouseOver: ((e: React.MouseEvent<HTMLDivElement>) => void) | undefined;\n onMouseLeave: ((e: React.MouseEvent<HTMLDivElement>) => void) | undefined;\n onDragStart: ((e: React.DragEvent<HTMLDivElement>) => void) | undefined;\n onDragOver: ((e: React.DragEvent<HTMLDivElement>) => void) | undefined;\n onDragEnd: (() => void) | undefined;\n onDrop: ((e: React.DragEvent<HTMLDivElement>) => void) | undefined;\n onTouchStart: ((e: React.TouchEvent<HTMLDivElement>) => void) | undefined;\n dropPosition: TreeDropPosition | undefined;\n dropIndentPx: number | undefined;\n}\n\ninterface UseRowDragResult {\n /** True while a row is being dragged (for the root reordering style). */\n draggingKey: string | number | null;\n /** Whether either drag mode is active. */\n dragEnabled: boolean;\n /** Drag props for the row at `index` / `key`. */\n getRowDragProps: (index: number, key: string | number) => RowDragProps;\n /** Returns true (and resets) if the next click should be swallowed (post-drag). */\n consumeSuppressedClick: () => boolean;\n}\n\n/**\n * Row drag-and-drop for both modes:\n * - `reorderable` (flat): native HTML5 drag on desktop; rows move live via\n * `onReorder` with midpoint hysteresis.\n * - `treeReorder`: a drop indicator (before/after/inside) is shown during the\n * drag and `onTreeDrop` fires once on release; dropping onto the dragged\n * node's own subtree is forbidden.\n * Touch uses a long-press → lifted-clone → pointer-move path with edge\n * auto-scroll. `dragHandle` restricts initiation to `[data-drag-handle]`.\n */\nexport function useRowDrag({\n reorderable,\n treeReorder,\n onReorder,\n onTreeDrop,\n longPressDelay,\n dragHandle,\n treeIndentPx,\n keys,\n depths,\n isGroupAt,\n rootRef,\n keysRef,\n}: UseRowDragArgs): UseRowDragResult {\n // The dragged item is tracked by its stable key (indices shift after each\n // live reorder); the ref drives the logic, the state drives the row's style.\n const dragKeyRef = useRef<string | number | null>(null);\n const [draggingKey, setDraggingKey] = useState<string | number | null>(null);\n // Tree-reorder drop indicator. Unlike flat reorder (which moves rows live),\n // tree mode shows where the drop will land and commits once on release.\n const [dropTarget, setDropTarget] = useState<TreeDropTarget | null>(null);\n const dropTargetRef = useRef<TreeDropTarget | null>(null);\n const onReorderRef = useRef(onReorder);\n const onTreeDropRef = useRef(onTreeDrop);\n const treeModeRef = useRef(treeReorder);\n const depthsRef = useRef<number[]>(depths);\n const isGroupAtRef = useRef<(i: number) => boolean>(isGroupAt);\n // When `dragHandle` is on, a mousedown sets this to whether it landed on a\n // `[data-drag-handle]` element, gating whether the ensuing dragstart is honoured.\n const dragFromHandleRef = useRef(true);\n // In `dragHandle` mode the `draggable` attribute is only on the row whose\n // handle the pointer is currently over (\"armed\"). A permanently `draggable`\n // row breaks mouse text selection (caret placement, double-click word select,\n // drag select) inside any child input in Firefox — see Mozilla bugs 739071 /\n // 800050 / 1189486. Arming on hover rather than on mousedown means the\n // attribute is already in place before the press, so the browser never has to\n // notice it appearing mid-gesture, and any pointer move re-derives it.\n const armedKeyRef = useRef<string | number | null>(null);\n const [armedKey, setArmedKeyState] = useState<string | number | null>(null);\n const longPressRef = useRef<number | null>(null);\n const touchActiveRef = useRef(false);\n const touchStartYRef = useRef<number | null>(null);\n const pointerXRef = useRef(0);\n const pointerYRef = useRef(0);\n const autoScrollRef = useRef<number | null>(null);\n const suppressNextClickRef = useRef(false);\n // Floating clone that follows the finger during a touch drag.\n const dragCloneRef = useRef<HTMLElement | null>(null);\n const grabOffsetYRef = useRef(0);\n const cloneLeftRef = useRef(0);\n const dragHeightRef = useRef(0);\n // The native `draggable` attribute (mouse DnD) makes a long-press start the\n // browser's own drag on touch devices, which hijacks our touch handlers. So\n // expose it only where the primary pointer is fine (mouse/trackpad); touch\n // devices use the touch handlers below instead.\n const [isCoarsePointer] = useState(() =>\n typeof window !== 'undefined' && typeof window.matchMedia === 'function'\n ? window.matchMedia('(pointer: coarse)').matches\n : false,\n );\n\n useLayoutEffect(() => {\n onReorderRef.current = onReorder;\n onTreeDropRef.current = onTreeDrop;\n treeModeRef.current = treeReorder;\n depthsRef.current = depths;\n dropTargetRef.current = dropTarget;\n isGroupAtRef.current = isGroupAt;\n });\n\n // ── Mouse / shared start ──────────────────────────────────────────────────\n\n /** Move the `draggable` attribute to `next` (or off entirely for `null`). */\n function arm(next: string | number | null) {\n // Arming exists purely to place the native `draggable` attribute, which is\n // never set on a coarse pointer (touch uses the handlers below instead). A\n // tap still synthesises mouseover/mousedown, so without this a touch device\n // would re-render the list on every handle tap to no effect whatsoever.\n if (isCoarsePointer) return;\n if (armedKeyRef.current === next) return; // mouseover fires constantly — no-op\n armedKeyRef.current = next;\n setArmedKeyState(next);\n }\n\n // Arm while the pointer rests on this row's handle, disarm as it moves off.\n // `buttons !== 0` means a press is already in progress: the pointer can drift\n // off the handle before the browser reaches its drag threshold, so hover is\n // ignored until release rather than disarming a drag that is about to start.\n function handleRowMouseOver(key: string | number, e: React.MouseEvent<HTMLDivElement>) {\n if (e.buttons !== 0) return;\n arm((e.target as HTMLElement).closest('[data-drag-handle]') ? key : null);\n }\n\n function handleRowMouseLeave(e: React.MouseEvent<HTMLDivElement>) {\n if (e.buttons !== 0) return;\n arm(null);\n }\n\n // With `dragHandle`, remember on mousedown whether the press landed on a\n // handle, so the dragstart that follows can be ignored if it didn't. Arming\n // here too covers a handle that came to sit under an already-stationary cursor\n // (the list scrolled), which produces no mouseover.\n function handleRowMouseDown(key: string | number, e: React.MouseEvent<HTMLDivElement>) {\n if (e.button !== 0) return; // only a primary press can begin a drag\n const fromHandle = !!(e.target as HTMLElement).closest('[data-drag-handle]');\n dragFromHandleRef.current = fromHandle;\n if (fromHandle) arm(key);\n }\n\n function handleReorderStart(key: string | number, e: React.DragEvent<HTMLDivElement>) {\n if (dragHandle && !dragFromHandleRef.current) {\n e.preventDefault(); // not started from a handle — let the row stay put\n return;\n }\n dragKeyRef.current = key;\n setDraggingKey(key);\n if (treeReorder) setDropTarget(null);\n }\n\n // ── Tree mode ─────────────────────────────────────────────────────────────\n\n /** Resolve the tree drop target for the row at `overIndex` under pointer `y`. */\n function computeTreeDrop(overIndex: number, y: number, rect: DOMRect): TreeDropTarget | null {\n const dragKey = dragKeyRef.current;\n if (dragKey == null) return null;\n const fromIndex = keys.indexOf(dragKey);\n // Forbid dropping onto the dragged node or anything in its own subtree.\n if (fromIndex !== -1 && overIndex >= fromIndex && overIndex <= lastDescendantIndex(fromIndex, depths)) {\n return null;\n }\n const position = resolveTreeDropTarget(y, rect.top, rect.height, isGroupAt(overIndex));\n return { key: keys[overIndex], position };\n }\n\n function handleTreeDragOver(overIndex: number, e: React.DragEvent<HTMLDivElement>) {\n e.preventDefault(); // allow the drop\n const rect = e.currentTarget.getBoundingClientRect();\n const next = computeTreeDrop(overIndex, e.clientY, rect);\n setDropTarget((prev) => (prev?.key === next?.key && prev?.position === next?.position ? prev : next));\n }\n\n function commitTreeDrop() {\n const dragKey = dragKeyRef.current;\n const target = dropTargetRef.current;\n if (dragKey != null && target && target.key !== dragKey) onTreeDrop?.(dragKey, target);\n }\n\n function handleTreeDrop(e: React.DragEvent<HTMLDivElement>) {\n e.preventDefault();\n commitTreeDrop();\n setDropTarget(null);\n dragKeyRef.current = null;\n setDraggingKey(null);\n arm(null);\n }\n\n function handleTreeDragEnd() {\n // Fires after `drop` (or on its own when released off a valid target).\n setDropTarget(null);\n dragKeyRef.current = null;\n setDraggingKey(null);\n arm(null);\n }\n\n // ── Flat mode ─────────────────────────────────────────────────────────────\n\n function handleReorderOver(overIndex: number, e: React.DragEvent<HTMLDivElement>) {\n e.preventDefault(); // allow the drop\n const dragKey = dragKeyRef.current;\n if (dragKey == null) return;\n const fromIndex = keys.indexOf(dragKey);\n // Midpoint hysteresis (see resolveReorderTarget): a move only happens once\n // the pointer crosses the centre of the row it's over. After a swap the\n // dragged row sits under the cursor, so moving again needs real pointer\n // travel — this stops the oscillation that made dragging feel jumpy.\n const rect = e.currentTarget.getBoundingClientRect();\n const target = resolveReorderTarget(fromIndex, overIndex, e.clientY, rect.top, rect.height);\n if (target != null) onReorder?.(fromIndex, target);\n }\n\n function handleReorderEnd() {\n dragKeyRef.current = null;\n setDraggingKey(null);\n arm(null);\n }\n\n // ── Touch reordering (long-press to arm, then drag) ──────────────────────────\n // These handlers run from window listeners during a touch drag, so they read\n // mutable state from refs and are stable across renders.\n\n const reorderAtPoint = useCallback(\n (x: number) => {\n const dragKey = dragKeyRef.current;\n const root = rootRef.current;\n if (dragKey == null || !root) return;\n // Reorder against the centre of the lifted clone, not the raw finger — the\n // finger sits below the clone's top by the grab offset, so using it would\n // make the top (and bottom) rows unreachable. The clone is what the user\n // aims with, so match it.\n const y = dragCloneRef.current\n ? pointerYRef.current - grabOffsetYRef.current + dragHeightRef.current / 2\n : pointerYRef.current;\n const el = document.elementFromPoint(x, y) as HTMLElement | null;\n const rowEl = el && root.contains(el) ? el.closest<HTMLElement>('[data-index]') : null;\n if (!rowEl) return;\n const overIndex = Number(rowEl.dataset.index);\n if (Number.isNaN(overIndex)) return;\n const fromIndex = keysRef.current.indexOf(dragKey);\n const rect = rowEl.getBoundingClientRect();\n if (treeModeRef.current) {\n // Tree mode: don't move live — just track where the drop will land.\n const ks = keysRef.current;\n const ds = depthsRef.current;\n if (fromIndex !== -1 && overIndex >= fromIndex && overIndex <= lastDescendantIndex(fromIndex, ds)) {\n setDropTarget(null);\n return;\n }\n const position = resolveTreeDropTarget(y, rect.top, rect.height, isGroupAtRef.current(overIndex));\n const next: TreeDropTarget = { key: ks[overIndex], position };\n setDropTarget((prev) => (prev?.key === next.key && prev?.position === next.position ? prev : next));\n return;\n }\n const target = resolveReorderTarget(fromIndex, overIndex, y, rect.top, rect.height);\n if (target != null) onReorderRef.current?.(fromIndex, target);\n },\n [rootRef, keysRef],\n );\n\n // Keep the floating clone glued under the finger (same grab point).\n const positionDragClone = useCallback(() => {\n const clone = dragCloneRef.current;\n if (!clone) return;\n clone.style.transform = `translate(${cloneLeftRef.current}px, ${pointerYRef.current - grabOffsetYRef.current}px)`;\n }, []);\n\n // While the finger sits near a top/bottom edge (and within the list's width),\n // scroll the list and keep reordering against the rows now passing under it.\n const autoScrollTick = useCallback(() => {\n const root = rootRef.current;\n if (!root || !touchActiveRef.current) {\n autoScrollRef.current = null;\n return;\n }\n const rect = root.getBoundingClientRect();\n const x = pointerXRef.current;\n const y = pointerYRef.current;\n const EDGE = 48;\n let dy = 0;\n // Only auto-scroll while the finger is actually over the list — moving it\n // outside (any direction) stops the scroll instead of running away; pinning\n // the dragged row keeps touch events alive so it resumes when you come back.\n const inside = x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;\n if (inside) {\n if (y < rect.top + EDGE) dy = -Math.min(18, Math.ceil((rect.top + EDGE - y) / 3));\n else if (y > rect.bottom - EDGE) dy = Math.min(18, Math.ceil((y - (rect.bottom - EDGE)) / 3));\n }\n if (dy !== 0) {\n root.scrollTop += dy;\n reorderAtPoint(x);\n }\n autoScrollRef.current = requestAnimationFrame(autoScrollTick);\n }, [reorderAtPoint, rootRef]);\n\n const endTouchDrag = useCallback(() => {\n if (longPressRef.current != null) {\n clearTimeout(longPressRef.current);\n longPressRef.current = null;\n }\n window.removeEventListener('touchmove', handleTouchMove);\n window.removeEventListener('touchend', endTouchDrag);\n window.removeEventListener('touchcancel', endTouchDrag);\n if (autoScrollRef.current != null) {\n cancelAnimationFrame(autoScrollRef.current);\n autoScrollRef.current = null;\n }\n if (dragCloneRef.current) {\n dragCloneRef.current.remove();\n dragCloneRef.current = null;\n }\n // Restore native scrolling that was locked for the duration of the drag.\n document.body.style.touchAction = '';\n document.body.style.overscrollBehavior = '';\n if (touchActiveRef.current) {\n // A real drag happened — swallow the click the browser fires afterwards\n // so it doesn't, e.g., toggle row selection.\n suppressNextClickRef.current = true;\n setTimeout(() => {\n suppressNextClickRef.current = false;\n }, 350);\n // Tree mode commits the pending drop on release (flat mode already moved\n // rows live during the drag, so there's nothing to commit here).\n if (treeModeRef.current) {\n const dk = dragKeyRef.current;\n const target = dropTargetRef.current;\n if (dk != null && target && target.key !== dk) onTreeDropRef.current?.(dk, target);\n }\n }\n setDropTarget(null);\n touchActiveRef.current = false;\n touchStartYRef.current = null;\n dragKeyRef.current = null;\n setDraggingKey(null);\n // handleTouchMove and endTouchDrag remove each other’s listeners, so neither can list\n // the other without a cycle.\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const handleTouchMove = useCallback(\n (e: TouchEvent) => {\n const t = e.touches[0];\n if (!t) return;\n pointerXRef.current = t.clientX;\n pointerYRef.current = t.clientY;\n if (!touchActiveRef.current) {\n // Before the long-press fires: if the finger travels, the user means to\n // scroll, so disarm and let the browser handle it (we don't preventDefault).\n const sy = touchStartYRef.current;\n if (sy != null && Math.abs(t.clientY - sy) > 10 && longPressRef.current != null) {\n clearTimeout(longPressRef.current);\n longPressRef.current = null;\n }\n return;\n }\n // Non-passive listener: preventDefault cancels the native scroll for this\n // gesture — the only reliable way once touch-action has been latched at\n // touchstart. This is what makes the drag actually move on touch devices.\n if (e.cancelable) e.preventDefault();\n positionDragClone();\n reorderAtPoint(t.clientX);\n },\n [reorderAtPoint, positionDragClone],\n );\n\n function handleRowTouchStart(key: string | number, e: React.TouchEvent<HTMLDivElement>) {\n if (!reorderable && !treeReorder) return;\n // With a drag handle, only a press that starts on it arms the drag, so the\n // rest of the row stays free for tapping/typing into inputs.\n if (dragHandle && !(e.target as HTMLElement).closest('[data-drag-handle]')) return;\n if (longPressRef.current != null || touchActiveRef.current) return; // ignore extra fingers\n const t = e.touches[0];\n if (!t) return;\n touchStartYRef.current = t.clientY;\n pointerXRef.current = t.clientX;\n pointerYRef.current = t.clientY;\n window.addEventListener('touchmove', handleTouchMove, { passive: false });\n window.addEventListener('touchend', endTouchDrag);\n window.addEventListener('touchcancel', endTouchDrag);\n longPressRef.current = window.setTimeout(() => {\n longPressRef.current = null;\n touchActiveRef.current = true;\n dragKeyRef.current = key;\n setDraggingKey(key);\n navigator.vibrate?.(10);\n // Lock native scrolling for the whole gesture — relying on per-move\n // preventDefault isn't enough once the finger leaves and re-enters the\n // list (the browser commits to a scroll and ignores it). We do all the\n // scrolling ourselves via autoScrollTick.\n document.body.style.touchAction = 'none';\n document.body.style.overscrollBehavior = 'none';\n createDragClone(key);\n if (autoScrollRef.current == null) autoScrollRef.current = requestAnimationFrame(autoScrollTick);\n }, longPressDelay);\n }\n\n // Build a floating copy of the picked-up row that tracks the finger, so the\n // drag has a visible \"lifted\" element (the original row stays dimmed in place\n // as a drop placeholder).\n function createDragClone(key: string | number) {\n const root = rootRef.current;\n if (!root) return;\n const idx = keysRef.current.indexOf(key);\n const src = root.querySelector<HTMLElement>(`[data-index=\"${idx}\"]`);\n if (!src) return;\n const r = src.getBoundingClientRect();\n grabOffsetYRef.current = pointerYRef.current - r.top;\n cloneLeftRef.current = r.left;\n dragHeightRef.current = r.height;\n const clone = src.cloneNode(true) as HTMLElement;\n clone.removeAttribute('data-index');\n clone.setAttribute('data-drag-clone', 'true');\n clone.setAttribute('aria-hidden', 'true');\n clone.classList.add(styles.virtualListDragClone);\n Object.assign(clone.style, {\n position: 'fixed',\n left: '0',\n top: '0',\n width: `${r.width}px`,\n height: `${r.height}px`,\n margin: '0',\n opacity: '1',\n pointerEvents: 'none',\n // Above the sticky header (z-index 2) and any focused row (z-index 1).\n zIndex: '3',\n background: 'var(--background)',\n boxShadow: 'var(--card-shadow)',\n borderRadius: 'var(--default-border-radius)',\n } as Partial<CSSStyleDeclaration>);\n // Appended inside the list so it inherits the active theme's CSS variables;\n // position:fixed keeps it in viewport coordinates regardless.\n root.appendChild(clone);\n dragCloneRef.current = clone;\n positionDragClone();\n }\n\n // Tear down any in-flight touch drag if the component unmounts mid-gesture.\n useEffect(() => endTouchDrag, [endTouchDrag]);\n\n // Either drag mode arms the same row gestures; the handlers above branch on\n // `treeReorder` for the differing target resolution / commit behaviour.\n const dragEnabled = reorderable || treeReorder;\n // Native mouse DnD is in play for this list. Row-level fallbacks that must\n // cover *every* row — swallowing the browser's own drop and context menu —\n // key off this, not off `draggable`, which in `dragHandle` mode is only ever\n // on the single armed row.\n const mouseDragEnabled = dragEnabled && !isCoarsePointer;\n\n function getRowDragProps(index: number, key: string | number): RowDragProps {\n // The drop indicator only renders on the row currently targeted. `before`/\n // `after` lines indent to the target's depth so they sit under where the\n // dropped content will start; `inside` highlights the whole row instead.\n const rowDrop = treeReorder && dropTarget?.key === key ? dropTarget.position : undefined;\n return {\n // Drives the row-wide grab cursor. With `dragHandle`, only the handle\n // initiates a drag, so the cursor belongs on the handle (styled by the\n // consumer), not across the whole row.\n reorderable: dragEnabled && !dragHandle,\n mouseDragEnabled,\n // In `dragHandle` mode the attribute only goes on the row the pointer has\n // armed, so rows stay text-selectable in Firefox the rest of the time.\n draggable: (mouseDragEnabled && (!dragHandle || key === armedKey)) || undefined,\n isDragging: dragEnabled && key === draggingKey,\n onMouseDown: dragEnabled && dragHandle ? (e) => handleRowMouseDown(key, e) : undefined,\n onMouseOver: dragEnabled && dragHandle ? (e) => handleRowMouseOver(key, e) : undefined,\n onMouseLeave: dragEnabled && dragHandle ? handleRowMouseLeave : undefined,\n onDragStart: dragEnabled ? (e) => handleReorderStart(key, e) : undefined,\n onDragOver: treeReorder\n ? (e) => handleTreeDragOver(index, e)\n : reorderable\n ? (e) => handleReorderOver(index, e)\n : undefined,\n onDragEnd: treeReorder ? handleTreeDragEnd : reorderable ? handleReorderEnd : undefined,\n onDrop: treeReorder ? handleTreeDrop : undefined,\n onTouchStart: dragEnabled ? (e) => handleRowTouchStart(key, e) : undefined,\n dropPosition: rowDrop,\n dropIndentPx: rowDrop ? (depths[index] ?? 0) * treeIndentPx : undefined,\n };\n }\n\n function consumeSuppressedClick(): boolean {\n if (suppressNextClickRef.current) {\n suppressNextClickRef.current = false;\n return true;\n }\n return false;\n }\n\n return { draggingKey, dragEnabled, getRowDragProps, consumeSuppressedClick };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"useRowDrag.mjs","names":[],"sources":["../../../../package/common/virtualList/useRowDrag.ts"],"sourcesContent":["import React, { useState, useRef, useEffect, useLayoutEffect, useCallback } from 'react';\n\nimport styles from './virtualList.module.pcss';\nimport { resolveReorderTarget, resolveTreeDropTarget, lastDescendantIndex } from './virtualList.utils';\nimport type { TreeDropPosition, TreeDropTarget } from './virtualList.types';\nimport type { ListViewportRect } from './useVirtualWindow';\n\ninterface UseRowDragArgs {\n reorderable: boolean;\n treeReorder: boolean;\n onReorder?: (fromIndex: number, toIndex: number) => void;\n onTreeDrop?: (dragKey: string | number, target: TreeDropTarget) => void;\n longPressDelay: number;\n dragHandle: boolean;\n treeIndentPx: number;\n /** Current flat ordering and depth tags (mouse handlers read these directly). */\n keys: (string | number)[];\n depths: number[];\n isGroupAt: (index: number) => boolean;\n rootRef: React.RefObject<HTMLDivElement | null>;\n /** Latest keys for async (touch / rAF) consumers — owned by useVirtualWindow. */\n keysRef: React.RefObject<(string | number)[]>;\n /** Where the list is on screen, which is not its own box once it rides a\n * scrollport — owned by useVirtualWindow, as is `scrollBy`. */\n getVisibleRect: () => ListViewportRect | null;\n scrollBy: (dy: number) => void;\n}\n\n/** The drag-related props the parent spreads onto a given row. */\nexport interface RowDragProps {\n reorderable: boolean;\n /** Native mouse DnD is active for the list (independent of which row is armed). */\n mouseDragEnabled: boolean;\n draggable: boolean | undefined;\n isDragging: boolean;\n onMouseDown: ((e: React.MouseEvent<HTMLDivElement>) => void) | undefined;\n onMouseOver: ((e: React.MouseEvent<HTMLDivElement>) => void) | undefined;\n onMouseLeave: ((e: React.MouseEvent<HTMLDivElement>) => void) | undefined;\n onDragStart: ((e: React.DragEvent<HTMLDivElement>) => void) | undefined;\n onDragOver: ((e: React.DragEvent<HTMLDivElement>) => void) | undefined;\n onDragEnd: (() => void) | undefined;\n onDrop: ((e: React.DragEvent<HTMLDivElement>) => void) | undefined;\n onTouchStart: ((e: React.TouchEvent<HTMLDivElement>) => void) | undefined;\n dropPosition: TreeDropPosition | undefined;\n dropIndentPx: number | undefined;\n}\n\ninterface UseRowDragResult {\n /** True while a row is being dragged (for the root reordering style). */\n draggingKey: string | number | null;\n /** Whether either drag mode is active. */\n dragEnabled: boolean;\n /** Drag props for the row at `index` / `key`. */\n getRowDragProps: (index: number, key: string | number) => RowDragProps;\n /** Returns true (and resets) if the next click should be swallowed (post-drag). */\n consumeSuppressedClick: () => boolean;\n}\n\n/**\n * Row drag-and-drop for both modes:\n * - `reorderable` (flat): native HTML5 drag on desktop; rows move live via\n * `onReorder` with midpoint hysteresis.\n * - `treeReorder`: a drop indicator (before/after/inside) is shown during the\n * drag and `onTreeDrop` fires once on release; dropping onto the dragged\n * node's own subtree is forbidden.\n * Touch uses a long-press → lifted-clone → pointer-move path with edge\n * auto-scroll. `dragHandle` restricts initiation to `[data-drag-handle]`.\n */\nexport function useRowDrag({\n reorderable,\n treeReorder,\n onReorder,\n onTreeDrop,\n longPressDelay,\n dragHandle,\n treeIndentPx,\n keys,\n depths,\n isGroupAt,\n rootRef,\n keysRef,\n getVisibleRect,\n scrollBy,\n}: UseRowDragArgs): UseRowDragResult {\n // The dragged item is tracked by its stable key (indices shift after each\n // live reorder); the ref drives the logic, the state drives the row's style.\n const dragKeyRef = useRef<string | number | null>(null);\n const [draggingKey, setDraggingKey] = useState<string | number | null>(null);\n // Tree-reorder drop indicator. Unlike flat reorder (which moves rows live),\n // tree mode shows where the drop will land and commits once on release.\n const [dropTarget, setDropTarget] = useState<TreeDropTarget | null>(null);\n const dropTargetRef = useRef<TreeDropTarget | null>(null);\n const onReorderRef = useRef(onReorder);\n const onTreeDropRef = useRef(onTreeDrop);\n const treeModeRef = useRef(treeReorder);\n const depthsRef = useRef<number[]>(depths);\n const isGroupAtRef = useRef<(i: number) => boolean>(isGroupAt);\n // When `dragHandle` is on, a mousedown sets this to whether it landed on a\n // `[data-drag-handle]` element, gating whether the ensuing dragstart is honoured.\n const dragFromHandleRef = useRef(true);\n // In `dragHandle` mode the `draggable` attribute is only on the row whose\n // handle the pointer is currently over (\"armed\"). A permanently `draggable`\n // row breaks mouse text selection (caret placement, double-click word select,\n // drag select) inside any child input in Firefox — see Mozilla bugs 739071 /\n // 800050 / 1189486. Arming on hover rather than on mousedown means the\n // attribute is already in place before the press, so the browser never has to\n // notice it appearing mid-gesture, and any pointer move re-derives it.\n const armedKeyRef = useRef<string | number | null>(null);\n const [armedKey, setArmedKeyState] = useState<string | number | null>(null);\n const longPressRef = useRef<number | null>(null);\n const touchActiveRef = useRef(false);\n const touchStartYRef = useRef<number | null>(null);\n const pointerXRef = useRef(0);\n const pointerYRef = useRef(0);\n const autoScrollRef = useRef<number | null>(null);\n const suppressNextClickRef = useRef(false);\n // Floating clone that follows the finger during a touch drag.\n const dragCloneRef = useRef<HTMLElement | null>(null);\n const grabOffsetYRef = useRef(0);\n const cloneLeftRef = useRef(0);\n const dragHeightRef = useRef(0);\n // The native `draggable` attribute (mouse DnD) makes a long-press start the\n // browser's own drag on touch devices, which hijacks our touch handlers. So\n // expose it only where the primary pointer is fine (mouse/trackpad); touch\n // devices use the touch handlers below instead.\n const [isCoarsePointer] = useState(() =>\n typeof window !== 'undefined' && typeof window.matchMedia === 'function'\n ? window.matchMedia('(pointer: coarse)').matches\n : false,\n );\n\n useLayoutEffect(() => {\n onReorderRef.current = onReorder;\n onTreeDropRef.current = onTreeDrop;\n treeModeRef.current = treeReorder;\n depthsRef.current = depths;\n dropTargetRef.current = dropTarget;\n isGroupAtRef.current = isGroupAt;\n });\n\n // ── Mouse / shared start ──────────────────────────────────────────────────\n\n /** Move the `draggable` attribute to `next` (or off entirely for `null`). */\n function arm(next: string | number | null) {\n // Arming exists purely to place the native `draggable` attribute, which is\n // never set on a coarse pointer (touch uses the handlers below instead). A\n // tap still synthesises mouseover/mousedown, so without this a touch device\n // would re-render the list on every handle tap to no effect whatsoever.\n if (isCoarsePointer) return;\n if (armedKeyRef.current === next) return; // mouseover fires constantly — no-op\n armedKeyRef.current = next;\n setArmedKeyState(next);\n }\n\n // Arm while the pointer rests on this row's handle, disarm as it moves off.\n // `buttons !== 0` means a press is already in progress: the pointer can drift\n // off the handle before the browser reaches its drag threshold, so hover is\n // ignored until release rather than disarming a drag that is about to start.\n function handleRowMouseOver(key: string | number, e: React.MouseEvent<HTMLDivElement>) {\n if (e.buttons !== 0) return;\n arm((e.target as HTMLElement).closest('[data-drag-handle]') ? key : null);\n }\n\n function handleRowMouseLeave(e: React.MouseEvent<HTMLDivElement>) {\n if (e.buttons !== 0) return;\n arm(null);\n }\n\n // With `dragHandle`, remember on mousedown whether the press landed on a\n // handle, so the dragstart that follows can be ignored if it didn't. Arming\n // here too covers a handle that came to sit under an already-stationary cursor\n // (the list scrolled), which produces no mouseover.\n function handleRowMouseDown(key: string | number, e: React.MouseEvent<HTMLDivElement>) {\n if (e.button !== 0) return; // only a primary press can begin a drag\n const fromHandle = !!(e.target as HTMLElement).closest('[data-drag-handle]');\n dragFromHandleRef.current = fromHandle;\n if (fromHandle) arm(key);\n }\n\n function handleReorderStart(key: string | number, e: React.DragEvent<HTMLDivElement>) {\n if (dragHandle && !dragFromHandleRef.current) {\n e.preventDefault(); // not started from a handle — let the row stay put\n return;\n }\n dragKeyRef.current = key;\n setDraggingKey(key);\n if (treeReorder) setDropTarget(null);\n }\n\n // ── Tree mode ─────────────────────────────────────────────────────────────\n\n /** Resolve the tree drop target for the row at `overIndex` under pointer `y`. */\n function computeTreeDrop(overIndex: number, y: number, rect: DOMRect): TreeDropTarget | null {\n const dragKey = dragKeyRef.current;\n if (dragKey == null) return null;\n const fromIndex = keys.indexOf(dragKey);\n // Forbid dropping onto the dragged node or anything in its own subtree.\n if (fromIndex !== -1 && overIndex >= fromIndex && overIndex <= lastDescendantIndex(fromIndex, depths)) {\n return null;\n }\n const position = resolveTreeDropTarget(y, rect.top, rect.height, isGroupAt(overIndex));\n return { key: keys[overIndex], position };\n }\n\n function handleTreeDragOver(overIndex: number, e: React.DragEvent<HTMLDivElement>) {\n e.preventDefault(); // allow the drop\n const rect = e.currentTarget.getBoundingClientRect();\n const next = computeTreeDrop(overIndex, e.clientY, rect);\n setDropTarget((prev) => (prev?.key === next?.key && prev?.position === next?.position ? prev : next));\n }\n\n function commitTreeDrop() {\n const dragKey = dragKeyRef.current;\n const target = dropTargetRef.current;\n if (dragKey != null && target && target.key !== dragKey) onTreeDrop?.(dragKey, target);\n }\n\n function handleTreeDrop(e: React.DragEvent<HTMLDivElement>) {\n e.preventDefault();\n commitTreeDrop();\n setDropTarget(null);\n dragKeyRef.current = null;\n setDraggingKey(null);\n arm(null);\n }\n\n function handleTreeDragEnd() {\n // Fires after `drop` (or on its own when released off a valid target).\n setDropTarget(null);\n dragKeyRef.current = null;\n setDraggingKey(null);\n arm(null);\n }\n\n // ── Flat mode ─────────────────────────────────────────────────────────────\n\n function handleReorderOver(overIndex: number, e: React.DragEvent<HTMLDivElement>) {\n e.preventDefault(); // allow the drop\n const dragKey = dragKeyRef.current;\n if (dragKey == null) return;\n const fromIndex = keys.indexOf(dragKey);\n // Midpoint hysteresis (see resolveReorderTarget): a move only happens once\n // the pointer crosses the centre of the row it's over. After a swap the\n // dragged row sits under the cursor, so moving again needs real pointer\n // travel — this stops the oscillation that made dragging feel jumpy.\n const rect = e.currentTarget.getBoundingClientRect();\n const target = resolveReorderTarget(fromIndex, overIndex, e.clientY, rect.top, rect.height);\n if (target != null) onReorder?.(fromIndex, target);\n }\n\n function handleReorderEnd() {\n dragKeyRef.current = null;\n setDraggingKey(null);\n arm(null);\n }\n\n // ── Touch reordering (long-press to arm, then drag) ──────────────────────────\n // These handlers run from window listeners during a touch drag, so they read\n // mutable state from refs and are stable across renders.\n\n const reorderAtPoint = useCallback(\n (x: number) => {\n const dragKey = dragKeyRef.current;\n const root = rootRef.current;\n if (dragKey == null || !root) return;\n // Reorder against the centre of the lifted clone, not the raw finger — the\n // finger sits below the clone's top by the grab offset, so using it would\n // make the top (and bottom) rows unreachable. The clone is what the user\n // aims with, so match it.\n const y = dragCloneRef.current\n ? pointerYRef.current - grabOffsetYRef.current + dragHeightRef.current / 2\n : pointerYRef.current;\n const el = document.elementFromPoint(x, y) as HTMLElement | null;\n const rowEl = el && root.contains(el) ? el.closest<HTMLElement>('[data-index]') : null;\n if (!rowEl) return;\n const overIndex = Number(rowEl.dataset.index);\n if (Number.isNaN(overIndex)) return;\n const fromIndex = keysRef.current.indexOf(dragKey);\n const rect = rowEl.getBoundingClientRect();\n if (treeModeRef.current) {\n // Tree mode: don't move live — just track where the drop will land.\n const ks = keysRef.current;\n const ds = depthsRef.current;\n if (fromIndex !== -1 && overIndex >= fromIndex && overIndex <= lastDescendantIndex(fromIndex, ds)) {\n setDropTarget(null);\n return;\n }\n const position = resolveTreeDropTarget(y, rect.top, rect.height, isGroupAtRef.current(overIndex));\n const next: TreeDropTarget = { key: ks[overIndex], position };\n setDropTarget((prev) => (prev?.key === next.key && prev?.position === next.position ? prev : next));\n return;\n }\n const target = resolveReorderTarget(fromIndex, overIndex, y, rect.top, rect.height);\n if (target != null) onReorderRef.current?.(fromIndex, target);\n },\n [rootRef, keysRef],\n );\n\n // Keep the floating clone glued under the finger (same grab point).\n const positionDragClone = useCallback(() => {\n const clone = dragCloneRef.current;\n if (!clone) return;\n clone.style.transform = `translate(${cloneLeftRef.current}px, ${pointerYRef.current - grabOffsetYRef.current}px)`;\n }, []);\n\n // While the finger sits near a top/bottom edge (and within the list's width),\n // scroll the list and keep reordering against the rows now passing under it.\n const autoScrollTick = useCallback(() => {\n const rect = touchActiveRef.current ? getVisibleRect() : null;\n if (!rect) {\n autoScrollRef.current = null;\n return;\n }\n const x = pointerXRef.current;\n const y = pointerYRef.current;\n const EDGE = 48;\n let dy = 0;\n // Only auto-scroll while the finger is actually over the list — moving it\n // outside (any direction) stops the scroll instead of running away; pinning\n // the dragged row keeps touch events alive so it resumes when you come back.\n const inside = x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;\n if (inside) {\n if (y < rect.top + EDGE) dy = -Math.min(18, Math.ceil((rect.top + EDGE - y) / 3));\n else if (y > rect.bottom - EDGE) dy = Math.min(18, Math.ceil((y - (rect.bottom - EDGE)) / 3));\n }\n if (dy !== 0) {\n scrollBy(dy);\n reorderAtPoint(x);\n }\n autoScrollRef.current = requestAnimationFrame(autoScrollTick);\n }, [reorderAtPoint, getVisibleRect, scrollBy]);\n\n const endTouchDrag = useCallback(() => {\n if (longPressRef.current != null) {\n clearTimeout(longPressRef.current);\n longPressRef.current = null;\n }\n window.removeEventListener('touchmove', handleTouchMove);\n window.removeEventListener('touchend', endTouchDrag);\n window.removeEventListener('touchcancel', endTouchDrag);\n if (autoScrollRef.current != null) {\n cancelAnimationFrame(autoScrollRef.current);\n autoScrollRef.current = null;\n }\n if (dragCloneRef.current) {\n dragCloneRef.current.remove();\n dragCloneRef.current = null;\n }\n // Restore native scrolling that was locked for the duration of the drag.\n document.body.style.touchAction = '';\n document.body.style.overscrollBehavior = '';\n if (touchActiveRef.current) {\n // A real drag happened — swallow the click the browser fires afterwards\n // so it doesn't, e.g., toggle row selection.\n suppressNextClickRef.current = true;\n setTimeout(() => {\n suppressNextClickRef.current = false;\n }, 350);\n // Tree mode commits the pending drop on release (flat mode already moved\n // rows live during the drag, so there's nothing to commit here).\n if (treeModeRef.current) {\n const dk = dragKeyRef.current;\n const target = dropTargetRef.current;\n if (dk != null && target && target.key !== dk) onTreeDropRef.current?.(dk, target);\n }\n }\n setDropTarget(null);\n touchActiveRef.current = false;\n touchStartYRef.current = null;\n dragKeyRef.current = null;\n setDraggingKey(null);\n // handleTouchMove and endTouchDrag remove each other’s listeners, so neither can list\n // the other without a cycle.\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const handleTouchMove = useCallback(\n (e: TouchEvent) => {\n const t = e.touches[0];\n if (!t) return;\n pointerXRef.current = t.clientX;\n pointerYRef.current = t.clientY;\n if (!touchActiveRef.current) {\n // Before the long-press fires: if the finger travels, the user means to\n // scroll, so disarm and let the browser handle it (we don't preventDefault).\n const sy = touchStartYRef.current;\n if (sy != null && Math.abs(t.clientY - sy) > 10 && longPressRef.current != null) {\n clearTimeout(longPressRef.current);\n longPressRef.current = null;\n }\n return;\n }\n // Non-passive listener: preventDefault cancels the native scroll for this\n // gesture — the only reliable way once touch-action has been latched at\n // touchstart. This is what makes the drag actually move on touch devices.\n if (e.cancelable) e.preventDefault();\n positionDragClone();\n reorderAtPoint(t.clientX);\n },\n [reorderAtPoint, positionDragClone],\n );\n\n function handleRowTouchStart(key: string | number, e: React.TouchEvent<HTMLDivElement>) {\n if (!reorderable && !treeReorder) return;\n // With a drag handle, only a press that starts on it arms the drag, so the\n // rest of the row stays free for tapping/typing into inputs.\n if (dragHandle && !(e.target as HTMLElement).closest('[data-drag-handle]')) return;\n if (longPressRef.current != null || touchActiveRef.current) return; // ignore extra fingers\n const t = e.touches[0];\n if (!t) return;\n touchStartYRef.current = t.clientY;\n pointerXRef.current = t.clientX;\n pointerYRef.current = t.clientY;\n window.addEventListener('touchmove', handleTouchMove, { passive: false });\n window.addEventListener('touchend', endTouchDrag);\n window.addEventListener('touchcancel', endTouchDrag);\n longPressRef.current = window.setTimeout(() => {\n longPressRef.current = null;\n touchActiveRef.current = true;\n dragKeyRef.current = key;\n setDraggingKey(key);\n navigator.vibrate?.(10);\n // Lock native scrolling for the whole gesture — relying on per-move\n // preventDefault isn't enough once the finger leaves and re-enters the\n // list (the browser commits to a scroll and ignores it). We do all the\n // scrolling ourselves via autoScrollTick.\n document.body.style.touchAction = 'none';\n document.body.style.overscrollBehavior = 'none';\n createDragClone(key);\n if (autoScrollRef.current == null) autoScrollRef.current = requestAnimationFrame(autoScrollTick);\n }, longPressDelay);\n }\n\n // Build a floating copy of the picked-up row that tracks the finger, so the\n // drag has a visible \"lifted\" element (the original row stays dimmed in place\n // as a drop placeholder).\n function createDragClone(key: string | number) {\n const root = rootRef.current;\n if (!root) return;\n const idx = keysRef.current.indexOf(key);\n const src = root.querySelector<HTMLElement>(`[data-index=\"${idx}\"]`);\n if (!src) return;\n const r = src.getBoundingClientRect();\n grabOffsetYRef.current = pointerYRef.current - r.top;\n cloneLeftRef.current = r.left;\n dragHeightRef.current = r.height;\n const clone = src.cloneNode(true) as HTMLElement;\n clone.removeAttribute('data-index');\n clone.setAttribute('data-drag-clone', 'true');\n clone.setAttribute('aria-hidden', 'true');\n clone.classList.add(styles.virtualListDragClone);\n Object.assign(clone.style, {\n position: 'fixed',\n left: '0',\n top: '0',\n width: `${r.width}px`,\n height: `${r.height}px`,\n margin: '0',\n opacity: '1',\n pointerEvents: 'none',\n // Above the sticky header (z-index 2) and any focused row (z-index 1).\n zIndex: '3',\n background: 'var(--background)',\n boxShadow: 'var(--card-shadow)',\n borderRadius: 'var(--default-border-radius)',\n } as Partial<CSSStyleDeclaration>);\n // Appended inside the list so it inherits the active theme's CSS variables;\n // position:fixed keeps it in viewport coordinates regardless.\n root.appendChild(clone);\n dragCloneRef.current = clone;\n positionDragClone();\n }\n\n // Tear down any in-flight touch drag if the component unmounts mid-gesture.\n useEffect(() => endTouchDrag, [endTouchDrag]);\n\n // Either drag mode arms the same row gestures; the handlers above branch on\n // `treeReorder` for the differing target resolution / commit behaviour.\n const dragEnabled = reorderable || treeReorder;\n // Native mouse DnD is in play for this list. Row-level fallbacks that must\n // cover *every* row — swallowing the browser's own drop and context menu —\n // key off this, not off `draggable`, which in `dragHandle` mode is only ever\n // on the single armed row.\n const mouseDragEnabled = dragEnabled && !isCoarsePointer;\n\n function getRowDragProps(index: number, key: string | number): RowDragProps {\n // The drop indicator only renders on the row currently targeted. `before`/\n // `after` lines indent to the target's depth so they sit under where the\n // dropped content will start; `inside` highlights the whole row instead.\n const rowDrop = treeReorder && dropTarget?.key === key ? dropTarget.position : undefined;\n return {\n // Drives the row-wide grab cursor. With `dragHandle`, only the handle\n // initiates a drag, so the cursor belongs on the handle (styled by the\n // consumer), not across the whole row.\n reorderable: dragEnabled && !dragHandle,\n mouseDragEnabled,\n // In `dragHandle` mode the attribute only goes on the row the pointer has\n // armed, so rows stay text-selectable in Firefox the rest of the time.\n draggable: (mouseDragEnabled && (!dragHandle || key === armedKey)) || undefined,\n isDragging: dragEnabled && key === draggingKey,\n onMouseDown: dragEnabled && dragHandle ? (e) => handleRowMouseDown(key, e) : undefined,\n onMouseOver: dragEnabled && dragHandle ? (e) => handleRowMouseOver(key, e) : undefined,\n onMouseLeave: dragEnabled && dragHandle ? handleRowMouseLeave : undefined,\n onDragStart: dragEnabled ? (e) => handleReorderStart(key, e) : undefined,\n onDragOver: treeReorder\n ? (e) => handleTreeDragOver(index, e)\n : reorderable\n ? (e) => handleReorderOver(index, e)\n : undefined,\n onDragEnd: treeReorder ? handleTreeDragEnd : reorderable ? handleReorderEnd : undefined,\n onDrop: treeReorder ? handleTreeDrop : undefined,\n onTouchStart: dragEnabled ? (e) => handleRowTouchStart(key, e) : undefined,\n dropPosition: rowDrop,\n dropIndentPx: rowDrop ? (depths[index] ?? 0) * treeIndentPx : undefined,\n };\n }\n\n function consumeSuppressedClick(): boolean {\n if (suppressNextClickRef.current) {\n suppressNextClickRef.current = false;\n return true;\n }\n return false;\n }\n\n return { draggingKey, dragEnabled, getRowDragProps, consumeSuppressedClick };\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- "use client";import{ROW_ENTER_SELECTOR as e,findStartIndex as t}from"./virtualList.utils.mjs";import{useRef as n,useState as r}from"react";function i({enabled:i,rowCount:a,offsets:o,rootRef:s,headerRef:c,scrollToIndex:l,initialIndex:u,multiSelect:d,onActivate:f,onToggleSelect:p}){let[m,h]=r(u),[g,_]=r(-1),v=n(o);v.current=o;let y=i&&a>0?Math.min(Math.max(m,0),a-1):-1;y>=0&&y!==m&&h(y);function b(e){let t=s.current;if(!t)return;let n=c.current?.offsetHeight??0,r=v.current[e]??0,i=v.current[e+1]??r;r<t.scrollTop+n?l(e,{align:`start`}):i>t.scrollTop+t.clientHeight&&l(e,{align:`end`})}function x(e){let t=Math.max(0,Math.min(e,a-1));h(t),b(t)}function S(e){let n=s.current;if(!n)return 1;let r=Math.max(0,n.clientHeight-(c.current?.offsetHeight??0)),i=t(v.current,(v.current[e]??0)+r,a);return Math.max(1,i-e)}function C(t){let n=(s.current?.querySelector(`[data-index="${t}"]`))?.querySelector(e);return n?(_(t),n.focus(),!0):!1}function w(e){let t=e?.closest?.(`[data-index]`),n=Number(t?.dataset.index);return Number.isFinite(n)?n:-1}function T(e){if(e.target===e.currentTarget)return;let t=w(e.target);t<0||(_(t),h(t))}function E(e){w(e.relatedTarget)!==g&&_(-1)}function D(e){if(e.key!==`Escape`||e.defaultPrevented)return;let t=w(e.target);t>=0&&h(t),_(-1),s.current?.focus(),e.preventDefault()}function O(e){if(e.target!==e.currentTarget){D(e);return}switch(e.key){case`ArrowDown`:x(y+1);break;case`ArrowUp`:x(y-1);break;case`Home`:x(0);break;case`End`:x(a-1);break;case`PageDown`:x(y+S(y));break;case`PageUp`:x(y-S(Math.max(0,y-1)));break;case`Enter`:if(f)f(y);else if(!C(y))return;break;case`F2`:if(!C(y))return;break;case` `:if(!d)return;p(y);break;default:return}e.preventDefault()}return y<0?{activeIndex:y,enteredIndex:-1,setActiveIndex:h,containerProps:{}}:{activeIndex:y,enteredIndex:g,setActiveIndex:h,containerProps:{tabIndex:0,onKeyDown:O,onFocus:T,onBlur:E}}}export{i as useRowNavigation};
1
+ "use client";import{ROW_ENTER_SELECTOR as e,ROW_FOCUSABLE_SELECTOR as t,findStartIndex as n}from"./virtualList.utils.mjs";import{useLayoutEffect as r,useRef as i,useState as a}from"react";function o({enabled:o,tabAcrossRows:s,rowCount:c,offsets:l,rootRef:u,headerRef:d,getVisibleSlice:f,viewportHeight:p,scrollToIndex:m,initialIndex:h,multiSelect:g,onActivate:_,onToggleSelect:v}){let[y,b]=a(h),[x,S]=a(-1),C=i(l);C.current=l;let w=o&&c>0?Math.min(Math.max(y,0),c-1):-1;w>=0&&w!==y&&b(w);let T=w>=0;function E(e){let t=d.current?.offsetHeight??0,n=C.current[e]??0,r=C.current[e+1]??n,{top:i,height:a}=f();n<i+t?m(e,{align:`start`}):r>i+a&&m(e,{align:`end`})}function D(e){let t=Math.max(0,Math.min(e,c-1));b(t),E(t)}function O(e){let t=Math.max(0,p-(d.current?.offsetHeight??0)),r=n(C.current,(C.current[e]??0)+t,c);return Math.max(1,r-e)}function k(e){return u.current?.querySelector(`[data-index="${e}"]`)??null}function A(t){let n=k(t)?.querySelector(e);return n?(S(t),n.focus(),!0):!1}let j=i(null),M=i(!1);r(()=>{let t=j.current;if(!t)return;j.current=null;let n=k(t.index)?.querySelectorAll(e);if(n?.length){(t.edge===`first`?n[0]:n[n.length-1]).focus();return}let r=t.index+(t.edge===`first`?1:-1);if(r>=0&&r<c){j.current={index:r,edge:t.edge},S(r),E(r);return}S(-1),M.current=T&&t.edge===`first`,T&&u.current?.focus()});function N(e){let t=e?.closest?.(`[data-index]`),n=Number(t?.dataset.index);return Number.isFinite(n)?n:-1}function P(e){if(e.target===e.currentTarget)return;let t=N(e.target);t<0||(S(t),b(t))}function F(e){e.currentTarget.contains(e.relatedTarget)||(M.current=!1),N(e.relatedTarget)!==x&&S(-1)}function I(e){if(e.key!==`Escape`||e.defaultPrevented||!T)return;let t=N(e.target);t>=0&&b(t),S(-1),u.current?.focus(),e.preventDefault()}function L(e){if(!s||e.key!==`Tab`||e.defaultPrevented||e.altKey||e.ctrlKey||e.metaKey)return;let n=N(e.target);if(n<0)return;let r=k(n)?.querySelectorAll(t);if(!r?.length)return;let i=Array.prototype.indexOf.call(r,e.target);if(i<0)return;let a=e.shiftKey;if(a?i>0:i<r.length-1)return;let o=a?n-1:n+1;o<0||o>=c||(e.preventDefault(),S(o),E(o),j.current={index:o,edge:a?`last`:`first`})}function R(e){if(e.target!==e.currentTarget){L(e),I(e);return}if(!T||e.defaultPrevented)return;let t=M.current;switch(M.current=!1,e.key){case`ArrowDown`:D(w+1);break;case`ArrowUp`:D(w-1);break;case`Home`:D(0);break;case`End`:D(c-1);break;case`PageDown`:D(w+O(w));break;case`PageUp`:D(w-O(Math.max(0,w-1)));break;case`Enter`:if(_)_(w);else if(!A(w))return;break;case`F2`:if(!A(w))return;break;case`Tab`:if(!s||e.shiftKey||t||!A(w))return;break;case` `:if(!g)return;v(w);break;default:return}e.preventDefault()}if(!T&&!s)return{activeIndex:w,enteredIndex:-1,setActiveIndex:b,containerProps:{}};let z={onKeyDown:R,onFocus:P,onBlur:F};return T&&(z.tabIndex=0),{activeIndex:w,enteredIndex:x,setActiveIndex:b,containerProps:z}}export{o as useRowNavigation};
2
2
  //# sourceMappingURL=useRowNavigation.mjs.map