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,568 @@
1
+ /**
2
+ * Caption Styling
3
+ *
4
+ * Customizable caption/subtitle appearance.
5
+ *
6
+ * @module core/caption-styles
7
+ */
8
+
9
+ // =============================================================================
10
+ // Types
11
+ // =============================================================================
12
+
13
+ export interface CaptionStyleOptions {
14
+ /** Font family */
15
+ fontFamily?: string
16
+ /** Font size (CSS value) */
17
+ fontSize?: string
18
+ /** Font weight */
19
+ fontWeight?: string | number
20
+ /** Font color */
21
+ fontColor?: string
22
+ /** Text opacity (0-1) */
23
+ fontOpacity?: number
24
+ /** Background color */
25
+ backgroundColor?: string
26
+ /** Background opacity (0-1) */
27
+ backgroundOpacity?: number
28
+ /** Text edge style */
29
+ edgeStyle?: 'none' | 'raised' | 'depressed' | 'uniform' | 'drop-shadow'
30
+ /** Edge color */
31
+ edgeColor?: string
32
+ /** Window color (container background) */
33
+ windowColor?: string
34
+ /** Window opacity (0-1) */
35
+ windowOpacity?: number
36
+ /** Text alignment */
37
+ textAlign?: 'left' | 'center' | 'right'
38
+ /** Vertical position (percentage from bottom) */
39
+ verticalPosition?: number
40
+ /** Horizontal padding */
41
+ horizontalPadding?: string
42
+ /** Vertical padding */
43
+ verticalPadding?: string
44
+ /** Border radius */
45
+ borderRadius?: string
46
+ /** Line height */
47
+ lineHeight?: string | number
48
+ /** Letter spacing */
49
+ letterSpacing?: string
50
+ }
51
+
52
+ export interface CaptionStylePreset {
53
+ name: string
54
+ styles: CaptionStyleOptions
55
+ }
56
+
57
+ // =============================================================================
58
+ // Default Values
59
+ // =============================================================================
60
+
61
+ export const defaultCaptionStyles: Required<CaptionStyleOptions> = {
62
+ fontFamily: 'Arial, Helvetica, sans-serif',
63
+ fontSize: '1.5em',
64
+ fontWeight: 'normal',
65
+ fontColor: '#ffffff',
66
+ fontOpacity: 1,
67
+ backgroundColor: '#000000',
68
+ backgroundOpacity: 0.75,
69
+ edgeStyle: 'none',
70
+ edgeColor: '#000000',
71
+ windowColor: 'transparent',
72
+ windowOpacity: 0,
73
+ textAlign: 'center',
74
+ verticalPosition: 10,
75
+ horizontalPadding: '8px',
76
+ verticalPadding: '4px',
77
+ borderRadius: '4px',
78
+ lineHeight: 1.4,
79
+ letterSpacing: 'normal',
80
+ }
81
+
82
+ // =============================================================================
83
+ // Style Presets
84
+ // =============================================================================
85
+
86
+ export const captionPresets: Record<string, CaptionStylePreset> = {
87
+ default: {
88
+ name: 'Default',
89
+ styles: {},
90
+ },
91
+ cinema: {
92
+ name: 'Cinema',
93
+ styles: {
94
+ fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
95
+ fontSize: '1.8em',
96
+ fontWeight: 'bold',
97
+ backgroundColor: 'transparent',
98
+ backgroundOpacity: 0,
99
+ edgeStyle: 'drop-shadow',
100
+ edgeColor: '#000000',
101
+ },
102
+ },
103
+ classic: {
104
+ name: 'Classic',
105
+ styles: {
106
+ fontFamily: '"Times New Roman", Times, serif',
107
+ fontSize: '1.4em',
108
+ fontColor: '#ffff00',
109
+ backgroundColor: '#000000',
110
+ backgroundOpacity: 0.8,
111
+ },
112
+ },
113
+ highContrast: {
114
+ name: 'High Contrast',
115
+ styles: {
116
+ fontFamily: 'Arial, sans-serif',
117
+ fontSize: '1.6em',
118
+ fontWeight: 'bold',
119
+ fontColor: '#ffff00',
120
+ backgroundColor: '#000000',
121
+ backgroundOpacity: 1,
122
+ edgeStyle: 'uniform',
123
+ edgeColor: '#000000',
124
+ },
125
+ },
126
+ minimal: {
127
+ name: 'Minimal',
128
+ styles: {
129
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
130
+ fontSize: '1.3em',
131
+ backgroundColor: 'transparent',
132
+ backgroundOpacity: 0,
133
+ edgeStyle: 'drop-shadow',
134
+ },
135
+ },
136
+ netflix: {
137
+ name: 'Netflix Style',
138
+ styles: {
139
+ fontFamily: 'Netflix Sans, Helvetica Neue, Helvetica, Arial, sans-serif',
140
+ fontSize: '1.6em',
141
+ fontWeight: 'bold',
142
+ backgroundColor: 'transparent',
143
+ backgroundOpacity: 0,
144
+ edgeStyle: 'drop-shadow',
145
+ edgeColor: '#000000',
146
+ },
147
+ },
148
+ youtube: {
149
+ name: 'YouTube Style',
150
+ styles: {
151
+ fontFamily: '"YouTube Noto", Roboto, Arial, Helvetica, sans-serif',
152
+ fontSize: '1.5em',
153
+ fontWeight: 500,
154
+ backgroundColor: '#000000',
155
+ backgroundOpacity: 0.75,
156
+ borderRadius: '2px',
157
+ horizontalPadding: '4px',
158
+ verticalPadding: '2px',
159
+ },
160
+ },
161
+ }
162
+
163
+ // =============================================================================
164
+ // Caption Style Manager
165
+ // =============================================================================
166
+
167
+ export class CaptionStyleManager {
168
+ private styles: CaptionStyleOptions
169
+ private container: HTMLElement | null = null
170
+ private styleElement: HTMLStyleElement | null = null
171
+
172
+ constructor(initialStyles: CaptionStyleOptions = {}) {
173
+ this.styles = { ...defaultCaptionStyles, ...initialStyles }
174
+ }
175
+
176
+ /**
177
+ * Get current styles
178
+ */
179
+ getStyles(): CaptionStyleOptions {
180
+ return { ...this.styles }
181
+ }
182
+
183
+ /**
184
+ * Update styles
185
+ */
186
+ setStyles(styles: Partial<CaptionStyleOptions>): void {
187
+ this.styles = { ...this.styles, ...styles }
188
+ this.applyStyles()
189
+ }
190
+
191
+ /**
192
+ * Apply a preset
193
+ */
194
+ applyPreset(presetName: string): void {
195
+ const preset = captionPresets[presetName]
196
+ if (preset) {
197
+ this.styles = { ...defaultCaptionStyles, ...preset.styles }
198
+ this.applyStyles()
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Reset to defaults
204
+ */
205
+ reset(): void {
206
+ this.styles = { ...defaultCaptionStyles }
207
+ this.applyStyles()
208
+ }
209
+
210
+ /**
211
+ * Attach to caption container
212
+ */
213
+ attach(container: HTMLElement): void {
214
+ this.container = container
215
+ this.applyStyles()
216
+ }
217
+
218
+ /**
219
+ * Generate CSS from styles
220
+ */
221
+ generateCSS(selector = '.tsvp-captions'): string {
222
+ const s = this.styles as Required<CaptionStyleOptions>
223
+
224
+ // Build text shadow based on edge style
225
+ let textShadow = 'none'
226
+ switch (s.edgeStyle) {
227
+ case 'raised':
228
+ textShadow = `1px 1px 0 ${s.edgeColor}, 2px 2px 0 ${s.edgeColor}`
229
+ break
230
+ case 'depressed':
231
+ textShadow = `-1px -1px 0 ${s.edgeColor}, -2px -2px 0 ${s.edgeColor}`
232
+ break
233
+ case 'uniform':
234
+ textShadow = `
235
+ -1px -1px 0 ${s.edgeColor},
236
+ 1px -1px 0 ${s.edgeColor},
237
+ -1px 1px 0 ${s.edgeColor},
238
+ 1px 1px 0 ${s.edgeColor}
239
+ `
240
+ break
241
+ case 'drop-shadow':
242
+ textShadow = `2px 2px 4px ${s.edgeColor}, 0 0 8px rgba(0, 0, 0, 0.5)`
243
+ break
244
+ }
245
+
246
+ // Build background with opacity
247
+ const bgOpacity = Math.round(s.backgroundOpacity * 255).toString(16).padStart(2, '0')
248
+ const bgColor = s.backgroundColor === 'transparent'
249
+ ? 'transparent'
250
+ : `${s.backgroundColor}${bgOpacity}`
251
+
252
+ // Build font color with opacity
253
+ const fontOpacity = Math.round(s.fontOpacity * 255).toString(16).padStart(2, '0')
254
+ const fontColor = `${s.fontColor}${fontOpacity}`
255
+
256
+ // Window background
257
+ const windowOpacity = Math.round(s.windowOpacity * 255).toString(16).padStart(2, '0')
258
+ const windowBg = s.windowColor === 'transparent'
259
+ ? 'transparent'
260
+ : `${s.windowColor}${windowOpacity}`
261
+
262
+ return `
263
+ ${selector} {
264
+ --tsvp-captions-font-family: ${s.fontFamily};
265
+ --tsvp-captions-font-size: ${s.fontSize};
266
+ --tsvp-captions-font-weight: ${s.fontWeight};
267
+ --tsvp-captions-text-color: ${fontColor};
268
+ --tsvp-captions-bg: ${bgColor};
269
+ --tsvp-captions-text-shadow: ${textShadow};
270
+ --tsvp-captions-text-align: ${s.textAlign};
271
+ --tsvp-captions-padding: ${s.verticalPadding} ${s.horizontalPadding};
272
+ --tsvp-captions-radius: ${s.borderRadius};
273
+ --tsvp-captions-line-height: ${s.lineHeight};
274
+ --tsvp-captions-letter-spacing: ${s.letterSpacing};
275
+ --tsvp-captions-bottom: ${s.verticalPosition}%;
276
+
277
+ position: absolute;
278
+ bottom: var(--tsvp-captions-bottom);
279
+ left: 50%;
280
+ transform: translateX(-50%);
281
+ max-width: 80%;
282
+ text-align: var(--tsvp-captions-text-align);
283
+ background: ${windowBg};
284
+ padding: 8px;
285
+ border-radius: 4px;
286
+ }
287
+
288
+ ${selector} span,
289
+ ${selector} .tsvp-caption-text {
290
+ font-family: var(--tsvp-captions-font-family);
291
+ font-size: var(--tsvp-captions-font-size);
292
+ font-weight: var(--tsvp-captions-font-weight);
293
+ color: var(--tsvp-captions-text-color);
294
+ background: var(--tsvp-captions-bg);
295
+ text-shadow: var(--tsvp-captions-text-shadow);
296
+ padding: var(--tsvp-captions-padding);
297
+ border-radius: var(--tsvp-captions-radius);
298
+ line-height: var(--tsvp-captions-line-height);
299
+ letter-spacing: var(--tsvp-captions-letter-spacing);
300
+ display: inline;
301
+ box-decoration-break: clone;
302
+ -webkit-box-decoration-break: clone;
303
+ }
304
+ `
305
+ }
306
+
307
+ /**
308
+ * Apply styles to container
309
+ */
310
+ private applyStyles(): void {
311
+ if (!this.container) return
312
+
313
+ // Remove existing style element
314
+ if (this.styleElement) {
315
+ this.styleElement.remove()
316
+ }
317
+
318
+ // Create new style element
319
+ this.styleElement = document.createElement('style')
320
+ this.styleElement.setAttribute('data-tsvp-caption-styles', 'true')
321
+ this.styleElement.textContent = this.generateCSS()
322
+ document.head.appendChild(this.styleElement)
323
+ }
324
+
325
+ /**
326
+ * Get available presets
327
+ */
328
+ static getPresets(): CaptionStylePreset[] {
329
+ return Object.values(captionPresets)
330
+ }
331
+
332
+ /**
333
+ * Detach and cleanup
334
+ */
335
+ destroy(): void {
336
+ if (this.styleElement) {
337
+ this.styleElement.remove()
338
+ this.styleElement = null
339
+ }
340
+ this.container = null
341
+ }
342
+ }
343
+
344
+ // =============================================================================
345
+ // Factory
346
+ // =============================================================================
347
+
348
+ export function createCaptionStyleManager(styles?: CaptionStyleOptions): CaptionStyleManager {
349
+ return new CaptionStyleManager(styles)
350
+ }
351
+
352
+ // =============================================================================
353
+ // Caption Settings Menu Component
354
+ // =============================================================================
355
+
356
+ export class CaptionSettingsMenu {
357
+ private container: HTMLElement
358
+ private manager: CaptionStyleManager
359
+ private onChange: ((_styles: CaptionStyleOptions) => void) | null = null
360
+
361
+ constructor(manager: CaptionStyleManager) {
362
+ this.manager = manager
363
+ this.container = document.createElement('div')
364
+ this.container.className = 'tsvp-caption-settings'
365
+ this.render()
366
+ }
367
+
368
+ private render(): void {
369
+ this.container.innerHTML = ''
370
+ this.container.style.cssText = `
371
+ padding: 16px;
372
+ background: var(--tsvp-menu-bg, rgba(20, 20, 20, 0.95));
373
+ border-radius: var(--tsvp-menu-radius, 8px);
374
+ color: var(--tsvp-color-text, #ffffff);
375
+ font-family: var(--tsvp-font-family, sans-serif);
376
+ font-size: var(--tsvp-font-size, 14px);
377
+ `
378
+
379
+ const styles = this.manager.getStyles()
380
+
381
+ // Preset selector
382
+ this.addPresetSelector()
383
+
384
+ // Font size
385
+ this.addSlider('Font Size', 'fontSize', ['0.8em', '1em', '1.2em', '1.5em', '1.8em', '2em'], styles.fontSize || '1.5em')
386
+
387
+ // Font color
388
+ this.addColorPicker('Text Color', 'fontColor', styles.fontColor || '#ffffff')
389
+
390
+ // Background opacity
391
+ this.addSlider('Background', 'backgroundOpacity', ['0', '0.25', '0.5', '0.75', '1'], String(styles.backgroundOpacity ?? 0.75))
392
+
393
+ // Edge style
394
+ this.addSelect('Text Edge', 'edgeStyle', [
395
+ { value: 'none', label: 'None' },
396
+ { value: 'raised', label: 'Raised' },
397
+ { value: 'depressed', label: 'Depressed' },
398
+ { value: 'uniform', label: 'Uniform' },
399
+ { value: 'drop-shadow', label: 'Drop Shadow' },
400
+ ], styles.edgeStyle || 'none')
401
+
402
+ // Reset button
403
+ const resetBtn = document.createElement('button')
404
+ resetBtn.textContent = 'Reset to Default'
405
+ resetBtn.style.cssText = `
406
+ width: 100%;
407
+ margin-top: 16px;
408
+ padding: 8px 16px;
409
+ border: 1px solid var(--tsvp-color-border, rgba(255, 255, 255, 0.2));
410
+ border-radius: var(--tsvp-radius-sm, 4px);
411
+ background: transparent;
412
+ color: var(--tsvp-color-text, #ffffff);
413
+ cursor: pointer;
414
+ `
415
+ resetBtn.addEventListener('click', () => {
416
+ this.manager.reset()
417
+ this.render()
418
+ if (this.onChange) {
419
+ this.onChange(this.manager.getStyles())
420
+ }
421
+ })
422
+ this.container.appendChild(resetBtn)
423
+ }
424
+
425
+ private addPresetSelector(): void {
426
+ const row = this.createRow('Preset')
427
+ const select = document.createElement('select')
428
+ select.style.cssText = this.getInputStyle()
429
+
430
+ for (const [key, preset] of Object.entries(captionPresets)) {
431
+ const option = document.createElement('option')
432
+ option.value = key
433
+ option.textContent = preset.name
434
+ select.appendChild(option)
435
+ }
436
+
437
+ select.addEventListener('change', () => {
438
+ this.manager.applyPreset(select.value)
439
+ this.render()
440
+ if (this.onChange) {
441
+ this.onChange(this.manager.getStyles())
442
+ }
443
+ })
444
+
445
+ row.appendChild(select)
446
+ this.container.appendChild(row)
447
+ }
448
+
449
+ private addSlider(label: string, key: keyof CaptionStyleOptions, values: string[], current: string): void {
450
+ const row = this.createRow(label)
451
+ const slider = document.createElement('input')
452
+ slider.type = 'range'
453
+ slider.min = '0'
454
+ slider.max = String(values.length - 1)
455
+ slider.value = String(values.indexOf(current))
456
+ slider.style.cssText = 'flex: 1;'
457
+
458
+ slider.addEventListener('input', () => {
459
+ const value = values[Number.parseInt(slider.value, 10)]
460
+ this.manager.setStyles({ [key]: key === 'backgroundOpacity' ? Number.parseFloat(value) : value })
461
+ if (this.onChange) {
462
+ this.onChange(this.manager.getStyles())
463
+ }
464
+ })
465
+
466
+ row.appendChild(slider)
467
+ this.container.appendChild(row)
468
+ }
469
+
470
+ private addColorPicker(label: string, key: keyof CaptionStyleOptions, current: string): void {
471
+ const row = this.createRow(label)
472
+ const input = document.createElement('input')
473
+ input.type = 'color'
474
+ input.value = current
475
+ input.style.cssText = 'width: 40px; height: 30px; border: none; cursor: pointer;'
476
+
477
+ input.addEventListener('input', () => {
478
+ this.manager.setStyles({ [key]: input.value })
479
+ if (this.onChange) {
480
+ this.onChange(this.manager.getStyles())
481
+ }
482
+ })
483
+
484
+ row.appendChild(input)
485
+ this.container.appendChild(row)
486
+ }
487
+
488
+ private addSelect(label: string, key: keyof CaptionStyleOptions, options: Array<{ value: string, label: string }>, current: string): void {
489
+ const row = this.createRow(label)
490
+ const select = document.createElement('select')
491
+ select.style.cssText = this.getInputStyle()
492
+
493
+ for (const opt of options) {
494
+ const option = document.createElement('option')
495
+ option.value = opt.value
496
+ option.textContent = opt.label
497
+ option.selected = opt.value === current
498
+ select.appendChild(option)
499
+ }
500
+
501
+ select.addEventListener('change', () => {
502
+ this.manager.setStyles({ [key]: select.value as any })
503
+ if (this.onChange) {
504
+ this.onChange(this.manager.getStyles())
505
+ }
506
+ })
507
+
508
+ row.appendChild(select)
509
+ this.container.appendChild(row)
510
+ }
511
+
512
+ private createRow(label: string): HTMLElement {
513
+ const row = document.createElement('div')
514
+ row.style.cssText = `
515
+ display: flex;
516
+ align-items: center;
517
+ justify-content: space-between;
518
+ gap: 12px;
519
+ margin-bottom: 12px;
520
+ `
521
+
522
+ const labelEl = document.createElement('span')
523
+ labelEl.textContent = label
524
+ labelEl.style.opacity = '0.8'
525
+ row.appendChild(labelEl)
526
+
527
+ return row
528
+ }
529
+
530
+ private getInputStyle(): string {
531
+ return `
532
+ padding: 6px 10px;
533
+ border: 1px solid var(--tsvp-color-border, rgba(255, 255, 255, 0.2));
534
+ border-radius: var(--tsvp-radius-sm, 4px);
535
+ background: rgba(0, 0, 0, 0.3);
536
+ color: var(--tsvp-color-text, #ffffff);
537
+ `
538
+ }
539
+
540
+ /**
541
+ * Set change handler
542
+ */
543
+ onStyleChange(handler: (styles: CaptionStyleOptions) => void): void {
544
+ this.onChange = handler
545
+ }
546
+
547
+ /**
548
+ * Get element
549
+ */
550
+ getElement(): HTMLElement {
551
+ return this.container
552
+ }
553
+
554
+ /**
555
+ * Destroy
556
+ */
557
+ destroy(): void {
558
+ this.container.remove()
559
+ }
560
+ }
561
+
562
+ // =============================================================================
563
+ // Factory
564
+ // =============================================================================
565
+
566
+ export function createCaptionSettingsMenu(manager: CaptionStyleManager): CaptionSettingsMenu {
567
+ return new CaptionSettingsMenu(manager)
568
+ }