@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.
Files changed (37) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +345 -0
  3. package/package.json +47 -0
  4. package/src/components/Asset/Asset.jsx +161 -0
  5. package/src/components/Asset/index.js +1 -0
  6. package/src/components/Disclaimer/Disclaimer.jsx +198 -0
  7. package/src/components/Disclaimer/index.js +1 -0
  8. package/src/components/FileLogo/FileLogo.jsx +148 -0
  9. package/src/components/FileLogo/index.js +1 -0
  10. package/src/components/Icon/Icon.jsx +214 -0
  11. package/src/components/Icon/index.js +1 -0
  12. package/src/components/Image/Image.jsx +194 -0
  13. package/src/components/Image/index.js +1 -0
  14. package/src/components/Link/Link.jsx +261 -0
  15. package/src/components/Link/index.js +1 -0
  16. package/src/components/Media/Media.jsx +322 -0
  17. package/src/components/Media/index.js +1 -0
  18. package/src/components/MediaIcon/MediaIcon.jsx +95 -0
  19. package/src/components/MediaIcon/index.js +1 -0
  20. package/src/components/SafeHtml/SafeHtml.jsx +93 -0
  21. package/src/components/SafeHtml/index.js +1 -0
  22. package/src/components/Section/Render.jsx +245 -0
  23. package/src/components/Section/Section.jsx +131 -0
  24. package/src/components/Section/index.js +3 -0
  25. package/src/components/Section/renderers/Alert.jsx +101 -0
  26. package/src/components/Section/renderers/Code.jsx +70 -0
  27. package/src/components/Section/renderers/Details.jsx +77 -0
  28. package/src/components/Section/renderers/Divider.jsx +42 -0
  29. package/src/components/Section/renderers/Table.jsx +55 -0
  30. package/src/components/Section/renderers/index.js +11 -0
  31. package/src/components/Text/Text.jsx +207 -0
  32. package/src/components/Text/index.js +14 -0
  33. package/src/hooks/index.js +1 -0
  34. package/src/hooks/useWebsite.js +77 -0
  35. package/src/index.js +69 -0
  36. package/src/styles/index.css +8 -0
  37. 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'