@haklex/rich-renderer-video 0.0.1

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.
package/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Innei
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+
24
+ Additional Terms and Conditions
25
+
26
+ ----------------
27
+
28
+ Use of this software is governed by the terms of MIT and, in addition, by the terms and conditions described in the additional file (ADDITIONAL_TERMS.md). By using this software, you agree to abide by these additional terms and conditions.
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # @haklex/rich-renderer-video
2
+
3
+ 视频播放器渲染器。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ pnpm add @haklex/rich-renderer-video @haklex/rich-editor
9
+ ```
10
+
11
+ ## 导出
12
+
13
+ ```ts
14
+ export { VideoRenderer } from './VideoRenderer'
15
+ export { VideoEditRenderer } from './VideoEditRenderer'
16
+ ```
17
+
18
+ ## 功能
19
+
20
+ - 自定义控制栏
21
+ - 进度条
22
+ - 音量控制
23
+ - 全屏
24
+
25
+ ## 使用
26
+
27
+ ```tsx
28
+ import { VideoRenderer } from '@haklex/rich-renderer-video'
29
+ import type { RendererConfig } from '@haklex/rich-editor'
30
+
31
+ const config: RendererConfig = {
32
+ Video: VideoRenderer,
33
+ }
34
+ ```
35
+
36
+ ## License
37
+
38
+ MIT
@@ -0,0 +1,3 @@
1
+ import { VideoRendererProps } from '@haklex/rich-editor';
2
+ export declare function VideoEditRenderer(props: VideoRendererProps): import("react/jsx-runtime").JSX.Element;
3
+ //# sourceMappingURL=VideoEditRenderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VideoEditRenderer.d.ts","sourceRoot":"","sources":["../src/VideoEditRenderer.tsx"],"names":[],"mappings":"AAAA,OAAO,cAAc,CAAA;AAErB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AAU7D,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,kBAAkB,2CAQ1D"}
@@ -0,0 +1,4 @@
1
+ import { VideoRendererProps } from '@haklex/rich-editor';
2
+ import { ComponentType } from 'react';
3
+ export declare const VideoRenderer: ComponentType<VideoRendererProps>;
4
+ //# sourceMappingURL=VideoRenderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VideoRenderer.d.ts","sourceRoot":"","sources":["../src/VideoRenderer.tsx"],"names":[],"mappings":"AAAA,OAAO,cAAc,CAAA;AAGrB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AAW7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAY1C,eAAO,MAAM,aAAa,EAAE,aAAa,CAAC,kBAAkB,CA8Q3D,CAAA"}
@@ -0,0 +1,4 @@
1
+ export { VideoEditRenderer } from './VideoEditRenderer';
2
+ export { VideoRenderer } from './VideoRenderer';
3
+ export { VideoRenderer as default } from './VideoRenderer';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAC/C,OAAO,EAAE,aAAa,IAAI,OAAO,EAAE,MAAM,iBAAiB,CAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,449 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useRendererMode } from "@haklex/rich-editor";
3
+ import { Popover, PopoverTrigger, PopoverPanel } from "@haklex/rich-editor-ui";
4
+ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
5
+ import { $getNearestNodeFromDOMNode } from "lexical";
6
+ import { Play, Pause, VolumeX, Volume2, Loader2, Download, Minimize, Maximize, Video, ImageIcon, ExternalLink, Trash2 } from "lucide-react";
7
+ import { useRef, useState, useMemo, useCallback, useEffect } from "react";
8
+ import { Slider } from "@base-ui/react/slider";
9
+ const PlayIcon = ({ size = 20 }) => /* @__PURE__ */ jsx(Play, { size });
10
+ const PauseIcon = ({ size = 20 }) => /* @__PURE__ */ jsx(Pause, { size });
11
+ const VolumeIcon = () => /* @__PURE__ */ jsx(Volume2, { size: 20 });
12
+ const VolumeMuteIcon = () => /* @__PURE__ */ jsx(VolumeX, { size: 20 });
13
+ const DownloadIcon = () => /* @__PURE__ */ jsx(Download, { size: 20 });
14
+ const LoadingIcon = () => /* @__PURE__ */ jsx(Loader2, { size: 20, className: "rr-video-spin" });
15
+ const FullscreenIcon = () => /* @__PURE__ */ jsx(Maximize, { size: 20 });
16
+ const ExitFullscreenIcon = () => /* @__PURE__ */ jsx(Minimize, { size: 20 });
17
+ const VideoRenderer = ({
18
+ src,
19
+ poster,
20
+ width,
21
+ height
22
+ }) => {
23
+ const wrapperRef = useRef(null);
24
+ const videoRef = useRef(null);
25
+ const [playing, setPlaying] = useState(false);
26
+ const [duration, setDuration] = useState(0);
27
+ const [currentTime, setCurrentTime] = useState(0);
28
+ const [muted, setMuted] = useState(false);
29
+ const [isFullscreen, setIsFullscreen] = useState(false);
30
+ const [indicator, setIndicator] = useState(null);
31
+ const [draggingTime, setDraggingTime] = useState(null);
32
+ const [isDownloading, setIsDownloading] = useState(false);
33
+ const dragWasPlayingRef = useRef(false);
34
+ const indicatorTimerRef = useRef(void 0);
35
+ const indicatorKeyRef = useRef(0);
36
+ const aspectRatio = useMemo(() => {
37
+ if (width && height && width > 0 && height > 0) {
38
+ return `${width} / ${height}`;
39
+ }
40
+ return "16 / 9";
41
+ }, [height, width]);
42
+ const showIndicator = useCallback((type) => {
43
+ indicatorKeyRef.current += 1;
44
+ setIndicator({ type, key: indicatorKeyRef.current });
45
+ clearTimeout(indicatorTimerRef.current);
46
+ indicatorTimerRef.current = setTimeout(() => setIndicator(null), 500);
47
+ }, []);
48
+ const play = useCallback(() => {
49
+ void videoRef.current?.play();
50
+ }, []);
51
+ const pause = useCallback(() => {
52
+ videoRef.current?.pause();
53
+ }, []);
54
+ const togglePlay = useCallback(() => {
55
+ const video = videoRef.current;
56
+ if (!video) return;
57
+ if (video.paused) {
58
+ play();
59
+ showIndicator("play");
60
+ } else {
61
+ pause();
62
+ showIndicator("pause");
63
+ }
64
+ }, [pause, play, showIndicator]);
65
+ const seekTo = useCallback((time) => {
66
+ const video = videoRef.current;
67
+ if (!video) return;
68
+ const clamped = Math.min(
69
+ Math.max(0, time),
70
+ Number.isFinite(video.duration) ? video.duration : time
71
+ );
72
+ video.currentTime = clamped;
73
+ setCurrentTime(clamped);
74
+ }, []);
75
+ const toggleMute = useCallback(() => {
76
+ const video = videoRef.current;
77
+ if (!video) return;
78
+ video.muted = !video.muted;
79
+ setMuted(video.muted);
80
+ }, []);
81
+ const toggleFullscreen = useCallback(() => {
82
+ const wrapper = wrapperRef.current;
83
+ if (!wrapper) return;
84
+ if (!document.fullscreenElement) {
85
+ void wrapper.requestFullscreen();
86
+ } else {
87
+ void document.exitFullscreen();
88
+ }
89
+ }, []);
90
+ const downloadingRef = useRef(false);
91
+ const downloadVideo = useCallback(() => {
92
+ if (downloadingRef.current) return;
93
+ downloadingRef.current = true;
94
+ setIsDownloading(true);
95
+ const filename = src.split("/").pop() || "video";
96
+ const fallback = () => {
97
+ const a = document.createElement("a");
98
+ a.href = src;
99
+ a.download = filename;
100
+ a.rel = "noopener noreferrer";
101
+ a.click();
102
+ };
103
+ fetch(src).then((res) => {
104
+ if (!res.ok) throw new Error(res.statusText);
105
+ return res.blob();
106
+ }).then((blob) => {
107
+ const url = URL.createObjectURL(blob);
108
+ const a = document.createElement("a");
109
+ a.href = url;
110
+ a.download = filename;
111
+ a.click();
112
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
113
+ }).catch(() => fallback()).finally(() => {
114
+ downloadingRef.current = false;
115
+ setIsDownloading(false);
116
+ });
117
+ }, [src]);
118
+ useEffect(() => {
119
+ const video = videoRef.current;
120
+ if (!video) return;
121
+ const onLoadedMetadata = () => {
122
+ setDuration(Number.isFinite(video.duration) ? video.duration : 0);
123
+ setMuted(video.muted);
124
+ };
125
+ const onDurationChange = () => {
126
+ setDuration(Number.isFinite(video.duration) ? video.duration : 0);
127
+ };
128
+ const onTimeUpdate = () => setCurrentTime(video.currentTime);
129
+ const onPlay = () => setPlaying(true);
130
+ const onPause = () => setPlaying(false);
131
+ const onVolumeChange = () => setMuted(video.muted);
132
+ video.addEventListener("loadedmetadata", onLoadedMetadata);
133
+ video.addEventListener("durationchange", onDurationChange);
134
+ video.addEventListener("timeupdate", onTimeUpdate);
135
+ video.addEventListener("play", onPlay);
136
+ video.addEventListener("pause", onPause);
137
+ video.addEventListener("volumechange", onVolumeChange);
138
+ return () => {
139
+ video.removeEventListener("loadedmetadata", onLoadedMetadata);
140
+ video.removeEventListener("durationchange", onDurationChange);
141
+ video.removeEventListener("timeupdate", onTimeUpdate);
142
+ video.removeEventListener("play", onPlay);
143
+ video.removeEventListener("pause", onPause);
144
+ video.removeEventListener("volumechange", onVolumeChange);
145
+ };
146
+ }, []);
147
+ useEffect(() => {
148
+ const handler = () => setIsFullscreen(!!document.fullscreenElement);
149
+ document.addEventListener("fullscreenchange", handler);
150
+ return () => document.removeEventListener("fullscreenchange", handler);
151
+ }, []);
152
+ useEffect(() => {
153
+ return () => clearTimeout(indicatorTimerRef.current);
154
+ }, []);
155
+ const timelineValue = draggingTime ?? currentTime;
156
+ return /* @__PURE__ */ jsx("figure", { className: "rr-video-root", children: /* @__PURE__ */ jsxs("div", { className: "rr-video-player", ref: wrapperRef, style: { aspectRatio }, children: [
157
+ /* @__PURE__ */ jsx(
158
+ "video",
159
+ {
160
+ ref: videoRef,
161
+ className: "rr-video-element",
162
+ src,
163
+ poster,
164
+ preload: "metadata",
165
+ playsInline: true,
166
+ onClick: togglePlay,
167
+ onDoubleClick: toggleFullscreen
168
+ }
169
+ ),
170
+ indicator && /* @__PURE__ */ jsx("span", { className: "rr-video-indicator", "aria-hidden": true, children: indicator.type === "play" ? /* @__PURE__ */ jsx(PlayIcon, { size: 32 }) : /* @__PURE__ */ jsx(PauseIcon, { size: 32 }) }, indicator.key),
171
+ /* @__PURE__ */ jsxs("div", { className: "rr-video-controls", onClick: (e) => e.stopPropagation(), children: [
172
+ /* @__PURE__ */ jsx(
173
+ "button",
174
+ {
175
+ type: "button",
176
+ className: "rr-video-btn",
177
+ "aria-label": playing ? "Pause" : "Play",
178
+ onClick: togglePlay,
179
+ children: playing ? /* @__PURE__ */ jsx(PauseIcon, {}) : /* @__PURE__ */ jsx(PlayIcon, {})
180
+ }
181
+ ),
182
+ /* @__PURE__ */ jsx(
183
+ Slider.Root,
184
+ {
185
+ className: "rr-video-progress",
186
+ min: 0,
187
+ max: duration || 0,
188
+ step: 0.01,
189
+ value: timelineValue,
190
+ onValueChange: (value) => {
191
+ setDraggingTime(value);
192
+ },
193
+ onValueCommitted: (value) => {
194
+ seekTo(value);
195
+ setDraggingTime(null);
196
+ if (dragWasPlayingRef.current) {
197
+ play();
198
+ }
199
+ },
200
+ children: /* @__PURE__ */ jsx(
201
+ Slider.Control,
202
+ {
203
+ className: "rr-video-progress-control",
204
+ onPointerDown: () => {
205
+ dragWasPlayingRef.current = playing;
206
+ pause();
207
+ setDraggingTime(currentTime);
208
+ },
209
+ children: /* @__PURE__ */ jsxs(Slider.Track, { className: "rr-video-progress-track", children: [
210
+ /* @__PURE__ */ jsx(Slider.Indicator, { className: "rr-video-progress-range" }),
211
+ /* @__PURE__ */ jsx(
212
+ Slider.Thumb,
213
+ {
214
+ className: "rr-video-progress-thumb",
215
+ "aria-label": "Playback progress"
216
+ }
217
+ )
218
+ ] })
219
+ }
220
+ )
221
+ }
222
+ ),
223
+ /* @__PURE__ */ jsx(
224
+ "button",
225
+ {
226
+ type: "button",
227
+ className: "rr-video-btn",
228
+ "aria-label": muted ? "Unmute" : "Mute",
229
+ onClick: toggleMute,
230
+ children: muted ? /* @__PURE__ */ jsx(VolumeMuteIcon, {}) : /* @__PURE__ */ jsx(VolumeIcon, {})
231
+ }
232
+ ),
233
+ /* @__PURE__ */ jsx(
234
+ "button",
235
+ {
236
+ type: "button",
237
+ className: "rr-video-btn",
238
+ "aria-label": "Download",
239
+ onClick: downloadVideo,
240
+ children: isDownloading ? /* @__PURE__ */ jsx(LoadingIcon, {}) : /* @__PURE__ */ jsx(DownloadIcon, {})
241
+ }
242
+ ),
243
+ /* @__PURE__ */ jsx(
244
+ "button",
245
+ {
246
+ type: "button",
247
+ className: "rr-video-btn",
248
+ "aria-label": isFullscreen ? "Exit fullscreen" : "Fullscreen",
249
+ onClick: toggleFullscreen,
250
+ children: isFullscreen ? /* @__PURE__ */ jsx(ExitFullscreenIcon, {}) : /* @__PURE__ */ jsx(FullscreenIcon, {})
251
+ }
252
+ )
253
+ ] })
254
+ ] }) });
255
+ };
256
+ function VideoEditRenderer(props) {
257
+ const mode = useRendererMode();
258
+ if (mode !== "editor") {
259
+ return /* @__PURE__ */ jsx(VideoRenderer, { ...props });
260
+ }
261
+ return /* @__PURE__ */ jsx(VideoEditRendererInner, { ...props });
262
+ }
263
+ function VideoEditRendererInner({
264
+ src,
265
+ poster,
266
+ width,
267
+ height
268
+ }) {
269
+ const [editor] = useLexicalComposerContext();
270
+ const editable = editor.isEditable();
271
+ const wrapperRef = useRef(null);
272
+ const srcInputRef = useRef(null);
273
+ const [open, setOpen] = useState(!src);
274
+ const [editSrc, setEditSrc] = useState(src);
275
+ const [editPoster, setEditPoster] = useState(poster || "");
276
+ useEffect(() => {
277
+ setEditSrc(src);
278
+ setEditPoster(poster || "");
279
+ }, [src, poster]);
280
+ useEffect(() => {
281
+ if (!src) setOpen(true);
282
+ }, [src]);
283
+ useEffect(() => {
284
+ if (open) {
285
+ requestAnimationFrame(() => srcInputRef.current?.focus());
286
+ }
287
+ }, [open]);
288
+ const commitChanges = useCallback(() => {
289
+ const trimmedSrc = editSrc.trim();
290
+ if (!trimmedSrc) return;
291
+ if (/^(?:javascript\s*:|vbscript\s*:|data\s*:(?!video\/))/i.test(trimmedSrc))
292
+ return;
293
+ if (!wrapperRef.current) return;
294
+ editor.update(() => {
295
+ const node = $getNearestNodeFromDOMNode(wrapperRef.current);
296
+ if (!node) return;
297
+ const writable = node.getWritable();
298
+ writable.__src = trimmedSrc;
299
+ writable.__poster = editPoster.trim() || void 0;
300
+ });
301
+ setOpen(false);
302
+ }, [editor, editSrc, editPoster]);
303
+ const handleDelete = useCallback(() => {
304
+ if (!wrapperRef.current) return;
305
+ editor.update(() => {
306
+ const node = $getNearestNodeFromDOMNode(wrapperRef.current);
307
+ if (node) node.remove();
308
+ });
309
+ setOpen(false);
310
+ }, [editor]);
311
+ const handleOpen = useCallback(() => {
312
+ if (src) window.open(src, "_blank", "noopener,noreferrer");
313
+ }, [src]);
314
+ const handleKeyDown = useCallback(
315
+ (e) => {
316
+ if (e.key === "Enter") {
317
+ e.preventDefault();
318
+ commitChanges();
319
+ } else if (e.key === "Escape") {
320
+ e.preventDefault();
321
+ setEditSrc(src);
322
+ setEditPoster(poster || "");
323
+ setOpen(false);
324
+ }
325
+ },
326
+ [commitChanges, src, poster]
327
+ );
328
+ if (!editable) {
329
+ return /* @__PURE__ */ jsx(VideoRenderer, { src, poster, width, height });
330
+ }
331
+ const aspectRatio = width && height && width > 0 && height > 0 ? `${width} / ${height}` : "16 / 9";
332
+ return /* @__PURE__ */ jsxs(
333
+ Popover,
334
+ {
335
+ open,
336
+ onOpenChange: (nextOpen) => {
337
+ setOpen(nextOpen);
338
+ if (!nextOpen) {
339
+ setEditSrc(src);
340
+ setEditPoster(poster || "");
341
+ }
342
+ },
343
+ children: [
344
+ /* @__PURE__ */ jsx(
345
+ PopoverTrigger,
346
+ {
347
+ render: /* @__PURE__ */ jsx("div", { ref: wrapperRef, className: "rr-video-edit-trigger" }),
348
+ children: src ? /* @__PURE__ */ jsx("figure", { className: "rr-video-root", children: /* @__PURE__ */ jsxs(
349
+ "div",
350
+ {
351
+ className: "rr-video-edit-preview",
352
+ style: {
353
+ aspectRatio,
354
+ backgroundImage: poster ? `url(${poster})` : void 0
355
+ },
356
+ children: [
357
+ /* @__PURE__ */ jsx(
358
+ "video",
359
+ {
360
+ className: "rr-video-edit-video",
361
+ src,
362
+ poster,
363
+ preload: "metadata"
364
+ }
365
+ ),
366
+ /* @__PURE__ */ jsx("span", { className: "rr-video-edit-overlay", children: /* @__PURE__ */ jsx(Video, { size: 32 }) })
367
+ ]
368
+ }
369
+ ) }) : /* @__PURE__ */ jsxs("div", { className: "rr-video-edit-placeholder", children: [
370
+ /* @__PURE__ */ jsx(Video, { size: 24 }),
371
+ /* @__PURE__ */ jsx("span", { children: "Click to add video" })
372
+ ] })
373
+ }
374
+ ),
375
+ /* @__PURE__ */ jsxs(
376
+ PopoverPanel,
377
+ {
378
+ side: "bottom",
379
+ sideOffset: 8,
380
+ className: "rr-video-edit-panel",
381
+ children: [
382
+ /* @__PURE__ */ jsxs("div", { className: "rr-video-edit-field", children: [
383
+ /* @__PURE__ */ jsx(Video, { className: "rr-video-edit-field-icon", size: 14 }),
384
+ /* @__PURE__ */ jsx(
385
+ "input",
386
+ {
387
+ ref: srcInputRef,
388
+ className: "rr-video-edit-input",
389
+ type: "url",
390
+ value: editSrc,
391
+ onChange: (e) => setEditSrc(e.target.value),
392
+ onKeyDown: handleKeyDown,
393
+ placeholder: "Video URL"
394
+ }
395
+ )
396
+ ] }),
397
+ /* @__PURE__ */ jsxs("div", { className: "rr-video-edit-field", children: [
398
+ /* @__PURE__ */ jsx(ImageIcon, { className: "rr-video-edit-field-icon", size: 14 }),
399
+ /* @__PURE__ */ jsx(
400
+ "input",
401
+ {
402
+ className: "rr-video-edit-input",
403
+ type: "url",
404
+ value: editPoster,
405
+ onChange: (e) => setEditPoster(e.target.value),
406
+ onBlur: commitChanges,
407
+ onKeyDown: handleKeyDown,
408
+ placeholder: "Poster URL (optional)"
409
+ }
410
+ )
411
+ ] }),
412
+ /* @__PURE__ */ jsxs("div", { className: "rr-video-edit-actions", children: [
413
+ /* @__PURE__ */ jsxs(
414
+ "button",
415
+ {
416
+ className: "rr-video-edit-action-btn",
417
+ type: "button",
418
+ onClick: handleOpen,
419
+ children: [
420
+ /* @__PURE__ */ jsx(ExternalLink, { size: 14 }),
421
+ "Open"
422
+ ]
423
+ }
424
+ ),
425
+ /* @__PURE__ */ jsxs(
426
+ "button",
427
+ {
428
+ className: "rr-video-edit-action-btn rr-video-edit-action-btn--end",
429
+ type: "button",
430
+ onClick: handleDelete,
431
+ children: [
432
+ /* @__PURE__ */ jsx(Trash2, { size: 14 }),
433
+ "Remove"
434
+ ]
435
+ }
436
+ )
437
+ ] })
438
+ ]
439
+ }
440
+ )
441
+ ]
442
+ }
443
+ );
444
+ }
445
+ export {
446
+ VideoEditRenderer,
447
+ VideoRenderer,
448
+ VideoRenderer as default
449
+ };
@@ -0,0 +1,266 @@
1
+ @keyframes _1x2h1ol0 {
2
+ 0% {
3
+ opacity: 1;
4
+ transform: scale(1);
5
+ }
6
+ 100% {
7
+ opacity: 0;
8
+ transform: scale(1.3);
9
+ }
10
+ }
11
+ @keyframes _1x2h1ol1 {
12
+ from {
13
+ transform: rotate(0deg);
14
+ }
15
+ to {
16
+ transform: rotate(360deg);
17
+ }
18
+ }
19
+ .rr-video-root {
20
+ margin: 1.25rem 0;
21
+ }
22
+ .rr-video-player {
23
+ position: relative;
24
+ width: 100%;
25
+ background: #000;
26
+ border-radius: 0.75rem;
27
+ overflow: hidden;
28
+ }
29
+ .rr-video-element {
30
+ width: 100%;
31
+ height: 100%;
32
+ display: block;
33
+ object-fit: contain;
34
+ background: #000;
35
+ cursor: pointer;
36
+ }
37
+ .rr-video-indicator {
38
+ position: absolute;
39
+ inset: 0;
40
+ margin: auto;
41
+ width: 5rem;
42
+ height: 5rem;
43
+ border-radius: 999px;
44
+ display: inline-flex;
45
+ align-items: center;
46
+ justify-content: center;
47
+ background: rgba(0, 0, 0, 0.7);
48
+ color: #fff;
49
+ pointer-events: none;
50
+ animation: _1x2h1ol0 0.5s ease forwards;
51
+ }
52
+ .rr-video-controls {
53
+ position: absolute;
54
+ left: 2rem;
55
+ right: 2rem;
56
+ bottom: 0.5rem;
57
+ height: 2.5rem;
58
+ border-radius: 999px;
59
+ border: 1px solid color-mix(in srgb, var(--rc-border) 20%, transparent);
60
+ background: color-mix(in srgb, var(--rc-bg-secondary) 90%, transparent);
61
+ backdrop-filter: blur(24px) saturate(180%);
62
+ color: var(--rc-text);
63
+ display: flex;
64
+ align-items: center;
65
+ gap: 0.75rem;
66
+ padding: 0 1rem;
67
+ max-width: 80vw;
68
+ margin: 0 auto;
69
+ opacity: 0;
70
+ transform: translateY(4px);
71
+ transition: opacity 0.2s ease, transform 0.2s ease;
72
+ }
73
+ .rr-video-player:hover .rr-video-controls, .rr-video-player:focus-within .rr-video-controls {
74
+ opacity: 1;
75
+ transform: translateY(0);
76
+ }
77
+ .rr-video-btn {
78
+ appearance: none;
79
+ border: none;
80
+ background: none;
81
+ color: inherit;
82
+ display: inline-flex;
83
+ align-items: center;
84
+ justify-content: center;
85
+ cursor: pointer;
86
+ padding: 0.125rem;
87
+ line-height: 1;
88
+ flex-shrink: 0;
89
+ transition: transform 0.15s ease;
90
+ }
91
+ .rr-video-btn:hover {
92
+ transform: scale(1.1);
93
+ }
94
+ .rr-video-btn:active {
95
+ transform: scale(0.93);
96
+ }
97
+ .rr-video-btn:focus-visible {
98
+ outline: 2px solid currentColor;
99
+ outline-offset: 2px;
100
+ border-radius: 4px;
101
+ }
102
+ .rr-video-btn:disabled {
103
+ opacity: 0.5;
104
+ pointer-events: none;
105
+ }
106
+ .rr-video-progress {
107
+ flex: 1;
108
+ height: 100%;
109
+ }
110
+ .rr-video-progress-control {
111
+ position: relative;
112
+ display: flex;
113
+ align-items: center;
114
+ width: 100%;
115
+ height: 100%;
116
+ touch-action: none;
117
+ user-select: none;
118
+ }
119
+ .rr-video-progress-track {
120
+ position: relative;
121
+ flex: 1;
122
+ height: 0.25rem;
123
+ border-radius: 999px;
124
+ background: var(--rc-bg);
125
+ }
126
+ .rr-video-progress-range {
127
+ position: absolute;
128
+ height: 100%;
129
+ border-radius: 999px;
130
+ background: color-mix(in srgb, var(--rc-text-secondary) 40%, transparent);
131
+ }
132
+ .rr-video-progress-thumb {
133
+ display: block;
134
+ height: 0.75rem;
135
+ width: 3px;
136
+ border-radius: 1px;
137
+ border: none;
138
+ background: var(--rc-text-secondary);
139
+ }
140
+ .rr-video-spin {
141
+ animation: _1x2h1ol1 0.8s linear infinite;
142
+ }
143
+ .rr-video-edit-trigger {
144
+ display: block;
145
+ cursor: pointer;
146
+ }
147
+ .rr-video-edit-preview {
148
+ position: relative;
149
+ width: 100%;
150
+ border-radius: 0.75rem;
151
+ overflow: hidden;
152
+ background: #000;
153
+ background-size: cover;
154
+ background-position: center;
155
+ cursor: pointer;
156
+ }
157
+ .rr-video-edit-video {
158
+ width: 100%;
159
+ height: 100%;
160
+ display: block;
161
+ object-fit: contain;
162
+ pointer-events: none;
163
+ }
164
+ .rr-video-edit-overlay {
165
+ position: absolute;
166
+ inset: 0;
167
+ display: flex;
168
+ align-items: center;
169
+ justify-content: center;
170
+ background: rgba(0, 0, 0, 0.35);
171
+ color: #fff;
172
+ transition: background 0.2s;
173
+ }
174
+ .rr-video-edit-trigger:hover .rr-video-edit-overlay {
175
+ background: rgba(0, 0, 0, 0.5);
176
+ }
177
+ .rr-video-edit-placeholder {
178
+ display: flex;
179
+ align-items: center;
180
+ justify-content: center;
181
+ gap: 8px;
182
+ padding: 2rem;
183
+ border: 2px dashed var(--rc-border);
184
+ border-radius: var(--rc-radius-md);
185
+ color: var(--rc-text-secondary);
186
+ font-size: 0.875rem;
187
+ cursor: pointer;
188
+ transition: border-color 0.2s, color 0.2s;
189
+ }
190
+ .rr-video-edit-placeholder:hover {
191
+ border-color: var(--rc-accent);
192
+ color: var(--rc-text);
193
+ }
194
+ .rr-video-edit-panel {
195
+ display: flex;
196
+ flex-direction: column;
197
+ gap: 8px;
198
+ width: 340px;
199
+ padding: 12px;
200
+ font-size: 13px;
201
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
202
+ }
203
+ .rr-video-edit-field {
204
+ display: flex;
205
+ align-items: center;
206
+ gap: 8px;
207
+ padding: 6px 10px;
208
+ background-color: var(--rc-bg-secondary);
209
+ border-radius: 6px;
210
+ min-width: 0;
211
+ }
212
+ .rr-video-edit-field-icon {
213
+ flex-shrink: 0;
214
+ color: var(--rc-text-secondary);
215
+ }
216
+ .rr-video-edit-input {
217
+ flex: 1;
218
+ appearance: none;
219
+ border: none;
220
+ background-color: transparent;
221
+ color: inherit;
222
+ font-size: 13px;
223
+ padding: 0;
224
+ outline: none;
225
+ min-width: 0;
226
+ }
227
+ .rr-video-edit-input::placeholder {
228
+ color: var(--rc-text-secondary);
229
+ }
230
+ .rr-video-edit-actions {
231
+ display: flex;
232
+ align-items: center;
233
+ gap: 4px;
234
+ }
235
+ .rr-video-edit-action-btn {
236
+ display: inline-flex;
237
+ align-items: center;
238
+ gap: 6px;
239
+ appearance: none;
240
+ border: none;
241
+ background: none;
242
+ color: inherit;
243
+ font-size: 13px;
244
+ font-weight: 500;
245
+ cursor: pointer;
246
+ padding: 4px 8px;
247
+ border-radius: 4px;
248
+ transition: color 0.15s ease, background-color 0.15s ease;
249
+ white-space: nowrap;
250
+ }
251
+ .rr-video-edit-action-btn:hover {
252
+ background-color: var(--rc-bg-secondary);
253
+ }
254
+ .rr-video-edit-action-btn--end {
255
+ margin-left: auto;
256
+ }
257
+ @media (max-width: 768px) {
258
+ .rr-video-controls {
259
+ left: 0.75rem;
260
+ right: 0.75rem;
261
+ opacity: 1;
262
+ transform: none;
263
+ gap: 0.5rem;
264
+ padding: 0 0.75rem;
265
+ }
266
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=styles.css.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styles.css.d.ts","sourceRoot":"","sources":["../src/styles.css.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@haklex/rich-renderer-video",
3
+ "version": "0.0.1",
4
+ "description": "Video player renderer",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.mjs",
10
+ "types": "./dist/index.d.ts"
11
+ },
12
+ "./style.css": "./dist/rich-renderer-video.css"
13
+ },
14
+ "main": "./dist/index.mjs",
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "dependencies": {
19
+ "@base-ui/react": "^1.1.0",
20
+ "lucide-react": "^0.574.0"
21
+ },
22
+ "devDependencies": {
23
+ "@lexical/react": "^0.39.0",
24
+ "@types/react": "^19.0.0",
25
+ "@types/react-dom": "^19.0.0",
26
+ "@vanilla-extract/css": "^1.17.1",
27
+ "@vanilla-extract/vite-plugin": "^4.0.20",
28
+ "lexical": "^0.39.0",
29
+ "react": "19.2.4",
30
+ "react-dom": "19.2.4",
31
+ "typescript": "^5.9.0",
32
+ "vite": "^7.3.1",
33
+ "vite-plugin-dts": "^4.5.0",
34
+ "@haklex/rich-editor-ui": "0.0.1",
35
+ "@haklex/rich-editor": "0.0.1",
36
+ "@haklex/rich-style-token": "0.0.1"
37
+ },
38
+ "peerDependencies": {
39
+ "@lexical/react": "^0.39.0",
40
+ "lexical": "^0.39.0",
41
+ "react": ">=19",
42
+ "react-dom": ">=19",
43
+ "@haklex/rich-editor": "0.0.1",
44
+ "@haklex/rich-style-token": "0.0.1",
45
+ "@haklex/rich-editor-ui": "0.0.1"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "scripts": {
51
+ "build": "vite build",
52
+ "dev:build": "vite build --watch"
53
+ },
54
+ "types": "./dist/index.d.ts"
55
+ }