@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.
Files changed (78) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +300 -0
  3. package/dist/bin/opendocs.js +712 -0
  4. package/dist/bin/opendocs.js.map +1 -0
  5. package/dist/templates/api-reference.mdx +308 -0
  6. package/dist/templates/components.mdx +286 -0
  7. package/dist/templates/configuration.mdx +190 -0
  8. package/dist/templates/docs.json +27 -0
  9. package/dist/templates/introduction.mdx +25 -0
  10. package/dist/templates/logo.svg +4 -0
  11. package/dist/templates/quickstart.mdx +59 -0
  12. package/dist/templates/writing-content.mdx +236 -0
  13. package/package.json +92 -0
  14. package/src/engine/astro.config.ts +75 -0
  15. package/src/engine/src/components/Analytics.astro +57 -0
  16. package/src/engine/src/components/ApiPlayground.astro +24 -0
  17. package/src/engine/src/components/Callout.astro +66 -0
  18. package/src/engine/src/components/Card.astro +75 -0
  19. package/src/engine/src/components/CardGroup.astro +29 -0
  20. package/src/engine/src/components/CodeGroup.astro +231 -0
  21. package/src/engine/src/components/CopyButton.astro +179 -0
  22. package/src/engine/src/components/Steps.astro +27 -0
  23. package/src/engine/src/components/Tab.astro +21 -0
  24. package/src/engine/src/components/TableOfContents.astro +119 -0
  25. package/src/engine/src/components/Tabs.astro +135 -0
  26. package/src/engine/src/components/index.ts +107 -0
  27. package/src/engine/src/components/react/ApiPlayground/AuthSection.tsx +91 -0
  28. package/src/engine/src/components/react/ApiPlayground/CodeBlock.tsx +66 -0
  29. package/src/engine/src/components/react/ApiPlayground/CodeSnippets.tsx +66 -0
  30. package/src/engine/src/components/react/ApiPlayground/CollapsibleSection.tsx +26 -0
  31. package/src/engine/src/components/react/ApiPlayground/KeyValueEditor.tsx +58 -0
  32. package/src/engine/src/components/react/ApiPlayground/ResponseDisplay.tsx +109 -0
  33. package/src/engine/src/components/react/ApiPlayground/Spinner.tsx +6 -0
  34. package/src/engine/src/components/react/ApiPlayground/constants.ts +16 -0
  35. package/src/engine/src/components/react/ApiPlayground/generators.test.ts +130 -0
  36. package/src/engine/src/components/react/ApiPlayground/generators.ts +75 -0
  37. package/src/engine/src/components/react/ApiPlayground/index.tsx +490 -0
  38. package/src/engine/src/components/react/ApiPlayground/types.ts +35 -0
  39. package/src/engine/src/components/react/Callout.tsx +54 -0
  40. package/src/engine/src/components/react/Card.tsx +48 -0
  41. package/src/engine/src/components/react/CardGroup.tsx +24 -0
  42. package/src/engine/src/components/react/FeedbackWidget.tsx +166 -0
  43. package/src/engine/src/components/react/GitHubLink.tsx +28 -0
  44. package/src/engine/src/components/react/NavigationCard.tsx +53 -0
  45. package/src/engine/src/components/react/PageActions.tsx +124 -0
  46. package/src/engine/src/components/react/PageFooter.tsx +91 -0
  47. package/src/engine/src/components/react/SearchModal.tsx +358 -0
  48. package/src/engine/src/components/react/SearchProvider.tsx +37 -0
  49. package/src/engine/src/components/react/Sidebar.tsx +369 -0
  50. package/src/engine/src/components/react/SidebarSearchTrigger.tsx +57 -0
  51. package/src/engine/src/components/react/Steps.tsx +25 -0
  52. package/src/engine/src/components/react/ThemeToggle.tsx +72 -0
  53. package/src/engine/src/components/react/index.ts +14 -0
  54. package/src/engine/src/env.d.ts +10 -0
  55. package/src/engine/src/layouts/DocsLayout.astro +357 -0
  56. package/src/engine/src/lib/__tests__/markdown.test.ts +124 -0
  57. package/src/engine/src/lib/__tests__/mdx-loader.test.ts +205 -0
  58. package/src/engine/src/lib/config.ts +79 -0
  59. package/src/engine/src/lib/markdown.ts +54 -0
  60. package/src/engine/src/lib/mdx-loader.ts +143 -0
  61. package/src/engine/src/lib/mdx-utils.ts +72 -0
  62. package/src/engine/src/lib/remark-opendocs.ts +195 -0
  63. package/src/engine/src/lib/utils.ts +221 -0
  64. package/src/engine/src/pages/[...slug].astro +115 -0
  65. package/src/engine/src/pages/index.astro +71 -0
  66. package/src/engine/src/scripts/mobile-sidebar.ts +56 -0
  67. package/src/engine/src/scripts/theme-init.ts +25 -0
  68. package/src/engine/src/styles/global.css +703 -0
  69. package/src/engine/tailwind.config.mjs +60 -0
  70. package/src/engine/tsconfig.json +15 -0
  71. package/src/templates/api-reference.mdx +308 -0
  72. package/src/templates/components.mdx +286 -0
  73. package/src/templates/configuration.mdx +190 -0
  74. package/src/templates/docs.json +27 -0
  75. package/src/templates/introduction.mdx +25 -0
  76. package/src/templates/logo.svg +4 -0
  77. package/src/templates/quickstart.mdx +59 -0
  78. 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;