@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.
Files changed (36) hide show
  1. package/dist/cli/commands/deploy.js +16 -11
  2. package/package.json +1 -1
  3. package/renderer/app/[...slug]/client.js +17 -0
  4. package/renderer/app/[...slug]/page.js +125 -0
  5. package/renderer/app/api/assets/[...path]/route.js +23 -4
  6. package/renderer/app/api/chat/route.js +188 -25
  7. package/renderer/app/api/collections/route.js +95 -2
  8. package/renderer/app/api/deploy/route.js +4 -0
  9. package/renderer/app/api/suggestions/route.js +98 -10
  10. package/renderer/app/globals.css +33 -0
  11. package/renderer/app/layout.js +83 -8
  12. package/renderer/components/docs/mdx/cards.js +16 -45
  13. package/renderer/components/docs/mdx/file-tree.js +102 -0
  14. package/renderer/components/docs/mdx/index.js +7 -0
  15. package/renderer/components/docs-viewer/agent/agent-chat.js +75 -11
  16. package/renderer/components/docs-viewer/agent/messages/assistant-message.js +67 -3
  17. package/renderer/components/docs-viewer/agent/messages/tool-call-display.js +49 -4
  18. package/renderer/components/docs-viewer/content/content-router.js +1 -1
  19. package/renderer/components/docs-viewer/content/doc-page.js +36 -28
  20. package/renderer/components/docs-viewer/index.js +223 -58
  21. package/renderer/components/docs-viewer/playground/graphql-playground.js +131 -33
  22. package/renderer/components/docs-viewer/shared/method-badge.js +11 -2
  23. package/renderer/components/docs-viewer/sidebar/collection-tree.js +44 -6
  24. package/renderer/components/docs-viewer/sidebar/index.js +2 -1
  25. package/renderer/components/docs-viewer/sidebar/right-sidebar.js +3 -1
  26. package/renderer/components/docs-viewer/sidebar/sidebar-item.js +5 -7
  27. package/renderer/hooks/use-route-state.js +44 -56
  28. package/renderer/lib/api-docs/agent/indexer.js +73 -12
  29. package/renderer/lib/api-docs/agent/use-suggestions.js +26 -16
  30. package/renderer/lib/api-docs/code-editor/mode-context.js +16 -18
  31. package/renderer/lib/api-docs/parsers/openapi/transformer.js +8 -1
  32. package/renderer/lib/cache/purge.js +98 -0
  33. package/renderer/lib/docs-link-utils.js +146 -0
  34. package/renderer/lib/docs-navigation-context.js +3 -2
  35. package/renderer/lib/docs-navigation.js +50 -41
  36. 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
- response && /*#__PURE__*/ _jsxs(Tooltip, {
845
+ /*#__PURE__*/ _jsxs("div", {
846
+ className: "flex items-center gap-1",
817
847
  children: [
818
- /*#__PURE__*/ _jsx(TooltipTrigger, {
819
- asChild: true,
820
- children: /*#__PURE__*/ _jsx(Button, {
821
- variant: "ghost",
822
- size: "sm",
823
- onClick: copyResponse,
824
- className: "h-7 px-2",
825
- children: copied ? /*#__PURE__*/ _jsxs(_Fragment, {
826
- children: [
827
- /*#__PURE__*/ _jsx(Check, {
828
- className: "h-3.5 w-3.5 mr-1 text-emerald-500"
829
- }),
830
- /*#__PURE__*/ _jsx("span", {
831
- className: "text-xs",
832
- children: "Copied"
833
- })
834
- ]
835
- }) : /*#__PURE__*/ _jsxs(_Fragment, {
836
- children: [
837
- /*#__PURE__*/ _jsx(Copy, {
838
- className: "h-3.5 w-3.5 mr-1"
839
- }),
840
- /*#__PURE__*/ _jsx("span", {
841
- className: "text-xs",
842
- children: "Copy"
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__*/ _jsx(TooltipContent, {
849
- children: "Copy response"
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', methodColors[method], sizeClasses[size], className),
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
- return /*#__PURE__*/ _jsxs(_Fragment, {
176
+ const treeContent = /*#__PURE__*/ _jsxs(_Fragment, {
153
177
  children: [
154
178
  filteredData.folders.map((folder)=>{
155
- const isExpanded = expandedFolders.has(folder.id);
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
- isExpanded ? /*#__PURE__*/ _jsx(FolderOpen, {
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: isExpanded,
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-background dark:bg-stone-800/50 pointer-events-none z-0",
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
- * Uses subtle sage/mint style when selected with smooth sliding animation
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 ? hasSliding ? 'docs-sidebar-item-active text-green-700 font-semibold dark:text-green-400' // No bg when sliding
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 { useState, useEffect, useCallback, useMemo } from 'react';
2
+ import { useCallback, useMemo } from 'react';
3
+ import { usePathname } from 'next/navigation';
3
4
  /**
4
- * Parse URL hash into route state
5
+ * Parse URL pathname into route state
5
6
  *
6
7
  * URL Schema:
7
- * - #tab Tab only, show default content (docs view)
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
- */ function parseHash(hash) {
14
- // Remove leading # if present
15
- const cleanHash = hash.startsWith('#') ? hash.slice(1) : hash;
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
- hash: cleanHash
24
+ path: cleanPath
23
25
  };
24
- if (!cleanHash) {
26
+ if (!cleanPath) {
25
27
  return defaultState;
26
28
  }
27
- // Split hash into parts: tab/type/id[/view]
28
- const parts = cleanHash.split('/');
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
- hash: cleanHash
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 automatically updates when the URL hash changes (via navigation or browser back/forward).
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 [hash, setHash] = useState(()=>getCurrentHash());
76
- // Parse hash into route state
77
- const state = useMemo(()=>parseHash(hash), [
78
- hash
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
- window.location.hash = targetTab ? `${targetTab}/page/${slug}${viewSuffix}` : `page/${slug}${viewSuffix}`;
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
- window.location.hash = targetTab ? `${targetTab}/endpoint/${id}${viewSuffix}` : `endpoint/${id}${viewSuffix}`;
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
- window.location.hash = targetTab ? `${targetTab}/section/${sectionId}` : `section/${sectionId}`;
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.location.hash = tab;
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.location.hash = targetTab;
117
+ window.history.pushState(null, '', `/${targetTab}`);
130
118
  } else {
131
- // Remove hash entirely
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 hash URLs (for href attributes)
147
- */ export const buildHashUrl = {
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 `#${tab}/page/${slug}${viewSuffix}`;
138
+ return `/${tab}/page/${slug}${viewSuffix}`;
151
139
  },
152
140
  endpoint: (tab, id, view)=>{
153
141
  const viewSuffix = view && view !== 'docs' ? `/${view}` : '';
154
- return `#${tab}/endpoint/${id}${viewSuffix}`;
142
+ return `/${tab}/endpoint/${id}${viewSuffix}`;
155
143
  },
156
- section: (tab, sectionId)=>`#${tab}/section/${sectionId}`,
157
- tab: (tab)=>`#${tab}`
144
+ section: (tab, sectionId)=>`/${tab}/section/${sectionId}`,
145
+ tab: (tab)=>`/${tab}`
158
146
  };
159
- export { parseHash };
147
+ export { parsePath };