@opendocsdev/cli 0.2.7 → 0.2.8
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/src/engine/src/components/react/PageFooter.tsx +46 -0
- package/src/engine/src/components/react/Sidebar.tsx +26 -13
- package/src/engine/src/components/react/SocialLinks.tsx +59 -0
- package/src/engine/src/components/react/ThemeToggle.tsx +5 -5
- package/src/engine/src/layouts/DocsLayout.astro +25 -5
package/package.json
CHANGED
|
@@ -8,6 +8,11 @@ interface PageLink {
|
|
|
8
8
|
description?: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
interface FooterConfig {
|
|
12
|
+
copyright?: string;
|
|
13
|
+
links?: { title: string; url: string }[];
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
interface PageFooterProps {
|
|
12
17
|
path: string;
|
|
13
18
|
backend?: string;
|
|
@@ -16,6 +21,7 @@ interface PageFooterProps {
|
|
|
16
21
|
previousPage?: PageLink | null;
|
|
17
22
|
nextPage?: PageLink | null;
|
|
18
23
|
lastUpdated?: string;
|
|
24
|
+
footer?: FooterConfig;
|
|
19
25
|
className?: string;
|
|
20
26
|
}
|
|
21
27
|
|
|
@@ -27,10 +33,12 @@ export function PageFooter({
|
|
|
27
33
|
previousPage,
|
|
28
34
|
nextPage,
|
|
29
35
|
lastUpdated,
|
|
36
|
+
footer,
|
|
30
37
|
className,
|
|
31
38
|
}: PageFooterProps) {
|
|
32
39
|
const hasNavigation = previousPage || nextPage;
|
|
33
40
|
const showFeedback = feedbackEnabled && backend && siteId;
|
|
41
|
+
const hasFooter = footer?.copyright || (footer?.links && footer.links.length > 0);
|
|
34
42
|
|
|
35
43
|
return (
|
|
36
44
|
<footer
|
|
@@ -84,6 +92,44 @@ export function PageFooter({
|
|
|
84
92
|
</div>
|
|
85
93
|
</nav>
|
|
86
94
|
)}
|
|
95
|
+
|
|
96
|
+
{/* Footer content */}
|
|
97
|
+
{hasFooter && (
|
|
98
|
+
<div className="mt-8 pt-6 border-t border-[var(--color-border)]">
|
|
99
|
+
<div className="flex flex-col sm:flex-row items-center justify-between gap-3">
|
|
100
|
+
{footer?.copyright && (
|
|
101
|
+
<p className="text-sm text-[var(--color-muted)]">{footer.copyright}</p>
|
|
102
|
+
)}
|
|
103
|
+
{footer?.links && footer.links.length > 0 && (
|
|
104
|
+
<div className="flex items-center gap-4">
|
|
105
|
+
{footer.links.map((link) => (
|
|
106
|
+
<a
|
|
107
|
+
key={link.url}
|
|
108
|
+
href={link.url}
|
|
109
|
+
target="_blank"
|
|
110
|
+
rel="noopener noreferrer"
|
|
111
|
+
className="text-sm text-[var(--color-muted)] hover:text-[var(--color-foreground)] transition-colors"
|
|
112
|
+
>
|
|
113
|
+
{link.title}
|
|
114
|
+
</a>
|
|
115
|
+
))}
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
)}
|
|
121
|
+
|
|
122
|
+
{/* Built with OpenDocs */}
|
|
123
|
+
<div className="mt-6 flex items-center justify-center">
|
|
124
|
+
<a
|
|
125
|
+
href="https://docs.opendocs.dev"
|
|
126
|
+
target="_blank"
|
|
127
|
+
rel="noopener noreferrer"
|
|
128
|
+
className="text-xs text-[var(--color-muted)]/60 hover:text-[var(--color-muted)] transition-colors"
|
|
129
|
+
>
|
|
130
|
+
Built with OpenDocs
|
|
131
|
+
</a>
|
|
132
|
+
</div>
|
|
87
133
|
</footer>
|
|
88
134
|
);
|
|
89
135
|
}
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
type NavigationGroup,
|
|
13
13
|
} from "../../lib/utils";
|
|
14
14
|
import { ThemeToggle } from "./ThemeToggle";
|
|
15
|
-
import {
|
|
15
|
+
import { SocialLinks } from "./SocialLinks";
|
|
16
16
|
import { SidebarSearchTrigger } from "./SidebarSearchTrigger";
|
|
17
17
|
|
|
18
18
|
interface LogoConfig {
|
|
@@ -26,6 +26,8 @@ interface SidebarProps {
|
|
|
26
26
|
logo?: string | LogoConfig;
|
|
27
27
|
currentPath: string;
|
|
28
28
|
githubUrl?: string;
|
|
29
|
+
discordUrl?: string;
|
|
30
|
+
twitterUrl?: string;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
// Check if any page in a group of children is active
|
|
@@ -250,11 +252,18 @@ function Logo({ logo, siteName }: { logo?: string | LogoConfig; siteName: string
|
|
|
250
252
|
);
|
|
251
253
|
}
|
|
252
254
|
|
|
255
|
+
const nameEl = (
|
|
256
|
+
<span className="text-base font-semibold text-[var(--color-foreground)]">
|
|
257
|
+
{siteName}
|
|
258
|
+
</span>
|
|
259
|
+
);
|
|
260
|
+
|
|
253
261
|
// String logo - no theme switching needed
|
|
254
262
|
if (typeof logo === "string") {
|
|
255
263
|
return (
|
|
256
|
-
<a href="/" className="flex items-center gap-2">
|
|
257
|
-
<img src={logo} alt={siteName} className="h-
|
|
264
|
+
<a href="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
|
|
265
|
+
<img src={logo} alt={siteName} className="h-7 w-auto" />
|
|
266
|
+
{nameEl}
|
|
258
267
|
</a>
|
|
259
268
|
);
|
|
260
269
|
}
|
|
@@ -264,17 +273,19 @@ function Logo({ logo, siteName }: { logo?: string | LogoConfig; siteName: string
|
|
|
264
273
|
|
|
265
274
|
if (dark) {
|
|
266
275
|
return (
|
|
267
|
-
<a href="/" className="flex items-center gap-2">
|
|
268
|
-
<img src={light} alt={siteName} className="h-
|
|
269
|
-
<img src={dark} alt={siteName} className="h-
|
|
276
|
+
<a href="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
|
|
277
|
+
<img src={light} alt={siteName} className="h-7 w-auto dark:hidden" />
|
|
278
|
+
<img src={dark} alt={siteName} className="h-7 w-auto hidden dark:block" />
|
|
279
|
+
{nameEl}
|
|
270
280
|
</a>
|
|
271
281
|
);
|
|
272
282
|
}
|
|
273
283
|
|
|
274
284
|
// Only light logo provided
|
|
275
285
|
return (
|
|
276
|
-
<a href="/" className="flex items-center gap-2">
|
|
277
|
-
<img src={light} alt={siteName} className="h-
|
|
286
|
+
<a href="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
|
|
287
|
+
<img src={light} alt={siteName} className="h-7 w-auto" />
|
|
288
|
+
{nameEl}
|
|
278
289
|
</a>
|
|
279
290
|
);
|
|
280
291
|
}
|
|
@@ -285,6 +296,8 @@ export function Sidebar({
|
|
|
285
296
|
logo,
|
|
286
297
|
currentPath: initialPath,
|
|
287
298
|
githubUrl,
|
|
299
|
+
discordUrl,
|
|
300
|
+
twitterUrl,
|
|
288
301
|
}: SidebarProps) {
|
|
289
302
|
// Track current path locally so it updates after View Transition navigations
|
|
290
303
|
const [currentPath, setCurrentPath] = useState(initialPath);
|
|
@@ -298,9 +311,9 @@ export function Sidebar({
|
|
|
298
311
|
return (
|
|
299
312
|
<div className="flex flex-col h-full">
|
|
300
313
|
{/* Logo/Site name + Theme toggle */}
|
|
301
|
-
<div className="flex items-center justify-between h-
|
|
314
|
+
<div className="flex items-center justify-between h-14 px-4">
|
|
302
315
|
<Logo logo={logo} siteName={siteName} />
|
|
303
|
-
<ThemeToggle />
|
|
316
|
+
<ThemeToggle className="flex-shrink-0" />
|
|
304
317
|
</div>
|
|
305
318
|
|
|
306
319
|
{/* Search trigger */}
|
|
@@ -363,10 +376,10 @@ export function Sidebar({
|
|
|
363
376
|
))}
|
|
364
377
|
</nav>
|
|
365
378
|
|
|
366
|
-
{/* Bottom section with
|
|
367
|
-
{githubUrl && (
|
|
379
|
+
{/* Bottom section with social links */}
|
|
380
|
+
{(githubUrl || discordUrl || twitterUrl) && (
|
|
368
381
|
<div className="px-4 py-3">
|
|
369
|
-
<
|
|
382
|
+
<SocialLinks github={githubUrl} discord={discordUrl} twitter={twitterUrl} />
|
|
370
383
|
</div>
|
|
371
384
|
)}
|
|
372
385
|
</div>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Github } from "lucide-react";
|
|
2
|
+
import { cn } from "../../lib/utils";
|
|
3
|
+
|
|
4
|
+
// Inline Discord SVG — lucide-react doesn't include Discord
|
|
5
|
+
function DiscordIcon({ className }: { className?: string }) {
|
|
6
|
+
return (
|
|
7
|
+
<svg viewBox="0 0 24 24" fill="currentColor" className={className} aria-hidden="true">
|
|
8
|
+
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
|
|
9
|
+
</svg>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Inline X/Twitter SVG
|
|
14
|
+
function TwitterIcon({ className }: { className?: string }) {
|
|
15
|
+
return (
|
|
16
|
+
<svg viewBox="0 0 24 24" fill="currentColor" className={className} aria-hidden="true">
|
|
17
|
+
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
|
18
|
+
</svg>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface SocialLinksProps {
|
|
23
|
+
github?: string;
|
|
24
|
+
discord?: string;
|
|
25
|
+
twitter?: string;
|
|
26
|
+
className?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const linkStyle = cn(
|
|
30
|
+
"flex items-center justify-center w-9 h-9 rounded-lg transition-colors",
|
|
31
|
+
"text-[var(--color-muted)] hover:text-[var(--color-foreground)]",
|
|
32
|
+
"hover:bg-[var(--color-surface-sunken)]"
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
export function SocialLinks({ github, discord, twitter, className }: SocialLinksProps) {
|
|
36
|
+
if (!github && !discord && !twitter) return null;
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className={cn("flex items-center gap-1", className)}>
|
|
40
|
+
{github && (
|
|
41
|
+
<a href={github} target="_blank" rel="noopener noreferrer" className={linkStyle} aria-label="GitHub">
|
|
42
|
+
<Github className="w-5 h-5" aria-hidden="true" />
|
|
43
|
+
</a>
|
|
44
|
+
)}
|
|
45
|
+
{discord && (
|
|
46
|
+
<a href={discord} target="_blank" rel="noopener noreferrer" className={linkStyle} aria-label="Discord">
|
|
47
|
+
<DiscordIcon className="w-5 h-5" />
|
|
48
|
+
</a>
|
|
49
|
+
)}
|
|
50
|
+
{twitter && (
|
|
51
|
+
<a href={twitter} target="_blank" rel="noopener noreferrer" className={linkStyle} aria-label="Twitter">
|
|
52
|
+
<TwitterIcon className="w-5 h-5" />
|
|
53
|
+
</a>
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export default SocialLinks;
|
|
@@ -36,14 +36,14 @@ export function ThemeToggle({ className }: ThemeToggleProps) {
|
|
|
36
36
|
<button
|
|
37
37
|
type="button"
|
|
38
38
|
className={cn(
|
|
39
|
-
"flex items-center justify-center w-
|
|
39
|
+
"flex items-center justify-center w-7 h-7 rounded-md transition-colors",
|
|
40
40
|
"text-[var(--color-muted)] hover:text-[var(--color-foreground)]",
|
|
41
41
|
"hover:bg-[var(--color-surface-sunken)]",
|
|
42
42
|
className
|
|
43
43
|
)}
|
|
44
44
|
aria-label="Toggle theme"
|
|
45
45
|
>
|
|
46
|
-
<span className="w-
|
|
46
|
+
<span className="w-4 h-4" />
|
|
47
47
|
</button>
|
|
48
48
|
);
|
|
49
49
|
}
|
|
@@ -53,7 +53,7 @@ export function ThemeToggle({ className }: ThemeToggleProps) {
|
|
|
53
53
|
type="button"
|
|
54
54
|
onClick={toggleTheme}
|
|
55
55
|
className={cn(
|
|
56
|
-
"flex items-center justify-center w-
|
|
56
|
+
"flex items-center justify-center w-7 h-7 rounded-md transition-colors",
|
|
57
57
|
"text-[var(--color-muted)] hover:text-[var(--color-foreground)]",
|
|
58
58
|
"hover:bg-[var(--color-surface-sunken)]",
|
|
59
59
|
className
|
|
@@ -61,9 +61,9 @@ export function ThemeToggle({ className }: ThemeToggleProps) {
|
|
|
61
61
|
aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
|
|
62
62
|
>
|
|
63
63
|
{isDark ? (
|
|
64
|
-
<Sun className="w-
|
|
64
|
+
<Sun className="w-4 h-4" />
|
|
65
65
|
) : (
|
|
66
|
-
<Moon className="w-
|
|
66
|
+
<Moon className="w-4 h-4" />
|
|
67
67
|
)}
|
|
68
68
|
</button>
|
|
69
69
|
);
|
|
@@ -35,7 +35,10 @@ const faviconPath = config.favicon || "/favicon.ico";
|
|
|
35
35
|
const navigation = config.navigation || [];
|
|
36
36
|
const logo = config.logo;
|
|
37
37
|
const githubUrl = config.socialLinks?.github;
|
|
38
|
+
const discordUrl = config.socialLinks?.discord;
|
|
39
|
+
const twitterUrl = config.socialLinks?.twitter;
|
|
38
40
|
const feedbackEnabled = config.features?.feedback !== false;
|
|
41
|
+
const footerConfig = config.footer as { copyright?: string; links?: { title: string; url: string }[] } | undefined;
|
|
39
42
|
|
|
40
43
|
// Backend config
|
|
41
44
|
const backendConfig = config.backend as { apiUrl?: string; siteId?: string } | undefined;
|
|
@@ -48,6 +51,10 @@ const accentColor = config.theme?.accentColor || primaryColor;
|
|
|
48
51
|
const primary = generateColorVariants(primaryColor);
|
|
49
52
|
const accent = generateColorVariants(accentColor);
|
|
50
53
|
|
|
54
|
+
// Favicon MIME type from extension
|
|
55
|
+
const faviconExt = faviconPath.split(".").pop()?.toLowerCase();
|
|
56
|
+
const faviconType = faviconExt === "svg" ? "image/svg+xml" : faviconExt === "png" ? "image/png" : faviconExt === "ico" ? "image/x-icon" : undefined;
|
|
57
|
+
|
|
51
58
|
// SEO metadata
|
|
52
59
|
const siteUrl = (config.metadata as { url?: string; ogImage?: string } | undefined)?.url || "";
|
|
53
60
|
const ogImage = (config.metadata as { url?: string; ogImage?: string } | undefined)?.ogImage;
|
|
@@ -55,6 +62,9 @@ const canonicalUrl = siteUrl ? `${siteUrl}${currentPath}` : "";
|
|
|
55
62
|
const pageDescription = description || `${title} - ${siteName}`;
|
|
56
63
|
const fullTitle = `${title} | ${siteName}`;
|
|
57
64
|
|
|
65
|
+
// Extract Twitter handle from URL for twitter:site meta tag
|
|
66
|
+
const twitterHandle = twitterUrl ? `@${twitterUrl.replace(/\/$/, "").split("/").pop()}` : undefined;
|
|
67
|
+
|
|
58
68
|
// Build breadcrumb segments from current path
|
|
59
69
|
const pathSegments = currentPath.split("/").filter(Boolean);
|
|
60
70
|
const breadcrumbItems = pathSegments.map((segment, i) => ({
|
|
@@ -72,7 +82,11 @@ const { previous: previousPage, next: nextPage } = getPageNavigation(navigation,
|
|
|
72
82
|
<meta charset="UTF-8" />
|
|
73
83
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
74
84
|
<meta name="description" content={pageDescription} />
|
|
75
|
-
|
|
85
|
+
{faviconType ? (
|
|
86
|
+
<link rel="icon" type={faviconType} href={faviconPath} />
|
|
87
|
+
) : (
|
|
88
|
+
<link rel="icon" href={faviconPath} />
|
|
89
|
+
)}
|
|
76
90
|
<title>{fullTitle}</title>
|
|
77
91
|
|
|
78
92
|
{/* Canonical URL */}
|
|
@@ -88,11 +102,13 @@ const { previous: previousPage, next: nextPage } = getPageNavigation(navigation,
|
|
|
88
102
|
<meta property="og:site_name" content={siteName} />
|
|
89
103
|
{canonicalUrl && <meta property="og:url" content={canonicalUrl} />}
|
|
90
104
|
{ogImage && <meta property="og:image" content={ogImage.startsWith("http") ? ogImage : `${siteUrl}${ogImage}`} />}
|
|
105
|
+
{ogImage && <meta property="og:image:alt" content={pageDescription} />}
|
|
91
106
|
|
|
92
107
|
{/* Twitter Card */}
|
|
93
108
|
<meta name="twitter:card" content={ogImage ? "summary_large_image" : "summary"} />
|
|
94
109
|
<meta name="twitter:title" content={fullTitle} />
|
|
95
110
|
<meta name="twitter:description" content={pageDescription} />
|
|
111
|
+
{twitterHandle && <meta name="twitter:site" content={twitterHandle} />}
|
|
96
112
|
{ogImage && <meta name="twitter:image" content={ogImage.startsWith("http") ? ogImage : `${siteUrl}${ogImage}`} />}
|
|
97
113
|
|
|
98
114
|
{/* Structured Data */}
|
|
@@ -163,6 +179,8 @@ const { previous: previousPage, next: nextPage } = getPageNavigation(navigation,
|
|
|
163
179
|
logo={logo}
|
|
164
180
|
currentPath={currentPath}
|
|
165
181
|
githubUrl={githubUrl}
|
|
182
|
+
discordUrl={discordUrl}
|
|
183
|
+
twitterUrl={twitterUrl}
|
|
166
184
|
/>
|
|
167
185
|
</aside>
|
|
168
186
|
|
|
@@ -187,6 +205,8 @@ const { previous: previousPage, next: nextPage } = getPageNavigation(navigation,
|
|
|
187
205
|
logo={logo}
|
|
188
206
|
currentPath={currentPath}
|
|
189
207
|
githubUrl={githubUrl}
|
|
208
|
+
discordUrl={discordUrl}
|
|
209
|
+
twitterUrl={twitterUrl}
|
|
190
210
|
/>
|
|
191
211
|
</div>
|
|
192
212
|
</aside>
|
|
@@ -209,7 +229,7 @@ const { previous: previousPage, next: nextPage } = getPageNavigation(navigation,
|
|
|
209
229
|
</button>
|
|
210
230
|
|
|
211
231
|
<!-- Logo/site name -->
|
|
212
|
-
<a href="/" class="flex items-center
|
|
232
|
+
<a href="/" class="flex items-center gap-2">
|
|
213
233
|
{logo ? (
|
|
214
234
|
typeof logo === "string" ? (
|
|
215
235
|
<img src={logo} alt={siteName} class="h-6 w-auto" />
|
|
@@ -221,9 +241,8 @@ const { previous: previousPage, next: nextPage } = getPageNavigation(navigation,
|
|
|
221
241
|
) : (
|
|
222
242
|
<img src={logo.light} alt={siteName} class="h-6 w-auto" />
|
|
223
243
|
)
|
|
224
|
-
) :
|
|
225
|
-
|
|
226
|
-
)}
|
|
244
|
+
) : null}
|
|
245
|
+
<span class="text-base font-semibold text-[var(--color-foreground)]">{siteName}</span>
|
|
227
246
|
</a>
|
|
228
247
|
|
|
229
248
|
<!-- Search button -->
|
|
@@ -259,6 +278,7 @@ const { previous: previousPage, next: nextPage } = getPageNavigation(navigation,
|
|
|
259
278
|
previousPage={previousPage}
|
|
260
279
|
nextPage={nextPage}
|
|
261
280
|
lastUpdated={lastUpdated}
|
|
281
|
+
footer={footerConfig}
|
|
262
282
|
/>
|
|
263
283
|
<CopyButton />
|
|
264
284
|
</main>
|