@linktr.ee/messaging-react 1.32.0 → 1.32.1-rc-1777007852

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 (30) hide show
  1. package/dist/Card-1CQEn-OT.js +171 -0
  2. package/dist/Card-1CQEn-OT.js.map +1 -0
  3. package/dist/Card-ClE_iExA.js +177 -0
  4. package/dist/Card-ClE_iExA.js.map +1 -0
  5. package/dist/{MediaPlayer-BCsdmsON.js → MediaPlayer-B9Ws2NeE.js} +115 -135
  6. package/dist/MediaPlayer-B9Ws2NeE.js.map +1 -0
  7. package/dist/index.d.ts +3 -2
  8. package/dist/index.js +1 -1
  9. package/dist/index.js.map +1 -1
  10. package/package.json +1 -1
  11. package/src/components/LockedAttachment/LockedAttachment.stories.tsx +136 -93
  12. package/src/components/LockedAttachment/components/Creator/Card.tsx +106 -106
  13. package/src/components/LockedAttachment/components/Creator/CardThumbnail.tsx +114 -0
  14. package/src/components/LockedAttachment/components/MediaPlayer.tsx +80 -66
  15. package/src/components/LockedAttachment/components/Visitor/Card.tsx +53 -78
  16. package/src/components/LockedAttachment/components/Visitor/CardActions.tsx +3 -3
  17. package/src/components/LockedAttachment/components/Visitor/CardThumbnail.tsx +81 -0
  18. package/src/components/LockedAttachment/types.ts +2 -0
  19. package/dist/Card-C5t3dZ5q.js +0 -350
  20. package/dist/Card-C5t3dZ5q.js.map +0 -1
  21. package/dist/Card-Cn2va-Qr.js +0 -205
  22. package/dist/Card-Cn2va-Qr.js.map +0 -1
  23. package/dist/MediaPlayer-BCsdmsON.js.map +0 -1
  24. package/src/components/LockedAttachment/components/Creator/CardAudioPreview.tsx +0 -161
  25. package/src/components/LockedAttachment/components/Creator/CardCollapsedThumbnail.tsx +0 -58
  26. package/src/components/LockedAttachment/components/Creator/CardImagePreview.tsx +0 -56
  27. package/src/components/LockedAttachment/components/Creator/CardVideoPreview.tsx +0 -91
  28. package/src/components/LockedAttachment/components/Visitor/CardImagePreview.tsx +0 -39
  29. package/src/components/LockedAttachment/components/Visitor/CardMediaPreview.tsx +0 -36
  30. package/src/components/LockedAttachment/components/Visitor/CardThumbnailPreview.tsx +0 -45
@@ -1 +0,0 @@
1
- {"version":3,"file":"MediaPlayer-BCsdmsON.js","sources":["../src/components/LockedAttachment/utils/mimeType.ts","../src/components/LockedAttachment/utils/icons.ts","../src/components/LockedAttachment/components/MediaPlayer.tsx"],"sourcesContent":["export type AttachmentSourceType = 'image' | 'audio' | 'video' | 'document'\n\nexport type DocumentIconType =\n | 'pdf'\n | 'doc'\n | 'xls'\n | 'csv'\n | 'ppt'\n | 'zip'\n | 'text'\n | 'markdown'\n | 'generic'\n\nconst DOCUMENT_ICON_PATTERNS: Array<[RegExp, DocumentIconType]> = [\n [/pdf/, 'pdf'],\n [/wordprocessingml|msword|\\.doc/, 'doc'],\n [/spreadsheetml|ms-excel|\\.xls/, 'xls'],\n [/csv/, 'csv'],\n [/presentationml|ms-powerpoint|\\.ppt/, 'ppt'],\n [/zip|x-rar|x-7z|x-tar|x-gzip/, 'zip'],\n [/plain|rtf/, 'text'],\n [/markdown/, 'markdown'],\n]\n\nexport function getSourceType(mimeType: string): AttachmentSourceType {\n if (mimeType.startsWith('video/')) return 'video'\n if (mimeType.startsWith('audio/')) return 'audio'\n if (mimeType.startsWith('image/')) return 'image'\n return 'document'\n}\n\nexport function getDocumentIconType(mimeType: string): DocumentIconType {\n const match = DOCUMENT_ICON_PATTERNS.find(([pattern]) =>\n pattern.test(mimeType)\n )\n return match ? match[1] : 'generic'\n}\n","import {\n FileIcon,\n FileCsvIcon,\n FileDocIcon,\n FileMdIcon,\n FilePdfIcon,\n FilePptIcon,\n FileTextIcon,\n FileXlsIcon,\n FileZipIcon,\n ImageIcon,\n SpeakerHighIcon,\n VideoCameraIcon,\n IconProps,\n} from '@phosphor-icons/react'\nimport React from 'react'\n\nimport { getDocumentIconType, getSourceType } from './mimeType'\nimport type { AttachmentSourceType } from './mimeType'\n\nexport const MEDIA_TYPE_ICON: Record<AttachmentSourceType, React.ElementType> =\n {\n video: VideoCameraIcon,\n audio: SpeakerHighIcon,\n image: ImageIcon,\n document: FileIcon,\n }\n\nconst DOCUMENT_ICON_COMPONENT = {\n pdf: FilePdfIcon,\n doc: FileDocIcon,\n xls: FileXlsIcon,\n csv: FileCsvIcon,\n ppt: FilePptIcon,\n zip: FileZipIcon,\n text: FileTextIcon,\n markdown: FileMdIcon,\n generic: FileIcon,\n} as const\n\nexport function getTypeIcon(mimeType: string): React.ElementType {\n const sourceType = getSourceType(mimeType)\n if (sourceType !== 'document') return MEDIA_TYPE_ICON[sourceType]\n return DOCUMENT_ICON_COMPONENT[getDocumentIconType(mimeType)]\n}\n\n/** Use instead of `<TypeIcon />` where TypeIcon = getTypeIcon(mime) to satisfy react-hooks/static-components. */\nexport function renderTypeIcon(\n mimeType: string,\n props: IconProps\n): React.ReactElement {\n return React.createElement(getTypeIcon(mimeType), props)\n}\n","import { CircleNotchIcon, PauseIcon, PlayIcon } from '@phosphor-icons/react'\nimport React, { useCallback, useEffect, useRef, useState } from 'react'\n\nimport { isDevBuild } from '../../../utils/isDevBuild'\nimport { renderTypeIcon } from '../utils/icons'\nimport { getSourceType, type AttachmentSourceType } from '../utils/mimeType'\n\nconst getPlayerBg = (sourceType: AttachmentSourceType, poster?: string) => {\n return sourceType === 'audio' && !poster ? 'bg-black/5' : 'bg-black'\n}\n\nconst getClientXFromEvent = ( e: MouseEvent | TouchEvent | React.MouseEvent | React.TouchEvent ): number => {\n if ('touches' in e) {\n return e.touches[0]?.clientX ?? e.changedTouches[0]?.clientX ?? 0\n }\n return e.clientX\n}\n\nexport interface MediaPlayerProps {\n source: string\n mimeType: string\n poster?: string\n autoPlay?: boolean\n /** Controlled playing state. When provided, syncs to internal play/pause. */\n playing?: boolean\n loop?: boolean\n controls?: boolean\n showProgress?: boolean\n onContainerClick?: () => void\n /** When true, requests muted playback (helps autoplay policies on video). */\n muted?: boolean\n}\n\nconst MediaPlayer: React.FC<MediaPlayerProps> = ({\n source,\n mimeType,\n poster,\n autoPlay = false,\n playing: playingProp,\n loop = false,\n controls = true,\n showProgress = false,\n onContainerClick,\n muted = false,\n}) => {\n const sourceType = getSourceType(mimeType)\n const [playing, setPlaying] = useState(autoPlay)\n\n // Sync controlled playing prop to internal state\n const prevPlayingPropRef = useRef(playingProp)\n useEffect(() => {\n if (\n playingProp !== undefined &&\n playingProp !== prevPlayingPropRef.current\n ) {\n prevPlayingPropRef.current = playingProp\n setPlaying(playingProp)\n }\n }, [playingProp])\n const [played, setPlayed] = useState(0)\n const [seeking, setSeeking] = useState(false)\n const [scrubberHovered, setScrubberHovered] = useState(false)\n const [videoAspect, setVideoAspect] = useState<number | null>(null)\n const [buffering, setBuffering] = useState(false)\n /** True until the first canPlay fires for the current source — hides controls/spinner behind poster. */\n const [initialLoad, setInitialLoad] = useState(true)\n /** Set when autoplay/play() was rejected so user can start via gesture (no controls UI). */\n const [manualPlayRequired, setManualPlayRequired] = useState(false)\n const playerRef = useRef<HTMLMediaElement>(null)\n const trackRef = useRef<HTMLDivElement>(null)\n const rafRef = useRef<number | null>(null)\n\n\n useEffect(() => {\n if (!playing) {\n if (rafRef.current !== null) {\n cancelAnimationFrame(rafRef.current)\n rafRef.current = null\n }\n return\n }\n const tick = () => {\n const el = playerRef.current\n if (el && el.duration && !seeking) setPlayed(el.currentTime / el.duration)\n rafRef.current = requestAnimationFrame(tick)\n }\n rafRef.current = requestAnimationFrame(tick)\n return () => {\n if (rafRef.current !== null) cancelAnimationFrame(rafRef.current)\n }\n }, [playing, seeking])\n\n // ReactPlayer v3 uses native HTML media elements and does not support a\n // declarative `playing` prop — playback must be driven imperatively.\n useEffect(() => {\n const el = playerRef.current\n if (!el) return\n if (playing) {\n void el.play().catch((err) => {\n setPlaying(false)\n setManualPlayRequired(true)\n if (isDevBuild()) {\n console.debug('[MediaPlayer] play() failed', err)\n }\n })\n } else {\n el.pause()\n }\n }, [playing])\n\n const startPlaybackFromGesture = useCallback(() => {\n setManualPlayRequired(false)\n setPlaying(true)\n }, [])\n\n const getFraction = useCallback(\n (e: MouseEvent | TouchEvent | React.MouseEvent | React.TouchEvent) => {\n const track = trackRef.current\n if (!track) return 0\n const rect = track.getBoundingClientRect()\n return Math.max(\n 0,\n Math.min(1, (getClientXFromEvent(e) - rect.left) / rect.width)\n )\n },\n []\n )\n\n const seekTo = useCallback((fraction: number) => {\n const el = playerRef.current\n if (el && el.duration) el.currentTime = fraction * el.duration\n }, [])\n\n const handleTrackPointerDown = (\n e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>\n ) => {\n e.stopPropagation()\n setSeeking(true)\n const fraction = getFraction(e)\n setPlayed(fraction)\n seekTo(fraction)\n }\n\n useEffect(() => {\n if (!seeking) return\n const onMove = (e: MouseEvent | TouchEvent) => setPlayed(getFraction(e))\n const onUp = (e: MouseEvent | TouchEvent) => {\n setSeeking(false)\n seekTo(getFraction(e))\n }\n window.addEventListener('mousemove', onMove)\n window.addEventListener('mouseup', onUp)\n window.addEventListener('touchmove', onMove, { passive: true })\n window.addEventListener('touchend', onUp)\n return () => {\n window.removeEventListener('mousemove', onMove)\n window.removeEventListener('mouseup', onUp)\n window.removeEventListener('touchmove', onMove)\n window.removeEventListener('touchend', onUp)\n }\n }, [seeking, getFraction, seekTo])\n\n // Use natural aspect ratio once metadata loads, fall back to 16:9 before then.\n const aspectStyle = videoAspect\n ? { aspectRatio: String(videoAspect) }\n : undefined\n const aspectClass = !videoAspect ? ' aspect-video' : ''\n const scrubberPercent = Math.round(played * 100)\n\n return (\n <div\n role=\"button\"\n tabIndex={0}\n className={`relative cursor-pointer overflow-hidden ${getPlayerBg(sourceType, poster)}${aspectClass}`}\n style={aspectStyle}\n onClick={() => {\n if (manualPlayRequired) return\n if (onContainerClick) {\n onContainerClick()\n return\n }\n if (controls) setPlaying((p) => !p)\n }}\n onKeyDown={(e) => {\n if (e.key !== 'Enter' && e.key !== ' ') return\n e.preventDefault()\n if (manualPlayRequired) return\n if (onContainerClick) {\n onContainerClick()\n return\n }\n if (controls) setPlaying((p) => !p)\n }}\n >\n {/* For audio, poster persists as a visual background. For video, hide once loaded. */}\n {poster && (sourceType === 'audio' || initialLoad) && (\n <img\n src={poster}\n alt=\"\"\n className=\"absolute inset-0 h-full w-full object-cover\"\n />\n )}\n {!poster && (sourceType === 'audio' || initialLoad) && (\n <div className=\"absolute inset-0 flex items-center justify-center\">\n {renderTypeIcon(mimeType, {\n className: 'size-12 text-black/20',\n weight: 'regular',\n })}\n </div>\n )}\n <div className=\"absolute inset-0\">\n {sourceType === 'audio' ? (\n <audio\n ref={playerRef as React.RefObject<HTMLAudioElement>}\n src={source}\n loop={loop}\n muted={muted}\n style={{ width: '100%', height: '100%' }}\n onLoadStart={() => setBuffering(true)}\n onCanPlay={() => { setBuffering(false); setInitialLoad(false) }}\n onWaiting={() => setBuffering(true)}\n onPlay={() => setManualPlayRequired(false)}\n onEnded={() => {\n if (!loop) {\n setPlaying(false)\n setPlayed(0)\n }\n }}\n >\n <track kind=\"captions\" />\n </audio>\n ) : (\n <video\n ref={playerRef as React.RefObject<HTMLVideoElement>}\n src={source}\n loop={loop}\n muted={muted}\n playsInline\n style={{ width: '100%', height: '100%' }}\n onLoadStart={() => setBuffering(true)}\n onCanPlay={() => { setBuffering(false); setInitialLoad(false) }}\n onWaiting={() => setBuffering(true)}\n onPlay={() => setManualPlayRequired(false)}\n onLoadedMetadata={() => {\n const el = playerRef.current\n if (el instanceof HTMLVideoElement && el.videoWidth && el.videoHeight) {\n setVideoAspect(el.videoWidth / el.videoHeight)\n }\n }}\n onEnded={() => {\n if (!loop) {\n setPlaying(false)\n setPlayed(0)\n }\n }}\n >\n <track kind=\"captions\" />\n </video>\n )}\n </div>\n\n {buffering && !manualPlayRequired && (\n <div className=\"absolute inset-0 z-10 flex items-center justify-center\">\n <CircleNotchIcon\n className=\"size-8 animate-spin text-white/80\"\n weight=\"bold\"\n />\n </div>\n )}\n\n {manualPlayRequired && !controls && (\n <div\n className=\"absolute inset-0 z-30 flex cursor-pointer items-center justify-center bg-black/35\"\n role=\"button\"\n tabIndex={0}\n aria-label=\"Play preview\"\n onClick={(e) => {\n e.stopPropagation()\n startPlaybackFromGesture()\n }}\n onKeyDown={(e) => {\n if (e.key !== 'Enter' && e.key !== ' ') return\n e.preventDefault()\n e.stopPropagation()\n startPlaybackFromGesture()\n }}\n >\n <span className=\"flex size-16 items-center justify-center rounded-full bg-white/20 text-white backdrop-blur-sm\">\n <PlayIcon className=\"size-9 translate-x-0.5\" weight=\"fill\" />\n </span>\n </div>\n )}\n\n {showProgress && !controls && (\n <div className=\"absolute inset-x-0 bottom-0 px-3 pb-2.5 pt-6 bg-gradient-to-t from-black/40 to-transparent\">\n <div\n role=\"slider\"\n aria-label=\"Playback position\"\n aria-valuenow={scrubberPercent}\n aria-valuemin={0}\n aria-valuemax={100}\n tabIndex={0}\n ref={trackRef}\n className=\"relative flex h-4 w-full cursor-pointer items-center\"\n onMouseDown={handleTrackPointerDown}\n onTouchStart={handleTrackPointerDown}\n onClick={(e) => e.stopPropagation()}\n onKeyDown={(e) => {\n if (e.key === 'ArrowRight') seekTo(Math.min(1, played + 0.05))\n if (e.key === 'ArrowLeft') seekTo(Math.max(0, played - 0.05))\n }}\n >\n <div className=\"w-full overflow-hidden rounded-full bg-white/30 h-1\">\n <div\n className=\"h-full rounded-full bg-white\"\n style={{ width: `${scrubberPercent}%` }}\n />\n </div>\n </div>\n </div>\n )}\n\n {controls && (\n <div className=\"absolute inset-x-0 bottom-0 flex items-center gap-2 bg-gradient-to-t from-black/60 to-transparent px-3 pb-2.5 pt-6 transition-all duration-200\">\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n setPlaying((p) => !p)\n }}\n className=\"shrink-0 text-white\"\n aria-label={playing ? 'Pause' : 'Play'}\n >\n {playing ? (\n <PauseIcon className=\"size-5\" weight=\"fill\" />\n ) : (\n <PlayIcon className=\"size-5 translate-x-px\" weight=\"fill\" />\n )}\n </button>\n\n <div\n role=\"slider\"\n aria-label=\"Playback position\"\n aria-valuenow={scrubberPercent}\n aria-valuemin={0}\n aria-valuemax={100}\n tabIndex={0}\n ref={trackRef}\n className=\"relative flex h-4 w-full cursor-pointer items-center\"\n onMouseDown={handleTrackPointerDown}\n onTouchStart={handleTrackPointerDown}\n onClick={(e) => e.stopPropagation()}\n onMouseEnter={() => setScrubberHovered(true)}\n onMouseLeave={() => setScrubberHovered(false)}\n onKeyDown={(e) => {\n if (e.key === 'ArrowRight') seekTo(Math.min(1, played + 0.05))\n if (e.key === 'ArrowLeft') seekTo(Math.max(0, played - 0.05))\n }}\n >\n <div\n className={`w-full overflow-hidden rounded-full bg-white/30 transition-all duration-200 ${scrubberHovered || seeking ? 'h-1.5' : 'h-1'}`}\n >\n <div\n className=\"h-full rounded-full bg-white\"\n style={{ width: `${scrubberPercent}%` }}\n />\n </div>\n <div\n className={`absolute size-3 -translate-x-1/2 rounded-full bg-white shadow transition-[opacity,transform] duration-200 ${scrubberHovered || seeking ? 'scale-100 opacity-100' : 'scale-0 opacity-0'}`}\n style={{ left: `${scrubberPercent}%` }}\n />\n </div>\n </div>\n )}\n </div>\n )\n}\n\nexport default MediaPlayer\n"],"names":["DOCUMENT_ICON_PATTERNS","getSourceType","mimeType","getDocumentIconType","match","pattern","MEDIA_TYPE_ICON","VideoCameraIcon","SpeakerHighIcon","ImageIcon","FileIcon","DOCUMENT_ICON_COMPONENT","FilePdfIcon","FileDocIcon","FileXlsIcon","FileCsvIcon","FilePptIcon","FileZipIcon","FileTextIcon","FileMdIcon","getTypeIcon","sourceType","renderTypeIcon","props","React","getPlayerBg","poster","getClientXFromEvent","e","_a","_b","MediaPlayer","source","autoPlay","playingProp","loop","controls","showProgress","onContainerClick","muted","playing","setPlaying","useState","prevPlayingPropRef","useRef","useEffect","played","setPlayed","seeking","setSeeking","scrubberHovered","setScrubberHovered","videoAspect","setVideoAspect","buffering","setBuffering","initialLoad","setInitialLoad","manualPlayRequired","setManualPlayRequired","playerRef","trackRef","rafRef","tick","el","err","startPlaybackFromGesture","useCallback","getFraction","track","rect","seekTo","fraction","handleTrackPointerDown","onMove","onUp","aspectStyle","aspectClass","scrubberPercent","jsxs","p","jsx","CircleNotchIcon","PlayIcon","PauseIcon"],"mappings":"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"}
@@ -1,161 +0,0 @@
1
- import { PauseIcon, PlayIcon } from '@phosphor-icons/react'
2
- import React, { useCallback, useEffect, useRef, useState } from 'react'
3
-
4
- import CollapsedThumbnail from './CardCollapsedThumbnail'
5
-
6
- interface AudioPreviewProps {
7
- sourceUrl?: string
8
- thumbnailUrl?: string
9
- mimeType: string
10
- }
11
-
12
- const AudioPreview: React.FC<AudioPreviewProps> = (props) => {
13
- const { sourceUrl, thumbnailUrl, mimeType } = props
14
- const [playing, setPlaying] = useState(false)
15
- const [played, setPlayed] = useState(0)
16
- const [seeking, setSeeking] = useState(false)
17
- const audioRef = useRef<HTMLAudioElement>(null)
18
- const trackRef = useRef<HTMLDivElement>(null)
19
- const rafRef = useRef<number | null>(null)
20
-
21
- useEffect(() => {
22
- const el = audioRef.current
23
- if (!el) return
24
- if (playing) {
25
- void el.play().catch(() => setPlaying(false))
26
- } else {
27
- el.pause()
28
- }
29
- }, [playing])
30
-
31
- useEffect(() => {
32
- if (!playing) {
33
- if (rafRef.current !== null) cancelAnimationFrame(rafRef.current)
34
- return
35
- }
36
- const tick = () => {
37
- const el = audioRef.current
38
- if (el && el.duration && !seeking) setPlayed(el.currentTime / el.duration)
39
- rafRef.current = requestAnimationFrame(tick)
40
- }
41
- rafRef.current = requestAnimationFrame(tick)
42
- return () => {
43
- if (rafRef.current !== null) cancelAnimationFrame(rafRef.current)
44
- }
45
- }, [playing, seeking])
46
-
47
- const [audioReady, setAudioReady] = useState(false)
48
-
49
- const getFraction = useCallback(
50
- (e: MouseEvent | TouchEvent | React.MouseEvent | React.TouchEvent) => {
51
- const track = trackRef.current
52
- if (!track) return 0
53
- const clientX =
54
- 'touches' in e
55
- ? (e.touches[0]?.clientX ?? e.changedTouches[0]?.clientX ?? 0)
56
- : e.clientX
57
- const rect = track.getBoundingClientRect()
58
- return Math.max(0, Math.min(1, (clientX - rect.left) / rect.width))
59
- },
60
- []
61
- )
62
-
63
- const seekTo = useCallback((fraction: number) => {
64
- const el = audioRef.current
65
- if (el && el.duration) el.currentTime = fraction * el.duration
66
- }, [])
67
-
68
- useEffect(() => {
69
- if (!seeking) return
70
- const onMove = (e: MouseEvent | TouchEvent) => {
71
- const f = getFraction(e)
72
- setPlayed(f)
73
- seekTo(f)
74
- }
75
- const onUp = (e: MouseEvent | TouchEvent) => {
76
- setSeeking(false)
77
- seekTo(getFraction(e))
78
- }
79
- window.addEventListener('mousemove', onMove)
80
- window.addEventListener('mouseup', onUp)
81
- window.addEventListener('touchmove', onMove, { passive: true })
82
- window.addEventListener('touchend', onUp)
83
- return () => {
84
- window.removeEventListener('mousemove', onMove)
85
- window.removeEventListener('mouseup', onUp)
86
- window.removeEventListener('touchmove', onMove)
87
- window.removeEventListener('touchend', onUp)
88
- }
89
- }, [seeking, getFraction, seekTo])
90
-
91
- const toggle = useCallback(() => setPlaying((p) => !p), [])
92
-
93
- return (
94
- <div className="relative">
95
- {sourceUrl && (
96
- <audio
97
- ref={audioRef}
98
- src={sourceUrl}
99
- loop
100
- onCanPlay={() => setAudioReady(true)}
101
- onEnded={() => {
102
- setPlaying(false)
103
- setPlayed(0)
104
- }}
105
- >
106
- <track kind="captions" />
107
- </audio>
108
- )}
109
- <CollapsedThumbnail
110
- thumbnailUrl={thumbnailUrl}
111
- mimeType={mimeType}
112
- overlayIcon={
113
- sourceUrl && audioReady ? (playing ? PauseIcon : PlayIcon) : undefined
114
- }
115
- onClick={sourceUrl && audioReady ? toggle : undefined}
116
- />
117
- {sourceUrl && audioReady && (
118
- <div className="absolute inset-x-0 bottom-0 px-3 pb-2.5 pt-6 bg-gradient-to-t from-black/40 to-transparent">
119
- <div
120
- ref={trackRef}
121
- role="slider"
122
- aria-label="Playback position"
123
- aria-valuenow={Math.round(played * 100)}
124
- aria-valuemin={0}
125
- aria-valuemax={100}
126
- tabIndex={0}
127
- className="relative flex h-4 w-full cursor-pointer items-center"
128
- onMouseDown={(e) => {
129
- e.stopPropagation()
130
- setSeeking(true)
131
- const f = getFraction(e)
132
- setPlayed(f)
133
- seekTo(f)
134
- }}
135
- onTouchStart={(e) => {
136
- e.stopPropagation()
137
- setSeeking(true)
138
- const f = getFraction(e)
139
- setPlayed(f)
140
- seekTo(f)
141
- }}
142
- onClick={(e) => e.stopPropagation()}
143
- onKeyDown={(e) => {
144
- if (e.key === 'ArrowRight') seekTo(Math.min(1, played + 0.05))
145
- if (e.key === 'ArrowLeft') seekTo(Math.max(0, played - 0.05))
146
- }}
147
- >
148
- <div className="w-full overflow-hidden rounded-full bg-white/30 h-1">
149
- <div
150
- className="h-full rounded-full bg-white"
151
- style={{ width: `${Math.round(played * 100)}%` }}
152
- />
153
- </div>
154
- </div>
155
- </div>
156
- )}
157
- </div>
158
- )
159
- }
160
-
161
- export default AudioPreview
@@ -1,58 +0,0 @@
1
- import classNames from 'classnames'
2
- import React from 'react'
3
-
4
- import { renderTypeIcon } from '../../utils/icons'
5
-
6
- interface CollapsedThumbnailProps {
7
- thumbnailUrl?: string
8
- mimeType: string
9
- overlayIcon?: React.ElementType
10
- darkOverlay?: boolean
11
- onClick?: () => void
12
- }
13
-
14
- const CollapsedThumbnail: React.FC<CollapsedThumbnailProps> = ({
15
- thumbnailUrl,
16
- mimeType,
17
- overlayIcon: OverlayIcon,
18
- darkOverlay,
19
- onClick,
20
- }) => {
21
- return (
22
- <button
23
- type="button"
24
- disabled={!onClick}
25
- className={classNames(
26
- 'relative aspect-video block w-full overflow-hidden border-0 bg-black/5 p-0 text-left appearance-none',
27
- { 'cursor-pointer': !!onClick, 'cursor-default': !onClick }
28
- )}
29
- onClick={onClick}
30
- aria-label={OverlayIcon ? 'Toggle preview' : undefined}
31
- >
32
- {thumbnailUrl ? (
33
- <img
34
- src={thumbnailUrl}
35
- alt=""
36
- className="absolute inset-0 h-full w-full object-cover"
37
- />
38
- ) : (
39
- <div className="absolute inset-0 flex items-center justify-center">
40
- {renderTypeIcon(mimeType, {
41
- className: 'size-12 text-black/20',
42
- weight: 'regular',
43
- })}
44
- </div>
45
- )}
46
- {darkOverlay && (
47
- <div className="pointer-events-none absolute inset-0 bg-black/30" />
48
- )}
49
- {OverlayIcon && (
50
- <div className="pointer-events-none absolute left-3 top-3 flex size-8 items-center justify-center rounded-full bg-black/60 text-white">
51
- <OverlayIcon className="size-4" weight="fill" />
52
- </div>
53
- )}
54
- </button>
55
- )
56
- }
57
-
58
- export default CollapsedThumbnail
@@ -1,56 +0,0 @@
1
- import { EyeIcon, EyeSlashIcon } from '@phosphor-icons/react'
2
- import { useState } from 'react'
3
-
4
- import CollapsedThumbnail from './CardCollapsedThumbnail'
5
-
6
- interface ImagePreviewProps {
7
- sourceUrl?: string
8
- thumbnailUrl?: string
9
- mimeType: string
10
- title?: string
11
- }
12
-
13
- const ImagePreview: React.FC<ImagePreviewProps> = (props) => {
14
- const { sourceUrl, thumbnailUrl, mimeType, title } = props
15
- const [expanded, setExpanded] = useState(false)
16
-
17
- if (expanded && sourceUrl) {
18
- return (
19
- <div className="relative">
20
- <button
21
- type="button"
22
- className="block w-full cursor-pointer border-0 p-0 text-left appearance-none"
23
- onClick={() => setExpanded(false)}
24
- aria-label="Close preview"
25
- >
26
- <img src={sourceUrl} alt={title ?? ''} className="block w-full" />
27
- </button>
28
- <CloseButton onClose={() => setExpanded(false)} />
29
- </div>
30
- )
31
- }
32
-
33
- return (
34
- <CollapsedThumbnail
35
- thumbnailUrl={thumbnailUrl}
36
- mimeType={mimeType}
37
- overlayIcon={sourceUrl ? EyeSlashIcon : undefined}
38
- onClick={sourceUrl ? () => setExpanded(true) : undefined}
39
- />
40
- )
41
- }
42
-
43
- const CloseButton: React.FC<{ onClose: () => void }> = ({ onClose }) => {
44
- return (
45
- <button
46
- type="button"
47
- onClick={onClose}
48
- className="absolute left-3 top-3 z-40 flex size-8 items-center justify-center rounded-full bg-black/60 text-white"
49
- aria-label="Close preview"
50
- >
51
- <EyeIcon className="size-4" weight="fill" />
52
- </button>
53
- )
54
- }
55
-
56
- export default ImagePreview
@@ -1,91 +0,0 @@
1
- import { EyeIcon, EyeSlashIcon } from '@phosphor-icons/react'
2
- import classNames from 'classnames'
3
- import React, { useState } from 'react'
4
-
5
- import { renderTypeIcon } from '../../utils/icons'
6
- import MediaPlayer from '../MediaPlayer'
7
-
8
- import CollapsedThumbnail from './CardCollapsedThumbnail'
9
-
10
- interface VideoPreviewProps {
11
- sourceUrl?: string
12
- thumbnailUrl?: string
13
- mimeType: string
14
- }
15
-
16
- const VideoPreview: React.FC<VideoPreviewProps> = (props) => {
17
- const { sourceUrl, thumbnailUrl, mimeType } = props
18
- const [expanded, setExpanded] = useState(false)
19
-
20
- const collapse = () => {
21
- setExpanded(false)
22
- }
23
-
24
- if (!sourceUrl) {
25
- return (
26
- <CollapsedThumbnail thumbnailUrl={thumbnailUrl} mimeType={mimeType} />
27
- )
28
- }
29
-
30
- return (
31
- <div
32
- className={classNames('relative overflow-hidden', {
33
- 'aspect-video': !expanded,
34
- })}
35
- >
36
- <MediaPlayer
37
- source={sourceUrl}
38
- mimeType={mimeType}
39
- poster={thumbnailUrl}
40
- playing={expanded}
41
- loop={true}
42
- controls={false}
43
- muted={true}
44
- showProgress={true}
45
- onContainerClick={collapse}
46
- />
47
- {!expanded && (
48
- <button
49
- type="button"
50
- className="absolute inset-0 block cursor-pointer border-0 p-0 text-left appearance-none"
51
- onClick={() => setExpanded(true)}
52
- aria-label="Expand video preview"
53
- >
54
- {thumbnailUrl ? (
55
- <img
56
- src={thumbnailUrl}
57
- alt=""
58
- className="absolute inset-0 h-full w-full object-cover"
59
- />
60
- ) : (
61
- <div className="absolute inset-0 flex items-center justify-center">
62
- {renderTypeIcon(mimeType, {
63
- className: 'size-12 text-black/20',
64
- weight: 'regular',
65
- })}
66
- </div>
67
- )}
68
- <div className="pointer-events-none absolute left-3 top-3 flex size-8 items-center justify-center rounded-full bg-black/60 text-white">
69
- <EyeSlashIcon className="size-4" weight="fill" />
70
- </div>
71
- </button>
72
- )}
73
- {expanded && <CloseButton onClose={collapse} />}
74
- </div>
75
- )
76
- }
77
-
78
- const CloseButton: React.FC<{ onClose: () => void }> = ({ onClose }) => {
79
- return (
80
- <button
81
- type="button"
82
- onClick={onClose}
83
- className="absolute left-3 top-3 z-40 flex size-8 items-center justify-center rounded-full bg-black/60 text-white"
84
- aria-label="Close preview"
85
- >
86
- <EyeIcon className="size-4" weight="fill" />
87
- </button>
88
- )
89
- }
90
-
91
- export default VideoPreview
@@ -1,39 +0,0 @@
1
- import React, { useState } from 'react'
2
-
3
- import ThumbnailPreview from './CardThumbnailPreview'
4
-
5
- interface ImagePreviewProps {
6
- sourceUrl?: string
7
- thumbnailUrl?: string
8
- mimeType: string
9
- title?: string
10
- LockIcon?: React.ElementType
11
- }
12
-
13
- const ImagePreview: React.FC<ImagePreviewProps> = (props) => {
14
- const { sourceUrl, thumbnailUrl, mimeType, title, LockIcon } = props
15
- const [sourceReady, setSourceReady] = useState(false)
16
-
17
- if (LockIcon != null) {
18
- return (
19
- <ThumbnailPreview
20
- thumbnailUrl={thumbnailUrl}
21
- mimeType={mimeType}
22
- LockIcon={LockIcon}
23
- />
24
- )
25
- }
26
-
27
- return (
28
- <div className="relative overflow-hidden bg-black/5">
29
- <img
30
- src={sourceUrl}
31
- alt={title}
32
- className={`block w-full transition-opacity duration-300 ${sourceReady ? 'opacity-100' : 'opacity-0'}`}
33
- onLoad={() => setSourceReady(true)}
34
- />
35
- </div>
36
- )
37
- }
38
-
39
- export default ImagePreview
@@ -1,36 +0,0 @@
1
- import React from 'react'
2
-
3
- import MediaPlayer from '../MediaPlayer'
4
-
5
- import ThumbnailPreview from './CardThumbnailPreview'
6
-
7
- interface MediaPreviewProps {
8
- sourceUrl?: string
9
- thumbnailUrl?: string
10
- mimeType: string
11
- LockIcon?: React.ElementType
12
- }
13
-
14
- const MediaPreview: React.FC<MediaPreviewProps> = (props) => {
15
- const { sourceUrl, thumbnailUrl, mimeType, LockIcon } = props
16
-
17
- if (LockIcon != null) {
18
- return (
19
- <ThumbnailPreview
20
- thumbnailUrl={thumbnailUrl}
21
- mimeType={mimeType}
22
- LockIcon={LockIcon}
23
- />
24
- )
25
- }
26
-
27
- return (
28
- <MediaPlayer
29
- source={sourceUrl ?? ''}
30
- mimeType={mimeType}
31
- poster={thumbnailUrl}
32
- />
33
- )
34
- }
35
-
36
- export default MediaPreview
@@ -1,45 +0,0 @@
1
- import React from 'react'
2
-
3
- import { renderTypeIcon } from '../../utils/icons'
4
-
5
- interface ThumbnailPreviewProps {
6
- thumbnailUrl?: string
7
- mimeType: string
8
- LockIcon?: React.ElementType
9
- }
10
-
11
- const ThumbnailPreview: React.FC<ThumbnailPreviewProps> = (props) => {
12
- const { thumbnailUrl, mimeType, LockIcon } = props
13
-
14
- return (
15
- <div className="relative aspect-video overflow-hidden bg-black/5">
16
- {thumbnailUrl != null ? (
17
- <img
18
- src={thumbnailUrl}
19
- alt=""
20
- className="absolute inset-0 h-full w-full object-cover"
21
- />
22
- ) : (
23
- <div className="absolute inset-0 flex items-center justify-center">
24
- {renderTypeIcon(mimeType, {
25
- className: 'size-12 text-black/20',
26
- weight: 'regular',
27
- })}
28
- </div>
29
- )}
30
- {LockIcon != null ? <LockOverlay icon={LockIcon} /> : null}
31
- </div>
32
- )
33
- }
34
-
35
- const LockOverlay: React.FC<{ icon: React.ElementType }> = ({ icon: Icon }) => {
36
- return (
37
- <div className="absolute inset-0 bg-black/30">
38
- <div className="absolute left-3 top-3 flex size-8 items-center justify-center rounded-full bg-black/60">
39
- <Icon className="size-4 text-white" weight="fill" />
40
- </div>
41
- </div>
42
- )
43
- }
44
-
45
- export default ThumbnailPreview