@windrun-huaiin/third-ui 3.2.0
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/LICENSE +21 -0
- package/README.md +210 -0
- package/dist/clerk/index.d.mts +33 -0
- package/dist/clerk/index.d.ts +33 -0
- package/dist/clerk/index.js +2395 -0
- package/dist/clerk/index.js.map +1 -0
- package/dist/clerk/index.mjs +2361 -0
- package/dist/clerk/index.mjs.map +1 -0
- package/dist/cta.css +16 -0
- package/dist/fuma/index.d.mts +51 -0
- package/dist/fuma/index.d.ts +51 -0
- package/dist/fuma/index.js +2976 -0
- package/dist/fuma/index.js.map +1 -0
- package/dist/fuma/index.mjs +2944 -0
- package/dist/fuma/index.mjs.map +1 -0
- package/dist/fuma/mdx/index.d.mts +94 -0
- package/dist/fuma/mdx/index.d.ts +94 -0
- package/dist/fuma/mdx/index.js +2866 -0
- package/dist/fuma/mdx/index.js.map +1 -0
- package/dist/fuma/mdx/index.mjs +2827 -0
- package/dist/fuma/mdx/index.mjs.map +1 -0
- package/dist/fuma.css +132 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3597 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +3549 -0
- package/dist/index.mjs.map +1 -0
- package/dist/lib/index.d.mts +4414 -0
- package/dist/lib/index.d.ts +4414 -0
- package/dist/lib/index.js +127 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/index.mjs +95 -0
- package/dist/lib/index.mjs.map +1 -0
- package/dist/main/index.d.mts +25 -0
- package/dist/main/index.d.ts +25 -0
- package/dist/main/index.js +3003 -0
- package/dist/main/index.js.map +1 -0
- package/dist/main/index.mjs +2963 -0
- package/dist/main/index.mjs.map +1 -0
- package/dist/third-ui.css +44 -0
- package/package.json +106 -0
- package/src/clerk/clerk-organization.tsx +47 -0
- package/src/clerk/clerk-page-generator.tsx +42 -0
- package/src/clerk/clerk-provider-client.tsx +57 -0
- package/src/clerk/clerk-user.tsx +59 -0
- package/src/clerk/index.ts +5 -0
- package/src/fuma/fuma-banner-suit.tsx +16 -0
- package/src/fuma/fuma-github-info.tsx +194 -0
- package/src/fuma/fuma-page-genarator.tsx +94 -0
- package/src/fuma/index.ts +4 -0
- package/src/fuma/mdx/airtical-card.tsx +56 -0
- package/src/fuma/mdx/gradient-button.tsx +62 -0
- package/src/fuma/mdx/image-grid.tsx +35 -0
- package/src/fuma/mdx/image-zoom.tsx +84 -0
- package/src/fuma/mdx/index.ts +8 -0
- package/src/fuma/mdx/mermaid.tsx +87 -0
- package/src/fuma/mdx/toc-base.tsx +88 -0
- package/src/fuma/mdx/toc.tsx +35 -0
- package/src/fuma/mdx/trophy-card.tsx +36 -0
- package/src/fuma/mdx/zia-card.tsx +46 -0
- package/src/index.ts +4 -0
- package/src/lib/clerk-intl.ts +13 -0
- package/src/lib/fuma-schema-check-util.ts +73 -0
- package/src/lib/fuma-search-util.ts +6 -0
- package/src/lib/index.ts +3 -0
- package/src/main/ads-alert-dialog.tsx +133 -0
- package/src/main/cta.tsx +28 -0
- package/src/main/faq.tsx +58 -0
- package/src/main/features.tsx +35 -0
- package/src/main/footer.tsx +37 -0
- package/src/main/gallery.tsx +68 -0
- package/src/main/go-to-top.tsx +44 -0
- package/src/main/index.ts +12 -0
- package/src/main/loading.tsx +93 -0
- package/src/main/nprogress-bar.tsx +24 -0
- package/src/main/seo-content.tsx +34 -0
- package/src/main/tips.tsx +38 -0
- package/src/main/usage.tsx +45 -0
- package/src/styles/cta.css +16 -0
- package/src/styles/fuma.css +132 -0
- package/src/styles/third-ui.css +43 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@lib/utils';
|
|
4
|
+
import Link from 'fumadocs-core/link';
|
|
5
|
+
import type { HTMLAttributes, ReactNode } from 'react';
|
|
6
|
+
|
|
7
|
+
export function AirticalCards(props: HTMLAttributes<HTMLDivElement>) {
|
|
8
|
+
return (
|
|
9
|
+
<div
|
|
10
|
+
{...props}
|
|
11
|
+
className={cn('grid grid-cols-2 gap-4 @container', props.className)}
|
|
12
|
+
>
|
|
13
|
+
{props.children}
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type AirticalCardProps = Omit<HTMLAttributes<HTMLElement>, 'title'> & {
|
|
19
|
+
icon?: ReactNode;
|
|
20
|
+
title: ReactNode;
|
|
21
|
+
description?: ReactNode;
|
|
22
|
+
|
|
23
|
+
href?: string;
|
|
24
|
+
external?: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function AirticalCard({ icon, title, description, ...props }: AirticalCardProps) {
|
|
28
|
+
const E = props.href ? Link : 'div';
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<E
|
|
32
|
+
{...props}
|
|
33
|
+
data-card
|
|
34
|
+
className={cn(
|
|
35
|
+
'block rounded-lg border bg-fd-card p-4 text-fd-card-foreground shadow-md transition-colors @max-lg:col-span-full',
|
|
36
|
+
props.href && 'hover:bg-fd-accent/80',
|
|
37
|
+
props.className,
|
|
38
|
+
)}
|
|
39
|
+
>
|
|
40
|
+
{icon ? (
|
|
41
|
+
<div className="not-prose mb-2 w-fit rounded-md border bg-fd-muted p-1.5 text-fd-muted-foreground [&_svg]:size-4">
|
|
42
|
+
{icon}
|
|
43
|
+
</div>
|
|
44
|
+
) : null}
|
|
45
|
+
<h3 className="not-prose mb-1 text-sm font-medium">{title}</h3>
|
|
46
|
+
{description ? (
|
|
47
|
+
<p className="!my-0 text-sm text-fd-muted-foreground">{description}</p>
|
|
48
|
+
) : null}
|
|
49
|
+
{props.children ? (
|
|
50
|
+
<div className="text-sm text-fd-muted-foreground prose-no-margin">
|
|
51
|
+
{props.children}
|
|
52
|
+
</div>
|
|
53
|
+
) : null}
|
|
54
|
+
</E>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Button } from "@base-ui/ui/button";
|
|
4
|
+
import { globalLucideIcons as icons } from "@base-ui/components/global-icon";
|
|
5
|
+
import Link from "fumadocs-core/link";
|
|
6
|
+
import React from 'react';
|
|
7
|
+
|
|
8
|
+
export function GradientButton({
|
|
9
|
+
title,
|
|
10
|
+
icon,
|
|
11
|
+
href,
|
|
12
|
+
align = 'left',
|
|
13
|
+
}: {
|
|
14
|
+
title: React.ReactNode;
|
|
15
|
+
icon?: React.ReactNode;
|
|
16
|
+
href: string;
|
|
17
|
+
align?: 'left' | 'center' | 'right';
|
|
18
|
+
}) {
|
|
19
|
+
// set justify class according to alignment
|
|
20
|
+
const getAlignmentClass = () => {
|
|
21
|
+
switch (align) {
|
|
22
|
+
case 'center':
|
|
23
|
+
return 'justify-center';
|
|
24
|
+
case 'right':
|
|
25
|
+
return 'justify-end';
|
|
26
|
+
default: // 'left'
|
|
27
|
+
return 'justify-start';
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className={`flex flex-col sm:flex-row gap-3 ${getAlignmentClass()}`}>
|
|
33
|
+
<Button
|
|
34
|
+
asChild
|
|
35
|
+
size="lg"
|
|
36
|
+
className="
|
|
37
|
+
bg-gradient-to-r
|
|
38
|
+
from-purple-400 to-pink-500
|
|
39
|
+
hover:from-purple-500 hover:to-pink-600
|
|
40
|
+
dark:from-purple-500 dark:to-pink-600
|
|
41
|
+
dark:hover:from-purple-600 dark:hover:to-pink-700
|
|
42
|
+
text-white text-base font-bold shadow-lg hover:shadow-xl
|
|
43
|
+
transition-all duration-300
|
|
44
|
+
rounded-full
|
|
45
|
+
"
|
|
46
|
+
>
|
|
47
|
+
<Link href={href} target="_blank" rel="noopener noreferrer" className="no-underline hover:no-underline">
|
|
48
|
+
<span>{title}</span>
|
|
49
|
+
<span className="ml-1">
|
|
50
|
+
{icon ?
|
|
51
|
+
React.cloneElement(icon as React.ReactElement<{ className?: string }>, {
|
|
52
|
+
className: "h-4 w-4 text-white"
|
|
53
|
+
}) :
|
|
54
|
+
<icons.ArrowRight className="h-4 w-4 text-white" />
|
|
55
|
+
}
|
|
56
|
+
</span>
|
|
57
|
+
</Link>
|
|
58
|
+
</Button>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ImageZoom } from '@third-ui/fuma/mdx/image-zoom';
|
|
4
|
+
|
|
5
|
+
export function ImageGrid({
|
|
6
|
+
type="url",
|
|
7
|
+
images,
|
|
8
|
+
altPrefix = '',
|
|
9
|
+
cdnBaseUrl,
|
|
10
|
+
}: {
|
|
11
|
+
type: "url" | "local";
|
|
12
|
+
images: string[];
|
|
13
|
+
altPrefix?: string;
|
|
14
|
+
cdnBaseUrl?: string;
|
|
15
|
+
}) {
|
|
16
|
+
return (
|
|
17
|
+
<div
|
|
18
|
+
style={{
|
|
19
|
+
display: 'grid',
|
|
20
|
+
gridTemplateColumns: 'repeat(2, 1fr)',
|
|
21
|
+
gap: '16px',
|
|
22
|
+
justifyItems: 'center',
|
|
23
|
+
alignItems: 'center',
|
|
24
|
+
}}
|
|
25
|
+
>
|
|
26
|
+
{images.map((img, idx) => (
|
|
27
|
+
<ImageZoom
|
|
28
|
+
key={img}
|
|
29
|
+
src={type === "url" ? `${cdnBaseUrl}/${img}` : img}
|
|
30
|
+
alt={`${altPrefix}-${idx+1}`}
|
|
31
|
+
/>
|
|
32
|
+
))}
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Image, type ImageProps } from 'fumadocs-core/framework';
|
|
4
|
+
import { type ImgHTMLAttributes, useState } from 'react';
|
|
5
|
+
import Zoom, { type UncontrolledProps } from 'react-medium-image-zoom';
|
|
6
|
+
|
|
7
|
+
export type ImageZoomProps = ImageProps & {
|
|
8
|
+
/**
|
|
9
|
+
* Image props when zoom in
|
|
10
|
+
*/
|
|
11
|
+
zoomInProps?: ImgHTMLAttributes<HTMLImageElement>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Props for `react-medium-image-zoom`
|
|
15
|
+
*/
|
|
16
|
+
rmiz?: UncontrolledProps;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* placeholder image path
|
|
20
|
+
*/
|
|
21
|
+
fallbackSrc?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function getImageSrc(src: ImageProps['src']): string {
|
|
25
|
+
if (typeof src === 'string') return src;
|
|
26
|
+
|
|
27
|
+
if (typeof src === 'object') {
|
|
28
|
+
// Next.js
|
|
29
|
+
if ('default' in src)
|
|
30
|
+
return (src as { default: { src: string } }).default.src;
|
|
31
|
+
return src.src;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return '';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @example
|
|
39
|
+
* <ImageZoom src="URL" fallbackSrc="/my-placeholder.png" />
|
|
40
|
+
*/
|
|
41
|
+
export function ImageZoom({
|
|
42
|
+
zoomInProps,
|
|
43
|
+
children,
|
|
44
|
+
rmiz,
|
|
45
|
+
fallbackSrc = '/default.webp',
|
|
46
|
+
...props
|
|
47
|
+
}: ImageZoomProps) {
|
|
48
|
+
const [imgSrc, setImgSrc] = useState(getImageSrc(props.src));
|
|
49
|
+
|
|
50
|
+
// fallback logic
|
|
51
|
+
const handleError = () => {
|
|
52
|
+
console.warn('ImageZoom check error:', imgSrc, fallbackSrc);
|
|
53
|
+
if (imgSrc !== fallbackSrc) {
|
|
54
|
+
setImgSrc(fallbackSrc);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Zoom
|
|
60
|
+
zoomMargin={20}
|
|
61
|
+
wrapElement="span"
|
|
62
|
+
{...rmiz}
|
|
63
|
+
zoomImg={{
|
|
64
|
+
src: imgSrc,
|
|
65
|
+
sizes: undefined,
|
|
66
|
+
...zoomInProps,
|
|
67
|
+
onError: handleError,
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
{children ?? (
|
|
71
|
+
<Image
|
|
72
|
+
{...props}
|
|
73
|
+
src={imgSrc}
|
|
74
|
+
onError={handleError}
|
|
75
|
+
sizes="(max-width: 400px) 100vw, 300px"
|
|
76
|
+
style={{ width: '100%', height: 'auto', maxWidth: 300 }}
|
|
77
|
+
alt={props.alt ?? ''}
|
|
78
|
+
width={300}
|
|
79
|
+
height={225}
|
|
80
|
+
/>
|
|
81
|
+
)}
|
|
82
|
+
</Zoom>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { globalLucideIcons as icons } from '@base-ui/components/global-icon';
|
|
4
|
+
import type { MermaidConfig } from 'mermaid';
|
|
5
|
+
import { useTheme } from 'next-themes';
|
|
6
|
+
import { useEffect, useId, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
interface MermaidProps {
|
|
9
|
+
chart: string;
|
|
10
|
+
title?: string;
|
|
11
|
+
watermarkEnabled?: boolean;
|
|
12
|
+
watermarkText?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function Mermaid({ chart, title, watermarkEnabled, watermarkText }: MermaidProps) {
|
|
16
|
+
const id = useId();
|
|
17
|
+
const [svg, setSvg] = useState('');
|
|
18
|
+
const { resolvedTheme } = useTheme();
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
let isMounted = true;
|
|
22
|
+
void renderChart();
|
|
23
|
+
|
|
24
|
+
async function renderChart() {
|
|
25
|
+
const mermaidConfig: MermaidConfig = {
|
|
26
|
+
startOnLoad: false,
|
|
27
|
+
securityLevel: 'loose',
|
|
28
|
+
fontFamily: 'inherit',
|
|
29
|
+
themeCSS: 'margin: 1.5rem auto 0;',
|
|
30
|
+
theme: resolvedTheme === 'dark' ? 'dark' : 'default',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const { default: mermaid } = await import('mermaid');
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
mermaid.initialize(mermaidConfig);
|
|
37
|
+
const { svg } = await mermaid.render(
|
|
38
|
+
id.replaceAll(':', ''),
|
|
39
|
+
chart.replaceAll('\\n', '\n')
|
|
40
|
+
);
|
|
41
|
+
let svgWithWatermark = svg;
|
|
42
|
+
if (watermarkEnabled && watermarkText) {
|
|
43
|
+
svgWithWatermark = addWatermarkToSvg(svg, watermarkText);
|
|
44
|
+
}
|
|
45
|
+
if (isMounted) setSvg(svgWithWatermark);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error('Error while rendering mermaid', error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return () => {
|
|
51
|
+
isMounted = false;
|
|
52
|
+
setSvg('');
|
|
53
|
+
};
|
|
54
|
+
}, [chart, id, resolvedTheme, watermarkEnabled, watermarkText]);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div>
|
|
58
|
+
<div dangerouslySetInnerHTML={{ __html: svg }} />
|
|
59
|
+
{title && (
|
|
60
|
+
<div
|
|
61
|
+
className="mt-2 flex items-center justify-center text-center text-[13px] font-italic text-[#AC62FD]"
|
|
62
|
+
>
|
|
63
|
+
<icons.Mmd className='mr-1 h-4 w-4' />
|
|
64
|
+
<span>{title}</span>
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function addWatermarkToSvg(svg: string, watermark: string) {
|
|
72
|
+
const watermarkText = `
|
|
73
|
+
<text
|
|
74
|
+
x="100%"
|
|
75
|
+
y="98%"
|
|
76
|
+
text-anchor="end"
|
|
77
|
+
font-size="12"
|
|
78
|
+
font-style="italic"
|
|
79
|
+
fill="#AC62FD"
|
|
80
|
+
opacity="0.40"
|
|
81
|
+
class="pointer-events-none"
|
|
82
|
+
dx="-8"
|
|
83
|
+
dy="-4"
|
|
84
|
+
>${watermark}</text>
|
|
85
|
+
`;
|
|
86
|
+
return svg.replace('</svg>', `${watermarkText}</svg>`);
|
|
87
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { useParams } from 'next/navigation';
|
|
4
|
+
import { useCopyButton } from 'fumadocs-ui/utils/use-copy-button';
|
|
5
|
+
import Link from 'fumadocs-core/link';
|
|
6
|
+
import { globalLucideIcons as icons } from '@base-ui/components/global-icon';
|
|
7
|
+
import { Button } from '@base-ui/ui/button';
|
|
8
|
+
|
|
9
|
+
const cache = new Map<string, string>();
|
|
10
|
+
|
|
11
|
+
export function LLMCopyButton({ llmApiUrl }: { llmApiUrl?: string } = {}) {
|
|
12
|
+
const [isLoading, setLoading] = useState(false);
|
|
13
|
+
const params = useParams();
|
|
14
|
+
const locale = params.locale as string;
|
|
15
|
+
const slug = params.slug as string[];
|
|
16
|
+
|
|
17
|
+
const [checked, onClick] = useCopyButton(async () => {
|
|
18
|
+
setLoading(true);
|
|
19
|
+
|
|
20
|
+
// Handle cases where slug might be undefined or empty
|
|
21
|
+
const path = (slug && Array.isArray(slug)) ? slug.join('/') : '';
|
|
22
|
+
const apiPrefix = llmApiUrl || '/api/llm-content';
|
|
23
|
+
const apiUrl = `${apiPrefix}?locale=${encodeURIComponent(locale)}&path=${encodeURIComponent(path)}`;
|
|
24
|
+
console.log('Fetching LLM content from:', apiUrl);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const content: string =
|
|
28
|
+
cache.get(apiUrl) ?? (await fetch(apiUrl).then((res) => {
|
|
29
|
+
if (!res.ok) {
|
|
30
|
+
throw new Error(`Failed to fetch LLM content: ${res.status} ${res.statusText}`);
|
|
31
|
+
}
|
|
32
|
+
return res.text();
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
cache.set(apiUrl, content);
|
|
36
|
+
await navigator.clipboard.writeText(content);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error("Error fetching or copying LLM content:", error);
|
|
39
|
+
} finally {
|
|
40
|
+
setLoading(false);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<Button
|
|
46
|
+
variant="ghost"
|
|
47
|
+
size="sm"
|
|
48
|
+
loading={isLoading}
|
|
49
|
+
// force button to left align
|
|
50
|
+
className="justify-start px-0 text-stone-600 hover:text-stone-500 dark:text-stone-400 dark:hover:text-stone-300"
|
|
51
|
+
onClick={onClick}
|
|
52
|
+
>
|
|
53
|
+
{checked ? (
|
|
54
|
+
<>
|
|
55
|
+
<icons.Check/>
|
|
56
|
+
Copied!
|
|
57
|
+
</>
|
|
58
|
+
) : (
|
|
59
|
+
<>
|
|
60
|
+
<icons.Markdown/>
|
|
61
|
+
Copy page as Markdown
|
|
62
|
+
</>
|
|
63
|
+
)}
|
|
64
|
+
</Button>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function EditOnGitHub({ url }: { url: string }) {
|
|
69
|
+
return (
|
|
70
|
+
<Link
|
|
71
|
+
className="flex items-center gap-x-2 text-stone-600 hover:text-stone-500 dark:text-stone-400 dark:hover:text-stone-300 text-sm"
|
|
72
|
+
href={url}
|
|
73
|
+
>
|
|
74
|
+
<icons.GitHub/>
|
|
75
|
+
Edit this page on GitHub
|
|
76
|
+
</Link>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// New component for displaying the last updated date with an icon
|
|
81
|
+
export function LastUpdatedDate({ date }: { date: string | undefined }) {
|
|
82
|
+
return (
|
|
83
|
+
<div className="flex items-center gap-x-2 text-stone-600 dark:text-stone-400 text-sm">
|
|
84
|
+
<icons.LastUpdated/>
|
|
85
|
+
Lastest on {date ? date : "Ages ago"}
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { EditOnGitHub, LastUpdatedDate, LLMCopyButton } from '@third-ui/fuma/mdx/toc-base';
|
|
4
|
+
|
|
5
|
+
interface TocFooterProps {
|
|
6
|
+
/**
|
|
7
|
+
* The last modified date of the page.
|
|
8
|
+
*/
|
|
9
|
+
lastModified: string | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* The path to the file for the \"Edit on GitHub\" link.
|
|
12
|
+
* This should be the relative path from the repository root, e.g., 'src/mdx/docs/your-page.mdx'.
|
|
13
|
+
*/
|
|
14
|
+
editPath?: string | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Whether to show the copy button.
|
|
17
|
+
*/
|
|
18
|
+
showCopy?: boolean | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* GitHub base URL for edit links
|
|
21
|
+
*/
|
|
22
|
+
githubBaseUrl?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function TocFooter({ lastModified, showCopy, editPath, githubBaseUrl }: TocFooterProps) {
|
|
26
|
+
const showEdit = githubBaseUrl && editPath;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="flex flex-col gap-y-2 items-start m-4">
|
|
30
|
+
<LastUpdatedDate date={lastModified} />
|
|
31
|
+
{showCopy && <LLMCopyButton />}
|
|
32
|
+
{showEdit && <EditOnGitHub url={`${githubBaseUrl}${editPath}`} />}
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
export function TrophyCard({
|
|
6
|
+
icon,
|
|
7
|
+
title,
|
|
8
|
+
children,
|
|
9
|
+
}: {
|
|
10
|
+
icon?: React.ReactNode;
|
|
11
|
+
title: React.ReactNode;
|
|
12
|
+
children?: React.ReactNode;
|
|
13
|
+
}) {
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
className="
|
|
17
|
+
border-2 rounded-xl px-4 py-2
|
|
18
|
+
border-purple-200 dark:border-gray-500
|
|
19
|
+
"
|
|
20
|
+
>
|
|
21
|
+
<div className="flex items-center font-bold text-sm">
|
|
22
|
+
<span className="mr-2">{icon}</span>
|
|
23
|
+
<span>{title}</span>
|
|
24
|
+
</div>
|
|
25
|
+
{/* leading-none:line-height: 1
|
|
26
|
+
leading-tight:line-height: 1.25
|
|
27
|
+
leading-snug:line-height: 1.375
|
|
28
|
+
leading-normal:line-height: 1.5
|
|
29
|
+
leading-relaxed:line-height: 1.625
|
|
30
|
+
leading-loose:line-height: 2 */}
|
|
31
|
+
<div className="text-sm -mt-1 leading-none">
|
|
32
|
+
{children}
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { globalLucideIcons as icons } from '@base-ui/components/global-icon';
|
|
4
|
+
import { cn } from '@lib/utils';
|
|
5
|
+
import Link from 'fumadocs-core/link';
|
|
6
|
+
import type { HTMLAttributes, ReactNode } from 'react';
|
|
7
|
+
|
|
8
|
+
export type ZiaCardProps = Omit<HTMLAttributes<HTMLElement>, 'title'> & {
|
|
9
|
+
icon?: ReactNode;
|
|
10
|
+
title: ReactNode;
|
|
11
|
+
description?: ReactNode;
|
|
12
|
+
|
|
13
|
+
href?: string;
|
|
14
|
+
external?: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function ZiaCard({ icon, title, description, ...props }: ZiaCardProps) {
|
|
18
|
+
const E = props.href ? Link : 'div';
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<E
|
|
22
|
+
{...props}
|
|
23
|
+
data-card
|
|
24
|
+
className={cn(
|
|
25
|
+
'block rounded-lg border bg-fd-card p-4 text-fd-card-foreground shadow-md transition-colors @max-lg:col-span-full',
|
|
26
|
+
props.href && 'hover:bg-fd-accent/80',
|
|
27
|
+
props.className,
|
|
28
|
+
)}
|
|
29
|
+
>
|
|
30
|
+
<div className="not-prose mb-2 w-fit rounded-md border bg-fd-muted p-1.5 text-fd-muted-foreground [&_svg]:size-4">
|
|
31
|
+
{icon ? icon : <icons.CircleSmall />}
|
|
32
|
+
</div>
|
|
33
|
+
<h3 className="not-prose mb-1 text-sm font-medium line-clamp-2 min-h-[2.5rem]">{title}</h3>
|
|
34
|
+
{description ? (
|
|
35
|
+
<p className="!my-0 text-sm text-fd-muted-foreground">{description}</p>
|
|
36
|
+
) : (
|
|
37
|
+
<p className="!my-0 text-sm text-fd-muted-foreground opacity-0 select-none"> </p>
|
|
38
|
+
)}
|
|
39
|
+
{props.children ? (
|
|
40
|
+
<div className="text-sm text-fd-muted-foreground prose-no-margin">
|
|
41
|
+
{props.children}
|
|
42
|
+
</div>
|
|
43
|
+
) : null}
|
|
44
|
+
</E>
|
|
45
|
+
);
|
|
46
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { enUS, zhCN } from '@clerk/localizations';
|
|
2
|
+
import type { LocalizationResource } from '@clerk/types';
|
|
3
|
+
|
|
4
|
+
// https://github.com/clerk/javascript/blob/main/packages/localizations/src/en-US.ts#L492
|
|
5
|
+
// https://clerk.com/docs/customization/localization
|
|
6
|
+
const customZH: LocalizationResource = {
|
|
7
|
+
...zhCN,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const clerkIntl = {
|
|
11
|
+
en: enUS,
|
|
12
|
+
zh: customZH,
|
|
13
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { frontmatterSchema, metaSchema } from 'fumadocs-mdx/config';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
// Reusable schema for title
|
|
6
|
+
export const createTitleSchema = () =>
|
|
7
|
+
z.string({
|
|
8
|
+
required_error: "Title is required",
|
|
9
|
+
invalid_type_error: "Title must be a string and cannot be null",
|
|
10
|
+
})
|
|
11
|
+
.trim()
|
|
12
|
+
.min(1, { message: "Title cannot be empty or consist only of whitespace" });
|
|
13
|
+
|
|
14
|
+
// Reusable schema for description
|
|
15
|
+
export const createDescriptionSchema = () =>
|
|
16
|
+
z.preprocess(
|
|
17
|
+
(val: any) => {
|
|
18
|
+
if (typeof val === 'string') {
|
|
19
|
+
return val.trim() === "" || val === null ? undefined : val.trim();
|
|
20
|
+
}
|
|
21
|
+
return val === null ? undefined : val;
|
|
22
|
+
},
|
|
23
|
+
z.string().optional()
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// Reusable schema for icon
|
|
27
|
+
export const createIconSchema = () =>
|
|
28
|
+
z.preprocess(
|
|
29
|
+
(val: any) => (val === "" || val === null ? undefined : val),
|
|
30
|
+
z.string().optional()
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Reusable schema for date
|
|
34
|
+
export const createDateSchema = () =>
|
|
35
|
+
z.preprocess((arg: any) => {
|
|
36
|
+
if (arg instanceof Date) {
|
|
37
|
+
// Format Date object to YYYY-MM-DD string
|
|
38
|
+
const year = arg.getFullYear();
|
|
39
|
+
const month = (arg.getMonth() + 1).toString().padStart(2, '0');
|
|
40
|
+
const day = arg.getDate().toString().padStart(2, '0');
|
|
41
|
+
return `${year}-${month}-${day}`;
|
|
42
|
+
}
|
|
43
|
+
if (typeof arg === 'string') {
|
|
44
|
+
return arg.trim();
|
|
45
|
+
}
|
|
46
|
+
// For other types or null/undefined, let the subsequent string validation handle it
|
|
47
|
+
return arg;
|
|
48
|
+
},
|
|
49
|
+
z.string()
|
|
50
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be in YYYY-MM-DD format or a valid Date object")
|
|
51
|
+
.refine((val: any) => !isNaN(new Date(val).getTime()), 'Invalid date!')
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// common docs frontmatter schema
|
|
55
|
+
export const createCommonDocsSchema = () => frontmatterSchema.extend({
|
|
56
|
+
title: createTitleSchema(),
|
|
57
|
+
description: createDescriptionSchema(),
|
|
58
|
+
icon: createIconSchema(),
|
|
59
|
+
date: createDateSchema(),
|
|
60
|
+
author: z.string().optional(),
|
|
61
|
+
keywords: z.array(z.string()).optional(),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// common meta schema
|
|
65
|
+
export const createCommonMetaSchema = () => metaSchema.extend({
|
|
66
|
+
description: z.string().optional(),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
export const remarkInstallOptions = {
|
|
70
|
+
persist: {
|
|
71
|
+
id: 'package-manager',
|
|
72
|
+
},
|
|
73
|
+
};
|
package/src/lib/index.ts
ADDED