@windrun-huaiin/third-ui 5.12.0 → 5.12.2
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/dist/main/index.d.mts +6 -1
- package/dist/main/index.d.ts +6 -1
- package/dist/main/index.js +317 -289
- package/dist/main/index.js.map +1 -1
- package/dist/main/index.mjs +259 -233
- package/dist/main/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/main/cta.tsx +2 -1
- package/src/main/faq.tsx +3 -2
- package/src/main/features.tsx +3 -2
- package/src/main/gallery.tsx +56 -46
- package/src/main/index.ts +2 -1
- package/src/main/rich-text-expert.tsx +35 -0
- package/src/main/seo-content.tsx +6 -5
- package/src/main/tips.tsx +11 -6
- package/src/main/usage.tsx +3 -2
package/package.json
CHANGED
package/src/main/cta.tsx
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { GradientButton } from "@third-ui/fuma/mdx/gradient-button";
|
|
4
4
|
import { useTranslations } from 'next-intl';
|
|
5
5
|
import { cn } from '@lib/utils';
|
|
6
|
+
import { richText } from '@third-ui/main/rich-text-expert';
|
|
6
7
|
|
|
7
8
|
export function CTA({ sectionClassName }: { sectionClassName?: string }) {
|
|
8
9
|
const t = useTranslations('cta');
|
|
@@ -18,7 +19,7 @@ export function CTA({ sectionClassName }: { sectionClassName?: string }) {
|
|
|
18
19
|
{t('title')} <span className="text-purple-400">{t('eyesOn')}</span>?
|
|
19
20
|
</h2>
|
|
20
21
|
<p className="text-2xl mx-auto mb-8">
|
|
21
|
-
{t
|
|
22
|
+
{richText(t, 'description1')}
|
|
22
23
|
<br />
|
|
23
24
|
<span className="text-purple-400">{t('description2')}</span>
|
|
24
25
|
</p>
|
package/src/main/faq.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { useState } from "react";
|
|
|
4
4
|
import { useTranslations } from 'next-intl';
|
|
5
5
|
import { globalLucideIcons as icons } from '@base-ui/components/global-icon';
|
|
6
6
|
import { cn } from '@lib/utils';
|
|
7
|
+
import { richText } from '@third-ui/main/rich-text-expert';
|
|
7
8
|
|
|
8
9
|
export function FAQ({ sectionClassName }: { sectionClassName?: string }) {
|
|
9
10
|
const t = useTranslations('faq');
|
|
@@ -27,7 +28,7 @@ export function FAQ({ sectionClassName }: { sectionClassName?: string }) {
|
|
|
27
28
|
{t('title')}
|
|
28
29
|
</h2>
|
|
29
30
|
<p className="text-center text-gray-600 dark:text-gray-400 mb-12 text-base md:text-lg mx-auto">
|
|
30
|
-
{t
|
|
31
|
+
{richText(t, 'description')}
|
|
31
32
|
</p>
|
|
32
33
|
<div className="space-y-6">
|
|
33
34
|
{items.map((item, idx) => {
|
|
@@ -48,7 +49,7 @@ export function FAQ({ sectionClassName }: { sectionClassName?: string }) {
|
|
|
48
49
|
</button>
|
|
49
50
|
{isOpen && (
|
|
50
51
|
<div className="mt-4 text-gray-700 dark:text-gray-300 text-base">
|
|
51
|
-
{
|
|
52
|
+
{richText(t, `items.${idx}.answer`)}
|
|
52
53
|
</div>
|
|
53
54
|
)}
|
|
54
55
|
</div>
|
package/src/main/features.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { getGlobalIcon } from '@base-ui/components/global-icon';
|
|
|
4
4
|
import { useTranslations } from 'next-intl'
|
|
5
5
|
import { globalLucideIcons as icons } from '@base-ui/components/global-icon';
|
|
6
6
|
import { cn } from '@lib/utils';
|
|
7
|
+
import { richText } from '@third-ui/main/rich-text-expert';
|
|
7
8
|
|
|
8
9
|
export function Features({ sectionClassName }: { sectionClassName?: string }) {
|
|
9
10
|
const t = useTranslations('features');
|
|
@@ -21,7 +22,7 @@ export function Features({ sectionClassName }: { sectionClassName?: string }) {
|
|
|
21
22
|
{t('title')} <span className="text-purple-500">{t('eyesOn')}</span>
|
|
22
23
|
</h2>
|
|
23
24
|
<p className="text-center text-gray-600 dark:text-gray-400 mb-12 text-base md:text-lg mx-auto whitespace-nowrap">
|
|
24
|
-
{t
|
|
25
|
+
{richText(t, 'description')}
|
|
25
26
|
</p>
|
|
26
27
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 gap-y-12">
|
|
27
28
|
{featureItems.map((feature, index) => {
|
|
@@ -35,7 +36,7 @@ export function Features({ sectionClassName }: { sectionClassName?: string }) {
|
|
|
35
36
|
<Icon className="w-8 h-8" />
|
|
36
37
|
</div>
|
|
37
38
|
<h3 className="text-xl font-semibold mb-3 text-gray-900 dark:text-gray-100">{feature.title}</h3>
|
|
38
|
-
<p className="text-gray-700 dark:text-gray-300">{
|
|
39
|
+
<p className="text-gray-700 dark:text-gray-300">{richText(t, `items.${index}.description`)}</p>
|
|
39
40
|
</div>
|
|
40
41
|
)
|
|
41
42
|
})}
|
package/src/main/gallery.tsx
CHANGED
|
@@ -21,68 +21,69 @@ export function Gallery({ sectionClassName, button }: GalleryProps) {
|
|
|
21
21
|
const galleryItems = t.raw('prompts') as GalleryItem[];
|
|
22
22
|
const defaultImgUrl = t.raw('defaultImgUrl') as string;
|
|
23
23
|
const [imageErrors, setImageErrors] = useState<Set<number>>(new Set());
|
|
24
|
+
const [downloadingItems, setDownloadingItems] = useState<Set<number>>(new Set());
|
|
25
|
+
const cdnProxyUrl = process.env.NEXT_PUBLIC_STYLE_CDN_PROXY_URL!;
|
|
24
26
|
|
|
25
27
|
const handleDownload = async (item: GalleryItem, index: number) => {
|
|
28
|
+
// prevent duplicate clicks
|
|
29
|
+
if (downloadingItems.has(index)) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// set download status
|
|
34
|
+
setDownloadingItems(prev => new Set(prev).add(index));
|
|
35
|
+
|
|
26
36
|
try {
|
|
27
|
-
// use
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
mode: 'cors',
|
|
32
|
-
});
|
|
37
|
+
// use R2 proxy to download directly, no need to fetch
|
|
38
|
+
// convert original R2 URL to proxy URL
|
|
39
|
+
const originalUrl = new URL(item.url);
|
|
40
|
+
const filename = originalUrl.pathname.substring(1);
|
|
33
41
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
42
|
+
// build proxy download URL
|
|
43
|
+
const proxyUrl = `${cdnProxyUrl}/${encodeURIComponent(filename)}`;
|
|
37
44
|
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
// set extension based on the actual file type
|
|
42
|
-
const contentType = response.headers.get('content-type');
|
|
45
|
+
// extract file extension from URL
|
|
46
|
+
const urlExtension = item.url.split('.').pop()?.toLowerCase();
|
|
43
47
|
let extension = '.webp';
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
switch (contentType) {
|
|
47
|
-
case 'image/jpeg':
|
|
48
|
-
case 'image/jpg':
|
|
49
|
-
extension = '.jpg';
|
|
50
|
-
break;
|
|
51
|
-
case 'image/png':
|
|
52
|
-
extension = '.png';
|
|
53
|
-
break;
|
|
54
|
-
case 'image/gif':
|
|
55
|
-
extension = '.gif';
|
|
56
|
-
break;
|
|
57
|
-
case 'image/webp':
|
|
58
|
-
extension = '.webp';
|
|
59
|
-
break;
|
|
60
|
-
case 'image/svg+xml':
|
|
61
|
-
extension = '.svg';
|
|
62
|
-
break;
|
|
63
|
-
default:
|
|
64
|
-
// if cannot determine, try to extract the extension from the URL
|
|
65
|
-
const urlExtension = item.url.split('.').pop()?.toLowerCase();
|
|
66
|
-
if (urlExtension && ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(urlExtension)) {
|
|
67
|
-
extension = `.${urlExtension}`;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
48
|
+
if (urlExtension && ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(urlExtension)) {
|
|
49
|
+
extension = `.${urlExtension}`;
|
|
70
50
|
}
|
|
71
51
|
const downloadPrefix = t('downloadPrefix');
|
|
52
|
+
|
|
53
|
+
// fetch file from proxy
|
|
54
|
+
const response = await fetch(proxyUrl);
|
|
55
|
+
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// convert to blob
|
|
61
|
+
const blob = await response.blob();
|
|
62
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
63
|
+
|
|
64
|
+
// create download link and trigger download
|
|
72
65
|
const a = document.createElement('a');
|
|
73
|
-
a.href =
|
|
66
|
+
a.href = blobUrl;
|
|
74
67
|
a.download = `${downloadPrefix}-${index + 1}${extension}`;
|
|
75
68
|
a.style.display = 'none';
|
|
76
69
|
document.body.appendChild(a);
|
|
77
70
|
a.click();
|
|
78
71
|
|
|
79
|
-
// clean up
|
|
72
|
+
// clean up DOM elements and blob URL
|
|
80
73
|
setTimeout(() => {
|
|
81
|
-
window.URL.revokeObjectURL(url);
|
|
82
74
|
document.body.removeChild(a);
|
|
75
|
+
URL.revokeObjectURL(blobUrl);
|
|
83
76
|
}, 100);
|
|
77
|
+
|
|
84
78
|
} catch (error) {
|
|
85
79
|
console.error('Download failed:', error);
|
|
80
|
+
} finally {
|
|
81
|
+
// clear download status
|
|
82
|
+
setDownloadingItems(prev => {
|
|
83
|
+
const newSet = new Set(prev);
|
|
84
|
+
newSet.delete(index);
|
|
85
|
+
return newSet;
|
|
86
|
+
});
|
|
86
87
|
}
|
|
87
88
|
};
|
|
88
89
|
|
|
@@ -116,9 +117,19 @@ export function Gallery({ sectionClassName, button }: GalleryProps) {
|
|
|
116
117
|
<div className="absolute inset-0 flex items-end justify-end p-4 opacity-0 group-hover:opacity-100 transition duration-300">
|
|
117
118
|
<button
|
|
118
119
|
onClick={() => handleDownload(item, index)}
|
|
119
|
-
|
|
120
|
+
disabled={downloadingItems.has(index)}
|
|
121
|
+
className={cn(
|
|
122
|
+
"p-2 rounded-full transition-all duration-300",
|
|
123
|
+
downloadingItems.has(index)
|
|
124
|
+
? "bg-black/30 text-white/50"
|
|
125
|
+
: "bg-black/50 hover:bg-black/70 text-white/80 hover:text-white"
|
|
126
|
+
)}
|
|
120
127
|
>
|
|
121
|
-
|
|
128
|
+
{downloadingItems.has(index) ? (
|
|
129
|
+
<icons.Loader2 className="h-5 w-5 text-white animate-spin" />
|
|
130
|
+
) : (
|
|
131
|
+
<icons.Download className="h-5 w-5 text-white" />
|
|
132
|
+
)}
|
|
122
133
|
</button>
|
|
123
134
|
</div>
|
|
124
135
|
</div>
|
|
@@ -132,4 +143,3 @@ export function Gallery({ sectionClassName, button }: GalleryProps) {
|
|
|
132
143
|
</section>
|
|
133
144
|
)
|
|
134
145
|
}
|
|
135
|
-
|
package/src/main/index.ts
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
// default tag renderers
|
|
4
|
+
const defaultTagRenderers = {
|
|
5
|
+
// text Stong
|
|
6
|
+
strong: (chunks: React.ReactNode) => <strong>{chunks}</strong>,
|
|
7
|
+
// text Emphasis
|
|
8
|
+
em: (chunks: React.ReactNode) => <em>{chunks}</em>,
|
|
9
|
+
// text Underline
|
|
10
|
+
u: (chunks: React.ReactNode) => <u>{chunks}</u>,
|
|
11
|
+
// text Mark
|
|
12
|
+
mark: (chunks: React.ReactNode) => <mark className="bg-purple-300 dark:bg-purple-500 text-neutral-800 dark:text-neutral-300 px-1 rounded">{chunks}</mark>,
|
|
13
|
+
// text Delete
|
|
14
|
+
del: (chunks: React.ReactNode) => <del>{chunks}</del>,
|
|
15
|
+
// text Subscript
|
|
16
|
+
sub: (chunks: React.ReactNode) => <sub>{chunks}</sub>,
|
|
17
|
+
// text Superscript
|
|
18
|
+
sup: (chunks: React.ReactNode) => <sup>{chunks}</sup>,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// custom tag renderers
|
|
22
|
+
type TagRenderer = (chunks: React.ReactNode) => React.ReactElement;
|
|
23
|
+
type TagRenderers = Record<string, TagRenderer>;
|
|
24
|
+
|
|
25
|
+
// create rich text renderer
|
|
26
|
+
export function createRichTextRenderer(customRenderers?: TagRenderers) {
|
|
27
|
+
const renderers = { ...defaultTagRenderers, ...customRenderers };
|
|
28
|
+
|
|
29
|
+
return function richText(t: any, key: string) {
|
|
30
|
+
return t.rich(key, renderers);
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// default rich text renderer
|
|
35
|
+
export const richText = createRichTextRenderer();
|
package/src/main/seo-content.tsx
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { useTranslations } from 'next-intl'
|
|
5
5
|
import { cn } from '@lib/utils';
|
|
6
|
+
import { richText } from '@third-ui/main/rich-text-expert';
|
|
6
7
|
|
|
7
8
|
interface Section {
|
|
8
9
|
title: string;
|
|
@@ -14,28 +15,28 @@ export function SeoContent({ sectionClassName }: { sectionClassName?: string })
|
|
|
14
15
|
|
|
15
16
|
return (
|
|
16
17
|
<section id="seo" className={cn("px-16 py-10 mx-16 md:mx-32 scroll-mt-20", sectionClassName)}>
|
|
17
|
-
<
|
|
18
|
+
<h2 className="text-3xl md:text-4xl font-bold text-center mb-8">
|
|
18
19
|
{t('title')} <span className="text-purple-500">{t('eyesOn')}</span>
|
|
19
|
-
</
|
|
20
|
+
</h2>
|
|
20
21
|
<h3 className="text-center text-gray-600 dark:text-gray-400 mb-12 text-lg">
|
|
21
22
|
{t('description')}
|
|
22
23
|
</h3>
|
|
23
24
|
<div className="bg-gray-50 dark:bg-gray-800/60 border border-gray-200 dark:border-gray-700 rounded-2xl p-8 md:p-12 shadow-sm dark:shadow-none">
|
|
24
25
|
<div className="space-y-10">
|
|
25
26
|
<p className="text-gray-600 dark:text-gray-400 text-lg">
|
|
26
|
-
{t
|
|
27
|
+
{richText(t, 'intro')}
|
|
27
28
|
</p>
|
|
28
29
|
{t.raw('sections').map((section: Section, index: number) => (
|
|
29
30
|
<div key={index}>
|
|
30
31
|
<h2 className="text-xl font-semibold mb-3 text-gray-900 dark:text-gray-100 flex items-center">
|
|
31
32
|
{section.title}
|
|
32
33
|
</h2>
|
|
33
|
-
<p className="text-gray-700 dark:text-gray-300">{
|
|
34
|
+
<p className="text-gray-700 dark:text-gray-300">{richText(t, `sections.${index}.content`)}</p>
|
|
34
35
|
</div>
|
|
35
36
|
))}
|
|
36
37
|
</div>
|
|
37
38
|
<p className="mt-10 text-gray-600 dark:text-gray-400 text-lg">
|
|
38
|
-
{t
|
|
39
|
+
{richText(t, 'conclusion')}
|
|
39
40
|
</p>
|
|
40
41
|
</div>
|
|
41
42
|
</section>
|
package/src/main/tips.tsx
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useTranslations } from 'next-intl'
|
|
4
4
|
import { cn } from '@lib/utils';
|
|
5
|
+
import { richText } from '@third-ui/main/rich-text-expert';
|
|
5
6
|
|
|
6
7
|
interface Tip {
|
|
7
8
|
title: string;
|
|
@@ -24,12 +25,16 @@ export function Tips({ sectionClassName }: { sectionClassName?: string }) {
|
|
|
24
25
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 bg-gray-50 dark:bg-gray-800/60 border border-gray-200 dark:border-gray-700 rounded-2xl p-8 md:p-12 shadow-sm dark:shadow-none">
|
|
25
26
|
{[leftColumn, rightColumn].map((column: Tip[], colIndex) => (
|
|
26
27
|
<div key={colIndex} className="space-y-8">
|
|
27
|
-
{column.map((tip: Tip, tipIndex) =>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
{column.map((tip: Tip, tipIndex) => {
|
|
29
|
+
// calculate the actual index in the original array
|
|
30
|
+
const actualIndex = colIndex === 0 ? tipIndex : tipIndex + midPoint;
|
|
31
|
+
return (
|
|
32
|
+
<div key={tipIndex} className="space-y-4">
|
|
33
|
+
<h3 className="text-2xl font-semibold">{tip.title}</h3>
|
|
34
|
+
<p className="">{richText(t, `sections.${actualIndex}.description`)}</p>
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
})}
|
|
33
38
|
</div>
|
|
34
39
|
))}
|
|
35
40
|
</div>
|
package/src/main/usage.tsx
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useTranslations } from 'next-intl'
|
|
4
4
|
import { cn } from '@lib/utils';
|
|
5
5
|
import { globalLucideIcons as icons, getGlobalIcon } from '@base-ui/components/global-icon'
|
|
6
|
+
import { richText } from '@third-ui/main/rich-text-expert';
|
|
6
7
|
|
|
7
8
|
export function Usage({ sectionClassName }: { sectionClassName?: string }) {
|
|
8
9
|
const t = useTranslations('usage');
|
|
@@ -18,7 +19,7 @@ export function Usage({ sectionClassName }: { sectionClassName?: string }) {
|
|
|
18
19
|
{t('title')} <span className="text-purple-500">{t('eyesOn')}</span>
|
|
19
20
|
</h2>
|
|
20
21
|
<p className="text-center text-gray-600 dark:text-gray-400 mb-12 text-base md:text-lg mx-auto whitespace-nowrap">
|
|
21
|
-
{t
|
|
22
|
+
{richText(t, 'description')}
|
|
22
23
|
</p>
|
|
23
24
|
<div className="bg-gray-50 dark:bg-gray-800/60 border border-gray-200 dark:border-gray-700 rounded-2xl p-8 md:p-12 shadow-sm dark:shadow-none">
|
|
24
25
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 gap-y-12">
|
|
@@ -33,7 +34,7 @@ export function Usage({ sectionClassName }: { sectionClassName?: string }) {
|
|
|
33
34
|
<h3 className="text-xl font-semibold mb-3 text-gray-900 dark:text-gray-100 flex items-center">
|
|
34
35
|
{`${idx + 1}. ${step.title}`}
|
|
35
36
|
</h3>
|
|
36
|
-
<p className="text-gray-700 dark:text-gray-300">{
|
|
37
|
+
<p className="text-gray-700 dark:text-gray-300">{richText(t, `steps.${idx}.description`)}</p>
|
|
37
38
|
</div>
|
|
38
39
|
</div>
|
|
39
40
|
)
|