@brainfish-ai/devdoc 0.1.48 → 0.1.49
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/deploy.js +16 -11
- package/package.json +1 -1
- package/renderer/app/[...slug]/client.js +17 -0
- package/renderer/app/[...slug]/page.js +125 -0
- package/renderer/app/api/assets/[...path]/route.js +23 -4
- package/renderer/app/api/chat/route.js +188 -25
- package/renderer/app/api/collections/route.js +95 -2
- package/renderer/app/api/deploy/route.js +4 -0
- package/renderer/app/api/suggestions/route.js +98 -10
- package/renderer/app/globals.css +33 -0
- package/renderer/app/layout.js +83 -8
- package/renderer/components/docs/mdx/cards.js +16 -45
- package/renderer/components/docs/mdx/file-tree.js +102 -0
- package/renderer/components/docs/mdx/index.js +7 -0
- package/renderer/components/docs-viewer/agent/agent-chat.js +75 -11
- package/renderer/components/docs-viewer/agent/messages/assistant-message.js +67 -3
- package/renderer/components/docs-viewer/agent/messages/tool-call-display.js +49 -4
- package/renderer/components/docs-viewer/content/content-router.js +1 -1
- package/renderer/components/docs-viewer/content/doc-page.js +36 -28
- package/renderer/components/docs-viewer/index.js +223 -58
- package/renderer/components/docs-viewer/playground/graphql-playground.js +131 -33
- package/renderer/components/docs-viewer/shared/method-badge.js +11 -2
- package/renderer/components/docs-viewer/sidebar/collection-tree.js +44 -6
- package/renderer/components/docs-viewer/sidebar/index.js +2 -1
- package/renderer/components/docs-viewer/sidebar/right-sidebar.js +3 -1
- package/renderer/components/docs-viewer/sidebar/sidebar-item.js +5 -7
- package/renderer/hooks/use-route-state.js +44 -56
- package/renderer/lib/api-docs/agent/indexer.js +73 -12
- package/renderer/lib/api-docs/agent/use-suggestions.js +26 -16
- package/renderer/lib/api-docs/code-editor/mode-context.js +16 -18
- package/renderer/lib/api-docs/parsers/openapi/transformer.js +8 -1
- package/renderer/lib/cache/purge.js +98 -0
- package/renderer/lib/docs-link-utils.js +146 -0
- package/renderer/lib/docs-navigation-context.js +3 -2
- package/renderer/lib/docs-navigation.js +50 -41
- package/renderer/lib/rate-limit.js +203 -0
|
@@ -147,25 +147,84 @@
|
|
|
147
147
|
}
|
|
148
148
|
return results;
|
|
149
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Detect if a request is a GraphQL operation based on its tags
|
|
152
|
+
*/ function isGraphQLOperation(tags) {
|
|
153
|
+
const graphqlTags = [
|
|
154
|
+
'query',
|
|
155
|
+
'mutation',
|
|
156
|
+
'subscription',
|
|
157
|
+
'queries',
|
|
158
|
+
'mutations',
|
|
159
|
+
'subscriptions'
|
|
160
|
+
];
|
|
161
|
+
return tags.some((tag)=>graphqlTags.includes(tag.toLowerCase()));
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Get the GraphQL operation type from tags
|
|
165
|
+
*/ function getGraphQLOperationType(tags) {
|
|
166
|
+
const tagLower = tags.map((t)=>t.toLowerCase());
|
|
167
|
+
if (tagLower.includes('query') || tagLower.includes('queries')) return 'query';
|
|
168
|
+
if (tagLower.includes('mutation') || tagLower.includes('mutations')) return 'mutation';
|
|
169
|
+
if (tagLower.includes('subscription') || tagLower.includes('subscriptions')) return 'subscription';
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Extract GraphQL variable names from the request body
|
|
174
|
+
*/ function extractGraphQLVariables(body) {
|
|
175
|
+
if (!body) return [];
|
|
176
|
+
try {
|
|
177
|
+
const parsed = JSON.parse(body);
|
|
178
|
+
if (parsed.variables && typeof parsed.variables === 'object') {
|
|
179
|
+
return Object.keys(parsed.variables);
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
// Not valid JSON, ignore
|
|
183
|
+
}
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
150
186
|
/**
|
|
151
187
|
* Builds an endpoint index for AI context
|
|
152
188
|
*/ export function buildEndpointIndex(collection) {
|
|
153
189
|
const allRequests = extractRequests(collection);
|
|
154
|
-
return allRequests.map(({ request, tags })=>
|
|
190
|
+
return allRequests.map(({ request, tags })=>{
|
|
191
|
+
const allTags = [
|
|
192
|
+
...new Set([
|
|
193
|
+
...tags,
|
|
194
|
+
...request.tags
|
|
195
|
+
])
|
|
196
|
+
].filter(Boolean);
|
|
197
|
+
const isGraphQL = isGraphQLOperation(allTags);
|
|
198
|
+
const operationType = isGraphQL ? getGraphQLOperationType(allTags) : undefined;
|
|
199
|
+
// For GraphQL operations, create a more meaningful path
|
|
200
|
+
let path = request.endpoint;
|
|
201
|
+
if (isGraphQL && operationType) {
|
|
202
|
+
// Create path like "query:getUser" or "mutation:createUser"
|
|
203
|
+
path = `${operationType}:${request.name}`;
|
|
204
|
+
}
|
|
205
|
+
// For GraphQL, extract variable names from the body
|
|
206
|
+
let parameters = request.params.map((p)=>p.key);
|
|
207
|
+
if (isGraphQL && request.body.body && typeof request.body.body === 'string') {
|
|
208
|
+
const graphqlVars = extractGraphQLVariables(request.body.body);
|
|
209
|
+
if (graphqlVars.length > 0) {
|
|
210
|
+
parameters = graphqlVars;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
155
214
|
id: request.id,
|
|
156
215
|
name: request.name,
|
|
157
|
-
method: request.method,
|
|
158
|
-
path
|
|
216
|
+
method: isGraphQL ? operationType?.toUpperCase() || 'GRAPHQL' : request.method,
|
|
217
|
+
path,
|
|
159
218
|
description: request.description,
|
|
160
|
-
parameters
|
|
219
|
+
parameters,
|
|
161
220
|
hasBody: request.body.body !== null,
|
|
162
|
-
tags:
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
221
|
+
tags: allTags,
|
|
222
|
+
type: isGraphQL ? 'graphql' : 'rest',
|
|
223
|
+
...operationType && {
|
|
224
|
+
operationType
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
});
|
|
169
228
|
}
|
|
170
229
|
/**
|
|
171
230
|
* Formats the endpoint index as a string for the AI system prompt
|
|
@@ -249,6 +308,8 @@
|
|
|
249
308
|
description: ep.description,
|
|
250
309
|
parameters: ep.parameters,
|
|
251
310
|
hasBody: ep.hasBody,
|
|
252
|
-
tags: ep.tags
|
|
311
|
+
tags: ep.tags,
|
|
312
|
+
type: ep.type,
|
|
313
|
+
operationType: ep.operationType
|
|
253
314
|
}));
|
|
254
315
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { useState, useEffect, useMemo } from 'react';
|
|
2
|
+
import { useState, useEffect, useMemo, useRef } from 'react';
|
|
3
3
|
/**
|
|
4
4
|
* Shared hook for fetching contextual suggestions
|
|
5
5
|
* Used by both AgentChat and AgentPopupButton
|
|
@@ -8,19 +8,28 @@ import { useState, useEffect, useMemo } from 'react';
|
|
|
8
8
|
*/ export function useSuggestions({ currentEndpoint, endpointIndex = [], limit }) {
|
|
9
9
|
const [suggestions, setSuggestions] = useState([]);
|
|
10
10
|
const [isLoading, setIsLoading] = useState(false);
|
|
11
|
-
//
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
// Use refs for values that shouldn't trigger re-fetches when they change
|
|
12
|
+
const endpointIndexRef = useRef(endpointIndex);
|
|
13
|
+
endpointIndexRef.current = endpointIndex;
|
|
14
|
+
const limitRef = useRef(limit);
|
|
15
|
+
limitRef.current = limit;
|
|
16
|
+
// Extract primitive values for stable dependencies
|
|
17
|
+
const currentEndpointId = currentEndpoint?.id;
|
|
18
|
+
const currentEndpointName = currentEndpoint?.name;
|
|
19
|
+
const endpointIndexLength = endpointIndex.length;
|
|
20
|
+
// Stable key for request deduplication - only based on primitives
|
|
21
|
+
const requestKey = useMemo(()=>currentEndpointId || `general:${endpointIndexLength}`, [
|
|
22
|
+
currentEndpointId,
|
|
23
|
+
endpointIndexLength
|
|
15
24
|
]);
|
|
16
|
-
// Fallback suggestions
|
|
25
|
+
// Fallback suggestions - only depend on primitive values
|
|
17
26
|
const fallbackSuggestions = useMemo(()=>{
|
|
18
|
-
if (
|
|
27
|
+
if (currentEndpointName) {
|
|
19
28
|
return [
|
|
20
29
|
{
|
|
21
30
|
title: 'What does this',
|
|
22
31
|
label: 'endpoint do?',
|
|
23
|
-
prompt: `What does ${
|
|
32
|
+
prompt: `What does ${currentEndpointName} do?`
|
|
24
33
|
},
|
|
25
34
|
{
|
|
26
35
|
title: 'What parameters',
|
|
@@ -52,9 +61,10 @@ import { useState, useEffect, useMemo } from 'react';
|
|
|
52
61
|
}
|
|
53
62
|
];
|
|
54
63
|
}, [
|
|
55
|
-
|
|
64
|
+
currentEndpointName
|
|
56
65
|
]);
|
|
57
66
|
// Fetch suggestions from API (server handles KV caching)
|
|
67
|
+
// Only re-fetch when requestKey changes (based on endpoint ID or index length)
|
|
58
68
|
useEffect(()=>{
|
|
59
69
|
let cancelled = false;
|
|
60
70
|
setIsLoading(true);
|
|
@@ -64,19 +74,21 @@ import { useState, useEffect, useMemo } from 'react';
|
|
|
64
74
|
'Content-Type': 'application/json'
|
|
65
75
|
},
|
|
66
76
|
body: JSON.stringify({
|
|
67
|
-
endpointIndex,
|
|
68
|
-
currentEndpointId
|
|
77
|
+
endpointIndex: endpointIndexRef.current,
|
|
78
|
+
currentEndpointId
|
|
69
79
|
})
|
|
70
80
|
}).then((res)=>res.json()).then((data)=>{
|
|
71
81
|
if (cancelled) return;
|
|
72
82
|
if (data.suggestions) {
|
|
73
83
|
const allSuggestions = data.suggestions;
|
|
74
|
-
|
|
84
|
+
const currentLimit = limitRef.current;
|
|
85
|
+
setSuggestions(currentLimit ? allSuggestions.slice(0, currentLimit) : allSuggestions);
|
|
75
86
|
}
|
|
76
87
|
}).catch((err)=>{
|
|
77
88
|
if (cancelled) return;
|
|
78
89
|
console.warn('[useSuggestions] Failed to fetch suggestions:', err);
|
|
79
|
-
|
|
90
|
+
const currentLimit = limitRef.current;
|
|
91
|
+
setSuggestions(currentLimit ? fallbackSuggestions.slice(0, currentLimit) : fallbackSuggestions);
|
|
80
92
|
}).finally(()=>{
|
|
81
93
|
if (!cancelled) setIsLoading(false);
|
|
82
94
|
});
|
|
@@ -85,9 +97,7 @@ import { useState, useEffect, useMemo } from 'react';
|
|
|
85
97
|
};
|
|
86
98
|
}, [
|
|
87
99
|
requestKey,
|
|
88
|
-
|
|
89
|
-
endpointIndex,
|
|
90
|
-
limit,
|
|
100
|
+
currentEndpointId,
|
|
91
101
|
fallbackSuggestions
|
|
92
102
|
]);
|
|
93
103
|
return {
|
|
@@ -4,14 +4,14 @@ import React, { createContext, useContext, useState, useCallback, useEffect, use
|
|
|
4
4
|
import { switchView } from '@/lib/docs-navigation';
|
|
5
5
|
const ModeContext = /*#__PURE__*/ createContext(null);
|
|
6
6
|
/**
|
|
7
|
-
* Parse view mode from URL
|
|
8
|
-
*/ function
|
|
7
|
+
* Parse view mode from URL pathname
|
|
8
|
+
*/ function parseViewFromPath() {
|
|
9
9
|
if (typeof window === 'undefined') return 'docs';
|
|
10
|
-
const
|
|
11
|
-
if (!
|
|
10
|
+
const pathname = window.location.pathname;
|
|
11
|
+
if (!pathname || pathname === '/') return 'docs';
|
|
12
12
|
// Check for view suffix at the end
|
|
13
|
-
if (
|
|
14
|
-
if (
|
|
13
|
+
if (pathname.endsWith('/playground')) return 'playground';
|
|
14
|
+
if (pathname.endsWith('/notes')) return 'notes';
|
|
15
15
|
return 'docs';
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
@@ -39,29 +39,27 @@ const ModeContext = /*#__PURE__*/ createContext(null);
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
export function ModeProvider({ children, defaultMode = 'docs' }) {
|
|
42
|
-
// Track current
|
|
43
|
-
const [
|
|
44
|
-
// Derive mode from URL
|
|
42
|
+
// Track current pathname to derive mode
|
|
43
|
+
const [pathname, setPathname] = useState(()=>typeof window !== 'undefined' ? window.location.pathname : '');
|
|
44
|
+
// Derive mode from URL pathname
|
|
45
45
|
const mode = useMemo(()=>{
|
|
46
|
-
const view =
|
|
46
|
+
const view = parseViewFromPath();
|
|
47
47
|
return viewToPlaygroundMode(view);
|
|
48
48
|
}, [
|
|
49
|
-
|
|
49
|
+
pathname
|
|
50
50
|
]);
|
|
51
51
|
// Notes-specific state (not URL-based yet)
|
|
52
52
|
const [activeFilePath, setActiveFilePathState] = useState(null);
|
|
53
53
|
const [streamingContent, setStreamingContent] = useState(null);
|
|
54
54
|
const [notesRefreshTrigger, setNotesRefreshTrigger] = useState(0);
|
|
55
|
-
// Listen for
|
|
55
|
+
// Listen for pathname changes to update mode
|
|
56
56
|
useEffect(()=>{
|
|
57
|
-
const
|
|
58
|
-
|
|
57
|
+
const handlePathChange = ()=>{
|
|
58
|
+
setPathname(window.location.pathname);
|
|
59
59
|
};
|
|
60
|
-
window.addEventListener('
|
|
61
|
-
window.addEventListener('popstate', handleHashChange);
|
|
60
|
+
window.addEventListener('popstate', handlePathChange);
|
|
62
61
|
return ()=>{
|
|
63
|
-
window.removeEventListener('
|
|
64
|
-
window.removeEventListener('popstate', handleHashChange);
|
|
62
|
+
window.removeEventListener('popstate', handlePathChange);
|
|
65
63
|
};
|
|
66
64
|
}, []);
|
|
67
65
|
// Wrapper for setActiveFilePath
|
|
@@ -158,7 +158,14 @@ import { parseOpenAPIAuth, extractSecurityHeaders, getDefaultAuth } from './extr
|
|
|
158
158
|
requestsByTags[tag] = [];
|
|
159
159
|
}
|
|
160
160
|
// Clone request for each tag (request can belong to multiple tags)
|
|
161
|
-
|
|
161
|
+
// Generate unique ID per tag to avoid multiple highlights when selecting
|
|
162
|
+
const clonedRequest = cloneDeep(request);
|
|
163
|
+
if (tags.length > 1) {
|
|
164
|
+
// Append tag slug to ID for uniqueness when endpoint appears in multiple tags
|
|
165
|
+
const tagSlug = tag.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
166
|
+
clonedRequest.id = `${request.id}--${tagSlug}`;
|
|
167
|
+
}
|
|
168
|
+
requestsByTags[tag].push(clonedRequest);
|
|
162
169
|
}
|
|
163
170
|
});
|
|
164
171
|
// Create folders (collections) for each tag
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vercel Edge Cache Purge Utilities
|
|
3
|
+
*
|
|
4
|
+
* Functions for invalidating CDN-cached content after deployments
|
|
5
|
+
*/ /**
|
|
6
|
+
* Purge Vercel Edge cache for a project
|
|
7
|
+
* Uses Vercel's Purge Cache API to invalidate CDN-cached content
|
|
8
|
+
*
|
|
9
|
+
* @param slug - Project slug to purge cache for
|
|
10
|
+
* @returns Promise that resolves when purge is complete (or skipped)
|
|
11
|
+
*/ export async function purgeProjectCache(slug) {
|
|
12
|
+
const token = process.env.VERCEL_API_TOKEN;
|
|
13
|
+
const teamId = process.env.VERCEL_TEAM_ID;
|
|
14
|
+
const projectId = process.env.VERCEL_PROJECT_ID;
|
|
15
|
+
if (!token) {
|
|
16
|
+
console.log('[Cache] Skipping purge: VERCEL_API_TOKEN not set');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const tags = [
|
|
20
|
+
`project-${slug}`,
|
|
21
|
+
`assets-${slug}`
|
|
22
|
+
];
|
|
23
|
+
try {
|
|
24
|
+
// Try tag-based purge first (more targeted)
|
|
25
|
+
const tagPurgeSuccess = await purgeByTags(tags, token, teamId);
|
|
26
|
+
if (tagPurgeSuccess) {
|
|
27
|
+
console.log('[Cache] Edge cache purged for tags:', tags);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
// Fallback to project-wide cache purge
|
|
31
|
+
if (projectId) {
|
|
32
|
+
const projectPurgeSuccess = await purgeProjectWide(projectId, token, teamId);
|
|
33
|
+
if (projectPurgeSuccess) {
|
|
34
|
+
console.log('[Cache] Project cache purged successfully');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
console.warn('[Cache] Cache purge failed - content may be stale for up to 5 minutes');
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.warn('[Cache] Purge error:', error);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Purge cache by tags using Vercel's tag-based invalidation
|
|
45
|
+
*/ async function purgeByTags(tags, token, teamId) {
|
|
46
|
+
try {
|
|
47
|
+
const url = new URL('https://api.vercel.com/v1/cache');
|
|
48
|
+
if (teamId) {
|
|
49
|
+
url.searchParams.set('teamId', teamId);
|
|
50
|
+
}
|
|
51
|
+
const response = await fetch(url.toString(), {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: {
|
|
54
|
+
'Authorization': `Bearer ${token}`,
|
|
55
|
+
'Content-Type': 'application/json'
|
|
56
|
+
},
|
|
57
|
+
body: JSON.stringify({
|
|
58
|
+
tags
|
|
59
|
+
})
|
|
60
|
+
});
|
|
61
|
+
return response.ok;
|
|
62
|
+
} catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Purge entire project cache (fallback when tag purge fails)
|
|
68
|
+
*/ async function purgeProjectWide(projectId, token, teamId) {
|
|
69
|
+
try {
|
|
70
|
+
const url = new URL(`https://api.vercel.com/v1/projects/${projectId}/cache`);
|
|
71
|
+
if (teamId) {
|
|
72
|
+
url.searchParams.set('teamId', teamId);
|
|
73
|
+
}
|
|
74
|
+
const response = await fetch(url.toString(), {
|
|
75
|
+
method: 'DELETE',
|
|
76
|
+
headers: {
|
|
77
|
+
'Authorization': `Bearer ${token}`
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
return response.ok;
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Purge specific asset paths from cache
|
|
87
|
+
*
|
|
88
|
+
* @param slug - Project slug
|
|
89
|
+
* @param paths - Array of asset paths to purge
|
|
90
|
+
*/ export async function purgeAssetPaths(slug, paths) {
|
|
91
|
+
const tags = paths.map((p)=>`assets-${slug}-${p.replace(/[^a-z0-9]/gi, '-')}`);
|
|
92
|
+
const token = process.env.VERCEL_API_TOKEN;
|
|
93
|
+
const teamId = process.env.VERCEL_TEAM_ID;
|
|
94
|
+
if (!token) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
await purgeByTags(tags, token, teamId);
|
|
98
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docs Link Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared utilities for resolving internal documentation links.
|
|
5
|
+
* Used by Card component, MdxLink, and other components that handle
|
|
6
|
+
* navigation within documentation.
|
|
7
|
+
*/ /**
|
|
8
|
+
* Check if a URL is external (starts with http/https or //)
|
|
9
|
+
*/ export function isExternalLink(href) {
|
|
10
|
+
return href.startsWith('http') || href.startsWith('//');
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Check if a path segment matches a known tab ID
|
|
14
|
+
*/ export function isTabId(segment, tabIds) {
|
|
15
|
+
if (!tabIds || tabIds.length === 0) return false;
|
|
16
|
+
return tabIds.includes(segment);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Parse a link href and determine its type and target
|
|
20
|
+
*/ export function parseLink(href, context) {
|
|
21
|
+
// External links
|
|
22
|
+
if (isExternalLink(href)) {
|
|
23
|
+
return {
|
|
24
|
+
href,
|
|
25
|
+
isTabLink: false,
|
|
26
|
+
isPageLink: false,
|
|
27
|
+
isExternal: true
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// Anchor links (same page)
|
|
31
|
+
if (href.startsWith('#')) {
|
|
32
|
+
return {
|
|
33
|
+
href,
|
|
34
|
+
isTabLink: false,
|
|
35
|
+
isPageLink: false,
|
|
36
|
+
isExternal: false
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// Internal links starting with /
|
|
40
|
+
if (href.startsWith('/')) {
|
|
41
|
+
const pathSegment = href.slice(1) // Remove leading /
|
|
42
|
+
;
|
|
43
|
+
const pathParts = pathSegment.split('/');
|
|
44
|
+
const firstPart = pathParts[0];
|
|
45
|
+
// Check if this is a direct tab link (e.g., /facebook-pages-api)
|
|
46
|
+
if (pathParts.length === 1 && isTabId(firstPart, context.tabIds)) {
|
|
47
|
+
return {
|
|
48
|
+
href,
|
|
49
|
+
isTabLink: true,
|
|
50
|
+
isPageLink: false,
|
|
51
|
+
isExternal: false,
|
|
52
|
+
targetTab: firstPart
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// Check if path already has proper structure (/tab/page/slug or /tab/endpoint/id)
|
|
56
|
+
if (href.includes('/page/') || href.includes('/endpoint/') || href.includes('/section/')) {
|
|
57
|
+
return {
|
|
58
|
+
href,
|
|
59
|
+
isTabLink: false,
|
|
60
|
+
isPageLink: true,
|
|
61
|
+
isExternal: false
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// Simple page path - needs to be resolved with active tab context
|
|
65
|
+
return {
|
|
66
|
+
href,
|
|
67
|
+
isTabLink: false,
|
|
68
|
+
isPageLink: true,
|
|
69
|
+
isExternal: false,
|
|
70
|
+
targetSlug: pathSegment
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// Relative links (no leading /)
|
|
74
|
+
return {
|
|
75
|
+
href,
|
|
76
|
+
isTabLink: false,
|
|
77
|
+
isPageLink: true,
|
|
78
|
+
isExternal: false,
|
|
79
|
+
targetSlug: href
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Resolve a link href to its final URL based on context
|
|
84
|
+
*
|
|
85
|
+
* - Tab links: /facebook-pages-api → /facebook-pages-api (unchanged)
|
|
86
|
+
* - Page links within tab: /quickstart → /documentation/page/quickstart
|
|
87
|
+
* - Already resolved: /tab/page/slug → /tab/page/slug (unchanged)
|
|
88
|
+
* - External: https://... → https://... (unchanged)
|
|
89
|
+
*/ export function resolveLink(href, context) {
|
|
90
|
+
if (!href) return href;
|
|
91
|
+
const parsed = parseLink(href, context);
|
|
92
|
+
// External links and anchor links stay as-is
|
|
93
|
+
if (parsed.isExternal || href.startsWith('#')) {
|
|
94
|
+
return href;
|
|
95
|
+
}
|
|
96
|
+
// Tab links stay as-is
|
|
97
|
+
if (parsed.isTabLink) {
|
|
98
|
+
return href;
|
|
99
|
+
}
|
|
100
|
+
// Already resolved paths stay as-is
|
|
101
|
+
if (href.includes('/page/') || href.includes('/endpoint/') || href.includes('/section/')) {
|
|
102
|
+
return href;
|
|
103
|
+
}
|
|
104
|
+
// Simple page paths need to be prefixed with active tab
|
|
105
|
+
if (parsed.targetSlug && context.activeTab) {
|
|
106
|
+
return `/${context.activeTab}/page/${parsed.targetSlug}`;
|
|
107
|
+
}
|
|
108
|
+
return href;
|
|
109
|
+
}
|
|
110
|
+
export function getLinkAction(href, context) {
|
|
111
|
+
if (!href) {
|
|
112
|
+
return {
|
|
113
|
+
type: 'navigate',
|
|
114
|
+
href: ''
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const parsed = parseLink(href, context);
|
|
118
|
+
if (parsed.isExternal) {
|
|
119
|
+
return {
|
|
120
|
+
type: 'external',
|
|
121
|
+
href
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (href.startsWith('#')) {
|
|
125
|
+
return {
|
|
126
|
+
type: 'anchor',
|
|
127
|
+
href
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (parsed.isTabLink && parsed.targetTab) {
|
|
131
|
+
return {
|
|
132
|
+
type: 'switchTab',
|
|
133
|
+
tabId: parsed.targetTab
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (parsed.targetSlug) {
|
|
137
|
+
return {
|
|
138
|
+
type: 'navigatePage',
|
|
139
|
+
slug: parsed.targetSlug
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
type: 'navigate',
|
|
144
|
+
href: resolveLink(href, context)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import React, { createContext, useContext, useCallback } from 'react';
|
|
4
4
|
const DocsNavigationContext = /*#__PURE__*/ createContext(null);
|
|
5
|
-
export function DocsNavigationProvider({ children, onNavigateToPage, onNavigateToEndpoint, onSwitchTab, activeTab }) {
|
|
5
|
+
export function DocsNavigationProvider({ children, onNavigateToPage, onNavigateToEndpoint, onSwitchTab, activeTab, tabIds }) {
|
|
6
6
|
const navigateToPage = useCallback((slug)=>{
|
|
7
7
|
if (onNavigateToPage) {
|
|
8
8
|
onNavigateToPage(slug);
|
|
@@ -30,7 +30,8 @@ export function DocsNavigationProvider({ children, onNavigateToPage, onNavigateT
|
|
|
30
30
|
navigateToEndpoint,
|
|
31
31
|
switchToTab,
|
|
32
32
|
isApiDocsView: !!onNavigateToPage,
|
|
33
|
-
activeTab
|
|
33
|
+
activeTab,
|
|
34
|
+
tabIds
|
|
34
35
|
},
|
|
35
36
|
children: children
|
|
36
37
|
});
|