@uniweb/kit 0.7.10 → 0.7.11

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 CHANGED
@@ -1,14 +1,11 @@
1
1
  {
2
2
  "name": "@uniweb/kit",
3
- "version": "0.7.10",
3
+ "version": "0.7.11",
4
4
  "description": "Standard component library for Uniweb foundations",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": "./src/index.js",
8
- "./theme-tokens.css": {
9
- "style": "./src/theme-tokens.css",
10
- "default": "./src/theme-tokens.css"
11
- }
8
+ "./theme-tokens.css": "./src/theme-tokens.css"
12
9
  },
13
10
  "files": [
14
11
  "src",
@@ -37,11 +34,11 @@
37
34
  "node": ">=20.19"
38
35
  },
39
36
  "dependencies": {
37
+ "@uniweb/core": "latest",
40
38
  "clsx": "^2.1.0",
41
39
  "fuse.js": "^7.0.0",
42
40
  "shiki": "^3.0.0",
43
- "tailwind-merge": "^2.6.0",
44
- "@uniweb/core": "0.5.10"
41
+ "tailwind-merge": "^2.6.0"
45
42
  },
46
43
  "peerDependencies": {
47
44
  "react": "^18.0.0 || ^19.0.0",
@@ -19,13 +19,13 @@ import { cn } from "../../utils/index.js";
19
19
  * Size presets for images
20
20
  */
21
21
  const SIZE_CLASSES = {
22
- xs: "w-8 h-8",
23
- sm: "w-12 h-12",
24
- md: "w-16 h-16",
25
- lg: "w-24 h-24",
26
- xl: "w-32 h-32",
27
- "2xl": "w-48 h-48",
28
- full: "w-full h-full",
22
+ xs: "w-8 h-8",
23
+ sm: "w-12 h-12",
24
+ md: "w-16 h-16",
25
+ lg: "w-24 h-24",
26
+ xl: "w-32 h-32",
27
+ "2xl": "w-48 h-48",
28
+ full: "w-full h-full",
29
29
  };
30
30
 
31
31
  /**
@@ -34,18 +34,18 @@ const SIZE_CLASSES = {
34
34
  * @returns {string} CSS filter value
35
35
  */
36
36
  function buildFilterStyle(filter) {
37
- if (!filter || typeof filter !== "object") return undefined;
37
+ if (!filter || typeof filter !== "object") return undefined;
38
38
 
39
- const filters = [];
39
+ const filters = [];
40
40
 
41
- if (filter.blur) filters.push(`blur(${filter.blur}px)`);
42
- if (filter.brightness) filters.push(`brightness(${filter.brightness}%)`);
43
- if (filter.contrast) filters.push(`contrast(${filter.contrast}%)`);
44
- if (filter.grayscale) filters.push(`grayscale(${filter.grayscale}%)`);
45
- if (filter.saturate) filters.push(`saturate(${filter.saturate}%)`);
46
- if (filter.sepia) filters.push(`sepia(${filter.sepia}%)`);
41
+ if (filter.blur) filters.push(`blur(${filter.blur}px)`);
42
+ if (filter.brightness) filters.push(`brightness(${filter.brightness}%)`);
43
+ if (filter.contrast) filters.push(`contrast(${filter.contrast}%)`);
44
+ if (filter.grayscale) filters.push(`grayscale(${filter.grayscale}%)`);
45
+ if (filter.saturate) filters.push(`saturate(${filter.saturate}%)`);
46
+ if (filter.sepia) filters.push(`sepia(${filter.sepia}%)`);
47
47
 
48
- return filters.length > 0 ? filters.join(" ") : undefined;
48
+ return filters.length > 0 ? filters.join(" ") : undefined;
49
49
  }
50
50
 
51
51
  /**
@@ -85,116 +85,115 @@ function buildFilterStyle(filter) {
85
85
  * <Image src="/logo.png" href="/about" alt="Company logo" />
86
86
  */
87
87
  export function Image({
88
- profile,
89
- type,
90
- size,
91
- value,
92
- src,
93
- url,
94
- alt = "",
95
- href,
96
- rounded,
97
- filter,
98
- loading = "lazy",
99
- className,
100
- ariaHidden,
101
- onError,
102
- onLoad,
103
- ...props
88
+ profile,
89
+ type,
90
+ size,
91
+ value,
92
+ src,
93
+ url,
94
+ alt = "",
95
+ href,
96
+ rounded,
97
+ filter,
98
+ loading = "lazy",
99
+ className,
100
+ ariaHidden,
101
+ onError,
102
+ onLoad,
103
+ ...props
104
104
  }) {
105
- const [hasError, setHasError] = useState(false);
106
- const [imageSrc, setImageSrc] = useState(null);
107
-
108
- // Determine the image source
109
- let resolvedSrc = src || url || "";
110
- let resolvedAlt = alt;
111
-
112
- // Handle profile-based images
113
- if (profile && type) {
114
- if (type === "avatar" || type === "banner") {
115
- // Use profile methods if available
116
- if (typeof profile.getImageInfo === "function") {
117
- const imageInfo = profile.getImageInfo(type, size);
118
- resolvedSrc = imageInfo?.url || resolvedSrc;
119
- resolvedAlt = imageInfo?.alt || resolvedAlt;
120
- }
121
- } else if (value && typeof profile.getAssetInfo === "function") {
122
- const assetInfo = profile.getAssetInfo(value, true, alt);
123
- resolvedSrc = assetInfo?.src || resolvedSrc;
124
- resolvedAlt = assetInfo?.alt || resolvedAlt;
105
+ const [hasError, setHasError] = useState(false);
106
+
107
+ // Determine the image source
108
+ let resolvedSrc = src || url || "";
109
+ let resolvedAlt = alt;
110
+
111
+ // Handle profile-based images
112
+ if (profile && type) {
113
+ if (type === "avatar" || type === "banner") {
114
+ // Use profile methods if available
115
+ if (typeof profile.getImageInfo === "function") {
116
+ const imageInfo = profile.getImageInfo(type, size);
117
+ resolvedSrc = imageInfo?.url || resolvedSrc;
118
+ resolvedAlt = imageInfo?.alt || resolvedAlt;
119
+ }
120
+ } else if (value && typeof profile.getAssetInfo === "function") {
121
+ const assetInfo = profile.getAssetInfo(value, true, alt);
122
+ resolvedSrc = assetInfo?.src || resolvedSrc;
123
+ resolvedAlt = assetInfo?.alt || resolvedAlt;
124
+ }
125
125
  }
126
- }
127
-
128
- // Handle value as direct source
129
- if (!resolvedSrc && value) {
130
- if (typeof value === "string") {
131
- resolvedSrc = value;
132
- } else if (value.url || value.src) {
133
- resolvedSrc = value.url || value.src;
134
- resolvedAlt = value.alt || resolvedAlt;
126
+
127
+ // Handle value as direct source
128
+ if (!resolvedSrc && value) {
129
+ if (typeof value === "string") {
130
+ resolvedSrc = value;
131
+ } else if (value.url || value.src) {
132
+ resolvedSrc = value.url || value.src;
133
+ resolvedAlt = value.alt || resolvedAlt;
134
+ }
135
+ }
136
+
137
+ // Build classes
138
+ const sizeClass = size && SIZE_CLASSES[size];
139
+ const roundedClass =
140
+ rounded === true
141
+ ? "rounded-full"
142
+ : typeof rounded === "string"
143
+ ? rounded
144
+ : "";
145
+
146
+ const imageClasses = cn("object-cover", sizeClass, roundedClass, className);
147
+
148
+ // Build filter style
149
+ const filterStyle = buildFilterStyle(filter);
150
+
151
+ // Handle error
152
+ const handleError = useCallback(
153
+ (e) => {
154
+ setHasError(true);
155
+ onError?.(e);
156
+ },
157
+ [onError],
158
+ );
159
+
160
+ // Handle load
161
+ const handleLoad = useCallback(
162
+ (e) => {
163
+ onLoad?.(e);
164
+ },
165
+ [onLoad],
166
+ );
167
+
168
+ // Don't render if no source or error
169
+ if (!resolvedSrc || hasError) {
170
+ return null;
135
171
  }
136
- }
137
-
138
- // Build classes
139
- const sizeClass = size && SIZE_CLASSES[size];
140
- const roundedClass =
141
- rounded === true
142
- ? "rounded-full"
143
- : typeof rounded === "string"
144
- ? rounded
145
- : "";
146
-
147
- const imageClasses = cn("object-cover", sizeClass, roundedClass, className);
148
-
149
- // Build filter style
150
- const filterStyle = buildFilterStyle(filter);
151
-
152
- // Handle error
153
- const handleError = useCallback(
154
- (e) => {
155
- setHasError(true);
156
- onError?.(e);
157
- },
158
- [onError]
159
- );
160
-
161
- // Handle load
162
- const handleLoad = useCallback(
163
- (e) => {
164
- onLoad?.(e);
165
- },
166
- [onLoad]
167
- );
168
-
169
- // Don't render if no source or error
170
- if (!resolvedSrc || hasError) {
171
- return null;
172
- }
173
-
174
- const imageElement = (
175
- <img
176
- src={resolvedSrc}
177
- alt={resolvedAlt}
178
- loading={loading}
179
- className={imageClasses}
180
- style={filterStyle ? { filter: filterStyle } : undefined}
181
- onError={handleError}
182
- onLoad={handleLoad}
183
- aria-hidden={ariaHidden}
184
- {...props}
185
- />
186
- );
187
-
188
- // Wrap in link if href provided
189
- if (href) {
190
- return (
191
- <Link to={href} className="inline-block">
192
- {imageElement}
193
- </Link>
172
+
173
+ const imageElement = (
174
+ <img
175
+ src={resolvedSrc}
176
+ alt={resolvedAlt}
177
+ loading={loading}
178
+ className={imageClasses}
179
+ style={filterStyle ? { filter: filterStyle } : undefined}
180
+ onError={handleError}
181
+ onLoad={handleLoad}
182
+ aria-hidden={ariaHidden}
183
+ {...props}
184
+ />
194
185
  );
195
- }
196
186
 
197
- return imageElement;
187
+ // Wrap in link if href provided
188
+ if (href) {
189
+ return (
190
+ <Link to={href} className="inline-block">
191
+ {imageElement}
192
+ </Link>
193
+ );
194
+ }
195
+
196
+ return imageElement;
198
197
  }
199
198
 
200
199
  export default Image;
@@ -8,114 +8,136 @@
8
8
  * @module @uniweb/kit/Media
9
9
  */
10
10
 
11
- import React, { useState, useEffect, useRef, useCallback } from 'react'
12
- import { cn } from '../../utils/index.js'
13
- import { detectMediaType } from '../../utils/index.js'
11
+ import React, { useState, useEffect, useRef } from "react";
12
+ import { cn } from "../../utils/index.js";
13
+ import { detectMediaType } from "../../utils/index.js";
14
14
 
15
15
  /**
16
16
  * Extract YouTube video ID from URL
17
17
  */
18
18
  function getYouTubeId(url) {
19
- if (!url) return null
20
- const match = url.match(/(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([^&?/]+)/)
21
- return match?.[1] || null
19
+ if (!url) return null;
20
+ const match = url.match(
21
+ /(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([^&?/]+)/,
22
+ );
23
+ return match?.[1] || null;
22
24
  }
23
25
 
24
26
  /**
25
27
  * Extract Vimeo video ID from URL
26
28
  */
27
29
  function getVimeoId(url) {
28
- if (!url) return null
29
- const match = url.match(/vimeo\.com\/(?:video\/)?(\d+)/)
30
- return match?.[1] || null
30
+ if (!url) return null;
31
+ const match = url.match(/vimeo\.com\/(?:video\/)?(\d+)/);
32
+ return match?.[1] || null;
31
33
  }
32
34
 
33
35
  /**
34
36
  * YouTube Player Component
35
37
  */
36
38
  function YouTubePlayer({ videoId, autoplay, muted, loop, className }) {
37
- const params = new URLSearchParams({
38
- enablejsapi: '1',
39
- autoplay: autoplay ? '1' : '0',
40
- mute: muted ? '1' : '0',
41
- loop: loop ? '1' : '0',
42
- playlist: loop ? videoId : '',
43
- rel: '0',
44
- modestbranding: '1'
45
- })
46
-
47
- return (
48
- <iframe
49
- src={`https://www.youtube.com/embed/${videoId}?${params}`}
50
- className={className}
51
- allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
52
- allowFullScreen
53
- title="YouTube video"
54
- />
55
- )
39
+ const params = new URLSearchParams({
40
+ enablejsapi: "1",
41
+ autoplay: autoplay ? "1" : "0",
42
+ mute: muted ? "1" : "0",
43
+ loop: loop ? "1" : "0",
44
+ playlist: loop ? videoId : "",
45
+ rel: "0",
46
+ modestbranding: "1",
47
+ });
48
+
49
+ return (
50
+ <iframe
51
+ src={`https://www.youtube.com/embed/${videoId}?${params}`}
52
+ className={className}
53
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
54
+ allowFullScreen
55
+ title="YouTube video"
56
+ />
57
+ );
56
58
  }
57
59
 
58
60
  /**
59
61
  * Vimeo Player Component
60
62
  */
61
63
  function VimeoPlayer({ videoId, autoplay, muted, loop, className }) {
62
- const params = new URLSearchParams({
63
- autoplay: autoplay ? '1' : '0',
64
- muted: muted ? '1' : '0',
65
- loop: loop ? '1' : '0',
66
- dnt: '1'
67
- })
68
-
69
- return (
70
- <iframe
71
- src={`https://player.vimeo.com/video/${videoId}?${params}`}
72
- className={className}
73
- allow="autoplay; fullscreen; picture-in-picture"
74
- allowFullScreen
75
- title="Vimeo video"
76
- />
77
- )
64
+ const params = new URLSearchParams({
65
+ autoplay: autoplay ? "1" : "0",
66
+ muted: muted ? "1" : "0",
67
+ loop: loop ? "1" : "0",
68
+ dnt: "1",
69
+ });
70
+
71
+ return (
72
+ <iframe
73
+ src={`https://player.vimeo.com/video/${videoId}?${params}`}
74
+ className={className}
75
+ allow="autoplay; fullscreen; picture-in-picture"
76
+ allowFullScreen
77
+ title="Vimeo video"
78
+ />
79
+ );
78
80
  }
79
81
 
80
82
  /**
81
83
  * Local/Direct Video Player Component
82
84
  */
83
- function LocalVideo({ src, autoplay, muted, loop, controls, poster, onProgress, className }) {
84
- const videoRef = useRef(null)
85
- const [milestones, setMilestones] = useState({ 25: false, 50: false, 75: false, 95: false })
86
-
87
- useEffect(() => {
88
- const video = videoRef.current
89
- if (!video || !onProgress) return
90
-
91
- const handleTimeUpdate = () => {
92
- const percent = (video.currentTime / video.duration) * 100
93
-
94
- Object.entries({ 25: 25, 50: 50, 75: 75, 95: 95 }).forEach(([key, threshold]) => {
95
- if (percent >= threshold && !milestones[key]) {
96
- setMilestones((prev) => ({ ...prev, [key]: true }))
97
- onProgress({ milestone: key, percent, currentTime: video.currentTime })
98
- }
99
- })
100
- }
101
-
102
- video.addEventListener('timeupdate', handleTimeUpdate)
103
- return () => video.removeEventListener('timeupdate', handleTimeUpdate)
104
- }, [milestones, onProgress])
105
-
106
- return (
107
- <video
108
- ref={videoRef}
109
- src={src}
110
- autoPlay={autoplay}
111
- muted={muted}
112
- loop={loop}
113
- controls={controls}
114
- poster={poster}
115
- playsInline
116
- className={className}
117
- />
118
- )
85
+ function LocalVideo({
86
+ src,
87
+ autoplay,
88
+ muted,
89
+ loop,
90
+ controls,
91
+ poster,
92
+ onProgress,
93
+ className,
94
+ }) {
95
+ const videoRef = useRef(null);
96
+ const [milestones, setMilestones] = useState({
97
+ 25: false,
98
+ 50: false,
99
+ 75: false,
100
+ 95: false,
101
+ });
102
+
103
+ useEffect(() => {
104
+ const video = videoRef.current;
105
+ if (!video || !onProgress) return;
106
+
107
+ const handleTimeUpdate = () => {
108
+ const percent = (video.currentTime / video.duration) * 100;
109
+
110
+ Object.entries({ 25: 25, 50: 50, 75: 75, 95: 95 }).forEach(
111
+ ([key, threshold]) => {
112
+ if (percent >= threshold && !milestones[key]) {
113
+ setMilestones((prev) => ({ ...prev, [key]: true }));
114
+ onProgress({
115
+ milestone: key,
116
+ percent,
117
+ currentTime: video.currentTime,
118
+ });
119
+ }
120
+ },
121
+ );
122
+ };
123
+
124
+ video.addEventListener("timeupdate", handleTimeUpdate);
125
+ return () => video.removeEventListener("timeupdate", handleTimeUpdate);
126
+ }, [milestones, onProgress]);
127
+
128
+ return (
129
+ <video
130
+ ref={videoRef}
131
+ src={src}
132
+ autoPlay={autoplay}
133
+ muted={muted}
134
+ loop={loop}
135
+ controls={controls}
136
+ poster={poster}
137
+ playsInline
138
+ className={className}
139
+ />
140
+ );
119
141
  }
120
142
 
121
143
  /**
@@ -141,84 +163,85 @@ function LocalVideo({ src, autoplay, muted, loop, controls, poster, onProgress,
141
163
  * <Media src="/videos/intro.mp4" controls className="w-full" />
142
164
  */
143
165
  export function Media({
144
- src,
145
- media,
146
- poster,
147
- autoplay = false,
148
- muted = false,
149
- loop = false,
150
- controls = true,
151
- aspectRatio = '16/9',
152
- className,
153
- videoClassName,
154
- onProgress,
155
- ...props
166
+ src,
167
+ media,
168
+ poster,
169
+ autoplay = false,
170
+ muted = false,
171
+ loop = false,
172
+ controls = true,
173
+ aspectRatio = "16/9",
174
+ className,
175
+ videoClassName,
176
+ onProgress,
177
+ ...props
156
178
  }) {
157
- // Normalize source
158
- const videoSrc = typeof src === 'string' ? src : (src?.src || media?.src || '')
159
-
160
- // Detect video type
161
- const mediaType = detectMediaType(videoSrc)
162
-
163
- // Render video player
164
- const videoContent = (() => {
165
- const playerClass = cn('w-full h-full', videoClassName)
166
-
167
- switch (mediaType) {
168
- case 'youtube': {
169
- const videoId = getYouTubeId(videoSrc)
170
- if (!videoId) return null
171
- return (
172
- <YouTubePlayer
173
- videoId={videoId}
174
- autoplay={autoplay}
175
- muted={muted}
176
- loop={loop}
177
- className={playerClass}
178
- />
179
- )
180
- }
181
-
182
- case 'vimeo': {
183
- const videoId = getVimeoId(videoSrc)
184
- if (!videoId) return null
185
- return (
186
- <VimeoPlayer
187
- videoId={videoId}
188
- autoplay={autoplay}
189
- muted={muted}
190
- loop={loop}
191
- className={playerClass}
192
- />
193
- )
194
- }
195
-
196
- case 'video':
197
- default:
198
- return (
199
- <LocalVideo
200
- src={videoSrc}
201
- autoplay={autoplay}
202
- muted={muted}
203
- loop={loop}
204
- controls={controls}
205
- poster={poster}
206
- onProgress={onProgress}
207
- className={playerClass}
208
- />
209
- )
210
- }
211
- })()
212
-
213
- return (
214
- <div
215
- className={cn('relative overflow-hidden', className)}
216
- style={{ aspectRatio }}
217
- {...props}
218
- >
219
- {videoContent}
220
- </div>
221
- )
179
+ // Normalize source
180
+ const videoSrc =
181
+ typeof src === "string" ? src : src?.src || media?.src || "";
182
+
183
+ // Detect video type
184
+ const mediaType = detectMediaType(videoSrc);
185
+
186
+ // Render video player
187
+ const videoContent = (() => {
188
+ const playerClass = cn("w-full h-full", videoClassName);
189
+
190
+ switch (mediaType) {
191
+ case "youtube": {
192
+ const videoId = getYouTubeId(videoSrc);
193
+ if (!videoId) return null;
194
+ return (
195
+ <YouTubePlayer
196
+ videoId={videoId}
197
+ autoplay={autoplay}
198
+ muted={muted}
199
+ loop={loop}
200
+ className={playerClass}
201
+ />
202
+ );
203
+ }
204
+
205
+ case "vimeo": {
206
+ const videoId = getVimeoId(videoSrc);
207
+ if (!videoId) return null;
208
+ return (
209
+ <VimeoPlayer
210
+ videoId={videoId}
211
+ autoplay={autoplay}
212
+ muted={muted}
213
+ loop={loop}
214
+ className={playerClass}
215
+ />
216
+ );
217
+ }
218
+
219
+ case "video":
220
+ default:
221
+ return (
222
+ <LocalVideo
223
+ src={videoSrc}
224
+ autoplay={autoplay}
225
+ muted={muted}
226
+ loop={loop}
227
+ controls={controls}
228
+ poster={poster}
229
+ onProgress={onProgress}
230
+ className={playerClass}
231
+ />
232
+ );
233
+ }
234
+ })();
235
+
236
+ return (
237
+ <div
238
+ className={cn("relative overflow-hidden", className)}
239
+ style={{ aspectRatio }}
240
+ {...props}
241
+ >
242
+ {videoContent}
243
+ </div>
244
+ );
222
245
  }
223
246
 
224
- export default Media
247
+ export default Media;
@@ -7,10 +7,10 @@
7
7
  * @module @uniweb/kit/styled/Visual
8
8
  */
9
9
 
10
- import React from 'react'
11
- import { Media } from '../../components/Media/Media.jsx'
12
- import { Image } from '../../components/Image/index.js'
13
- import { getChildBlockRenderer } from '../../utils/index.js'
10
+ import React from "react";
11
+ import { Media } from "../../components/Media/Media.jsx";
12
+ import { Image } from "../../components/Image/index.js";
13
+ import { getChildBlockRenderer } from "../../utils/index.js";
14
14
 
15
15
  /**
16
16
  * Renders the first non-empty visual from the given candidates.
@@ -35,18 +35,18 @@ import { getChildBlockRenderer } from '../../utils/index.js'
35
35
  * <Visual image={content.images[1]} className="aspect-video" />
36
36
  */
37
37
  export function Visual({ inset, video, image, className, fallback = null }) {
38
- if (inset) {
39
- const Renderer = getChildBlockRenderer()
40
- return <Renderer blocks={[inset]} as="div" extra={{ className }} />
41
- }
38
+ if (inset) {
39
+ const Renderer = getChildBlockRenderer();
40
+ return <Renderer blocks={[inset]} as="div" extra={{ className }} />;
41
+ }
42
42
 
43
- if (video) {
44
- return <Media src={video.src || video} className={className} />
45
- }
43
+ if (video) {
44
+ return <Media {...video} className={className} />;
45
+ }
46
46
 
47
- if (image) {
48
- return <Image src={image.src || image} alt={image.alt} className={className} />
49
- }
47
+ if (image) {
48
+ return <Image {...image} className={className} />;
49
+ }
50
50
 
51
- return fallback
51
+ return fallback;
52
52
  }