@coffic/cosy-ui 0.1.25 → 0.1.28

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.
@@ -0,0 +1,251 @@
1
+ ---
2
+ import { Image as AstroImage } from 'astro:assets';
3
+ import type { ImageMetadata } from 'astro';
4
+
5
+ interface Props {
6
+ /**
7
+ * 图片源,可以是本地图片或远程URL
8
+ */
9
+ src: ImageMetadata | string;
10
+ /**
11
+ * 图片的替代文本
12
+ */
13
+ alt: string;
14
+ /**
15
+ * 图片的宽度
16
+ */
17
+ width?: number;
18
+ /**
19
+ * 图片的高度
20
+ */
21
+ height?: number;
22
+ /**
23
+ * 图片的加载方式
24
+ * @default "lazy"
25
+ */
26
+ loading?: 'lazy' | 'eager';
27
+ /**
28
+ * 图片的填充方式
29
+ * @default "cover"
30
+ */
31
+ objectFit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
32
+ /**
33
+ * 图片的位置
34
+ * @default "center"
35
+ */
36
+ objectPosition?: string;
37
+ /**
38
+ * 是否显示加载中的占位图
39
+ * @default true
40
+ */
41
+ showPlaceholder?: boolean;
42
+ /**
43
+ * 是否显示加载失败的错误图
44
+ * @default true
45
+ */
46
+ showError?: boolean;
47
+ /**
48
+ * 自定义类名
49
+ */
50
+ class?: string;
51
+ /**
52
+ * 是否启用图片懒加载
53
+ * @default true
54
+ */
55
+ lazy?: boolean;
56
+ /**
57
+ * 图片的圆角大小
58
+ * @default "none"
59
+ */
60
+ rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'full';
61
+ /**
62
+ * 图片的阴影效果
63
+ * @default "none"
64
+ */
65
+ shadow?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
66
+ /**
67
+ * 图片的悬停效果
68
+ * @default "none"
69
+ */
70
+ hover?: 'none' | 'scale' | 'brightness' | 'blur';
71
+ /**
72
+ * 图片的过渡动画
73
+ * @default "none"
74
+ */
75
+ transition?: 'none' | 'fade' | 'slide' | 'zoom';
76
+ }
77
+
78
+ const {
79
+ src,
80
+ alt,
81
+ width,
82
+ height,
83
+ loading = 'lazy',
84
+ objectFit = 'cover',
85
+ objectPosition = 'center',
86
+ showPlaceholder = true,
87
+ showError = true,
88
+ class: className = '',
89
+ lazy = true,
90
+ rounded = 'none',
91
+ shadow = 'none',
92
+ hover = 'none',
93
+ transition = 'none',
94
+ } = Astro.props;
95
+
96
+ // 处理类名
97
+ const classes = [
98
+ 'relative',
99
+ // 圆角
100
+ rounded !== 'none' && `rounded-${rounded}`,
101
+ // 阴影
102
+ shadow !== 'none' && `shadow-${shadow}`,
103
+ // 悬停效果
104
+ hover !== 'none' && {
105
+ 'scale': 'hover:scale-105',
106
+ 'brightness': 'hover:brightness-110',
107
+ 'blur': 'hover:blur-sm',
108
+ }[hover],
109
+ // 过渡动画
110
+ transition !== 'none' && {
111
+ 'fade': 'transition-opacity duration-300',
112
+ 'slide': 'transition-transform duration-300',
113
+ 'zoom': 'transition-all duration-300',
114
+ }[transition],
115
+ className,
116
+ ].filter(Boolean).join(' ');
117
+
118
+ // 处理图片样式
119
+ const imageStyles = {
120
+ objectFit,
121
+ objectPosition,
122
+ };
123
+
124
+ // 判断是否为本地图片
125
+ const isLocalImage = typeof src !== 'string' && 'src' in src;
126
+ ---
127
+
128
+ <div class={classes}>
129
+ <div class="relative w-full h-full">
130
+ {isLocalImage ? (
131
+ <AstroImage
132
+ src={src}
133
+ alt={alt}
134
+ width={width}
135
+ height={height}
136
+ loading={loading}
137
+ class="w-full h-full opacity-0 transition-opacity duration-300"
138
+ style={imageStyles}
139
+ />
140
+ ) : (
141
+ <img
142
+ src={src}
143
+ alt={alt}
144
+ width={width}
145
+ height={height}
146
+ loading={loading}
147
+ class="w-full h-full opacity-0 transition-opacity duration-300"
148
+ style={imageStyles}
149
+ />
150
+ )}
151
+
152
+ {/* 加载占位图 */}
153
+ {showPlaceholder && (
154
+ <div class="absolute inset-0 bg-base-200 animate-pulse" />
155
+ )}
156
+
157
+ {/* 错误占位图 */}
158
+ {showError && (
159
+ <div class="absolute inset-0 bg-error/10 flex items-center justify-center hidden">
160
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-error" fill="none" viewBox="0 0 24 24" stroke="currentColor">
161
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
162
+ </svg>
163
+ </div>
164
+ )}
165
+ </div>
166
+ </div>
167
+
168
+ <script>
169
+ // 处理图片加载状态
170
+ function handleImageLoad(img) {
171
+ // 移除加载占位图
172
+ const placeholder = img.parentElement?.querySelector('.animate-pulse');
173
+ if (placeholder) {
174
+ placeholder.classList.add('opacity-0');
175
+ setTimeout(() => {
176
+ placeholder.remove();
177
+ }, 300);
178
+ }
179
+
180
+ // 添加加载完成动画
181
+ img.classList.add('opacity-100');
182
+ }
183
+
184
+ // 处理图片加载错误
185
+ function handleImageError(img) {
186
+ // 移除加载占位图
187
+ const placeholder = img.parentElement?.querySelector('.animate-pulse');
188
+ if (placeholder) {
189
+ placeholder.classList.add('opacity-0');
190
+ setTimeout(() => {
191
+ placeholder.remove();
192
+ }, 300);
193
+ }
194
+
195
+ // 显示错误占位图
196
+ const errorPlaceholder = img.parentElement?.querySelector('.bg-error\\/10');
197
+ if (errorPlaceholder) {
198
+ errorPlaceholder.classList.remove('hidden');
199
+ }
200
+ }
201
+
202
+ // 初始化图片加载处理
203
+ function initializeImageHandlers() {
204
+ const images = document.querySelectorAll('img[loading="lazy"]');
205
+ images.forEach(img => {
206
+ if (img instanceof HTMLImageElement) {
207
+ // 如果图片已经加载完成
208
+ if (img.complete) {
209
+ handleImageLoad(img);
210
+ } else {
211
+ // 添加加载事件监听
212
+ img.addEventListener('load', () => handleImageLoad(img));
213
+ img.addEventListener('error', () => handleImageError(img));
214
+ }
215
+ }
216
+ });
217
+ }
218
+
219
+ // 页面加载时初始化
220
+ document.addEventListener('astro:page-load', initializeImageHandlers);
221
+ // 初始加载时也初始化
222
+ initializeImageHandlers();
223
+ </script>
224
+
225
+ <style>
226
+ /* 过渡动画 */
227
+ .transition-opacity {
228
+ transition-property: opacity;
229
+ }
230
+
231
+ .transition-transform {
232
+ transition-property: transform;
233
+ }
234
+
235
+ .transition-all {
236
+ transition-property: all;
237
+ }
238
+
239
+ /* 悬停效果 */
240
+ .hover\:scale-105:hover {
241
+ transform: scale(1.05);
242
+ }
243
+
244
+ .hover\:brightness-110:hover {
245
+ filter: brightness(1.1);
246
+ }
247
+
248
+ .hover\:blur-sm:hover {
249
+ filter: blur(4px);
250
+ }
251
+ </style>
@@ -1,16 +1,82 @@
1
1
  ---
2
- interface Props {
2
+ import type { HTMLAttributes } from 'astro/types';
3
+
4
+ interface Props extends HTMLAttributes<'a'> {
3
5
  href: string;
4
6
  external?: boolean;
5
7
  class?: string;
8
+ 'class:list'?: any;
9
+ variant?: 'default' | 'primary' | 'secondary' | 'text' | 'cta' | 'ghost';
10
+ animation?: 'none' | 'hover-lift' | 'hover-glow' | 'hover-scale';
11
+ size?: 'sm' | 'md' | 'lg';
6
12
  }
7
13
 
8
- const { href, external = false, class: className = '' } = Astro.props;
14
+ const {
15
+ href,
16
+ external = false,
17
+ variant = 'default',
18
+ animation = 'none',
19
+ size = 'md',
20
+ class: className = '',
21
+ 'class:list': classList,
22
+ ...rest
23
+ } = Astro.props;
24
+
25
+ // 基础样式
26
+ const baseStyles = "link no-underline hover:no-underline transition-all duration-200 ease-in-out";
27
+
28
+ // 尺寸变体
29
+ const sizeStyles = {
30
+ sm: "px-4 py-2 text-sm",
31
+ md: "px-6 py-3 text-base",
32
+ lg: "px-8 py-4 text-lg"
33
+ };
34
+
35
+ // 主题变体
36
+ const variantStyles = {
37
+ default: "link-hover text-current hover:text-primary",
38
+ primary: "rounded-lg bg-blue-600 text-white font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 active:bg-blue-800",
39
+ secondary: "rounded-lg bg-gray-100 text-gray-800 font-medium hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 active:bg-gray-300",
40
+ text: "text-current hover:text-primary underline hover:no-underline",
41
+ cta: "rounded-lg bg-gradient-to-r from-blue-600 to-indigo-600 text-white font-medium hover:from-blue-700 hover:to-indigo-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 active:from-blue-800 active:to-indigo-800",
42
+ ghost: "text-current hover:text-primary bg-transparent hover:bg-base-200/50 rounded-lg"
43
+ };
44
+
45
+ // 动画效果
46
+ const animationStyles = {
47
+ none: "",
48
+ "hover-lift": "hover:-translate-y-0.5 hover:shadow-lg",
49
+ "hover-glow": "hover:shadow-[0_0_15px_rgba(59,130,246,0.5)]",
50
+ "hover-scale": "hover:scale-105"
51
+ };
52
+
53
+ // 合并所有样式
54
+ const finalClassName = [
55
+ baseStyles,
56
+ sizeStyles[size],
57
+ variantStyles[variant],
58
+ animationStyles[animation],
59
+ className
60
+ ].join(' ');
9
61
  ---
10
62
 
11
63
  <a
12
64
  href={href}
13
- class={`link link-hover ${className} no-underline hover:no-underline`}
14
- {...external ? { target: '_blank', rel: 'noopener noreferrer' } : {}}>
65
+ class:list={[finalClassName, classList]}
66
+ {...external ? { target: '_blank', rel: 'noopener noreferrer' } : {}}
67
+ {...rest}
68
+ >
15
69
  <slot />
16
70
  </a>
71
+
72
+ <style>
73
+ /* 添加渐变动画 */
74
+ .bg-gradient-to-r {
75
+ background-size: 200% auto;
76
+ transition: background-position 0.3s ease-in-out;
77
+ }
78
+
79
+ .bg-gradient-to-r:hover {
80
+ background-position: right center;
81
+ }
82
+ </style>
@@ -0,0 +1,67 @@
1
+ ---
2
+ interface Props {
3
+ /**
4
+ * Modal 的唯一标识符
5
+ */
6
+ id: string;
7
+ /**
8
+ * 模态框的标题
9
+ */
10
+ title?: string;
11
+ /**
12
+ * 是否显示关闭按钮
13
+ * @default true
14
+ */
15
+ showCloseButton?: boolean;
16
+ /**
17
+ * 自定义类名
18
+ */
19
+ class?: string;
20
+ }
21
+
22
+ const {
23
+ id,
24
+ title,
25
+ showCloseButton = true,
26
+ class: className = '',
27
+ } = Astro.props;
28
+ ---
29
+
30
+ <dialog id={id} class="modal">
31
+ <div class:list={["modal-box relative", className]}>
32
+ {showCloseButton && (
33
+ <form method="dialog">
34
+ <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
35
+ </form>
36
+ )}
37
+
38
+ {title && <h3 class="font-bold text-lg mb-4">{title}</h3>}
39
+
40
+ <div class="modal-content">
41
+ <slot />
42
+ </div>
43
+
44
+ <div class="modal-action">
45
+ <slot name="actions" />
46
+ </div>
47
+ </div>
48
+
49
+ <form method="dialog" class="modal-backdrop">
50
+ <button>关闭</button>
51
+ </form>
52
+ </dialog>
53
+
54
+ <script define:vars={{ id }}>
55
+ // 为了方便使用,我们提供一些辅助方法
56
+ document.addEventListener('DOMContentLoaded', () => {
57
+ const modal = document.getElementById(id);
58
+ if (!modal) return;
59
+
60
+ // 为所有触发这个模态框的按钮添加点击事件
61
+ document.querySelectorAll(`[data-modal-target="${id}"]`).forEach(trigger => {
62
+ trigger.addEventListener('click', () => {
63
+ modal.showModal();
64
+ });
65
+ });
66
+ });
67
+ </script>
@@ -0,0 +1,68 @@
1
+ ---
2
+ import { Image } from 'astro:assets';
3
+ import type { ImageMetadata } from 'astro';
4
+ import Link from './Link.astro';
5
+
6
+ interface SocialLink {
7
+ platform: 'github' | 'twitter' | 'linkedin' | 'website' | 'email';
8
+ url: string;
9
+ }
10
+
11
+ export interface Props {
12
+ name: string;
13
+ role: string;
14
+ avatar: ImageMetadata | string;
15
+ bio: string;
16
+ socialLinks?: SocialLink[];
17
+ class?: string;
18
+ }
19
+
20
+ const { name, role, avatar, bio, socialLinks, class: className = '' } = Astro.props;
21
+
22
+ const socialIcons = {
23
+ github: `<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>`,
24
+ twitter: `<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="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"/></svg>`,
25
+ linkedin: `<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>`,
26
+ website: `<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm1 16.057v-3.05c.959-.69 2-1.367 2-3.007 0-2.233-1.5-3-2-3s-2 .767-2 3c0 1.64 1.041 2.317 2 3.007v3.05c-6.497 1.199-10 3.777-10 6.443h20c0-2.666-3.503-5.244-10-6.443z"/></svg>`,
27
+ email: `<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="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"/><path d="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"/></svg>`
28
+ } as const;
29
+ ---
30
+
31
+ <div class:list={["card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow duration-300", className]}>
32
+ <figure class="px-6 pt-6">
33
+ {typeof avatar === 'string' ? (
34
+ <img
35
+ src={avatar}
36
+ alt={`${name}'s avatar`}
37
+ class="rounded-xl w-48 h-48 object-cover"
38
+ />
39
+ ) : (
40
+ <Image
41
+ src={avatar}
42
+ alt={`${name}'s avatar`}
43
+ class="rounded-xl w-48 h-48 object-cover"
44
+ />
45
+ )}
46
+ </figure>
47
+ <div class="card-body items-center text-center">
48
+ <h2 class="card-title text-2xl font-bold">{name}</h2>
49
+ <p class="text-primary font-medium">{role}</p>
50
+ <p class="text-base-content/80">{bio}</p>
51
+
52
+ {socialLinks && socialLinks.length > 0 && (
53
+ <div class="flex gap-4 mt-4">
54
+ {socialLinks.map(link => (
55
+ <Link
56
+ href={link.url}
57
+ external
58
+ variant="ghost"
59
+ class="p-2 hover:text-primary"
60
+ aria-label={`Visit ${name}'s ${link.platform} profile`}
61
+ >
62
+ <Fragment set:html={socialIcons[link.platform]} />
63
+ </Link>
64
+ ))}
65
+ </div>
66
+ )}
67
+ </div>
68
+ </div>
@@ -0,0 +1,43 @@
1
+ ---
2
+ import type { ImageMetadata } from 'astro';
3
+ import TeamMember from './TeamMember.astro';
4
+
5
+ interface SocialLink {
6
+ platform: 'github' | 'twitter' | 'linkedin' | 'website' | 'email';
7
+ url: string;
8
+ }
9
+
10
+ interface TeamMemberData {
11
+ name: string;
12
+ role: string;
13
+ avatar: ImageMetadata | string;
14
+ bio: string;
15
+ socialLinks?: SocialLink[];
16
+ }
17
+
18
+ interface Props {
19
+ members: TeamMemberData[];
20
+ columns?: 2 | 3 | 4;
21
+ class?: string;
22
+ }
23
+
24
+ const {
25
+ members,
26
+ columns = 3,
27
+ class: className = ''
28
+ } = Astro.props;
29
+
30
+ const gridCols = {
31
+ 2: 'grid-cols-1 md:grid-cols-2',
32
+ 3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
33
+ 4: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4'
34
+ } as const;
35
+ ---
36
+
37
+ <div class:list={["w-full", className]}>
38
+ <div class:list={["grid gap-8", gridCols[columns]]}>
39
+ {members.map(member => (
40
+ <TeamMember {...member} />
41
+ ))}
42
+ </div>
43
+ </div>
@@ -1,14 +1,21 @@
1
1
  ---
2
+ import Button from './Button.astro';
3
+
2
4
  interface Props {
3
- theme: string;
4
- label: string;
5
+ theme: string;
6
+ label: string;
5
7
  }
6
8
 
7
9
  const { theme, label } = Astro.props;
8
10
  ---
9
11
 
10
- <button
11
- class="btn btn-sm w-full text-left justify-start"
12
- data-set-theme={theme}>
13
- {label}
14
- </button>
12
+ <Button
13
+ variant="ghost"
14
+ size="sm"
15
+ block
16
+ class="text-left justify-start"
17
+ data-set-theme={theme}
18
+ >
19
+ {label}
20
+ </Button>
21
+
package/src/env.d.ts CHANGED
@@ -1 +0,0 @@
1
- /// <reference types="astro/client" />
@@ -1,11 +1,33 @@
1
- export interface FooterLink {
2
- key: string;
3
- href: string;
4
- external: boolean;
5
- text: string;
1
+ export interface Logo {
2
+ src: string;
3
+ alt: string;
6
4
  }
7
5
 
8
- export interface FooterNavGroup {
9
- titleKey: string;
10
- links: FooterLink[];
6
+ export interface Product {
7
+ name: string;
8
+ href: string;
9
+ external?: boolean;
11
10
  }
11
+
12
+ export interface SocialLink {
13
+ name: string;
14
+ url: string;
15
+ platform: string;
16
+ }
17
+
18
+ export interface FooterProps {
19
+ siteName: string;
20
+ homeLink: string;
21
+ slogan: string;
22
+ company: string;
23
+ copyright: string;
24
+ inspirationalSlogan: string;
25
+ icp?: string;
26
+ logo?: Logo;
27
+ products?: Product[];
28
+ aboutLink?: string;
29
+ contactLink?: string;
30
+ termsLink?: string;
31
+ privacyLink?: string;
32
+ socialLinks?: SocialLink[];
33
+ }
@@ -4,6 +4,13 @@ interface PlatformConfig {
4
4
  domains: string[];
5
5
  }
6
6
 
7
+ // 社交链接类型
8
+ export interface SocialLink {
9
+ name: string;
10
+ url: string;
11
+ platform: string;
12
+ }
13
+
7
14
  // 处理后的社交链接类型
8
15
  export interface ProcessedSocialLink {
9
16
  url: string;
@@ -69,21 +76,21 @@ function detectPlatform(url: string): [string, PlatformConfig] | null {
69
76
  }
70
77
 
71
78
  // 处理社交链接
72
- export function processSocialLink(url: string): ProcessedSocialLink {
73
- const platformInfo = detectPlatform(url);
79
+ export function processSocialLink(link: SocialLink): ProcessedSocialLink {
80
+ const platformInfo = detectPlatform(link.url);
74
81
 
75
82
  if (!platformInfo) {
76
83
  // 如果无法识别平台,返回默认值
77
84
  return {
78
- url,
79
- name: new URL(url).hostname,
85
+ url: link.url,
86
+ name: link.name,
80
87
  platform: 'default',
81
88
  };
82
89
  }
83
90
 
84
91
  const [platform, config] = platformInfo;
85
92
  return {
86
- url,
93
+ url: link.url,
87
94
  name: config.name,
88
95
  platform,
89
96
  };
@@ -1,10 +0,0 @@
1
- ---
2
- // Write your component code in this file!
3
- type Props = {
4
- message: string;
5
- };
6
-
7
- const { message } = Astro.props;
8
- ---
9
-
10
- <div>Hello {message}!</div>