@coffic/cosy-ui 0.3.39 → 0.3.45

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.
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  /**
3
3
  * @component Image
4
- *
4
+ *
5
5
  * @description
6
6
  * Image 组件是一个增强的图片组件,提供了丰富的功能,包括懒加载、加载状态指示、错误处理、
7
7
  * 以及各种视觉效果。它旨在提供更好的用户体验和性能优化。
8
- *
8
+ *
9
9
  * @design
10
10
  * 设计理念:
11
11
  * 1. 用户体验优先 - 提供加载状态反馈,减少用户等待焦虑
@@ -13,141 +13,141 @@
13
13
  * 3. 错误处理 - 优雅地处理图片加载失败的情况
14
14
  * 4. 视觉一致性 - 提供统一的圆角、阴影和过渡效果
15
15
  * 5. 灵活配置 - 支持多种自定义选项,适应不同场景
16
- *
16
+ *
17
17
  * @usage
18
18
  * 基本用法:
19
19
  * ```astro
20
20
  * <Image src="/images/photo.jpg" alt="一张照片" />
21
21
  * ```
22
- *
22
+ *
23
23
  * 带样式效果:
24
24
  * ```astro
25
- * <Image
26
- * src="/images/photo.jpg"
27
- * alt="一张照片"
28
- * rounded="lg"
29
- * shadow="md"
30
- * hover="scale"
25
+ * <Image
26
+ * src="/images/photo.jpg"
27
+ * alt="一张照片"
28
+ * rounded="lg"
29
+ * shadow="md"
30
+ * hover="scale"
31
31
  * />
32
32
  * ```
33
- *
33
+ *
34
34
  * 自定义加载指示器:
35
35
  * ```astro
36
- * <Image
37
- * src="https://example.com/large-image.jpg"
38
- * alt="远程大图"
39
- * loadingIndicator="progress"
36
+ * <Image
37
+ * src="https://example.com/large-image.jpg"
38
+ * alt="远程大图"
39
+ * loadingIndicator="progress"
40
40
  * />
41
41
  * ```
42
42
  */
43
43
 
44
- import "../../app.css";
44
+ import '../../app.css';
45
45
  import { AlertTriangle } from '../../index';
46
46
 
47
47
  // 自定义图片元数据接口
48
48
  interface ImageMetadata {
49
- src: string;
50
- width: number;
51
- height: number;
52
- format: string;
49
+ src: string;
50
+ width: number;
51
+ height: number;
52
+ format: string;
53
53
  }
54
54
 
55
55
  interface Props {
56
- /**
57
- * 图片源,可以是本地图片或远程URL
58
- */
59
- src: ImageMetadata | string;
60
- /**
61
- * 图片的替代文本
62
- */
63
- alt: string;
64
- /**
65
- * 图片的宽度
66
- */
67
- width?: number;
68
- /**
69
- * 图片的高度
70
- */
71
- height?: number;
72
- /**
73
- * 图片的加载方式
74
- * @default "lazy"
75
- */
76
- loading?: 'lazy' | 'eager';
77
- /**
78
- * 图片的填充方式
79
- * @default "cover"
80
- */
81
- objectFit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
82
- /**
83
- * 图片的位置
84
- * @default "center"
85
- */
86
- objectPosition?: string;
87
- /**
88
- * 是否显示加载中的占位图
89
- * @default true
90
- */
91
- showPlaceholder?: boolean;
92
- /**
93
- * 是否显示加载失败的错误图
94
- * @default true
95
- */
96
- showError?: boolean;
97
- /**
98
- * 自定义类名
99
- */
100
- class?: string;
101
- /**
102
- * 是否启用图片懒加载
103
- * @default true
104
- */
105
- lazy?: boolean;
106
- /**
107
- * 图片的圆角大小
108
- * @default "none"
109
- */
110
- rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'full';
111
- /**
112
- * 图片的阴影效果
113
- * @default "none"
114
- */
115
- shadow?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
116
- /**
117
- * 图片的悬停效果
118
- * @default "none"
119
- */
120
- hover?: 'none' | 'scale' | 'brightness' | 'blur';
121
- /**
122
- * 图片的过渡动画
123
- * @default "none"
124
- */
125
- transition?: 'none' | 'fade' | 'slide' | 'zoom';
56
+ /**
57
+ * 图片源,可以是本地图片或远程URL
58
+ */
59
+ src: ImageMetadata | string;
60
+ /**
61
+ * 图片的替代文本
62
+ */
63
+ alt: string;
64
+ /**
65
+ * 图片的宽度
66
+ */
67
+ width?: number;
68
+ /**
69
+ * 图片的高度
70
+ */
71
+ height?: number;
72
+ /**
73
+ * 图片的加载方式
74
+ * @default "lazy"
75
+ */
76
+ loading?: 'lazy' | 'eager';
77
+ /**
78
+ * 图片的填充方式
79
+ * @default "cover"
80
+ */
81
+ objectFit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
82
+ /**
83
+ * 图片的位置
84
+ * @default "center"
85
+ */
86
+ objectPosition?: string;
87
+ /**
88
+ * 是否显示加载中的占位图
89
+ * @default true
90
+ */
91
+ showPlaceholder?: boolean;
92
+ /**
93
+ * 是否显示加载失败的错误图
94
+ * @default true
95
+ */
96
+ showError?: boolean;
97
+ /**
98
+ * 自定义类名
99
+ */
100
+ class?: string;
101
+ /**
102
+ * 是否启用图片懒加载
103
+ * @default true
104
+ */
105
+ lazy?: boolean;
106
+ /**
107
+ * 图片的圆角大小
108
+ * @default "none"
109
+ */
110
+ rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'full';
111
+ /**
112
+ * 图片的阴影效果
113
+ * @default "none"
114
+ */
115
+ shadow?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
116
+ /**
117
+ * 图片的悬停效果
118
+ * @default "none"
119
+ */
120
+ hover?: 'none' | 'scale' | 'brightness' | 'blur';
121
+ /**
122
+ * 图片的过渡动画
123
+ * @default "none"
124
+ */
125
+ transition?: 'none' | 'fade' | 'slide' | 'zoom';
126
126
 
127
- /**
128
- * 加载指示器类型
129
- * @default "skeleton"
130
- */
131
- loadingIndicator?: 'pulse' | 'spinner' | 'progress' | 'skeleton';
127
+ /**
128
+ * 加载指示器类型
129
+ * @default "skeleton"
130
+ */
131
+ loadingIndicator?: 'pulse' | 'spinner' | 'progress' | 'skeleton';
132
132
  }
133
133
 
134
134
  const {
135
- src,
136
- alt,
137
- width,
138
- height,
139
- loading = 'lazy',
140
- objectFit = 'cover',
141
- objectPosition = 'center',
142
- showPlaceholder = true,
143
- showError = true,
144
- class: className = '',
145
- lazy = true,
146
- rounded = 'none',
147
- shadow = 'none',
148
- hover = 'none',
149
- transition = 'none',
150
- loadingIndicator = 'skeleton',
135
+ src,
136
+ alt,
137
+ width,
138
+ height,
139
+ loading = 'lazy',
140
+ objectFit = 'cover',
141
+ objectPosition = 'center',
142
+ showPlaceholder = true,
143
+ showError = true,
144
+ class: className = '',
145
+ lazy = true,
146
+ rounded = 'none',
147
+ shadow = 'none',
148
+ hover = 'none',
149
+ transition = 'none',
150
+ loadingIndicator = 'skeleton',
151
151
  } = Astro.props;
152
152
 
153
153
  // 判断是否为本地图片
@@ -159,172 +159,184 @@ const imgSrc = typeof src === 'string' ? src : src.src;
159
159
 
160
160
  // 对象映射定义所有可能的类名
161
161
  const objectFitClasses = {
162
- 'contain': 'object-contain',
163
- 'cover': 'object-cover',
164
- 'fill': 'object-fill',
165
- 'none': 'object-none',
166
- 'scale-down': 'object-scale-down'
162
+ contain: 'cosy:object-contain',
163
+ cover: 'cosy:object-cover',
164
+ fill: 'cosy:object-fill',
165
+ none: 'cosy:object-none',
166
+ 'scale-down': 'cosy:object-scale-down',
167
167
  };
168
168
 
169
169
  const roundedClasses = {
170
- 'none': '',
171
- 'sm': 'rounded-sm',
172
- 'md': 'rounded-md',
173
- 'lg': 'rounded-lg',
174
- 'xl': 'rounded-xl',
175
- '2xl': 'rounded-2xl',
176
- '3xl': 'rounded-3xl',
177
- 'full': 'rounded-full'
170
+ none: '',
171
+ sm: 'cosy:rounded-sm',
172
+ md: 'cosy:rounded-md',
173
+ lg: 'cosy:rounded-lg',
174
+ xl: 'cosy:rounded-xl',
175
+ '2xl': 'cosy:rounded-2xl',
176
+ '3xl': 'cosy:rounded-3xl',
177
+ full: 'cosy:rounded-full',
178
178
  };
179
179
 
180
180
  const shadowClasses = {
181
- 'none': '',
182
- 'sm': 'shadow-sm',
183
- 'md': 'shadow-md',
184
- 'lg': 'shadow-lg',
185
- 'xl': 'shadow-xl',
186
- '2xl': 'shadow-2xl'
181
+ none: '',
182
+ sm: 'cosy:shadow-sm',
183
+ md: 'cosy:shadow-md',
184
+ lg: 'cosy:shadow-lg',
185
+ xl: 'cosy:shadow-xl',
186
+ '2xl': 'cosy:shadow-2xl',
187
187
  };
188
188
 
189
189
  const hoverClasses = {
190
- 'none': '',
191
- 'scale': 'hover:scale-110',
192
- 'brightness': 'hover:brightness-110',
193
- 'blur': 'hover:blur-sm'
190
+ none: '',
191
+ scale: 'cosy:hover:scale-110',
192
+ brightness: 'cosy:hover:brightness-110',
193
+ blur: 'cosy:hover:blur-sm',
194
194
  };
195
195
 
196
196
  const transitionClasses = {
197
- 'none': '',
198
- 'fade': 'transition-opacity duration-300',
199
- 'slide': 'transition-transform duration-300',
200
- 'zoom': 'transition-all duration-300'
197
+ none: '',
198
+ fade: 'cosy:transition-opacity cosy:duration-300',
199
+ slide: 'cosy:transition-transform cosy:duration-300',
200
+ zoom: 'cosy:transition-all cosy:duration-300',
201
201
  };
202
202
 
203
203
  // 构建图片类名
204
204
  const imgClasses = [
205
- 'object-center',
206
- objectFitClasses[objectFit] || objectFitClasses['cover'],
207
- roundedClasses[rounded],
208
- shadowClasses[shadow],
209
- hoverClasses[hover],
210
- transitionClasses[transition],
211
- className
212
- ].filter(Boolean).join(' ');
205
+ 'cosy:object-center',
206
+ objectFitClasses[objectFit] || objectFitClasses['cover'],
207
+ roundedClasses[rounded],
208
+ shadowClasses[shadow],
209
+ hoverClasses[hover],
210
+ transitionClasses[transition],
211
+ className,
212
+ ]
213
+ .filter(Boolean)
214
+ .join(' ');
213
215
 
214
216
  // 构建占位图类名
215
217
  const placeholderClasses = [
216
- 'absolute inset-0',
217
- roundedClasses[rounded],
218
- loadingIndicator === 'skeleton' || loadingIndicator === 'pulse' ? 'animate-pulse bg-base-300' : ''
219
- ].filter(Boolean).join(' ');
218
+ 'cosy:absolute cosy:inset-0',
219
+ roundedClasses[rounded],
220
+ loadingIndicator === 'skeleton' || loadingIndicator === 'pulse'
221
+ ? 'cosy:animate-pulse cosy:bg-base-300'
222
+ : '',
223
+ ]
224
+ .filter(Boolean)
225
+ .join(' ');
220
226
 
221
227
  // 构建错误占位图类名
222
228
  const errorClasses = [
223
- 'absolute inset-0 flex items-center justify-center',
224
- roundedClasses[rounded],
225
- 'bg-error bg-opacity-10'
226
- ].filter(Boolean).join(' ');
229
+ 'cosy:absolute cosy:inset-0 cosy:flex cosy:items-center cosy:justify-center',
230
+ roundedClasses[rounded],
231
+ 'cosy:bg-error cosy:bg-opacity-10',
232
+ ]
233
+ .filter(Boolean)
234
+ .join(' ');
227
235
  ---
228
236
 
229
- <div class="relative w-full h-full">
230
- <img
231
- src={imgSrc}
232
- alt={alt}
233
- width={width}
234
- height={height}
235
- loading={loading}
236
- class={imgClasses}
237
- style={{ objectPosition }}
238
- data-remote={isRemoteImage ? 'true' : 'false'}
239
- />
237
+ <script is:inline>
238
+ function handleImageLoad(img) {
239
+ if (!(img instanceof HTMLImageElement)) return;
240
+ const placeholder = img.parentElement?.querySelector('[data-placeholder]');
241
+ if (placeholder) {
242
+ placeholder.classList.add('opacity-0');
243
+ setTimeout(() => placeholder.remove(), 300);
244
+ }
245
+ }
240
246
 
241
- {/* 加载占位图 */}
242
- {showPlaceholder && (
243
- <div class={placeholderClasses} data-placeholder>
244
- {/* 远程图片加载指示器 */}
245
- {isRemoteImage && loadingIndicator === 'spinner' && (
246
- <div class="absolute inset-0 m-auto text-primary loading loading-spinner loading-lg" />
247
- )}
248
-
249
- {isRemoteImage && loadingIndicator === 'progress' && (
250
- <div class="right-0 bottom-0 left-0 absolute">
251
- <progress class="w-full progress progress-primary" />
252
- </div>
253
- )}
254
- </div>
255
- )}
247
+ function handleImageError(img) {
248
+ if (!(img instanceof HTMLImageElement)) return;
249
+ const placeholder = img.parentElement?.querySelector('[data-placeholder]');
250
+ if (placeholder) {
251
+ placeholder.remove();
252
+ }
253
+ const errorElement = img.parentElement?.querySelector('[data-error]');
254
+ if (errorElement) {
255
+ errorElement.classList.remove('hidden');
256
+ }
257
+ }
256
258
 
257
- {/* 错误占位图 */}
258
- {showError && (
259
- <div class={errorClasses} data-error hidden>
260
- <AlertTriangle size="32px" class="text-error" />
261
- </div>
262
- )}
263
- </div>
259
+ function simulateLoadingProgress(img) {
260
+ if (!(img instanceof HTMLImageElement) || img.getAttribute('data-remote') !== 'true') return;
261
+ const progressBar = img.parentElement?.querySelector('progress');
262
+ if (!progressBar) return;
264
263
 
265
- <script is:inline>
266
- function handleImageLoad(img) {
267
- if (!(img instanceof HTMLImageElement)) return;
268
- const placeholder = img.parentElement?.querySelector('[data-placeholder]');
269
- if (placeholder) {
270
- placeholder.classList.add('opacity-0');
271
- setTimeout(() => placeholder.remove(), 300);
272
- }
273
- }
264
+ let progress = 0;
265
+ const interval = setInterval(() => {
266
+ progress += Math.random() * 15;
267
+ if (progress >= 100 || img.complete) {
268
+ clearInterval(interval);
269
+ progress = 100;
270
+ }
271
+ progressBar.value = Math.min(progress, 100);
272
+ }, 200);
274
273
 
275
- function handleImageError(img) {
276
- if (!(img instanceof HTMLImageElement)) return;
277
- const placeholder = img.parentElement?.querySelector('[data-placeholder]');
278
- if (placeholder) {
279
- placeholder.remove();
280
- }
281
- const errorElement = img.parentElement?.querySelector('[data-error]');
282
- if (errorElement) {
283
- errorElement.classList.remove('hidden');
284
- }
285
- }
274
+ img.addEventListener('load', () => {
275
+ clearInterval(interval);
276
+ progressBar.value = 100;
277
+ });
286
278
 
287
- function simulateLoadingProgress(img) {
288
- if (!(img instanceof HTMLImageElement) || img.getAttribute('data-remote') !== 'true') return;
289
- const progressBar = img.parentElement?.querySelector('progress');
290
- if (!progressBar) return;
291
-
292
- let progress = 0;
293
- const interval = setInterval(() => {
294
- progress += Math.random() * 15;
295
- if (progress >= 100 || img.complete) {
296
- clearInterval(interval);
297
- progress = 100;
298
- }
299
- progressBar.value = Math.min(progress, 100);
300
- }, 200);
301
-
302
- img.addEventListener('load', () => {
303
- clearInterval(interval);
304
- progressBar.value = 100;
305
- });
306
-
307
- img.addEventListener('error', () => {
308
- clearInterval(interval);
309
- });
310
- }
279
+ img.addEventListener('error', () => {
280
+ clearInterval(interval);
281
+ });
282
+ }
311
283
 
312
- function initializeImageHandlers() {
313
- document.querySelectorAll('img[data-remote]').forEach(img => {
314
- if (img instanceof HTMLImageElement) {
315
- if (img.complete) {
316
- handleImageLoad(img);
317
- } else {
318
- img.addEventListener('load', () => handleImageLoad(img));
319
- img.addEventListener('error', () => handleImageError(img));
320
- if (img.getAttribute('data-remote') === 'true') {
321
- simulateLoadingProgress(img);
322
- }
323
- }
324
- }
325
- });
326
- }
284
+ function initializeImageHandlers() {
285
+ document.querySelectorAll('img[data-remote]').forEach((img) => {
286
+ if (img instanceof HTMLImageElement) {
287
+ if (img.complete) {
288
+ handleImageLoad(img);
289
+ } else {
290
+ img.addEventListener('load', () => handleImageLoad(img));
291
+ img.addEventListener('error', () => handleImageError(img));
292
+ if (img.getAttribute('data-remote') === 'true') {
293
+ simulateLoadingProgress(img);
294
+ }
295
+ }
296
+ }
297
+ });
298
+ }
327
299
 
328
- document.addEventListener('astro:page-load', initializeImageHandlers);
329
- initializeImageHandlers();
300
+ document.addEventListener('astro:page-load', initializeImageHandlers);
301
+ initializeImageHandlers();
330
302
  </script>
303
+
304
+ <div class="cosy:relative cosy:w-full cosy:h-full">
305
+ <img
306
+ src={imgSrc}
307
+ alt={alt}
308
+ width={width}
309
+ height={height}
310
+ loading={loading}
311
+ class={imgClasses}
312
+ style={{ objectPosition }}
313
+ data-remote={isRemoteImage ? 'true' : 'false'}
314
+ />
315
+
316
+ {/* 加载占位图 */}
317
+ {
318
+ showPlaceholder && (
319
+ <div class={placeholderClasses} data-placeholder>
320
+ {/* 远程图片加载指示器 */}
321
+ {isRemoteImage && loadingIndicator === 'spinner' && (
322
+ <div class="cosy:absolute cosy:inset-0 cosy:m-auto cosy:text-primary cosy:loading cosy:loading-spinner cosy:loading-lg" />
323
+ )}
324
+
325
+ {isRemoteImage && loadingIndicator === 'progress' && (
326
+ <div class="cosy:right-0 cosy:bottom-0 cosy:left-0 cosy:absolute">
327
+ <progress class="cosy:w-full cosy:progress cosy:progress-primary" />
328
+ </div>
329
+ )}
330
+ </div>
331
+ )
332
+ }
333
+
334
+ {/* 错误占位图 */}
335
+ {
336
+ showError && (
337
+ <div class={errorClasses} data-error hidden>
338
+ <AlertTriangle size="32px" class="cosy:text-error" />
339
+ </div>
340
+ )
341
+ }
342
+ </div>