@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opendocsdev/cli",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -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 { GitHubLink } from "./GitHubLink";
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-8 w-auto" />
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-8 w-auto dark:hidden" />
269
- <img src={dark} alt={siteName} className="h-8 w-auto hidden dark:block" />
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-8 w-auto" />
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-16 px-4">
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 GitHub link */}
367
- {githubUrl && (
379
+ {/* Bottom section with social links */}
380
+ {(githubUrl || discordUrl || twitterUrl) && (
368
381
  <div className="px-4 py-3">
369
- <GitHubLink url={githubUrl} />
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-9 h-9 rounded-lg transition-colors",
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-5 h-5" />
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-9 h-9 rounded-lg transition-colors",
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-5 h-5" />
64
+ <Sun className="w-4 h-4" />
65
65
  ) : (
66
- <Moon className="w-5 h-5" />
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
- <link rel="icon" type="image/x-icon" href={faviconPath} />
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 justify-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
- <span class="text-lg font-semibold text-[var(--color-foreground)]">{siteName}</span>
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>