ts-video-player 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +438 -0
  3. package/dist/cdn.d.ts +10 -0
  4. package/dist/cdn.d.ts.map +1 -0
  5. package/dist/cdn.min.js +1046 -0
  6. package/dist/cdn.min.js.map +54 -0
  7. package/dist/cdn.test.d.ts +2 -0
  8. package/dist/cdn.test.d.ts.map +1 -0
  9. package/dist/chunk-36x4yhjn.js +765 -0
  10. package/dist/chunk-36x4yhjn.js.map +12 -0
  11. package/dist/chunk-eajts2jv.js +4243 -0
  12. package/dist/chunk-eajts2jv.js.map +42 -0
  13. package/dist/chunk-z1ehbz9f.js +380 -0
  14. package/dist/chunk-z1ehbz9f.js.map +19 -0
  15. package/dist/config.d.ts +9 -0
  16. package/dist/config.d.ts.map +1 -0
  17. package/dist/core/audio-gain.d.ts +67 -0
  18. package/dist/core/audio-gain.d.ts.map +1 -0
  19. package/dist/core/caption-styles.d.ts +121 -0
  20. package/dist/core/caption-styles.d.ts.map +1 -0
  21. package/dist/core/chapters.d.ts +150 -0
  22. package/dist/core/chapters.d.ts.map +1 -0
  23. package/dist/core/error-overlay.d.ts +71 -0
  24. package/dist/core/error-overlay.d.ts.map +1 -0
  25. package/dist/core/events.d.ts +119 -0
  26. package/dist/core/events.d.ts.map +1 -0
  27. package/dist/core/features.d.ts +64 -0
  28. package/dist/core/features.d.ts.map +1 -0
  29. package/dist/core/i18n.d.ts +132 -0
  30. package/dist/core/i18n.d.ts.map +1 -0
  31. package/dist/core/index.d.ts +37 -0
  32. package/dist/core/index.d.ts.map +1 -0
  33. package/dist/core/live.d.ts +130 -0
  34. package/dist/core/live.d.ts.map +1 -0
  35. package/dist/core/media-session.d.ts +38 -0
  36. package/dist/core/media-session.d.ts.map +1 -0
  37. package/dist/core/orientation.d.ts +22 -0
  38. package/dist/core/orientation.d.ts.map +1 -0
  39. package/dist/core/popover.d.ts +42 -0
  40. package/dist/core/popover.d.ts.map +1 -0
  41. package/dist/core/remote-playback.d.ts +195 -0
  42. package/dist/core/remote-playback.d.ts.map +1 -0
  43. package/dist/core/settings-menu.d.ts +132 -0
  44. package/dist/core/settings-menu.d.ts.map +1 -0
  45. package/dist/core/spinner.d.ts +67 -0
  46. package/dist/core/spinner.d.ts.map +1 -0
  47. package/dist/core/state.d.ts +142 -0
  48. package/dist/core/state.d.ts.map +1 -0
  49. package/dist/core/text-tracks.d.ts +162 -0
  50. package/dist/core/text-tracks.d.ts.map +1 -0
  51. package/dist/core/themes.d.ts +213 -0
  52. package/dist/core/themes.d.ts.map +1 -0
  53. package/dist/core/thumbnails.d.ts +124 -0
  54. package/dist/core/thumbnails.d.ts.map +1 -0
  55. package/dist/core/tooltips.d.ts +100 -0
  56. package/dist/core/tooltips.d.ts.map +1 -0
  57. package/dist/elements/base.d.ts +3 -0
  58. package/dist/elements/base.d.ts.map +1 -0
  59. package/dist/elements/index.d.ts +33 -0
  60. package/dist/elements/index.d.ts.map +1 -0
  61. package/dist/elements/index.js +55 -0
  62. package/dist/elements/index.js.map +9 -0
  63. package/dist/elements/media-airplay-button.d.ts +13 -0
  64. package/dist/elements/media-airplay-button.d.ts.map +1 -0
  65. package/dist/elements/media-error-display.d.ts +13 -0
  66. package/dist/elements/media-error-display.d.ts.map +1 -0
  67. package/dist/elements/media-fullscreen-button.d.ts +13 -0
  68. package/dist/elements/media-fullscreen-button.d.ts.map +1 -0
  69. package/dist/elements/media-live-button.d.ts +13 -0
  70. package/dist/elements/media-live-button.d.ts.map +1 -0
  71. package/dist/elements/media-mute-button.d.ts +13 -0
  72. package/dist/elements/media-mute-button.d.ts.map +1 -0
  73. package/dist/elements/media-pip-button.d.ts +13 -0
  74. package/dist/elements/media-pip-button.d.ts.map +1 -0
  75. package/dist/elements/media-play-button.d.ts +14 -0
  76. package/dist/elements/media-play-button.d.ts.map +1 -0
  77. package/dist/elements/media-playback-rate-button.d.ts +10 -0
  78. package/dist/elements/media-playback-rate-button.d.ts.map +1 -0
  79. package/dist/elements/media-preview-time.d.ts +18 -0
  80. package/dist/elements/media-preview-time.d.ts.map +1 -0
  81. package/dist/elements/media-progress-bar.d.ts +16 -0
  82. package/dist/elements/media-progress-bar.d.ts.map +1 -0
  83. package/dist/elements/media-remote-playback-button.d.ts +16 -0
  84. package/dist/elements/media-remote-playback-button.d.ts.map +1 -0
  85. package/dist/elements/media-seek-button.d.ts +9 -0
  86. package/dist/elements/media-seek-button.d.ts.map +1 -0
  87. package/dist/elements/media-settings-menu.d.ts +32 -0
  88. package/dist/elements/media-settings-menu.d.ts.map +1 -0
  89. package/dist/elements/media-time-display.d.ts +21 -0
  90. package/dist/elements/media-time-display.d.ts.map +1 -0
  91. package/dist/elements/media-time-group.d.ts +22 -0
  92. package/dist/elements/media-time-group.d.ts.map +1 -0
  93. package/dist/elements/media-time-separator.d.ts +13 -0
  94. package/dist/elements/media-time-separator.d.ts.map +1 -0
  95. package/dist/elements/media-volume-slider.d.ts +16 -0
  96. package/dist/elements/media-volume-slider.d.ts.map +1 -0
  97. package/dist/elements/utils.d.ts +27 -0
  98. package/dist/elements/utils.d.ts.map +1 -0
  99. package/dist/elements/video-player.d.ts +23 -0
  100. package/dist/elements/video-player.d.ts.map +1 -0
  101. package/dist/elements/video-skin.d.ts +23 -0
  102. package/dist/elements/video-skin.d.ts.map +1 -0
  103. package/dist/features/captions.d.ts +8 -0
  104. package/dist/features/captions.d.ts.map +1 -0
  105. package/dist/features/create-player.d.ts +45 -0
  106. package/dist/features/create-player.d.ts.map +1 -0
  107. package/dist/features/fullscreen.d.ts +8 -0
  108. package/dist/features/fullscreen.d.ts.map +1 -0
  109. package/dist/features/gestures.d.ts +8 -0
  110. package/dist/features/gestures.d.ts.map +1 -0
  111. package/dist/features/index.d.ts +26 -0
  112. package/dist/features/index.d.ts.map +1 -0
  113. package/dist/features/index.js +31 -0
  114. package/dist/features/index.js.map +9 -0
  115. package/dist/features/keyboard.d.ts +8 -0
  116. package/dist/features/keyboard.d.ts.map +1 -0
  117. package/dist/features/pip.d.ts +8 -0
  118. package/dist/features/pip.d.ts.map +1 -0
  119. package/dist/features/playback.d.ts +8 -0
  120. package/dist/features/playback.d.ts.map +1 -0
  121. package/dist/features/quality.d.ts +8 -0
  122. package/dist/features/quality.d.ts.map +1 -0
  123. package/dist/features/types.d.ts +26 -0
  124. package/dist/features/types.d.ts.map +1 -0
  125. package/dist/features/volume.d.ts +8 -0
  126. package/dist/features/volume.d.ts.map +1 -0
  127. package/dist/index.d.ts +84 -0
  128. package/dist/index.d.ts.map +1 -0
  129. package/dist/index.js +8149 -0
  130. package/dist/index.js.map +31 -0
  131. package/dist/layouts/index.d.ts +58 -0
  132. package/dist/layouts/index.d.ts.map +1 -0
  133. package/dist/player.d.ts +90 -0
  134. package/dist/player.d.ts.map +1 -0
  135. package/dist/plugins/ads.d.ts +311 -0
  136. package/dist/plugins/ads.d.ts.map +1 -0
  137. package/dist/plugins/analytics.d.ts +203 -0
  138. package/dist/plugins/analytics.d.ts.map +1 -0
  139. package/dist/plugins/end-screen.d.ts +184 -0
  140. package/dist/plugins/end-screen.d.ts.map +1 -0
  141. package/dist/plugins/index.d.ts +13 -0
  142. package/dist/plugins/index.d.ts.map +1 -0
  143. package/dist/plugins/index.js +20 -0
  144. package/dist/plugins/index.js.map +9 -0
  145. package/dist/plugins/skip-segments.d.ts +159 -0
  146. package/dist/plugins/skip-segments.d.ts.map +1 -0
  147. package/dist/plugins/watermark.d.ts +157 -0
  148. package/dist/plugins/watermark.d.ts.map +1 -0
  149. package/dist/providers/base.d.ts +56 -0
  150. package/dist/providers/base.d.ts.map +1 -0
  151. package/dist/providers/dash.d.ts +94 -0
  152. package/dist/providers/dash.d.ts.map +1 -0
  153. package/dist/providers/hls.d.ts +182 -0
  154. package/dist/providers/hls.d.ts.map +1 -0
  155. package/dist/providers/html5.d.ts +64 -0
  156. package/dist/providers/html5.d.ts.map +1 -0
  157. package/dist/providers/index.d.ts +41 -0
  158. package/dist/providers/index.d.ts.map +1 -0
  159. package/dist/providers/vimeo.d.ts +157 -0
  160. package/dist/providers/vimeo.d.ts.map +1 -0
  161. package/dist/providers/youtube.d.ts +134 -0
  162. package/dist/providers/youtube.d.ts.map +1 -0
  163. package/dist/stx/index.d.ts +59 -0
  164. package/dist/stx/index.d.ts.map +1 -0
  165. package/dist/stx/index.js +480 -0
  166. package/dist/stx/index.js.map +11 -0
  167. package/dist/types.d.ts +724 -0
  168. package/dist/types.d.ts.map +1 -0
  169. package/dist/ui/components.d.ts +121 -0
  170. package/dist/ui/components.d.ts.map +1 -0
  171. package/dist/ui/index.d.ts +9 -0
  172. package/dist/ui/index.d.ts.map +1 -0
  173. package/dist/ui/index.js +16 -0
  174. package/dist/ui/index.js.map +9 -0
  175. package/package.json +109 -0
  176. package/src/cdn.test.ts +10 -0
  177. package/src/cdn.ts +60 -0
  178. package/src/config.ts +25 -0
  179. package/src/core/audio-gain.ts +171 -0
  180. package/src/core/caption-styles.ts +568 -0
  181. package/src/core/chapters.ts +551 -0
  182. package/src/core/error-overlay.ts +354 -0
  183. package/src/core/events.ts +552 -0
  184. package/src/core/features.ts +287 -0
  185. package/src/core/i18n.ts +590 -0
  186. package/src/core/index.ts +249 -0
  187. package/src/core/live.ts +472 -0
  188. package/src/core/media-session.ts +137 -0
  189. package/src/core/orientation.ts +57 -0
  190. package/src/core/popover.ts +143 -0
  191. package/src/core/remote-playback.ts +661 -0
  192. package/src/core/settings-menu.ts +630 -0
  193. package/src/core/spinner.ts +310 -0
  194. package/src/core/state.ts +476 -0
  195. package/src/core/text-tracks.ts +599 -0
  196. package/src/core/themes.ts +607 -0
  197. package/src/core/thumbnails.ts +457 -0
  198. package/src/core/tooltips.ts +370 -0
  199. package/src/elements/base.ts +2 -0
  200. package/src/elements/index.ts +82 -0
  201. package/src/elements/media-airplay-button.ts +78 -0
  202. package/src/elements/media-error-display.ts +47 -0
  203. package/src/elements/media-fullscreen-button.ts +72 -0
  204. package/src/elements/media-live-button.ts +65 -0
  205. package/src/elements/media-mute-button.ts +69 -0
  206. package/src/elements/media-pip-button.ts +66 -0
  207. package/src/elements/media-play-button.ts +69 -0
  208. package/src/elements/media-playback-rate-button.ts +59 -0
  209. package/src/elements/media-preview-time.ts +75 -0
  210. package/src/elements/media-progress-bar.ts +200 -0
  211. package/src/elements/media-remote-playback-button.ts +108 -0
  212. package/src/elements/media-seek-button.ts +50 -0
  213. package/src/elements/media-settings-menu.ts +302 -0
  214. package/src/elements/media-time-display.ts +99 -0
  215. package/src/elements/media-time-group.ts +41 -0
  216. package/src/elements/media-time-separator.ts +35 -0
  217. package/src/elements/media-volume-slider.ts +140 -0
  218. package/src/elements/utils.ts +82 -0
  219. package/src/elements/video-player.ts +175 -0
  220. package/src/elements/video-skin.ts +127 -0
  221. package/src/features/captions.ts +56 -0
  222. package/src/features/create-player.ts +91 -0
  223. package/src/features/fullscreen.ts +44 -0
  224. package/src/features/gestures.ts +44 -0
  225. package/src/features/index.ts +44 -0
  226. package/src/features/keyboard.ts +89 -0
  227. package/src/features/pip.ts +47 -0
  228. package/src/features/playback.ts +68 -0
  229. package/src/features/quality.ts +19 -0
  230. package/src/features/types.ts +28 -0
  231. package/src/features/volume.ts +46 -0
  232. package/src/index.ts +635 -0
  233. package/src/layouts/index.ts +672 -0
  234. package/src/player.ts +821 -0
  235. package/src/plugins/ads.ts +1045 -0
  236. package/src/plugins/analytics.ts +673 -0
  237. package/src/plugins/end-screen.ts +793 -0
  238. package/src/plugins/index.ts +61 -0
  239. package/src/plugins/skip-segments.ts +537 -0
  240. package/src/plugins/watermark.ts +537 -0
  241. package/src/providers/base.ts +197 -0
  242. package/src/providers/dash.ts +588 -0
  243. package/src/providers/hls.ts +624 -0
  244. package/src/providers/html5.ts +473 -0
  245. package/src/providers/index.ts +97 -0
  246. package/src/providers/vimeo.ts +533 -0
  247. package/src/providers/youtube.ts +542 -0
  248. package/src/stx/index.ts +606 -0
  249. package/src/types.ts +874 -0
  250. package/src/ui/components.ts +910 -0
  251. package/src/ui/index.ts +29 -0
@@ -0,0 +1,137 @@
1
+ /**
2
+ * ts-video-player Media Session API Integration
3
+ *
4
+ * Provides lock screen / notification controls via navigator.mediaSession.
5
+ *
6
+ * @module core/media-session
7
+ */
8
+
9
+ import type { StateStore } from './state'
10
+
11
+ export interface MediaSessionOptions {
12
+ /** Title to display */
13
+ title?: string
14
+ /** Artist name */
15
+ artist?: string
16
+ /** Album name */
17
+ album?: string
18
+ /** Artwork images for lock screen */
19
+ artwork?: MediaImage[]
20
+ }
21
+
22
+ /**
23
+ * Check if Media Session API is available
24
+ */
25
+ export function isMediaSessionSupported(): boolean {
26
+ return typeof navigator !== 'undefined' && 'mediaSession' in navigator
27
+ }
28
+
29
+ /**
30
+ * Create a Media Session controller that syncs player state to the browser's
31
+ * media session (lock screen controls, notification area, etc.)
32
+ */
33
+ export function createMediaSession(
34
+ store: StateStore,
35
+ actions: {
36
+ play: () => void
37
+ pause: () => void
38
+ seekTo: (time: number) => void
39
+ seekBy: (offset: number) => void
40
+ stop: () => void
41
+ },
42
+ options?: MediaSessionOptions,
43
+ ): () => void {
44
+ if (!isMediaSessionSupported()) return () => {}
45
+
46
+ const session = navigator.mediaSession
47
+
48
+ // Set metadata
49
+ if (options) {
50
+ updateMetadata(options)
51
+ }
52
+
53
+ // Action handlers
54
+ const handlers: Array<[MediaSessionAction, MediaSessionActionHandler]> = [
55
+ ['play', () => actions.play()],
56
+ ['pause', () => actions.pause()],
57
+ ['stop', () => actions.stop()],
58
+ ['seekbackward', (details) => {
59
+ const offset = (details as MediaSessionActionDetails & { seekOffset?: number }).seekOffset || -10
60
+ actions.seekBy(offset)
61
+ }],
62
+ ['seekforward', (details) => {
63
+ const offset = (details as MediaSessionActionDetails & { seekOffset?: number }).seekOffset || 10
64
+ actions.seekBy(offset)
65
+ }],
66
+ ['seekto', (details) => {
67
+ const seekTime = (details as MediaSessionActionDetails & { seekTime?: number }).seekTime
68
+ if (seekTime !== undefined) actions.seekTo(seekTime)
69
+ }],
70
+ ]
71
+
72
+ for (const [action, handler] of handlers) {
73
+ try {
74
+ session.setActionHandler(action, handler)
75
+ } catch {
76
+ // Some actions may not be supported on all platforms
77
+ }
78
+ }
79
+
80
+ // Sync playback state
81
+ const unsubPaused = store.subscribe('paused', (state) => {
82
+ session.playbackState = state.paused ? 'paused' : 'playing'
83
+ })
84
+
85
+ // Sync position state
86
+ let positionTimer: ReturnType<typeof setInterval> | null = null
87
+
88
+ const updatePosition = () => {
89
+ const state = store.getState()
90
+ if (!isFinite(state.duration) || state.duration <= 0) return
91
+ try {
92
+ session.setPositionState({
93
+ duration: state.duration,
94
+ playbackRate: state.playbackRate,
95
+ position: Math.min(state.currentTime, state.duration),
96
+ })
97
+ } catch {
98
+ // Position state may fail if values are invalid
99
+ }
100
+ }
101
+
102
+ const unsubPlaying = store.subscribe('playing', (state) => {
103
+ if (state.playing) {
104
+ updatePosition()
105
+ if (!positionTimer) {
106
+ positionTimer = setInterval(updatePosition, 1000)
107
+ }
108
+ } else {
109
+ if (positionTimer) { clearInterval(positionTimer); positionTimer = null }
110
+ updatePosition()
111
+ }
112
+ })
113
+
114
+ return () => {
115
+ if (positionTimer) clearInterval(positionTimer)
116
+ unsubPaused()
117
+ unsubPlaying()
118
+
119
+ // Clear action handlers
120
+ for (const [action] of handlers) {
121
+ try { session.setActionHandler(action, null) } catch { /* ignore */ }
122
+ }
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Update media session metadata
128
+ */
129
+ export function updateMetadata(options: MediaSessionOptions): void {
130
+ if (!isMediaSessionSupported()) return
131
+ navigator.mediaSession.metadata = new MediaMetadata({
132
+ title: options.title || '',
133
+ artist: options.artist || '',
134
+ album: options.album || '',
135
+ artwork: options.artwork || [],
136
+ })
137
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * ts-video-player Orientation Lock
3
+ *
4
+ * Auto-lock screen orientation to landscape when entering fullscreen on mobile.
5
+ *
6
+ * @module core/orientation
7
+ */
8
+
9
+ type OrientationType = 'landscape' | 'portrait' | 'any'
10
+
11
+ interface ScreenOrientationAPI {
12
+ lock(orientation: string): Promise<void>
13
+ unlock(): void
14
+ }
15
+
16
+ /**
17
+ * Check if Screen Orientation API is available
18
+ */
19
+ export function isOrientationLockSupported(): boolean {
20
+ return (
21
+ typeof screen !== 'undefined' &&
22
+ 'orientation' in screen &&
23
+ typeof (screen.orientation as unknown as ScreenOrientationAPI).lock === 'function'
24
+ )
25
+ }
26
+
27
+ /**
28
+ * Lock the screen orientation (best-effort, silently fails if unsupported)
29
+ */
30
+ export function lockOrientation(type: OrientationType): void {
31
+ if (!isOrientationLockSupported()) return
32
+
33
+ const orientationMap: Record<OrientationType, string> = {
34
+ landscape: 'landscape',
35
+ portrait: 'portrait',
36
+ any: 'any',
37
+ }
38
+
39
+ ;(screen.orientation as unknown as ScreenOrientationAPI)
40
+ .lock(orientationMap[type])
41
+ .catch(() => {
42
+ // Orientation lock may fail if not in fullscreen or unsupported
43
+ })
44
+ }
45
+
46
+ /**
47
+ * Unlock the screen orientation
48
+ */
49
+ export function unlockOrientation(): void {
50
+ if (!isOrientationLockSupported()) return
51
+
52
+ try {
53
+ ;(screen.orientation as unknown as ScreenOrientationAPI).unlock()
54
+ } catch {
55
+ // Unlock may fail if lock was never acquired
56
+ }
57
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * ts-video-player Popover Positioning
3
+ *
4
+ * Collision-aware positioning for menus and tooltips.
5
+ *
6
+ * @module core/popover
7
+ */
8
+
9
+ export interface Rect {
10
+ top: number
11
+ left: number
12
+ bottom: number
13
+ right: number
14
+ width: number
15
+ height: number
16
+ }
17
+
18
+ export type Placement = 'top' | 'bottom' | 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end'
19
+
20
+ export interface PositionOptions {
21
+ /** Preferred placement */
22
+ placement?: Placement
23
+ /** Gap between anchor and popover in px */
24
+ offset?: number
25
+ /** Boundary element (defaults to viewport) */
26
+ boundary?: HTMLElement | null
27
+ /** Minimum distance from boundary edges in px */
28
+ padding?: number
29
+ }
30
+
31
+ export interface PositionResult {
32
+ top: number
33
+ left: number
34
+ placement: Placement
35
+ }
36
+
37
+ /**
38
+ * Calculate the position of a popover relative to an anchor element,
39
+ * flipping when it would overflow the boundary.
40
+ */
41
+ export function computePosition(
42
+ anchor: HTMLElement,
43
+ popover: HTMLElement,
44
+ options: PositionOptions = {},
45
+ ): PositionResult {
46
+ const {
47
+ placement = 'top',
48
+ offset = 4,
49
+ boundary = null,
50
+ padding = 8,
51
+ } = options
52
+
53
+ const anchorRect = anchor.getBoundingClientRect()
54
+ const popoverRect = popover.getBoundingClientRect()
55
+
56
+ const boundaryRect = boundary
57
+ ? boundary.getBoundingClientRect()
58
+ : { top: 0, left: 0, bottom: window.innerHeight, right: window.innerWidth, width: window.innerWidth, height: window.innerHeight }
59
+
60
+ const availableTop = anchorRect.top - boundaryRect.top - padding
61
+ const availableBottom = boundaryRect.bottom - anchorRect.bottom - padding
62
+
63
+ // Determine vertical placement (flip if needed)
64
+ let vertical: 'top' | 'bottom'
65
+ if (placement.startsWith('top')) {
66
+ vertical = popoverRect.height + offset <= availableTop ? 'top' : 'bottom'
67
+ } else {
68
+ vertical = popoverRect.height + offset <= availableBottom ? 'bottom' : 'top'
69
+ }
70
+
71
+ // Calculate top
72
+ let top: number
73
+ if (vertical === 'top') {
74
+ top = anchorRect.top - popoverRect.height - offset
75
+ } else {
76
+ top = anchorRect.bottom + offset
77
+ }
78
+
79
+ // Determine horizontal alignment
80
+ let align: '' | '-start' | '-end' = ''
81
+ if (placement.endsWith('-start')) align = '-start'
82
+ else if (placement.endsWith('-end')) align = '-end'
83
+
84
+ // Calculate left
85
+ let left: number
86
+ switch (align) {
87
+ case '-start':
88
+ left = anchorRect.left
89
+ break
90
+ case '-end':
91
+ left = anchorRect.right - popoverRect.width
92
+ break
93
+ default:
94
+ left = anchorRect.left + (anchorRect.width - popoverRect.width) / 2
95
+ break
96
+ }
97
+
98
+ // Clamp horizontal to boundary
99
+ const minLeft = boundaryRect.left + padding
100
+ const maxLeft = boundaryRect.right - popoverRect.width - padding
101
+ left = Math.max(minLeft, Math.min(maxLeft, left))
102
+
103
+ // Clamp vertical to boundary
104
+ const minTop = boundaryRect.top + padding
105
+ const maxTop = boundaryRect.bottom - popoverRect.height - padding
106
+ top = Math.max(minTop, Math.min(maxTop, top))
107
+
108
+ const finalPlacement: Placement = `${vertical}${align}` as Placement
109
+
110
+ return { top, left, placement: finalPlacement }
111
+ }
112
+
113
+ /**
114
+ * Apply computed position to a popover element using absolute positioning
115
+ * relative to an offset parent.
116
+ */
117
+ export function applyPosition(
118
+ anchor: HTMLElement,
119
+ popover: HTMLElement,
120
+ options: PositionOptions = {},
121
+ ): Placement {
122
+ // Temporarily make visible for measurement
123
+ const prevVisibility = popover.style.visibility
124
+ popover.style.visibility = 'hidden'
125
+ popover.style.display = 'block'
126
+
127
+ const result = computePosition(anchor, popover, options)
128
+
129
+ // Convert to relative coordinates if popover has an offset parent
130
+ const offsetParent = popover.offsetParent as HTMLElement | null
131
+ if (offsetParent) {
132
+ const parentRect = offsetParent.getBoundingClientRect()
133
+ popover.style.top = `${result.top - parentRect.top}px`
134
+ popover.style.left = `${result.left - parentRect.left}px`
135
+ } else {
136
+ popover.style.top = `${result.top}px`
137
+ popover.style.left = `${result.left}px`
138
+ }
139
+
140
+ popover.style.visibility = prevVisibility
141
+
142
+ return result.placement
143
+ }