@uniweb/kit 0.1.4 → 0.1.6
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/package.json +3 -2
- package/src/components/Asset/Asset.jsx +31 -81
- package/src/components/Media/Media.jsx +27 -125
- package/src/components/SocialIcon/index.jsx +146 -0
- package/src/hooks/index.js +7 -0
- package/src/hooks/useAccordion.js +143 -0
- package/src/hooks/useActiveRoute.js +97 -0
- package/src/hooks/useGridLayout.js +71 -0
- package/src/hooks/useMobileMenu.js +58 -0
- package/src/hooks/useRouting.js +119 -0
- package/src/hooks/useScrolled.js +48 -0
- package/src/hooks/useTheme.js +205 -0
- package/src/index.js +29 -10
- package/src/styled/Asset/Asset.jsx +161 -0
- package/src/styled/Asset/index.js +1 -0
- package/src/{components → styled}/Disclaimer/Disclaimer.jsx +1 -1
- package/src/styled/Media/Media.jsx +322 -0
- package/src/styled/Media/index.js +1 -0
- package/src/{components → styled}/Section/Render.jsx +4 -4
- package/src/{components → styled}/Section/index.js +6 -0
- package/src/{components → styled}/Section/renderers/Alert.jsx +1 -1
- package/src/{components → styled}/Section/renderers/Details.jsx +1 -1
- package/src/{components → styled}/Section/renderers/Table.jsx +1 -1
- package/src/{components → styled}/Section/renderers/index.js +1 -1
- package/src/styled/SidebarLayout/SidebarLayout.jsx +310 -0
- package/src/styled/SidebarLayout/index.js +1 -0
- package/src/styled/index.js +40 -0
- /package/src/{components → styled}/Disclaimer/index.js +0 -0
- /package/src/{components → styled}/Section/Section.jsx +0 -0
- /package/src/{components → styled}/Section/renderers/Code.jsx +0 -0
- /package/src/{components → styled}/Section/renderers/Divider.jsx +0 -0
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/kit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Standard component library for Uniweb foundations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/index.js",
|
|
8
|
+
"./styled": "./src/styled/index.js",
|
|
8
9
|
"./styles": "./src/styles/index.css",
|
|
9
10
|
"./search": "./src/search/index.js"
|
|
10
11
|
},
|
|
@@ -36,7 +37,7 @@
|
|
|
36
37
|
},
|
|
37
38
|
"dependencies": {
|
|
38
39
|
"tailwind-merge": "^2.6.0",
|
|
39
|
-
"@uniweb/core": "0.1.
|
|
40
|
+
"@uniweb/core": "0.1.11"
|
|
40
41
|
},
|
|
41
42
|
"peerDependencies": {
|
|
42
43
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -1,55 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Asset Component
|
|
2
|
+
* Asset Component (Plain)
|
|
3
3
|
*
|
|
4
|
-
* File
|
|
4
|
+
* File download link with basic functionality.
|
|
5
|
+
* This is the unstyled version - for styled card with preview,
|
|
6
|
+
* use @uniweb/kit/tailwind.
|
|
5
7
|
*
|
|
6
8
|
* @module @uniweb/kit/Asset
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
|
-
import React, {
|
|
11
|
+
import React, { useCallback, forwardRef, useImperativeHandle } from 'react'
|
|
10
12
|
import { cn } from '../../utils/index.js'
|
|
11
13
|
import { FileLogo } from '../FileLogo/index.js'
|
|
12
|
-
import { Image } from '../Image/index.js'
|
|
13
14
|
import { useWebsite } from '../../hooks/useWebsite.js'
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
|
-
*
|
|
17
|
-
* @param {string} filename
|
|
18
|
-
* @returns {boolean}
|
|
19
|
-
*/
|
|
20
|
-
function isImageFile(filename) {
|
|
21
|
-
if (!filename) return false
|
|
22
|
-
const ext = filename.toLowerCase().split('.').pop()
|
|
23
|
-
return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(ext)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Asset - File preview with download
|
|
17
|
+
* Asset - File download component (plain/unstyled)
|
|
28
18
|
*
|
|
29
19
|
* @param {Object} props
|
|
30
20
|
* @param {string|Object} props.value - Asset identifier or object
|
|
31
21
|
* @param {Object} [props.profile] - Profile object for asset resolution
|
|
32
|
-
* @param {boolean} [props.
|
|
33
|
-
* @param {string} [props.
|
|
22
|
+
* @param {boolean} [props.showIcon=true] - Show file type icon
|
|
23
|
+
* @param {string} [props.iconSize='24'] - Icon size
|
|
24
|
+
* @param {string} [props.className] - CSS classes for the link
|
|
25
|
+
* @param {string} [props.iconClassName] - CSS classes for the icon
|
|
26
|
+
* @param {React.ReactNode} [props.children] - Custom content (overrides default filename display)
|
|
34
27
|
*
|
|
35
28
|
* @example
|
|
36
|
-
* <Asset value="document.pdf"
|
|
29
|
+
* <Asset value="document.pdf" className="text-blue-600 hover:underline" />
|
|
37
30
|
*
|
|
38
31
|
* @example
|
|
39
|
-
* <Asset value={{ src: "/files/report.pdf", filename: "report.pdf" }}
|
|
32
|
+
* <Asset value={{ src: "/files/report.pdf", filename: "report.pdf" }}>
|
|
33
|
+
* Download Report
|
|
34
|
+
* </Asset>
|
|
40
35
|
*/
|
|
41
36
|
export const Asset = forwardRef(function Asset(
|
|
42
|
-
{ value, profile,
|
|
37
|
+
{ value, profile, showIcon = true, iconSize = '24', className, iconClassName, children, ...props },
|
|
43
38
|
ref
|
|
44
39
|
) {
|
|
45
40
|
const { localize } = useWebsite()
|
|
46
|
-
const [imageError, setImageError] = useState(false)
|
|
47
|
-
const [isHovered, setIsHovered] = useState(false)
|
|
48
41
|
|
|
49
42
|
// Resolve asset info
|
|
50
43
|
let src = ''
|
|
51
44
|
let filename = ''
|
|
52
|
-
let alt = ''
|
|
53
45
|
|
|
54
46
|
if (typeof value === 'string') {
|
|
55
47
|
src = value
|
|
@@ -57,7 +49,6 @@ export const Asset = forwardRef(function Asset(
|
|
|
57
49
|
} else if (value && typeof value === 'object') {
|
|
58
50
|
src = value.src || value.url || ''
|
|
59
51
|
filename = value.filename || value.name || src.split('/').pop() || ''
|
|
60
|
-
alt = value.alt || filename
|
|
61
52
|
}
|
|
62
53
|
|
|
63
54
|
// Use profile to resolve asset if available
|
|
@@ -65,17 +56,15 @@ export const Asset = forwardRef(function Asset(
|
|
|
65
56
|
const assetInfo = profile.getAssetInfo(value, true, filename)
|
|
66
57
|
src = assetInfo?.src || src
|
|
67
58
|
filename = assetInfo?.filename || filename
|
|
68
|
-
alt = assetInfo?.alt || alt
|
|
69
59
|
}
|
|
70
60
|
|
|
71
|
-
const isImage = isImageFile(filename)
|
|
72
|
-
|
|
73
61
|
// Handle download
|
|
74
|
-
const handleDownload = useCallback(async () => {
|
|
62
|
+
const handleDownload = useCallback(async (e) => {
|
|
75
63
|
if (!src) return
|
|
76
64
|
|
|
65
|
+
e.preventDefault()
|
|
66
|
+
|
|
77
67
|
try {
|
|
78
|
-
// Try to trigger download
|
|
79
68
|
const downloadUrl = src.includes('?') ? `${src}&download=true` : `${src}?download=true`
|
|
80
69
|
const response = await fetch(downloadUrl)
|
|
81
70
|
const blob = await response.blob()
|
|
@@ -98,63 +87,24 @@ export const Asset = forwardRef(function Asset(
|
|
|
98
87
|
triggerDownload: handleDownload
|
|
99
88
|
}), [handleDownload])
|
|
100
89
|
|
|
101
|
-
// Handle image error
|
|
102
|
-
const handleImageError = useCallback(() => {
|
|
103
|
-
setImageError(true)
|
|
104
|
-
}, [])
|
|
105
|
-
|
|
106
90
|
if (!src) return null
|
|
107
91
|
|
|
108
92
|
return (
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
93
|
+
<a
|
|
94
|
+
href={src}
|
|
95
|
+
onClick={handleDownload}
|
|
96
|
+
className={cn('inline-flex items-center gap-2', className)}
|
|
97
|
+
title={localize({
|
|
98
|
+
en: `Download ${filename}`,
|
|
99
|
+
fr: `Télécharger ${filename}`
|
|
100
|
+
})}
|
|
117
101
|
{...props}
|
|
118
102
|
>
|
|
119
|
-
{
|
|
120
|
-
|
|
121
|
-
{isImage && !imageError ? (
|
|
122
|
-
<Image
|
|
123
|
-
src={src}
|
|
124
|
-
alt={alt}
|
|
125
|
-
className="w-full h-full object-cover"
|
|
126
|
-
onError={handleImageError}
|
|
127
|
-
/>
|
|
128
|
-
) : (
|
|
129
|
-
<FileLogo filename={filename} size="48" className="text-gray-400" />
|
|
130
|
-
)}
|
|
131
|
-
</div>
|
|
132
|
-
|
|
133
|
-
{/* Filename */}
|
|
134
|
-
<div className="px-2 py-1 text-xs text-gray-600 truncate max-w-[128px]" title={filename}>
|
|
135
|
-
{filename}
|
|
136
|
-
</div>
|
|
137
|
-
|
|
138
|
-
{/* Download overlay */}
|
|
139
|
-
{withDownload && (
|
|
140
|
-
<button
|
|
141
|
-
onClick={handleDownload}
|
|
142
|
-
className={cn(
|
|
143
|
-
'absolute inset-0 flex items-center justify-center',
|
|
144
|
-
'bg-black/50 text-white transition-opacity',
|
|
145
|
-
isHovered ? 'opacity-100' : 'opacity-0'
|
|
146
|
-
)}
|
|
147
|
-
aria-label={localize({
|
|
148
|
-
en: 'Download file',
|
|
149
|
-
fr: 'Télécharger le fichier'
|
|
150
|
-
})}
|
|
151
|
-
>
|
|
152
|
-
<svg className="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
153
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
|
154
|
-
</svg>
|
|
155
|
-
</button>
|
|
103
|
+
{showIcon && (
|
|
104
|
+
<FileLogo filename={filename} size={iconSize} className={iconClassName} />
|
|
156
105
|
)}
|
|
157
|
-
|
|
106
|
+
{children || filename}
|
|
107
|
+
</a>
|
|
158
108
|
)
|
|
159
109
|
})
|
|
160
110
|
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Media Component
|
|
2
|
+
* Media Component (Plain)
|
|
3
3
|
*
|
|
4
|
-
* Video player supporting
|
|
5
|
-
* -
|
|
6
|
-
*
|
|
7
|
-
* - Local/direct video files
|
|
8
|
-
* - Thumbnail facades
|
|
9
|
-
* - Playback tracking
|
|
4
|
+
* Video player supporting YouTube, Vimeo, and local video files.
|
|
5
|
+
* This is the unstyled version - for styled facade with play button,
|
|
6
|
+
* use @uniweb/kit/tailwind.
|
|
10
7
|
*
|
|
11
8
|
* @module @uniweb/kit/Media
|
|
12
9
|
*/
|
|
@@ -17,8 +14,6 @@ import { detectMediaType } from '../../utils/index.js'
|
|
|
17
14
|
|
|
18
15
|
/**
|
|
19
16
|
* Extract YouTube video ID from URL
|
|
20
|
-
* @param {string} url
|
|
21
|
-
* @returns {string|null}
|
|
22
17
|
*/
|
|
23
18
|
function getYouTubeId(url) {
|
|
24
19
|
if (!url) return null
|
|
@@ -28,8 +23,6 @@ function getYouTubeId(url) {
|
|
|
28
23
|
|
|
29
24
|
/**
|
|
30
25
|
* Extract Vimeo video ID from URL
|
|
31
|
-
* @param {string} url
|
|
32
|
-
* @returns {string|null}
|
|
33
26
|
*/
|
|
34
27
|
function getVimeoId(url) {
|
|
35
28
|
if (!url) return null
|
|
@@ -37,27 +30,10 @@ function getVimeoId(url) {
|
|
|
37
30
|
return match?.[1] || null
|
|
38
31
|
}
|
|
39
32
|
|
|
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
33
|
/**
|
|
56
34
|
* YouTube Player Component
|
|
57
35
|
*/
|
|
58
|
-
function YouTubePlayer({ videoId, autoplay, muted, loop,
|
|
59
|
-
const iframeRef = useRef(null)
|
|
60
|
-
|
|
36
|
+
function YouTubePlayer({ videoId, autoplay, muted, loop, className }) {
|
|
61
37
|
const params = new URLSearchParams({
|
|
62
38
|
enablejsapi: '1',
|
|
63
39
|
autoplay: autoplay ? '1' : '0',
|
|
@@ -70,9 +46,8 @@ function YouTubePlayer({ videoId, autoplay, muted, loop, onReady, onStateChange,
|
|
|
70
46
|
|
|
71
47
|
return (
|
|
72
48
|
<iframe
|
|
73
|
-
ref={iframeRef}
|
|
74
49
|
src={`https://www.youtube.com/embed/${videoId}?${params}`}
|
|
75
|
-
className={
|
|
50
|
+
className={className}
|
|
76
51
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
77
52
|
allowFullScreen
|
|
78
53
|
title="YouTube video"
|
|
@@ -94,7 +69,7 @@ function VimeoPlayer({ videoId, autoplay, muted, loop, className }) {
|
|
|
94
69
|
return (
|
|
95
70
|
<iframe
|
|
96
71
|
src={`https://player.vimeo.com/video/${videoId}?${params}`}
|
|
97
|
-
className={
|
|
72
|
+
className={className}
|
|
98
73
|
allow="autoplay; fullscreen; picture-in-picture"
|
|
99
74
|
allowFullScreen
|
|
100
75
|
title="Vimeo video"
|
|
@@ -138,133 +113,57 @@ function LocalVideo({ src, autoplay, muted, loop, controls, poster, onProgress,
|
|
|
138
113
|
controls={controls}
|
|
139
114
|
poster={poster}
|
|
140
115
|
playsInline
|
|
141
|
-
className={
|
|
116
|
+
className={className}
|
|
142
117
|
/>
|
|
143
118
|
)
|
|
144
119
|
}
|
|
145
120
|
|
|
146
121
|
/**
|
|
147
|
-
*
|
|
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
|
|
122
|
+
* Media - Video player component (plain/unstyled)
|
|
172
123
|
*
|
|
173
124
|
* @param {Object} props
|
|
174
125
|
* @param {string|Object} props.src - Video URL or media object
|
|
175
126
|
* @param {Object} [props.media] - Media object with src/caption
|
|
176
|
-
* @param {string} [props.
|
|
127
|
+
* @param {string} [props.poster] - Poster/thumbnail URL for local video
|
|
177
128
|
* @param {boolean} [props.autoplay=false] - Auto-play video
|
|
178
129
|
* @param {boolean} [props.muted=false] - Mute video
|
|
179
130
|
* @param {boolean} [props.loop=false] - Loop video
|
|
180
131
|
* @param {boolean} [props.controls=true] - Show controls
|
|
181
|
-
* @param {boolean} [props.facade=false] - Show thumbnail with play button
|
|
182
132
|
* @param {string} [props.aspectRatio='16/9'] - Aspect ratio
|
|
183
|
-
* @param {string} [props.className] -
|
|
133
|
+
* @param {string} [props.className] - CSS classes for the container
|
|
134
|
+
* @param {string} [props.videoClassName] - CSS classes for the video element
|
|
184
135
|
* @param {Function} [props.onProgress] - Progress callback for tracking
|
|
185
|
-
* @param {Object} [props.block] - Block object for event tracking
|
|
186
136
|
*
|
|
187
137
|
* @example
|
|
188
|
-
*
|
|
189
|
-
* <Media src="https://youtube.com/watch?v=abc123" />
|
|
138
|
+
* <Media src="https://youtube.com/watch?v=abc123" className="rounded-lg" />
|
|
190
139
|
*
|
|
191
140
|
* @example
|
|
192
|
-
*
|
|
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} />
|
|
141
|
+
* <Media src="/videos/intro.mp4" controls className="w-full" />
|
|
202
142
|
*/
|
|
203
143
|
export function Media({
|
|
204
144
|
src,
|
|
205
145
|
media,
|
|
206
|
-
|
|
146
|
+
poster,
|
|
207
147
|
autoplay = false,
|
|
208
148
|
muted = false,
|
|
209
149
|
loop = false,
|
|
210
150
|
controls = true,
|
|
211
|
-
facade = false,
|
|
212
151
|
aspectRatio = '16/9',
|
|
213
152
|
className,
|
|
153
|
+
videoClassName,
|
|
214
154
|
onProgress,
|
|
215
|
-
block,
|
|
216
155
|
...props
|
|
217
156
|
}) {
|
|
218
|
-
const [showVideo, setShowVideo] = useState(!facade)
|
|
219
|
-
|
|
220
157
|
// Normalize source
|
|
221
158
|
const videoSrc = typeof src === 'string' ? src : (src?.src || media?.src || '')
|
|
222
|
-
const caption = media?.caption || src?.caption || ''
|
|
223
159
|
|
|
224
160
|
// Detect video type
|
|
225
161
|
const mediaType = detectMediaType(videoSrc)
|
|
226
162
|
|
|
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
163
|
// Render video player
|
|
267
164
|
const videoContent = (() => {
|
|
165
|
+
const playerClass = cn('w-full h-full', videoClassName)
|
|
166
|
+
|
|
268
167
|
switch (mediaType) {
|
|
269
168
|
case 'youtube': {
|
|
270
169
|
const videoId = getYouTubeId(videoSrc)
|
|
@@ -272,9 +171,10 @@ export function Media({
|
|
|
272
171
|
return (
|
|
273
172
|
<YouTubePlayer
|
|
274
173
|
videoId={videoId}
|
|
275
|
-
autoplay={autoplay
|
|
174
|
+
autoplay={autoplay}
|
|
276
175
|
muted={muted}
|
|
277
176
|
loop={loop}
|
|
177
|
+
className={playerClass}
|
|
278
178
|
/>
|
|
279
179
|
)
|
|
280
180
|
}
|
|
@@ -285,9 +185,10 @@ export function Media({
|
|
|
285
185
|
return (
|
|
286
186
|
<VimeoPlayer
|
|
287
187
|
videoId={videoId}
|
|
288
|
-
autoplay={autoplay
|
|
188
|
+
autoplay={autoplay}
|
|
289
189
|
muted={muted}
|
|
290
190
|
loop={loop}
|
|
191
|
+
className={playerClass}
|
|
291
192
|
/>
|
|
292
193
|
)
|
|
293
194
|
}
|
|
@@ -297,12 +198,13 @@ export function Media({
|
|
|
297
198
|
return (
|
|
298
199
|
<LocalVideo
|
|
299
200
|
src={videoSrc}
|
|
300
|
-
autoplay={autoplay
|
|
201
|
+
autoplay={autoplay}
|
|
301
202
|
muted={muted}
|
|
302
203
|
loop={loop}
|
|
303
204
|
controls={controls}
|
|
304
|
-
poster={
|
|
305
|
-
onProgress={
|
|
205
|
+
poster={poster}
|
|
206
|
+
onProgress={onProgress}
|
|
207
|
+
className={playerClass}
|
|
306
208
|
/>
|
|
307
209
|
)
|
|
308
210
|
}
|
|
@@ -310,7 +212,7 @@ export function Media({
|
|
|
310
212
|
|
|
311
213
|
return (
|
|
312
214
|
<div
|
|
313
|
-
className={cn('relative overflow-hidden
|
|
215
|
+
className={cn('relative overflow-hidden', className)}
|
|
314
216
|
style={{ aspectRatio }}
|
|
315
217
|
{...props}
|
|
316
218
|
>
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Social Link Utilities
|
|
3
|
+
*
|
|
4
|
+
* Utilities for detecting social platforms from URLs and rendering social icons.
|
|
5
|
+
* Consolidates duplicated patterns from Team and Footer components.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* import { getSocialPlatform, SocialIcon } from '@uniweb/kit'
|
|
9
|
+
*
|
|
10
|
+
* const platform = getSocialPlatform('https://twitter.com/user')
|
|
11
|
+
* // Returns: 'twitter'
|
|
12
|
+
*
|
|
13
|
+
* <SocialIcon platform="twitter" className="w-5 h-5" />
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // Auto-detect from URL
|
|
17
|
+
* <SocialIcon url="https://github.com/user" className="w-5 h-5" />
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import React from 'react'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Social platform domain patterns
|
|
24
|
+
*/
|
|
25
|
+
const SOCIAL_PATTERNS = {
|
|
26
|
+
twitter: ['twitter.com', 'x.com'],
|
|
27
|
+
linkedin: ['linkedin.com'],
|
|
28
|
+
github: ['github.com'],
|
|
29
|
+
facebook: ['facebook.com', 'fb.com'],
|
|
30
|
+
instagram: ['instagram.com'],
|
|
31
|
+
youtube: ['youtube.com', 'youtu.be'],
|
|
32
|
+
tiktok: ['tiktok.com'],
|
|
33
|
+
discord: ['discord.gg', 'discord.com'],
|
|
34
|
+
slack: ['slack.com'],
|
|
35
|
+
mastodon: ['mastodon.social', 'mastodon.online'],
|
|
36
|
+
bluesky: ['bsky.app', 'bsky.social'],
|
|
37
|
+
threads: ['threads.net'],
|
|
38
|
+
// Academic
|
|
39
|
+
scholar: ['scholar.google.com'],
|
|
40
|
+
orcid: ['orcid.org'],
|
|
41
|
+
researchgate: ['researchgate.net'],
|
|
42
|
+
// Email
|
|
43
|
+
email: ['mailto:'],
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Detect social platform from URL
|
|
48
|
+
*
|
|
49
|
+
* @param {string} url - URL to analyze
|
|
50
|
+
* @returns {string|null} Platform name or null if not recognized
|
|
51
|
+
*/
|
|
52
|
+
export function getSocialPlatform(url) {
|
|
53
|
+
if (!url) return null
|
|
54
|
+
|
|
55
|
+
const lowerUrl = url.toLowerCase()
|
|
56
|
+
|
|
57
|
+
for (const [platform, patterns] of Object.entries(SOCIAL_PATTERNS)) {
|
|
58
|
+
if (patterns.some(pattern => lowerUrl.includes(pattern))) {
|
|
59
|
+
return platform
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if URL is a social link
|
|
68
|
+
*
|
|
69
|
+
* @param {string} url - URL to check
|
|
70
|
+
* @returns {boolean}
|
|
71
|
+
*/
|
|
72
|
+
export function isSocialLink(url) {
|
|
73
|
+
return getSocialPlatform(url) !== null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Filter links to only social links
|
|
78
|
+
*
|
|
79
|
+
* @param {Array} links - Array of link objects with href/url property
|
|
80
|
+
* @returns {Array} Filtered array of social links
|
|
81
|
+
*/
|
|
82
|
+
export function filterSocialLinks(links) {
|
|
83
|
+
if (!Array.isArray(links)) return []
|
|
84
|
+
return links.filter(link => isSocialLink(link.href || link.url))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* SVG icon paths for each platform
|
|
89
|
+
*/
|
|
90
|
+
const ICON_PATHS = {
|
|
91
|
+
twitter: 'M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z',
|
|
92
|
+
linkedin: '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',
|
|
93
|
+
github: 'M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z',
|
|
94
|
+
facebook: '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',
|
|
95
|
+
instagram: '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.76-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',
|
|
96
|
+
youtube: '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',
|
|
97
|
+
tiktok: 'M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z',
|
|
98
|
+
discord: 'M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189z',
|
|
99
|
+
email: 'M1.5 8.67v8.58a3 3 0 003 3h15a3 3 0 003-3V8.67l-8.928 5.493a3 3 0 01-3.144 0L1.5 8.67z M22.5 6.908V6.75a3 3 0 00-3-3h-15a3 3 0 00-3 3v.158l9.714 5.978a1.5 1.5 0 001.572 0L22.5 6.908z',
|
|
100
|
+
scholar: 'M5.242 13.769L0 9.5 12 0l12 9.5-5.242 4.269C17.548 11.249 14.978 9.5 12 9.5c-2.977 0-5.548 1.748-6.758 4.269zM12 10a7 7 0 1 0 0 14 7 7 0 0 0 0-14z',
|
|
101
|
+
orcid: '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 0 1-.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',
|
|
102
|
+
researchgate: 'M19.586 0c-.818 0-1.508.19-2.073.565-.563.377-.97.936-1.213 1.68a3.193 3.193 0 0 0-.112.437 8.365 8.365 0 0 0-.078.53 9 9 0 0 0-.05.727c-.01.282-.013.621-.013 1.016a31.121 31.123 0 0 0 .014 1.017 9 9 0 0 0 .05.727 7.946 7.946 0 0 0 .077.53h-.005a3.334 3.334 0 0 0 .113.438c.245.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 0 0 .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 0 1-.147-.762 17.366 17.367 0 0 1-.034-.656c-.01-.26-.014-.572-.014-.939a26.401 26.403 0 0 1 .014-.938 15.821 15.822 0 0 1 .035-.656 3.19 3.19 0 0 1 .148-.76 1.89 1.89 0 0 1 .742-1.01c.344-.244.593-.352 1.137-.352.508 0 .815.09 1.144.326.33.236.53.588.652 1.14.025.12.037.313.037.313h1.458s.036-.353-.023-.6a3.381 3.381 0 0 0-.146-.58 3.716 3.716 0 0 0-1.259-1.652C21.08.193 20.328 0 19.586 0zm-9.586.12h-3.94v.477h1.442v5.373H5.586v-.003H5v.003H1.5v3.793c0 .095.047.142.14.142h.917c.093 0 .14-.047.14-.142v-2.79h2.77c1.053 0 1.58-.636 1.58-1.905V1.074c0-.095-.047-.143-.14-.143H5.99c-.093 0-.14.048-.14.143v3.983c0 .56-.242.84-.725.84h-.99V.6H6v-.48zM4.86 7.27h.913c.095 0 .143.047.143.14v.96c0 .095-.048.142-.143.142H4.86a.136.136 0 0 1-.14-.141v-.96c0-.094.047-.141.14-.141z',
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* SocialIcon Component
|
|
107
|
+
*
|
|
108
|
+
* Renders an SVG icon for a social platform.
|
|
109
|
+
*
|
|
110
|
+
* @param {Object} props
|
|
111
|
+
* @param {string} [props.platform] - Platform name (twitter, linkedin, etc.)
|
|
112
|
+
* @param {string} [props.url] - URL to auto-detect platform from
|
|
113
|
+
* @param {string} [props.className] - CSS classes for styling
|
|
114
|
+
*/
|
|
115
|
+
export function SocialIcon({ platform, url, className = 'w-5 h-5' }) {
|
|
116
|
+
// Auto-detect platform from URL if not provided
|
|
117
|
+
const detectedPlatform = platform || getSocialPlatform(url)
|
|
118
|
+
|
|
119
|
+
if (!detectedPlatform) {
|
|
120
|
+
// Return generic link icon
|
|
121
|
+
return (
|
|
122
|
+
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
123
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
|
|
124
|
+
</svg>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const path = ICON_PATHS[detectedPlatform]
|
|
129
|
+
|
|
130
|
+
if (!path) {
|
|
131
|
+
// Fallback for platforms without icons
|
|
132
|
+
return (
|
|
133
|
+
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
134
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
|
|
135
|
+
</svg>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<svg className={className} fill="currentColor" viewBox="0 0 24 24">
|
|
141
|
+
<path d={path} />
|
|
142
|
+
</svg>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export default SocialIcon
|
package/src/hooks/index.js
CHANGED
|
@@ -1 +1,8 @@
|
|
|
1
1
|
export { useWebsite, default } from './useWebsite.js'
|
|
2
|
+
export { useRouting } from './useRouting.js'
|
|
3
|
+
export { useActiveRoute } from './useActiveRoute.js'
|
|
4
|
+
export { useScrolled } from './useScrolled.js'
|
|
5
|
+
export { useMobileMenu } from './useMobileMenu.js'
|
|
6
|
+
export { useAccordion } from './useAccordion.js'
|
|
7
|
+
export { useGridLayout, getGridClasses } from './useGridLayout.js'
|
|
8
|
+
export { useTheme, getThemeClasses, THEMES, THEME_NAMES } from './useTheme.js'
|