@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,221 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ /**
5
+ * Combines clsx and tailwind-merge for conditional class names.
6
+ * - clsx: Handles conditional classes
7
+ * - tailwind-merge: Resolves conflicting Tailwind classes (e.g., 'px-2 px-4' → 'px-4')
8
+ */
9
+ export function cn(...inputs: ClassValue[]) {
10
+ return twMerge(clsx(inputs));
11
+ }
12
+
13
+ // =============================================================================
14
+ // Color Utilities
15
+ // =============================================================================
16
+
17
+ interface HSL {
18
+ h: number;
19
+ s: number;
20
+ l: number;
21
+ }
22
+
23
+ /**
24
+ * Converts a hex color string to HSL values.
25
+ */
26
+ export function hexToHSL(hex: string): HSL {
27
+ const r = parseInt(hex.slice(1, 3), 16) / 255;
28
+ const g = parseInt(hex.slice(3, 5), 16) / 255;
29
+ const b = parseInt(hex.slice(5, 7), 16) / 255;
30
+ const max = Math.max(r, g, b);
31
+ const min = Math.min(r, g, b);
32
+ let h = 0;
33
+ let s = 0;
34
+ const l = (max + min) / 2;
35
+
36
+ if (max !== min) {
37
+ const d = max - min;
38
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
39
+ switch (max) {
40
+ case r:
41
+ h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
42
+ break;
43
+ case g:
44
+ h = ((b - r) / d + 2) / 6;
45
+ break;
46
+ case b:
47
+ h = ((r - g) / d + 4) / 6;
48
+ break;
49
+ }
50
+ }
51
+ return { h: h * 360, s: s * 100, l: l * 100 };
52
+ }
53
+
54
+ /**
55
+ * Converts HSL values to a hex color string.
56
+ */
57
+ export function hslToHex(h: number, s: number, l: number): string {
58
+ s /= 100;
59
+ l /= 100;
60
+ const a = s * Math.min(l, 1 - l);
61
+ const f = (n: number) => {
62
+ const k = (n + h / 30) % 12;
63
+ const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
64
+ return Math.round(255 * color).toString(16).padStart(2, "0");
65
+ };
66
+ return `#${f(0)}${f(8)}${f(4)}`;
67
+ }
68
+
69
+ /**
70
+ * Generates theme color variants from a base hex color.
71
+ */
72
+ export function generateColorVariants(hex: string) {
73
+ const hsl = hexToHSL(hex);
74
+ return {
75
+ base: hex,
76
+ light: hslToHex(hsl.h, Math.min(hsl.s, 30), 95),
77
+ lightDark: `rgba(${parseInt(hex.slice(1, 3), 16)}, ${parseInt(hex.slice(3, 5), 16)}, ${parseInt(hex.slice(5, 7), 16)}, 0.15)`,
78
+ dark: hslToHex(hsl.h, hsl.s, Math.max(hsl.l - 15, 25)),
79
+ darkMode: hslToHex(hsl.h, hsl.s, Math.min(hsl.l + 15, 70)),
80
+ };
81
+ }
82
+
83
+ // =============================================================================
84
+ // Navigation Utilities
85
+ // =============================================================================
86
+
87
+ // Types for navigation - use loose types to match Zod schema inference
88
+ export type PageWithChildren = { page: string; children: string[] };
89
+ export type NestedGroup = { group: string; pages: readonly unknown[] };
90
+ export type PageItem = string | PageWithChildren | NestedGroup;
91
+
92
+ // Generic navigation group interface that accepts any pages array type
93
+ export interface NavigationGroup {
94
+ group: string;
95
+ pages: readonly unknown[];
96
+ }
97
+
98
+ /**
99
+ * Type guard for PageWithChildren navigation items.
100
+ */
101
+ export function isPageWithChildren(item: unknown): item is PageWithChildren {
102
+ return (
103
+ typeof item === "object" &&
104
+ item !== null &&
105
+ "page" in item &&
106
+ "children" in item
107
+ );
108
+ }
109
+
110
+ /**
111
+ * Type guard for NestedGroup navigation items.
112
+ */
113
+ export function isNestedGroup(item: unknown): item is NestedGroup {
114
+ return (
115
+ typeof item === "object" &&
116
+ item !== null &&
117
+ "group" in item &&
118
+ "pages" in item
119
+ );
120
+ }
121
+
122
+ /**
123
+ * Converts a page path to a URL.
124
+ */
125
+ export function pageToUrl(page: string): string {
126
+ const cleanPage = page.replace(/\.mdx?$/, "");
127
+ return `/${cleanPage}`;
128
+ }
129
+
130
+ /**
131
+ * Formats a page path for display (e.g., "getting-started" → "Getting Started").
132
+ */
133
+ export function formatPageName(page: string): string {
134
+ const fileName = page.split("/").pop()?.replace(/\.mdx?$/, "") || page;
135
+ return fileName
136
+ .replace(/[-_]/g, " ")
137
+ .replace(/\b\w/g, (char) => char.toUpperCase());
138
+ }
139
+
140
+ /**
141
+ * Checks if a page path matches the current URL path.
142
+ */
143
+ export function isPageActive(page: string, currentPath: string): boolean {
144
+ const pageUrl = pageToUrl(page);
145
+ return currentPath === pageUrl || currentPath === `${pageUrl}/`;
146
+ }
147
+
148
+ /**
149
+ * Recursively flattens navigation page items into a single array.
150
+ */
151
+ function flattenPagesRecursive(items: readonly unknown[]): string[] {
152
+ const pages: string[] = [];
153
+ for (const item of items) {
154
+ if (typeof item === "string") {
155
+ pages.push(item);
156
+ } else if (isPageWithChildren(item)) {
157
+ pages.push(item.page);
158
+ pages.push(...item.children);
159
+ } else if (isNestedGroup(item)) {
160
+ pages.push(...flattenPagesRecursive(item.pages));
161
+ }
162
+ }
163
+ return pages;
164
+ }
165
+
166
+ /**
167
+ * Flattens all navigation groups into a single ordered array of page paths.
168
+ */
169
+ export function flattenNavigation(navigation: readonly NavigationGroup[]): string[] {
170
+ const pages: string[] = [];
171
+ for (const group of navigation) {
172
+ pages.push(...flattenPagesRecursive(group.pages));
173
+ }
174
+ return pages;
175
+ }
176
+
177
+ /**
178
+ * Gets all navigation slugs for static path generation.
179
+ * Returns paths with .md/.mdx extensions stripped.
180
+ */
181
+ export function getNavigationSlugs(navigation: readonly NavigationGroup[]): string[] {
182
+ return flattenNavigation(navigation).map((page) => page.replace(/\.mdx?$/, ""));
183
+ }
184
+
185
+ /**
186
+ * Gets previous and next page links for pagination.
187
+ */
188
+ export function getPageNavigation(
189
+ navigation: readonly NavigationGroup[],
190
+ currentPath: string
191
+ ): {
192
+ previous: { title: string; url: string } | null;
193
+ next: { title: string; url: string } | null;
194
+ } {
195
+ const allPages = flattenNavigation(navigation);
196
+ const currentIndex = allPages.findIndex((page) => {
197
+ const pageUrl = pageToUrl(page);
198
+ return currentPath === pageUrl || currentPath === `${pageUrl}/`;
199
+ });
200
+
201
+ let previous: { title: string; url: string } | null = null;
202
+ let next: { title: string; url: string } | null = null;
203
+
204
+ if (currentIndex > 0) {
205
+ const prevPagePath = allPages[currentIndex - 1];
206
+ previous = {
207
+ title: formatPageName(prevPagePath),
208
+ url: pageToUrl(prevPagePath),
209
+ };
210
+ }
211
+
212
+ if (currentIndex >= 0 && currentIndex < allPages.length - 1) {
213
+ const nextPagePath = allPages[currentIndex + 1];
214
+ next = {
215
+ title: formatPageName(nextPagePath),
216
+ url: pageToUrl(nextPagePath),
217
+ };
218
+ }
219
+
220
+ return { previous, next };
221
+ }
@@ -0,0 +1,115 @@
1
+ ---
2
+ /**
3
+ * [...slug] - Dynamic documentation page route
4
+ * Renders MDX content with custom components
5
+ */
6
+ import DocsLayout from "../layouts/DocsLayout.astro";
7
+ import { getProjectDir, loadConfig } from "../lib/config.js";
8
+ import { parseFrontmatter, extractHeadings } from "../lib/markdown.js";
9
+ import { loadMDX } from "../lib/mdx-loader.js";
10
+ import { getNavigationSlugs } from "../lib/utils";
11
+ import fs from "node:fs/promises";
12
+ import path from "node:path";
13
+
14
+ // MDX components
15
+ import Callout from "../components/Callout.astro";
16
+ import Card from "../components/Card.astro";
17
+ import CardGroup from "../components/CardGroup.astro";
18
+ import CodeGroup from "../components/CodeGroup.astro";
19
+ import Steps from "../components/Steps.astro";
20
+ import Tabs from "../components/Tabs.astro";
21
+ import Tab from "../components/Tab.astro";
22
+ import ApiPlayground from "../components/ApiPlayground.astro";
23
+
24
+ const components = {
25
+ Callout,
26
+ Card,
27
+ CardGroup,
28
+ CodeGroup,
29
+ Steps,
30
+ Tabs,
31
+ Tab,
32
+ ApiPlayground,
33
+ };
34
+
35
+ export async function getStaticPaths() {
36
+ const config = await loadConfig();
37
+ const slugs = getNavigationSlugs(config.navigation || []);
38
+ return slugs.map((slug) => ({ params: { slug } }));
39
+ }
40
+
41
+ const { slug } = Astro.params;
42
+ const projectDir = getProjectDir();
43
+
44
+ let title = slug || "Documentation";
45
+ let description = "";
46
+ let headings: { depth: number; slug: string; text: string }[] = [];
47
+ let Content: any = null;
48
+ let loadError = false;
49
+
50
+ try {
51
+ // Load the MDX module using dynamic imports via @content alias
52
+ const mdxModule = await loadMDX(slug || "");
53
+
54
+ if (mdxModule) {
55
+ Content = mdxModule.Content;
56
+ title = (mdxModule.frontmatter.title as string) || title;
57
+ description = (mdxModule.frontmatter.description as string) || "";
58
+
59
+ // Read the raw file to extract headings for table of contents
60
+ // Try .mdx first, then .md
61
+ const mdxPath = path.join(projectDir, `${slug}.mdx`);
62
+ const mdPath = path.join(projectDir, `${slug}.md`);
63
+
64
+ let filePath = mdxPath;
65
+ try {
66
+ await fs.access(mdxPath);
67
+ } catch {
68
+ // .mdx not found, fall back to .md extension
69
+ filePath = mdPath;
70
+ }
71
+
72
+ const rawContent = await fs.readFile(filePath, "utf-8");
73
+ const { body } = parseFrontmatter(rawContent);
74
+ headings = extractHeadings(body);
75
+ } else {
76
+ loadError = true;
77
+ }
78
+ } catch (error) {
79
+ console.error("Error loading page:", error);
80
+ loadError = true;
81
+ }
82
+ ---
83
+
84
+ <DocsLayout title={title} description={description} headings={headings}>
85
+ {loadError ? (
86
+ <article class="prose dark:prose-invert max-w-none">
87
+ <h1>Page Not Found</h1>
88
+ <p>The page "{slug}" could not be found.</p>
89
+ <div class="mt-6 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
90
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mt-0">What you can do:</h3>
91
+ <ul class="mt-2 space-y-2">
92
+ <li>
93
+ <a href="/" class="text-blue-600 hover:underline">Go to the homepage</a>
94
+ </li>
95
+ <li>
96
+ Use the search feature to find what you're looking for
97
+ </li>
98
+ <li>
99
+ Check the sidebar navigation for available pages
100
+ </li>
101
+ <li>
102
+ Verify the URL is spelled correctly
103
+ </li>
104
+ </ul>
105
+ </div>
106
+ <p class="mt-4 text-sm text-gray-500 dark:text-gray-400">
107
+ If you believe this page should exist, please check your <code class="bg-gray-100 dark:bg-gray-700 px-1 py-0.5 rounded text-sm">docs.json</code> configuration.
108
+ </p>
109
+ </article>
110
+ ) : (
111
+ <article class="prose dark:prose-invert max-w-none">
112
+ <Content components={components} />
113
+ </article>
114
+ )}
115
+ </DocsLayout>
@@ -0,0 +1,71 @@
1
+ ---
2
+ /**
3
+ * Index page - Redirects to first documentation page or shows welcome
4
+ */
5
+ import DocsLayout from "../layouts/DocsLayout.astro";
6
+ import { loadConfig } from "../lib/config.js";
7
+ import { pageToUrl } from "../lib/utils";
8
+
9
+ const config = await loadConfig();
10
+ const navigation = config.navigation || [];
11
+
12
+ // Find the first page from navigation to redirect to, or show welcome
13
+ const firstGroup = navigation[0];
14
+ const firstPage = firstGroup?.pages?.[0];
15
+ const redirectUrl = typeof firstPage === "string" ? pageToUrl(firstPage) : null;
16
+ ---
17
+
18
+ {redirectUrl ? (
19
+ <html>
20
+ <head>
21
+ <meta charset="utf-8" />
22
+ <meta http-equiv="refresh" content={`0;url=${redirectUrl}`} />
23
+ <link rel="canonical" href={redirectUrl} />
24
+ <script is:inline define:vars={{ redirectUrl }}>
25
+ window.location.replace(redirectUrl);
26
+ </script>
27
+ </head>
28
+ <body>
29
+ <p>Redirecting to <a href={redirectUrl}>{redirectUrl}</a>...</p>
30
+ </body>
31
+ </html>
32
+ ) : (
33
+
34
+ <DocsLayout title="Welcome" description="Welcome to your documentation site">
35
+ <h1>Welcome to {config.name || "Your Documentation"}</h1>
36
+ <p>
37
+ Your documentation site is ready. Add pages to your <code>docs.json</code>
38
+ navigation to get started.
39
+ </p>
40
+
41
+ <h2>Getting Started</h2>
42
+ <p>
43
+ Create MDX files in your project directory and add them to the{" "}
44
+ <code>navigation</code> array in your <code>docs.json</code> file.
45
+ </p>
46
+
47
+ <h3>Example docs.json</h3>
48
+ <pre><code>{`{
49
+ "name": "My Documentation",
50
+ "navigation": [
51
+ {
52
+ "group": "Getting Started",
53
+ "pages": ["introduction", "quickstart"]
54
+ }
55
+ ]
56
+ }`}</code></pre>
57
+
58
+ <h3>Example MDX file</h3>
59
+ <p>
60
+ Create an <code>introduction.mdx</code> file with frontmatter:
61
+ </p>
62
+ <pre><code>{`---
63
+ title: Introduction
64
+ description: Learn about our product
65
+ ---
66
+
67
+ # Introduction
68
+
69
+ Welcome to the documentation!`}</code></pre>
70
+ </DocsLayout>
71
+ )}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Mobile sidebar toggle functionality.
3
+ * Handles opening/closing the mobile navigation drawer.
4
+ */
5
+
6
+ function initMobileSidebar(): void {
7
+ const hamburger =
8
+ document.querySelector<HTMLButtonElement>("[data-hamburger]");
9
+ const mobileSidebar = document.querySelector<HTMLElement>(
10
+ "[data-mobile-sidebar]",
11
+ );
12
+ const backdrop = document.querySelector<HTMLElement>("[data-backdrop]");
13
+
14
+ if (!hamburger || !mobileSidebar || !backdrop) return;
15
+
16
+ function openSidebar(): void {
17
+ if (!mobileSidebar || !backdrop) return;
18
+ mobileSidebar.classList.remove("-translate-x-full");
19
+ mobileSidebar.classList.add("translate-x-0");
20
+ backdrop.classList.remove("opacity-0", "pointer-events-none");
21
+ backdrop.classList.add("opacity-100", "pointer-events-auto");
22
+ document.body.classList.add("overflow-hidden");
23
+ }
24
+
25
+ function closeSidebarFn(): void {
26
+ if (!mobileSidebar || !backdrop) return;
27
+ mobileSidebar.classList.add("-translate-x-full");
28
+ mobileSidebar.classList.remove("translate-x-0");
29
+ backdrop.classList.add("opacity-0", "pointer-events-none");
30
+ backdrop.classList.remove("opacity-100", "pointer-events-auto");
31
+ document.body.classList.remove("overflow-hidden");
32
+ }
33
+
34
+ // Open sidebar on hamburger click
35
+ hamburger.addEventListener("click", openSidebar);
36
+
37
+ // Close sidebar on backdrop click
38
+ backdrop.addEventListener("click", closeSidebarFn);
39
+
40
+ // Close sidebar on Escape key
41
+ document.addEventListener("keydown", (e: KeyboardEvent) => {
42
+ if (
43
+ e.key === "Escape" &&
44
+ mobileSidebar &&
45
+ !mobileSidebar.classList.contains("-translate-x-full")
46
+ ) {
47
+ closeSidebarFn();
48
+ }
49
+ });
50
+ }
51
+
52
+ // Initialize on page load
53
+ initMobileSidebar();
54
+
55
+ // Re-initialize after View Transitions (Astro navigation)
56
+ document.addEventListener("astro:after-swap", initMobileSidebar);
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Theme initialization script.
3
+ * This script should be inlined in the <head> to prevent flash of wrong theme.
4
+ * It reads the user's preference from localStorage or system preference.
5
+ */
6
+
7
+ // Immediately invoked to run before paint
8
+ (function () {
9
+ // Check localStorage first
10
+ const storedTheme = localStorage.getItem("theme");
11
+
12
+ if (storedTheme === "dark") {
13
+ document.documentElement.classList.add("dark");
14
+ } else if (storedTheme === "light") {
15
+ document.documentElement.classList.remove("dark");
16
+ } else {
17
+ // No stored preference - check system preference
18
+ const prefersDark = window.matchMedia(
19
+ "(prefers-color-scheme: dark)",
20
+ ).matches;
21
+ if (prefersDark) {
22
+ document.documentElement.classList.add("dark");
23
+ }
24
+ }
25
+ })();