@uniweb/kit 0.1.5 → 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.
Files changed (30) hide show
  1. package/package.json +3 -2
  2. package/src/components/Asset/Asset.jsx +31 -81
  3. package/src/components/Media/Media.jsx +27 -125
  4. package/src/components/SocialIcon/index.jsx +146 -0
  5. package/src/hooks/index.js +6 -0
  6. package/src/hooks/useAccordion.js +143 -0
  7. package/src/hooks/useActiveRoute.js +97 -0
  8. package/src/hooks/useGridLayout.js +71 -0
  9. package/src/hooks/useMobileMenu.js +58 -0
  10. package/src/hooks/useScrolled.js +48 -0
  11. package/src/hooks/useTheme.js +205 -0
  12. package/src/index.js +29 -10
  13. package/src/styled/Asset/Asset.jsx +161 -0
  14. package/src/styled/Asset/index.js +1 -0
  15. package/src/{components → styled}/Disclaimer/Disclaimer.jsx +1 -1
  16. package/src/styled/Media/Media.jsx +322 -0
  17. package/src/styled/Media/index.js +1 -0
  18. package/src/{components → styled}/Section/Render.jsx +4 -4
  19. package/src/{components → styled}/Section/index.js +6 -0
  20. package/src/{components → styled}/Section/renderers/Alert.jsx +1 -1
  21. package/src/{components → styled}/Section/renderers/Details.jsx +1 -1
  22. package/src/{components → styled}/Section/renderers/Table.jsx +1 -1
  23. package/src/{components → styled}/Section/renderers/index.js +1 -1
  24. package/src/styled/SidebarLayout/SidebarLayout.jsx +310 -0
  25. package/src/styled/SidebarLayout/index.js +1 -0
  26. package/src/styled/index.js +40 -0
  27. /package/src/{components → styled}/Disclaimer/index.js +0 -0
  28. /package/src/{components → styled}/Section/Section.jsx +0 -0
  29. /package/src/{components → styled}/Section/renderers/Code.jsx +0 -0
  30. /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.5",
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.8"
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 asset preview with download functionality.
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, { useState, useCallback, forwardRef, useImperativeHandle } from '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
- * Check if file is an image
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.withDownload=true] - Show download button
33
- * @param {string} [props.className] - Additional CSS classes
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" profile={profile} />
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, withDownload = true, className, ...props },
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
- <div
110
- className={cn(
111
- 'relative inline-block rounded-lg overflow-hidden border border-gray-200',
112
- 'transition-shadow hover:shadow-md',
113
- className
114
- )}
115
- onMouseEnter={() => setIsHovered(true)}
116
- onMouseLeave={() => setIsHovered(false)}
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
- {/* Preview */}
120
- <div className="w-32 h-32 flex items-center justify-center bg-gray-50">
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
- </div>
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
- * - YouTube embeds
6
- * - Vimeo embeds
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, onReady, onStateChange, className }) {
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={cn('w-full h-full', 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={cn('w-full h-full', 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={cn('w-full h-full object-cover', className)}
116
+ className={className}
142
117
  />
143
118
  )
144
119
  }
145
120
 
146
121
  /**
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
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.thumbnail] - Thumbnail URL
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] - Additional CSS classes
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
- * // YouTube video
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
- * // 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} />
141
+ * <Media src="/videos/intro.mp4" controls className="w-full" />
202
142
  */
203
143
  export function Media({
204
144
  src,
205
145
  media,
206
- thumbnail,
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 || (facade && showVideo)}
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 || (facade && showVideo)}
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 || (facade && showVideo)}
201
+ autoplay={autoplay}
301
202
  muted={muted}
302
203
  loop={loop}
303
204
  controls={controls}
304
- poster={thumbnailSrc}
305
- onProgress={handleProgress}
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 bg-black', className)}
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
@@ -1,2 +1,8 @@
1
1
  export { useWebsite, default } from './useWebsite.js'
2
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'