@aravindc26/velu 0.11.5 → 0.11.9
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/package.json +1 -1
- package/schema/velu.schema.json +383 -122
- package/src/build.ts +679 -551
- package/src/cli.ts +157 -2
- package/src/engine/app/(docs)/[...slug]/layout.tsx +179 -13
- package/src/engine/app/(docs)/[...slug]/page.tsx +87 -12
- package/src/engine/app/copy-page.css +17 -1
- package/src/engine/app/global.css +111 -5
- package/src/engine/app/layout.tsx +8 -1
- package/src/engine/app/page.tsx +4 -1
- package/src/engine/app/search.css +4 -0
- package/src/engine/components/banner.tsx +80 -0
- package/src/engine/components/copy-page.tsx +162 -35
- package/src/engine/components/dropdown-switcher.tsx +142 -0
- package/src/engine/components/header-tab-link.tsx +43 -0
- package/src/engine/components/lang-switcher.tsx +7 -1
- package/src/engine/components/page-feedback.tsx +14 -3
- package/src/engine/components/product-switcher.tsx +8 -2
- package/src/engine/components/search.tsx +136 -49
- package/src/engine/components/version-switcher.tsx +8 -2
- package/src/engine/lib/layout.shared.ts +68 -68
- package/src/engine/lib/velu.ts +305 -2
- package/src/engine/next.config.mjs +4 -0
- package/src/validate.ts +8 -0
|
@@ -32,6 +32,12 @@
|
|
|
32
32
|
margin-top: 0.35rem;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
.velu-page-last-updated {
|
|
36
|
+
margin: 0.35rem 0 0;
|
|
37
|
+
color: var(--color-fd-muted-foreground, #a1a1aa);
|
|
38
|
+
font-size: 0.8125rem;
|
|
39
|
+
}
|
|
40
|
+
|
|
35
41
|
.velu-rss-button {
|
|
36
42
|
display: inline-flex;
|
|
37
43
|
align-items: center;
|
|
@@ -155,10 +161,20 @@
|
|
|
155
161
|
background-color: var(--color-fd-accent, #27272a);
|
|
156
162
|
}
|
|
157
163
|
|
|
164
|
+
.velu-copy-option-icon {
|
|
165
|
+
display: inline-flex;
|
|
166
|
+
align-items: center;
|
|
167
|
+
justify-content: center;
|
|
168
|
+
width: 18px;
|
|
169
|
+
height: 18px;
|
|
170
|
+
flex-shrink: 0;
|
|
171
|
+
margin-top: 0.15rem;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.velu-copy-option-icon svg,
|
|
158
175
|
.velu-copy-option svg {
|
|
159
176
|
flex-shrink: 0;
|
|
160
177
|
opacity: 0.7;
|
|
161
|
-
margin-top: 0.15rem;
|
|
162
178
|
overflow: visible;
|
|
163
179
|
}
|
|
164
180
|
|
|
@@ -7,6 +7,69 @@ body {
|
|
|
7
7
|
min-height: 100vh;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
body:has(.velu-announcement) #nd-docs-layout,
|
|
11
|
+
body:has(.velu-announcement) #nd-notebook-layout {
|
|
12
|
+
--fd-banner-height: var(--velu-announcement-h, 0px) !important;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* ── Global announcement banner ── */
|
|
16
|
+
.velu-announcement {
|
|
17
|
+
position: sticky;
|
|
18
|
+
top: 0;
|
|
19
|
+
z-index: 30;
|
|
20
|
+
display: flex;
|
|
21
|
+
align-items: center;
|
|
22
|
+
justify-content: center;
|
|
23
|
+
width: 100%;
|
|
24
|
+
padding: 0.5rem 1rem;
|
|
25
|
+
background-color: var(--color-fd-primary);
|
|
26
|
+
color: #fff;
|
|
27
|
+
font-size: 0.875rem;
|
|
28
|
+
line-height: 1.4;
|
|
29
|
+
text-align: center;
|
|
30
|
+
gap: 0.5rem;
|
|
31
|
+
border-radius: 0;
|
|
32
|
+
margin: 0;
|
|
33
|
+
border: none;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.velu-announcement a {
|
|
37
|
+
color: #fff;
|
|
38
|
+
text-decoration: underline;
|
|
39
|
+
text-underline-offset: 2px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.velu-announcement a:hover {
|
|
43
|
+
opacity: 0.85;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.velu-announcement-content {
|
|
47
|
+
flex: 1;
|
|
48
|
+
min-width: 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.velu-announcement-dismiss {
|
|
52
|
+
flex-shrink: 0;
|
|
53
|
+
display: inline-flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
justify-content: center;
|
|
56
|
+
width: 1.5rem;
|
|
57
|
+
height: 1.5rem;
|
|
58
|
+
padding: 0;
|
|
59
|
+
border: none;
|
|
60
|
+
border-radius: 0.25rem;
|
|
61
|
+
background: transparent;
|
|
62
|
+
color: #fff;
|
|
63
|
+
font-size: 1rem;
|
|
64
|
+
cursor: pointer;
|
|
65
|
+
opacity: 0.8;
|
|
66
|
+
transition: opacity 0.15s;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.velu-announcement-dismiss:hover {
|
|
70
|
+
opacity: 1;
|
|
71
|
+
}
|
|
72
|
+
|
|
10
73
|
.velu-nav-brand {
|
|
11
74
|
display: inline-flex;
|
|
12
75
|
align-items: center;
|
|
@@ -581,13 +644,50 @@ nextjs-portal {
|
|
|
581
644
|
|
|
582
645
|
.velu-footer {
|
|
583
646
|
margin-top: 3rem;
|
|
584
|
-
padding-top: 1.
|
|
647
|
+
padding-top: 1.1rem;
|
|
585
648
|
border-top: 1px solid var(--color-fd-border);
|
|
586
|
-
|
|
587
|
-
|
|
649
|
+
display: flex;
|
|
650
|
+
align-items: center;
|
|
651
|
+
justify-content: space-between;
|
|
652
|
+
gap: 1rem;
|
|
653
|
+
flex-wrap: wrap;
|
|
654
|
+
font-size: 1rem;
|
|
588
655
|
color: var(--color-fd-muted-foreground);
|
|
589
656
|
}
|
|
590
657
|
|
|
658
|
+
.velu-footer-socials {
|
|
659
|
+
display: inline-flex;
|
|
660
|
+
align-items: center;
|
|
661
|
+
gap: 1rem;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
.velu-footer-social-link {
|
|
665
|
+
display: inline-flex;
|
|
666
|
+
align-items: center;
|
|
667
|
+
justify-content: center;
|
|
668
|
+
color: var(--color-fd-muted-foreground);
|
|
669
|
+
opacity: 0.78;
|
|
670
|
+
text-decoration: none;
|
|
671
|
+
transition: color 0.15s, opacity 0.15s;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
.velu-footer-social-link:hover {
|
|
675
|
+
color: var(--color-fd-foreground);
|
|
676
|
+
opacity: 1;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
.velu-footer-social-icon {
|
|
680
|
+
width: 1.05rem;
|
|
681
|
+
height: 1.05rem;
|
|
682
|
+
flex-shrink: 0;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
.velu-footer-powered {
|
|
686
|
+
margin-left: auto;
|
|
687
|
+
font-size: 1.15rem;
|
|
688
|
+
line-height: 1.3;
|
|
689
|
+
}
|
|
690
|
+
|
|
591
691
|
.velu-page-feedback-wrap {
|
|
592
692
|
margin-top: 2.75rem;
|
|
593
693
|
display: grid;
|
|
@@ -867,16 +967,22 @@ nextjs-portal {
|
|
|
867
967
|
}
|
|
868
968
|
}
|
|
869
969
|
|
|
870
|
-
.velu-footer a {
|
|
970
|
+
.velu-footer-powered a {
|
|
871
971
|
color: var(--color-fd-primary);
|
|
872
972
|
font-weight: 600;
|
|
873
973
|
text-decoration: none;
|
|
874
974
|
}
|
|
875
975
|
|
|
876
|
-
.velu-footer a:hover {
|
|
976
|
+
.velu-footer-powered a:hover {
|
|
877
977
|
text-decoration: underline;
|
|
878
978
|
}
|
|
879
979
|
|
|
980
|
+
@media (max-width: 768px) {
|
|
981
|
+
.velu-footer-powered {
|
|
982
|
+
margin-left: 0;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
880
986
|
[data-card].velu-card-horizontal {
|
|
881
987
|
display: grid !important;
|
|
882
988
|
grid-template-columns: 2.25rem minmax(0, 1fr);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { Metadata } from 'next';
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
|
-
import { getAppearance, getSeoConfig, getSiteFavicon, getSiteName, getSiteOrigin, getSitePrimaryColor } from '@/lib/velu';
|
|
3
|
+
import { getAppearance, getBannerConfig, getSeoConfig, getSiteDescription, getSiteFavicon, getSiteName, getSiteOrigin, getSitePrimaryColor } from '@/lib/velu';
|
|
4
4
|
import { Providers } from '@/components/providers';
|
|
5
5
|
import { VeluAssistant } from '@/components/assistant';
|
|
6
|
+
import { VeluBanner } from '@/components/banner';
|
|
6
7
|
import './global.css';
|
|
7
8
|
import './search.css';
|
|
8
9
|
import './assistant.css';
|
|
@@ -17,10 +18,12 @@ function toAbsoluteUrl(origin: string, value: string): string {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
const siteName = getSiteName();
|
|
21
|
+
const siteDescription = getSiteDescription();
|
|
20
22
|
const siteOrigin = getSiteOrigin();
|
|
21
23
|
const seo = getSeoConfig();
|
|
22
24
|
const favicon = getSiteFavicon();
|
|
23
25
|
const primaryColor = getSitePrimaryColor();
|
|
26
|
+
const bannerConfig = getBannerConfig();
|
|
24
27
|
const generatedDefaultSocialImage = '/og/index.svg';
|
|
25
28
|
const defaultSocialImage = seo.metatags['og:image'] ?? seo.metatags['twitter:image'] ?? generatedDefaultSocialImage;
|
|
26
29
|
const absoluteDefaultSocialImage = defaultSocialImage ? toAbsoluteUrl(siteOrigin, defaultSocialImage) : undefined;
|
|
@@ -31,6 +34,7 @@ export const metadata: Metadata = {
|
|
|
31
34
|
default: siteName,
|
|
32
35
|
template: `%s - ${siteName}`,
|
|
33
36
|
},
|
|
37
|
+
...(siteDescription ? { description: siteDescription } : {}),
|
|
34
38
|
applicationName: siteName,
|
|
35
39
|
generator: seo.metatags.generator || 'Mintlify',
|
|
36
40
|
appleWebApp: {
|
|
@@ -39,12 +43,14 @@ export const metadata: Metadata = {
|
|
|
39
43
|
openGraph: {
|
|
40
44
|
type: 'website',
|
|
41
45
|
siteName,
|
|
46
|
+
...(siteDescription ? { description: siteDescription } : {}),
|
|
42
47
|
...(absoluteDefaultSocialImage
|
|
43
48
|
? { images: [{ url: absoluteDefaultSocialImage, width: 1200, height: 630 }] }
|
|
44
49
|
: {}),
|
|
45
50
|
},
|
|
46
51
|
twitter: {
|
|
47
52
|
card: 'summary_large_image',
|
|
53
|
+
...(siteDescription ? { description: siteDescription } : {}),
|
|
48
54
|
...(absoluteDefaultSocialImage ? { images: [absoluteDefaultSocialImage] } : {}),
|
|
49
55
|
},
|
|
50
56
|
...(favicon
|
|
@@ -76,6 +82,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
|
|
|
76
82
|
</head>
|
|
77
83
|
<body className="min-h-screen" suppressHydrationWarning>
|
|
78
84
|
<Providers theme={theme}>
|
|
85
|
+
{bannerConfig && <VeluBanner content={bannerConfig.content} dismissible={bannerConfig.dismissible} />}
|
|
79
86
|
{children}
|
|
80
87
|
<VeluAssistant />
|
|
81
88
|
</Providers>
|
package/src/engine/app/page.tsx
CHANGED
|
@@ -37,7 +37,10 @@ function resolveDefaultDocsHref(): string {
|
|
|
37
37
|
const defaultLanguage = getLanguages()[0] ?? 'en';
|
|
38
38
|
const tree = source.getPageTree(defaultLanguage);
|
|
39
39
|
const first = findFirstPageUrl(tree);
|
|
40
|
-
|
|
40
|
+
if (!first || first === '/') return '/';
|
|
41
|
+
if (!first.startsWith('/')) return first;
|
|
42
|
+
if (first.endsWith('/')) return first;
|
|
43
|
+
return `${first}/`;
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
export default function HomePage() {
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useMemo, useRef, useCallback } from 'react';
|
|
4
|
+
|
|
5
|
+
function hashContent(content: string): string {
|
|
6
|
+
let hash = 0;
|
|
7
|
+
for (let i = 0; i < content.length; i++) {
|
|
8
|
+
hash = ((hash << 5) - hash + content.charCodeAt(i)) | 0;
|
|
9
|
+
}
|
|
10
|
+
return `velu-banner-${hash}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parseMarkdownLinks(text: string): string {
|
|
14
|
+
return text.replace(
|
|
15
|
+
/\[([^\]]+)\]\(([^)]+)\)/g,
|
|
16
|
+
'<a href="$2">$1</a>',
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface VeluBannerProps {
|
|
21
|
+
content: string;
|
|
22
|
+
dismissible: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function setSizeVar(el: HTMLElement | null) {
|
|
26
|
+
const h = el ? el.offsetHeight : 0;
|
|
27
|
+
document.documentElement.style.setProperty('--velu-announcement-h', `${h}px`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function VeluBanner({ content, dismissible }: VeluBannerProps) {
|
|
31
|
+
const storageKey = useMemo(() => hashContent(content), [content]);
|
|
32
|
+
const [dismissed, setDismissed] = useState(true);
|
|
33
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (dismissible) {
|
|
37
|
+
const stored = localStorage.getItem(storageKey);
|
|
38
|
+
setDismissed(stored === '1');
|
|
39
|
+
} else {
|
|
40
|
+
setDismissed(false);
|
|
41
|
+
}
|
|
42
|
+
}, [dismissible, storageKey]);
|
|
43
|
+
|
|
44
|
+
const measuredRef = useCallback((node: HTMLDivElement | null) => {
|
|
45
|
+
(ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
|
46
|
+
setSizeVar(node);
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (dismissed) {
|
|
51
|
+
setSizeVar(null);
|
|
52
|
+
}
|
|
53
|
+
}, [dismissed]);
|
|
54
|
+
|
|
55
|
+
if (dismissed) return null;
|
|
56
|
+
|
|
57
|
+
const html = parseMarkdownLinks(content);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div className="velu-announcement" role="banner" ref={measuredRef}>
|
|
61
|
+
<span
|
|
62
|
+
className="velu-announcement-content"
|
|
63
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
64
|
+
/>
|
|
65
|
+
{dismissible && (
|
|
66
|
+
<button
|
|
67
|
+
type="button"
|
|
68
|
+
className="velu-announcement-dismiss"
|
|
69
|
+
aria-label="Dismiss banner"
|
|
70
|
+
onClick={() => {
|
|
71
|
+
localStorage.setItem(storageKey, '1');
|
|
72
|
+
setDismissed(true);
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
✕
|
|
76
|
+
</button>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -1,8 +1,59 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useRef, useEffect } from 'react';
|
|
4
|
+
import type { VeluContextualOption } from '@/lib/velu';
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
interface CopyPageButtonProps {
|
|
7
|
+
options: VeluContextualOption[];
|
|
8
|
+
mcpUrl: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const ICON_MAP: Record<string, React.ReactNode> = {
|
|
12
|
+
copy: (
|
|
13
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
14
|
+
),
|
|
15
|
+
view: (
|
|
16
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
|
|
17
|
+
),
|
|
18
|
+
chatgpt: (
|
|
19
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073zM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494zM3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085 4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646zM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.676l5.815 3.355-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.855l-5.833-3.387L15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667zm2.01-3.023l-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135l-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08L8.704 5.46a.795.795 0 0 0-.393.681zm1.097-2.365l2.602-1.5 2.607 1.5v2.999l-2.597 1.5-2.607-1.5z"/></svg>
|
|
20
|
+
),
|
|
21
|
+
claude: (
|
|
22
|
+
<svg width="18" height="18" viewBox="0 0 200 200" style={{ overflow: 'visible' }} fill="currentColor"><path d="m50.228 170.321 50.357-28.257.843-2.463-.843-1.361h-2.462l-8.426-.518-28.775-.778-24.952-1.037-24.175-1.296-6.092-1.297L0 125.796l.583-3.759 5.12-3.434 7.324.648 16.202 1.101 24.304 1.685 17.629 1.037 26.118 2.722h4.148l.583-1.685-1.426-1.037-1.101-1.037-25.147-17.045-27.22-18.017-14.258-10.37-7.713-5.25-3.888-4.925-1.685-10.758 7-7.713 9.397.649 2.398.648 9.527 7.323 20.35 15.75L94.817 91.9l3.889 3.24 1.555-1.102.195-.777-1.75-2.917-14.453-26.118-15.425-26.572-6.87-11.018-1.814-6.61c-.648-2.723-1.102-4.991-1.102-7.778l7.972-10.823L71.42 0 82.05 1.426l4.472 3.888 6.61 15.101 10.694 23.786 16.591 32.34 4.861 9.592 2.592 8.879.973 2.722h1.685v-1.556l1.36-18.211 2.528-22.36 2.463-28.776.843-8.1 4.018-9.722 7.971-5.25 6.222 2.981 5.12 7.324-.713 4.73-3.046 19.768-5.962 30.98-3.889 20.739h2.268l2.593-2.593 10.499-13.934 17.628-22.036 7.778-8.749 9.073-9.657 5.833-4.601h11.018l8.1 12.055-3.628 12.443-11.342 14.388-9.398 12.184-13.48 18.147-8.426 14.518.778 1.166 2.01-.194 30.46-6.481 16.462-2.982 19.637-3.37 8.88 4.148.971 4.213-3.5 8.62-20.998 5.184-24.628 4.926-36.682 8.685-.454.324.519.648 16.526 1.555 7.065.389h17.304l32.21 2.398 8.426 5.574 5.055 6.805-.843 5.184-12.962 6.611-17.498-4.148-40.83-9.721-14-3.5h-1.944v1.167l11.666 11.406 21.387 19.314 26.767 24.887 1.36 6.157-3.434 4.86-3.63-.518-23.526-17.693-9.073-7.972-20.545-17.304h-1.36v1.814l4.73 6.935 25.017 37.59 1.296 11.536-1.814 3.76-6.481 2.268-7.13-1.297-14.647-20.544-15.1-23.138-12.185-20.739-1.49.843-7.194 77.448-3.37 3.953-7.778 2.981-6.48-4.925-3.436-7.972 3.435-15.749 4.148-20.544 3.37-16.333 3.046-20.285 1.815-6.74-.13-.454-1.49.194-15.295 20.999-23.267 31.433-18.406 19.702-4.407 1.75-7.648-3.954.713-7.064 4.277-6.286 25.47-32.405 15.36-20.092 9.917-11.6-.065-1.686h-.583L44.07 198.125l-12.055 1.555-5.185-4.86.648-7.972 2.463-2.593 20.35-13.999-.064.065Z"/></svg>
|
|
23
|
+
),
|
|
24
|
+
perplexity: (
|
|
25
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M22.3977 7.0896h-2.3106V.0676l-7.5094 6.3542V.1577h-1.1554v6.1966L4.4904 0v7.0896H1.6023v10.3976h2.8882V24l6.932-6.3591v6.2005h1.1554v-6.0469l6.9318 6.1807v-6.4879h2.8882V7.0896zm-3.4657-4.531v4.531h-5.355l5.355-4.531zm-13.2862.0676 4.8691 4.4634H5.6458V2.6262zM2.7576 16.332V8.245h7.8476l-6.1149 6.1147v1.9723H2.7576zm2.8882 5.0404v-3.8852h.0001v-2.6488l5.7763-5.7764v7.0111l-5.7764 5.2993zm12.7086.0248-5.7766-5.1509V9.0618l5.7766 5.7766v6.5588zm2.8882-5.0652h-1.733v-1.9723L13.3948 8.245h7.8478v8.087z"/></svg>
|
|
26
|
+
),
|
|
27
|
+
grok: (
|
|
28
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M3 3l8.5 12.5L3 21h1.9l7.1-4.6L18.1 21H21l-8.5-12.5L21 3h-1.9l-7.1 4.6L5.9 3H3z"/></svg>
|
|
29
|
+
),
|
|
30
|
+
mcp: (
|
|
31
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
|
|
32
|
+
),
|
|
33
|
+
'add-mcp': (
|
|
34
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
|
|
35
|
+
),
|
|
36
|
+
cursor: (
|
|
37
|
+
<svg width="18" height="18" viewBox="0 0 466.73 532.09" fill="currentColor"><path d="M457.43,125.94L244.42,2.96c-6.84-3.95-15.28-3.95-22.12,0L9.3,125.94c-5.75,3.32-9.3,9.46-9.3,16.11v247.99c0,6.65,3.55,12.79,9.3,16.11l213.01,122.98c6.84,3.95,15.28,3.95,22.12,0l213.01-122.98c5.75-3.32,9.3-9.46,9.3-16.11v-247.99c0-6.65-3.55-12.79-9.3-16.11h-.01ZM444.05,151.99l-205.63,356.16c-1.39,2.4-5.06,1.42-5.06-1.36v-233.21c0-4.66-2.49-8.97-6.53-11.31L24.87,145.67c-2.4-1.39-1.42-5.06,1.36-5.06h411.26c5.84,0,9.49,6.33,6.57,11.39h-.01Z"/></svg>
|
|
38
|
+
),
|
|
39
|
+
vscode: (
|
|
40
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M17.583 2.286L9.23 9.98 4.553 6.196 2 7.403v9.193l2.553 1.208 4.677-3.784L17.583 21.714 22 19.6V4.4l-4.417-2.114zM4.553 13.7V10.3L6.8 12l-2.247 1.7zM17.583 17.4l-6.06-5.4 6.06-5.4v10.8z"/></svg>
|
|
41
|
+
),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function getDefaultIcon() {
|
|
45
|
+
return (
|
|
46
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const EXTERNAL_IDS = new Set(['chatgpt', 'claude', 'perplexity', 'grok', 'cursor', 'vscode']);
|
|
51
|
+
|
|
52
|
+
function buildPromptUrl(baseUrl: string, paramName: string, pageUrl: string): string {
|
|
53
|
+
return `${baseUrl}${paramName}=Read+from+${encodeURIComponent(pageUrl)}+so+I+can+ask+questions+about+it.`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function CopyPageButton({ options, mcpUrl }: CopyPageButtonProps) {
|
|
6
57
|
const [label, setLabel] = useState('Copy page');
|
|
7
58
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
|
8
59
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
@@ -17,13 +68,20 @@ export function CopyPageButton() {
|
|
|
17
68
|
return () => document.removeEventListener('click', handleClick);
|
|
18
69
|
}, []);
|
|
19
70
|
|
|
20
|
-
|
|
21
|
-
|
|
71
|
+
if (options.length === 0) return null;
|
|
72
|
+
|
|
73
|
+
function getPageText(): string {
|
|
22
74
|
const titleEl = document.querySelector('h1');
|
|
23
75
|
const article = document.querySelector('[data-pagefind-body]') || document.querySelector('main');
|
|
24
76
|
let text = '';
|
|
25
77
|
if (titleEl) text = '# ' + titleEl.textContent + '\n\n';
|
|
26
78
|
if (article) text += (article as HTMLElement).innerText;
|
|
79
|
+
return text;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function doCopy() {
|
|
83
|
+
setLabel('Copying...');
|
|
84
|
+
const text = getPageText();
|
|
27
85
|
if (text) {
|
|
28
86
|
navigator.clipboard.writeText(text).then(() => {
|
|
29
87
|
setLabel('Copied!');
|
|
@@ -33,7 +91,106 @@ export function CopyPageButton() {
|
|
|
33
91
|
setDropdownOpen(false);
|
|
34
92
|
}
|
|
35
93
|
|
|
36
|
-
|
|
94
|
+
function flashLabel(msg: string) {
|
|
95
|
+
setLabel(msg);
|
|
96
|
+
setTimeout(() => setLabel('Copy page'), 1500);
|
|
97
|
+
setDropdownOpen(false);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function handleBuiltinAction(id: string) {
|
|
101
|
+
const currentUrl = window.location.href;
|
|
102
|
+
const currentPath = window.location.pathname;
|
|
103
|
+
|
|
104
|
+
switch (id) {
|
|
105
|
+
case 'copy':
|
|
106
|
+
doCopy();
|
|
107
|
+
return;
|
|
108
|
+
case 'view': {
|
|
109
|
+
const text = getPageText();
|
|
110
|
+
if (text) {
|
|
111
|
+
const blob = new Blob([text], { type: 'text/plain' });
|
|
112
|
+
window.open(URL.createObjectURL(blob), '_blank');
|
|
113
|
+
}
|
|
114
|
+
setDropdownOpen(false);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
case 'chatgpt':
|
|
118
|
+
window.open(buildPromptUrl('https://chatgpt.com/?', 'prompt', currentUrl), '_blank');
|
|
119
|
+
setDropdownOpen(false);
|
|
120
|
+
return;
|
|
121
|
+
case 'claude':
|
|
122
|
+
window.open(buildPromptUrl('https://claude.ai/new?', 'q', currentUrl), '_blank');
|
|
123
|
+
setDropdownOpen(false);
|
|
124
|
+
return;
|
|
125
|
+
case 'perplexity':
|
|
126
|
+
window.open(buildPromptUrl('https://www.perplexity.ai/?', 'q', currentUrl), '_blank');
|
|
127
|
+
setDropdownOpen(false);
|
|
128
|
+
return;
|
|
129
|
+
case 'grok':
|
|
130
|
+
window.open(buildPromptUrl('https://grok.com/?', 'q', currentUrl), '_blank');
|
|
131
|
+
setDropdownOpen(false);
|
|
132
|
+
return;
|
|
133
|
+
case 'mcp':
|
|
134
|
+
navigator.clipboard.writeText(mcpUrl).then(() => flashLabel('Copied MCP URL!'));
|
|
135
|
+
return;
|
|
136
|
+
case 'add-mcp':
|
|
137
|
+
navigator.clipboard.writeText(`npx @anthropic-ai/claude-code --mcp-server-uri=${mcpUrl}`).then(() => flashLabel('Copied!'));
|
|
138
|
+
return;
|
|
139
|
+
case 'cursor':
|
|
140
|
+
window.open(`cursor://anysphere.cursor.mcp/install?url=${encodeURIComponent(mcpUrl)}`, '_self');
|
|
141
|
+
setDropdownOpen(false);
|
|
142
|
+
return;
|
|
143
|
+
case 'vscode':
|
|
144
|
+
window.open(`vscode://anthropic.claude-code/mcp/install?url=${encodeURIComponent(mcpUrl)}`, '_self');
|
|
145
|
+
setDropdownOpen(false);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function resolveCustomHref(href: string): string {
|
|
151
|
+
const currentUrl = typeof window !== 'undefined' ? window.location.href : '';
|
|
152
|
+
const currentPath = typeof window !== 'undefined' ? window.location.pathname : '';
|
|
153
|
+
return href.replace(/\$page/g, encodeURIComponent(currentUrl)).replace(/\$path/g, encodeURIComponent(currentPath));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function renderOption(opt: typeof options[number]) {
|
|
157
|
+
const icon = ICON_MAP[opt.id] ?? getDefaultIcon();
|
|
158
|
+
const isExternal = EXTERNAL_IDS.has(opt.id) || opt.type === 'custom';
|
|
159
|
+
const arrow = isExternal ? <span className="velu-external-arrow">↗</span> : null;
|
|
160
|
+
|
|
161
|
+
if (opt.type === 'custom' && opt.href) {
|
|
162
|
+
return (
|
|
163
|
+
<a
|
|
164
|
+
key={opt.id}
|
|
165
|
+
className="velu-copy-option"
|
|
166
|
+
href={resolveCustomHref(opt.href)}
|
|
167
|
+
target="_blank"
|
|
168
|
+
rel="noopener noreferrer"
|
|
169
|
+
onClick={() => setDropdownOpen(false)}
|
|
170
|
+
>
|
|
171
|
+
<span className="velu-copy-option-icon">{icon}</span>
|
|
172
|
+
<div>
|
|
173
|
+
<div className="velu-copy-option-title">{opt.title} {arrow}</div>
|
|
174
|
+
{opt.description ? <div className="velu-copy-option-desc">{opt.description}</div> : null}
|
|
175
|
+
</div>
|
|
176
|
+
</a>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<button
|
|
182
|
+
key={opt.id}
|
|
183
|
+
className="velu-copy-option"
|
|
184
|
+
onClick={() => handleBuiltinAction(opt.id)}
|
|
185
|
+
>
|
|
186
|
+
<span className="velu-copy-option-icon">{icon}</span>
|
|
187
|
+
<div>
|
|
188
|
+
<div className="velu-copy-option-title">{opt.title} {arrow}</div>
|
|
189
|
+
{opt.description ? <div className="velu-copy-option-desc">{opt.description}</div> : null}
|
|
190
|
+
</div>
|
|
191
|
+
</button>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
37
194
|
|
|
38
195
|
return (
|
|
39
196
|
<div className="velu-copy-page-container" ref={containerRef}>
|
|
@@ -58,37 +215,7 @@ export function CopyPageButton() {
|
|
|
58
215
|
|
|
59
216
|
{dropdownOpen && (
|
|
60
217
|
<div className="velu-copy-dropdown" onClick={(e) => e.stopPropagation()}>
|
|
61
|
-
|
|
62
|
-
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
63
|
-
<div>
|
|
64
|
-
<div className="velu-copy-option-title">Copy page</div>
|
|
65
|
-
<div className="velu-copy-option-desc">Copy page as Markdown for LLMs</div>
|
|
66
|
-
</div>
|
|
67
|
-
</button>
|
|
68
|
-
<a
|
|
69
|
-
className="velu-copy-option"
|
|
70
|
-
href={`https://chatgpt.com/?prompt=Read+from+${encodeURIComponent(currentUrl)}+so+I+can+ask+questions+about+it.`}
|
|
71
|
-
target="_blank"
|
|
72
|
-
rel="noopener noreferrer"
|
|
73
|
-
>
|
|
74
|
-
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073zM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494zM3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085 4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646zM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.676l5.815 3.355-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.855l-5.833-3.387L15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667zm2.01-3.023l-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135l-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08L8.704 5.46a.795.795 0 0 0-.393.681zm1.097-2.365l2.602-1.5 2.607 1.5v2.999l-2.597 1.5-2.607-1.5z"/></svg>
|
|
75
|
-
<div>
|
|
76
|
-
<div className="velu-copy-option-title">Open in ChatGPT <span className="velu-external-arrow">↗</span></div>
|
|
77
|
-
<div className="velu-copy-option-desc">Ask questions about this page</div>
|
|
78
|
-
</div>
|
|
79
|
-
</a>
|
|
80
|
-
<a
|
|
81
|
-
className="velu-copy-option"
|
|
82
|
-
href={`https://claude.ai/new?q=Read+from+${encodeURIComponent(currentUrl)}+so+I+can+ask+questions+about+it.`}
|
|
83
|
-
target="_blank"
|
|
84
|
-
rel="noopener noreferrer"
|
|
85
|
-
>
|
|
86
|
-
<svg width="18" height="18" viewBox="0 0 200 200" style={{ overflow: 'visible' }} fill="currentColor"><path d="m50.228 170.321 50.357-28.257.843-2.463-.843-1.361h-2.462l-8.426-.518-28.775-.778-24.952-1.037-24.175-1.296-6.092-1.297L0 125.796l.583-3.759 5.12-3.434 7.324.648 16.202 1.101 24.304 1.685 17.629 1.037 26.118 2.722h4.148l.583-1.685-1.426-1.037-1.101-1.037-25.147-17.045-27.22-18.017-14.258-10.37-7.713-5.25-3.888-4.925-1.685-10.758 7-7.713 9.397.649 2.398.648 9.527 7.323 20.35 15.75L94.817 91.9l3.889 3.24 1.555-1.102.195-.777-1.75-2.917-14.453-26.118-15.425-26.572-6.87-11.018-1.814-6.61c-.648-2.723-1.102-4.991-1.102-7.778l7.972-10.823L71.42 0 82.05 1.426l4.472 3.888 6.61 15.101 10.694 23.786 16.591 32.34 4.861 9.592 2.592 8.879.973 2.722h1.685v-1.556l1.36-18.211 2.528-22.36 2.463-28.776.843-8.1 4.018-9.722 7.971-5.25 6.222 2.981 5.12 7.324-.713 4.73-3.046 19.768-5.962 30.98-3.889 20.739h2.268l2.593-2.593 10.499-13.934 17.628-22.036 7.778-8.749 9.073-9.657 5.833-4.601h11.018l8.1 12.055-3.628 12.443-11.342 14.388-9.398 12.184-13.48 18.147-8.426 14.518.778 1.166 2.01-.194 30.46-6.481 16.462-2.982 19.637-3.37 8.88 4.148.971 4.213-3.5 8.62-20.998 5.184-24.628 4.926-36.682 8.685-.454.324.519.648 16.526 1.555 7.065.389h17.304l32.21 2.398 8.426 5.574 5.055 6.805-.843 5.184-12.962 6.611-17.498-4.148-40.83-9.721-14-3.5h-1.944v1.167l11.666 11.406 21.387 19.314 26.767 24.887 1.36 6.157-3.434 4.86-3.63-.518-23.526-17.693-9.073-7.972-20.545-17.304h-1.36v1.814l4.73 6.935 25.017 37.59 1.296 11.536-1.814 3.76-6.481 2.268-7.13-1.297-14.647-20.544-15.1-23.138-12.185-20.739-1.49.843-7.194 77.448-3.37 3.953-7.778 2.981-6.48-4.925-3.436-7.972 3.435-15.749 4.148-20.544 3.37-16.333 3.046-20.285 1.815-6.74-.13-.454-1.49.194-15.295 20.999-23.267 31.433-18.406 19.702-4.407 1.75-7.648-3.954.713-7.064 4.277-6.286 25.47-32.405 15.36-20.092 9.917-11.6-.065-1.686h-.583L44.07 198.125l-12.055 1.555-5.185-4.86.648-7.972 2.463-2.593 20.35-13.999-.064.065Z"/></svg>
|
|
87
|
-
<div>
|
|
88
|
-
<div className="velu-copy-option-title">Open in Claude <span className="velu-external-arrow">↗</span></div>
|
|
89
|
-
<div className="velu-copy-option-desc">Ask questions about this page</div>
|
|
90
|
-
</div>
|
|
91
|
-
</a>
|
|
218
|
+
{options.map(renderOption)}
|
|
92
219
|
</div>
|
|
93
220
|
)}
|
|
94
221
|
</div>
|