@brainfish-ai/devdoc 0.1.43 → 0.1.44

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 (35) hide show
  1. package/dist/cli/commands/create.js +2 -2
  2. package/package.json +1 -1
  3. package/renderer/app/api/collections/route.js +35 -4
  4. package/renderer/app/api/suggestions/route.js +33 -13
  5. package/renderer/app/globals.css +69 -0
  6. package/renderer/app/layout.js +2 -2
  7. package/renderer/app/llms-full.txt/route.js +10 -1
  8. package/renderer/app/llms.txt/route.js +10 -1
  9. package/renderer/app/sitemap.xml/route.js +11 -1
  10. package/renderer/components/docs/mdx/cards.js +1 -1
  11. package/renderer/components/docs/mdx/landing.js +7 -5
  12. package/renderer/components/docs-viewer/agent/agent-chat.js +13 -112
  13. package/renderer/components/docs-viewer/agent/agent-popup-button.js +99 -0
  14. package/renderer/components/docs-viewer/agent/index.js +3 -0
  15. package/renderer/components/docs-viewer/content/content-router.js +182 -0
  16. package/renderer/components/docs-viewer/content/doc-page.js +31 -5
  17. package/renderer/components/docs-viewer/content/index.js +2 -0
  18. package/renderer/components/docs-viewer/index.js +381 -485
  19. package/renderer/components/docs-viewer/playground/graphql-playground.js +205 -3
  20. package/renderer/components/docs-viewer/sidebar/right-sidebar.js +35 -39
  21. package/renderer/components/theme-toggle.js +1 -21
  22. package/renderer/hooks/use-route-state.js +159 -0
  23. package/renderer/lib/api-docs/agent/use-suggestions.js +97 -0
  24. package/renderer/lib/api-docs/code-editor/mode-context.js +61 -89
  25. package/renderer/lib/api-docs/mobile-context.js +40 -3
  26. package/renderer/lib/docs/config/environment.js +38 -0
  27. package/renderer/lib/docs/config/index.js +1 -0
  28. package/renderer/lib/docs/config/schema.js +17 -5
  29. package/renderer/lib/docs/mdx/compiler.js +5 -2
  30. package/renderer/lib/docs/mdx/index.js +2 -0
  31. package/renderer/lib/docs/mdx/remark-mermaid.js +63 -0
  32. package/renderer/lib/docs/navigation/index.js +1 -2
  33. package/renderer/lib/docs/navigation/types.js +3 -1
  34. package/renderer/lib/docs-navigation.js +140 -0
  35. 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, useRef } from 'react';
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
- // Helper to parse notes path from URL hash
6
- function parseNotesHash() {
7
- if (typeof window === 'undefined') return {
8
- isNotes: false,
9
- filePath: null
10
- };
11
- const hash = window.location.hash.slice(1) // Remove #
12
- ;
13
- if (hash === 'notes') {
14
- return {
15
- isNotes: true,
16
- filePath: null
17
- };
18
- }
19
- if (hash.startsWith('notes/')) {
20
- // Decode any encoded characters but path should mostly be plain text
21
- const filePath = decodeURIComponent(hash.slice(6)) // Remove 'notes/'
22
- ;
23
- return {
24
- isNotes: true,
25
- filePath: filePath || null
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
- // Helper to update URL hash for notes
34
- // Only encode special chars that would break the URL, keep slashes readable
35
- function updateNotesHash(filePath) {
36
- if (typeof window === 'undefined') return;
37
- const newHash = filePath ? `#notes/${filePath.replace(/#/g, '%23').replace(/\?/g, '%3F')}` : '#notes';
38
- window.history.pushState(null, '', newHash);
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
- const [mode, setMode] = useState(defaultMode);
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
- // Use ref to track mode for callbacks (avoids stale closure issues)
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 handlePopState = ()=>{
65
- const { isNotes, filePath } = parseNotesHash();
66
- if (isNotes) {
67
- setMode('notes');
68
- modeRef.current = 'notes';
69
- setActiveFilePathState(filePath);
70
- } else {
71
- // Default to docs mode when no notes hash
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 that also updates URL
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
- // Update URL if we're in notes mode (use ref to avoid stale closure)
85
- if (modeRef.current === 'notes') {
86
- updateNotesHash(cleanPath);
87
- }
88
- }, []) // No dependencies - uses ref
89
- ;
72
+ }, []);
90
73
  const switchToNotes = useCallback((openFile)=>{
91
- const filePath = openFile || null;
92
- setActiveFilePathState(filePath);
93
- setMode('notes');
94
- modeRef.current = 'notes';
95
- updateNotesHash(filePath);
74
+ if (openFile) {
75
+ setActiveFilePathState(openFile);
76
+ }
77
+ switchView('notes');
96
78
  }, []);
97
79
  const switchToApiClient = useCallback(()=>{
98
- setMode('api_client');
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
- setMode('docs');
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 sidebars when switching to desktop
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)=>!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 * from './types';
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
- * A navigation item representing a single page
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
+ }
@@ -33,6 +33,7 @@
33
33
  "dexie": "^4.0.10",
34
34
  "fflate": "^0.8.2",
35
35
  "graphql": "^16.12.0",
36
+ "graphql-language-service": "^5.5.0",
36
37
  "gray-matter": "^4.0.3",
37
38
  "js-yaml": "^4.1.0",
38
39
  "mermaid": "^11.12.2",