@uniweb/kit 0.1.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 +201 -0
- package/README.md +345 -0
- package/package.json +47 -0
- package/src/components/Asset/Asset.jsx +161 -0
- package/src/components/Asset/index.js +1 -0
- package/src/components/Disclaimer/Disclaimer.jsx +198 -0
- package/src/components/Disclaimer/index.js +1 -0
- package/src/components/FileLogo/FileLogo.jsx +148 -0
- package/src/components/FileLogo/index.js +1 -0
- package/src/components/Icon/Icon.jsx +214 -0
- package/src/components/Icon/index.js +1 -0
- package/src/components/Image/Image.jsx +194 -0
- package/src/components/Image/index.js +1 -0
- package/src/components/Link/Link.jsx +261 -0
- package/src/components/Link/index.js +1 -0
- package/src/components/Media/Media.jsx +322 -0
- package/src/components/Media/index.js +1 -0
- package/src/components/MediaIcon/MediaIcon.jsx +95 -0
- package/src/components/MediaIcon/index.js +1 -0
- package/src/components/SafeHtml/SafeHtml.jsx +93 -0
- package/src/components/SafeHtml/index.js +1 -0
- package/src/components/Section/Render.jsx +245 -0
- package/src/components/Section/Section.jsx +131 -0
- package/src/components/Section/index.js +3 -0
- package/src/components/Section/renderers/Alert.jsx +101 -0
- package/src/components/Section/renderers/Code.jsx +70 -0
- package/src/components/Section/renderers/Details.jsx +77 -0
- package/src/components/Section/renderers/Divider.jsx +42 -0
- package/src/components/Section/renderers/Table.jsx +55 -0
- package/src/components/Section/renderers/index.js +11 -0
- package/src/components/Text/Text.jsx +207 -0
- package/src/components/Text/index.js +14 -0
- package/src/hooks/index.js +1 -0
- package/src/hooks/useWebsite.js +77 -0
- package/src/index.js +69 -0
- package/src/styles/index.css +8 -0
- package/src/utils/index.js +104 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media Component
|
|
3
|
+
*
|
|
4
|
+
* Video player supporting:
|
|
5
|
+
* - YouTube embeds
|
|
6
|
+
* - Vimeo embeds
|
|
7
|
+
* - Local/direct video files
|
|
8
|
+
* - Thumbnail facades
|
|
9
|
+
* - Playback tracking
|
|
10
|
+
*
|
|
11
|
+
* @module @uniweb/kit/Media
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import React, { useState, useEffect, useRef, useCallback } from 'react'
|
|
15
|
+
import { cn } from '../../utils/index.js'
|
|
16
|
+
import { detectMediaType } from '../../utils/index.js'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Extract YouTube video ID from URL
|
|
20
|
+
* @param {string} url
|
|
21
|
+
* @returns {string|null}
|
|
22
|
+
*/
|
|
23
|
+
function getYouTubeId(url) {
|
|
24
|
+
if (!url) return null
|
|
25
|
+
const match = url.match(/(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([^&?/]+)/)
|
|
26
|
+
return match?.[1] || null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Extract Vimeo video ID from URL
|
|
31
|
+
* @param {string} url
|
|
32
|
+
* @returns {string|null}
|
|
33
|
+
*/
|
|
34
|
+
function getVimeoId(url) {
|
|
35
|
+
if (!url) return null
|
|
36
|
+
const match = url.match(/vimeo\.com\/(?:video\/)?(\d+)/)
|
|
37
|
+
return match?.[1] || null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get thumbnail URL for a video
|
|
42
|
+
* @param {string} src - Video URL
|
|
43
|
+
* @param {string} type - Media type
|
|
44
|
+
* @returns {string|null}
|
|
45
|
+
*/
|
|
46
|
+
function getVideoThumbnail(src, type) {
|
|
47
|
+
if (type === 'youtube') {
|
|
48
|
+
const id = getYouTubeId(src)
|
|
49
|
+
return id ? `https://img.youtube.com/vi/${id}/maxresdefault.jpg` : null
|
|
50
|
+
}
|
|
51
|
+
// Vimeo requires API call, return null for now
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* YouTube Player Component
|
|
57
|
+
*/
|
|
58
|
+
function YouTubePlayer({ videoId, autoplay, muted, loop, onReady, onStateChange, className }) {
|
|
59
|
+
const iframeRef = useRef(null)
|
|
60
|
+
|
|
61
|
+
const params = new URLSearchParams({
|
|
62
|
+
enablejsapi: '1',
|
|
63
|
+
autoplay: autoplay ? '1' : '0',
|
|
64
|
+
mute: muted ? '1' : '0',
|
|
65
|
+
loop: loop ? '1' : '0',
|
|
66
|
+
playlist: loop ? videoId : '',
|
|
67
|
+
rel: '0',
|
|
68
|
+
modestbranding: '1'
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<iframe
|
|
73
|
+
ref={iframeRef}
|
|
74
|
+
src={`https://www.youtube.com/embed/${videoId}?${params}`}
|
|
75
|
+
className={cn('w-full h-full', className)}
|
|
76
|
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
77
|
+
allowFullScreen
|
|
78
|
+
title="YouTube video"
|
|
79
|
+
/>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Vimeo Player Component
|
|
85
|
+
*/
|
|
86
|
+
function VimeoPlayer({ videoId, autoplay, muted, loop, className }) {
|
|
87
|
+
const params = new URLSearchParams({
|
|
88
|
+
autoplay: autoplay ? '1' : '0',
|
|
89
|
+
muted: muted ? '1' : '0',
|
|
90
|
+
loop: loop ? '1' : '0',
|
|
91
|
+
dnt: '1'
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<iframe
|
|
96
|
+
src={`https://player.vimeo.com/video/${videoId}?${params}`}
|
|
97
|
+
className={cn('w-full h-full', className)}
|
|
98
|
+
allow="autoplay; fullscreen; picture-in-picture"
|
|
99
|
+
allowFullScreen
|
|
100
|
+
title="Vimeo video"
|
|
101
|
+
/>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Local/Direct Video Player Component
|
|
107
|
+
*/
|
|
108
|
+
function LocalVideo({ src, autoplay, muted, loop, controls, poster, onProgress, className }) {
|
|
109
|
+
const videoRef = useRef(null)
|
|
110
|
+
const [milestones, setMilestones] = useState({ 25: false, 50: false, 75: false, 95: false })
|
|
111
|
+
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
const video = videoRef.current
|
|
114
|
+
if (!video || !onProgress) return
|
|
115
|
+
|
|
116
|
+
const handleTimeUpdate = () => {
|
|
117
|
+
const percent = (video.currentTime / video.duration) * 100
|
|
118
|
+
|
|
119
|
+
Object.entries({ 25: 25, 50: 50, 75: 75, 95: 95 }).forEach(([key, threshold]) => {
|
|
120
|
+
if (percent >= threshold && !milestones[key]) {
|
|
121
|
+
setMilestones((prev) => ({ ...prev, [key]: true }))
|
|
122
|
+
onProgress({ milestone: key, percent, currentTime: video.currentTime })
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
video.addEventListener('timeupdate', handleTimeUpdate)
|
|
128
|
+
return () => video.removeEventListener('timeupdate', handleTimeUpdate)
|
|
129
|
+
}, [milestones, onProgress])
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<video
|
|
133
|
+
ref={videoRef}
|
|
134
|
+
src={src}
|
|
135
|
+
autoPlay={autoplay}
|
|
136
|
+
muted={muted}
|
|
137
|
+
loop={loop}
|
|
138
|
+
controls={controls}
|
|
139
|
+
poster={poster}
|
|
140
|
+
playsInline
|
|
141
|
+
className={cn('w-full h-full object-cover', className)}
|
|
142
|
+
/>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Play Button Overlay
|
|
148
|
+
*/
|
|
149
|
+
function PlayButton({ onClick, className }) {
|
|
150
|
+
return (
|
|
151
|
+
<button
|
|
152
|
+
onClick={onClick}
|
|
153
|
+
className={cn(
|
|
154
|
+
'absolute inset-0 flex items-center justify-center',
|
|
155
|
+
'bg-black/30 hover:bg-black/40 transition-colors',
|
|
156
|
+
'group cursor-pointer',
|
|
157
|
+
className
|
|
158
|
+
)}
|
|
159
|
+
aria-label="Play video"
|
|
160
|
+
>
|
|
161
|
+
<div className="w-16 h-16 rounded-full bg-white/90 group-hover:bg-white flex items-center justify-center transition-colors">
|
|
162
|
+
<svg className="w-8 h-8 text-gray-900 ml-1" fill="currentColor" viewBox="0 0 24 24">
|
|
163
|
+
<path d="M8 5v14l11-7z" />
|
|
164
|
+
</svg>
|
|
165
|
+
</div>
|
|
166
|
+
</button>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Media - Video player component
|
|
172
|
+
*
|
|
173
|
+
* @param {Object} props
|
|
174
|
+
* @param {string|Object} props.src - Video URL or media object
|
|
175
|
+
* @param {Object} [props.media] - Media object with src/caption
|
|
176
|
+
* @param {string} [props.thumbnail] - Thumbnail URL
|
|
177
|
+
* @param {boolean} [props.autoplay=false] - Auto-play video
|
|
178
|
+
* @param {boolean} [props.muted=false] - Mute video
|
|
179
|
+
* @param {boolean} [props.loop=false] - Loop video
|
|
180
|
+
* @param {boolean} [props.controls=true] - Show controls
|
|
181
|
+
* @param {boolean} [props.facade=false] - Show thumbnail with play button
|
|
182
|
+
* @param {string} [props.aspectRatio='16/9'] - Aspect ratio
|
|
183
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
184
|
+
* @param {Function} [props.onProgress] - Progress callback for tracking
|
|
185
|
+
* @param {Object} [props.block] - Block object for event tracking
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* // YouTube video
|
|
189
|
+
* <Media src="https://youtube.com/watch?v=abc123" />
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* // With thumbnail facade
|
|
193
|
+
* <Media
|
|
194
|
+
* src="https://youtube.com/watch?v=abc123"
|
|
195
|
+
* thumbnail="/images/video-poster.jpg"
|
|
196
|
+
* facade
|
|
197
|
+
* />
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* // Local video
|
|
201
|
+
* <Media src="/videos/intro.mp4" controls autoplay={false} />
|
|
202
|
+
*/
|
|
203
|
+
export function Media({
|
|
204
|
+
src,
|
|
205
|
+
media,
|
|
206
|
+
thumbnail,
|
|
207
|
+
autoplay = false,
|
|
208
|
+
muted = false,
|
|
209
|
+
loop = false,
|
|
210
|
+
controls = true,
|
|
211
|
+
facade = false,
|
|
212
|
+
aspectRatio = '16/9',
|
|
213
|
+
className,
|
|
214
|
+
onProgress,
|
|
215
|
+
block,
|
|
216
|
+
...props
|
|
217
|
+
}) {
|
|
218
|
+
const [showVideo, setShowVideo] = useState(!facade)
|
|
219
|
+
|
|
220
|
+
// Normalize source
|
|
221
|
+
const videoSrc = typeof src === 'string' ? src : (src?.src || media?.src || '')
|
|
222
|
+
const caption = media?.caption || src?.caption || ''
|
|
223
|
+
|
|
224
|
+
// Detect video type
|
|
225
|
+
const mediaType = detectMediaType(videoSrc)
|
|
226
|
+
|
|
227
|
+
// Get thumbnail
|
|
228
|
+
const thumbnailSrc = thumbnail || getVideoThumbnail(videoSrc, mediaType)
|
|
229
|
+
|
|
230
|
+
// Handle play click (for facade mode)
|
|
231
|
+
const handlePlay = useCallback(() => {
|
|
232
|
+
setShowVideo(true)
|
|
233
|
+
}, [])
|
|
234
|
+
|
|
235
|
+
// Handle progress tracking
|
|
236
|
+
const handleProgress = useCallback((data) => {
|
|
237
|
+
onProgress?.(data)
|
|
238
|
+
|
|
239
|
+
// Track via block if available
|
|
240
|
+
if (block?.trackEvent && typeof window !== 'undefined' && window.uniweb?.analytics?.initialized) {
|
|
241
|
+
block.trackEvent(`video_milestone_${data.milestone}`, {
|
|
242
|
+
milestone: `${data.milestone}%`,
|
|
243
|
+
src: videoSrc
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
}, [onProgress, block, videoSrc])
|
|
247
|
+
|
|
248
|
+
// Render facade (thumbnail with play button)
|
|
249
|
+
if (facade && !showVideo && thumbnailSrc) {
|
|
250
|
+
return (
|
|
251
|
+
<div
|
|
252
|
+
className={cn('relative overflow-hidden', className)}
|
|
253
|
+
style={{ aspectRatio }}
|
|
254
|
+
{...props}
|
|
255
|
+
>
|
|
256
|
+
<img
|
|
257
|
+
src={thumbnailSrc}
|
|
258
|
+
alt={caption || 'Video thumbnail'}
|
|
259
|
+
className="w-full h-full object-cover"
|
|
260
|
+
/>
|
|
261
|
+
<PlayButton onClick={handlePlay} />
|
|
262
|
+
</div>
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Render video player
|
|
267
|
+
const videoContent = (() => {
|
|
268
|
+
switch (mediaType) {
|
|
269
|
+
case 'youtube': {
|
|
270
|
+
const videoId = getYouTubeId(videoSrc)
|
|
271
|
+
if (!videoId) return null
|
|
272
|
+
return (
|
|
273
|
+
<YouTubePlayer
|
|
274
|
+
videoId={videoId}
|
|
275
|
+
autoplay={autoplay || (facade && showVideo)}
|
|
276
|
+
muted={muted}
|
|
277
|
+
loop={loop}
|
|
278
|
+
/>
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
case 'vimeo': {
|
|
283
|
+
const videoId = getVimeoId(videoSrc)
|
|
284
|
+
if (!videoId) return null
|
|
285
|
+
return (
|
|
286
|
+
<VimeoPlayer
|
|
287
|
+
videoId={videoId}
|
|
288
|
+
autoplay={autoplay || (facade && showVideo)}
|
|
289
|
+
muted={muted}
|
|
290
|
+
loop={loop}
|
|
291
|
+
/>
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
case 'video':
|
|
296
|
+
default:
|
|
297
|
+
return (
|
|
298
|
+
<LocalVideo
|
|
299
|
+
src={videoSrc}
|
|
300
|
+
autoplay={autoplay || (facade && showVideo)}
|
|
301
|
+
muted={muted}
|
|
302
|
+
loop={loop}
|
|
303
|
+
controls={controls}
|
|
304
|
+
poster={thumbnailSrc}
|
|
305
|
+
onProgress={handleProgress}
|
|
306
|
+
/>
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
})()
|
|
310
|
+
|
|
311
|
+
return (
|
|
312
|
+
<div
|
|
313
|
+
className={cn('relative overflow-hidden bg-black', className)}
|
|
314
|
+
style={{ aspectRatio }}
|
|
315
|
+
{...props}
|
|
316
|
+
>
|
|
317
|
+
{videoContent}
|
|
318
|
+
</div>
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export default Media
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Media, default } from './Media.jsx'
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MediaIcon Component
|
|
3
|
+
*
|
|
4
|
+
* Social media and contact platform icons.
|
|
5
|
+
*
|
|
6
|
+
* @module @uniweb/kit/MediaIcon
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React from 'react'
|
|
10
|
+
import { cn } from '../../utils/index.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* SVG paths for social media icons
|
|
14
|
+
* Simplified versions of popular brand icons
|
|
15
|
+
*/
|
|
16
|
+
const SOCIAL_ICONS = {
|
|
17
|
+
facebook: (
|
|
18
|
+
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" />
|
|
19
|
+
),
|
|
20
|
+
twitter: (
|
|
21
|
+
<path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z" />
|
|
22
|
+
),
|
|
23
|
+
x: (
|
|
24
|
+
<path d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z" />
|
|
25
|
+
),
|
|
26
|
+
linkedin: (
|
|
27
|
+
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
|
|
28
|
+
),
|
|
29
|
+
instagram: (
|
|
30
|
+
<path d="M12 0C8.74 0 8.333.015 7.053.072 5.775.132 4.905.333 4.14.63c-.789.306-1.459.717-2.126 1.384S.935 3.35.63 4.14C.333 4.905.131 5.775.072 7.053.012 8.333 0 8.74 0 12s.015 3.667.072 4.947c.06 1.277.261 2.148.558 2.913.306.788.717 1.459 1.384 2.126.667.666 1.336 1.079 2.126 1.384.766.296 1.636.499 2.913.558C8.333 23.988 8.74 24 12 24s3.667-.015 4.947-.072c1.277-.06 2.148-.262 2.913-.558.788-.306 1.459-.718 2.126-1.384.666-.667 1.079-1.335 1.384-2.126.296-.765.499-1.636.558-2.913.06-1.28.072-1.687.072-4.947s-.015-3.667-.072-4.947c-.06-1.277-.262-2.149-.558-2.913-.306-.789-.718-1.459-1.384-2.126C21.319 1.347 20.651.935 19.86.63c-.765-.297-1.636-.499-2.913-.558C15.667.012 15.26 0 12 0zm0 2.16c3.203 0 3.585.016 4.85.071 1.17.055 1.805.249 2.227.415.562.217.96.477 1.382.896.419.42.679.819.896 1.381.164.422.36 1.057.413 2.227.057 1.266.07 1.646.07 4.85s-.015 3.585-.074 4.85c-.061 1.17-.256 1.805-.421 2.227-.224.562-.479.96-.899 1.382-.419.419-.824.679-1.38.896-.42.164-1.065.36-2.235.413-1.274.057-1.649.07-4.859.07-3.211 0-3.586-.015-4.859-.074-1.171-.061-1.816-.256-2.236-.421-.569-.224-.96-.479-1.379-.899-.421-.419-.69-.824-.9-1.38-.165-.42-.359-1.065-.42-2.235-.045-1.26-.061-1.649-.061-4.844 0-3.196.016-3.586.061-4.861.061-1.17.255-1.814.42-2.234.21-.57.479-.96.9-1.381.419-.419.81-.689 1.379-.898.42-.166 1.051-.361 2.221-.421 1.275-.045 1.65-.06 4.859-.06l.045.03zm0 3.678c-3.405 0-6.162 2.76-6.162 6.162 0 3.405 2.76 6.162 6.162 6.162 3.405 0 6.162-2.76 6.162-6.162 0-3.405-2.757-6.162-6.162-6.162zM12 16c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4zm7.846-10.405c0 .795-.646 1.44-1.44 1.44-.795 0-1.44-.646-1.44-1.44 0-.794.646-1.439 1.44-1.439.793-.001 1.44.645 1.44 1.439z" />
|
|
31
|
+
),
|
|
32
|
+
youtube: (
|
|
33
|
+
<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
|
|
34
|
+
),
|
|
35
|
+
github: (
|
|
36
|
+
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
|
37
|
+
),
|
|
38
|
+
medium: (
|
|
39
|
+
<path d="M13.54 12a6.8 6.8 0 01-6.77 6.82A6.8 6.8 0 010 12a6.8 6.8 0 016.77-6.82A6.8 6.8 0 0113.54 12zM20.96 12c0 3.54-1.51 6.42-3.38 6.42-1.87 0-3.39-2.88-3.39-6.42s1.52-6.42 3.39-6.42 3.38 2.88 3.38 6.42M24 12c0 3.17-.53 5.75-1.19 5.75-.66 0-1.19-2.58-1.19-5.75s.53-5.75 1.19-5.75C23.47 6.25 24 8.83 24 12z" />
|
|
40
|
+
),
|
|
41
|
+
pinterest: (
|
|
42
|
+
<path d="M12.017 0C5.396 0 .029 5.367.029 11.987c0 5.079 3.158 9.417 7.618 11.162-.105-.949-.199-2.403.041-3.439.219-.937 1.406-5.957 1.406-5.957s-.359-.72-.359-1.781c0-1.663.967-2.911 2.168-2.911 1.024 0 1.518.769 1.518 1.688 0 1.029-.653 2.567-.992 3.992-.285 1.193.6 2.165 1.775 2.165 2.128 0 3.768-2.245 3.768-5.487 0-2.861-2.063-4.869-5.008-4.869-3.41 0-5.409 2.562-5.409 5.199 0 1.033.394 2.143.889 2.741.099.12.112.225.085.345-.09.375-.293 1.199-.334 1.363-.053.225-.172.271-.401.165-1.495-.69-2.433-2.878-2.433-4.646 0-3.776 2.748-7.252 7.92-7.252 4.158 0 7.392 2.967 7.392 6.923 0 4.135-2.607 7.462-6.233 7.462-1.214 0-2.354-.629-2.758-1.379l-.749 2.848c-.269 1.045-1.004 2.352-1.498 3.146 1.123.345 2.306.535 3.55.535 6.607 0 11.985-5.365 11.985-11.987C23.97 5.39 18.592.026 11.985.026L12.017 0z" />
|
|
43
|
+
),
|
|
44
|
+
email: (
|
|
45
|
+
<path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z" />
|
|
46
|
+
),
|
|
47
|
+
phone: (
|
|
48
|
+
<path d="M20.01 15.38c-1.23 0-2.42-.2-3.53-.56-.35-.12-.74-.03-1.01.24l-1.57 1.97c-2.83-1.35-5.48-3.9-6.89-6.83l1.95-1.66c.27-.28.35-.67.24-1.02-.37-1.11-.56-2.3-.56-3.53 0-.54-.45-.99-.99-.99H4.19C3.65 3 3 3.24 3 3.99 3 13.68 10.32 21 20.01 21c.71 0 .99-.63.99-1.18v-3.45c0-.54-.45-.99-.99-.99z" />
|
|
49
|
+
),
|
|
50
|
+
link: (
|
|
51
|
+
<path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z" />
|
|
52
|
+
),
|
|
53
|
+
orcid: (
|
|
54
|
+
<path d="M12 0C5.372 0 0 5.372 0 12s5.372 12 12 12 12-5.372 12-12S18.628 0 12 0zM7.369 4.378c.525 0 .947.431.947.947s-.422.947-.947.947a.95.95 0 01-.947-.947c0-.525.422-.947.947-.947zm-.722 3.038h1.444v10.041H6.647V7.416zm3.562 0h3.9c3.712 0 5.344 2.653 5.344 5.025 0 2.578-2.016 5.025-5.325 5.025h-3.919V7.416zm1.444 1.303v7.444h2.297c3.272 0 4.022-2.484 4.022-3.722 0-2.016-1.284-3.722-4.097-3.722h-2.222z" />
|
|
55
|
+
),
|
|
56
|
+
researchgate: (
|
|
57
|
+
<path d="M19.586 0c-.818 0-1.508.19-2.073.565-.563.377-.97.936-1.213 1.68a3.193 3.193 0 00-.112.437 8.365 8.365 0 00-.078.53 9 9 0 00-.05.727c-.01.282-.013.621-.013 1.016a31.121 31.121 0 00.014 1.017 9 9 0 00.05.727 7.946 7.946 0 00.078.53h-.005a3.334 3.334 0 00.112.438c.244.743.65 1.303 1.214 1.68.565.376 1.256.564 2.075.564.8 0 1.536-.213 2.105-.603.57-.39.94-.916 1.175-1.65.076-.235.135-.558.177-.93a10.9 10.9 0 00.043-1.207v-.82c0-.095-.047-.142-.14-.142h-3.064c-.094 0-.14.047-.14.141v.956c0 .094.046.14.14.14h1.666c.056 0 .084.03.084.086 0 .36 0 .62-.036.865-.038.244-.1.447-.147.606-.108.385-.348.664-.638.876-.29.212-.738.35-1.227.35-.545 0-.901-.15-1.21-.353-.306-.203-.517-.454-.67-.915a3.136 3.136 0 01-.147-.762 17.366 17.366 0 01-.034-.656c-.01-.26-.014-.572-.014-.939a26.401 26.401 0 01.014-.938 15.821 15.821 0 01.035-.656 3.19 3.19 0 01.148-.76c.152-.46.362-.713.67-.917.308-.203.665-.354 1.21-.354.51 0 .836.092 1.12.274.284.18.49.394.6.645.036.078.064.158.093.233.03.075.057.142.098.218.04.076.1.101.198.101h1.1c.095 0 .142-.046.142-.14v-.076c-.053-.277-.158-.58-.27-.856-.113-.277-.28-.53-.5-.764-.222-.234-.508-.427-.836-.58-.328-.153-.71-.249-1.137-.303a7.185 7.185 0 00-1.315-.076c-.38.01-.78.063-1.23.162zm-8.61 1.83c-.373.08-.732.226-1.082.437-.349.21-.66.481-.928.81-.27.329-.489.712-.655 1.147-.163.435-.25.917-.25 1.447 0 1.028.267 1.846.795 2.45.528.606 1.27.972 2.228 1.097-.056.116-.11.237-.167.36a3.78 3.78 0 01-.172.34c-.165.303-.399.634-.711.997-.313.365-.697.708-1.147 1.033a.21.21 0 00-.087.184c0 .057.019.113.056.17a.231.231 0 00.085.078l.864.513c.028.02.056.028.085.028a.2.2 0 00.142-.057c.614-.55 1.11-1.117 1.49-1.697.376-.58.657-1.163.838-1.747h.057c.205 0 .388-.01.542-.026.154-.017.294-.044.418-.082V15.9c0 .095.047.141.141.141h1.228c.095 0 .141-.046.141-.14V7.197c0-.095-.046-.142-.14-.142h-1.693a.237.237 0 00-.14.05.163.163 0 00-.055.134v.072c0 .056.019.11.055.162a.237.237 0 00.14.078h.282v7.133c-.113.011-.248.015-.395.015h-.56c-.575 0-1.03-.112-1.361-.338-.33-.226-.57-.525-.71-.896a3.03 3.03 0 01-.193-.76c-.03-.253-.05-.473-.05-.66 0-.357.03-.668.08-.932.055-.264.13-.489.233-.673.163-.29.377-.512.65-.67.273-.155.59-.244.948-.273.358-.028.746-.011 1.16.05v-1.2c-.414-.102-.792-.144-1.135-.13a3.652 3.652 0 00-.93.145z" />
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* MediaIcon - Social media platform icon
|
|
63
|
+
*
|
|
64
|
+
* @param {Object} props
|
|
65
|
+
* @param {string} props.type - Platform type (facebook, twitter, linkedin, etc.)
|
|
66
|
+
* @param {string} [props.size='24'] - Icon size in pixels
|
|
67
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* <MediaIcon type="twitter" size="32" className="text-blue-400" />
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* <MediaIcon type="email" />
|
|
74
|
+
*/
|
|
75
|
+
export function MediaIcon({ type, size = '24', className, ...props }) {
|
|
76
|
+
// Normalize type
|
|
77
|
+
const iconType = type?.toLowerCase().replace(/[^a-z]/g, '') || 'link'
|
|
78
|
+
const icon = SOCIAL_ICONS[iconType] || SOCIAL_ICONS.link
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<svg
|
|
82
|
+
viewBox="0 0 24 24"
|
|
83
|
+
fill="currentColor"
|
|
84
|
+
className={cn('inline-block', className)}
|
|
85
|
+
style={{ width: size, height: size }}
|
|
86
|
+
aria-label={type}
|
|
87
|
+
role="img"
|
|
88
|
+
{...props}
|
|
89
|
+
>
|
|
90
|
+
{icon}
|
|
91
|
+
</svg>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export default MediaIcon
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { MediaIcon, default } from './MediaIcon.jsx'
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SafeHtml Component
|
|
3
|
+
*
|
|
4
|
+
* Safely renders HTML content with topic link resolution.
|
|
5
|
+
* Handles the `topic:` protocol for internal content references.
|
|
6
|
+
*
|
|
7
|
+
* @module @uniweb/kit/SafeHtml
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React, { Suspense, useMemo } from 'react'
|
|
11
|
+
import { useWebsite } from '../../hooks/useWebsite.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve topic: links in HTML content
|
|
15
|
+
* @param {string} html - HTML string with potential topic: links
|
|
16
|
+
* @param {Object} website - Website instance
|
|
17
|
+
* @returns {string} HTML with resolved links
|
|
18
|
+
*/
|
|
19
|
+
function resolveTopicLinks(html, website) {
|
|
20
|
+
if (!html || typeof html !== 'string') return html
|
|
21
|
+
if (!html.includes('topic:')) return html
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const parser = new DOMParser()
|
|
25
|
+
const doc = parser.parseFromString(html, 'text/html')
|
|
26
|
+
const links = doc.querySelectorAll('a[href^="topic:"]')
|
|
27
|
+
|
|
28
|
+
links.forEach((link) => {
|
|
29
|
+
const href = link.getAttribute('href')
|
|
30
|
+
if (href) {
|
|
31
|
+
link.setAttribute('href', website.makeHref(href))
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
return doc.body.innerHTML
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.warn('[SafeHtml] Error resolving topic links:', error)
|
|
38
|
+
return html
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* SafeHtml - Safely render HTML content
|
|
44
|
+
*
|
|
45
|
+
* @param {Object} props
|
|
46
|
+
* @param {string|string[]} props.value - HTML content to render
|
|
47
|
+
* @param {string} [props.className] - CSS classes
|
|
48
|
+
* @param {string} [props.as='div'] - HTML element to render as
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* <SafeHtml value="<p>Hello <strong>World</strong></p>" />
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* // With topic links
|
|
55
|
+
* <SafeHtml value='<a href="topic:about">About</a>' />
|
|
56
|
+
*/
|
|
57
|
+
export function SafeHtml({ value, className, as: Component = 'div', ...props }) {
|
|
58
|
+
const { website, getRoutingComponents } = useWebsite()
|
|
59
|
+
|
|
60
|
+
// Get the runtime's SafeHtml if available (handles sanitization)
|
|
61
|
+
const RuntimeSafeHtml = getRoutingComponents()?.SafeHtml
|
|
62
|
+
|
|
63
|
+
// Process the value
|
|
64
|
+
const processedValue = useMemo(() => {
|
|
65
|
+
if (!value) return ''
|
|
66
|
+
|
|
67
|
+
// Handle array of HTML strings
|
|
68
|
+
const html = Array.isArray(value) ? value.join('') : value
|
|
69
|
+
|
|
70
|
+
// Resolve topic: links
|
|
71
|
+
return website ? resolveTopicLinks(html, website) : html
|
|
72
|
+
}, [value, website])
|
|
73
|
+
|
|
74
|
+
// Use runtime SafeHtml if available (recommended for proper sanitization)
|
|
75
|
+
if (RuntimeSafeHtml) {
|
|
76
|
+
return (
|
|
77
|
+
<Suspense fallback={null}>
|
|
78
|
+
<RuntimeSafeHtml value={processedValue} className={className} {...props} />
|
|
79
|
+
</Suspense>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Fallback: render directly (less safe, but works without runtime)
|
|
84
|
+
return (
|
|
85
|
+
<Component
|
|
86
|
+
className={className}
|
|
87
|
+
dangerouslySetInnerHTML={{ __html: processedValue }}
|
|
88
|
+
{...props}
|
|
89
|
+
/>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default SafeHtml
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SafeHtml, default } from './SafeHtml.jsx'
|