@opendocsdev/cli 0.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 +661 -0
- package/README.md +300 -0
- package/dist/bin/opendocs.js +712 -0
- package/dist/bin/opendocs.js.map +1 -0
- package/dist/templates/api-reference.mdx +308 -0
- package/dist/templates/components.mdx +286 -0
- package/dist/templates/configuration.mdx +190 -0
- package/dist/templates/docs.json +27 -0
- package/dist/templates/introduction.mdx +25 -0
- package/dist/templates/logo.svg +4 -0
- package/dist/templates/quickstart.mdx +59 -0
- package/dist/templates/writing-content.mdx +236 -0
- package/package.json +92 -0
- package/src/engine/astro.config.ts +75 -0
- package/src/engine/src/components/Analytics.astro +57 -0
- package/src/engine/src/components/ApiPlayground.astro +24 -0
- package/src/engine/src/components/Callout.astro +66 -0
- package/src/engine/src/components/Card.astro +75 -0
- package/src/engine/src/components/CardGroup.astro +29 -0
- package/src/engine/src/components/CodeGroup.astro +231 -0
- package/src/engine/src/components/CopyButton.astro +179 -0
- package/src/engine/src/components/Steps.astro +27 -0
- package/src/engine/src/components/Tab.astro +21 -0
- package/src/engine/src/components/TableOfContents.astro +119 -0
- package/src/engine/src/components/Tabs.astro +135 -0
- package/src/engine/src/components/index.ts +107 -0
- package/src/engine/src/components/react/ApiPlayground/AuthSection.tsx +91 -0
- package/src/engine/src/components/react/ApiPlayground/CodeBlock.tsx +66 -0
- package/src/engine/src/components/react/ApiPlayground/CodeSnippets.tsx +66 -0
- package/src/engine/src/components/react/ApiPlayground/CollapsibleSection.tsx +26 -0
- package/src/engine/src/components/react/ApiPlayground/KeyValueEditor.tsx +58 -0
- package/src/engine/src/components/react/ApiPlayground/ResponseDisplay.tsx +109 -0
- package/src/engine/src/components/react/ApiPlayground/Spinner.tsx +6 -0
- package/src/engine/src/components/react/ApiPlayground/constants.ts +16 -0
- package/src/engine/src/components/react/ApiPlayground/generators.test.ts +130 -0
- package/src/engine/src/components/react/ApiPlayground/generators.ts +75 -0
- package/src/engine/src/components/react/ApiPlayground/index.tsx +490 -0
- package/src/engine/src/components/react/ApiPlayground/types.ts +35 -0
- package/src/engine/src/components/react/Callout.tsx +54 -0
- package/src/engine/src/components/react/Card.tsx +48 -0
- package/src/engine/src/components/react/CardGroup.tsx +24 -0
- package/src/engine/src/components/react/FeedbackWidget.tsx +166 -0
- package/src/engine/src/components/react/GitHubLink.tsx +28 -0
- package/src/engine/src/components/react/NavigationCard.tsx +53 -0
- package/src/engine/src/components/react/PageActions.tsx +124 -0
- package/src/engine/src/components/react/PageFooter.tsx +91 -0
- package/src/engine/src/components/react/SearchModal.tsx +358 -0
- package/src/engine/src/components/react/SearchProvider.tsx +37 -0
- package/src/engine/src/components/react/Sidebar.tsx +369 -0
- package/src/engine/src/components/react/SidebarSearchTrigger.tsx +57 -0
- package/src/engine/src/components/react/Steps.tsx +25 -0
- package/src/engine/src/components/react/ThemeToggle.tsx +72 -0
- package/src/engine/src/components/react/index.ts +14 -0
- package/src/engine/src/env.d.ts +10 -0
- package/src/engine/src/layouts/DocsLayout.astro +357 -0
- package/src/engine/src/lib/__tests__/markdown.test.ts +124 -0
- package/src/engine/src/lib/__tests__/mdx-loader.test.ts +205 -0
- package/src/engine/src/lib/config.ts +79 -0
- package/src/engine/src/lib/markdown.ts +54 -0
- package/src/engine/src/lib/mdx-loader.ts +143 -0
- package/src/engine/src/lib/mdx-utils.ts +72 -0
- package/src/engine/src/lib/remark-opendocs.ts +195 -0
- package/src/engine/src/lib/utils.ts +221 -0
- package/src/engine/src/pages/[...slug].astro +115 -0
- package/src/engine/src/pages/index.astro +71 -0
- package/src/engine/src/scripts/mobile-sidebar.ts +56 -0
- package/src/engine/src/scripts/theme-init.ts +25 -0
- package/src/engine/src/styles/global.css +703 -0
- package/src/engine/tailwind.config.mjs +60 -0
- package/src/engine/tsconfig.json +15 -0
- package/src/templates/api-reference.mdx +308 -0
- package/src/templates/components.mdx +286 -0
- package/src/templates/configuration.mdx +190 -0
- package/src/templates/docs.json +27 -0
- package/src/templates/introduction.mdx +25 -0
- package/src/templates/logo.svg +4 -0
- package/src/templates/quickstart.mdx +59 -0
- package/src/templates/writing-content.mdx +236 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface ApiPlaygroundProps {
|
|
2
|
+
endpoint: string;
|
|
3
|
+
method: string;
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
defaultHeaders?: Record<string, string>;
|
|
6
|
+
defaultBody?: Record<string, unknown> | string;
|
|
7
|
+
defaultParams?: Record<string, string>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface AuthState {
|
|
11
|
+
type: "none" | "bearer" | "apikey" | "basic";
|
|
12
|
+
bearer: { token: string };
|
|
13
|
+
apikey: { name: string; value: string; location: "header" | "query" };
|
|
14
|
+
basic: { username: string; password: string };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface KeyValue {
|
|
18
|
+
key: string;
|
|
19
|
+
value: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ResponseState {
|
|
23
|
+
status: number;
|
|
24
|
+
statusText: string;
|
|
25
|
+
body: string;
|
|
26
|
+
headers: Record<string, string>;
|
|
27
|
+
time: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ErrorState {
|
|
31
|
+
title: string;
|
|
32
|
+
message: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type CodeLanguage = "curl" | "javascript" | "python";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { cn } from "../../lib/utils";
|
|
2
|
+
|
|
3
|
+
interface CalloutProps {
|
|
4
|
+
type?: "info" | "warning" | "error" | "tip";
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const typeConfig = {
|
|
9
|
+
info: {
|
|
10
|
+
bg: "bg-[var(--color-info-light)]",
|
|
11
|
+
border: "border-[var(--color-info)]",
|
|
12
|
+
icon: "ℹ️",
|
|
13
|
+
},
|
|
14
|
+
warning: {
|
|
15
|
+
bg: "bg-[var(--color-warning-light)]",
|
|
16
|
+
border: "border-[var(--color-warning)]",
|
|
17
|
+
icon: "⚠️",
|
|
18
|
+
},
|
|
19
|
+
error: {
|
|
20
|
+
bg: "bg-[var(--color-error-light)]",
|
|
21
|
+
border: "border-[var(--color-error)]",
|
|
22
|
+
icon: "🚫",
|
|
23
|
+
},
|
|
24
|
+
tip: {
|
|
25
|
+
bg: "bg-[var(--color-success-light)]",
|
|
26
|
+
border: "border-[var(--color-success)]",
|
|
27
|
+
icon: "💡",
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function Callout({ type = "info", children }: CalloutProps) {
|
|
32
|
+
const config = typeConfig[type];
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div
|
|
36
|
+
className={cn(
|
|
37
|
+
"callout my-4 p-4 rounded-lg border-l-4",
|
|
38
|
+
config.bg,
|
|
39
|
+
config.border
|
|
40
|
+
)}
|
|
41
|
+
>
|
|
42
|
+
<div className="flex gap-3">
|
|
43
|
+
<span className="flex-shrink-0 text-lg" aria-hidden="true">
|
|
44
|
+
{config.icon}
|
|
45
|
+
</span>
|
|
46
|
+
<div className="prose prose-sm dark:prose-invert max-w-none [&>p]:my-0 [&>p:first-child]:mt-0 [&>p:last-child]:mb-0">
|
|
47
|
+
{children}
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default Callout;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { cn } from "../../lib/utils";
|
|
2
|
+
|
|
3
|
+
interface CardProps {
|
|
4
|
+
title: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
href?: string;
|
|
7
|
+
icon?: string;
|
|
8
|
+
children?: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Card({ title, description, href, icon, children }: CardProps) {
|
|
12
|
+
const content = (
|
|
13
|
+
<>
|
|
14
|
+
{icon && (
|
|
15
|
+
<span className="text-2xl mb-2 block" aria-hidden="true">
|
|
16
|
+
{icon}
|
|
17
|
+
</span>
|
|
18
|
+
)}
|
|
19
|
+
<h3 className="font-semibold text-[var(--color-foreground)] m-0">
|
|
20
|
+
{title}
|
|
21
|
+
</h3>
|
|
22
|
+
{description && (
|
|
23
|
+
<p className="mt-1 text-sm text-[var(--color-muted)] m-0">
|
|
24
|
+
{description}
|
|
25
|
+
</p>
|
|
26
|
+
)}
|
|
27
|
+
{children && <div className="mt-2">{children}</div>}
|
|
28
|
+
</>
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const className = cn(
|
|
32
|
+
"block p-4 rounded-lg border border-[var(--color-border)]",
|
|
33
|
+
"bg-[var(--color-surface)] transition-colors",
|
|
34
|
+
href && "hover:border-[var(--color-primary)] hover:bg-[var(--color-surface-raised)]"
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (href) {
|
|
38
|
+
return (
|
|
39
|
+
<a href={href} className={className}>
|
|
40
|
+
{content}
|
|
41
|
+
</a>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return <div className={className}>{content}</div>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default Card;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { cn } from "../../lib/utils";
|
|
2
|
+
|
|
3
|
+
interface CardGroupProps {
|
|
4
|
+
cols?: 1 | 2 | 3 | 4;
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function CardGroup({ cols = 2, children }: CardGroupProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
className={cn(
|
|
12
|
+
"card-group grid gap-4 my-6",
|
|
13
|
+
cols === 1 && "grid-cols-1",
|
|
14
|
+
cols === 2 && "grid-cols-1 sm:grid-cols-2",
|
|
15
|
+
cols === 3 && "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3",
|
|
16
|
+
cols === 4 && "grid-cols-1 sm:grid-cols-2 lg:grid-cols-4"
|
|
17
|
+
)}
|
|
18
|
+
>
|
|
19
|
+
{children}
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default CardGroup;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Check, ThumbsUp, ThumbsDown } from "lucide-react";
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
|
+
|
|
5
|
+
interface FeedbackWidgetProps {
|
|
6
|
+
path: string;
|
|
7
|
+
backend?: string;
|
|
8
|
+
siteId?: string;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type FeedbackState = "initial" | "form" | "submitted";
|
|
13
|
+
|
|
14
|
+
export function FeedbackWidget({
|
|
15
|
+
path,
|
|
16
|
+
backend,
|
|
17
|
+
siteId,
|
|
18
|
+
className,
|
|
19
|
+
}: FeedbackWidgetProps) {
|
|
20
|
+
const [state, setState] = useState<FeedbackState>("initial");
|
|
21
|
+
const [helpful, setHelpful] = useState<boolean | null>(null);
|
|
22
|
+
const [comment, setComment] = useState("");
|
|
23
|
+
const [submitting, setSubmitting] = useState(false);
|
|
24
|
+
|
|
25
|
+
// Don't render if backend/siteId not configured
|
|
26
|
+
if (!backend || !siteId) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const handleVote = (isHelpful: boolean) => {
|
|
31
|
+
setHelpful(isHelpful);
|
|
32
|
+
setState("form");
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const submitFeedback = async () => {
|
|
36
|
+
if (submitting) return;
|
|
37
|
+
|
|
38
|
+
setSubmitting(true);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
await fetch(`${backend}/api/feedback`, {
|
|
42
|
+
method: "POST",
|
|
43
|
+
headers: {
|
|
44
|
+
"Content-Type": "application/json",
|
|
45
|
+
},
|
|
46
|
+
body: JSON.stringify({
|
|
47
|
+
siteId,
|
|
48
|
+
page: path,
|
|
49
|
+
helpful,
|
|
50
|
+
comment: comment.trim() || undefined,
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
} catch (err) {
|
|
54
|
+
console.debug("[opendocs] Feedback error:", err);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
setState("submitted");
|
|
58
|
+
setSubmitting(false);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const handleSkip = () => {
|
|
62
|
+
submitFeedback();
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
if (state === "submitted") {
|
|
66
|
+
return (
|
|
67
|
+
<div className={cn("flex items-center gap-2", className)}>
|
|
68
|
+
<span className="text-sm text-[var(--color-success)] flex items-center gap-1.5">
|
|
69
|
+
<Check className="w-4 h-4" />
|
|
70
|
+
Thanks for your feedback!
|
|
71
|
+
</span>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (state === "form") {
|
|
77
|
+
return (
|
|
78
|
+
<div className={cn("max-w-md", className)}>
|
|
79
|
+
<p className="text-sm text-[var(--color-muted)] mb-3">
|
|
80
|
+
Thanks! Any additional feedback?{" "}
|
|
81
|
+
<span className="text-[var(--color-muted-foreground)]">
|
|
82
|
+
(optional)
|
|
83
|
+
</span>
|
|
84
|
+
</p>
|
|
85
|
+
<textarea
|
|
86
|
+
value={comment}
|
|
87
|
+
onChange={(e) => setComment(e.target.value)}
|
|
88
|
+
placeholder="What could be improved?"
|
|
89
|
+
rows={3}
|
|
90
|
+
className={cn(
|
|
91
|
+
"w-full px-3 py-2 text-sm rounded-lg resize-none",
|
|
92
|
+
"bg-[var(--color-surface)] border border-[var(--color-border)]",
|
|
93
|
+
"text-[var(--color-foreground)] placeholder:text-[var(--color-muted-foreground)]",
|
|
94
|
+
"focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-transparent",
|
|
95
|
+
)}
|
|
96
|
+
/>
|
|
97
|
+
<div className="flex gap-2 mt-3">
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
onClick={submitFeedback}
|
|
101
|
+
disabled={submitting}
|
|
102
|
+
className={cn(
|
|
103
|
+
"px-4 py-2 text-sm font-medium rounded-lg transition-colors",
|
|
104
|
+
"bg-[var(--color-primary)] text-[var(--color-primary-foreground)]",
|
|
105
|
+
"hover:opacity-90",
|
|
106
|
+
"disabled:opacity-50 disabled:cursor-not-allowed",
|
|
107
|
+
)}
|
|
108
|
+
>
|
|
109
|
+
Submit
|
|
110
|
+
</button>
|
|
111
|
+
<button
|
|
112
|
+
type="button"
|
|
113
|
+
onClick={handleSkip}
|
|
114
|
+
disabled={submitting}
|
|
115
|
+
className={cn(
|
|
116
|
+
"px-4 py-2 text-sm font-medium rounded-lg transition-colors",
|
|
117
|
+
"text-[var(--color-muted)] bg-[var(--color-surface-raised)]",
|
|
118
|
+
"hover:text-[var(--color-foreground)]",
|
|
119
|
+
"disabled:opacity-50 disabled:cursor-not-allowed",
|
|
120
|
+
)}
|
|
121
|
+
>
|
|
122
|
+
Skip
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<div className={cn("flex items-center gap-3", className)}>
|
|
131
|
+
<span className="text-sm text-[var(--color-muted)]">
|
|
132
|
+
Was this page helpful?
|
|
133
|
+
</span>
|
|
134
|
+
<div className="flex items-center gap-1.5">
|
|
135
|
+
<button
|
|
136
|
+
type="button"
|
|
137
|
+
onClick={() => handleVote(true)}
|
|
138
|
+
className={cn(
|
|
139
|
+
"inline-flex items-center gap-1 px-2.5 py-1 text-sm rounded-lg transition-colors",
|
|
140
|
+
"text-[var(--color-muted)] bg-[var(--color-surface-raised)]",
|
|
141
|
+
"hover:text-[var(--color-success)] hover:bg-[var(--color-success-light)]",
|
|
142
|
+
)}
|
|
143
|
+
aria-label="Yes, this page was helpful"
|
|
144
|
+
>
|
|
145
|
+
<ThumbsUp className="w-4 h-4" />
|
|
146
|
+
<span>Yes</span>
|
|
147
|
+
</button>
|
|
148
|
+
<button
|
|
149
|
+
type="button"
|
|
150
|
+
onClick={() => handleVote(false)}
|
|
151
|
+
className={cn(
|
|
152
|
+
"inline-flex items-center gap-1 px-2.5 py-1 text-sm rounded-lg transition-colors",
|
|
153
|
+
"text-[var(--color-muted)] bg-[var(--color-surface-raised)]",
|
|
154
|
+
"hover:text-[var(--color-error)] hover:bg-[var(--color-error-light)]",
|
|
155
|
+
)}
|
|
156
|
+
aria-label="No, this page needs improvement"
|
|
157
|
+
>
|
|
158
|
+
<ThumbsDown className="w-4 h-4" />
|
|
159
|
+
<span>No</span>
|
|
160
|
+
</button>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export default FeedbackWidget;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Github } from "lucide-react";
|
|
2
|
+
import { cn } from "../../lib/utils";
|
|
3
|
+
|
|
4
|
+
interface GitHubLinkProps {
|
|
5
|
+
url: string;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function GitHubLink({ url, className }: GitHubLinkProps) {
|
|
10
|
+
return (
|
|
11
|
+
<a
|
|
12
|
+
href={url}
|
|
13
|
+
target="_blank"
|
|
14
|
+
rel="noopener noreferrer"
|
|
15
|
+
className={cn(
|
|
16
|
+
"flex items-center justify-center w-9 h-9 rounded-lg transition-colors",
|
|
17
|
+
"text-[var(--color-muted)] hover:text-[var(--color-foreground)]",
|
|
18
|
+
"hover:bg-[var(--color-surface-sunken)]",
|
|
19
|
+
className
|
|
20
|
+
)}
|
|
21
|
+
aria-label="View on GitHub"
|
|
22
|
+
>
|
|
23
|
+
<Github className="w-5 h-5" aria-hidden="true" />
|
|
24
|
+
</a>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default GitHubLink;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
2
|
+
import { cn } from "../../lib/utils";
|
|
3
|
+
|
|
4
|
+
interface NavigationCardProps {
|
|
5
|
+
title: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
href: string;
|
|
8
|
+
direction: "previous" | "next";
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function NavigationCard({
|
|
13
|
+
title,
|
|
14
|
+
description,
|
|
15
|
+
href,
|
|
16
|
+
direction,
|
|
17
|
+
className,
|
|
18
|
+
}: NavigationCardProps) {
|
|
19
|
+
const isNext = direction === "next";
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<a
|
|
23
|
+
href={href}
|
|
24
|
+
className={cn(
|
|
25
|
+
"group block p-5 rounded-xl border border-transparent transition-all duration-200",
|
|
26
|
+
"bg-[var(--color-surface-raised)] hover:border-[var(--color-border)]",
|
|
27
|
+
className
|
|
28
|
+
)}
|
|
29
|
+
>
|
|
30
|
+
<div className={cn("flex items-center gap-1.5 mb-1", isNext && "justify-end")}>
|
|
31
|
+
{!isNext && (
|
|
32
|
+
<ChevronLeft className="w-4 h-4 text-[var(--color-muted)] transition-transform group-hover:-translate-x-0.5" />
|
|
33
|
+
)}
|
|
34
|
+
<span className="text-base font-semibold text-[var(--color-foreground)]">
|
|
35
|
+
{title}
|
|
36
|
+
</span>
|
|
37
|
+
{isNext && (
|
|
38
|
+
<ChevronRight className="w-4 h-4 text-[var(--color-muted)] transition-transform group-hover:translate-x-0.5" />
|
|
39
|
+
)}
|
|
40
|
+
</div>
|
|
41
|
+
{description && (
|
|
42
|
+
<p className={cn(
|
|
43
|
+
"text-sm text-[var(--color-muted)] m-0",
|
|
44
|
+
isNext && "text-right"
|
|
45
|
+
)}>
|
|
46
|
+
{description}
|
|
47
|
+
</p>
|
|
48
|
+
)}
|
|
49
|
+
</a>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default NavigationCard;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { useState, useRef, useEffect } from "react";
|
|
2
|
+
import { Check, Copy, ChevronDown, Github } from "lucide-react";
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
|
+
|
|
5
|
+
interface PageActionsProps {
|
|
6
|
+
githubEditUrl?: string;
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function PageActions({ githubEditUrl, className }: PageActionsProps) {
|
|
11
|
+
const [copied, setCopied] = useState(false);
|
|
12
|
+
const [openDropdownOpen, setOpenDropdownOpen] = useState(false);
|
|
13
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
14
|
+
|
|
15
|
+
// Close dropdown when clicking outside
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
18
|
+
if (
|
|
19
|
+
dropdownRef.current &&
|
|
20
|
+
!dropdownRef.current.contains(event.target as Node)
|
|
21
|
+
) {
|
|
22
|
+
setOpenDropdownOpen(false);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
27
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
const handleCopyMarkdown = async () => {
|
|
31
|
+
try {
|
|
32
|
+
// Get the article content as text
|
|
33
|
+
const article = document.querySelector("article");
|
|
34
|
+
if (!article) return;
|
|
35
|
+
|
|
36
|
+
// Get the raw text content
|
|
37
|
+
const textContent = article.innerText;
|
|
38
|
+
|
|
39
|
+
await navigator.clipboard.writeText(textContent);
|
|
40
|
+
setCopied(true);
|
|
41
|
+
|
|
42
|
+
// Reset after 2 seconds
|
|
43
|
+
setTimeout(() => setCopied(false), 2000);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.error("Failed to copy:", err);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className={cn("flex items-center gap-2", className)}>
|
|
51
|
+
{/* Copy Markdown button */}
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
onClick={handleCopyMarkdown}
|
|
55
|
+
className={cn(
|
|
56
|
+
"inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium rounded-lg transition-colors",
|
|
57
|
+
"text-[var(--color-muted)] bg-[var(--color-surface-raised)]",
|
|
58
|
+
"hover:text-[var(--color-foreground)] hover:bg-[var(--color-surface-sunken)]",
|
|
59
|
+
copied && "text-[var(--color-success)]"
|
|
60
|
+
)}
|
|
61
|
+
aria-label={copied ? "Copied!" : "Copy markdown content"}
|
|
62
|
+
>
|
|
63
|
+
{copied ? (
|
|
64
|
+
<Check className="w-4 h-4" />
|
|
65
|
+
) : (
|
|
66
|
+
<Copy className="w-4 h-4" />
|
|
67
|
+
)}
|
|
68
|
+
<span>{copied ? "Copied!" : "Copy Markdown"}</span>
|
|
69
|
+
</button>
|
|
70
|
+
|
|
71
|
+
{/* Open dropdown */}
|
|
72
|
+
{githubEditUrl && (
|
|
73
|
+
<div className="relative" ref={dropdownRef}>
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
onClick={() => setOpenDropdownOpen(!openDropdownOpen)}
|
|
77
|
+
className={cn(
|
|
78
|
+
"inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-lg transition-colors",
|
|
79
|
+
"text-[var(--color-muted)] bg-[var(--color-surface-raised)]",
|
|
80
|
+
"hover:text-[var(--color-foreground)] hover:bg-[var(--color-surface-sunken)]"
|
|
81
|
+
)}
|
|
82
|
+
aria-expanded={openDropdownOpen}
|
|
83
|
+
aria-haspopup="true"
|
|
84
|
+
>
|
|
85
|
+
<span>Open</span>
|
|
86
|
+
<ChevronDown
|
|
87
|
+
className={cn(
|
|
88
|
+
"w-4 h-4 transition-transform",
|
|
89
|
+
openDropdownOpen && "rotate-180"
|
|
90
|
+
)}
|
|
91
|
+
/>
|
|
92
|
+
</button>
|
|
93
|
+
|
|
94
|
+
{/* Dropdown menu */}
|
|
95
|
+
{openDropdownOpen && (
|
|
96
|
+
<div
|
|
97
|
+
className={cn(
|
|
98
|
+
"absolute right-0 mt-1 w-48 rounded-lg shadow-lg z-10",
|
|
99
|
+
"bg-[var(--color-surface)] border border-[var(--color-border)]"
|
|
100
|
+
)}
|
|
101
|
+
>
|
|
102
|
+
<a
|
|
103
|
+
href={githubEditUrl}
|
|
104
|
+
target="_blank"
|
|
105
|
+
rel="noopener noreferrer"
|
|
106
|
+
className={cn(
|
|
107
|
+
"flex items-center gap-2 px-4 py-2.5 text-sm rounded-lg",
|
|
108
|
+
"text-[var(--color-muted)] hover:text-[var(--color-foreground)]",
|
|
109
|
+
"hover:bg-[var(--color-surface-raised)]"
|
|
110
|
+
)}
|
|
111
|
+
onClick={() => setOpenDropdownOpen(false)}
|
|
112
|
+
>
|
|
113
|
+
<Github className="w-4 h-4" />
|
|
114
|
+
<span>Edit on GitHub</span>
|
|
115
|
+
</a>
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export default PageActions;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { cn } from "../../lib/utils";
|
|
2
|
+
import { FeedbackWidget } from "./FeedbackWidget";
|
|
3
|
+
import { NavigationCard } from "./NavigationCard";
|
|
4
|
+
|
|
5
|
+
interface PageLink {
|
|
6
|
+
title: string;
|
|
7
|
+
url: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface PageFooterProps {
|
|
12
|
+
path: string;
|
|
13
|
+
backend?: string;
|
|
14
|
+
siteId?: string;
|
|
15
|
+
feedbackEnabled?: boolean;
|
|
16
|
+
previousPage?: PageLink | null;
|
|
17
|
+
nextPage?: PageLink | null;
|
|
18
|
+
lastUpdated?: string;
|
|
19
|
+
className?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function PageFooter({
|
|
23
|
+
path,
|
|
24
|
+
backend,
|
|
25
|
+
siteId,
|
|
26
|
+
feedbackEnabled = true,
|
|
27
|
+
previousPage,
|
|
28
|
+
nextPage,
|
|
29
|
+
lastUpdated,
|
|
30
|
+
className,
|
|
31
|
+
}: PageFooterProps) {
|
|
32
|
+
const hasNavigation = previousPage || nextPage;
|
|
33
|
+
const showFeedback = feedbackEnabled && backend && siteId;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<footer
|
|
37
|
+
className={cn("mt-12 pt-8 border-t border-[var(--color-border)]", className)}
|
|
38
|
+
>
|
|
39
|
+
{/* Feedback widget */}
|
|
40
|
+
{showFeedback && (
|
|
41
|
+
<div className="mb-6">
|
|
42
|
+
<FeedbackWidget path={path} backend={backend} siteId={siteId} />
|
|
43
|
+
</div>
|
|
44
|
+
)}
|
|
45
|
+
|
|
46
|
+
{/* Last updated date */}
|
|
47
|
+
{lastUpdated && (
|
|
48
|
+
<div className="mb-6">
|
|
49
|
+
<span className="text-sm text-[var(--color-muted)]">
|
|
50
|
+
Last updated on{" "}
|
|
51
|
+
<time dateTime={lastUpdated}>
|
|
52
|
+
{new Date(lastUpdated).toLocaleDateString("en-US", {
|
|
53
|
+
year: "numeric",
|
|
54
|
+
month: "long",
|
|
55
|
+
day: "numeric",
|
|
56
|
+
})}
|
|
57
|
+
</time>
|
|
58
|
+
</span>
|
|
59
|
+
</div>
|
|
60
|
+
)}
|
|
61
|
+
|
|
62
|
+
{/* Navigation cards */}
|
|
63
|
+
{hasNavigation && (
|
|
64
|
+
<nav className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
65
|
+
<div>
|
|
66
|
+
{previousPage && (
|
|
67
|
+
<NavigationCard
|
|
68
|
+
title={previousPage.title}
|
|
69
|
+
description={previousPage.description}
|
|
70
|
+
href={previousPage.url}
|
|
71
|
+
direction="previous"
|
|
72
|
+
/>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
<div>
|
|
76
|
+
{nextPage && (
|
|
77
|
+
<NavigationCard
|
|
78
|
+
title={nextPage.title}
|
|
79
|
+
description={nextPage.description}
|
|
80
|
+
href={nextPage.url}
|
|
81
|
+
direction="next"
|
|
82
|
+
/>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
</nav>
|
|
86
|
+
)}
|
|
87
|
+
</footer>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export default PageFooter;
|