@ahrowe/ui 0.29.1 → 0.31.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 (129) hide show
  1. package/dist/esm/common/accordion/accordion.mjs +1 -1
  2. package/dist/esm/common/accordion/accordion.mjs.map +1 -1
  3. package/dist/esm/common/accordion/accordion.module.mjs.map +1 -1
  4. package/dist/esm/common/accordion/accordion.plain.module.mjs.map +1 -1
  5. package/dist/esm/common/accordion/accordion.primary.module.mjs.map +1 -1
  6. package/dist/esm/common/alert/alert.module.mjs.map +1 -1
  7. package/dist/esm/common/badge/badge.module.mjs.map +1 -1
  8. package/dist/esm/common/bodyEnd/bodyEnd.mjs +1 -1
  9. package/dist/esm/common/bodyEnd/bodyEnd.mjs.map +1 -1
  10. package/dist/esm/common/button/button.module.mjs +1 -1
  11. package/dist/esm/common/button/button.module.mjs.map +1 -1
  12. package/dist/esm/common/card/card.module.mjs.map +1 -1
  13. package/dist/esm/common/checkbox/checkbox.mjs +1 -1
  14. package/dist/esm/common/checkbox/checkbox.mjs.map +1 -1
  15. package/dist/esm/common/checkbox/checkbox.module.mjs +1 -1
  16. package/dist/esm/common/checkbox/checkbox.module.mjs.map +1 -1
  17. package/dist/esm/common/chip/chip.module.mjs.map +1 -1
  18. package/dist/esm/common/colorPicker/colorPicker.mjs +1 -1
  19. package/dist/esm/common/colorPicker/colorPicker.mjs.map +1 -1
  20. package/dist/esm/common/colorPicker/colorPicker.module.mjs.map +1 -1
  21. package/dist/esm/common/colorPicker/toHex.mjs +2 -0
  22. package/dist/esm/common/colorPicker/toHex.mjs.map +1 -0
  23. package/dist/esm/common/datePicker/datePicker.mjs +1 -1
  24. package/dist/esm/common/datePicker/datePicker.mjs.map +1 -1
  25. package/dist/esm/common/fab/components/fabBase/fabBase.mjs +1 -1
  26. package/dist/esm/common/fab/components/fabBase/fabBase.mjs.map +1 -1
  27. package/dist/esm/common/fab/components/fabBase/fabBase.module.mjs.map +1 -1
  28. package/dist/esm/common/fab/fab.module.mjs.map +1 -1
  29. package/dist/esm/common/iconPicker/iconPicker.module.mjs.map +1 -1
  30. package/dist/esm/common/input/input.mjs +1 -1
  31. package/dist/esm/common/input/input.mjs.map +1 -1
  32. package/dist/esm/common/input/input.module.mjs +1 -1
  33. package/dist/esm/common/input/input.module.mjs.map +1 -1
  34. package/dist/esm/common/inputDropdown/inputDropdown.mjs +1 -1
  35. package/dist/esm/common/inputDropdown/inputDropdown.mjs.map +1 -1
  36. package/dist/esm/common/kanbanBoard/kanbanBoard.module.mjs.map +1 -1
  37. package/dist/esm/common/optionPicker/optionPicker.module.mjs.map +1 -1
  38. package/dist/esm/common/otpInput/otpInput.module.mjs.map +1 -1
  39. package/dist/esm/common/pagination/pagination.module.mjs.map +1 -1
  40. package/dist/esm/common/progressBar/progressBar.module.mjs +1 -1
  41. package/dist/esm/common/progressBar/progressBar.module.mjs.map +1 -1
  42. package/dist/esm/common/radioGroup/radioGroup.mjs +1 -1
  43. package/dist/esm/common/radioGroup/radioGroup.mjs.map +1 -1
  44. package/dist/esm/common/radioGroup/radioGroup.module.mjs.map +1 -1
  45. package/dist/esm/common/rating/rating.module.mjs.map +1 -1
  46. package/dist/esm/common/searchInput/searchInput.mjs +1 -1
  47. package/dist/esm/common/searchInput/searchInput.mjs.map +1 -1
  48. package/dist/esm/common/slider/slider.mjs +1 -1
  49. package/dist/esm/common/slider/slider.mjs.map +1 -1
  50. package/dist/esm/common/slider/slider.module.mjs.map +1 -1
  51. package/dist/esm/common/switch/switch.module.mjs.map +1 -1
  52. package/dist/esm/common/tabHeader/tabHeader.module.mjs.map +1 -1
  53. package/dist/esm/common/themeProvider/theme.module.mjs.map +1 -1
  54. package/dist/esm/common/themeProvider/themeProvider.mjs +1 -1
  55. package/dist/esm/common/themeProvider/themeProvider.mjs.map +1 -1
  56. package/dist/esm/common/themeProvider/themeVariablesContext.mjs +2 -0
  57. package/dist/esm/common/themeProvider/themeVariablesContext.mjs.map +1 -0
  58. package/dist/esm/common/timeInput/timeInput.mjs +1 -1
  59. package/dist/esm/common/timeInput/timeInput.mjs.map +1 -1
  60. package/dist/esm/common/timeline/timeline.mjs +1 -1
  61. package/dist/esm/common/timeline/timeline.mjs.map +1 -1
  62. package/dist/esm/common/timeline/timeline.module.mjs +1 -1
  63. package/dist/esm/common/timeline/timeline.module.mjs.map +1 -1
  64. package/dist/esm/common/timer/timer.mjs +1 -1
  65. package/dist/esm/common/timer/timer.mjs.map +1 -1
  66. package/dist/esm/common/timer/timer.module.mjs.map +1 -1
  67. package/dist/esm/common/toast/toast.module.mjs.map +1 -1
  68. package/dist/esm/common/tooltip/tooltip.module.mjs.map +1 -1
  69. package/dist/esm/common/tree/tree.mjs +1 -1
  70. package/dist/esm/common/tree/tree.mjs.map +1 -1
  71. package/dist/esm/common/virtualList/useColumnResize.mjs +2 -0
  72. package/dist/esm/common/virtualList/useColumnResize.mjs.map +1 -0
  73. package/dist/esm/common/virtualList/useColumns.mjs +1 -1
  74. package/dist/esm/common/virtualList/useColumns.mjs.map +1 -1
  75. package/dist/esm/common/virtualList/useHeaderNavigation.mjs +2 -0
  76. package/dist/esm/common/virtualList/useHeaderNavigation.mjs.map +1 -0
  77. package/dist/esm/common/virtualList/useRowNavigation.mjs +2 -0
  78. package/dist/esm/common/virtualList/useRowNavigation.mjs.map +1 -0
  79. package/dist/esm/common/virtualList/useSorting.mjs +2 -0
  80. package/dist/esm/common/virtualList/useSorting.mjs.map +1 -0
  81. package/dist/esm/common/virtualList/useVirtualWindow.mjs +1 -1
  82. package/dist/esm/common/virtualList/useVirtualWindow.mjs.map +1 -1
  83. package/dist/esm/common/virtualList/virtualList.mjs +1 -1
  84. package/dist/esm/common/virtualList/virtualList.mjs.map +1 -1
  85. package/dist/esm/common/virtualList/virtualList.module.mjs +1 -1
  86. package/dist/esm/common/virtualList/virtualList.module.mjs.map +1 -1
  87. package/dist/esm/common/virtualList/virtualList.utils.mjs +1 -1
  88. package/dist/esm/common/virtualList/virtualList.utils.mjs.map +1 -1
  89. package/dist/esm/common/virtualList/virtualListHeader.mjs +2 -0
  90. package/dist/esm/common/virtualList/virtualListHeader.mjs.map +1 -0
  91. package/dist/esm/common/virtualList/virtualRow.mjs +1 -1
  92. package/dist/esm/common/virtualList/virtualRow.mjs.map +1 -1
  93. package/dist/index.cjs +3 -3
  94. package/dist/index.cjs.map +1 -1
  95. package/dist/style.css +1 -1
  96. package/dist/types/common/colorPicker/toHex.d.ts +11 -0
  97. package/dist/types/common/themeProvider/theme.types.d.ts +75 -3
  98. package/dist/types/common/themeProvider/themeVariablesContext.d.ts +11 -0
  99. package/dist/types/common/virtualList/useColumnResize.d.ts +59 -0
  100. package/dist/types/common/virtualList/useColumns.d.ts +12 -2
  101. package/dist/types/common/virtualList/useHeaderNavigation.d.ts +22 -0
  102. package/dist/types/common/virtualList/useRowNavigation.d.ts +50 -0
  103. package/dist/types/common/virtualList/useSorting.d.ts +26 -0
  104. package/dist/types/common/virtualList/useVirtualWindow.d.ts +1 -8
  105. package/dist/types/common/virtualList/virtualList.d.ts +1 -1
  106. package/dist/types/common/virtualList/virtualList.types.d.ts +82 -1
  107. package/dist/types/common/virtualList/virtualList.utils.d.ts +83 -1
  108. package/dist/types/common/virtualList/virtualListHeader.d.ts +35 -0
  109. package/dist/types/common/virtualList/virtualRow.d.ts +14 -1
  110. package/docs/Accordion.md +4 -0
  111. package/docs/Alert.md +4 -0
  112. package/docs/BodyEnd.md +3 -0
  113. package/docs/Button.md +6 -0
  114. package/docs/Card.md +6 -0
  115. package/docs/Checkbox.md +8 -0
  116. package/docs/ColorPicker.md +11 -1
  117. package/docs/Fab.md +16 -0
  118. package/docs/IconPicker.md +8 -0
  119. package/docs/Input.md +12 -0
  120. package/docs/OtpInput.md +4 -0
  121. package/docs/RadioGroup.md +6 -0
  122. package/docs/Rating.md +5 -0
  123. package/docs/Slider.md +6 -2
  124. package/docs/ThemeProvider.md +89 -1
  125. package/docs/Timer.md +7 -1
  126. package/docs/Toast.md +6 -0
  127. package/docs/Tooltip.md +15 -0
  128. package/docs/VirtualList.md +125 -7
  129. package/package.json +2 -1
@@ -1 +1 @@
1
- {"version":3,"file":"timer.mjs","names":[],"sources":["../../../../package/common/timer/timer.tsx"],"sourcesContent":["import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';\nimport cx from 'classnames';\nimport { faPlus, faMinus, faPause, faPlay, faXmark } from '@fortawesome/free-solid-svg-icons';\n\nimport ActionIcon from '../actionIcon';\nimport Button from '../button';\nimport { ButtonSize } from '../button/button.types';\nimport { useComponentDefaults } from '../configProvider';\nimport { DEFAULT_CHIME_DATA_URI } from './timer.chime';\nimport type { TimerHandle, TimerProps, TimerStatus } from './timer.types';\nimport styles from './timer.module.pcss';\n\nconst TICK_MS = 200;\nconst RIPPLE_SPAWN_INTERVAL_MS = 1000;\n\nfunction prefersReducedMotion(): boolean {\n return typeof window !== 'undefined' && window.matchMedia?.('(prefers-reduced-motion: reduce)').matches === true;\n}\n\nfunction formatTimeDefault(remainingMs: number) {\n const totalSeconds = Math.ceil(remainingMs / 1000);\n const hours = Math.floor(totalSeconds / 3600);\n const minutes = Math.floor((totalSeconds % 3600) / 60);\n const seconds = totalSeconds % 60;\n const mm = String(minutes).padStart(2, '0');\n const ss = String(seconds).padStart(2, '0');\n return hours > 0 ? `${hours}:${mm}:${ss}` : `${mm}:${ss}`;\n}\n\nconst Timer = forwardRef<TimerHandle, TimerProps>(function Timer(props, ref) {\n const {\n durationSeconds,\n autoStart = true,\n resetOnDurationChange = true,\n\n onTick,\n onComplete,\n onStart,\n onPause,\n onResume,\n onCancel,\n onAddTime,\n onRemoveTime,\n\n showControls = true,\n hideAddButton = false,\n hideRemoveButton = false,\n hidePauseButton = false,\n hideCancelButton = false,\n adjustSeconds = 30,\n maxSeconds,\n\n label,\n formatTime = formatTimeDefault,\n size = '160px',\n strokeWidth = 10,\n counterClockwise = false,\n\n urgentThreshold = 0.1,\n urgentMinSeconds = 3,\n pulseOnUrgent = true,\n showRunningRipples = false,\n\n playSoundOnComplete = false,\n soundUrl,\n soundVolume = 0.5,\n notifyOnComplete = false,\n notificationTitle,\n notificationBody,\n\n className,\n style,\n classNames,\n styles: slotStyles,\n ...rest\n } = useComponentDefaults('Timer', props);\n\n const initialMs = durationSeconds * 1000;\n const [status, setStatusState] = useState<TimerStatus>(autoStart ? 'running' : 'idle');\n const [remainingMs, setRemainingMsState] = useState(initialMs);\n const [epoch, setEpoch] = useState(0);\n const [ripples, setRipples] = useState<number[]>([]);\n\n const remainingRef = useRef(remainingMs);\n const statusRef = useRef(status);\n const totalMsRef = useRef(initialMs);\n const completedFiredRef = useRef(false);\n const notifyPermissionRequestedRef = useRef(false);\n const mountedRef = useRef(false);\n const rippleIdRef = useRef(0);\n\n function setRemaining(next: number) {\n remainingRef.current = next;\n setRemainingMsState(next);\n }\n\n function setStatus(next: TimerStatus) {\n statusRef.current = next;\n setStatusState(next);\n }\n\n function bumpEpoch() {\n setEpoch((e) => e + 1);\n }\n\n // Ticking effect. Re-derives the remaining time from Date.now() every tick (rather than\n // accumulating setInterval's own drift), so it stays accurate over long countdowns. Any\n // external mutation of remainingRef (addTime/removeTime while already running, or a resume)\n // must bump `epoch` so this effect re-arms and re-captures a fresh start/startRemaining —\n // without that, a stale `startRemaining` captured before the mutation would silently\n // overwrite it on the next tick.\n useEffect(() => {\n if (status !== 'running') return undefined;\n const start = Date.now();\n const startRemaining = remainingRef.current;\n const id = setInterval(() => {\n const next = Math.max(0, startRemaining - (Date.now() - start));\n setRemaining(next);\n onTick?.(next);\n if (next <= 0) setStatus('completed');\n }, TICK_MS);\n return () => clearInterval(id);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [status, epoch]);\n\n // Fires onComplete exactly once per completion, plus the opt-in sound/notification.\n useEffect(() => {\n if (status === 'completed' && !completedFiredRef.current) {\n completedFiredRef.current = true;\n onComplete?.();\n if (playSoundOnComplete) {\n try {\n const audio = new Audio(soundUrl || DEFAULT_CHIME_DATA_URI);\n audio.volume = soundVolume;\n // play() can reject (autoplay policy) or, in some environments, return undefined\n // instead of a Promise — never let a blocked/unsupported playback surface as an error.\n audio.play()?.catch(() => {});\n } catch {\n // Audio unavailable/unsupported — fail silently.\n }\n }\n if (notifyOnComplete && typeof Notification !== 'undefined' && Notification.permission === 'granted') {\n try {\n new Notification(notificationTitle ?? 'Timer done', {\n body: notificationBody ?? (typeof label === 'string' ? label : undefined),\n });\n } catch {\n // Some browsers (e.g. certain mobile Safari versions) throw constructing\n // Notification outside specific contexts — fail silently, as documented.\n }\n }\n }\n if (status !== 'completed') completedFiredRef.current = false;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [status]);\n\n // durationSeconds prop change: reset (and optionally restart) at the new duration. Skips\n // the initial mount, since autoStart/initial state already cover that.\n useEffect(() => {\n if (!mountedRef.current) {\n mountedRef.current = true;\n return;\n }\n const nextMs = durationSeconds * 1000;\n totalMsRef.current = nextMs;\n setRemaining(nextMs);\n completedFiredRef.current = false;\n bumpEpoch();\n if (resetOnDurationChange) {\n setStatus(autoStart ? 'running' : 'idle');\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [durationSeconds]);\n\n // Running-ripple spawner: spawns a new ripple roughly every RIPPLE_SPAWN_INTERVAL_MS while\n // running, each animating once (via CSS, `forwards`) and removing itself on `onAnimationEnd`.\n // Pausing (or otherwise leaving `running`) only stops new spawns here — it doesn't touch\n // `ripples` — so whichever ripples are already mid-flight keep animating to completion\n // instead of vanishing mid-cycle.\n useEffect(() => {\n if (!showRunningRipples || status !== 'running' || prefersReducedMotion()) return undefined;\n function spawnRipple() {\n rippleIdRef.current += 1;\n setRipples((prev) => [...prev, rippleIdRef.current]);\n }\n spawnRipple();\n const id = setInterval(spawnRipple, RIPPLE_SPAWN_INTERVAL_MS);\n return () => clearInterval(id);\n }, [showRunningRipples, status]);\n\n function handleRippleAnimationEnd(id: number) {\n setRipples((prev) => prev.filter((existingId) => existingId !== id));\n }\n\n function requestNotificationPermissionIfNeeded() {\n if (!notifyOnComplete || notifyPermissionRequestedRef.current) return;\n if (typeof Notification === 'undefined' || Notification.permission !== 'default') return;\n notifyPermissionRequestedRef.current = true;\n Notification.requestPermission().catch(() => {});\n }\n\n function start() {\n requestNotificationPermissionIfNeeded();\n if (statusRef.current === 'completed' || statusRef.current === 'cancelled') {\n setRemaining(totalMsRef.current);\n completedFiredRef.current = false;\n }\n bumpEpoch();\n setStatus('running');\n onStart?.();\n }\n\n function pause() {\n if (statusRef.current !== 'running') return;\n setStatus('paused');\n onPause?.();\n }\n\n function resume() {\n if (statusRef.current !== 'paused') return;\n bumpEpoch();\n setStatus('running');\n onResume?.();\n }\n\n function addTime(seconds: number) {\n const capped = maxSeconds != null ? maxSeconds * 1000 : Infinity;\n const next = Math.min(remainingRef.current + seconds * 1000, capped);\n setRemaining(next);\n bumpEpoch();\n onAddTime?.(seconds, Math.round(next / 1000));\n if (statusRef.current === 'completed' && next > 0) {\n completedFiredRef.current = false;\n setStatus('running');\n }\n }\n\n function removeTime(seconds: number) {\n const next = Math.max(0, remainingRef.current - seconds * 1000);\n setRemaining(next);\n bumpEpoch();\n onRemoveTime?.(seconds, Math.round(next / 1000));\n if (next <= 0 && statusRef.current === 'running') setStatus('completed');\n }\n\n function cancel() {\n setRemaining(totalMsRef.current);\n setStatus('cancelled');\n onCancel?.();\n }\n\n function reset() {\n setRemaining(totalMsRef.current);\n completedFiredRef.current = false;\n setStatus('idle');\n }\n\n useImperativeHandle(ref, () => ({\n start,\n pause,\n resume,\n addTime,\n removeTime,\n cancel,\n reset,\n getStatus: () => statusRef.current,\n getRemainingMs: () => remainingRef.current,\n }));\n\n const radius = 50 - strokeWidth / 2;\n const circumference = useMemo(() => 2 * Math.PI * radius, [radius]);\n const clampedFraction = totalMsRef.current > 0 ? Math.min(1, remainingMs / totalMsRef.current) : 0;\n const dashOffset = circumference * (1 - clampedFraction);\n const urgentMs = Math.max(totalMsRef.current * urgentThreshold, urgentMinSeconds * 1000);\n const isUrgent = status === 'running' && remainingMs <= urgentMs;\n const isCritical = isUrgent && remainingMs <= Math.min(5000, urgentMs);\n const isPaused = status === 'paused';\n\n // The pause button doubles as a play/restart button whenever there's nothing to pause —\n // idle (autoStart={false}), completed, or cancelled — rather than hiding itself, so there's\n // always a way to (re)start the countdown from the built-in controls alone.\n const showAddButton = showControls && !hideAddButton;\n const showRemoveButton = showControls && !hideRemoveButton;\n const showPauseButton = showControls && !hidePauseButton;\n const pauseButtonIcon = status === 'running' ? faPause : faPlay;\n const pauseButtonTitle =\n status === 'running' ? 'Pause' : isPaused ? 'Resume' : status === 'idle' ? 'Start' : 'Restart';\n const handlePauseButtonClick = status === 'running' ? pause : isPaused ? resume : start;\n const showCancelButton = showControls && !hideCancelButton;\n\n return (\n <div\n {...rest}\n className={cx(\n styles.timer,\n pulseOnUrgent && isUrgent && styles.timerPulsing,\n isCritical && styles.timerCritical,\n className,\n classNames?.root,\n )}\n style={\n {\n '--timer-size': size,\n '--timer-stroke-width': `${strokeWidth}px`,\n ...style,\n ...slotStyles?.root,\n } as React.CSSProperties\n }\n >\n <div className={cx(styles.timerRingWrapper, classNames?.ringWrapper)} style={slotStyles?.ringWrapper}>\n {ripples.length > 0 && (\n <div className={cx(styles.timerRipples, classNames?.ripples)} style={slotStyles?.ripples}>\n {ripples.map((id) => (\n <span\n key={id}\n className={cx(styles.timerRipple, classNames?.ripple)}\n style={slotStyles?.ripple}\n onAnimationEnd={() => handleRippleAnimationEnd(id)}\n />\n ))}\n </div>\n )}\n <svg className={cx(styles.timerRingSvg, classNames?.ringSvg)} viewBox='0 0 100 100' style={slotStyles?.ringSvg}>\n <circle\n className={cx(styles.timerRingTrack, classNames?.ringTrack)}\n cx='50'\n cy='50'\n r={radius}\n style={slotStyles?.ringTrack}\n />\n <circle\n className={cx(\n styles.timerRingProgress,\n isUrgent && styles.timerRingProgressUrgent,\n classNames?.ringProgress,\n )}\n cx='50'\n cy='50'\n r={radius}\n strokeDasharray={circumference}\n strokeDashoffset={dashOffset}\n style={{\n transition: isPaused ? 'none' : undefined,\n transform: counterClockwise ? 'rotate(-90deg)' : undefined,\n ...slotStyles?.ringProgress,\n }}\n />\n </svg>\n <div className={cx(styles.timerDisplay, classNames?.display)} style={slotStyles?.display}>\n <span className={cx(styles.timerTime, classNames?.time)} style={slotStyles?.time}>\n {formatTime(remainingMs)}\n </span>\n {label && (\n <span className={cx(styles.timerLabel, classNames?.label)} style={slotStyles?.label}>\n {label}\n </span>\n )}\n </div>\n </div>\n\n {showControls && (\n <div className={cx(styles.timerControls, classNames?.controls)} style={slotStyles?.controls}>\n <div className={styles.timerControlsRow}>\n {showRemoveButton && (\n <ActionIcon\n icon={faMinus}\n title={`Remove ${adjustSeconds}s`}\n onClick={() => removeTime(adjustSeconds)}\n className={cx(styles.timerAdjustButton, classNames?.removeButton)}\n style={slotStyles?.removeButton}\n />\n )}\n {showPauseButton && (\n <Button\n size={ButtonSize.Small}\n icon={pauseButtonIcon}\n title={pauseButtonTitle}\n onClick={handlePauseButtonClick}\n className={classNames?.pauseButton}\n style={slotStyles?.pauseButton}\n />\n )}\n {showAddButton && (\n <ActionIcon\n icon={faPlus}\n title={`Add ${adjustSeconds}s`}\n onClick={() => addTime(adjustSeconds)}\n className={cx(styles.timerAdjustButton, classNames?.addButton)}\n style={slotStyles?.addButton}\n />\n )}\n </div>\n {showCancelButton && (\n <ActionIcon\n icon={faXmark}\n title='Cancel timer'\n onClick={cancel}\n className={cx(styles.timerCancelButton, classNames?.cancelButton)}\n style={slotStyles?.cancelButton}\n />\n )}\n </div>\n )}\n </div>\n );\n});\n\nTimer.displayName = 'Timer';\n\nexport default Timer;\n"],"mappings":"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"}
1
+ {"version":3,"file":"timer.mjs","names":[],"sources":["../../../../package/common/timer/timer.tsx"],"sourcesContent":["import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';\nimport cx from 'classnames';\nimport { faPlus, faMinus, faPause, faPlay, faXmark } from '@fortawesome/free-solid-svg-icons';\n\nimport ActionIcon from '../actionIcon';\nimport Button from '../button';\nimport { ButtonSize } from '../button/button.types';\nimport { useComponentDefaults } from '../configProvider';\nimport { DEFAULT_CHIME_DATA_URI } from './timer.chime';\nimport type { TimerHandle, TimerProps, TimerStatus } from './timer.types';\nimport styles from './timer.module.pcss';\n\nconst TICK_MS = 200;\nconst RIPPLE_SPAWN_INTERVAL_MS = 1000;\n\nfunction prefersReducedMotion(): boolean {\n return typeof window !== 'undefined' && window.matchMedia?.('(prefers-reduced-motion: reduce)').matches === true;\n}\n\nfunction formatTimeDefault(remainingMs: number) {\n const totalSeconds = Math.ceil(remainingMs / 1000);\n const hours = Math.floor(totalSeconds / 3600);\n const minutes = Math.floor((totalSeconds % 3600) / 60);\n const seconds = totalSeconds % 60;\n const mm = String(minutes).padStart(2, '0');\n const ss = String(seconds).padStart(2, '0');\n return hours > 0 ? `${hours}:${mm}:${ss}` : `${mm}:${ss}`;\n}\n\nconst Timer = forwardRef<TimerHandle, TimerProps>(function Timer(props, ref) {\n const {\n durationSeconds,\n autoStart = true,\n resetOnDurationChange = true,\n\n onTick,\n onComplete,\n onStart,\n onPause,\n onResume,\n onCancel,\n onAddTime,\n onRemoveTime,\n\n showControls = true,\n hideAddButton = false,\n hideRemoveButton = false,\n hidePauseButton = false,\n hideCancelButton = false,\n adjustSeconds = 30,\n maxSeconds,\n\n label,\n formatTime = formatTimeDefault,\n size,\n strokeWidth = 10,\n counterClockwise = false,\n\n urgentThreshold = 0.1,\n urgentMinSeconds = 3,\n pulseOnUrgent = true,\n showRunningRipples = false,\n\n playSoundOnComplete = false,\n soundUrl,\n soundVolume = 0.5,\n notifyOnComplete = false,\n notificationTitle,\n notificationBody,\n\n className,\n style,\n classNames,\n styles: slotStyles,\n ...rest\n } = useComponentDefaults('Timer', props);\n\n const initialMs = durationSeconds * 1000;\n const [status, setStatusState] = useState<TimerStatus>(autoStart ? 'running' : 'idle');\n const [remainingMs, setRemainingMsState] = useState(initialMs);\n const [epoch, setEpoch] = useState(0);\n const [ripples, setRipples] = useState<number[]>([]);\n\n const remainingRef = useRef(remainingMs);\n const statusRef = useRef(status);\n const totalMsRef = useRef(initialMs);\n const completedFiredRef = useRef(false);\n const notifyPermissionRequestedRef = useRef(false);\n const mountedRef = useRef(false);\n const rippleIdRef = useRef(0);\n\n function setRemaining(next: number) {\n remainingRef.current = next;\n setRemainingMsState(next);\n }\n\n function setStatus(next: TimerStatus) {\n statusRef.current = next;\n setStatusState(next);\n }\n\n function bumpEpoch() {\n setEpoch((e) => e + 1);\n }\n\n // Ticking effect. Re-derives the remaining time from Date.now() every tick (rather than\n // accumulating setInterval's own drift), so it stays accurate over long countdowns. Any\n // external mutation of remainingRef (addTime/removeTime while already running, or a resume)\n // must bump `epoch` so this effect re-arms and re-captures a fresh start/startRemaining —\n // without that, a stale `startRemaining` captured before the mutation would silently\n // overwrite it on the next tick.\n useEffect(() => {\n if (status !== 'running') return undefined;\n const start = Date.now();\n const startRemaining = remainingRef.current;\n const id = setInterval(() => {\n const next = Math.max(0, startRemaining - (Date.now() - start));\n setRemaining(next);\n onTick?.(next);\n if (next <= 0) setStatus('completed');\n }, TICK_MS);\n return () => clearInterval(id);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [status, epoch]);\n\n // Fires onComplete exactly once per completion, plus the opt-in sound/notification.\n useEffect(() => {\n if (status === 'completed' && !completedFiredRef.current) {\n completedFiredRef.current = true;\n onComplete?.();\n if (playSoundOnComplete) {\n try {\n const audio = new Audio(soundUrl || DEFAULT_CHIME_DATA_URI);\n audio.volume = soundVolume;\n // play() can reject (autoplay policy) or, in some environments, return undefined\n // instead of a Promise — never let a blocked/unsupported playback surface as an error.\n audio.play()?.catch(() => {});\n } catch {\n // Audio unavailable/unsupported — fail silently.\n }\n }\n if (notifyOnComplete && typeof Notification !== 'undefined' && Notification.permission === 'granted') {\n try {\n new Notification(notificationTitle ?? 'Timer done', {\n body: notificationBody ?? (typeof label === 'string' ? label : undefined),\n });\n } catch {\n // Some browsers (e.g. certain mobile Safari versions) throw constructing\n // Notification outside specific contexts — fail silently, as documented.\n }\n }\n }\n if (status !== 'completed') completedFiredRef.current = false;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [status]);\n\n // durationSeconds prop change: reset (and optionally restart) at the new duration. Skips\n // the initial mount, since autoStart/initial state already cover that.\n useEffect(() => {\n if (!mountedRef.current) {\n mountedRef.current = true;\n return;\n }\n const nextMs = durationSeconds * 1000;\n totalMsRef.current = nextMs;\n setRemaining(nextMs);\n completedFiredRef.current = false;\n bumpEpoch();\n if (resetOnDurationChange) {\n setStatus(autoStart ? 'running' : 'idle');\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [durationSeconds]);\n\n // Running-ripple spawner: spawns a new ripple roughly every RIPPLE_SPAWN_INTERVAL_MS while\n // running, each animating once (via CSS, `forwards`) and removing itself on `onAnimationEnd`.\n // Pausing (or otherwise leaving `running`) only stops new spawns here — it doesn't touch\n // `ripples` — so whichever ripples are already mid-flight keep animating to completion\n // instead of vanishing mid-cycle.\n useEffect(() => {\n if (!showRunningRipples || status !== 'running' || prefersReducedMotion()) return undefined;\n function spawnRipple() {\n rippleIdRef.current += 1;\n setRipples((prev) => [...prev, rippleIdRef.current]);\n }\n spawnRipple();\n const id = setInterval(spawnRipple, RIPPLE_SPAWN_INTERVAL_MS);\n return () => clearInterval(id);\n }, [showRunningRipples, status]);\n\n function handleRippleAnimationEnd(id: number) {\n setRipples((prev) => prev.filter((existingId) => existingId !== id));\n }\n\n function requestNotificationPermissionIfNeeded() {\n if (!notifyOnComplete || notifyPermissionRequestedRef.current) return;\n if (typeof Notification === 'undefined' || Notification.permission !== 'default') return;\n notifyPermissionRequestedRef.current = true;\n Notification.requestPermission().catch(() => {});\n }\n\n function start() {\n requestNotificationPermissionIfNeeded();\n if (statusRef.current === 'completed' || statusRef.current === 'cancelled') {\n setRemaining(totalMsRef.current);\n completedFiredRef.current = false;\n }\n bumpEpoch();\n setStatus('running');\n onStart?.();\n }\n\n function pause() {\n if (statusRef.current !== 'running') return;\n setStatus('paused');\n onPause?.();\n }\n\n function resume() {\n if (statusRef.current !== 'paused') return;\n bumpEpoch();\n setStatus('running');\n onResume?.();\n }\n\n function addTime(seconds: number) {\n const capped = maxSeconds != null ? maxSeconds * 1000 : Infinity;\n const next = Math.min(remainingRef.current + seconds * 1000, capped);\n setRemaining(next);\n bumpEpoch();\n onAddTime?.(seconds, Math.round(next / 1000));\n if (statusRef.current === 'completed' && next > 0) {\n completedFiredRef.current = false;\n setStatus('running');\n }\n }\n\n function removeTime(seconds: number) {\n const next = Math.max(0, remainingRef.current - seconds * 1000);\n setRemaining(next);\n bumpEpoch();\n onRemoveTime?.(seconds, Math.round(next / 1000));\n if (next <= 0 && statusRef.current === 'running') setStatus('completed');\n }\n\n function cancel() {\n setRemaining(totalMsRef.current);\n setStatus('cancelled');\n onCancel?.();\n }\n\n function reset() {\n setRemaining(totalMsRef.current);\n completedFiredRef.current = false;\n setStatus('idle');\n }\n\n useImperativeHandle(ref, () => ({\n start,\n pause,\n resume,\n addTime,\n removeTime,\n cancel,\n reset,\n getStatus: () => statusRef.current,\n getRemainingMs: () => remainingRef.current,\n }));\n\n const radius = 50 - strokeWidth / 2;\n const circumference = useMemo(() => 2 * Math.PI * radius, [radius]);\n const clampedFraction = totalMsRef.current > 0 ? Math.min(1, remainingMs / totalMsRef.current) : 0;\n const dashOffset = circumference * (1 - clampedFraction);\n const urgentMs = Math.max(totalMsRef.current * urgentThreshold, urgentMinSeconds * 1000);\n const isUrgent = status === 'running' && remainingMs <= urgentMs;\n const isCritical = isUrgent && remainingMs <= Math.min(5000, urgentMs);\n const isPaused = status === 'paused';\n\n // The pause button doubles as a play/restart button whenever there's nothing to pause —\n // idle (autoStart={false}), completed, or cancelled — rather than hiding itself, so there's\n // always a way to (re)start the countdown from the built-in controls alone.\n const showAddButton = showControls && !hideAddButton;\n const showRemoveButton = showControls && !hideRemoveButton;\n const showPauseButton = showControls && !hidePauseButton;\n const pauseButtonIcon = status === 'running' ? faPause : faPlay;\n const pauseButtonTitle =\n status === 'running' ? 'Pause' : isPaused ? 'Resume' : status === 'idle' ? 'Start' : 'Restart';\n const handlePauseButtonClick = status === 'running' ? pause : isPaused ? resume : start;\n const showCancelButton = showControls && !hideCancelButton;\n\n return (\n <div\n {...rest}\n className={cx(\n styles.timer,\n pulseOnUrgent && isUrgent && styles.timerPulsing,\n isCritical && styles.timerCritical,\n className,\n classNames?.root,\n )}\n style={\n {\n ...(size ? { '--timer-size': size } : null),\n '--timer-stroke-width': `${strokeWidth}px`,\n ...style,\n ...slotStyles?.root,\n } as React.CSSProperties\n }\n >\n <div className={cx(styles.timerRingWrapper, classNames?.ringWrapper)} style={slotStyles?.ringWrapper}>\n {ripples.length > 0 && (\n <div className={cx(styles.timerRipples, classNames?.ripples)} style={slotStyles?.ripples}>\n {ripples.map((id) => (\n <span\n key={id}\n className={cx(styles.timerRipple, classNames?.ripple)}\n style={slotStyles?.ripple}\n onAnimationEnd={() => handleRippleAnimationEnd(id)}\n />\n ))}\n </div>\n )}\n <svg className={cx(styles.timerRingSvg, classNames?.ringSvg)} viewBox='0 0 100 100' style={slotStyles?.ringSvg}>\n <circle\n className={cx(styles.timerRingTrack, classNames?.ringTrack)}\n cx='50'\n cy='50'\n r={radius}\n style={slotStyles?.ringTrack}\n />\n <circle\n className={cx(\n styles.timerRingProgress,\n isUrgent && styles.timerRingProgressUrgent,\n classNames?.ringProgress,\n )}\n cx='50'\n cy='50'\n r={radius}\n strokeDasharray={circumference}\n strokeDashoffset={dashOffset}\n style={{\n transition: isPaused ? 'none' : undefined,\n transform: counterClockwise ? 'rotate(-90deg)' : undefined,\n ...slotStyles?.ringProgress,\n }}\n />\n </svg>\n <div className={cx(styles.timerDisplay, classNames?.display)} style={slotStyles?.display}>\n <span className={cx(styles.timerTime, classNames?.time)} style={slotStyles?.time}>\n {formatTime(remainingMs)}\n </span>\n {label && (\n <span className={cx(styles.timerLabel, classNames?.label)} style={slotStyles?.label}>\n {label}\n </span>\n )}\n </div>\n </div>\n\n {showControls && (\n <div className={cx(styles.timerControls, classNames?.controls)} style={slotStyles?.controls}>\n <div className={styles.timerControlsRow}>\n {showRemoveButton && (\n <ActionIcon\n icon={faMinus}\n title={`Remove ${adjustSeconds}s`}\n onClick={() => removeTime(adjustSeconds)}\n className={cx(styles.timerAdjustButton, classNames?.removeButton)}\n style={slotStyles?.removeButton}\n />\n )}\n {showPauseButton && (\n <Button\n size={ButtonSize.Small}\n icon={pauseButtonIcon}\n title={pauseButtonTitle}\n onClick={handlePauseButtonClick}\n className={classNames?.pauseButton}\n style={slotStyles?.pauseButton}\n />\n )}\n {showAddButton && (\n <ActionIcon\n icon={faPlus}\n title={`Add ${adjustSeconds}s`}\n onClick={() => addTime(adjustSeconds)}\n className={cx(styles.timerAdjustButton, classNames?.addButton)}\n style={slotStyles?.addButton}\n />\n )}\n </div>\n {showCancelButton && (\n <ActionIcon\n icon={faXmark}\n title='Cancel timer'\n onClick={cancel}\n className={cx(styles.timerCancelButton, classNames?.cancelButton)}\n style={slotStyles?.cancelButton}\n />\n )}\n </div>\n )}\n </div>\n );\n});\n\nTimer.displayName = 'Timer';\n\nexport default Timer;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"timer.module.mjs","names":[],"sources":["../../../../package/common/timer/timer.module.pcss"],"sourcesContent":[".timer {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 16px;\n width: fit-content;\n\n &-ringWrapper {\n position: relative;\n width: var(--timer-size, 160px);\n height: var(--timer-size, 160px);\n transform-origin: center;\n }\n\n &-ringSvg {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: visible;\n }\n\n &-ripples {\n position: absolute;\n inset: 0;\n pointer-events: none;\n }\n\n &-ripple {\n position: absolute;\n inset: 0;\n border-radius: 50%;\n border: 2px solid var(--timer-ripple-color, var(--timer-progress-color, var(--primary-color)));\n opacity: 0;\n transform: scale(0.94);\n\n /* One-shot per spawned instance (see the ripple spawner in timer.tsx), not an infinite\n loop — so pausing simply stops new spawns while already-mid-flight ripples keep\n animating to completion via `forwards` instead of vanishing mid-cycle. */\n animation: timerRippleHalo 3s ease-out forwards;\n }\n\n &-ringTrack {\n fill: none;\n stroke: var(--timer-track-color, var(--background-accent-light));\n stroke-width: var(--timer-stroke-width, 10px);\n }\n\n &-ringProgress {\n fill: none;\n stroke: var(--timer-progress-color, var(--primary-color));\n stroke-width: var(--timer-stroke-width, 10px);\n stroke-linecap: round;\n\n /* Single combined transform, not split across an SVG `transform` attribute on this element\n plus a separate CSS transform on an ancestor — that split combination, together with an\n actively-transitioning stroke-dashoffset, is a known trigger for SVG dash-rendering glitches\n in some engines (the visible arc appearing to jump/drift independent of the actual\n dashoffset value). scaleX(-1) mirrors so the depleting boundary sweeps clockwise (matching\n a clock face) instead of the counter-clockwise direction a <circle>'s implicit path winds\n by default; rotate(-90deg) moves the fixed anchor from 3 o'clock to 12 o'clock. Both applied\n around the circle's own center, which coincides with the viewBox center here. `counterClockwise`\n overrides this inline (timer.tsx) by dropping the scaleX(-1), keeping just the rotate — that\n default winding direction is exactly counter-clockwise, so no mirroring is needed for it. */\n transform: scaleX(-1) rotate(-90deg);\n transform-origin: 50% 50%;\n\n /* Matches (slightly exceeds) the JS tick interval (TICK_MS in timer.tsx) so the ring\n tracks the real remaining time closely instead of perpetually chasing a target that's\n always a full second stale — a much longer transition here reads as sluggish since the\n rendered ring never catches up to the true value while ticking continues. */\n transition:\n stroke-dashoffset 220ms linear,\n stroke 0.3s ease;\n\n &-urgent {\n stroke: var(--timer-urgent-color, var(--error-color));\n }\n }\n\n &-display {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n text-align: center;\n padding: 0 12px;\n }\n\n &-time {\n font-size: 1.4em;\n font-weight: 600;\n font-variant-numeric: tabular-nums;\n color: var(--timer-text-color, var(--text-color));\n }\n\n &-label {\n margin-top: 4px;\n font-size: 0.8em;\n color: var(--timer-label-color, var(--text-dark));\n }\n\n &-controls {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 8px;\n }\n\n &-controlsRow {\n display: flex;\n align-items: center;\n gap: 8px;\n }\n\n &-adjustButton {\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n }\n\n &-cancelButton {\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n opacity: 0.6;\n\n &:hover {\n opacity: 1;\n }\n }\n\n &-pulsing &-ringWrapper {\n animation: timerHeartbeat 1s ease-in-out infinite;\n }\n\n &-critical &-ringWrapper {\n animation-duration: 0.6s;\n }\n}\n\n@keyframes timerHeartbeat {\n 0%,\n 100% {\n transform: scale(1);\n }\n\n 50% {\n transform: scale(1.045);\n }\n}\n\n@keyframes timerRippleHalo {\n 0% {\n transform: scale(0.94);\n opacity: 0.5;\n }\n\n 100% {\n transform: scale(1.35);\n opacity: 0;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .timer-ringProgress {\n transition: stroke 0.3s ease;\n }\n\n .timer-pulsing .timer-ringWrapper {\n animation: none;\n }\n\n .timer-ripple {\n animation: none;\n opacity: 0;\n }\n}\n"],"mappings":""}
1
+ {"version":3,"file":"timer.module.mjs","names":[],"sources":["../../../../package/common/timer/timer.module.pcss"],"sourcesContent":[".timer {\n /* em, because the readout inside the ring is em too: a fixed ring stayed put while its own\n digits grew with the theme until they filled it. */\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 1em;\n width: fit-content;\n\n &-ringWrapper {\n position: relative;\n width: var(--timer-size, 10em);\n height: var(--timer-size, 10em);\n transform-origin: center;\n }\n\n &-ringSvg {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: visible;\n }\n\n &-ripples {\n position: absolute;\n inset: 0;\n pointer-events: none;\n }\n\n &-ripple {\n position: absolute;\n inset: 0;\n border-radius: 50%;\n border: 0.125em solid var(--timer-ripple-color, var(--timer-progress-color, var(--primary-color)));\n opacity: 0;\n transform: scale(0.94);\n\n /* One-shot per spawned instance (see the ripple spawner in timer.tsx), not an infinite\n loop — so pausing simply stops new spawns while already-mid-flight ripples keep\n animating to completion via `forwards` instead of vanishing mid-cycle. */\n animation: timerRippleHalo 3s ease-out forwards;\n }\n\n /* Not em, unlike everything else here: inside the svg this is a viewBox user unit, so it\n already scales with the ring, and it has to stay equal to the `strokeWidth` prop that\n timer.tsx subtracts from the circle's radius. */\n &-ringTrack {\n fill: none;\n stroke: var(--timer-track-color, var(--background-accent-light));\n stroke-width: var(--timer-stroke-width, 10px);\n }\n\n &-ringProgress {\n fill: none;\n stroke: var(--timer-progress-color, var(--primary-color));\n stroke-width: var(--timer-stroke-width, 10px);\n stroke-linecap: round;\n\n /* Single combined transform, not split across an SVG `transform` attribute on this element\n plus a separate CSS transform on an ancestor — that split combination, together with an\n actively-transitioning stroke-dashoffset, is a known trigger for SVG dash-rendering glitches\n in some engines (the visible arc appearing to jump/drift independent of the actual\n dashoffset value). scaleX(-1) mirrors so the depleting boundary sweeps clockwise (matching\n a clock face) instead of the counter-clockwise direction a <circle>'s implicit path winds\n by default; rotate(-90deg) moves the fixed anchor from 3 o'clock to 12 o'clock. Both applied\n around the circle's own center, which coincides with the viewBox center here. `counterClockwise`\n overrides this inline (timer.tsx) by dropping the scaleX(-1), keeping just the rotate — that\n default winding direction is exactly counter-clockwise, so no mirroring is needed for it. */\n transform: scaleX(-1) rotate(-90deg);\n transform-origin: 50% 50%;\n\n /* Matches (slightly exceeds) the JS tick interval (TICK_MS in timer.tsx) so the ring\n tracks the real remaining time closely instead of perpetually chasing a target that's\n always a full second stale — a much longer transition here reads as sluggish since the\n rendered ring never catches up to the true value while ticking continues. */\n transition:\n stroke-dashoffset 220ms linear,\n stroke 0.3s ease;\n\n &-urgent {\n stroke: var(--timer-urgent-color, var(--error-color));\n }\n }\n\n &-display {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n text-align: center;\n padding: 0 0.75em;\n }\n\n &-time {\n font-size: 1.4em;\n font-weight: 600;\n font-variant-numeric: tabular-nums;\n color: var(--timer-text-color, var(--text-color));\n }\n\n &-label {\n margin-top: 0.25em;\n font-size: 0.8em;\n color: var(--timer-label-color, var(--text-dark));\n }\n\n &-controls {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 0.5em;\n }\n\n &-controlsRow {\n display: flex;\n align-items: center;\n gap: 0.5em;\n }\n\n &-adjustButton {\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n }\n\n &-cancelButton {\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n opacity: 0.6;\n\n &:hover {\n opacity: 1;\n }\n }\n\n &-pulsing &-ringWrapper {\n animation: timerHeartbeat 1s ease-in-out infinite;\n }\n\n &-critical &-ringWrapper {\n animation-duration: 0.6s;\n }\n}\n\n@keyframes timerHeartbeat {\n 0%,\n 100% {\n transform: scale(1);\n }\n\n 50% {\n transform: scale(1.045);\n }\n}\n\n@keyframes timerRippleHalo {\n 0% {\n transform: scale(0.94);\n opacity: 0.5;\n }\n\n 100% {\n transform: scale(1.35);\n opacity: 0;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .timer-ringProgress {\n transition: stroke 0.3s ease;\n }\n\n .timer-pulsing .timer-ringWrapper {\n animation: none;\n }\n\n .timer-ripple {\n animation: none;\n opacity: 0;\n }\n}\n"],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"toast.module.mjs","names":[],"sources":["../../../../package/common/toast/toast.module.pcss"],"sourcesContent":[".container {\n position: fixed;\n z-index: 9999;\n display: flex;\n flex-direction: column;\n gap: 8px;\n pointer-events: none;\n padding: 16px;\n box-sizing: border-box;\n\n &-top-left {\n top: 0;\n left: 0;\n align-items: flex-start;\n }\n\n &-top-center {\n top: 0;\n left: 50%;\n transform: translateX(-50%);\n align-items: center;\n }\n\n &-top-right {\n top: 0;\n right: 0;\n align-items: flex-end;\n }\n\n &-bottom-left {\n bottom: 0;\n left: 0;\n align-items: flex-start;\n flex-direction: column-reverse;\n }\n\n &-bottom-center {\n bottom: 0;\n left: 50%;\n transform: translateX(-50%);\n align-items: center;\n flex-direction: column-reverse;\n }\n\n &-bottom-right {\n bottom: 0;\n right: 0;\n align-items: flex-end;\n flex-direction: column-reverse;\n }\n}\n\n.toast {\n pointer-events: all;\n display: flex;\n align-items: flex-start;\n gap: 10px;\n padding: 12px 14px;\n border-radius: var(--default-border-radius);\n background: var(--background-modal);\n color: var(--text-color);\n font-family: var(--default-font);\n box-shadow: var(--card-shadow);\n min-width: 240px;\n max-width: 380px;\n box-sizing: border-box;\n position: relative;\n\n /* Tells the browser vertical panning is native (so the page can still scroll behind the toast)\n while horizontal movement is left for useSwipeDismiss's own pointer handling to decide. */\n touch-action: pan-y;\n overflow: hidden;\n animation: toastSlideDown 0.25s ease forwards;\n\n &-bottom {\n animation: toastSlideUp 0.25s ease forwards;\n }\n\n &-dismissing {\n animation: toastFadeOut 0.2s ease forwards;\n }\n\n &-paused .toastProgressBar {\n animation-play-state: paused;\n }\n\n &-info {\n border-left: 3px solid var(--info-color, #3b82f6);\n\n .toastIcon {\n color: var(--info-color, #3b82f6);\n }\n }\n\n &-success {\n border-left: 3px solid var(--success-color);\n\n .toastIcon {\n color: var(--success-color);\n }\n }\n\n &-error {\n border-left: 3px solid var(--error-color, #ef4444);\n\n .toastIcon {\n color: var(--error-color, #ef4444);\n }\n }\n\n &-warn {\n border-left: 3px solid var(--warn-color);\n\n .toastIcon {\n color: var(--warn-color);\n }\n }\n\n &-default {\n border-left: 3px solid var(--primary-color);\n\n .toastIcon {\n color: var(--primary-color);\n }\n }\n}\n\n.toastIcon {\n flex-shrink: 0;\n margin-top: 1px;\n font-size: 15px;\n}\n\n.toastContent {\n flex: 1;\n font-size: 14px;\n line-height: 1.4;\n}\n\n.toastCloseButton {\n flex-shrink: 0;\n background: none;\n border: none;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n padding: 0;\n color: var(--text-dark);\n font-size: 13px;\n opacity: 0.6;\n transition: opacity 0.15s;\n margin-top: 1px;\n\n &:hover {\n opacity: 1;\n }\n}\n\n.toastProgressBar {\n position: absolute;\n bottom: 0;\n left: 0;\n height: 3px;\n width: 100%;\n background: currentcolor;\n opacity: 0.35;\n transform-origin: left;\n animation: toastShrink var(--toast-duration) linear forwards;\n}\n\n@keyframes toastSlideDown {\n from {\n opacity: 0;\n transform: translateY(-12px);\n }\n\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n@keyframes toastSlideUp {\n from {\n opacity: 0;\n transform: translateY(12px);\n }\n\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n@keyframes toastFadeOut {\n from {\n opacity: 1;\n transform: scale(1);\n }\n\n to {\n opacity: 0;\n transform: scale(0.92);\n }\n}\n\n@keyframes toastShrink {\n from {\n transform: scaleX(1);\n }\n\n to {\n transform: scaleX(0);\n }\n}\n"],"mappings":""}
1
+ {"version":3,"file":"toast.module.mjs","names":[],"sources":["../../../../package/common/toast/toast.module.pcss"],"sourcesContent":[".container {\n position: fixed;\n z-index: 9999;\n display: flex;\n flex-direction: column;\n gap: 0.5em;\n pointer-events: none;\n padding: 1em;\n box-sizing: border-box;\n\n &-top-left {\n top: 0;\n left: 0;\n align-items: flex-start;\n }\n\n &-top-center {\n top: 0;\n left: 50%;\n transform: translateX(-50%);\n align-items: center;\n }\n\n &-top-right {\n top: 0;\n right: 0;\n align-items: flex-end;\n }\n\n &-bottom-left {\n bottom: 0;\n left: 0;\n align-items: flex-start;\n flex-direction: column-reverse;\n }\n\n &-bottom-center {\n bottom: 0;\n left: 50%;\n transform: translateX(-50%);\n align-items: center;\n flex-direction: column-reverse;\n }\n\n &-bottom-right {\n bottom: 0;\n right: 0;\n align-items: flex-end;\n flex-direction: column-reverse;\n }\n}\n\n.toast {\n pointer-events: all;\n display: flex;\n align-items: flex-start;\n gap: 0.625em;\n padding: 0.75em 0.875em;\n border-radius: var(--default-border-radius);\n background: var(--background-modal);\n color: var(--text-color);\n font-family: var(--default-font);\n box-shadow: var(--card-shadow);\n\n /* em, so the line length stays a measure rather than a pixel count. */\n min-width: 15em;\n max-width: 23.75em;\n box-sizing: border-box;\n position: relative;\n\n /* Tells the browser vertical panning is native (so the page can still scroll behind the toast)\n while horizontal movement is left for useSwipeDismiss's own pointer handling to decide. */\n touch-action: pan-y;\n overflow: hidden;\n animation: toastSlideDown 0.25s ease forwards;\n\n &-bottom {\n animation: toastSlideUp 0.25s ease forwards;\n }\n\n &-dismissing {\n animation: toastFadeOut 0.2s ease forwards;\n }\n\n &-paused .toastProgressBar {\n animation-play-state: paused;\n }\n\n &-info {\n border-left: 0.1875em solid var(--info-color, #3b82f6);\n\n .toastIcon {\n color: var(--info-color, #3b82f6);\n }\n }\n\n &-success {\n border-left: 0.1875em solid var(--success-color);\n\n .toastIcon {\n color: var(--success-color);\n }\n }\n\n &-error {\n border-left: 0.1875em solid var(--error-color, #ef4444);\n\n .toastIcon {\n color: var(--error-color, #ef4444);\n }\n }\n\n &-warn {\n border-left: 0.1875em solid var(--warn-color);\n\n .toastIcon {\n color: var(--warn-color);\n }\n }\n\n &-default {\n border-left: 0.1875em solid var(--primary-color);\n\n /* Paints the gradient exactly over border-left, which a background layer only reaches\n because its origin is the border box. The border stays as the no-gradient fallback. */\n background-image: var(--primary-gradient, none);\n background-repeat: no-repeat;\n background-size: 0.1875em 100%;\n background-origin: border-box;\n\n .toastIcon {\n color: var(--primary-color);\n }\n }\n}\n\n.toastIcon {\n flex-shrink: 0;\n margin-top: 0.0667em; /* 1px against this element's own 0.9375em */\n font-size: 0.9375em;\n}\n\n.toastContent {\n flex: 1;\n font-size: 0.875em;\n line-height: 1.4;\n}\n\n.toastCloseButton {\n flex-shrink: 0;\n background: none;\n border: none;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n padding: 0;\n color: var(--text-dark);\n font-size: 0.8125em;\n opacity: 0.6;\n transition: opacity 0.15s;\n margin-top: 0.0769em; /* 1px against this element's own 0.8125em */\n\n &:hover {\n opacity: 1;\n }\n}\n\n.toastProgressBar {\n position: absolute;\n bottom: 0;\n left: 0;\n height: 0.1875em;\n width: 100%;\n background: currentcolor;\n opacity: 0.35;\n transform-origin: left;\n animation: toastShrink var(--toast-duration) linear forwards;\n}\n\n@keyframes toastSlideDown {\n from {\n opacity: 0;\n transform: translateY(-0.75em);\n }\n\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n@keyframes toastSlideUp {\n from {\n opacity: 0;\n transform: translateY(0.75em);\n }\n\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n@keyframes toastFadeOut {\n from {\n opacity: 1;\n transform: scale(1);\n }\n\n to {\n opacity: 0;\n transform: scale(0.92);\n }\n}\n\n@keyframes toastShrink {\n from {\n transform: scaleX(1);\n }\n\n to {\n transform: scaleX(0);\n }\n}\n"],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"tooltip.module.mjs","names":[],"sources":["../../../../package/common/tooltip/tooltip.module.pcss"],"sourcesContent":[".tooltipAnchor {\n position: fixed;\n z-index: 10;\n}\n\n.tooltipBubble {\n position: absolute;\n bottom: 0;\n margin-bottom: 6px;\n left: 0;\n border-radius: 3px;\n width: max-content;\n max-width: 200px;\n font-size: 12px;\n background-color: #454953;\n padding: 6px 9px;\n color: white;\n pointer-events: none;\n\n .tooltip-anchor-flip & {\n bottom: auto;\n top: 0;\n margin-bottom: 0;\n margin-top: 6px;\n }\n\n &::after {\n content: '';\n position: absolute;\n top: 100%;\n left: calc(var(--tooltip-trigger-width, 10px) / 2 - 5px);\n border-width: 5px;\n border-style: solid;\n border-color: #454953 transparent transparent;\n }\n\n .tooltip-anchor-flip &::after {\n top: auto;\n bottom: 100%;\n border-color: transparent transparent #454953;\n }\n}\n\n.tooltipDetails {\n position: relative;\n display: inline-flex;\n align-items: center;\n\n &-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n font-size: 11px;\n font-weight: 700;\n background-color: #454953;\n color: white;\n cursor: default;\n user-select: none;\n }\n}\n\n.tooltipError {\n position: absolute;\n bottom: 0;\n margin-bottom: 6px;\n right: 0;\n width: max-content;\n border-radius: 3px;\n background-color: var(--error-lighter);\n color: var(--red);\n font-size: 10px;\n padding: 6px 9px;\n\n .tooltip-anchor-flip & {\n bottom: auto;\n top: 0;\n margin-bottom: 0;\n margin-top: 6px;\n }\n\n &::after {\n content: '';\n position: absolute;\n top: 100%;\n right: 12px;\n border-width: 5px;\n border-style: solid;\n border-color: var(--error-border-color) transparent transparent transparent;\n }\n\n .tooltip-anchor-flip &::after {\n top: auto;\n bottom: 100%;\n border-color: transparent transparent var(--error-border-color) transparent;\n }\n}\n"],"mappings":""}
1
+ {"version":3,"file":"tooltip.module.mjs","names":[],"sources":["../../../../package/common/tooltip/tooltip.module.pcss"],"sourcesContent":[".tooltipAnchor {\n /* Resolved once per root so the bubble, its arrow and the badge cannot drift apart. The\n default is an inverse of the theme, not a fixed slate: a tooltip has to separate from\n the page it floats over, and a dark bubble sat almost invisibly on a dark theme. */\n --custom-tooltip-background: var(--tooltip-background, color-mix(in oklab, var(--text-color) 82%, var(--background)));\n --custom-tooltip-color: var(--tooltip-color, var(--background));\n\n position: fixed;\n z-index: 10;\n}\n\n.tooltipBubble {\n position: absolute;\n bottom: 0;\n\n /* The lengths here are em against this bubble's own 0.75em, so the ratios are of 12px,\n not 16, and max-width is a line measure rather than a pixel cap. */\n margin-bottom: 0.5em;\n left: 0;\n border-radius: 0.25em;\n width: max-content;\n max-width: 16.667em;\n font-size: 0.75em;\n background-color: var(--custom-tooltip-background);\n padding: 0.5em 0.75em;\n color: var(--custom-tooltip-color);\n pointer-events: none;\n\n .tooltip-anchor-flip & {\n bottom: auto;\n top: 0;\n margin-bottom: 0;\n margin-top: 0.5em;\n }\n\n &::after {\n content: '';\n position: absolute;\n top: 100%;\n\n /* The offset must stay half the arrow's own width, or it stops pointing at the trigger. */\n left: calc(var(--tooltip-trigger-width, 10px) / 2 - 0.4167em);\n border-width: 0.4167em;\n border-style: solid;\n border-color: var(--custom-tooltip-background) transparent transparent;\n }\n\n .tooltip-anchor-flip &::after {\n top: auto;\n bottom: 100%;\n border-color: transparent transparent var(--custom-tooltip-background);\n }\n}\n\n.tooltipDetails {\n /* Repeated from .tooltipAnchor above: the badge is a second root, rendered without one. */\n --custom-tooltip-background: var(--tooltip-background, color-mix(in oklab, var(--text-color) 82%, var(--background)));\n --custom-tooltip-color: var(--tooltip-color, var(--background));\n\n position: relative;\n display: inline-flex;\n align-items: center;\n\n &-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n\n /* The theme's base, not a multiple of the font below: that landed a fraction of a pixel short. */\n width: var(--default-font-size);\n height: var(--default-font-size);\n border-radius: 50%;\n\n /* calc, not em: this badge renders inline wherever the consumer puts it, so em would\n measure that text. The bubble above can use em, being portaled to #bodyEnd. */\n font-size: calc(var(--default-font-size) * 0.6875);\n font-weight: 700;\n background-color: var(--custom-tooltip-background);\n color: var(--custom-tooltip-color);\n cursor: default;\n user-select: none;\n }\n}\n\n.tooltipError {\n position: absolute;\n bottom: 0;\n margin-bottom: 0.6em;\n right: 0;\n width: max-content;\n border-radius: 0.3em;\n background-color: var(--error-lighter);\n color: var(--error-color);\n font-size: 0.625em;\n padding: 0.6em 0.9em;\n\n .tooltip-anchor-flip & {\n bottom: auto;\n top: 0;\n margin-bottom: 0;\n margin-top: 0.6em;\n }\n\n &::after {\n content: '';\n position: absolute;\n top: 100%;\n right: 1.2em;\n border-width: 0.5em;\n border-style: solid;\n border-color: var(--error-border-color) transparent transparent transparent;\n }\n\n .tooltip-anchor-flip &::after {\n top: auto;\n bottom: 100%;\n border-color: transparent transparent var(--error-border-color) transparent;\n }\n}\n"],"mappings":""}
@@ -1,2 +1,2 @@
1
- import{useComponentDefaults as e}from"../configProvider/useComponentDefaults.mjs";import{renderIcon as t}from"../utils/renderIcon.mjs";import n from"../virtualList/virtualList.mjs";import{flattenTree as r}from"./flattenTree.mjs";import i from"./tree.module.mjs";import{useEffect as a,useRef as o,useState as s}from"react";import c from"classnames";import{faChevronRight as l}from"@fortawesome/free-solid-svg-icons";import{jsx as u,jsxs as d}from"react/jsx-runtime";var f=[];function p(p){let{nodes:m,expandedKeys:h,defaultExpandedKeys:g=f,onExpandedChange:_=()=>{},selectedKey:v,defaultSelectedKey:y,onSelect:b=()=>{},expandOnSelect:x=!0,indent:S=20,toggleIcon:C=l,height:w,estimatedRowHeight:T=32,emptyLabel:ee=`Nothing here`,disabled:E=!1,"aria-label":D=`Tree`,className:O,style:k,classNames:A,styles:j,...M}=e(`Tree`,p),N=o(new Map),P=o(null),F=o(null),I=w!==void 0,L=h!==void 0,[R,z]=s(g),B=L?h:R,V=v!==void 0,[H,U]=s(y??null),W=V?v:H,G=r(m,B),K=Math.max(0,G.findIndex(e=>e.node.key===W)),q=G[K]?.node.key;function J(e){L||z(e),_(e)}function Y(e){J(B.includes(e)?B.filter(t=>t!==e):[...B,e])}function X(e){E||e.node.disabled||(V||U(e.node.key),b(e.node.key,e.node),x&&e.isBranch&&Y(e.node.key))}function Z(e){let t=G[e]?.node.key;if(t===void 0)return;let n=N.current.get(t);if(n){n.focus();return}F.current=t,P.current?.scrollToIndex(e,{align:`center`})}a(()=>{let e=F.current;if(e===null)return;let t=N.current.get(e);t&&(t.focus(),F.current=null)});function Q(e,t,n){if(!E)switch(e.key){case`ArrowDown`:e.preventDefault(),Z(Math.min(n+1,G.length-1));break;case`ArrowUp`:e.preventDefault(),Z(Math.max(n-1,0));break;case`ArrowRight`:if(e.preventDefault(),!t.isBranch)break;t.isExpanded?Z(n+1):Y(t.node.key);break;case`ArrowLeft`:{if(e.preventDefault(),t.isBranch&&t.isExpanded){Y(t.node.key);break}let n=G.findIndex(e=>e.node.key===t.parentKey);n>=0&&Z(n);break}case`Home`:e.preventDefault(),Z(0);break;case`End`:e.preventDefault(),Z(G.length-1);break;case`Enter`:case` `:e.preventDefault(),X(t)}}function $(e,n){let{node:r,level:a,isBranch:o,isExpanded:s}=e,l=r.key===W,f=E||r.disabled;return d(`div`,{ref:e=>{e?N.current.set(r.key,e):N.current.delete(r.key)},role:`treeitem`,"aria-level":a+1,"aria-setsize":e.setSize,"aria-posinset":e.posInSet,"aria-expanded":o?s:void 0,"aria-selected":l,"aria-disabled":f||void 0,tabIndex:r.key===q&&!f?0:-1,className:c(i.treeNode,l&&i.treeNodeSelected,f&&i.treeNodeDisabled,A?.node),style:{paddingLeft:a*S,...j?.node},onClick:()=>X(e),onKeyDown:t=>Q(t,e,n),children:[u(`span`,{className:c(i.treeToggle,s&&i.treeToggleOpen,!o&&i.treeToggleHidden,A?.toggle),style:j?.toggle,"aria-hidden":`true`,onClick:e=>{e.stopPropagation(),!f&&o&&Y(r.key)},children:o&&t(C)}),r.icon&&t(r.icon,{className:c(i.treeIcon,A?.icon),style:j?.icon}),u(`span`,{className:c(i.treeLabel,A?.label),style:j?.label,children:r.label})]},r.key)}return u(`div`,{...M,role:`tree`,"aria-label":D,"aria-disabled":E||void 0,tabIndex:I&&!E?0:void 0,onFocus:e=>{e.target===e.currentTarget&&Z(K)},className:c(i.tree,E&&i.treeDisabled,A?.root,O),style:{...k,...j?.root},children:G.length===0?u(`div`,{className:c(i.treeEmpty,A?.empty),style:j?.empty,children:ee}):I?u(n,{ref:P,items:G,height:w,estimatedRowHeight:T,getItemKey:e=>e.node.key,getItemDepth:e=>e.level,ariaRoles:{container:`none`,row:`none`},renderRow:$}):G.map($)})}export{p as default};
1
+ import{useComponentDefaults as e}from"../configProvider/useComponentDefaults.mjs";import{renderIcon as t}from"../utils/renderIcon.mjs";import n from"../virtualList/virtualList.mjs";import{flattenTree as r}from"./flattenTree.mjs";import i from"./tree.module.mjs";import{useEffect as a,useRef as o,useState as s}from"react";import c from"classnames";import{faChevronRight as l}from"@fortawesome/free-solid-svg-icons";import{jsx as u,jsxs as d}from"react/jsx-runtime";var f=[];function p(p){let{nodes:m,expandedKeys:h,defaultExpandedKeys:g=f,onExpandedChange:_=()=>{},selectedKey:v,defaultSelectedKey:y,onSelect:b=()=>{},expandOnSelect:x=!0,indent:S=20,toggleIcon:C=l,height:w,estimatedRowHeight:T=32,emptyLabel:ee=`Nothing here`,disabled:E=!1,"aria-label":D=`Tree`,className:O,style:k,classNames:A,styles:j,...M}=e(`Tree`,p),N=o(new Map),P=o(null),F=o(null),I=w!==void 0,L=h!==void 0,[R,z]=s(g),B=L?h:R,V=v!==void 0,[H,U]=s(y??null),W=V?v:H,G=r(m,B),K=Math.max(0,G.findIndex(e=>e.node.key===W)),q=G[K]?.node.key;function J(e){L||z(e),_(e)}function Y(e){J(B.includes(e)?B.filter(t=>t!==e):[...B,e])}function X(e){E||e.node.disabled||(V||U(e.node.key),b(e.node.key,e.node),x&&e.isBranch&&Y(e.node.key))}function Z(e){let t=G[e]?.node.key;if(t===void 0)return;let n=N.current.get(t);if(n){n.focus();return}F.current=t,P.current?.scrollToIndex(e,{align:`center`})}a(()=>{let e=F.current;if(e===null)return;let t=N.current.get(e);t&&(t.focus(),F.current=null)});function Q(e,t,n){if(!E)switch(e.key){case`ArrowDown`:e.preventDefault(),Z(Math.min(n+1,G.length-1));break;case`ArrowUp`:e.preventDefault(),Z(Math.max(n-1,0));break;case`ArrowRight`:if(e.preventDefault(),!t.isBranch)break;t.isExpanded?Z(n+1):Y(t.node.key);break;case`ArrowLeft`:{if(e.preventDefault(),t.isBranch&&t.isExpanded){Y(t.node.key);break}let n=G.findIndex(e=>e.node.key===t.parentKey);n>=0&&Z(n);break}case`Home`:e.preventDefault(),Z(0);break;case`End`:e.preventDefault(),Z(G.length-1);break;case`Enter`:case` `:e.preventDefault(),X(t)}}function $(e,n){let{node:r,level:a,isBranch:o,isExpanded:s}=e,l=r.key===W,f=E||r.disabled;return d(`div`,{ref:e=>{e?N.current.set(r.key,e):N.current.delete(r.key)},role:`treeitem`,"aria-level":a+1,"aria-setsize":e.setSize,"aria-posinset":e.posInSet,"aria-expanded":o?s:void 0,"aria-selected":l,"aria-disabled":f||void 0,tabIndex:r.key===q&&!f?0:-1,className:c(i.treeNode,l&&i.treeNodeSelected,f&&i.treeNodeDisabled,A?.node),style:{paddingLeft:a*S,...j?.node},onClick:()=>X(e),onKeyDown:t=>Q(t,e,n),children:[u(`span`,{className:c(i.treeToggle,s&&i.treeToggleOpen,!o&&i.treeToggleHidden,A?.toggle),style:j?.toggle,"aria-hidden":`true`,onClick:e=>{e.stopPropagation(),!f&&o&&Y(r.key)},children:o&&t(C)}),r.icon&&t(r.icon,{className:c(i.treeIcon,A?.icon),style:j?.icon}),u(`span`,{className:c(i.treeLabel,A?.label),style:j?.label,children:r.label})]},r.key)}return u(`div`,{...M,role:`tree`,"aria-label":D,"aria-disabled":E||void 0,tabIndex:I&&!E?0:void 0,onFocus:e=>{e.target===e.currentTarget&&Z(K)},className:c(i.tree,E&&i.treeDisabled,A?.root,O),style:{...k,...j?.root},children:G.length===0?u(`div`,{className:c(i.treeEmpty,A?.empty),style:j?.empty,children:ee}):I?u(n,{ref:P,items:G,height:w,estimatedRowHeight:T,getItemKey:e=>e.node.key,getItemDepth:e=>e.level,ariaRoles:{container:`none`,row:`none`},keyboardNavigation:!1,renderRow:$}):G.map($)})}export{p as default};
2
2
  //# sourceMappingURL=tree.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"tree.mjs","names":[],"sources":["../../../../package/common/tree/tree.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from 'react';\nimport type { ReactElement } from 'react';\nimport cx from 'classnames';\nimport { faChevronRight } from '@fortawesome/free-solid-svg-icons';\n\nimport VirtualList from 'components/common/virtualList';\nimport type { VirtualListHandle } from 'components/common/virtualList';\nimport { useComponentDefaults } from '../configProvider';\nimport { renderIcon } from '../utils/renderIcon';\nimport { flattenTree } from './flattenTree';\nimport type { FlatTreeNode, TreeProps, TreeValue } from './tree.types';\n\nimport styles from './tree.module.pcss';\n\nconst EMPTY_KEYS: TreeValue[] = [];\n\nfunction Tree(props: TreeProps): ReactElement {\n const {\n nodes,\n expandedKeys: controlledExpanded,\n defaultExpandedKeys = EMPTY_KEYS,\n onExpandedChange = () => {},\n selectedKey: controlledSelected,\n defaultSelectedKey,\n onSelect = () => {},\n expandOnSelect = true,\n indent = 20,\n toggleIcon = faChevronRight,\n height,\n estimatedRowHeight = 32,\n emptyLabel = 'Nothing here',\n disabled = false,\n 'aria-label': ariaLabel = 'Tree',\n className,\n style,\n classNames,\n styles: slotStyles,\n ...rest\n } = useComponentDefaults('Tree', props);\n\n const nodeRefs = useRef(new Map<TreeValue, HTMLDivElement>());\n const listRef = useRef<VirtualListHandle>(null);\n const pendingFocusRef = useRef<TreeValue | null>(null);\n const isWindowed = height !== undefined;\n\n const isExpandedControlled = controlledExpanded !== undefined;\n const [internalExpanded, setInternalExpanded] = useState<TreeValue[]>(defaultExpandedKeys);\n const expandedKeys = isExpandedControlled ? controlledExpanded : internalExpanded;\n\n const isSelectedControlled = controlledSelected !== undefined;\n const [internalSelected, setInternalSelected] = useState<TreeValue | null>(defaultSelectedKey ?? null);\n const selectedKey = isSelectedControlled ? controlledSelected : internalSelected;\n\n const visible = flattenTree(nodes, expandedKeys);\n\n // Windowed, the tab stop sits on the container instead of this row: a roving tabindex on a row\n // scrolled out of the window would leave the tree with no tab stop at all.\n const activeIndex = Math.max(\n 0,\n visible.findIndex((entry) => entry.node.key === selectedKey),\n );\n const activeKey = visible[activeIndex]?.node.key;\n\n function setExpanded(next: TreeValue[]) {\n if (!isExpandedControlled) setInternalExpanded(next);\n onExpandedChange(next);\n }\n\n function toggle(key: TreeValue) {\n setExpanded(expandedKeys.includes(key) ? expandedKeys.filter((k) => k !== key) : [...expandedKeys, key]);\n }\n\n function select(entry: FlatTreeNode) {\n if (disabled || entry.node.disabled) return;\n if (!isSelectedControlled) setInternalSelected(entry.node.key);\n onSelect(entry.node.key, entry.node);\n if (expandOnSelect && entry.isBranch) toggle(entry.node.key);\n }\n\n function focusAt(index: number) {\n const key = visible[index]?.node.key;\n if (key === undefined) return;\n\n const element = nodeRefs.current.get(key);\n if (element) {\n element.focus();\n return;\n }\n\n // Outside the window, so it has to be scrolled in before it exists to receive focus.\n pendingFocusRef.current = key;\n listRef.current?.scrollToIndex(index, { align: 'center' });\n }\n\n useEffect(() => {\n const key = pendingFocusRef.current;\n if (key === null) return;\n const element = nodeRefs.current.get(key);\n if (element) {\n element.focus();\n pendingFocusRef.current = null;\n }\n });\n\n function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>, entry: FlatTreeNode, index: number) {\n if (disabled) return;\n\n switch (event.key) {\n case 'ArrowDown':\n event.preventDefault();\n focusAt(Math.min(index + 1, visible.length - 1));\n break;\n case 'ArrowUp':\n event.preventDefault();\n focusAt(Math.max(index - 1, 0));\n break;\n case 'ArrowRight':\n event.preventDefault();\n if (!entry.isBranch) break;\n if (entry.isExpanded) focusAt(index + 1);\n else toggle(entry.node.key);\n break;\n case 'ArrowLeft': {\n event.preventDefault();\n if (entry.isBranch && entry.isExpanded) {\n toggle(entry.node.key);\n break;\n }\n const parentIndex = visible.findIndex((e) => e.node.key === entry.parentKey);\n if (parentIndex >= 0) focusAt(parentIndex);\n break;\n }\n case 'Home':\n event.preventDefault();\n focusAt(0);\n break;\n case 'End':\n event.preventDefault();\n focusAt(visible.length - 1);\n break;\n case 'Enter':\n case ' ':\n event.preventDefault();\n select(entry);\n break;\n default:\n break;\n }\n }\n\n function renderNode(entry: FlatTreeNode, index: number) {\n const { node, level, isBranch, isExpanded } = entry;\n const isSelected = node.key === selectedKey;\n const isDisabled = disabled || node.disabled;\n\n return (\n <div\n key={node.key}\n ref={(el) => {\n if (el) nodeRefs.current.set(node.key, el);\n else nodeRefs.current.delete(node.key);\n }}\n role='treeitem'\n aria-level={level + 1}\n aria-setsize={entry.setSize}\n aria-posinset={entry.posInSet}\n aria-expanded={isBranch ? isExpanded : undefined}\n aria-selected={isSelected}\n aria-disabled={isDisabled || undefined}\n tabIndex={node.key === activeKey && !isDisabled ? 0 : -1}\n className={cx(\n styles.treeNode,\n isSelected && styles.treeNodeSelected,\n isDisabled && styles.treeNodeDisabled,\n classNames?.node,\n )}\n style={{ paddingLeft: level * indent, ...slotStyles?.node }}\n onClick={() => select(entry)}\n onKeyDown={(event) => handleKeyDown(event, entry, index)}\n >\n <span\n className={cx(\n styles.treeToggle,\n isExpanded && styles.treeToggleOpen,\n !isBranch && styles.treeToggleHidden,\n classNames?.toggle,\n )}\n style={slotStyles?.toggle}\n aria-hidden='true'\n onClick={(event) => {\n event.stopPropagation();\n if (!isDisabled && isBranch) toggle(node.key);\n }}\n >\n {isBranch && renderIcon(toggleIcon)}\n </span>\n\n {node.icon &&\n renderIcon(node.icon, {\n className: cx(styles.treeIcon, classNames?.icon),\n style: slotStyles?.icon,\n })}\n\n <span className={cx(styles.treeLabel, classNames?.label)} style={slotStyles?.label}>\n {node.label}\n </span>\n </div>\n );\n }\n\n return (\n <div\n {...rest}\n role='tree'\n aria-label={ariaLabel}\n aria-disabled={disabled || undefined}\n tabIndex={isWindowed && !disabled ? 0 : undefined}\n onFocus={(event) => {\n if (event.target === event.currentTarget) focusAt(activeIndex);\n }}\n className={cx(styles.tree, disabled && styles.treeDisabled, classNames?.root, className)}\n style={{ ...style, ...slotStyles?.root }}\n >\n {visible.length === 0 ? (\n <div className={cx(styles.treeEmpty, classNames?.empty)} style={slotStyles?.empty}>\n {emptyLabel}\n </div>\n ) : isWindowed ? (\n <VirtualList\n ref={listRef}\n items={visible}\n height={height}\n estimatedRowHeight={estimatedRowHeight}\n getItemKey={(entry) => entry.node.key}\n getItemDepth={(entry) => entry.level}\n ariaRoles={{ container: 'none', row: 'none' }}\n renderRow={renderNode}\n />\n ) : (\n visible.map(renderNode)\n )}\n </div>\n );\n}\n\nexport default Tree;\n"],"mappings":"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"}
1
+ {"version":3,"file":"tree.mjs","names":[],"sources":["../../../../package/common/tree/tree.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from 'react';\nimport type { ReactElement } from 'react';\nimport cx from 'classnames';\nimport { faChevronRight } from '@fortawesome/free-solid-svg-icons';\n\nimport VirtualList from 'components/common/virtualList';\nimport type { VirtualListHandle } from 'components/common/virtualList';\nimport { useComponentDefaults } from '../configProvider';\nimport { renderIcon } from '../utils/renderIcon';\nimport { flattenTree } from './flattenTree';\nimport type { FlatTreeNode, TreeProps, TreeValue } from './tree.types';\n\nimport styles from './tree.module.pcss';\n\nconst EMPTY_KEYS: TreeValue[] = [];\n\nfunction Tree(props: TreeProps): ReactElement {\n const {\n nodes,\n expandedKeys: controlledExpanded,\n defaultExpandedKeys = EMPTY_KEYS,\n onExpandedChange = () => {},\n selectedKey: controlledSelected,\n defaultSelectedKey,\n onSelect = () => {},\n expandOnSelect = true,\n indent = 20,\n toggleIcon = faChevronRight,\n height,\n estimatedRowHeight = 32,\n emptyLabel = 'Nothing here',\n disabled = false,\n 'aria-label': ariaLabel = 'Tree',\n className,\n style,\n classNames,\n styles: slotStyles,\n ...rest\n } = useComponentDefaults('Tree', props);\n\n const nodeRefs = useRef(new Map<TreeValue, HTMLDivElement>());\n const listRef = useRef<VirtualListHandle>(null);\n const pendingFocusRef = useRef<TreeValue | null>(null);\n const isWindowed = height !== undefined;\n\n const isExpandedControlled = controlledExpanded !== undefined;\n const [internalExpanded, setInternalExpanded] = useState<TreeValue[]>(defaultExpandedKeys);\n const expandedKeys = isExpandedControlled ? controlledExpanded : internalExpanded;\n\n const isSelectedControlled = controlledSelected !== undefined;\n const [internalSelected, setInternalSelected] = useState<TreeValue | null>(defaultSelectedKey ?? null);\n const selectedKey = isSelectedControlled ? controlledSelected : internalSelected;\n\n const visible = flattenTree(nodes, expandedKeys);\n\n // Windowed, the tab stop sits on the container instead of this row: a roving tabindex on a row\n // scrolled out of the window would leave the tree with no tab stop at all.\n const activeIndex = Math.max(\n 0,\n visible.findIndex((entry) => entry.node.key === selectedKey),\n );\n const activeKey = visible[activeIndex]?.node.key;\n\n function setExpanded(next: TreeValue[]) {\n if (!isExpandedControlled) setInternalExpanded(next);\n onExpandedChange(next);\n }\n\n function toggle(key: TreeValue) {\n setExpanded(expandedKeys.includes(key) ? expandedKeys.filter((k) => k !== key) : [...expandedKeys, key]);\n }\n\n function select(entry: FlatTreeNode) {\n if (disabled || entry.node.disabled) return;\n if (!isSelectedControlled) setInternalSelected(entry.node.key);\n onSelect(entry.node.key, entry.node);\n if (expandOnSelect && entry.isBranch) toggle(entry.node.key);\n }\n\n function focusAt(index: number) {\n const key = visible[index]?.node.key;\n if (key === undefined) return;\n\n const element = nodeRefs.current.get(key);\n if (element) {\n element.focus();\n return;\n }\n\n // Outside the window, so it has to be scrolled in before it exists to receive focus.\n pendingFocusRef.current = key;\n listRef.current?.scrollToIndex(index, { align: 'center' });\n }\n\n useEffect(() => {\n const key = pendingFocusRef.current;\n if (key === null) return;\n const element = nodeRefs.current.get(key);\n if (element) {\n element.focus();\n pendingFocusRef.current = null;\n }\n });\n\n function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>, entry: FlatTreeNode, index: number) {\n if (disabled) return;\n\n switch (event.key) {\n case 'ArrowDown':\n event.preventDefault();\n focusAt(Math.min(index + 1, visible.length - 1));\n break;\n case 'ArrowUp':\n event.preventDefault();\n focusAt(Math.max(index - 1, 0));\n break;\n case 'ArrowRight':\n event.preventDefault();\n if (!entry.isBranch) break;\n if (entry.isExpanded) focusAt(index + 1);\n else toggle(entry.node.key);\n break;\n case 'ArrowLeft': {\n event.preventDefault();\n if (entry.isBranch && entry.isExpanded) {\n toggle(entry.node.key);\n break;\n }\n const parentIndex = visible.findIndex((e) => e.node.key === entry.parentKey);\n if (parentIndex >= 0) focusAt(parentIndex);\n break;\n }\n case 'Home':\n event.preventDefault();\n focusAt(0);\n break;\n case 'End':\n event.preventDefault();\n focusAt(visible.length - 1);\n break;\n case 'Enter':\n case ' ':\n event.preventDefault();\n select(entry);\n break;\n default:\n break;\n }\n }\n\n function renderNode(entry: FlatTreeNode, index: number) {\n const { node, level, isBranch, isExpanded } = entry;\n const isSelected = node.key === selectedKey;\n const isDisabled = disabled || node.disabled;\n\n return (\n <div\n key={node.key}\n ref={(el) => {\n if (el) nodeRefs.current.set(node.key, el);\n else nodeRefs.current.delete(node.key);\n }}\n role='treeitem'\n aria-level={level + 1}\n aria-setsize={entry.setSize}\n aria-posinset={entry.posInSet}\n aria-expanded={isBranch ? isExpanded : undefined}\n aria-selected={isSelected}\n aria-disabled={isDisabled || undefined}\n tabIndex={node.key === activeKey && !isDisabled ? 0 : -1}\n className={cx(\n styles.treeNode,\n isSelected && styles.treeNodeSelected,\n isDisabled && styles.treeNodeDisabled,\n classNames?.node,\n )}\n style={{ paddingLeft: level * indent, ...slotStyles?.node }}\n onClick={() => select(entry)}\n onKeyDown={(event) => handleKeyDown(event, entry, index)}\n >\n <span\n className={cx(\n styles.treeToggle,\n isExpanded && styles.treeToggleOpen,\n !isBranch && styles.treeToggleHidden,\n classNames?.toggle,\n )}\n style={slotStyles?.toggle}\n aria-hidden='true'\n onClick={(event) => {\n event.stopPropagation();\n if (!isDisabled && isBranch) toggle(node.key);\n }}\n >\n {isBranch && renderIcon(toggleIcon)}\n </span>\n\n {node.icon &&\n renderIcon(node.icon, {\n className: cx(styles.treeIcon, classNames?.icon),\n style: slotStyles?.icon,\n })}\n\n <span className={cx(styles.treeLabel, classNames?.label)} style={slotStyles?.label}>\n {node.label}\n </span>\n </div>\n );\n }\n\n return (\n <div\n {...rest}\n role='tree'\n aria-label={ariaLabel}\n aria-disabled={disabled || undefined}\n tabIndex={isWindowed && !disabled ? 0 : undefined}\n onFocus={(event) => {\n if (event.target === event.currentTarget) focusAt(activeIndex);\n }}\n className={cx(styles.tree, disabled && styles.treeDisabled, classNames?.root, className)}\n style={{ ...style, ...slotStyles?.root }}\n >\n {visible.length === 0 ? (\n <div className={cx(styles.treeEmpty, classNames?.empty)} style={slotStyles?.empty}>\n {emptyLabel}\n </div>\n ) : isWindowed ? (\n <VirtualList\n ref={listRef}\n items={visible}\n height={height}\n estimatedRowHeight={estimatedRowHeight}\n getItemKey={(entry) => entry.node.key}\n getItemDepth={(entry) => entry.level}\n ariaRoles={{ container: 'none', row: 'none' }}\n // Tree owns the keyboard: its rows carry the roving tabindex and their\n // own key handling, and a second tab stop here would sit inside it.\n keyboardNavigation={false}\n renderRow={renderNode}\n />\n ) : (\n visible.map(renderNode)\n )}\n </div>\n );\n}\n\nexport default Tree;\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{columnCellSelector as e,columnWidthVar as t,loadPersistedColumns as n,resolveFrozenWidth as r}from"./virtualList.utils.mjs";import{useCallback as i,useLayoutEffect as a,useRef as o,useState as s}from"react";function c(e){return Math.max(1,e?.minWidth??48)}function l({columns:l,visibleColumnsRef:u,resizableColumns:d,columnWidths:f,onColumnWidthsChange:p,persistColumnsKey:m,rootRef:h}){let g=f!==void 0,[_,v]=s(()=>n(m)?.widths??{}),[y,b]=s(!1),x=g?f:_,S=o(x);S.current=x;let C=i(e=>{g||v(e),p?.(e)},[g,p]),w=i(()=>{let t=h.current,n={...S.current};for(let i of u.current){if(n[i.key]!=null)continue;let a=t?.querySelector(`[aria-rowindex="1"] ${e(i.key)}`),o=a?Math.round(a.getBoundingClientRect().width):0;n[i.key]=o>0?o:r(i)}return n},[h,u]);a(()=>{let e=h.current;if(e)for(let[n,r]of Object.entries(x))e.style.setProperty(t(n),`${r}px`)},[x,h]);let T=i((e,n)=>{h.current?.style.setProperty(t(e),`${n}px`)},[h]),E=i(()=>{g&&requestAnimationFrame(()=>{for(let[e,t]of Object.entries(S.current))T(e,t)})},[T,g]),D=i((e,t)=>{let n=w(),r=c(l?.find(t=>t.key===e)),i={...n,[e]:Math.max(r,Math.round(t(n[e])))};T(e,i[e]),C(i),E()},[T,l,C,w,E]),O=i((e,t)=>{if(e.button!==0)return;e.preventDefault(),e.stopPropagation();let n=e.currentTarget,r=w(),i=r[t],a=e.clientX,o=c(l?.find(e=>e.key===t)),s=i,u=0,d=()=>{u=0,T(t,s)};function f(a){if(u&&cancelAnimationFrame(u),n.releasePointerCapture?.(e.pointerId),n.removeEventListener(`pointermove`,p),n.removeEventListener(`pointerup`,m),n.removeEventListener(`pointercancel`,h),n.removeEventListener(`lostpointercapture`,h),window.removeEventListener(`keydown`,g,!0),document.body.style.cursor=``,document.body.style.userSelect=``,b(!1),a==null){T(t,i);return}a!==i&&C({...r,[t]:a}),E()}function p(e){s=Math.max(o,Math.round(i+e.clientX-a)),u||=requestAnimationFrame(d)}function m(){f(s)}function h(){f(null)}function g(e){e.key===`Escape`&&(e.preventDefault(),f(null))}n.setPointerCapture?.(e.pointerId),n.addEventListener(`pointermove`,p),n.addEventListener(`pointerup`,m),n.addEventListener(`pointercancel`,h),n.addEventListener(`lostpointercapture`,h),window.addEventListener(`keydown`,g,!0),document.body.style.cursor=`col-resize`,document.body.style.userSelect=`none`,b(!0),Object.keys(S.current).length===0&&C(r)},[T,l,C,w,E]),k=i(n=>{let r=h.current;if(!r)return 0;let i=t(n),a=r.style.getPropertyValue(i),o=r.style.getPropertyValue(`--vl-total`);r.style.setProperty(i,`max-content`),r.style.removeProperty(`--vl-total`);let s=0;return r.querySelectorAll(e(n)).forEach(e=>{let t=Math.ceil(e.getBoundingClientRect().width);t>s&&(s=t)}),r.style.setProperty(i,a),o&&r.style.setProperty(`--vl-total`,o),Math.min(s,r.clientWidth)},[h]),A=i(e=>{let t=()=>{let t=k(e);t>0&&D(e,()=>t)};Object.keys(S.current).length>0?t():(C(w()),requestAnimationFrame(t))},[C,w,k,D]),j=i(()=>{let e=h.current;if(e)for(let n of Object.keys(S.current))e.style.removeProperty(t(n));C({})},[C,h]),M=i(e=>d&&e.resizable!==!1,[d]),N=i((e,t)=>D(e,e=>e+t),[D]),P=i(e=>D(e,()=>c(l?.find(t=>t.key===e))),[l,D]),F=i(e=>M(e)?{"aria-hidden":!0,onPointerDown:t=>O(t,e.key),onDoubleClick:t=>{t.stopPropagation(),A(e.key)},onClick:e=>e.stopPropagation()}:null,[A,O,M]);return{widths:x,isFrozen:Object.keys(x).length>0,isResizing:y,reset:j,isResizable:M,resizeBy:N,resizeToMin:P,autoFit:A,getHandleProps:F}}export{l as useColumnResize};
2
+ //# sourceMappingURL=useColumnResize.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useColumnResize.mjs","names":[],"sources":["../../../../package/common/virtualList/useColumnResize.ts"],"sourcesContent":["import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';\n\nimport {\n columnCellSelector,\n columnWidthVar,\n loadPersistedColumns,\n resolveFrozenWidth,\n MIN_COLUMN_WIDTH,\n} from './virtualList.utils';\nimport type { VirtualListColumn } from './virtualList.types';\n\ninterface UseColumnResizeArgs<T> {\n columns?: VirtualListColumn<T>[];\n /**\n * The columns currently rendered, in order. A ref rather than a value because\n * the visible set is derived one hook later, and only the freeze reads it, at\n * event time.\n */\n visibleColumnsRef: React.RefObject<VirtualListColumn<T>[]>;\n resizableColumns: boolean;\n /** Controlled widths. When provided, internal state is ignored. */\n columnWidths?: Record<string, number>;\n onColumnWidthsChange?: (widths: Record<string, number>) => void;\n /** Restores widths saved under this key. Uncontrolled mode only. */\n persistColumnsKey?: string;\n rootRef: React.RefObject<HTMLDivElement | null>;\n}\n\nexport interface UseColumnResizeResult<T> {\n widths: Record<string, number>;\n /**\n * True once every visible column has an exact px width. Nothing moves at that\n * moment, but from then on the layout is pixels rather than `fr`/`fit`.\n */\n isFrozen: boolean;\n isResizing: boolean;\n /** Drops every user width and hands the layout back to the column definitions. */\n reset: () => void;\n /** Whether this column takes a resize at all, by handle or by keyboard. */\n isResizable: (col: VirtualListColumn<T>) => boolean;\n /** Widens (or, negative, narrows) one column. The keyboard entry point. */\n resizeBy: (key: string, delta: number) => void;\n /** Takes one column down to its minimum. */\n resizeToMin: (key: string) => void;\n /** Sizes one column to its widest rendered cell. */\n autoFit: (key: string) => void;\n /** Pointer props for one column's handle, or null when that column can't be resized. */\n getHandleProps: (col: VirtualListColumn<T>) => React.HTMLAttributes<HTMLDivElement> | null;\n}\n\nfunction minWidthFor<T>(col: VirtualListColumn<T> | undefined): number {\n return Math.max(1, col?.minWidth ?? MIN_COLUMN_WIDTH);\n}\n\n/**\n * User column resizing: the width state, and a drag that doesn't go through\n * React.\n *\n * A pointer move writes the dragged column's custom property straight onto the\n * list element. The header and every row read that same property, so one DOM\n * write resizes all of them in a single layout pass. Going through state\n * instead would re-render the header plus every visible row on every frame, and\n * the handle would trail the cursor by one frame, which is the whole difference\n * between this feeling attached to the pointer and feeling cheap. React is told\n * once, on release.\n *\n * The first resize **freezes** every visible column to its measured width. A\n * `fr` track can't express \"this one column is now 240px\" without the others\n * silently absorbing the difference, and once they hit their minimum the dragged\n * edge stops following the cursor, which is the worst moment for it to happen.\n * Frozen, the table is exact pixels and simply scrolls horizontally instead.\n */\nexport function useColumnResize<T>({\n columns,\n visibleColumnsRef,\n resizableColumns,\n columnWidths,\n onColumnWidthsChange,\n persistColumnsKey,\n rootRef,\n}: UseColumnResizeArgs<T>): UseColumnResizeResult<T> {\n const isControlled = columnWidths !== undefined;\n const [internalWidths, setInternalWidths] = useState<Record<string, number>>(\n () => loadPersistedColumns(persistColumnsKey)?.widths ?? {},\n );\n const [isResizing, setIsResizing] = useState(false);\n const effectiveWidths = isControlled ? columnWidths! : internalWidths;\n\n // Read by the pointer handlers, which are installed once per drag and must not\n // capture a stale snapshot of either.\n const widthsRef = useRef(effectiveWidths);\n widthsRef.current = effectiveWidths;\n\n const commit = useCallback(\n (next: Record<string, number>) => {\n if (!isControlled) setInternalWidths(next);\n onColumnWidthsChange?.(next);\n },\n [isControlled, onColumnWidthsChange],\n );\n\n /**\n * Current width of every visible column, measured from the rendered header so\n * the freeze is invisible: the px values are the ones already on screen.\n * Columns that already have a width keep it, and a header cell that can't be\n * measured (jsdom, a display:none list) falls back to its declared width.\n */\n const freeze = useCallback((): Record<string, number> => {\n const root = rootRef.current;\n const next: Record<string, number> = { ...widthsRef.current };\n for (const col of visibleColumnsRef.current) {\n if (next[col.key] != null) continue;\n const cell = root?.querySelector(`[aria-rowindex=\"1\"] ${columnCellSelector(col.key)}`);\n const measured = cell ? Math.round(cell.getBoundingClientRect().width) : 0;\n next[col.key] = measured > 0 ? measured : resolveFrozenWidth(col);\n }\n return next;\n }, [rootRef, visibleColumnsRef]);\n\n // The widths live on the element as custom properties, written from here and\n // from the drag, never through React's `style`. One owner means a drag's\n // imperative writes can't be clobbered by a render that happens to land\n // mid-gesture, and the steady state still follows the committed widths.\n useLayoutEffect(() => {\n const root = rootRef.current;\n if (!root) return;\n for (const [key, width] of Object.entries(effectiveWidths))\n root.style.setProperty(columnWidthVar(key), `${width}px`);\n }, [effectiveWidths, rootRef]);\n\n const applyWidth = useCallback(\n (key: string, width: number) => {\n rootRef.current?.style.setProperty(columnWidthVar(key), `${width}px`);\n },\n [rootRef],\n );\n\n /**\n * In controlled mode the drag has already written widths the prop may not\n * agree with. A frame later either the consumer has accepted them (the ref\n * holds the new values and this writes them again, a no-op) or it hasn't, and\n * the columns snap back to what the prop says. Without it a rejected width\n * would stay on screen, since nothing re-rendered to correct it.\n */\n const syncControlled = useCallback(() => {\n if (!isControlled) return;\n requestAnimationFrame(() => {\n for (const [key, width] of Object.entries(widthsRef.current)) applyWidth(key, width);\n });\n }, [applyWidth, isControlled]);\n\n const resize = useCallback(\n (key: string, toWidth: (from: number) => number) => {\n const base = freeze();\n const min = minWidthFor(columns?.find((c) => c.key === key));\n const next = { ...base, [key]: Math.max(min, Math.round(toWidth(base[key]))) };\n applyWidth(key, next[key]);\n commit(next);\n syncControlled();\n },\n [applyWidth, columns, commit, freeze, syncControlled],\n );\n\n const beginDrag = useCallback(\n (event: React.PointerEvent<HTMLDivElement>, key: string) => {\n if (event.button !== 0) return;\n // The handle sits on top of the header's sort button; without this the\n // drag would also count as a click on it.\n event.preventDefault();\n event.stopPropagation();\n\n const handle = event.currentTarget;\n const base = freeze();\n const startWidth = base[key];\n const startX = event.clientX;\n const min = minWidthFor(columns?.find((c) => c.key === key));\n\n let width = startWidth;\n let frame = 0;\n const paint = () => {\n frame = 0;\n applyWidth(key, width);\n };\n\n function finish(commitWidth: number | null) {\n if (frame) cancelAnimationFrame(frame);\n handle.releasePointerCapture?.(event.pointerId);\n handle.removeEventListener('pointermove', onMove);\n handle.removeEventListener('pointerup', onUp);\n handle.removeEventListener('pointercancel', onCancel);\n handle.removeEventListener('lostpointercapture', onCancel);\n window.removeEventListener('keydown', onKeyDown, true);\n document.body.style.cursor = '';\n document.body.style.userSelect = '';\n setIsResizing(false);\n if (commitWidth == null) {\n // The freeze was committed at pointerdown and stays: on its own it\n // changes nothing the user can see. Only the drag is undone.\n applyWidth(key, startWidth);\n return;\n }\n if (commitWidth !== startWidth) commit({ ...base, [key]: commitWidth });\n syncControlled();\n }\n\n function onMove(moveEvent: PointerEvent) {\n // Always from the values captured at pointerdown, never from the\n // element's current geometry: re-measuring mid-drag lets rounding\n // accumulate and the handle slides away from the cursor.\n width = Math.max(min, Math.round(startWidth + moveEvent.clientX - startX));\n if (!frame) frame = requestAnimationFrame(paint);\n }\n function onUp() {\n finish(width);\n }\n function onCancel() {\n finish(null);\n }\n function onKeyDown(keyEvent: KeyboardEvent) {\n if (keyEvent.key !== 'Escape') return;\n keyEvent.preventDefault();\n finish(null);\n }\n\n handle.setPointerCapture?.(event.pointerId);\n handle.addEventListener('pointermove', onMove);\n handle.addEventListener('pointerup', onUp);\n handle.addEventListener('pointercancel', onCancel);\n handle.addEventListener('lostpointercapture', onCancel);\n window.addEventListener('keydown', onKeyDown, true);\n // On the body, not on the handle: the pointer leaves the handle as soon as\n // the drag starts, and the cursor must not flicker back to the default.\n document.body.style.cursor = 'col-resize';\n document.body.style.userSelect = 'none';\n setIsResizing(true);\n // Committing the freeze up front, rather than on release: until React has\n // rendered the frozen template, the tracks are still `fr`/`fit` and read\n // no custom property, so the drag would move nothing at all.\n if (Object.keys(widthsRef.current).length === 0) commit(base);\n },\n [applyWidth, columns, commit, freeze, syncControlled],\n );\n\n /**\n * Width the column would need for its widest cell, measured by sizing its\n * track to `max-content` for one synchronous layout.\n *\n * Reading `scrollWidth` off the cells instead reports the *min*-content width:\n * a body cell is free to wrap, so long text never overflows it, it only makes\n * the row taller, and the widest thing that can't wrap is a single word.\n * `max-content` is the width with no wrapping at all, which is what fitting a\n * column means. The header and every row are separate grids, so each sizes its\n * own track independently and all of them have to be read: the widest is the\n * one width that fits the whole column.\n */\n const measureAutoFit = useCallback(\n (key: string): number => {\n const root = rootRef.current;\n if (!root) return 0;\n const widthProperty = columnWidthVar(key);\n const previousWidth = root.style.getPropertyValue(widthProperty);\n // `--vl-total` is a calc() over these same properties, so it turns invalid\n // while one of them holds a keyword, and the header and body would be left\n // with an unresolvable width mid-measurement.\n const previousTotal = root.style.getPropertyValue('--vl-total');\n\n root.style.setProperty(widthProperty, 'max-content');\n root.style.removeProperty('--vl-total');\n let measured = 0;\n root.querySelectorAll(columnCellSelector(key)).forEach((cell) => {\n const width = Math.ceil(cell.getBoundingClientRect().width);\n if (width > measured) measured = width;\n });\n root.style.setProperty(widthProperty, previousWidth);\n if (previousTotal) root.style.setProperty('--vl-total', previousTotal);\n\n // Only the rendered rows are measurable, so this fits what is on screen.\n return Math.min(measured, root.clientWidth);\n },\n [rootRef],\n );\n\n const autoFit = useCallback(\n (key: string) => {\n const run = () => {\n const measured = measureAutoFit(key);\n if (measured > 0) resize(key, () => measured);\n };\n // Until the frozen template has rendered, the tracks read no custom\n // property and sizing one to `max-content` measures nothing. A drag has\n // already frozen by the time its double-click lands; a keyboard `End` on\n // an untouched list has not, so it waits for the render it just asked for.\n if (Object.keys(widthsRef.current).length > 0) run();\n else {\n commit(freeze());\n requestAnimationFrame(run);\n }\n },\n [commit, freeze, measureAutoFit, resize],\n );\n\n const reset = useCallback(() => {\n const root = rootRef.current;\n // The layout effect only ever writes the widths it has, so the properties of\n // the ones being dropped have to be cleared here or they would linger and\n // reappear with the next freeze.\n if (root) for (const key of Object.keys(widthsRef.current)) root.style.removeProperty(columnWidthVar(key));\n commit({});\n }, [commit, rootRef]);\n\n const isResizable = useCallback(\n (col: VirtualListColumn<T>) => resizableColumns && col.resizable !== false,\n [resizableColumns],\n );\n\n const resizeBy = useCallback((key: string, delta: number) => resize(key, (w) => w + delta), [resize]);\n\n const resizeToMin = useCallback(\n (key: string) => resize(key, () => minWidthFor(columns?.find((c) => c.key === key))),\n [columns, resize],\n );\n\n const getHandleProps = useCallback(\n (col: VirtualListColumn<T>): React.HTMLAttributes<HTMLDivElement> | null => {\n if (!isResizable(col)) return null;\n return {\n // Deliberately not focusable, and hidden from assistive technology: the\n // keyboard path is Alt plus the arrow keys on the header cell itself, so\n // a second tab stop per column would buy nothing and cost a lot.\n 'aria-hidden': true,\n onPointerDown: (event: React.PointerEvent<HTMLDivElement>) => beginDrag(event, col.key),\n onDoubleClick: (event: React.MouseEvent) => {\n event.stopPropagation();\n autoFit(col.key);\n },\n onClick: (event: React.MouseEvent) => event.stopPropagation(),\n };\n },\n [autoFit, beginDrag, isResizable],\n );\n\n return {\n widths: effectiveWidths,\n isFrozen: Object.keys(effectiveWidths).length > 0,\n isResizing,\n reset,\n isResizable,\n resizeBy,\n resizeToMin,\n autoFit,\n getHandleProps,\n };\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{COLUMN_STORAGE_PREFIX as e,buildColumnTemplate as t,isColumnInitiallyVisible as n,loadPersistedColumns 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({columns:l,items:u,multiSelect:d,visibleColumnKeys:f,onVisibleColumnsChange:p,persistColumnsKey:m,showColumnToggle:h,rootRef:g,startIndex:_,endIndex:v,containerHeight:y,containerWidth:b}){let x=f!==void 0,[S,C]=c(!1),[w,T]=c({}),[E,D]=c(()=>{let e=r(m);return new Set((l??[]).filter(t=>n(t,e)).map(e=>e.key))}),O=s(new Set((l??[]).map(e=>e.key)));a(()=>{if(x)return;let e=l??[],t=new Set(e.map(e=>e.key)),i=O.current,a=t.size===i.size;if(a){for(let e of t)if(!i.has(e)){a=!1;break}}if(a)return;O.current=t;let o=r(m);D(t=>{let r=new Set;for(let a of e)i.has(a.key)?t.has(a.key)&&r.add(a.key):n(a,o)&&r.add(a.key);return r})},[l,x,m]),a(()=>{if(x||!m||typeof window>`u`)return;let t=l??[];if(t.length!==0&&E.size!==0)try{window.localStorage.setItem(e+m,JSON.stringify({visible:[...E],known:t.map(e=>e.key)}))}catch{}},[E,l,x,m]);let k=x?new Set(f):E,A=(l??[]).filter(e=>e.lockVisible).map(e=>e.key),j=A.some(e=>!k.has(e))?new Set([...k,...A]):k,M=!!l?.length,N=M?l.filter(e=>j.has(e.key)):[],P=M?l.filter(e=>!e.defaultHidden&&!j.has(e.key)).length:0,F=M?`${d&&M?`40px `:``}${t(N,w)}`:void 0,I=h!==!1&&M&&l.some(e=>!e.hideFromToggle),L=N.filter(e=>e.width.type===`fit`),R=L.length>0,z=L.map(e=>e.key).join(`|`),[B,V]=c({key:z,items:u});(B.key!==z||B.items!==u)&&(V({key:z,items:u}),Object.keys(w).length>0&&T({}));let H=i(()=>{let e=g.current;if(!e)return;let t=new Map;e.querySelectorAll(`[data-fit-key]`).forEach(e=>{let n=e,r=n.getAttribute(`data-fit-key`);if(!r)return;let i=n.scrollWidth>n.clientWidth,a=n.scrollWidth;if(i||n.firstElementChild){let e=getComputedStyle(n),t=parseFloat(e.paddingLeft)||0,r=parseFloat(e.paddingRight)||0;if(i&&(a+=r),n.firstElementChild&&!i&&n.scrollWidth<=t+r+1){let e=1/0,i=-1/0;if(n.querySelectorAll(`*`).forEach(t=>{let n=t.getBoundingClientRect();n.left<e&&(e=n.left),n.right>i&&(i=n.right)}),i>e){let n=Math.ceil(i-e)+t+r;n>a&&(a=n)}}}a>(t.get(r)??0)&&t.set(r,a)}),t.size!==0&&T(n=>{let r=!1,i={...n};for(let[a,o]of t){if(o<=0)continue;let t=Math.min(Math.ceil(o),e.clientWidth);t>(n[a]??0)&&(i[a]=t,r=!0)}return r?i:n})},[g]),[U]=c(()=>typeof ResizeObserver>`u`?null:new ResizeObserver(()=>H())),W=s(new Set);a(()=>()=>{U?.disconnect(),W.current.clear()},[U]),o(()=>{if(!R)return;let e=g.current;if(!e||(H(),!U))return;let t=new Set(e.querySelectorAll(`[data-fit-key]`)),n=W.current;for(let e of n)t.has(e)||(U.unobserve(e),n.delete(e));for(let e of t)n.has(e)||(U.observe(e),n.add(e))},[g,R,z,w,_,v,y,b,u,H,U]),a(()=>{if(!R)return;let e=0,t=requestAnimationFrame(()=>{H(),e=requestAnimationFrame(()=>H())});return()=>{cancelAnimationFrame(t),cancelAnimationFrame(e)}},[R,z,u,H]);let G=i(()=>T({}),[]);function K(e){if(l?.find(t=>t.key===e)?.lockVisible)return;let t=new Set(j);if(t.has(e)){if(t.size<=1)return;t.delete(e)}else t.add(e);let n=[...t];x||D(t),p?.(n)}return{hasColumns:M,visibleColumns:N,columnTemplate:F,shouldShowToggle:I,effectiveVisible:j,manuallyHiddenCount:P,toggleOpen:S,setToggleOpen:C,handleToggleColumn:K,recalculateColumns:G}}export{l as useColumns};
1
+ import{COLUMN_STORAGE_PREFIX as e,buildColumnTemplate as t,buildFrozenTemplate as n,columnWidthVar as r,isColumnInitiallyVisible as i,loadPersistedColumns as a,measureCellWidth as o}from"./virtualList.utils.mjs";import{useCallback as s,useEffect as c,useLayoutEffect as l,useRef as u,useState as d}from"react";function f({columns:f,items:p,multiSelect:m,visibleColumnKeys:h,onVisibleColumnsChange:g,persistColumnsKey:_,showColumnToggle:v,userWidths:y,isFrozen:b,rootRef:x,startIndex:S,endIndex:C,containerHeight:w,containerWidth:T}){let E=h!==void 0,[D,O]=d(!1),[k,A]=d({}),[j,M]=d(()=>{let e=a(_);return new Set((f??[]).filter(t=>i(t,e)).map(e=>e.key))}),N=u(new Set((f??[]).map(e=>e.key)));c(()=>{if(E)return;let e=f??[],t=new Set(e.map(e=>e.key)),n=N.current,r=t.size===n.size;if(r){for(let e of t)if(!n.has(e)){r=!1;break}}if(r)return;N.current=t;let o=a(_);M(t=>{let r=new Set;for(let a of e)n.has(a.key)?t.has(a.key)&&r.add(a.key):i(a,o)&&r.add(a.key);return r})},[f,E,_]),c(()=>{if(E||!_||typeof window>`u`)return;let t=f??[];if(t.length!==0&&j.size!==0)try{window.localStorage.setItem(e+_,JSON.stringify({visible:[...j],known:t.map(e=>e.key),widths:y}))}catch{}},[j,f,E,_,y]);let P=E?new Set(h):j,F=(f??[]).filter(e=>e.lockVisible).map(e=>e.key),I=F.some(e=>!P.has(e))?new Set([...P,...F]):P,L=!!f?.length,R=L?f.filter(e=>I.has(e.key)):[],z=L?f.filter(e=>!e.defaultHidden&&!I.has(e.key)).length:0,B=L?`${m&&L?`40px `:``}${b?n(R):t(R,k)}`:void 0,V=v!==!1&&L&&f.some(e=>!e.hideFromToggle),H={};if(b&&L){let e=R.map(e=>`var(${r(e.key)}, 160px)`);m&&e.unshift(`40px`),V&&e.push(`32px`),H[`--vl-total`]=`calc(${e.join(` + `)})`}let U=R.filter(e=>e.width.type===`fit`),W=!b&&U.length>0,G=U.map(e=>e.key).join(`|`),[K,q]=d({key:G,items:p});(K.key!==G||K.items!==p)&&(q({key:G,items:p}),Object.keys(k).length>0&&A({}));let J=s(()=>{let e=x.current;if(!e)return;let t=new Map;e.querySelectorAll(`[data-fit-key]`).forEach(e=>{let n=e,r=n.getAttribute(`data-fit-key`);if(!r)return;let i=o(n);i>(t.get(r)??0)&&t.set(r,i)}),t.size!==0&&A(n=>{let r=!1,i={...n};for(let[a,o]of t){if(o<=0)continue;let t=Math.min(Math.ceil(o),e.clientWidth);t>(n[a]??0)&&(i[a]=t,r=!0)}return r?i:n})},[x]),[Y]=d(()=>typeof ResizeObserver>`u`?null:new ResizeObserver(()=>J())),X=u(new Set);c(()=>()=>{Y?.disconnect(),X.current.clear()},[Y]),l(()=>{if(!W)return;let e=x.current;if(!e||(J(),!Y))return;let t=new Set(e.querySelectorAll(`[data-fit-key]`)),n=X.current;for(let e of n)t.has(e)||(Y.unobserve(e),n.delete(e));for(let e of t)n.has(e)||(Y.observe(e),n.add(e))},[x,W,G,k,S,C,w,T,p,J,Y]),c(()=>{if(!W)return;let e=0,t=requestAnimationFrame(()=>{J(),e=requestAnimationFrame(()=>J())});return()=>{cancelAnimationFrame(t),cancelAnimationFrame(e)}},[W,G,p,J]);let Z=s(()=>A({}),[]);function Q(e){if(f?.find(t=>t.key===e)?.lockVisible)return;let t=new Set(I);if(t.has(e)){if(t.size<=1)return;t.delete(e)}else t.add(e);let n=[...t];E||M(t),g?.(n)}return{hasColumns:L,visibleColumns:R,columnTemplate:B,widthVars:H,shouldShowToggle:V,effectiveVisible:I,manuallyHiddenCount:z,toggleOpen:D,setToggleOpen:O,handleToggleColumn:Q,recalculateColumns:Z}}export{f as useColumns};
2
2
  //# sourceMappingURL=useColumns.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"useColumns.mjs","names":[],"sources":["../../../../package/common/virtualList/useColumns.ts"],"sourcesContent":["import React, { useState, useRef, useEffect, useLayoutEffect, useCallback } from 'react';\n\nimport {\n buildColumnTemplate,\n loadPersistedColumns,\n isColumnInitiallyVisible,\n COLUMN_STORAGE_PREFIX,\n type PersistedColumns,\n} from './virtualList.utils';\nimport type { VirtualListColumn } from './virtualList.types';\n\ninterface UseColumnsArgs<T> {\n columns?: VirtualListColumn<T>[];\n items: T[];\n multiSelect: boolean;\n visibleColumnKeys?: string[];\n onVisibleColumnsChange?: (keys: string[]) => void;\n persistColumnsKey?: string;\n showColumnToggle?: boolean;\n rootRef: React.RefObject<HTMLDivElement | null>;\n /** Visible-window bounds — re-measure `fit` columns as new cells scroll in. */\n startIndex: number;\n endIndex: number;\n containerHeight: number;\n /** Re-measure `fit` columns when the container width settles (e.g. after a route transition). */\n containerWidth: number;\n}\n\ninterface UseColumnsResult<T> {\n hasColumns: boolean;\n visibleColumns: VirtualListColumn<T>[];\n columnTemplate: string | undefined;\n shouldShowToggle: boolean;\n effectiveVisible: Set<string>;\n /** How many default-visible columns the user has hidden (excludes defaultHidden). */\n manuallyHiddenCount: number;\n toggleOpen: boolean;\n setToggleOpen: React.Dispatch<React.SetStateAction<boolean>>;\n handleToggleColumn: (key: string) => void;\n /** Imperative escape hatch: drop fit-column pins so they re-measure on the\n * next layout. For out-of-band content changes that don't flow through a new\n * `items` array or `columns` set (which reset automatically). */\n recalculateColumns: () => void;\n}\n\n/**\n * Table-mode column logic: visibility (controlled or uncontrolled, with\n * localStorage persistence and runtime reconciliation), the toggle popover, and\n * `fit`-column width measurement that pins one shared px track across the header\n * and every row grid so they stay aligned.\n */\nexport function useColumns<T>({\n columns,\n items,\n multiSelect,\n visibleColumnKeys,\n onVisibleColumnsChange,\n persistColumnsKey,\n showColumnToggle,\n rootRef,\n startIndex,\n endIndex,\n containerHeight,\n containerWidth,\n}: UseColumnsArgs<T>): UseColumnsResult<T> {\n const isControlled = visibleColumnKeys !== undefined;\n const [toggleOpen, setToggleOpen] = useState(false);\n // Measured px widths for `fit` columns, keyed by column key. Pinning these\n // into the shared column template keeps the header and rows aligned (a raw\n // `fit-content` track resolves per-grid and drifts). See the effects below.\n const [fitWidths, setFitWidths] = useState<Record<string, number>>({});\n\n const [internalVisibleKeys, setInternalVisibleKeys] = useState<Set<string>>(() => {\n const persisted = loadPersistedColumns(persistColumnsKey);\n return new Set((columns ?? []).filter((c) => isColumnInitiallyVisible(c, persisted)).map((c) => c.key));\n });\n // The set of column keys the internal visibility state was last reconciled\n // against. The lazy initializer above only runs at mount, so without this a\n // list that mounts in list mode (no columns) and later switches to table mode\n // would keep an empty visible set and render no columns.\n const knownColumnKeysRef = useRef<Set<string>>(new Set((columns ?? []).map((c) => c.key)));\n\n // Re-derive internal visibility when the column set changes at runtime: new\n // columns get their saved/default visibility, removed columns drop out, and\n // existing columns keep whatever the user toggled them to. This is also what\n // applies persisted settings when a list switches from list mode to table\n // mode (columns absent at mount, supplied later).\n useEffect(() => {\n if (isControlled) return;\n const cols = columns ?? [];\n const currentKeys = new Set(cols.map((c) => c.key));\n const known = knownColumnKeysRef.current;\n let identical = currentKeys.size === known.size;\n if (identical)\n for (const k of currentKeys)\n if (!known.has(k)) {\n identical = false;\n break;\n }\n if (identical) return;\n knownColumnKeysRef.current = currentKeys;\n const persisted = loadPersistedColumns(persistColumnsKey);\n setInternalVisibleKeys((prev) => {\n const next = new Set<string>();\n for (const c of cols) {\n if (known.has(c.key)) {\n if (prev.has(c.key)) next.add(c.key); // preserve the current-session choice\n } else if (isColumnInitiallyVisible(c, persisted)) {\n next.add(c.key); // newly-added column: saved setting, else defaultHidden\n }\n }\n return next;\n });\n }, [columns, isControlled, persistColumnsKey]);\n\n // Persist visibility whenever it changes (uncontrolled mode only). Skip while\n // there are no columns, or the transient empty state during a list→table\n // switch before the reconcile effect above has filled in the visible set —\n // writing then would clobber the saved config with an empty set.\n useEffect(() => {\n if (isControlled || !persistColumnsKey || typeof window === 'undefined') return;\n const cols = columns ?? [];\n if (cols.length === 0 || internalVisibleKeys.size === 0) return;\n try {\n window.localStorage.setItem(\n COLUMN_STORAGE_PREFIX + persistColumnsKey,\n JSON.stringify({ visible: [...internalVisibleKeys], known: cols.map((c) => c.key) } satisfies PersistedColumns),\n );\n } catch {\n // Storage full or unavailable — non-fatal, just don't persist.\n }\n }, [internalVisibleKeys, columns, isControlled, persistColumnsKey]);\n\n // Locked columns can never be hidden — force them into the visible set even if\n // a controlled `visibleColumnKeys` or persisted state omits them. Only clone\n // (rather than mutate the state Set) when a locked key is actually missing;\n // in uncontrolled mode they're already present via isColumnInitiallyVisible.\n const baseVisible: Set<string> = isControlled ? new Set(visibleColumnKeys) : internalVisibleKeys;\n const lockedKeys = (columns ?? []).filter((c) => c.lockVisible).map((c) => c.key);\n const effectiveVisible: Set<string> = lockedKeys.some((k) => !baseVisible.has(k))\n ? new Set([...baseVisible, ...lockedKeys])\n : baseVisible;\n\n const hasColumns = !!columns?.length;\n const visibleColumns = hasColumns ? columns!.filter((c) => effectiveVisible.has(c.key)) : [];\n // A column counts as \"manually hidden\" only if it's default-visible yet\n // currently hidden — a defaultHidden column being hidden is its default\n // state, not a user choice, so it never contributes to the chip.\n const manuallyHiddenCount = hasColumns\n ? columns!.filter((c) => !c.defaultHidden && !effectiveVisible.has(c.key)).length\n : 0;\n const selectPrefix = multiSelect && hasColumns ? '40px ' : '';\n const columnTemplate = hasColumns ? `${selectPrefix}${buildColumnTemplate(visibleColumns, fitWidths)}` : undefined;\n // Hide the gear entirely when nothing is left to toggle (every column opts out).\n const shouldShowToggle = showColumnToggle !== false && hasColumns && columns!.some((c) => !c.hideFromToggle);\n\n // ── `fit` column width measurement ──────────────────────────────────────────\n const fitColumns = visibleColumns.filter((c) => c.width.type === 'fit');\n const hasFit = fitColumns.length > 0;\n const fitColKey = fitColumns.map((c) => c.key).join('|');\n\n // Drop the measured pins when the data or the fit-column set changes, so the\n // measure effect below re-reads the true intrinsic width on the next pass —\n // this is what lets a column shrink (during scroll the effect only grows it,\n // to avoid jitter). Resetting state during render is React's recommended\n // pattern for \"reset on prop change\" and avoids a cascading effect render.\n const [fitReset, setFitReset] = useState<{ key: string; items: T[] }>({ key: fitColKey, items });\n if (fitReset.key !== fitColKey || fitReset.items !== items) {\n setFitReset({ key: fitColKey, items });\n if (Object.keys(fitWidths).length > 0) setFitWidths({});\n }\n\n // Measure every fit cell currently in the DOM (header + visible rows), keyed\n // by its `data-fit-key`, and grow each column's pin to the widest. Reading the\n // key off the DOM (rather than closing over `fitColumns`) keeps this a stable\n // callback the ResizeObserver below can reuse. `scrollWidth` reports the true\n // content width even when the cell is pinned narrower than its content, so a\n // later pass always recovers a column first measured while it was still empty\n // (e.g. an actions cell whose buttons mount a tick after the row commits — its\n // empty width is just the padding, ~24px, which \"only grow\" can't undo).\n const measureFitWidths = useCallback(() => {\n const root = rootRef.current;\n if (!root) return;\n const maxByKey = new Map<string, number>();\n root.querySelectorAll('[data-fit-key]').forEach((node) => {\n const el = node as HTMLElement;\n const key = el.getAttribute('data-fit-key');\n if (!key) return;\n const overflowing = el.scrollWidth > el.clientWidth;\n let w = el.scrollWidth;\n // Only resolve computed padding when we actually need it — the overflow\n // fix-up, or the collapse check below — so the hot scroll path stays cheap\n // for plain in-flow cells.\n if (overflowing || el.firstElementChild) {\n const cs = getComputedStyle(el);\n const padLeft = parseFloat(cs.paddingLeft) || 0;\n const padRight = parseFloat(cs.paddingRight) || 0;\n // scrollWidth includes the left padding but drops the right padding when\n // the content overflows — add it back.\n if (overflowing) w += padRight;\n // Collapsed-content case (FontAwesome icons etc.): element children are\n // present, yet scrollWidth is only the cell's padding because their boxes\n // overflow via `overflow: visible` and carry no intrinsic width, so they\n // don't count toward scrollWidth (and, when right-aligned, overflow to the\n // *left* into negative offsets). ONLY then walk the descendants for their\n // true span — once a real width pins, the cell is no longer collapsed, so\n // this stops running on every scroll frame.\n if (el.firstElementChild && !overflowing && el.scrollWidth <= padLeft + padRight + 1) {\n let minLeft = Infinity;\n let maxRight = -Infinity;\n el.querySelectorAll('*').forEach((d) => {\n const r = d.getBoundingClientRect();\n if (r.left < minLeft) minLeft = r.left;\n if (r.right > maxRight) maxRight = r.right;\n });\n if (maxRight > minLeft) {\n const span = Math.ceil(maxRight - minLeft) + padLeft + padRight;\n if (span > w) w = span;\n }\n }\n }\n if (w > (maxByKey.get(key) ?? 0)) maxByKey.set(key, w);\n });\n if (maxByKey.size === 0) return;\n setFitWidths((prev) => {\n let changed = false;\n const next = { ...prev };\n for (const [key, max] of maxByKey) {\n if (max <= 0) continue;\n // Cap at the viewport width so one oversized cell can't blow out layout.\n const pinned = Math.min(Math.ceil(max), root.clientWidth);\n if (pinned > (prev[key] ?? 0)) {\n next[key] = pinned;\n changed = true;\n }\n }\n return changed ? next : prev;\n });\n }, [rootRef]);\n\n // One ResizeObserver shared across all fit cells. It re-measures whenever a\n // cell's box changes — crucially when content first appears after mount (which\n // changes the cell's height even while its width is pinned), so a too-early or\n // empty-cell measure self-corrects instead of sticking. Its initial callback\n // also fires once after layout settles, covering route transitions and other\n // post-mount layout shifts without depending on font/rAF timing.\n const [fitObserver] = useState<ResizeObserver | null>(() =>\n typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(() => measureFitWidths()),\n );\n const observedRef = useRef<Set<Element>>(new Set());\n useEffect(\n () => () => {\n fitObserver?.disconnect();\n observedRef.current.clear();\n },\n [fitObserver],\n );\n\n // Measure synchronously (before paint, so a wider row scrolling in never shows\n // a clipped frame), then *diff* the observed set rather than re-observing every\n // cell. Re-observing fires a redundant ResizeObserver callback per node, and\n // `startIndex`/`endIndex` change every scroll frame — so observing only the\n // cells that entered (and dropping the ones that left) keeps stable, already\n // pinned cells from generating any observer traffic while scrolling. A newly\n // entered cell still fires exactly once, which is when we want to measure it.\n useLayoutEffect(() => {\n if (!hasFit) return;\n const root = rootRef.current;\n if (!root) return;\n measureFitWidths();\n if (!fitObserver) return;\n const current = new Set<Element>(root.querySelectorAll('[data-fit-key]'));\n const observed = observedRef.current;\n for (const el of observed)\n if (!current.has(el)) {\n fitObserver.unobserve(el);\n observed.delete(el);\n }\n for (const el of current)\n if (!observed.has(el)) {\n fitObserver.observe(el);\n observed.add(el);\n }\n }, [\n rootRef,\n hasFit,\n fitColKey,\n fitWidths,\n startIndex,\n endIndex,\n containerHeight,\n containerWidth,\n items,\n measureFitWidths,\n fitObserver,\n ]);\n\n // Cheap insurance: re-measure on the next two frames after mount / data /\n // fit-column changes, in case the icons (or the injected FontAwesome sizing\n // CSS) resolve a frame after the first synchronous pass. Safe and scoped —\n // measureFitWidths only ever grows a pin, and this runs on mount / data change\n // (not every scroll frame), so it keeps the diff-observe perf win.\n useEffect(() => {\n if (!hasFit) return;\n let raf2 = 0;\n const raf1 = requestAnimationFrame(() => {\n measureFitWidths();\n raf2 = requestAnimationFrame(() => measureFitWidths());\n });\n return () => {\n cancelAnimationFrame(raf1);\n cancelAnimationFrame(raf2);\n };\n }, [hasFit, fitColKey, items, measureFitWidths]);\n\n // Force a fresh fit-column measure regardless of current pins. Always installs\n // a new empty object so the measure layout effect re-runs even when nothing\n // was pinned (e.g. content changed while the column was still fit-content).\n const recalculateColumns = useCallback(() => setFitWidths({}), []);\n\n function handleToggleColumn(key: string) {\n // Locked columns can't be toggled off.\n if (columns?.find((c) => c.key === key)?.lockVisible) return;\n const next = new Set(effectiveVisible);\n if (next.has(key)) {\n if (next.size <= 1) return;\n next.delete(key);\n } else {\n next.add(key);\n }\n const nextArray = [...next];\n if (!isControlled) setInternalVisibleKeys(next);\n onVisibleColumnsChange?.(nextArray);\n }\n\n return {\n hasColumns,\n visibleColumns,\n columnTemplate,\n shouldShowToggle,\n effectiveVisible,\n manuallyHiddenCount,\n toggleOpen,\n setToggleOpen,\n handleToggleColumn,\n recalculateColumns,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"useColumns.mjs","names":[],"sources":["../../../../package/common/virtualList/useColumns.ts"],"sourcesContent":["import React, { useState, useRef, useEffect, useLayoutEffect, useCallback } from 'react';\n\nimport {\n buildColumnTemplate,\n buildFrozenTemplate,\n columnWidthVar,\n DEFAULT_FROZEN_WIDTH,\n measureCellWidth,\n loadPersistedColumns,\n isColumnInitiallyVisible,\n COLUMN_STORAGE_PREFIX,\n type PersistedColumns,\n} from './virtualList.utils';\nimport type { VirtualListColumn } from './virtualList.types';\n\ninterface UseColumnsArgs<T> {\n columns?: VirtualListColumn<T>[];\n items: T[];\n multiSelect: boolean;\n visibleColumnKeys?: string[];\n onVisibleColumnsChange?: (keys: string[]) => void;\n persistColumnsKey?: string;\n showColumnToggle?: boolean;\n /** User-set column widths, from `useColumnResize`. */\n userWidths: Record<string, number>;\n /** Whether those widths have taken over the layout. */\n isFrozen: boolean;\n rootRef: React.RefObject<HTMLDivElement | null>;\n /** Visible-window bounds — re-measure `fit` columns as new cells scroll in. */\n startIndex: number;\n endIndex: number;\n containerHeight: number;\n /** Re-measure `fit` columns when the container width settles (e.g. after a route transition). */\n containerWidth: number;\n}\n\nexport interface UseColumnsResult<T> {\n hasColumns: boolean;\n visibleColumns: VirtualListColumn<T>[];\n columnTemplate: string | undefined;\n /**\n * Custom properties for the list element: the total width of a frozen layout,\n * as a `calc()` over the per-column properties so that rewriting one width\n * during a drag updates the total without any bookkeeping.\n */\n widthVars: React.CSSProperties;\n shouldShowToggle: boolean;\n effectiveVisible: Set<string>;\n /** How many default-visible columns the user has hidden (excludes defaultHidden). */\n manuallyHiddenCount: number;\n toggleOpen: boolean;\n setToggleOpen: React.Dispatch<React.SetStateAction<boolean>>;\n handleToggleColumn: (key: string) => void;\n /** Imperative escape hatch: drop fit-column pins so they re-measure on the\n * next layout. For out-of-band content changes that don't flow through a new\n * `items` array or `columns` set (which reset automatically). */\n recalculateColumns: () => void;\n}\n\n/**\n * Table-mode column logic: visibility (controlled or uncontrolled, with\n * localStorage persistence and runtime reconciliation), the toggle popover, and\n * `fit`-column width measurement that pins one shared px track across the header\n * and every row grid so they stay aligned.\n */\nexport function useColumns<T>({\n columns,\n items,\n multiSelect,\n visibleColumnKeys,\n onVisibleColumnsChange,\n persistColumnsKey,\n showColumnToggle,\n userWidths,\n isFrozen,\n rootRef,\n startIndex,\n endIndex,\n containerHeight,\n containerWidth,\n}: UseColumnsArgs<T>): UseColumnsResult<T> {\n const isControlled = visibleColumnKeys !== undefined;\n const [toggleOpen, setToggleOpen] = useState(false);\n // Measured px widths for `fit` columns, keyed by column key. Pinning these\n // into the shared column template keeps the header and rows aligned (a raw\n // `fit-content` track resolves per-grid and drifts). See the effects below.\n const [fitWidths, setFitWidths] = useState<Record<string, number>>({});\n\n const [internalVisibleKeys, setInternalVisibleKeys] = useState<Set<string>>(() => {\n const persisted = loadPersistedColumns(persistColumnsKey);\n return new Set((columns ?? []).filter((c) => isColumnInitiallyVisible(c, persisted)).map((c) => c.key));\n });\n // The set of column keys the internal visibility state was last reconciled\n // against. The lazy initializer above only runs at mount, so without this a\n // list that mounts in list mode (no columns) and later switches to table mode\n // would keep an empty visible set and render no columns.\n const knownColumnKeysRef = useRef<Set<string>>(new Set((columns ?? []).map((c) => c.key)));\n\n // Re-derive internal visibility when the column set changes at runtime: new\n // columns get their saved/default visibility, removed columns drop out, and\n // existing columns keep whatever the user toggled them to. This is also what\n // applies persisted settings when a list switches from list mode to table\n // mode (columns absent at mount, supplied later).\n useEffect(() => {\n if (isControlled) return;\n const cols = columns ?? [];\n const currentKeys = new Set(cols.map((c) => c.key));\n const known = knownColumnKeysRef.current;\n let identical = currentKeys.size === known.size;\n if (identical)\n for (const k of currentKeys)\n if (!known.has(k)) {\n identical = false;\n break;\n }\n if (identical) return;\n knownColumnKeysRef.current = currentKeys;\n const persisted = loadPersistedColumns(persistColumnsKey);\n setInternalVisibleKeys((prev) => {\n const next = new Set<string>();\n for (const c of cols) {\n if (known.has(c.key)) {\n if (prev.has(c.key)) next.add(c.key); // preserve the current-session choice\n } else if (isColumnInitiallyVisible(c, persisted)) {\n next.add(c.key); // newly-added column: saved setting, else defaultHidden\n }\n }\n return next;\n });\n }, [columns, isControlled, persistColumnsKey]);\n\n // Persist visibility whenever it changes (uncontrolled mode only). Skip while\n // there are no columns, or the transient empty state during a list→table\n // switch before the reconcile effect above has filled in the visible set —\n // writing then would clobber the saved config with an empty set.\n useEffect(() => {\n if (isControlled || !persistColumnsKey || typeof window === 'undefined') return;\n const cols = columns ?? [];\n if (cols.length === 0 || internalVisibleKeys.size === 0) return;\n try {\n window.localStorage.setItem(\n COLUMN_STORAGE_PREFIX + persistColumnsKey,\n JSON.stringify({\n visible: [...internalVisibleKeys],\n known: cols.map((c) => c.key),\n widths: userWidths,\n } satisfies PersistedColumns),\n );\n } catch {\n // Storage full or unavailable — non-fatal, just don't persist.\n }\n }, [internalVisibleKeys, columns, isControlled, persistColumnsKey, userWidths]);\n\n // Locked columns can never be hidden — force them into the visible set even if\n // a controlled `visibleColumnKeys` or persisted state omits them. Only clone\n // (rather than mutate the state Set) when a locked key is actually missing;\n // in uncontrolled mode they're already present via isColumnInitiallyVisible.\n const baseVisible: Set<string> = isControlled ? new Set(visibleColumnKeys) : internalVisibleKeys;\n const lockedKeys = (columns ?? []).filter((c) => c.lockVisible).map((c) => c.key);\n const effectiveVisible: Set<string> = lockedKeys.some((k) => !baseVisible.has(k))\n ? new Set([...baseVisible, ...lockedKeys])\n : baseVisible;\n\n const hasColumns = !!columns?.length;\n const visibleColumns = hasColumns ? columns!.filter((c) => effectiveVisible.has(c.key)) : [];\n // A column counts as \"manually hidden\" only if it's default-visible yet\n // currently hidden — a defaultHidden column being hidden is its default\n // state, not a user choice, so it never contributes to the chip.\n const manuallyHiddenCount = hasColumns\n ? columns!.filter((c) => !c.defaultHidden && !effectiveVisible.has(c.key)).length\n : 0;\n const selectPrefix = multiSelect && hasColumns ? '40px ' : '';\n const columnTemplate = hasColumns\n ? `${selectPrefix}${isFrozen ? buildFrozenTemplate(visibleColumns) : buildColumnTemplate(visibleColumns, fitWidths)}`\n : undefined;\n // Hide the gear entirely when nothing is left to toggle (every column opts out).\n const shouldShowToggle = showColumnToggle !== false && hasColumns && columns!.some((c) => !c.hideFromToggle);\n\n // Sum of the tracks, so the header and body can stretch past the viewport and\n // the list scrolls horizontally instead of clipping. Kept as a `calc()` over\n // the same custom properties the tracks read, so a drag rewriting one of them\n // resizes the row canvas in the same layout pass.\n const widthVars: React.CSSProperties = {};\n if (isFrozen && hasColumns) {\n const parts = visibleColumns.map((c) => `var(${columnWidthVar(c.key)}, ${DEFAULT_FROZEN_WIDTH}px)`);\n if (multiSelect) parts.unshift('40px');\n if (shouldShowToggle) parts.push('32px');\n (widthVars as Record<string, string>)['--vl-total'] = `calc(${parts.join(' + ')})`;\n }\n\n // ── `fit` column width measurement ──────────────────────────────────────────\n const fitColumns = visibleColumns.filter((c) => c.width.type === 'fit');\n // A frozen layout is exact px and ignores the pins, so measuring would only\n // fight the user: `measureFitWidths` never shrinks a column, and every drag\n // narrower than the content would be undone on the next scroll frame.\n const hasFit = !isFrozen && fitColumns.length > 0;\n const fitColKey = fitColumns.map((c) => c.key).join('|');\n\n // Drop the measured pins when the data or the fit-column set changes, so the\n // measure effect below re-reads the true intrinsic width on the next pass —\n // this is what lets a column shrink (during scroll the effect only grows it,\n // to avoid jitter). Resetting state during render is React's recommended\n // pattern for \"reset on prop change\" and avoids a cascading effect render.\n const [fitReset, setFitReset] = useState<{ key: string; items: T[] }>({ key: fitColKey, items });\n if (fitReset.key !== fitColKey || fitReset.items !== items) {\n setFitReset({ key: fitColKey, items });\n if (Object.keys(fitWidths).length > 0) setFitWidths({});\n }\n\n // Measure every fit cell currently in the DOM (header + visible rows), keyed\n // by its `data-fit-key`, and grow each column's pin to the widest. Reading the\n // key off the DOM (rather than closing over `fitColumns`) keeps this a stable\n // callback the ResizeObserver below can reuse. `measureCellWidth` reports the\n // true content width even when the cell is pinned narrower, so a later pass\n // always recovers a column first measured while it was still empty.\n const measureFitWidths = useCallback(() => {\n const root = rootRef.current;\n if (!root) return;\n const maxByKey = new Map<string, number>();\n root.querySelectorAll('[data-fit-key]').forEach((node) => {\n const el = node as HTMLElement;\n const key = el.getAttribute('data-fit-key');\n if (!key) return;\n const w = measureCellWidth(el);\n if (w > (maxByKey.get(key) ?? 0)) maxByKey.set(key, w);\n });\n if (maxByKey.size === 0) return;\n setFitWidths((prev) => {\n let changed = false;\n const next = { ...prev };\n for (const [key, max] of maxByKey) {\n if (max <= 0) continue;\n // Cap at the viewport width so one oversized cell can't blow out layout.\n const pinned = Math.min(Math.ceil(max), root.clientWidth);\n if (pinned > (prev[key] ?? 0)) {\n next[key] = pinned;\n changed = true;\n }\n }\n return changed ? next : prev;\n });\n }, [rootRef]);\n\n // One ResizeObserver shared across all fit cells. It re-measures whenever a\n // cell's box changes — crucially when content first appears after mount (which\n // changes the cell's height even while its width is pinned), so a too-early or\n // empty-cell measure self-corrects instead of sticking. Its initial callback\n // also fires once after layout settles, covering route transitions and other\n // post-mount layout shifts without depending on font/rAF timing.\n const [fitObserver] = useState<ResizeObserver | null>(() =>\n typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(() => measureFitWidths()),\n );\n const observedRef = useRef<Set<Element>>(new Set());\n useEffect(\n () => () => {\n fitObserver?.disconnect();\n observedRef.current.clear();\n },\n [fitObserver],\n );\n\n // Measure synchronously (before paint, so a wider row scrolling in never shows\n // a clipped frame), then *diff* the observed set rather than re-observing every\n // cell. Re-observing fires a redundant ResizeObserver callback per node, and\n // `startIndex`/`endIndex` change every scroll frame — so observing only the\n // cells that entered (and dropping the ones that left) keeps stable, already\n // pinned cells from generating any observer traffic while scrolling. A newly\n // entered cell still fires exactly once, which is when we want to measure it.\n useLayoutEffect(() => {\n if (!hasFit) return;\n const root = rootRef.current;\n if (!root) return;\n measureFitWidths();\n if (!fitObserver) return;\n const current = new Set<Element>(root.querySelectorAll('[data-fit-key]'));\n const observed = observedRef.current;\n for (const el of observed)\n if (!current.has(el)) {\n fitObserver.unobserve(el);\n observed.delete(el);\n }\n for (const el of current)\n if (!observed.has(el)) {\n fitObserver.observe(el);\n observed.add(el);\n }\n }, [\n rootRef,\n hasFit,\n fitColKey,\n fitWidths,\n startIndex,\n endIndex,\n containerHeight,\n containerWidth,\n items,\n measureFitWidths,\n fitObserver,\n ]);\n\n // Cheap insurance: re-measure on the next two frames after mount / data /\n // fit-column changes, in case the icons (or the injected FontAwesome sizing\n // CSS) resolve a frame after the first synchronous pass. Safe and scoped —\n // measureFitWidths only ever grows a pin, and this runs on mount / data change\n // (not every scroll frame), so it keeps the diff-observe perf win.\n useEffect(() => {\n if (!hasFit) return;\n let raf2 = 0;\n const raf1 = requestAnimationFrame(() => {\n measureFitWidths();\n raf2 = requestAnimationFrame(() => measureFitWidths());\n });\n return () => {\n cancelAnimationFrame(raf1);\n cancelAnimationFrame(raf2);\n };\n }, [hasFit, fitColKey, items, measureFitWidths]);\n\n // Force a fresh fit-column measure regardless of current pins. Always installs\n // a new empty object so the measure layout effect re-runs even when nothing\n // was pinned (e.g. content changed while the column was still fit-content).\n const recalculateColumns = useCallback(() => setFitWidths({}), []);\n\n function handleToggleColumn(key: string) {\n // Locked columns can't be toggled off.\n if (columns?.find((c) => c.key === key)?.lockVisible) return;\n const next = new Set(effectiveVisible);\n if (next.has(key)) {\n if (next.size <= 1) return;\n next.delete(key);\n } else {\n next.add(key);\n }\n const nextArray = [...next];\n if (!isControlled) setInternalVisibleKeys(next);\n onVisibleColumnsChange?.(nextArray);\n }\n\n return {\n hasColumns,\n visibleColumns,\n columnTemplate,\n widthVars,\n shouldShowToggle,\n effectiveVisible,\n manuallyHiddenCount,\n toggleOpen,\n setToggleOpen,\n handleToggleColumn,\n recalculateColumns,\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{useRef as e,useState as t}from"react";function n(n){let[r,i]=t(0),a=e([]),o=Math.min(r,Math.max(0,n-1));o!==r&&i(o);function s(e){let t=Math.max(0,Math.min(e,n-1));i(t),a.current[t]?.focus()}function c(e){return{tabIndex:e===o?0:-1,ref:t=>{a.current[e]=t},onFocus:()=>i(e)}}function l(e,t){if(e.altKey||e.ctrlKey||e.metaKey)return!1;switch(e.key){case`ArrowRight`:s(t+1);break;case`ArrowLeft`:s(t-1);break;case`Home`:s(0);break;case`End`:s(n-1);break;default:return!1}return e.preventDefault(),e.stopPropagation(),!0}return{getStationProps:c,handleNavigationKey:l}}export{n as useHeaderNavigation};
2
+ //# sourceMappingURL=useHeaderNavigation.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useHeaderNavigation.mjs","names":[],"sources":["../../../../package/common/virtualList/useHeaderNavigation.ts"],"sourcesContent":["import React, { useRef, useState } from 'react';\n\ninterface StationProps {\n tabIndex: number;\n ref: (el: HTMLElement | null) => void;\n onFocus: () => void;\n}\n\ninterface UseHeaderNavigationResult {\n getStationProps: (index: number) => StationProps;\n /** True when the key moved the focus, so the caller stops handling it. */\n handleNavigationKey: (event: React.KeyboardEvent, index: number) => boolean;\n}\n\n/**\n * Roving tab stop across the header: the select-all cell, every column header,\n * and the column menu are stations, and arrow keys move between them.\n *\n * Without it each of those is its own tab stop, so a table with eight sortable,\n * resizable columns takes seventeen presses of Tab before the data. The stops\n * also belong together: they all act on the same header, which is exactly the\n * case the roving pattern exists for.\n */\nexport function useHeaderNavigation(stationCount: number): UseHeaderNavigationResult {\n const [activeIndex, setActiveIndex] = useState(0);\n const stationsRef = useRef<(HTMLElement | null)[]>([]);\n\n // Hiding a column can shrink the header past the active station. Clamping\n // during render rather than in an effect keeps the header from painting a\n // single frame with no tab stop at all.\n const active = Math.min(activeIndex, Math.max(0, stationCount - 1));\n if (active !== activeIndex) setActiveIndex(active);\n\n function focusStation(index: number) {\n const next = Math.max(0, Math.min(index, stationCount - 1));\n setActiveIndex(next);\n stationsRef.current[next]?.focus();\n }\n\n function getStationProps(index: number): StationProps {\n return {\n tabIndex: index === active ? 0 : -1,\n ref: (el: HTMLElement | null) => {\n stationsRef.current[index] = el;\n },\n // Also fires for a click, which keeps the tab stop where the user last\n // was rather than snapping back to the first column.\n onFocus: () => setActiveIndex(index),\n };\n }\n\n function handleNavigationKey(event: React.KeyboardEvent, index: number): boolean {\n // Alt is the resize modifier, so those combinations belong to the station.\n if (event.altKey || event.ctrlKey || event.metaKey) return false;\n switch (event.key) {\n case 'ArrowRight':\n focusStation(index + 1);\n break;\n case 'ArrowLeft':\n focusStation(index - 1);\n break;\n case 'Home':\n focusStation(0);\n break;\n case 'End':\n focusStation(stationCount - 1);\n break;\n default:\n return false;\n }\n event.preventDefault();\n event.stopPropagation();\n return true;\n }\n\n return { getStationProps, handleNavigationKey };\n}\n"],"mappings":"6CAuBA,SAAgB,EAAoB,EAAiD,CACnF,GAAM,CAAC,EAAa,GAAkB,EAAS,CAAC,EAC1C,EAAc,EAA+B,CAAC,CAAC,EAK/C,EAAS,KAAK,IAAI,EAAa,KAAK,IAAI,EAAG,EAAe,CAAC,CAAC,EAC9D,IAAW,GAAa,EAAe,CAAM,EAEjD,SAAS,EAAa,EAAe,CACnC,IAAM,EAAO,KAAK,IAAI,EAAG,KAAK,IAAI,EAAO,EAAe,CAAC,CAAC,EAC1D,EAAe,CAAI,EACnB,EAAY,QAAQ,EAAK,EAAE,MAAM,CACnC,CAEA,SAAS,EAAgB,EAA6B,CACpD,MAAO,CACL,SAAU,IAAU,EAAS,EAAI,GACjC,IAAM,GAA2B,CAC/B,EAAY,QAAQ,GAAS,CAC/B,EAGA,YAAe,EAAe,CAAK,CACrC,CACF,CAEA,SAAS,EAAoB,EAA4B,EAAwB,CAE/E,GAAI,EAAM,QAAU,EAAM,SAAW,EAAM,QAAS,MAAO,GAC3D,OAAQ,EAAM,IAAd,CACE,IAAK,aACH,EAAa,EAAQ,CAAC,EACtB,MACF,IAAK,YACH,EAAa,EAAQ,CAAC,EACtB,MACF,IAAK,OACH,EAAa,CAAC,EACd,MACF,IAAK,MACH,EAAa,EAAe,CAAC,EAC7B,MACF,QACE,MAAO,EACX,CAGA,OAFA,EAAM,eAAe,EACrB,EAAM,gBAAgB,EACf,EACT,CAEA,MAAO,CAAE,kBAAiB,qBAAoB,CAChD"}
@@ -0,0 +1,2 @@
1
+ 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};
2
+ //# sourceMappingURL=useRowNavigation.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useRowNavigation.mjs","names":[],"sources":["../../../../package/common/virtualList/useRowNavigation.ts"],"sourcesContent":["import React, { useRef, useState } from 'react';\n\nimport { findStartIndex, ROW_ENTER_SELECTOR } from './virtualList.utils';\nimport type { VirtualListScrollOptions } from './virtualList.types';\n\ninterface UseRowNavigationArgs {\n enabled: boolean;\n rowCount: number;\n /** Row tops, from the windowing engine, used for paging and visibility. */\n offsets: number[];\n rootRef: React.RefObject<HTMLDivElement | null>;\n headerRef: React.RefObject<HTMLDivElement | null>;\n scrollToIndex: (index: number, options?: VirtualListScrollOptions) => void;\n /** Row index the active row starts on, normally the selected one. */\n initialIndex: number;\n multiSelect: boolean;\n /** Undefined when the consumer gave the rows no activation of their own. */\n onActivate?: (index: number) => void;\n onToggleSelect: (index: number) => void;\n}\n\ninterface UseRowNavigationResult {\n /** Index of the row the arrow keys are on, or -1 when navigation is off. */\n activeIndex: number;\n /** Keeps the arrow keys where the pointer last was. */\n setActiveIndex: (index: number) => void;\n /**\n * The row currently being used, or -1. Its controls are in the tab order and\n * it stays mounted even when it scrolls out of the window.\n */\n enteredIndex: number;\n containerProps: {\n tabIndex?: number;\n onKeyDown?: (event: React.KeyboardEvent) => void;\n onFocus?: (event: React.FocusEvent) => void;\n onBlur?: (event: React.FocusEvent) => void;\n };\n}\n\n/**\n * Arrow-key navigation over the rows, with the tab stop on the list itself.\n *\n * The stop cannot sit on the active row, the way a roving tabindex normally\n * would: a virtualized row unmounts when it scrolls out of the window, and the\n * list would be left with no tab stop at all (the same reason `Tree` puts its\n * stop on its container). Instead the list keeps the focus and points at the\n * active row with `aria-activedescendant`.\n *\n * Keys are only handled when the list element itself has the focus. A cell may\n * hold an input, a dropdown or a button, and every one of them wants the arrows,\n * Enter and Space for itself. `F2` steps into such a row and `Escape` steps back\n * out, which is the one pair of keys that does work from inside a cell.\n */\nexport function useRowNavigation({\n enabled,\n rowCount,\n offsets,\n rootRef,\n headerRef,\n scrollToIndex,\n initialIndex,\n multiSelect,\n onActivate,\n onToggleSelect,\n}: UseRowNavigationArgs): UseRowNavigationResult {\n const [index, setIndex] = useState(initialIndex);\n const [enteredIndex, setEnteredIndex] = useState(-1);\n const offsetsRef = useRef(offsets);\n offsetsRef.current = offsets;\n\n // Rows come and go (a filter, a page, a reset), so the active index is clamped\n // during render rather than corrected in an effect.\n const activeIndex = enabled && rowCount > 0 ? Math.min(Math.max(index, 0), rowCount - 1) : -1;\n if (activeIndex >= 0 && activeIndex !== index) setIndex(activeIndex);\n\n /** Scrolls only when the row isn't already fully in view, so paging doesn't jump. */\n function revealRow(target: number) {\n const el = rootRef.current;\n if (!el) return;\n const headerHeight = headerRef.current?.offsetHeight ?? 0;\n const rowTop = offsetsRef.current[target] ?? 0;\n const rowBottom = offsetsRef.current[target + 1] ?? rowTop;\n if (rowTop < el.scrollTop + headerHeight) scrollToIndex(target, { align: 'start' });\n else if (rowBottom > el.scrollTop + el.clientHeight) scrollToIndex(target, { align: 'end' });\n }\n\n function moveTo(target: number) {\n const next = Math.max(0, Math.min(target, rowCount - 1));\n setIndex(next);\n revealRow(next);\n }\n\n /** How many rows fit in the viewport below the sticky header, at least one. */\n function pageSize(from: number): number {\n const el = rootRef.current;\n if (!el) return 1;\n const usable = Math.max(0, el.clientHeight - (headerRef.current?.offsetHeight ?? 0));\n const edge = findStartIndex(offsetsRef.current, (offsetsRef.current[from] ?? 0) + usable, rowCount);\n return Math.max(1, edge - from);\n }\n\n /** Moves the focus into the row's first control. False when it has none. */\n function enterRow(target: number): boolean {\n const row = rootRef.current?.querySelector(`[data-index=\"${target}\"]`);\n const focusable = row?.querySelector<HTMLElement>(ROW_ENTER_SELECTOR);\n if (!focusable) return false;\n // The row's controls are still out of the tab order at this point, which is\n // no obstacle to focusing one directly; the render that follows puts them\n // back in, so Tab then moves along that row.\n setEnteredIndex(target);\n focusable.focus();\n return true;\n }\n\n function rowIndexOf(node: EventTarget | null): number {\n const row = (node as HTMLElement | null)?.closest?.<HTMLElement>('[data-index]');\n const parsed = Number(row?.dataset.index);\n return Number.isFinite(parsed) ? parsed : -1;\n }\n\n /** A click or a Tab landing in a row counts as entering it, same as F2. */\n function handleFocus(event: React.FocusEvent) {\n if (event.target === event.currentTarget) return;\n const rowIndex = rowIndexOf(event.target);\n if (rowIndex < 0) return;\n setEnteredIndex(rowIndex);\n setIndex(rowIndex);\n }\n\n /** Leaving the entered row puts its controls back out of the tab order. */\n function handleBlur(event: React.FocusEvent) {\n if (rowIndexOf(event.relatedTarget) === enteredIndex) return;\n setEnteredIndex(-1);\n }\n\n /** Escape from inside a cell: hands the focus back to the list. */\n function handleExitKey(event: React.KeyboardEvent): void {\n // Something inside the row may own Escape, a dropdown closing for instance.\n if (event.key !== 'Escape' || event.defaultPrevented) return;\n const rowIndex = rowIndexOf(event.target);\n if (rowIndex >= 0) setIndex(rowIndex);\n setEnteredIndex(-1);\n rootRef.current?.focus();\n event.preventDefault();\n }\n\n function handleKeyDown(event: React.KeyboardEvent) {\n // Focus inside a cell means the row's own content is being used.\n if (event.target !== event.currentTarget) {\n handleExitKey(event);\n return;\n }\n switch (event.key) {\n case 'ArrowDown':\n moveTo(activeIndex + 1);\n break;\n case 'ArrowUp':\n moveTo(activeIndex - 1);\n break;\n case 'Home':\n moveTo(0);\n break;\n case 'End':\n moveTo(rowCount - 1);\n break;\n case 'PageDown':\n moveTo(activeIndex + pageSize(activeIndex));\n break;\n case 'PageUp':\n moveTo(activeIndex - pageSize(Math.max(0, activeIndex - 1)));\n break;\n case 'Enter':\n // With no activation of its own, a row's content is the only thing\n // Enter could mean, so it steps in rather than doing nothing.\n if (onActivate) onActivate(activeIndex);\n else if (!enterRow(activeIndex)) return;\n break;\n case 'F2':\n if (!enterRow(activeIndex)) return;\n break;\n case ' ':\n if (!multiSelect) return;\n onToggleSelect(activeIndex);\n break;\n default:\n return;\n }\n // Reached only by a handled key, so the page never scrolls under the list.\n event.preventDefault();\n }\n\n if (activeIndex < 0) return { activeIndex, enteredIndex: -1, setActiveIndex: setIndex, containerProps: {} };\n return {\n activeIndex,\n enteredIndex,\n setActiveIndex: setIndex,\n containerProps: { tabIndex: 0, onKeyDown: handleKeyDown, onFocus: handleFocus, onBlur: handleBlur },\n };\n}\n"],"mappings":"8HAqDA,SAAgB,EAAiB,CAC/B,UACA,WACA,UACA,UACA,YACA,gBACA,eACA,cACA,aACA,kBAC+C,CAC/C,GAAM,CAAC,EAAO,GAAY,EAAS,CAAY,EACzC,CAAC,EAAc,GAAmB,EAAS,EAAE,EAC7C,EAAa,EAAO,CAAO,EACjC,EAAW,QAAU,EAIrB,IAAM,EAAc,GAAW,EAAW,EAAI,KAAK,IAAI,KAAK,IAAI,EAAO,CAAC,EAAG,EAAW,CAAC,EAAI,GACvF,GAAe,GAAK,IAAgB,GAAO,EAAS,CAAW,EAGnE,SAAS,EAAU,EAAgB,CACjC,IAAM,EAAK,EAAQ,QACnB,GAAI,CAAC,EAAI,OACT,IAAM,EAAe,EAAU,SAAS,cAAgB,EAClD,EAAS,EAAW,QAAQ,IAAW,EACvC,EAAY,EAAW,QAAQ,EAAS,IAAM,EAChD,EAAS,EAAG,UAAY,EAAc,EAAc,EAAQ,CAAE,MAAO,OAAQ,CAAC,EACzE,EAAY,EAAG,UAAY,EAAG,cAAc,EAAc,EAAQ,CAAE,MAAO,KAAM,CAAC,CAC7F,CAEA,SAAS,EAAO,EAAgB,CAC9B,IAAM,EAAO,KAAK,IAAI,EAAG,KAAK,IAAI,EAAQ,EAAW,CAAC,CAAC,EACvD,EAAS,CAAI,EACb,EAAU,CAAI,CAChB,CAGA,SAAS,EAAS,EAAsB,CACtC,IAAM,EAAK,EAAQ,QACnB,GAAI,CAAC,EAAI,MAAO,GAChB,IAAM,EAAS,KAAK,IAAI,EAAG,EAAG,cAAgB,EAAU,SAAS,cAAgB,EAAE,EAC7E,EAAO,EAAe,EAAW,SAAU,EAAW,QAAQ,IAAS,GAAK,EAAQ,CAAQ,EAClG,OAAO,KAAK,IAAI,EAAG,EAAO,CAAI,CAChC,CAGA,SAAS,EAAS,EAAyB,CAEzC,IAAM,GADM,EAAQ,SAAS,cAAc,gBAAgB,EAAO,GAAG,EAAA,EAC9C,cAA2B,CAAkB,EAOpE,OANK,GAIL,EAAgB,CAAM,EACtB,EAAU,MAAM,EACT,IANgB,EAOzB,CAEA,SAAS,EAAW,EAAkC,CACpD,IAAM,EAAO,GAA6B,UAAuB,cAAc,EACzE,EAAS,OAAO,GAAK,QAAQ,KAAK,EACxC,OAAO,OAAO,SAAS,CAAM,EAAI,EAAS,EAC5C,CAGA,SAAS,EAAY,EAAyB,CAC5C,GAAI,EAAM,SAAW,EAAM,cAAe,OAC1C,IAAM,EAAW,EAAW,EAAM,MAAM,EACpC,EAAW,IACf,EAAgB,CAAQ,EACxB,EAAS,CAAQ,EACnB,CAGA,SAAS,EAAW,EAAyB,CACvC,EAAW,EAAM,aAAa,IAAM,GACxC,EAAgB,EAAE,CACpB,CAGA,SAAS,EAAc,EAAkC,CAEvD,GAAI,EAAM,MAAQ,UAAY,EAAM,iBAAkB,OACtD,IAAM,EAAW,EAAW,EAAM,MAAM,EACpC,GAAY,GAAG,EAAS,CAAQ,EACpC,EAAgB,EAAE,EAClB,EAAQ,SAAS,MAAM,EACvB,EAAM,eAAe,CACvB,CAEA,SAAS,EAAc,EAA4B,CAEjD,GAAI,EAAM,SAAW,EAAM,cAAe,CACxC,EAAc,CAAK,EACnB,MACF,CACA,OAAQ,EAAM,IAAd,CACE,IAAK,YACH,EAAO,EAAc,CAAC,EACtB,MACF,IAAK,UACH,EAAO,EAAc,CAAC,EACtB,MACF,IAAK,OACH,EAAO,CAAC,EACR,MACF,IAAK,MACH,EAAO,EAAW,CAAC,EACnB,MACF,IAAK,WACH,EAAO,EAAc,EAAS,CAAW,CAAC,EAC1C,MACF,IAAK,SACH,EAAO,EAAc,EAAS,KAAK,IAAI,EAAG,EAAc,CAAC,CAAC,CAAC,EAC3D,MACF,IAAK,QAGH,GAAI,EAAY,EAAW,CAAW,OACjC,GAAI,CAAC,EAAS,CAAW,EAAG,OACjC,MACF,IAAK,KACH,GAAI,CAAC,EAAS,CAAW,EAAG,OAC5B,MACF,IAAK,IACH,GAAI,CAAC,EAAa,OAClB,EAAe,CAAW,EAC1B,MACF,QACE,MACJ,CAEA,EAAM,eAAe,CACvB,CAGA,OADI,EAAc,EAAU,CAAE,cAAa,aAAc,GAAI,eAAgB,EAAU,eAAgB,CAAC,CAAE,EACnG,CACL,cACA,eACA,eAAgB,EAChB,eAAgB,CAAE,SAAU,EAAG,UAAW,EAAe,QAAS,EAAa,OAAQ,CAAW,CACpG,CACF"}
@@ -0,0 +1,2 @@
1
+ import{nextSort as e,sortItems as t}from"./virtualList.utils.mjs";import{useMemo as n,useState as r}from"react";function i({items:i,columns:a,sort:o,defaultSort:s=null,onSortChange:c}){let l=o!==void 0,[u,d]=r(s),f=l?o:u,p=n(()=>t(i,f,a),[i,f,a]);function m(t){let n=e(f,t);l||d(n),c?.(n)}return{rows:p,effectiveSort:f,isSorted:p!==i,handleHeaderSort:m}}export{i as useSorting};
2
+ //# sourceMappingURL=useSorting.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSorting.mjs","names":[],"sources":["../../../../package/common/virtualList/useSorting.ts"],"sourcesContent":["import { useMemo, useState } from 'react';\n\nimport { nextSort, sortItems } from './virtualList.utils';\nimport type { VirtualListColumn, VirtualListSort } from './virtualList.types';\n\ninterface UseSortingArgs<T> {\n items: T[];\n columns?: VirtualListColumn<T>[];\n /** Controlled sort. When provided, internal state is ignored. */\n sort?: VirtualListSort | null;\n defaultSort?: VirtualListSort | null;\n onSortChange?: (sort: VirtualListSort | null) => void;\n}\n\ninterface UseSortingResult<T> {\n /** `items`, reordered when the sorted column carries a comparator. */\n rows: T[];\n effectiveSort: VirtualListSort | null;\n /** True only when this hook reordered the rows, not when it merely reported. */\n isSorted: boolean;\n handleHeaderSort: (key: string) => void;\n}\n\n/**\n * Header sorting, controlled or uncontrolled. Whether the list reorders the data\n * or only reports the click is the column's decision, not a prop: a column with\n * `sortValue`/`compare` is sorted here, a bare `sortable` one is left alone so a\n * server-side sort can own it. Sorting a paginated list here would silently\n * order only the rows loaded so far.\n */\nexport function useSorting<T>({\n items,\n columns,\n sort,\n defaultSort = null,\n onSortChange,\n}: UseSortingArgs<T>): UseSortingResult<T> {\n const isControlled = sort !== undefined;\n const [internalSort, setInternalSort] = useState<VirtualListSort | null>(defaultSort);\n const effectiveSort = isControlled ? sort! : internalSort;\n\n // Returns `items` itself when nothing applies, which is what `isSorted` reads.\n const rows = useMemo(() => sortItems(items, effectiveSort, columns), [items, effectiveSort, columns]);\n\n function handleHeaderSort(key: string) {\n const next = nextSort(effectiveSort, key);\n if (!isControlled) setInternalSort(next);\n onSortChange?.(next);\n }\n\n return { rows, effectiveSort, isSorted: rows !== items, handleHeaderSort };\n}\n"],"mappings":"gHA8BA,SAAgB,EAAc,CAC5B,QACA,UACA,OACA,cAAc,KACd,gBACyC,CACzC,IAAM,EAAe,IAAS,IAAA,GACxB,CAAC,EAAc,GAAmB,EAAiC,CAAW,EAC9E,EAAgB,EAAe,EAAQ,EAGvC,EAAO,MAAc,EAAU,EAAO,EAAe,CAAO,EAAG,CAAC,EAAO,EAAe,CAAO,CAAC,EAEpG,SAAS,EAAiB,EAAa,CACrC,IAAM,EAAO,EAAS,EAAe,CAAG,EACnC,GAAc,EAAgB,CAAI,EACvC,IAAe,CAAI,CACrB,CAEA,MAAO,CAAE,OAAM,gBAAe,SAAU,IAAS,EAAO,kBAAiB,CAC3E"}
@@ -1,2 +1,2 @@
1
- import{buildOffsets as e,findStartIndex as t,getVisibleRange as n}from"./virtualList.utils.mjs";import{useCallback as r,useEffect as i,useLayoutEffect as a,useMemo as o,useRef as s,useState as c}from"react";function l(e,t){if(typeof e==`number`)return e;let n=e.trim();if(n===``)return 0;let r=Number(n);if(!Number.isNaN(r))return r;let i=/^(-?\d*\.?\d+)px$/.exec(n);if(i)return parseFloat(i[1]);if(typeof document>`u`||!t)return 0;let a=document.createElement(`div`);a.style.cssText=`position:absolute;visibility:hidden;height:${n};`,t.appendChild(a);let o=a.getBoundingClientRect().height;return t.removeChild(a),Number.isFinite(o)?o:0}function u({items:u,keys:d,getItemKey:f,estimatedRowHeight:p,rowGap:m,overscan:h,onLoadMore:g,loadMoreThreshold:_,isLoading:v}){let[y,b]=c(0),[x,S]=c(0),[C,w]=c(0),[T,E]=c(()=>l(m,null)),[D,O]=c(new Map),[k,A]=c(!1),j=s(!1),M=s(null),N=s(null),P=s([]),F=s(null),I=s(0),L=s([]),[R]=c(()=>typeof ResizeObserver>`u`?null:new ResizeObserver(e=>{O(t=>{let n=null;for(let r of e){let e=r.target,i=Number(e.dataset.index);if(Number.isNaN(i))continue;let a=L.current[i];if(a===void 0)continue;let o=r.borderBoxSize?.[0],s=o?Math.round(o.blockSize):e.offsetHeight;t.get(a)!==s&&(n||=new Map(t),n.set(a,s))}return n??t})}));i(()=>()=>R?.disconnect(),[R]),a(()=>{let e=M.current;if(!e)return;S(e.clientHeight),w(e.clientWidth);let t=new ResizeObserver(()=>{S(e.clientHeight),w(e.clientWidth)});return t.observe(e),()=>t.disconnect()},[]);let z=s(g);a(()=>{z.current=g});let B=r(()=>{let e=z.current;e&&!j.current&&(j.current=!0,A(!0),e().finally(()=>{j.current=!1,A(!1)}))},[]),V=r(e=>{let t=e.currentTarget;I.current=t.scrollTop,F.current??=requestAnimationFrame(()=>{F.current=null,b(I.current)}),t.scrollTop+t.clientHeight>=t.scrollHeight-_&&B()},[_,B]);i(()=>()=>{F.current!=null&&cancelAnimationFrame(F.current)},[]),a(()=>{L.current=d}),a(()=>{let e=l(m,M.current);E(t=>t===e?t:e)},[m,C,x]);let{offsets:H,totalHeight:U}=o(()=>e(d,D,p,T),[d,D,p,T]),W=s(``);i(()=>{if(!g||v||k||x===0)return;let e=N.current?.offsetHeight??0;if(U>=x-e)return;let t=`${u.length}:${x}`;W.current!==t&&(W.current=t,B())},[g,v,k,u.length,U,x,B]),a(()=>{let e=M.current,n=P.current;if(P.current=H,!e||n.length===0)return;let r=e.scrollTop;if(r<=0)return;let i=n.length-1,a=t(n,r,i);if(a>=u.length)return;let o=r-n[a],s=H[a]+o;Math.abs(s-r)>.5&&(e.scrollTop=s,I.current=s,b(s))},[H,u.length]);let G=o(()=>{function e(e,t){let n=M.current;if(!n)return;let r=Math.max(0,e);typeof n.scrollTo==`function`?n.scrollTo({top:r,behavior:t??`auto`}):n.scrollTop=r}function t(t,n){let r=M.current;if(!r||u.length===0)return;let i=Math.max(0,Math.min(t,u.length-1)),a=H[i]??0,o=(H[i+1]??a)-a,s=N.current?.offsetHeight??0,c=Math.max(0,r.clientHeight-s),l=n?.align??`start`,d=a-s;l===`center`?d-=(c-o)/2:l===`end`&&(d-=c-o),e(d,n?.behavior)}return{scrollToIndex:t,scrollToKey(e,n){let r=u.findIndex((t,n)=>(f?f(t,n):n)===e);r>=0&&t(r,n)},scrollToTop(t){e(0,t?.behavior)},getScrollElement:()=>M.current}},[u,H,f]),{startIndex:K,endIndex:q}=o(()=>n(H,y,x,h,u.length),[H,y,x,h,u.length]);return{rootRef:M,headerRef:N,rowObserver:R,keysRef:L,offsets:H,totalHeight:U,containerHeight:x,containerWidth:C,startIndex:K,endIndex:q,handleScroll:V,showLoadMoreSpinner:k,scrollApi:G}}export{u as useVirtualWindow};
1
+ import{buildOffsets as e,findStartIndex as t,getVisibleRange as n}from"./virtualList.utils.mjs";import{useCallback as r,useEffect as i,useLayoutEffect as a,useMemo as o,useRef as s,useState as c}from"react";function l(e,t){if(typeof e==`number`)return e;let n=e.trim();if(n===``)return 0;let r=Number(n);if(!Number.isNaN(r))return r;let i=/^(-?\d*\.?\d+)px$/.exec(n);if(i)return parseFloat(i[1]);if(typeof document>`u`||!t)return 0;let a=document.createElement(`div`);a.style.cssText=`position:absolute;visibility:hidden;height:${n};`,t.appendChild(a);let o=a.getBoundingClientRect().height;return t.removeChild(a),Number.isFinite(o)?o:0}var u=20;function d({items:d,keys:f,getItemKey:p,estimatedRowHeight:m,rowGap:h,overscan:g,onLoadMore:_,loadMoreThreshold:v,isLoading:y}){let[b,x]=c(0),[S,C]=c(0),[w,T]=c(0),[E,D]=c(()=>l(h,null)),[O,k]=c(new Map),[A,j]=c(!1),M=s(!1),N=s(null),P=s(null),F=s([]),I=s(null),L=s(0),R=s([]),[z]=c(()=>typeof ResizeObserver>`u`?null:new ResizeObserver(e=>{k(t=>{let n=null;for(let r of e){let e=r.target,i=Number(e.dataset.index);if(Number.isNaN(i))continue;let a=R.current[i];if(a===void 0)continue;let o=r.borderBoxSize?.[0],s=o?Math.round(o.blockSize):e.offsetHeight;t.get(a)!==s&&(n||=new Map(t),n.set(a,s))}return n??t})}));i(()=>()=>z?.disconnect(),[z]),a(()=>{let e=N.current;if(!e)return;C(e.clientHeight),T(e.clientWidth);let t=new ResizeObserver(()=>{C(e.clientHeight),T(e.clientWidth)});return t.observe(e),()=>t.disconnect()},[]);let B=s(_);a(()=>{B.current=_});let V=r(()=>{let e=B.current;e&&!M.current&&(M.current=!0,j(!0),e().finally(()=>{M.current=!1,j(!1)}))},[]),H=r(e=>{let t=e.currentTarget;L.current=t.scrollTop,I.current??=requestAnimationFrame(()=>{I.current=null,x(L.current)}),t.scrollTop+t.clientHeight>=t.scrollHeight-v&&V()},[v,V]);i(()=>()=>{I.current!=null&&cancelAnimationFrame(I.current)},[]),a(()=>{R.current=f}),a(()=>{let e=l(h,N.current);D(t=>t===e?t:e)},[h,w,S]);let U=o(()=>{if(O.size===0)return m;let e=0;for(let t of O.values())e+=t;return e/O.size},[O,m]),{offsets:W,totalHeight:G}=o(()=>e(f,O,U,E),[f,O,U,E]),K=s(``);i(()=>{if(!_||y||A||S===0)return;let e=P.current?.offsetHeight??0;if(G>=S-e)return;let t=`${d.length}:${S}`;K.current!==t&&(K.current=t,V())},[_,y,A,d.length,G,S,V]),a(()=>{let e=N.current,n=F.current;if(F.current=W,!e||n.length===0||Y.current)return;let r=e.scrollTop;if(r<=0)return;let i=n.length-1,a=t(n,r,i);if(a>=d.length)return;let o=r-n[a],s=W[a]+o;Math.abs(s-r)>.5&&(e.scrollTop=s,L.current=s,x(s))},[W,d.length]);let q=r((e,t)=>{let n=N.current;if(!n)return;let r=Math.max(0,e);typeof n.scrollTo==`function`?n.scrollTo({top:r,behavior:t??`auto`}):n.scrollTop=r},[]),J=r((e,t)=>{let n=N.current;if(!n||d.length===0)return null;let r=Math.max(0,Math.min(e,d.length-1)),i=W[r]??0,a=(W[r+1]??i)-i,o=P.current?.offsetHeight??0,s=Math.max(0,n.clientHeight-o),c=t?.align??`start`,l=i;return c===`center`?l-=(s-a)/2:c===`end`&&(l-=s-a),l},[d.length,W]),Y=s(null);a(()=>{let e=Y.current,t=N.current;if(!e)return;let n=e.items===d&&t?J(e.index,e.options):null;if(n==null){Y.current=null;return}let r=Math.abs(n-e.lastTarget)<=1;if(r&&Math.abs(t.scrollTop-n)<=1||e.attempts>=u){Y.current=null;return}r||(e.lastTarget=n,e.attempts+=1,q(n,e.options?.behavior))}),i(()=>{let e=N.current;if(!e)return;let t=()=>{Y.current=null};return e.addEventListener(`wheel`,t,{passive:!0}),e.addEventListener(`touchstart`,t,{passive:!0}),e.addEventListener(`keydown`,t),()=>{e.removeEventListener(`wheel`,t),e.removeEventListener(`touchstart`,t),e.removeEventListener(`keydown`,t)}},[]);let X=o(()=>{function e(e,t){let n=J(e,t);n!=null&&(Y.current={index:e,options:t,lastTarget:n,attempts:0,items:d},q(n,t?.behavior))}return{scrollToIndex:e,scrollToKey(t,n){let r=d.findIndex((e,n)=>(p?p(e,n):n)===t);r>=0&&e(r,n)},scrollToTop(e){Y.current=null,q(0,e?.behavior)},getScrollElement:()=>N.current}},[d,p,J,q]),{startIndex:Z,endIndex:Q}=o(()=>n(W,b,S,g,d.length),[W,b,S,g,d.length]);return{rootRef:N,headerRef:P,rowObserver:z,keysRef:R,offsets:W,totalHeight:G,containerHeight:S,containerWidth:w,startIndex:Z,endIndex:Q,handleScroll:H,showLoadMoreSpinner:A,scrollApi:X}}export{d as useVirtualWindow};
2
2
  //# sourceMappingURL=useVirtualWindow.mjs.map