@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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
4
4
|
import { cn } from '@/lib/utils';
|
|
5
|
-
import { Play, ArrowClockwise, Copy, Check, Spinner, X, FileText, BracketsCurly, Key, CaretRight, CaretDown, MagnifyingGlass, Lightning, ArrowsClockwise } from '@phosphor-icons/react';
|
|
5
|
+
import { Play, ArrowClockwise, Copy, Check, Spinner, X, FileText, BracketsCurly, Key, CaretRight, CaretDown, MagnifyingGlass, Lightning, ArrowsClockwise, Bug, Lightbulb } from '@phosphor-icons/react';
|
|
6
6
|
import { Button } from '@/components/ui/button';
|
|
7
7
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
8
8
|
import Editor, { loader } from '@monaco-editor/react';
|
|
@@ -354,7 +354,7 @@ export function GraphQLPlayground({ endpoint, defaultQuery = `# Welcome to the G
|
|
|
354
354
|
query {
|
|
355
355
|
__typename
|
|
356
356
|
}
|
|
357
|
-
`, headers: defaultHeaders = {}, operations = [], selectedOperationId: externalSelectedId, hideExplorer = false, className, theme = 'dark', schemaSDL }) {
|
|
357
|
+
`, headers: defaultHeaders = {}, operations = [], selectedOperationId: externalSelectedId, externalVariables, onStateChange, onDebugRequest, onExplainRequest, hideExplorer = false, className, theme = 'dark', schemaSDL }) {
|
|
358
358
|
const [query, setQuery] = useState(defaultQuery);
|
|
359
359
|
const [variables, setVariables] = useState('{}');
|
|
360
360
|
const [customHeaders, setCustomHeaders] = useState(Object.keys(defaultHeaders).length > 0 ? JSON.stringify(defaultHeaders, null, 2) : '{}');
|
|
@@ -459,6 +459,35 @@ query {
|
|
|
459
459
|
externalSelectedId,
|
|
460
460
|
operations
|
|
461
461
|
]);
|
|
462
|
+
// Sync with external variables (from AI agent prefill)
|
|
463
|
+
useEffect(()=>{
|
|
464
|
+
if (externalVariables && Object.keys(externalVariables).length > 0) {
|
|
465
|
+
setVariables(JSON.stringify(externalVariables, null, 2));
|
|
466
|
+
}
|
|
467
|
+
}, [
|
|
468
|
+
externalVariables
|
|
469
|
+
]);
|
|
470
|
+
// Report state changes to parent (for AI context)
|
|
471
|
+
useEffect(()=>{
|
|
472
|
+
if (onStateChange) {
|
|
473
|
+
onStateChange({
|
|
474
|
+
query,
|
|
475
|
+
variables,
|
|
476
|
+
response,
|
|
477
|
+
responseTime,
|
|
478
|
+
error,
|
|
479
|
+
isLoading
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}, [
|
|
483
|
+
query,
|
|
484
|
+
variables,
|
|
485
|
+
response,
|
|
486
|
+
responseTime,
|
|
487
|
+
error,
|
|
488
|
+
isLoading,
|
|
489
|
+
onStateChange
|
|
490
|
+
]);
|
|
462
491
|
// Execute GraphQL query
|
|
463
492
|
const executeQuery = useCallback(async ()=>{
|
|
464
493
|
setIsLoading(true);
|
|
@@ -813,40 +842,109 @@ query {
|
|
|
813
842
|
})
|
|
814
843
|
]
|
|
815
844
|
}),
|
|
816
|
-
|
|
845
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
846
|
+
className: "flex items-center gap-1",
|
|
817
847
|
children: [
|
|
818
|
-
/*#__PURE__*/
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
848
|
+
response && hasErrors && onDebugRequest && /*#__PURE__*/ _jsxs(Tooltip, {
|
|
849
|
+
children: [
|
|
850
|
+
/*#__PURE__*/ _jsx(TooltipTrigger, {
|
|
851
|
+
asChild: true,
|
|
852
|
+
children: /*#__PURE__*/ _jsxs(Button, {
|
|
853
|
+
variant: "ghost",
|
|
854
|
+
size: "sm",
|
|
855
|
+
onClick: ()=>onDebugRequest({
|
|
856
|
+
query,
|
|
857
|
+
variables,
|
|
858
|
+
response,
|
|
859
|
+
responseTime,
|
|
860
|
+
error,
|
|
861
|
+
operationName: operations.find((op)=>op.id === selectedOperationId)?.name
|
|
862
|
+
}),
|
|
863
|
+
className: "h-7 px-2",
|
|
864
|
+
children: [
|
|
865
|
+
/*#__PURE__*/ _jsx(Bug, {
|
|
866
|
+
className: "h-3.5 w-3.5 mr-1 text-red-500"
|
|
867
|
+
}),
|
|
868
|
+
/*#__PURE__*/ _jsx("span", {
|
|
869
|
+
className: "text-xs",
|
|
870
|
+
children: "Debug"
|
|
871
|
+
})
|
|
872
|
+
]
|
|
873
|
+
})
|
|
874
|
+
}),
|
|
875
|
+
/*#__PURE__*/ _jsx(TooltipContent, {
|
|
876
|
+
children: "Ask AI to help debug this error"
|
|
877
|
+
})
|
|
878
|
+
]
|
|
879
|
+
}),
|
|
880
|
+
response && !hasErrors && onExplainRequest && /*#__PURE__*/ _jsxs(Tooltip, {
|
|
881
|
+
children: [
|
|
882
|
+
/*#__PURE__*/ _jsx(TooltipTrigger, {
|
|
883
|
+
asChild: true,
|
|
884
|
+
children: /*#__PURE__*/ _jsxs(Button, {
|
|
885
|
+
variant: "ghost",
|
|
886
|
+
size: "sm",
|
|
887
|
+
onClick: ()=>onExplainRequest({
|
|
888
|
+
query,
|
|
889
|
+
variables,
|
|
890
|
+
response,
|
|
891
|
+
responseTime,
|
|
892
|
+
error,
|
|
893
|
+
operationName: operations.find((op)=>op.id === selectedOperationId)?.name
|
|
894
|
+
}),
|
|
895
|
+
className: "h-7 px-2",
|
|
896
|
+
children: [
|
|
897
|
+
/*#__PURE__*/ _jsx(Lightbulb, {
|
|
898
|
+
className: "h-3.5 w-3.5 mr-1 text-yellow-500"
|
|
899
|
+
}),
|
|
900
|
+
/*#__PURE__*/ _jsx("span", {
|
|
901
|
+
className: "text-xs",
|
|
902
|
+
children: "Explain"
|
|
903
|
+
})
|
|
904
|
+
]
|
|
905
|
+
})
|
|
906
|
+
}),
|
|
907
|
+
/*#__PURE__*/ _jsx(TooltipContent, {
|
|
908
|
+
children: "Ask AI to explain this response"
|
|
845
909
|
})
|
|
846
|
-
|
|
910
|
+
]
|
|
847
911
|
}),
|
|
848
|
-
/*#__PURE__*/
|
|
849
|
-
children:
|
|
912
|
+
response && /*#__PURE__*/ _jsxs(Tooltip, {
|
|
913
|
+
children: [
|
|
914
|
+
/*#__PURE__*/ _jsx(TooltipTrigger, {
|
|
915
|
+
asChild: true,
|
|
916
|
+
children: /*#__PURE__*/ _jsx(Button, {
|
|
917
|
+
variant: "ghost",
|
|
918
|
+
size: "sm",
|
|
919
|
+
onClick: copyResponse,
|
|
920
|
+
className: "h-7 px-2",
|
|
921
|
+
children: copied ? /*#__PURE__*/ _jsxs(_Fragment, {
|
|
922
|
+
children: [
|
|
923
|
+
/*#__PURE__*/ _jsx(Check, {
|
|
924
|
+
className: "h-3.5 w-3.5 mr-1 text-emerald-500"
|
|
925
|
+
}),
|
|
926
|
+
/*#__PURE__*/ _jsx("span", {
|
|
927
|
+
className: "text-xs",
|
|
928
|
+
children: "Copied"
|
|
929
|
+
})
|
|
930
|
+
]
|
|
931
|
+
}) : /*#__PURE__*/ _jsxs(_Fragment, {
|
|
932
|
+
children: [
|
|
933
|
+
/*#__PURE__*/ _jsx(Copy, {
|
|
934
|
+
className: "h-3.5 w-3.5 mr-1"
|
|
935
|
+
}),
|
|
936
|
+
/*#__PURE__*/ _jsx("span", {
|
|
937
|
+
className: "text-xs",
|
|
938
|
+
children: "Copy"
|
|
939
|
+
})
|
|
940
|
+
]
|
|
941
|
+
})
|
|
942
|
+
})
|
|
943
|
+
}),
|
|
944
|
+
/*#__PURE__*/ _jsx(TooltipContent, {
|
|
945
|
+
children: "Copy response"
|
|
946
|
+
})
|
|
947
|
+
]
|
|
850
948
|
})
|
|
851
949
|
]
|
|
852
950
|
})
|
|
@@ -2,13 +2,19 @@
|
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { cn } from '@/lib/utils';
|
|
4
4
|
const methodColors = {
|
|
5
|
+
// HTTP methods
|
|
5
6
|
GET: 'bg-emerald-500/90 text-white',
|
|
6
7
|
POST: 'bg-blue-500/90 text-white',
|
|
7
8
|
PUT: 'bg-amber-500/90 text-white',
|
|
8
9
|
DELETE: 'bg-red-500/90 text-white',
|
|
9
10
|
PATCH: 'bg-violet-500/90 text-white',
|
|
10
11
|
HEAD: 'bg-slate-500/90 text-white',
|
|
11
|
-
OPTIONS: 'bg-slate-500/90 text-white'
|
|
12
|
+
OPTIONS: 'bg-slate-500/90 text-white',
|
|
13
|
+
// GraphQL operations
|
|
14
|
+
QUERY: 'bg-pink-500/90 text-white',
|
|
15
|
+
MUTATION: 'bg-orange-500/90 text-white',
|
|
16
|
+
SUBSCRIPTION: 'bg-cyan-500/90 text-white',
|
|
17
|
+
GRAPHQL: 'bg-pink-500/90 text-white'
|
|
12
18
|
};
|
|
13
19
|
const sizeClasses = {
|
|
14
20
|
sm: 'px-1.5 py-0.5 text-[10px]',
|
|
@@ -16,8 +22,11 @@ const sizeClasses = {
|
|
|
16
22
|
lg: 'px-2.5 py-1 text-sm'
|
|
17
23
|
};
|
|
18
24
|
export function MethodBadge({ method, size = 'md', className = '' }) {
|
|
25
|
+
// Normalize method to uppercase and get color (fallback for unknown methods)
|
|
26
|
+
const normalizedMethod = method?.toUpperCase();
|
|
27
|
+
const colorClass = methodColors[normalizedMethod] || 'bg-slate-500/90 text-white';
|
|
19
28
|
return /*#__PURE__*/ _jsx("span", {
|
|
20
|
-
className: cn('font-semibold rounded uppercase tracking-wide',
|
|
29
|
+
className: cn('font-semibold rounded uppercase tracking-wide', colorClass, sizeClasses[size], className),
|
|
21
30
|
children: method
|
|
22
31
|
});
|
|
23
32
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { useState, useMemo, useEffect } from 'react';
|
|
3
|
+
import { useState, useMemo, useEffect, useRef } from 'react';
|
|
4
4
|
import { Folder, FolderOpen } from '@phosphor-icons/react';
|
|
5
5
|
import { MethodBadge } from '../shared/method-badge';
|
|
6
6
|
import { SidebarItem } from './sidebar-item';
|
|
7
7
|
import { SidebarGroup } from './sidebar-group';
|
|
8
|
+
import { cn } from '@/lib/utils';
|
|
8
9
|
/**
|
|
9
10
|
* Find the folder ID that contains a request (recursive)
|
|
10
11
|
*/ function _findFolderContainingRequest(collection, requestId) {
|
|
@@ -45,7 +46,30 @@ import { SidebarGroup } from './sidebar-group';
|
|
|
45
46
|
}
|
|
46
47
|
return path;
|
|
47
48
|
}
|
|
48
|
-
export function CollectionTree({ collection, selectedRequest, onSelectRequest, searchQuery = '', level = 0 }) {
|
|
49
|
+
export function CollectionTree({ collection, selectedRequest, onSelectRequest, searchQuery = '', level = 0, animate = false }) {
|
|
50
|
+
const contentRef = useRef(null);
|
|
51
|
+
const [isExpanded, setIsExpanded] = useState(!animate);
|
|
52
|
+
const [contentHeight, setContentHeight] = useState(animate ? 0 : 'auto');
|
|
53
|
+
// Tree expand animation on mount (only at root level)
|
|
54
|
+
useEffect(()=>{
|
|
55
|
+
if (animate && level === 0 && !isExpanded) {
|
|
56
|
+
// Small delay to let content render, then measure and animate
|
|
57
|
+
const timer = setTimeout(()=>{
|
|
58
|
+
if (contentRef.current) {
|
|
59
|
+
const height = contentRef.current.scrollHeight;
|
|
60
|
+
setContentHeight(height);
|
|
61
|
+
setIsExpanded(true);
|
|
62
|
+
// After animation completes, set to auto for dynamic content
|
|
63
|
+
setTimeout(()=>setContentHeight('auto'), 600);
|
|
64
|
+
}
|
|
65
|
+
}, 100);
|
|
66
|
+
return ()=>clearTimeout(timer);
|
|
67
|
+
}
|
|
68
|
+
}, [
|
|
69
|
+
animate,
|
|
70
|
+
level,
|
|
71
|
+
isExpanded
|
|
72
|
+
]);
|
|
49
73
|
// Track which folders are expanded
|
|
50
74
|
const [expandedFolders, setExpandedFolders] = useState(()=>{
|
|
51
75
|
const expanded = new Set();
|
|
@@ -149,10 +173,10 @@ export function CollectionTree({ collection, selectedRequest, onSelectRequest, s
|
|
|
149
173
|
}
|
|
150
174
|
return null;
|
|
151
175
|
}
|
|
152
|
-
|
|
176
|
+
const treeContent = /*#__PURE__*/ _jsxs(_Fragment, {
|
|
153
177
|
children: [
|
|
154
178
|
filteredData.folders.map((folder)=>{
|
|
155
|
-
const
|
|
179
|
+
const isFolderExpanded = expandedFolders.has(folder.id);
|
|
156
180
|
const hasContent = folder.requests.length > 0 || folder.folders.length > 0;
|
|
157
181
|
if (!hasContent) {
|
|
158
182
|
// Empty folder - render as disabled item
|
|
@@ -170,7 +194,7 @@ export function CollectionTree({ collection, selectedRequest, onSelectRequest, s
|
|
|
170
194
|
title: /*#__PURE__*/ _jsxs("span", {
|
|
171
195
|
className: "flex items-center gap-2",
|
|
172
196
|
children: [
|
|
173
|
-
|
|
197
|
+
isFolderExpanded ? /*#__PURE__*/ _jsx(FolderOpen, {
|
|
174
198
|
className: "h-4 w-4 text-sidebar-foreground/60 shrink-0",
|
|
175
199
|
weight: "fill"
|
|
176
200
|
}) : /*#__PURE__*/ _jsx(Folder, {
|
|
@@ -183,7 +207,7 @@ export function CollectionTree({ collection, selectedRequest, onSelectRequest, s
|
|
|
183
207
|
})
|
|
184
208
|
]
|
|
185
209
|
}),
|
|
186
|
-
defaultOpen:
|
|
210
|
+
defaultOpen: isFolderExpanded,
|
|
187
211
|
indent: level,
|
|
188
212
|
onClick: ()=>toggleFolder(folder.id),
|
|
189
213
|
children: [
|
|
@@ -195,6 +219,7 @@ export function CollectionTree({ collection, selectedRequest, onSelectRequest, s
|
|
|
195
219
|
level: level + 1
|
|
196
220
|
}),
|
|
197
221
|
folder.requests.map((request)=>/*#__PURE__*/ _jsx(SidebarItem, {
|
|
222
|
+
itemId: request.id,
|
|
198
223
|
selected: selectedRequest?.id === request.id,
|
|
199
224
|
indent: level + 1,
|
|
200
225
|
onClick: ()=>onSelectRequest(request),
|
|
@@ -208,6 +233,7 @@ export function CollectionTree({ collection, selectedRequest, onSelectRequest, s
|
|
|
208
233
|
}, folder.id);
|
|
209
234
|
}),
|
|
210
235
|
filteredData.requests.map((request)=>/*#__PURE__*/ _jsx(SidebarItem, {
|
|
236
|
+
itemId: request.id,
|
|
211
237
|
selected: selectedRequest?.id === request.id,
|
|
212
238
|
indent: level,
|
|
213
239
|
onClick: ()=>onSelectRequest(request),
|
|
@@ -219,4 +245,16 @@ export function CollectionTree({ collection, selectedRequest, onSelectRequest, s
|
|
|
219
245
|
}, request.id))
|
|
220
246
|
]
|
|
221
247
|
});
|
|
248
|
+
// At root level with animation enabled, wrap in animated container
|
|
249
|
+
if (level === 0 && animate) {
|
|
250
|
+
return /*#__PURE__*/ _jsx("div", {
|
|
251
|
+
ref: contentRef,
|
|
252
|
+
className: cn("overflow-hidden transition-all duration-500 ease-out", !isExpanded && "opacity-0", isExpanded && "opacity-100"),
|
|
253
|
+
style: {
|
|
254
|
+
height: contentHeight === 'auto' ? 'auto' : `${contentHeight}px`
|
|
255
|
+
},
|
|
256
|
+
children: treeContent
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
return treeContent;
|
|
222
260
|
}
|
|
@@ -162,7 +162,8 @@ export function DocsSidebar({ collection, selectedRequest, selectedDocSection, s
|
|
|
162
162
|
children: /*#__PURE__*/ _jsx(CollectionTree, {
|
|
163
163
|
collection: collection,
|
|
164
164
|
selectedRequest: selectedRequest,
|
|
165
|
-
onSelectRequest: handleSelectRequest
|
|
165
|
+
onSelectRequest: handleSelectRequest,
|
|
166
|
+
animate: true
|
|
166
167
|
})
|
|
167
168
|
})
|
|
168
169
|
]
|
|
@@ -8,7 +8,7 @@ import { Button } from '@/components/ui/button';
|
|
|
8
8
|
import { useMobile } from '@/lib/api-docs/mobile-context';
|
|
9
9
|
import { cn } from '@/lib/utils';
|
|
10
10
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
|
11
|
-
export function RightSidebar({ request, collection, apiSummary, onNavigateToEndpoint, onPrefillParameters, debugContext, onClearDebugContext, explainContext, onClearExplainContext, onOpenGlobalAuth, onNavigateToAuthTab, onNavigateToParamsTab, onNavigateToBodyTab, onNavigateToHeadersTab, onNavigateToDocSection, onNavigateToDocPage }) {
|
|
11
|
+
export function RightSidebar({ request, collection, apiSummary, onNavigateToEndpoint, onPrefillParameters, onPrefillGraphQLVariables, graphqlPlaygroundState, debugContext, onClearDebugContext, explainContext, onClearExplainContext, onOpenGlobalAuth, onNavigateToAuthTab, onNavigateToParamsTab, onNavigateToBodyTab, onNavigateToHeadersTab, onNavigateToDocSection, onNavigateToDocPage }) {
|
|
12
12
|
// Mobile context - now used for both mobile and desktop
|
|
13
13
|
const { isMobile, isRightSidebarOpen, closeRightSidebar } = useMobile();
|
|
14
14
|
const [mounted, setMounted] = useState(false);
|
|
@@ -124,6 +124,8 @@ export function RightSidebar({ request, collection, apiSummary, onNavigateToEndp
|
|
|
124
124
|
apiSummary: apiSummary,
|
|
125
125
|
onNavigate: onNavigateToEndpoint,
|
|
126
126
|
onPrefill: onPrefillParameters,
|
|
127
|
+
onPrefillGraphQLVariables: onPrefillGraphQLVariables,
|
|
128
|
+
graphqlPlaygroundState: graphqlPlaygroundState,
|
|
127
129
|
debugContext: pendingDebugContext,
|
|
128
130
|
onDebugContextConsumed: handleDebugContextConsumed,
|
|
129
131
|
explainContext: explainContext,
|
|
@@ -82,7 +82,7 @@ export function SlidingIndicatorProvider({ children, className }) {
|
|
|
82
82
|
className: cn('relative', className),
|
|
83
83
|
children: [
|
|
84
84
|
/*#__PURE__*/ _jsx("div", {
|
|
85
|
-
className: "docs-sidebar-indicator absolute rounded-lg bg-
|
|
85
|
+
className: "docs-sidebar-indicator absolute rounded-lg bg-primary/10 pointer-events-none z-0",
|
|
86
86
|
style: {
|
|
87
87
|
...indicatorStyle,
|
|
88
88
|
transition: 'top 350ms cubic-bezier(0.4, 0, 0.2, 1), height 250ms cubic-bezier(0.4, 0, 0.2, 1), opacity 200ms ease-out'
|
|
@@ -95,7 +95,8 @@ export function SlidingIndicatorProvider({ children, className }) {
|
|
|
95
95
|
}
|
|
96
96
|
/**
|
|
97
97
|
* Sidebar Item component - individual clickable item in the sidebar
|
|
98
|
-
*
|
|
98
|
+
* Selection is indicated by the sliding indicator background (from provider)
|
|
99
|
+
* and a subtle text emphasis on the selected item
|
|
99
100
|
*/ export function SidebarItem({ children, selected = false, active = false, disabled = false, indent = 0, onClick, className, asideContent, itemId, icon }) {
|
|
100
101
|
const buttonRef = useRef(null);
|
|
101
102
|
const context = useContext(SlidingIndicatorContext);
|
|
@@ -135,8 +136,6 @@ export function SlidingIndicatorProvider({ children, className }) {
|
|
|
135
136
|
}, [
|
|
136
137
|
selected
|
|
137
138
|
]);
|
|
138
|
-
// Check if we're inside a sliding indicator provider
|
|
139
|
-
const hasSliding = context !== null;
|
|
140
139
|
return /*#__PURE__*/ _jsx("li", {
|
|
141
140
|
className: "docs-sidebar-item-wrapper flex flex-col",
|
|
142
141
|
children: /*#__PURE__*/ _jsxs("button", {
|
|
@@ -147,9 +146,8 @@ export function SlidingIndicatorProvider({ children, className }) {
|
|
|
147
146
|
onClick: onClick,
|
|
148
147
|
className: cn(// Base styles
|
|
149
148
|
'docs-sidebar-item group/button flex items-center rounded-lg px-3 py-2 w-full text-left relative z-10', 'text-sm leading-5 transition-colors duration-150', // Indentation
|
|
150
|
-
indent > 0 && `ml-${indent * 3}`, // State variants
|
|
151
|
-
selected ?
|
|
152
|
-
: 'docs-sidebar-item-active bg-background text-green-700 font-semibold dark:bg-stone-800/50 dark:text-green-400' : active ? 'text-sidebar-foreground font-medium hover:bg-sidebar-accent/50' : disabled ? 'text-sidebar-foreground/50 cursor-default' : 'text-sidebar-foreground/80 hover:bg-sidebar-accent/30 hover:text-sidebar-foreground', className),
|
|
149
|
+
indent > 0 && `ml-${indent * 3}`, // State variants - selected items only get text emphasis, indicator handles background
|
|
150
|
+
selected ? 'docs-sidebar-item-active text-sidebar-foreground font-medium' : active ? 'text-sidebar-foreground font-medium hover:bg-sidebar-accent/50' : disabled ? 'text-sidebar-foreground/50 cursor-default' : 'text-sidebar-foreground/80 hover:bg-sidebar-accent/30 hover:text-sidebar-foreground', className),
|
|
153
151
|
style: {
|
|
154
152
|
paddingLeft: indent > 0 ? `${indent * 12 + 12}px` : undefined
|
|
155
153
|
},
|
|
@@ -1,31 +1,33 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import {
|
|
2
|
+
import { useCallback, useMemo } from 'react';
|
|
3
|
+
import { usePathname } from 'next/navigation';
|
|
3
4
|
/**
|
|
4
|
-
* Parse URL
|
|
5
|
+
* Parse URL pathname into route state
|
|
5
6
|
*
|
|
6
7
|
* URL Schema:
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
12
|
-
* -
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
* - / → Home, show default content
|
|
9
|
+
* - /[tab] → Tab only, show default content (docs view)
|
|
10
|
+
* - /[tab]/page/[slug] → Doc page (docs view)
|
|
11
|
+
* - /[tab]/endpoint/[id] → API endpoint (docs view)
|
|
12
|
+
* - /[tab]/endpoint/[id]/playground → API endpoint (playground view)
|
|
13
|
+
* - /[tab]/endpoint/[id]/notes → API endpoint (notes view)
|
|
14
|
+
* - /[tab]/page/[slug]/notes → Doc page (notes view)
|
|
15
|
+
*/ function parsePath(pathname) {
|
|
16
|
+
// Remove leading slash
|
|
17
|
+
const cleanPath = pathname.startsWith('/') ? pathname.slice(1) : pathname;
|
|
16
18
|
// Default state
|
|
17
19
|
const defaultState = {
|
|
18
20
|
tab: null,
|
|
19
21
|
contentType: null,
|
|
20
22
|
contentId: null,
|
|
21
23
|
view: 'docs',
|
|
22
|
-
|
|
24
|
+
path: cleanPath
|
|
23
25
|
};
|
|
24
|
-
if (!
|
|
26
|
+
if (!cleanPath) {
|
|
25
27
|
return defaultState;
|
|
26
28
|
}
|
|
27
|
-
// Split
|
|
28
|
-
const parts =
|
|
29
|
+
// Split path into parts: tab/type/id[/view]
|
|
30
|
+
const parts = cleanPath.split('/');
|
|
29
31
|
const tab = parts[0] || null;
|
|
30
32
|
const type = parts[1] || null;
|
|
31
33
|
// Check if last part is a view mode
|
|
@@ -49,20 +51,14 @@ import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
|
49
51
|
contentType,
|
|
50
52
|
contentId: id,
|
|
51
53
|
view,
|
|
52
|
-
|
|
54
|
+
path: cleanPath
|
|
53
55
|
};
|
|
54
56
|
}
|
|
55
|
-
/**
|
|
56
|
-
* Get the current hash from window.location
|
|
57
|
-
*/ function getCurrentHash() {
|
|
58
|
-
if (typeof window === 'undefined') return '';
|
|
59
|
-
return window.location.hash;
|
|
60
|
-
}
|
|
61
57
|
/**
|
|
62
58
|
* Hook that provides URL-based route state
|
|
63
59
|
*
|
|
64
60
|
* This hook is the single source of truth for page/content selection.
|
|
65
|
-
* It
|
|
61
|
+
* It uses Next.js pathname for routing (SEO-friendly URLs).
|
|
66
62
|
*
|
|
67
63
|
* @example
|
|
68
64
|
* const { tab, contentType, contentId } = useRouteState()
|
|
@@ -72,26 +68,11 @@ import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
|
72
68
|
* return <DocPage slug={contentId} />
|
|
73
69
|
* }
|
|
74
70
|
*/ export function useRouteState() {
|
|
75
|
-
const
|
|
76
|
-
// Parse
|
|
77
|
-
const state = useMemo(()=>
|
|
78
|
-
|
|
71
|
+
const pathname = usePathname();
|
|
72
|
+
// Parse pathname into route state
|
|
73
|
+
const state = useMemo(()=>parsePath(pathname), [
|
|
74
|
+
pathname
|
|
79
75
|
]);
|
|
80
|
-
// Listen for hash changes
|
|
81
|
-
useEffect(()=>{
|
|
82
|
-
const handleHashChange = ()=>{
|
|
83
|
-
setHash(getCurrentHash());
|
|
84
|
-
};
|
|
85
|
-
// Listen for both hashchange and popstate (browser back/forward)
|
|
86
|
-
window.addEventListener('hashchange', handleHashChange);
|
|
87
|
-
window.addEventListener('popstate', handleHashChange);
|
|
88
|
-
// Set initial hash (in case it was set before this effect ran)
|
|
89
|
-
handleHashChange();
|
|
90
|
-
return ()=>{
|
|
91
|
-
window.removeEventListener('hashchange', handleHashChange);
|
|
92
|
-
window.removeEventListener('popstate', handleHashChange);
|
|
93
|
-
};
|
|
94
|
-
}, []);
|
|
95
76
|
return state;
|
|
96
77
|
}
|
|
97
78
|
/**
|
|
@@ -103,34 +84,41 @@ import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
|
103
84
|
const navigateToPage = useCallback((slug, tab, view)=>{
|
|
104
85
|
const targetTab = tab || defaultTab || '';
|
|
105
86
|
const viewSuffix = view && view !== 'docs' ? `/${view}` : '';
|
|
106
|
-
|
|
87
|
+
const path = targetTab ? `/${targetTab}/page/${slug}${viewSuffix}` : `/page/${slug}${viewSuffix}`;
|
|
88
|
+
window.history.pushState(null, '', path);
|
|
89
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
107
90
|
}, [
|
|
108
91
|
defaultTab
|
|
109
92
|
]);
|
|
110
93
|
const navigateToEndpoint = useCallback((id, tab, view)=>{
|
|
111
94
|
const targetTab = tab || defaultTab || '';
|
|
112
95
|
const viewSuffix = view && view !== 'docs' ? `/${view}` : '';
|
|
113
|
-
|
|
96
|
+
const path = targetTab ? `/${targetTab}/endpoint/${id}${viewSuffix}` : `/endpoint/${id}${viewSuffix}`;
|
|
97
|
+
window.history.pushState(null, '', path);
|
|
98
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
114
99
|
}, [
|
|
115
100
|
defaultTab
|
|
116
101
|
]);
|
|
117
102
|
const navigateToSection = useCallback((sectionId, tab)=>{
|
|
118
103
|
const targetTab = tab || defaultTab || '';
|
|
119
|
-
|
|
104
|
+
const path = targetTab ? `/${targetTab}/section/${sectionId}` : `/section/${sectionId}`;
|
|
105
|
+
window.history.pushState(null, '', path);
|
|
106
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
120
107
|
}, [
|
|
121
108
|
defaultTab
|
|
122
109
|
]);
|
|
123
110
|
const navigateToTab = useCallback((tab)=>{
|
|
124
|
-
window.
|
|
111
|
+
window.history.pushState(null, '', `/${tab}`);
|
|
112
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
125
113
|
}, []);
|
|
126
114
|
const clearSelection = useCallback((tab)=>{
|
|
127
115
|
const targetTab = tab || defaultTab;
|
|
128
116
|
if (targetTab) {
|
|
129
|
-
window.
|
|
117
|
+
window.history.pushState(null, '', `/${targetTab}`);
|
|
130
118
|
} else {
|
|
131
|
-
|
|
132
|
-
window.history.pushState(null, '', window.location.pathname + window.location.search);
|
|
119
|
+
window.history.pushState(null, '', '/');
|
|
133
120
|
}
|
|
121
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
134
122
|
}, [
|
|
135
123
|
defaultTab
|
|
136
124
|
]);
|
|
@@ -143,17 +131,17 @@ import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
|
143
131
|
};
|
|
144
132
|
}
|
|
145
133
|
/**
|
|
146
|
-
* Utility to build
|
|
147
|
-
*/ export const
|
|
134
|
+
* Utility to build URLs (for href attributes)
|
|
135
|
+
*/ export const buildUrl = {
|
|
148
136
|
page: (tab, slug, view)=>{
|
|
149
137
|
const viewSuffix = view && view !== 'docs' ? `/${view}` : '';
|
|
150
|
-
return
|
|
138
|
+
return `/${tab}/page/${slug}${viewSuffix}`;
|
|
151
139
|
},
|
|
152
140
|
endpoint: (tab, id, view)=>{
|
|
153
141
|
const viewSuffix = view && view !== 'docs' ? `/${view}` : '';
|
|
154
|
-
return
|
|
142
|
+
return `/${tab}/endpoint/${id}${viewSuffix}`;
|
|
155
143
|
},
|
|
156
|
-
section: (tab, sectionId)
|
|
157
|
-
tab: (tab)
|
|
144
|
+
section: (tab, sectionId)=>`/${tab}/section/${sectionId}`,
|
|
145
|
+
tab: (tab)=>`/${tab}`
|
|
158
146
|
};
|
|
159
|
-
export {
|
|
147
|
+
export { parsePath };
|