react-helios 2.0.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.
@@ -0,0 +1,501 @@
1
+ .container {
2
+ position: relative;
3
+ width: 100%;
4
+ background-color: #000;
5
+ aspect-ratio: 16 / 9;
6
+ max-width: 100%;
7
+ overflow: hidden;
8
+ }
9
+
10
+ .videoWrapper {
11
+ position: relative;
12
+ width: 100%;
13
+ height: 100%;
14
+ }
15
+
16
+ .video {
17
+ width: 100%;
18
+ height: 100%;
19
+ object-fit: contain;
20
+ display: block;
21
+ }
22
+
23
+ .bufferingIndicator {
24
+ position: absolute;
25
+ top: 50%;
26
+ left: 50%;
27
+ transform: translate(-50%, -50%);
28
+ display: flex;
29
+ flex-direction: column;
30
+ align-items: center;
31
+ gap: 12px;
32
+ color: white;
33
+ z-index: 10;
34
+ }
35
+
36
+ .spinner {
37
+ width: 48px;
38
+ height: 48px;
39
+ border: 4px solid rgba(255, 255, 255, 0.3);
40
+ border-top-color: white;
41
+ border-radius: 50%;
42
+ animation: spin 1s linear infinite;
43
+ }
44
+
45
+ @keyframes spin {
46
+ to {
47
+ transform: rotate(360deg);
48
+ }
49
+ }
50
+
51
+ .bufferingText {
52
+ font-size: 14px;
53
+ font-weight: 500;
54
+ }
55
+
56
+ .errorOverlay {
57
+ position: absolute;
58
+ top: 0;
59
+ left: 0;
60
+ right: 0;
61
+ bottom: 0;
62
+ display: flex;
63
+ align-items: center;
64
+ justify-content: center;
65
+ background-color: rgba(0, 0, 0, 0.9);
66
+ z-index: 20;
67
+ }
68
+
69
+ .errorContent {
70
+ text-align: center;
71
+ color: white;
72
+ padding: 24px;
73
+ max-width: 400px;
74
+ }
75
+
76
+ .errorTitle {
77
+ margin: 0 0 12px;
78
+ font-size: 20px;
79
+ font-weight: 600;
80
+ }
81
+
82
+ .errorMessage {
83
+ margin: 0;
84
+ font-size: 14px;
85
+ color: rgba(255, 255, 255, 0.8);
86
+ }
87
+
88
+ @media (max-width: 640px) {
89
+ .container {
90
+ border-radius: 0;
91
+ }
92
+ }
93
+
94
+
95
+ .controls {
96
+ position: absolute;
97
+ bottom: 0;
98
+ left: 0;
99
+ right: 0;
100
+ background: linear-gradient(to top, rgba(0, 0, 0, 0.8) 0%, transparent 100%);
101
+ padding: 48px 16px 16px;
102
+ transition: opacity 0.3s ease;
103
+ z-index: 5;
104
+ }
105
+
106
+ .controls.visible {
107
+ opacity: 1;
108
+ }
109
+
110
+ .controls.hidden {
111
+ opacity: 0;
112
+ pointer-events: none;
113
+ }
114
+
115
+ .controls:hover {
116
+ opacity: 1;
117
+ pointer-events: all;
118
+ }
119
+
120
+ .controlsBar {
121
+ display: flex;
122
+ align-items: center;
123
+ justify-content: space-between;
124
+ gap: 12px;
125
+ margin-top: 12px;
126
+ flex-wrap: wrap;
127
+ }
128
+
129
+ .controlsLeft {
130
+ display: flex;
131
+ align-items: center;
132
+ gap: 12px;
133
+ }
134
+
135
+ .controlsRight {
136
+ display: flex;
137
+ align-items: center;
138
+ gap: 8px;
139
+ }
140
+
141
+ .timeDisplay {
142
+ color: white;
143
+ font-size: 14px;
144
+ font-weight: 500;
145
+ white-space: nowrap;
146
+ user-select: none;
147
+ }
148
+
149
+ .currentTime,
150
+ .duration {
151
+ font-variant-numeric: tabular-nums;
152
+ }
153
+
154
+ .separator {
155
+ opacity: 0.7;
156
+ }
157
+
158
+ @media (max-width: 640px) {
159
+ .controlsBar {
160
+ gap: 4px;
161
+ }
162
+
163
+ .timeDisplay {
164
+ font-size: 12px;
165
+ }
166
+ }
167
+
168
+ @media (max-width: 480px) {
169
+ .controlButton {
170
+ padding: 6px;
171
+ }
172
+ }
173
+
174
+
175
+ /* ─── Control button base ────────────────────────────────────────────────── */
176
+ .controlButton {
177
+ background: none;
178
+ border: none;
179
+ color: #fff;
180
+ cursor: pointer;
181
+ padding: 10px;
182
+ min-width: 40px;
183
+ min-height: 40px;
184
+ display: flex;
185
+ align-items: center;
186
+ justify-content: center;
187
+ border-radius: 4px;
188
+ transition: opacity 0.15s, background-color 0.15s, transform 0.1s;
189
+ flex-shrink: 0;
190
+ }
191
+
192
+ .controlButton:hover {
193
+ background-color: rgba(255, 255, 255, 0.12);
194
+ opacity: 1;
195
+ }
196
+
197
+ .controlButton:active {
198
+ transform: scale(0.92);
199
+ }
200
+
201
+ .controlButton svg {
202
+ width: 20px;
203
+ height: 20px;
204
+ pointer-events: none;
205
+ }
206
+
207
+ @media (max-width: 480px) {
208
+ .controlButton {
209
+ padding: 8px;
210
+ min-width: 36px;
211
+ min-height: 36px;
212
+ }
213
+ }
214
+
215
+ /* ─── Volume control ─────────────────────────────────────────────────────── */
216
+ .volumeContainer {
217
+ position: relative;
218
+ display: flex;
219
+ align-items: center;
220
+ }
221
+
222
+ .volumeSlider {
223
+ width: 80px;
224
+ cursor: pointer;
225
+ -webkit-appearance: none;
226
+ appearance: none;
227
+ background: rgba(255, 255, 255, 0.3);
228
+ border-radius: 2px;
229
+ height: 4px;
230
+ outline: none;
231
+ transition: width 0.15s;
232
+ flex-shrink: 0;
233
+ }
234
+
235
+ .volumeSlider::-webkit-slider-thumb {
236
+ -webkit-appearance: none;
237
+ appearance: none;
238
+ width: 12px;
239
+ height: 12px;
240
+ background: #fff;
241
+ border-radius: 50%;
242
+ cursor: pointer;
243
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
244
+ }
245
+
246
+ .volumeSlider::-moz-range-thumb {
247
+ width: 12px;
248
+ height: 12px;
249
+ background: #fff;
250
+ border-radius: 50%;
251
+ cursor: pointer;
252
+ border: none;
253
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
254
+ }
255
+
256
+ /* ─── Time display ───────────────────────────────────────────────────────── */
257
+ .timeDisplay {
258
+ color: #fff;
259
+ font-size: 13px;
260
+ font-weight: 500;
261
+ user-select: none;
262
+ white-space: nowrap;
263
+ padding: 0 4px;
264
+ letter-spacing: 0.01em;
265
+ }
266
+
267
+ /* ─── Settings menu ──────────────────────────────────────────────────────── */
268
+ .settingsContainer {
269
+ position: relative;
270
+ }
271
+
272
+ .settingsDropdown {
273
+ position: absolute;
274
+ bottom: calc(100% + 8px);
275
+ right: 0;
276
+ background-color: rgba(15, 15, 15, 0.95);
277
+ border-radius: 6px;
278
+ padding: 6px;
279
+ min-width: 150px;
280
+ z-index: 30;
281
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.6);
282
+ backdrop-filter: blur(8px);
283
+ }
284
+
285
+ /* Tabs (Speed / Quality) */
286
+ .settingsTabs {
287
+ display: flex;
288
+ border-bottom: 1px solid rgba(255, 255, 255, 0.12);
289
+ margin-bottom: 4px;
290
+ }
291
+
292
+ .settingsTab {
293
+ flex: 1;
294
+ background: none;
295
+ border: none;
296
+ color: rgba(255, 255, 255, 0.6);
297
+ cursor: pointer;
298
+ font-size: 12px;
299
+ font-weight: 600;
300
+ padding: 6px 0;
301
+ letter-spacing: 0.04em;
302
+ border-bottom: 2px solid transparent;
303
+ transition: color 0.15s, border-color 0.15s;
304
+ text-transform: uppercase;
305
+ }
306
+
307
+ .settingsTab.active {
308
+ color: #fff;
309
+ border-bottom-color: #3b82f6;
310
+ }
311
+
312
+ .settingsTab:hover:not(.active) {
313
+ color: rgba(255, 255, 255, 0.9);
314
+ }
315
+
316
+ /* Panel section label */
317
+ .settingsPanelLabel {
318
+ color: rgba(255, 255, 255, 0.5);
319
+ font-size: 10px;
320
+ font-weight: 700;
321
+ letter-spacing: 0.08em;
322
+ text-transform: uppercase;
323
+ padding: 4px 8px 2px;
324
+ }
325
+
326
+ /* Option rows */
327
+ .settingsOption {
328
+ display: flex;
329
+ align-items: center;
330
+ justify-content: space-between;
331
+ width: 100%;
332
+ padding: 7px 10px;
333
+ background: none;
334
+ border: none;
335
+ color: rgba(255, 255, 255, 0.85);
336
+ cursor: pointer;
337
+ text-align: left;
338
+ border-radius: 4px;
339
+ font-size: 13px;
340
+ transition: background-color 0.15s;
341
+ }
342
+
343
+ .settingsOption:hover {
344
+ background-color: rgba(255, 255, 255, 0.1);
345
+ color: #fff;
346
+ }
347
+
348
+ .settingsOption.active {
349
+ color: #60a5fa;
350
+ font-weight: 600;
351
+ }
352
+
353
+ .settingsOptionBadge {
354
+ font-size: 10px;
355
+ color: rgba(255, 255, 255, 0.4);
356
+ margin-left: 8px;
357
+ flex-shrink: 0;
358
+ }
359
+
360
+
361
+ /* ─── Progress bar container ─────────────────────────────────────────────── */
362
+ .progressContainer {
363
+ position: relative;
364
+ width: 100%;
365
+ /* Tall hit area so scrub feels easy to grab on touch and mouse */
366
+ padding: 10px 0;
367
+ cursor: pointer;
368
+ /* Expand the clickable zone without affecting layout neighbours */
369
+ box-sizing: content-box;
370
+ }
371
+
372
+ .progressContainer:focus {
373
+ outline: none;
374
+ }
375
+
376
+ .progressContainer:focus-visible {
377
+ outline: 2px solid #60a5fa;
378
+ outline-offset: 2px;
379
+ border-radius: 2px;
380
+ }
381
+
382
+ /* ─── Hidden preview video ───────────────────────────────────────────────── */
383
+ .previewVideo {
384
+ display: none;
385
+ }
386
+
387
+ /* ─── Thumbnail tooltip ──────────────────────────────────────────────────── */
388
+ .previewTooltip {
389
+ position: absolute;
390
+ /* Sits above the container; bottom 100% + a small gap */
391
+ bottom: calc(100% + 6px);
392
+ transform: translateX(-50%);
393
+ pointer-events: none;
394
+ z-index: 20;
395
+ background-color: #000;
396
+ border-radius: 4px;
397
+ overflow: hidden;
398
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
399
+ }
400
+
401
+ .previewCanvas {
402
+ display: block;
403
+ width: 160px;
404
+ height: 90px;
405
+ }
406
+
407
+ .previewTime {
408
+ padding: 3px 8px;
409
+ font-size: 11px;
410
+ font-weight: 600;
411
+ color: #fff;
412
+ text-align: center;
413
+ background-color: rgba(0, 0, 0, 0.7);
414
+ }
415
+
416
+ /* ─── Track background ───────────────────────────────────────────────────── */
417
+ .progressBackground {
418
+ position: relative;
419
+ width: 100%;
420
+ height: 4px;
421
+ background-color: rgba(255, 255, 255, 0.25);
422
+ border-radius: 2px;
423
+ /* Keep overflow:hidden for buffered / filled bars ONLY.
424
+ The scrub handle must live OUTSIDE this element. */
425
+ overflow: hidden;
426
+ transition: height 0.15s;
427
+ will-change: height;
428
+ }
429
+
430
+ /* Grow the track on hover for a "YouTube-style" feel */
431
+ .progressContainer:hover .progressBackground {
432
+ height: 6px;
433
+ }
434
+
435
+ /* ─── Buffered range indicator ───────────────────────────────────────────── */
436
+ .bufferedSegment {
437
+ position: absolute;
438
+ top: 0;
439
+ height: 100%;
440
+ background-color: rgba(255, 255, 255, 0.45);
441
+ border-radius: 2px;
442
+ }
443
+
444
+ /* ─── Played progress fill ───────────────────────────────────────────────── */
445
+ .progressFilled {
446
+ position: absolute;
447
+ top: 0;
448
+ left: 0;
449
+ height: 100%;
450
+ background-color: #3b82f6;
451
+ border-radius: 2px;
452
+ will-change: width;
453
+ }
454
+
455
+ /* ─── Hover position indicator (thin white line) ────────────────────────── */
456
+ .hoverIndicator {
457
+ position: absolute;
458
+ top: 50%;
459
+ transform: translate(-50%, -50%);
460
+ width: 2px;
461
+ height: 100%;
462
+ background-color: rgba(255, 255, 255, 0.8);
463
+ pointer-events: none;
464
+ }
465
+
466
+ /* ─── Scrub handle ───────────────────────────────────────────────────────── */
467
+ /*
468
+ * The handle is a SIBLING of .progressBackground (outside overflow:hidden).
469
+ * It is positioned absolutely within .progressContainer, which provides
470
+ * the full padded height. `top: 50%` centers it on the container mid-line,
471
+ * which aligns with the track regardless of the container's padding.
472
+ *
473
+ * Fix: previously the handle was inside overflow:hidden and only a tiny
474
+ * sliver was visible on the 4px-tall track.
475
+ */
476
+ .scrubHandle {
477
+ position: absolute;
478
+ top: 50%;
479
+ transform: translate(-50%, -50%);
480
+ width: 14px;
481
+ height: 14px;
482
+ background-color: #fff;
483
+ border-radius: 50%;
484
+ box-shadow: 0 1px 6px rgba(0, 0, 0, 0.5);
485
+ pointer-events: none;
486
+ transition: transform 0.12s, opacity 0.12s;
487
+ /* Hidden by default; shown on container hover */
488
+ opacity: 0;
489
+ z-index: 2;
490
+ will-change: transform, opacity;
491
+ }
492
+
493
+ .progressContainer:hover .scrubHandle,
494
+ .scrubHandle.dragging {
495
+ opacity: 1;
496
+ }
497
+
498
+ .scrubHandle.dragging {
499
+ transform: translate(-50%, -50%) scale(1.25);
500
+ transition: none;
501
+ }
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "react-helios",
3
+ "version": "2.0.0",
4
+ "description": "Production-grade React video player with HLS, quality selection, live streams, subtitles, and thumbnail preview",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "README.md",
11
+ "LICENSE"
12
+ ],
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.mjs",
17
+ "require": "./dist/index.js"
18
+ },
19
+ "./styles": "./dist/styles.css"
20
+ },
21
+ "scripts": {
22
+ "build": "tsup",
23
+ "dev": "tsup --watch",
24
+ "typecheck": "tsc --noEmit",
25
+ "prepublishOnly": "npm run build"
26
+ },
27
+ "keywords": [
28
+ "react",
29
+ "video",
30
+ "player",
31
+ "video-player",
32
+ "hls",
33
+ "hls.js",
34
+ "live-stream",
35
+ "thumbnail",
36
+ "preview",
37
+ "quality",
38
+ "subtitles",
39
+ "captions",
40
+ "typescript",
41
+ "nextjs",
42
+ "picture-in-picture"
43
+ ],
44
+ "author": "Sanish Manandhar <mail.sanishmanandhar@gmail.com>",
45
+ "license": "MIT",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "https://github.com/sanishmdhr96/react-video-player"
49
+ },
50
+ "bugs": {
51
+ "url": "https://github.com/sanishmdhr96/react-video-player/issues"
52
+ },
53
+ "homepage": "https://github.com/sanishmdhr96/react-video-player#readme",
54
+ "peerDependencies": {
55
+ "react": "^18.0.0 || ^19.0.0",
56
+ "react-dom": "^18.0.0 || ^19.0.0"
57
+ },
58
+ "dependencies": {
59
+ "hls.js": "^1.6.15"
60
+ },
61
+ "devDependencies": {
62
+ "@types/node": "^25.0.0",
63
+ "@types/react": "^19.0.0",
64
+ "@types/react-dom": "^19.0.0",
65
+ "typescript": "^5.0.0",
66
+ "tsup": "^8.0.0"
67
+ }
68
+ }