@brainfish-ai/devdoc 0.1.43 → 0.1.45
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/dist/cli/commands/create.js +2 -2
- package/package.json +1 -1
- package/renderer/app/api/collections/route.js +35 -4
- package/renderer/app/api/docs/route.js +9 -4
- package/renderer/app/api/suggestions/route.js +33 -13
- package/renderer/app/globals.css +69 -0
- package/renderer/app/layout.js +2 -2
- package/renderer/app/llms-full.txt/route.js +10 -1
- package/renderer/app/llms.txt/route.js +10 -1
- package/renderer/app/sitemap.xml/route.js +11 -1
- package/renderer/components/docs/mdx/cards.js +1 -1
- package/renderer/components/docs/mdx/landing.js +7 -5
- package/renderer/components/docs-viewer/agent/agent-chat.js +13 -112
- package/renderer/components/docs-viewer/agent/agent-popup-button.js +99 -0
- package/renderer/components/docs-viewer/agent/index.js +3 -0
- package/renderer/components/docs-viewer/content/content-router.js +182 -0
- package/renderer/components/docs-viewer/content/doc-page.js +73 -37
- package/renderer/components/docs-viewer/content/index.js +2 -0
- package/renderer/components/docs-viewer/content/mdx-error-boundary.js +184 -0
- package/renderer/components/docs-viewer/index.js +381 -485
- package/renderer/components/docs-viewer/playground/graphql-playground.js +205 -3
- package/renderer/components/docs-viewer/sidebar/right-sidebar.js +35 -39
- package/renderer/components/theme-toggle.js +1 -21
- package/renderer/hooks/use-route-state.js +159 -0
- package/renderer/lib/api-docs/agent/use-suggestions.js +97 -0
- package/renderer/lib/api-docs/code-editor/mode-context.js +61 -89
- package/renderer/lib/api-docs/mobile-context.js +40 -3
- package/renderer/lib/docs/config/environment.js +38 -0
- package/renderer/lib/docs/config/index.js +1 -0
- package/renderer/lib/docs/config/schema.js +17 -5
- package/renderer/lib/docs/mdx/compiler.js +5 -2
- package/renderer/lib/docs/mdx/index.js +2 -0
- package/renderer/lib/docs/mdx/remark-mermaid.js +63 -0
- package/renderer/lib/docs/navigation/index.js +1 -2
- package/renderer/lib/docs/navigation/types.js +3 -1
- package/renderer/lib/docs-navigation.js +140 -0
- package/renderer/package.json +1 -0
|
@@ -1,114 +1,86 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import React, { createContext, useContext, useState, useCallback, useEffect,
|
|
3
|
+
import React, { createContext, useContext, useState, useCallback, useEffect, useMemo } from 'react';
|
|
4
|
+
import { switchView } from '@/lib/docs-navigation';
|
|
4
5
|
const ModeContext = /*#__PURE__*/ createContext(null);
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
;
|
|
13
|
-
if (hash
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
};
|
|
6
|
+
/**
|
|
7
|
+
* Parse view mode from URL hash
|
|
8
|
+
*/ function parseViewFromHash() {
|
|
9
|
+
if (typeof window === 'undefined') return 'docs';
|
|
10
|
+
const hash = window.location.hash.slice(1);
|
|
11
|
+
if (!hash) return 'docs';
|
|
12
|
+
// Check for view suffix at the end
|
|
13
|
+
if (hash.endsWith('/playground')) return 'playground';
|
|
14
|
+
if (hash.endsWith('/notes')) return 'notes';
|
|
15
|
+
return 'docs';
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Convert ViewMode to PlaygroundMode
|
|
19
|
+
*/ function viewToPlaygroundMode(view) {
|
|
20
|
+
switch(view){
|
|
21
|
+
case 'playground':
|
|
22
|
+
return 'api_client';
|
|
23
|
+
case 'notes':
|
|
24
|
+
return 'notes';
|
|
25
|
+
default:
|
|
26
|
+
return 'docs';
|
|
27
27
|
}
|
|
28
|
-
return {
|
|
29
|
-
isNotes: false,
|
|
30
|
-
filePath: null
|
|
31
|
-
};
|
|
32
28
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Convert PlaygroundMode to ViewMode
|
|
31
|
+
*/ function playgroundModeToView(mode) {
|
|
32
|
+
switch(mode){
|
|
33
|
+
case 'api_client':
|
|
34
|
+
return 'playground';
|
|
35
|
+
case 'notes':
|
|
36
|
+
return 'notes';
|
|
37
|
+
default:
|
|
38
|
+
return 'docs';
|
|
39
|
+
}
|
|
39
40
|
}
|
|
40
41
|
export function ModeProvider({ children, defaultMode = 'docs' }) {
|
|
41
|
-
|
|
42
|
+
// Track current hash to derive mode
|
|
43
|
+
const [hash, setHash] = useState(()=>typeof window !== 'undefined' ? window.location.hash : '');
|
|
44
|
+
// Derive mode from URL hash
|
|
45
|
+
const mode = useMemo(()=>{
|
|
46
|
+
const view = parseViewFromHash();
|
|
47
|
+
return viewToPlaygroundMode(view);
|
|
48
|
+
}, [
|
|
49
|
+
hash
|
|
50
|
+
]);
|
|
51
|
+
// Notes-specific state (not URL-based yet)
|
|
42
52
|
const [activeFilePath, setActiveFilePathState] = useState(null);
|
|
43
53
|
const [streamingContent, setStreamingContent] = useState(null);
|
|
44
54
|
const [notesRefreshTrigger, setNotesRefreshTrigger] = useState(0);
|
|
45
|
-
//
|
|
46
|
-
const modeRef = useRef(defaultMode);
|
|
47
|
-
// Keep ref in sync with state
|
|
48
|
-
useEffect(()=>{
|
|
49
|
-
modeRef.current = mode;
|
|
50
|
-
}, [
|
|
51
|
-
mode
|
|
52
|
-
]);
|
|
53
|
-
// Initialize from URL hash on mount
|
|
54
|
-
useEffect(()=>{
|
|
55
|
-
const { isNotes, filePath } = parseNotesHash();
|
|
56
|
-
if (isNotes) {
|
|
57
|
-
setMode('notes');
|
|
58
|
-
modeRef.current = 'notes';
|
|
59
|
-
setActiveFilePathState(filePath);
|
|
60
|
-
}
|
|
61
|
-
}, []);
|
|
62
|
-
// Handle browser back/forward navigation
|
|
55
|
+
// Listen for hash changes to update mode
|
|
63
56
|
useEffect(()=>{
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
setMode('docs');
|
|
73
|
-
modeRef.current = 'docs';
|
|
74
|
-
}
|
|
57
|
+
const handleHashChange = ()=>{
|
|
58
|
+
setHash(window.location.hash);
|
|
59
|
+
};
|
|
60
|
+
window.addEventListener('hashchange', handleHashChange);
|
|
61
|
+
window.addEventListener('popstate', handleHashChange);
|
|
62
|
+
return ()=>{
|
|
63
|
+
window.removeEventListener('hashchange', handleHashChange);
|
|
64
|
+
window.removeEventListener('popstate', handleHashChange);
|
|
75
65
|
};
|
|
76
|
-
window.addEventListener('popstate', handlePopState);
|
|
77
|
-
return ()=>window.removeEventListener('popstate', handlePopState);
|
|
78
66
|
}, []);
|
|
79
|
-
// Wrapper for setActiveFilePath
|
|
67
|
+
// Wrapper for setActiveFilePath
|
|
80
68
|
const setActiveFilePath = useCallback((path)=>{
|
|
81
69
|
// Handle the ?t= timestamp suffix for forcing reloads
|
|
82
70
|
const cleanPath = path?.split('?')[0] || null;
|
|
83
71
|
setActiveFilePathState(cleanPath);
|
|
84
|
-
|
|
85
|
-
if (modeRef.current === 'notes') {
|
|
86
|
-
updateNotesHash(cleanPath);
|
|
87
|
-
}
|
|
88
|
-
}, []) // No dependencies - uses ref
|
|
89
|
-
;
|
|
72
|
+
}, []);
|
|
90
73
|
const switchToNotes = useCallback((openFile)=>{
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
updateNotesHash(filePath);
|
|
74
|
+
if (openFile) {
|
|
75
|
+
setActiveFilePathState(openFile);
|
|
76
|
+
}
|
|
77
|
+
switchView('notes');
|
|
96
78
|
}, []);
|
|
97
79
|
const switchToApiClient = useCallback(()=>{
|
|
98
|
-
|
|
99
|
-
modeRef.current = 'api_client';
|
|
100
|
-
// Don't update URL here - let the main API docs component handle it
|
|
80
|
+
switchView('playground');
|
|
101
81
|
}, []);
|
|
102
82
|
const switchToDocs = useCallback(()=>{
|
|
103
|
-
|
|
104
|
-
modeRef.current = 'docs';
|
|
105
|
-
// Only clear the hash if it's a notes hash, preserve doc/endpoint hashes
|
|
106
|
-
if (typeof window !== 'undefined') {
|
|
107
|
-
const hash = window.location.hash.slice(1);
|
|
108
|
-
if (hash === 'notes' || hash.startsWith('notes/')) {
|
|
109
|
-
window.history.pushState(null, '', window.location.pathname + window.location.search);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
83
|
+
switchView('docs');
|
|
112
84
|
}, []);
|
|
113
85
|
const appendStreamingContent = useCallback((delta)=>{
|
|
114
86
|
setStreamingContent((prev)=>{
|
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { createContext, useContext, useState, useCallback, useEffect } from 'react';
|
|
4
|
+
const AGENT_ASSIST_STORAGE_KEY = 'devdoc-agent-assist-open';
|
|
4
5
|
const MobileContext = /*#__PURE__*/ createContext(null);
|
|
6
|
+
// Helper to get initial state from localStorage (default to true)
|
|
7
|
+
function getInitialAgentAssistState() {
|
|
8
|
+
if (typeof window === 'undefined') return true;
|
|
9
|
+
const stored = localStorage.getItem(AGENT_ASSIST_STORAGE_KEY);
|
|
10
|
+
// Default to true (open) if not set
|
|
11
|
+
return stored === null ? true : stored === 'true';
|
|
12
|
+
}
|
|
5
13
|
export function MobileProvider({ children }) {
|
|
6
14
|
const [isLeftSidebarOpen, setIsLeftSidebarOpen] = useState(false);
|
|
7
15
|
const [isRightSidebarOpen, setIsRightSidebarOpen] = useState(false);
|
|
8
16
|
const [isMobile, setIsMobile] = useState(false);
|
|
17
|
+
const [isHydrated, setIsHydrated] = useState(false);
|
|
18
|
+
// Initialize from localStorage after hydration (only on desktop)
|
|
19
|
+
useEffect(()=>{
|
|
20
|
+
const isMobileView = window.innerWidth < 1024;
|
|
21
|
+
if (!isMobileView) {
|
|
22
|
+
const savedState = getInitialAgentAssistState();
|
|
23
|
+
setIsRightSidebarOpen(savedState);
|
|
24
|
+
}
|
|
25
|
+
setIsHydrated(true);
|
|
26
|
+
}, []);
|
|
9
27
|
// Detect mobile on mount and window resize
|
|
10
28
|
useEffect(()=>{
|
|
11
29
|
const checkMobile = ()=>{
|
|
@@ -17,11 +35,11 @@ export function MobileProvider({ children }) {
|
|
|
17
35
|
window.addEventListener('resize', checkMobile);
|
|
18
36
|
return ()=>window.removeEventListener('resize', checkMobile);
|
|
19
37
|
}, []);
|
|
20
|
-
// Close
|
|
38
|
+
// Close left sidebar when switching to desktop (left sidebar is mobile-only)
|
|
39
|
+
// Note: Right sidebar can stay open on desktop as it's now a toggleable panel
|
|
21
40
|
useEffect(()=>{
|
|
22
41
|
if (!isMobile) {
|
|
23
42
|
setIsLeftSidebarOpen(false);
|
|
24
|
-
setIsRightSidebarOpen(false);
|
|
25
43
|
}
|
|
26
44
|
}, [
|
|
27
45
|
isMobile
|
|
@@ -32,7 +50,14 @@ export function MobileProvider({ children }) {
|
|
|
32
50
|
setIsRightSidebarOpen(false);
|
|
33
51
|
}, []);
|
|
34
52
|
const toggleRightSidebar = useCallback(()=>{
|
|
35
|
-
setIsRightSidebarOpen((prev)
|
|
53
|
+
setIsRightSidebarOpen((prev)=>{
|
|
54
|
+
const newState = !prev;
|
|
55
|
+
// Persist to localStorage (only on desktop)
|
|
56
|
+
if (typeof window !== 'undefined' && window.innerWidth >= 1024) {
|
|
57
|
+
localStorage.setItem(AGENT_ASSIST_STORAGE_KEY, String(newState));
|
|
58
|
+
}
|
|
59
|
+
return newState;
|
|
60
|
+
});
|
|
36
61
|
// Close left sidebar when opening right
|
|
37
62
|
setIsLeftSidebarOpen(false);
|
|
38
63
|
}, []);
|
|
@@ -46,13 +71,25 @@ export function MobileProvider({ children }) {
|
|
|
46
71
|
const openRightSidebar = useCallback(()=>{
|
|
47
72
|
setIsRightSidebarOpen(true);
|
|
48
73
|
setIsLeftSidebarOpen(false);
|
|
74
|
+
// Persist to localStorage (only on desktop)
|
|
75
|
+
if (typeof window !== 'undefined' && window.innerWidth >= 1024) {
|
|
76
|
+
localStorage.setItem(AGENT_ASSIST_STORAGE_KEY, 'true');
|
|
77
|
+
}
|
|
49
78
|
}, []);
|
|
50
79
|
const closeRightSidebar = useCallback(()=>{
|
|
51
80
|
setIsRightSidebarOpen(false);
|
|
81
|
+
// Persist to localStorage (only on desktop)
|
|
82
|
+
if (typeof window !== 'undefined' && window.innerWidth >= 1024) {
|
|
83
|
+
localStorage.setItem(AGENT_ASSIST_STORAGE_KEY, 'false');
|
|
84
|
+
}
|
|
52
85
|
}, []);
|
|
53
86
|
const closeAllSidebars = useCallback(()=>{
|
|
54
87
|
setIsLeftSidebarOpen(false);
|
|
55
88
|
setIsRightSidebarOpen(false);
|
|
89
|
+
// Persist to localStorage (only on desktop)
|
|
90
|
+
if (typeof window !== 'undefined' && window.innerWidth >= 1024) {
|
|
91
|
+
localStorage.setItem(AGENT_ASSIST_STORAGE_KEY, 'false');
|
|
92
|
+
}
|
|
56
93
|
}, []);
|
|
57
94
|
return /*#__PURE__*/ _jsx(MobileContext.Provider, {
|
|
58
95
|
value: {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment Detection Utilities
|
|
3
|
+
*
|
|
4
|
+
* Utilities for detecting development vs production environment
|
|
5
|
+
* Used for visibility filtering of private documentation content
|
|
6
|
+
*/ /**
|
|
7
|
+
* Check if the application is running in development mode
|
|
8
|
+
*
|
|
9
|
+
* Development mode is determined by:
|
|
10
|
+
* 1. NODE_ENV !== 'production'
|
|
11
|
+
* 2. DEVDOC_DEV_MODE env var is set to 'true' (set by devdoc dev command)
|
|
12
|
+
*
|
|
13
|
+
* @returns true if in development mode, false if in production
|
|
14
|
+
*/ export function isDevMode() {
|
|
15
|
+
return process.env.NODE_ENV !== 'production' || process.env.DEVDOC_DEV_MODE === 'true';
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Check if the application is running in production mode
|
|
19
|
+
*
|
|
20
|
+
* @returns true if in production mode, false if in development
|
|
21
|
+
*/ export function isProductionMode() {
|
|
22
|
+
return !isDevMode();
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Check if an item should be visible based on its visibility setting
|
|
26
|
+
*
|
|
27
|
+
* @param visibility - The visibility setting ('public', 'private', or undefined)
|
|
28
|
+
* @param devMode - Whether we're in development mode (defaults to isDevMode())
|
|
29
|
+
* @returns true if the item should be shown, false if it should be hidden
|
|
30
|
+
*/ export function shouldShowItem(visibility, devMode) {
|
|
31
|
+
const isDev = devMode ?? isDevMode();
|
|
32
|
+
// Private items are only visible in dev mode
|
|
33
|
+
if (visibility === 'private') {
|
|
34
|
+
return isDev;
|
|
35
|
+
}
|
|
36
|
+
// Public or undefined visibility = always show
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
@@ -3,3 +3,4 @@
|
|
|
3
3
|
*/ export { docsConfigSchema, parseDocsConfig, safeParseDocsConfig, getDefaultDocsConfig } from './schema';
|
|
4
4
|
export { domainConfigSchema, parseDomainConfig, safeParseDomainConfig, isValidDomain, normalizeDomain, getDnsInstructions } from './domain-schema';
|
|
5
5
|
export { loadDocsConfig, safeLoadDocsConfig, clearConfigCache, hasDocsConfig, getContentDir, resolvePagePath, loadPageContent, listMdxFiles } from './loader';
|
|
6
|
+
export { isDevMode, isProductionMode, shouldShowItem } from './environment';
|
|
@@ -15,6 +15,13 @@ const navLinkSchema = z.object({
|
|
|
15
15
|
label: z.string(),
|
|
16
16
|
href: z.string()
|
|
17
17
|
});
|
|
18
|
+
// Visibility schema for tabs, groups, and pages
|
|
19
|
+
// - 'public': Always visible (default)
|
|
20
|
+
// - 'private': Only visible in development mode, hidden in production
|
|
21
|
+
const visibilitySchema = z.enum([
|
|
22
|
+
'public',
|
|
23
|
+
'private'
|
|
24
|
+
]).optional();
|
|
18
25
|
// Page can be a string or a nested group
|
|
19
26
|
const pageRefSchema = z.lazy(()=>z.union([
|
|
20
27
|
z.string(),
|
|
@@ -29,7 +36,8 @@ const pageRefSchema = z.lazy(()=>z.union([
|
|
|
29
36
|
const groupSchema = z.object({
|
|
30
37
|
group: z.string(),
|
|
31
38
|
pages: z.array(pageRefSchema),
|
|
32
|
-
icon: z.string().optional()
|
|
39
|
+
icon: z.string().optional(),
|
|
40
|
+
visibility: visibilitySchema
|
|
33
41
|
});
|
|
34
42
|
// OpenAPI version schema
|
|
35
43
|
const openapiVersionSchema = z.object({
|
|
@@ -41,7 +49,8 @@ const openapiVersionSchema = z.object({
|
|
|
41
49
|
const docsTabSchema = z.object({
|
|
42
50
|
tab: z.string(),
|
|
43
51
|
type: z.literal('docs').optional(),
|
|
44
|
-
groups: z.array(groupSchema)
|
|
52
|
+
groups: z.array(groupSchema),
|
|
53
|
+
visibility: visibilitySchema
|
|
45
54
|
});
|
|
46
55
|
const openapiTabSchema = z.object({
|
|
47
56
|
tab: z.string(),
|
|
@@ -49,7 +58,8 @@ const openapiTabSchema = z.object({
|
|
|
49
58
|
path: z.string().optional(),
|
|
50
59
|
versions: z.array(openapiVersionSchema).optional(),
|
|
51
60
|
spec: z.string().optional(),
|
|
52
|
-
groups: z.array(groupSchema).optional()
|
|
61
|
+
groups: z.array(groupSchema).optional(),
|
|
62
|
+
visibility: visibilitySchema
|
|
53
63
|
});
|
|
54
64
|
const graphqlTabSchema = z.object({
|
|
55
65
|
tab: z.string(),
|
|
@@ -57,12 +67,14 @@ const graphqlTabSchema = z.object({
|
|
|
57
67
|
path: z.string().optional(),
|
|
58
68
|
schema: z.string(),
|
|
59
69
|
endpoint: z.string().optional(),
|
|
60
|
-
groups: z.array(groupSchema).optional()
|
|
70
|
+
groups: z.array(groupSchema).optional(),
|
|
71
|
+
visibility: visibilitySchema
|
|
61
72
|
});
|
|
62
73
|
const changelogTabSchema = z.object({
|
|
63
74
|
tab: z.string(),
|
|
64
75
|
type: z.literal('changelog'),
|
|
65
|
-
path: z.string().optional()
|
|
76
|
+
path: z.string().optional(),
|
|
77
|
+
visibility: visibilitySchema
|
|
66
78
|
});
|
|
67
79
|
const tabSchema = z.union([
|
|
68
80
|
docsTabSchema,
|
|
@@ -4,6 +4,7 @@ import rehypeSlug from 'rehype-slug';
|
|
|
4
4
|
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
|
|
5
5
|
import remarkGfm from 'remark-gfm';
|
|
6
6
|
import { parseFrontmatter, safeParseFrontmatter } from './frontmatter';
|
|
7
|
+
import { remarkMermaid } from './remark-mermaid';
|
|
7
8
|
/**
|
|
8
9
|
* Extract frontmatter from raw MDX content
|
|
9
10
|
*/ export function extractFrontmatter(source) {
|
|
@@ -50,7 +51,8 @@ import { parseFrontmatter, safeParseFrontmatter } from './frontmatter';
|
|
|
50
51
|
parseFrontmatter: false,
|
|
51
52
|
mdxOptions: {
|
|
52
53
|
remarkPlugins: [
|
|
53
|
-
remarkGfm
|
|
54
|
+
remarkGfm,
|
|
55
|
+
remarkMermaid
|
|
54
56
|
],
|
|
55
57
|
rehypePlugins: [
|
|
56
58
|
rehypeSlug,
|
|
@@ -85,7 +87,8 @@ import { parseFrontmatter, safeParseFrontmatter } from './frontmatter';
|
|
|
85
87
|
parseFrontmatter: false,
|
|
86
88
|
mdxOptions: {
|
|
87
89
|
remarkPlugins: [
|
|
88
|
-
remarkGfm
|
|
90
|
+
remarkGfm,
|
|
91
|
+
remarkMermaid
|
|
89
92
|
],
|
|
90
93
|
rehypePlugins: [
|
|
91
94
|
rehypeSlug
|
|
@@ -6,3 +6,5 @@
|
|
|
6
6
|
export { compileMDXContent, quickCompileMDX, validateMDXContent, extractFrontmatter, extractHeadings } from './compiler';
|
|
7
7
|
// Frontmatter
|
|
8
8
|
export { frontmatterSchema, parseFrontmatter, safeParseFrontmatter, getDefaultFrontmatter } from './frontmatter';
|
|
9
|
+
// Remark plugins
|
|
10
|
+
export { remarkMermaid } from './remark-mermaid';
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { visit } from 'unist-util-visit';
|
|
2
|
+
/**
|
|
3
|
+
* Remark plugin to transform mermaid code blocks into Mermaid components
|
|
4
|
+
*
|
|
5
|
+
* Transforms:
|
|
6
|
+
* ```mermaid
|
|
7
|
+
* flowchart LR
|
|
8
|
+
* A --> B
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* Into:
|
|
12
|
+
* <Mermaid>
|
|
13
|
+
* {`flowchart LR
|
|
14
|
+
* A --> B`}
|
|
15
|
+
* </Mermaid>
|
|
16
|
+
*/ export function remarkMermaid() {
|
|
17
|
+
return (tree)=>{
|
|
18
|
+
visit(tree, 'code', (node, index, parent)=>{
|
|
19
|
+
if (node.lang !== 'mermaid' || !parent || index === undefined) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
// Replace the code block with MDX JSX element
|
|
23
|
+
const mermaidNode = {
|
|
24
|
+
type: 'mdxJsxFlowElement',
|
|
25
|
+
name: 'Mermaid',
|
|
26
|
+
attributes: [],
|
|
27
|
+
children: [
|
|
28
|
+
{
|
|
29
|
+
type: 'mdxFlowExpression',
|
|
30
|
+
value: '`' + node.value + '`',
|
|
31
|
+
data: {
|
|
32
|
+
estree: {
|
|
33
|
+
type: 'Program',
|
|
34
|
+
body: [
|
|
35
|
+
{
|
|
36
|
+
type: 'ExpressionStatement',
|
|
37
|
+
expression: {
|
|
38
|
+
type: 'TemplateLiteral',
|
|
39
|
+
expressions: [],
|
|
40
|
+
quasis: [
|
|
41
|
+
{
|
|
42
|
+
type: 'TemplateElement',
|
|
43
|
+
value: {
|
|
44
|
+
raw: node.value,
|
|
45
|
+
cooked: node.value
|
|
46
|
+
},
|
|
47
|
+
tail: true
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
sourceType: 'module'
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
};
|
|
59
|
+
// Replace the code node with the Mermaid component
|
|
60
|
+
parent.children.splice(index, 1, mermaidNode);
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Navigation Module Exports
|
|
3
|
-
*/ export
|
|
4
|
-
export { generateNavigation, findPageNavContext, getAllPagePaths, loadPageMeta } from './generator';
|
|
3
|
+
*/ export { generateNavigation, findPageNavContext, getAllPagePaths, loadPageMeta } from './generator';
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Types for the documentation navigation system
|
|
5
5
|
*/ /**
|
|
6
|
-
*
|
|
6
|
+
* Visibility type for navigation items
|
|
7
|
+
* - 'public': Always visible (default)
|
|
8
|
+
* - 'private': Only visible in development mode, hidden in production
|
|
7
9
|
*/ /**
|
|
8
10
|
* Page metadata for navigation
|
|
9
11
|
*/ export { };
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navigation utilities for docs viewer
|
|
3
|
+
*
|
|
4
|
+
* These functions provide simple URL updates without state management.
|
|
5
|
+
* The URL is the single source of truth - components derive their state from the URL.
|
|
6
|
+
*
|
|
7
|
+
* URL Schema:
|
|
8
|
+
* - #tab/page/{slug} → Doc page (docs view)
|
|
9
|
+
* - #tab/endpoint/{id} → API endpoint (docs view)
|
|
10
|
+
* - #tab/endpoint/{id}/playground → API endpoint (playground view)
|
|
11
|
+
* - #tab/endpoint/{id}/notes → API endpoint (notes view)
|
|
12
|
+
* - #tab/page/{slug}/notes → Doc page (notes view)
|
|
13
|
+
*/ /**
|
|
14
|
+
* Navigate to a documentation page
|
|
15
|
+
* @param tab - Tab ID (e.g., 'guides', 'graphql-api')
|
|
16
|
+
* @param slug - Page slug (e.g., 'graphql/introduction')
|
|
17
|
+
* @param view - View mode (docs, playground, notes). Defaults to 'docs'
|
|
18
|
+
*/ export function navigateToPage(tab, slug, view) {
|
|
19
|
+
const viewSuffix = view && view !== 'docs' ? `/${view}` : '';
|
|
20
|
+
window.location.hash = `${tab}/page/${slug}${viewSuffix}`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Navigate to an API endpoint or GraphQL operation
|
|
24
|
+
* @param tab - Tab ID (e.g., 'api-reference', 'graphql-api')
|
|
25
|
+
* @param id - Endpoint ID
|
|
26
|
+
* @param view - View mode (docs, playground, notes). Defaults to 'docs'
|
|
27
|
+
*/ export function navigateToEndpoint(tab, id, view) {
|
|
28
|
+
const viewSuffix = view && view !== 'docs' ? `/${view}` : '';
|
|
29
|
+
window.location.hash = `${tab}/endpoint/${id}${viewSuffix}`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Navigate to a section within a page (scroll to element)
|
|
33
|
+
* @param tab - Tab ID
|
|
34
|
+
* @param sectionId - Section/heading ID to scroll to
|
|
35
|
+
*/ export function navigateToSection(tab, sectionId) {
|
|
36
|
+
window.location.hash = `${tab}/section/${sectionId}`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Navigate to a tab (show default content for that tab)
|
|
40
|
+
* @param tab - Tab ID
|
|
41
|
+
*/ export function navigateToTab(tab) {
|
|
42
|
+
window.location.hash = tab;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Switch view mode while keeping the current content
|
|
46
|
+
* @param view - View mode to switch to
|
|
47
|
+
*/ export function switchView(view) {
|
|
48
|
+
const hash = window.location.hash.slice(1);
|
|
49
|
+
if (!hash) return;
|
|
50
|
+
// Remove existing view suffix if present
|
|
51
|
+
const cleanHash = hash.replace(/\/(playground|notes)$/, '');
|
|
52
|
+
// Add new view suffix (unless switching to docs, which is the default)
|
|
53
|
+
const viewSuffix = view !== 'docs' ? `/${view}` : '';
|
|
54
|
+
window.location.hash = `${cleanHash}${viewSuffix}`;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Clear current selection, optionally staying on a tab
|
|
58
|
+
* @param tab - Optional tab to stay on
|
|
59
|
+
*/ export function clearSelection(tab) {
|
|
60
|
+
if (tab) {
|
|
61
|
+
window.location.hash = tab;
|
|
62
|
+
} else {
|
|
63
|
+
// Remove hash entirely using pushState to avoid scroll jump
|
|
64
|
+
window.history.pushState(null, '', window.location.pathname + window.location.search);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Update URL without triggering navigation (for history management)
|
|
69
|
+
* @param hash - Hash value (without #)
|
|
70
|
+
*/ export function updateUrlSilently(hash) {
|
|
71
|
+
window.history.replaceState(null, '', `#${hash}`);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Push a new URL to history (for back/forward navigation)
|
|
75
|
+
* @param hash - Hash value (without #)
|
|
76
|
+
*/ export function pushUrl(hash) {
|
|
77
|
+
window.history.pushState(null, '', `#${hash}`);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Build hash URL strings (for href attributes)
|
|
81
|
+
*/ export const hashUrls = {
|
|
82
|
+
page: (tab, slug, view)=>{
|
|
83
|
+
const viewSuffix = view && view !== 'docs' ? `/${view}` : '';
|
|
84
|
+
return `#${tab}/page/${slug}${viewSuffix}`;
|
|
85
|
+
},
|
|
86
|
+
endpoint: (tab, id, view)=>{
|
|
87
|
+
const viewSuffix = view && view !== 'docs' ? `/${view}` : '';
|
|
88
|
+
return `#${tab}/endpoint/${id}${viewSuffix}`;
|
|
89
|
+
},
|
|
90
|
+
section: (tab, sectionId)=>`#${tab}/section/${sectionId}`,
|
|
91
|
+
tab: (tab)=>`#${tab}`
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Parse a hash URL into its components
|
|
95
|
+
* @param hash - Hash value (with or without #)
|
|
96
|
+
* @returns Parsed components
|
|
97
|
+
*/ export function parseHashUrl(hash) {
|
|
98
|
+
const cleanHash = hash.startsWith('#') ? hash.slice(1) : hash;
|
|
99
|
+
if (!cleanHash) {
|
|
100
|
+
return {
|
|
101
|
+
tab: null,
|
|
102
|
+
type: null,
|
|
103
|
+
id: null,
|
|
104
|
+
view: 'docs'
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const parts = cleanHash.split('/');
|
|
108
|
+
const tab = parts[0] || null;
|
|
109
|
+
const type = parts[1];
|
|
110
|
+
// Check if last part is a view mode
|
|
111
|
+
const lastPart = parts[parts.length - 1];
|
|
112
|
+
const isViewSuffix = lastPart === 'playground' || lastPart === 'notes';
|
|
113
|
+
const view = isViewSuffix ? lastPart : 'docs';
|
|
114
|
+
// Get ID (everything between type and view, or type and end)
|
|
115
|
+
const idParts = isViewSuffix ? parts.slice(2, -1) : parts.slice(2);
|
|
116
|
+
const id = idParts.join('/') || null;
|
|
117
|
+
// Validate type
|
|
118
|
+
const validTypes = [
|
|
119
|
+
'page',
|
|
120
|
+
'endpoint',
|
|
121
|
+
'section'
|
|
122
|
+
];
|
|
123
|
+
const validatedType = type && validTypes.includes(type) ? type : null;
|
|
124
|
+
return {
|
|
125
|
+
tab,
|
|
126
|
+
type: validatedType,
|
|
127
|
+
id,
|
|
128
|
+
view
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Check if two hash URLs point to the same content (ignoring view mode)
|
|
133
|
+
* @param hash1 - First hash
|
|
134
|
+
* @param hash2 - Second hash
|
|
135
|
+
* @returns True if they point to the same content
|
|
136
|
+
*/ export function isSameContent(hash1, hash2) {
|
|
137
|
+
const parsed1 = parseHashUrl(hash1);
|
|
138
|
+
const parsed2 = parseHashUrl(hash2);
|
|
139
|
+
return parsed1.tab === parsed2.tab && parsed1.type === parsed2.type && parsed1.id === parsed2.id;
|
|
140
|
+
}
|