@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.
- package/README.md +10 -50
- package/index.ts +15 -4
- package/package.json +10 -7
- package/src/components/Alert.astro +78 -0
- package/src/components/Article.astro +11 -0
- package/src/components/Banner.astro +41 -4
- package/src/components/Blog.astro +115 -0
- package/src/components/Button.astro +49 -0
- package/src/components/Card.astro +113 -0
- package/src/components/CodeBlock.astro +186 -0
- package/src/components/Footer.astro +9 -7
- package/src/components/Header.astro +31 -48
- package/src/components/Hero.astro +69 -0
- package/src/components/Image.astro +251 -0
- package/src/components/Link.astro +70 -4
- package/src/components/Modal.astro +67 -0
- package/src/components/TeamMember.astro +68 -0
- package/src/components/TeamMembers.astro +43 -0
- package/src/components/ThemeItem.astro +14 -7
- package/src/env.d.ts +0 -1
- package/src/types/footer.ts +30 -8
- package/src/utils/social.ts +12 -5
- package/src/components/Component.astro +0 -10
@@ -0,0 +1,186 @@
|
|
1
|
+
---
|
2
|
+
interface Props {
|
3
|
+
code: string;
|
4
|
+
lang?: string;
|
5
|
+
title?: string;
|
6
|
+
showLineNumbers?: boolean;
|
7
|
+
}
|
8
|
+
|
9
|
+
const {
|
10
|
+
code,
|
11
|
+
lang = 'plaintext',
|
12
|
+
title,
|
13
|
+
showLineNumbers = true,
|
14
|
+
} = Astro.props;
|
15
|
+
|
16
|
+
// 移除代码字符串开头和结尾的空行
|
17
|
+
const trimmedCode = code.trim();
|
18
|
+
|
19
|
+
// 生成行号
|
20
|
+
const lines = trimmedCode.split('\n');
|
21
|
+
const lineNumbers = Array.from({ length: lines.length }, (_, i) => i + 1);
|
22
|
+
---
|
23
|
+
|
24
|
+
<div class="code-block not-prose rounded-lg border border-base-300 bg-base-200 my-4">
|
25
|
+
{/* 标题栏 */}
|
26
|
+
{title && (
|
27
|
+
<div class="flex items-center justify-between px-4 py-2 border-b border-base-300">
|
28
|
+
<span class="text-sm font-medium">{title}</span>
|
29
|
+
<button
|
30
|
+
class="copy-button btn btn-ghost btn-xs gap-1"
|
31
|
+
data-code={trimmedCode}
|
32
|
+
title="复制代码"
|
33
|
+
>
|
34
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
35
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
36
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
37
|
+
</svg>
|
38
|
+
<span class="copy-text">复制</span>
|
39
|
+
</button>
|
40
|
+
</div>
|
41
|
+
)}
|
42
|
+
|
43
|
+
{/* 代码区域 */}
|
44
|
+
<div class="relative">
|
45
|
+
{/* 行号 */}
|
46
|
+
{showLineNumbers && (
|
47
|
+
<div class="hidden md:block absolute left-0 top-0 bottom-0 px-4 py-4 select-none text-base-content/50 bg-base-300 border-r border-base-300">
|
48
|
+
{lineNumbers.map(num => (
|
49
|
+
<div class="text-right leading-6 text-sm">{num}</div>
|
50
|
+
))}
|
51
|
+
</div>
|
52
|
+
)}
|
53
|
+
|
54
|
+
{/* 代码内容 */}
|
55
|
+
<pre class={`language-${lang} ${showLineNumbers ? 'pl-[3.5rem]' : 'px-4'} py-4 m-0 overflow-x-auto`}><code class={`language-${lang}`} set:html={trimmedCode} /></pre>
|
56
|
+
</div>
|
57
|
+
</div>
|
58
|
+
|
59
|
+
<script>
|
60
|
+
function initializeCopyButtons() {
|
61
|
+
const copyButtons = document.querySelectorAll('.copy-button');
|
62
|
+
|
63
|
+
copyButtons.forEach(button => {
|
64
|
+
button.addEventListener('click', async () => {
|
65
|
+
const code = button.getAttribute('data-code') || '';
|
66
|
+
const copyText = button.querySelector('.copy-text');
|
67
|
+
|
68
|
+
try {
|
69
|
+
await navigator.clipboard.writeText(code);
|
70
|
+
if (copyText) {
|
71
|
+
copyText.textContent = '已复制!';
|
72
|
+
setTimeout(() => {
|
73
|
+
copyText.textContent = '复制';
|
74
|
+
}, 2000);
|
75
|
+
}
|
76
|
+
} catch (err) {
|
77
|
+
console.error('复制失败:', err);
|
78
|
+
if (copyText) {
|
79
|
+
copyText.textContent = '复制失败';
|
80
|
+
setTimeout(() => {
|
81
|
+
copyText.textContent = '复制';
|
82
|
+
}, 2000);
|
83
|
+
}
|
84
|
+
}
|
85
|
+
});
|
86
|
+
});
|
87
|
+
}
|
88
|
+
|
89
|
+
// 在页面加载时初始化
|
90
|
+
initializeCopyButtons();
|
91
|
+
|
92
|
+
// 在 Astro 页面切换时重新初始化
|
93
|
+
document.addEventListener('astro:page-load', initializeCopyButtons);
|
94
|
+
</script>
|
95
|
+
|
96
|
+
<style is:global>
|
97
|
+
/* Prism.js 暗色主题自定义 */
|
98
|
+
.code-block pre {
|
99
|
+
background: transparent !important;
|
100
|
+
}
|
101
|
+
|
102
|
+
.code-block code {
|
103
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
104
|
+
font-size: 0.875rem;
|
105
|
+
line-height: 1.5rem;
|
106
|
+
}
|
107
|
+
|
108
|
+
/* 代码高亮颜色 */
|
109
|
+
.token.comment,
|
110
|
+
.token.prolog,
|
111
|
+
.token.doctype,
|
112
|
+
.token.cdata {
|
113
|
+
color: #636f88;
|
114
|
+
}
|
115
|
+
|
116
|
+
.token.punctuation {
|
117
|
+
color: #81a1c1;
|
118
|
+
}
|
119
|
+
|
120
|
+
.namespace {
|
121
|
+
opacity: 0.7;
|
122
|
+
}
|
123
|
+
|
124
|
+
.token.property,
|
125
|
+
.token.tag,
|
126
|
+
.token.constant,
|
127
|
+
.token.symbol,
|
128
|
+
.token.deleted {
|
129
|
+
color: #81a1c1;
|
130
|
+
}
|
131
|
+
|
132
|
+
.token.number {
|
133
|
+
color: #b48ead;
|
134
|
+
}
|
135
|
+
|
136
|
+
.token.boolean {
|
137
|
+
color: #81a1c1;
|
138
|
+
}
|
139
|
+
|
140
|
+
.token.selector,
|
141
|
+
.token.attr-name,
|
142
|
+
.token.string,
|
143
|
+
.token.char,
|
144
|
+
.token.builtin,
|
145
|
+
.token.inserted {
|
146
|
+
color: #a3be8c;
|
147
|
+
}
|
148
|
+
|
149
|
+
.token.operator,
|
150
|
+
.token.entity,
|
151
|
+
.token.url,
|
152
|
+
.language-css .token.string,
|
153
|
+
.style .token.string,
|
154
|
+
.token.variable {
|
155
|
+
color: #81a1c1;
|
156
|
+
}
|
157
|
+
|
158
|
+
.token.atrule,
|
159
|
+
.token.attr-value,
|
160
|
+
.token.function,
|
161
|
+
.token.class-name {
|
162
|
+
color: #88c0d0;
|
163
|
+
}
|
164
|
+
|
165
|
+
.token.keyword {
|
166
|
+
color: #81a1c1;
|
167
|
+
}
|
168
|
+
|
169
|
+
.token.regex,
|
170
|
+
.token.important {
|
171
|
+
color: #ebcb8b;
|
172
|
+
}
|
173
|
+
|
174
|
+
.token.important,
|
175
|
+
.token.bold {
|
176
|
+
font-weight: bold;
|
177
|
+
}
|
178
|
+
|
179
|
+
.token.italic {
|
180
|
+
font-style: italic;
|
181
|
+
}
|
182
|
+
|
183
|
+
.token.entity {
|
184
|
+
cursor: help;
|
185
|
+
}
|
186
|
+
</style>
|
@@ -3,6 +3,9 @@ import { processSocialLink } from '../utils/social';
|
|
3
3
|
import Link from './Link.astro';
|
4
4
|
import Banner from './Banner.astro';
|
5
5
|
import SocialIcon from './SocialIcon.astro';
|
6
|
+
import type { FooterProps } from '../types/footer';
|
7
|
+
|
8
|
+
interface Props extends FooterProps {}
|
6
9
|
|
7
10
|
const {
|
8
11
|
siteName,
|
@@ -19,7 +22,7 @@ const {
|
|
19
22
|
termsLink,
|
20
23
|
privacyLink,
|
21
24
|
socialLinks = [],
|
22
|
-
} = Astro.props;
|
25
|
+
}: Props = Astro.props;
|
23
26
|
|
24
27
|
const currentYear = new Date().getFullYear();
|
25
28
|
|
@@ -27,7 +30,7 @@ const currentYear = new Date().getFullYear();
|
|
27
30
|
const processedSocialLinks = socialLinks.map((link) => processSocialLink(link));
|
28
31
|
---
|
29
32
|
|
30
|
-
<footer class="bg-
|
33
|
+
<footer class="bg-base-200/50 dark:bg-base-300/50 z-50 backdrop-blur-md">
|
31
34
|
<div class="footer sm:footer-horizontal p-10 text-base-content">
|
32
35
|
<aside class="flex flex-col items-center sm:items-start gap-2 place-self-center">
|
33
36
|
<Link href={homeLink}>
|
@@ -51,15 +54,14 @@ const processedSocialLinks = socialLinks.map((link) => processSocialLink(link));
|
|
51
54
|
<div class="flex gap-4 mt-4 sm:mt-0">
|
52
55
|
{
|
53
56
|
processedSocialLinks.map((link) => (
|
54
|
-
<
|
57
|
+
<Link
|
55
58
|
href={link.url}
|
56
|
-
|
57
|
-
rel="noopener noreferrer"
|
59
|
+
external={true}
|
58
60
|
class="btn btn-ghost btn-sm p-1 hover:text-primary"
|
59
|
-
|
61
|
+
>
|
60
62
|
<SocialIcon platform={link.platform} />
|
61
63
|
<span class="sr-only">{link.name}</span>
|
62
|
-
</
|
64
|
+
</Link>
|
63
65
|
))
|
64
66
|
}
|
65
67
|
</div>
|
@@ -1,8 +1,9 @@
|
|
1
1
|
---
|
2
2
|
import { Image } from 'astro:assets';
|
3
|
-
import {
|
3
|
+
import { RiSearch2Line, RiMenuLine, RiSunCloudyLine } from '@remixicon/vue';
|
4
4
|
import ThemeItem from './ThemeItem.astro';
|
5
5
|
import Link from './Link.astro';
|
6
|
+
import Button from './Button.astro';
|
6
7
|
|
7
8
|
interface Props {
|
8
9
|
logo: ImageMetadata;
|
@@ -70,7 +71,8 @@ function generateLanguageUrl(langCode: string): string {
|
|
70
71
|
text-neutral-200 dark:text-neutral-300
|
71
72
|
px-4
|
72
73
|
rounded-lg h-12 mt-1 mx-4 fixed top-0 left-0 right-0 z-50
|
73
|
-
border border-white/5 dark:border-black/5"
|
74
|
+
border border-white/5 dark:border-black/5"
|
75
|
+
>
|
74
76
|
<div class="flex flex-row justify-start items-center">
|
75
77
|
<Link href="/" transition:persist>
|
76
78
|
<div class="h-8 w-8 flex flex-col items-center justify-center">
|
@@ -79,68 +81,48 @@ function generateLanguageUrl(langCode: string): string {
|
|
79
81
|
</Link>
|
80
82
|
|
81
83
|
<!-- 移动端菜单按钮 -->
|
82
|
-
<
|
83
|
-
<RiMenuLine class="w-5 h-5" />
|
84
|
-
</
|
84
|
+
<Button variant="ghost" size="sm" class="lg:hidden ml-2" onClick="mobile_menu.showModal()">
|
85
|
+
<RiMenuLine class="w-5 h-5" slot="icon-left" />
|
86
|
+
</Button>
|
85
87
|
|
86
88
|
<!-- 桌面端导航 -->
|
87
89
|
<div class="hidden lg:flex flex-row gap-4 items-center ml-4" transition:animate="fade">
|
88
|
-
{
|
89
|
-
navItems.map((item) => (
|
90
|
-
<Link
|
91
|
-
class:list={[
|
92
|
-
'btn btn-sm',
|
93
|
-
{
|
94
|
-
'btn-ghost': !item.match(currentPath),
|
95
|
-
'btn-primary': item.match(currentPath),
|
96
|
-
},
|
97
|
-
]}
|
98
|
-
href={item.href}>
|
99
|
-
{item.label}
|
100
|
-
</Link>
|
101
|
-
))
|
102
|
-
}
|
90
|
+
{navItems.map((item) => <Link href={item.href}>{item.label}</Link>)}
|
103
91
|
</div>
|
104
92
|
</div>
|
105
93
|
|
106
94
|
<div class="h-12 flex flex-row justify-end gap-2 items-center">
|
107
95
|
<div class="dropdown dropdown-end">
|
108
|
-
<
|
96
|
+
<Button variant="ghost" size="sm" class="p-1">
|
109
97
|
{currentLanguageName}
|
110
|
-
</
|
98
|
+
</Button>
|
111
99
|
<ul
|
112
100
|
tabindex={0}
|
113
|
-
class="dropdown-content menu bg-slate-900 dark:bg-slate-800 rounded-box z-[1] w-40 p-2 shadow-lg"
|
101
|
+
class="dropdown-content menu bg-slate-900 dark:bg-slate-800 rounded-box z-[1] w-40 p-2 shadow-lg"
|
102
|
+
>
|
114
103
|
{
|
115
104
|
languages.map((lang) => (
|
116
105
|
<li>
|
117
|
-
<Link
|
118
|
-
href={generateLanguageUrl(lang.code)}
|
119
|
-
class:list={[
|
120
|
-
{
|
121
|
-
'bg-primary text-white': currentLocale === lang.code,
|
122
|
-
},
|
123
|
-
]}>
|
124
|
-
{lang.name}
|
125
|
-
</Link>
|
106
|
+
<Link href={generateLanguageUrl(lang.code)}>{lang.name}</Link>
|
126
107
|
</li>
|
127
108
|
))
|
128
109
|
}
|
129
110
|
</ul>
|
130
111
|
</div>
|
131
112
|
<div class="dropdown dropdown-end">
|
132
|
-
<
|
133
|
-
<RiSunCloudyLine class="w-5 h-5" />
|
134
|
-
</
|
113
|
+
<Button variant="ghost" size="sm" class="p-1">
|
114
|
+
<RiSunCloudyLine class="w-5 h-5" slot="icon-left" />
|
115
|
+
</Button>
|
135
116
|
<ul
|
136
117
|
tabindex={0}
|
137
|
-
class="dropdown-content menu bg-neutral-900 dark:bg-neutral-800 rounded-box z-[1] w-56 p-2 shadow-lg"
|
118
|
+
class="dropdown-content menu bg-neutral-900 dark:bg-neutral-800 rounded-box z-[1] w-56 p-2 shadow-lg"
|
119
|
+
>
|
138
120
|
{themes.map((theme) => <ThemeItem theme={theme.id} label={theme.name} />)}
|
139
121
|
</ul>
|
140
122
|
</div>
|
141
|
-
<
|
142
|
-
<RiSearch2Line class="w-5 h-5" />
|
143
|
-
</
|
123
|
+
<Button variant="ghost" size="sm" class="p-1" onClick="my_modal_1.showModal()">
|
124
|
+
<RiSearch2Line class="w-5 h-5" slot="icon-left" />
|
125
|
+
</Button>
|
144
126
|
</div>
|
145
127
|
</header>
|
146
128
|
|
@@ -153,7 +135,8 @@ function generateLanguageUrl(langCode: string): string {
|
|
153
135
|
<div class="flex flex-col gap-2">
|
154
136
|
{
|
155
137
|
navItems.map((item) => (
|
156
|
-
<
|
138
|
+
<Link
|
139
|
+
href={item.href}
|
157
140
|
class:list={[
|
158
141
|
'btn btn-sm w-full text-left justify-start',
|
159
142
|
{
|
@@ -161,22 +144,22 @@ function generateLanguageUrl(langCode: string): string {
|
|
161
144
|
'btn-primary': item.match(currentPath),
|
162
145
|
},
|
163
146
|
]}
|
164
|
-
|
147
|
+
>
|
165
148
|
{item.label}
|
166
|
-
</
|
149
|
+
</Link>
|
167
150
|
))
|
168
151
|
}
|
169
152
|
</div>
|
170
153
|
<div class="modal-action">
|
171
154
|
<form method="dialog">
|
172
|
-
<
|
155
|
+
<Button>
|
173
156
|
{currentLocale === 'zh-cn' ? '关闭' : 'Close'}
|
174
|
-
</
|
157
|
+
</Button>
|
175
158
|
</form>
|
176
159
|
</div>
|
177
160
|
</div>
|
178
161
|
<form method="dialog" class="modal-backdrop bg-black/20 backdrop-blur-sm">
|
179
|
-
<
|
162
|
+
<Button variant="ghost">关闭</Button>
|
180
163
|
</form>
|
181
164
|
</dialog>
|
182
165
|
|
@@ -284,7 +267,7 @@ function generateLanguageUrl(langCode: string): string {
|
|
284
267
|
<script>
|
285
268
|
function initializeThemeSwitch() {
|
286
269
|
const themeItems = document.querySelectorAll('[data-set-theme]');
|
287
|
-
const updateActiveTheme = (currentTheme
|
270
|
+
const updateActiveTheme = (currentTheme) => {
|
288
271
|
themeItems.forEach((item) => {
|
289
272
|
const themeId = item.getAttribute('data-set-theme');
|
290
273
|
const isActive = themeId === currentTheme;
|
@@ -300,8 +283,8 @@ function generateLanguageUrl(langCode: string): string {
|
|
300
283
|
item.addEventListener('click', handleThemeClick);
|
301
284
|
});
|
302
285
|
|
303
|
-
function handleThemeClick(event
|
304
|
-
const item = event.currentTarget
|
286
|
+
function handleThemeClick(event) {
|
287
|
+
const item = event.currentTarget;
|
305
288
|
const theme = item.getAttribute('data-set-theme');
|
306
289
|
document.documentElement.setAttribute('data-theme', theme ?? 'default');
|
307
290
|
localStorage.setItem('theme', theme ?? 'default');
|
@@ -0,0 +1,69 @@
|
|
1
|
+
---
|
2
|
+
import Link from './Link.astro';
|
3
|
+
|
4
|
+
interface Link {
|
5
|
+
text: string;
|
6
|
+
href: string;
|
7
|
+
}
|
8
|
+
|
9
|
+
interface Props {
|
10
|
+
title: string;
|
11
|
+
description: string;
|
12
|
+
image?: {
|
13
|
+
src: string;
|
14
|
+
alt: string;
|
15
|
+
};
|
16
|
+
links: Link[];
|
17
|
+
}
|
18
|
+
|
19
|
+
const { title, description, image, links = [] } = Astro.props;
|
20
|
+
---
|
21
|
+
|
22
|
+
<div class="py-16 px-8 text-center w-full min-h-screen relative overflow-hidden">
|
23
|
+
<div class="relative z-10 rounded-lg w-full h-full">
|
24
|
+
{image && <img src={image.src} alt={image.alt} class="h-1/2 mx-auto mb-8 drop-shadow-xl" />}
|
25
|
+
|
26
|
+
<h2 class="text-4xl mb-4 animate-fade-up">{title}</h2>
|
27
|
+
<p class="text-lg mb-12 text-center max-w-2xl mx-auto">
|
28
|
+
{description}
|
29
|
+
</p>
|
30
|
+
|
31
|
+
<div class="my-12 w-full">
|
32
|
+
<slot name="app" />
|
33
|
+
</div>
|
34
|
+
|
35
|
+
<div class="flex flex-row justify-center gap-8 mx-auto w-full">
|
36
|
+
{
|
37
|
+
links.map((link) => (
|
38
|
+
<Link
|
39
|
+
href={link.href}
|
40
|
+
external
|
41
|
+
variant="cta"
|
42
|
+
animation="hover-lift"
|
43
|
+
size="lg"
|
44
|
+
>
|
45
|
+
{link.text}
|
46
|
+
</Link>
|
47
|
+
))
|
48
|
+
}
|
49
|
+
</div>
|
50
|
+
</div>
|
51
|
+
</div>
|
52
|
+
|
53
|
+
<style>
|
54
|
+
@keyframes fade-up {
|
55
|
+
from {
|
56
|
+
opacity: 0;
|
57
|
+
transform: translateY(-20px);
|
58
|
+
}
|
59
|
+
|
60
|
+
to {
|
61
|
+
opacity: 1;
|
62
|
+
transform: translateY(0);
|
63
|
+
}
|
64
|
+
}
|
65
|
+
|
66
|
+
.animate-fade-up {
|
67
|
+
animation: fade-up 0.8s ease-out forwards;
|
68
|
+
}
|
69
|
+
</style>
|