jamdesk 1.0.13 → 1.0.14
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/README.md +89 -4
- package/dist/__tests__/unit/auth.test.d.ts +2 -0
- package/dist/__tests__/unit/auth.test.d.ts.map +1 -0
- package/dist/__tests__/unit/auth.test.js +169 -0
- package/dist/__tests__/unit/auth.test.js.map +1 -0
- package/dist/__tests__/unit/config.test.d.ts +2 -0
- package/dist/__tests__/unit/config.test.d.ts.map +1 -0
- package/dist/__tests__/unit/config.test.js +76 -0
- package/dist/__tests__/unit/config.test.js.map +1 -0
- package/dist/__tests__/unit/deploy.test.d.ts +2 -0
- package/dist/__tests__/unit/deploy.test.d.ts.map +1 -0
- package/dist/__tests__/unit/deploy.test.js +273 -0
- package/dist/__tests__/unit/deploy.test.js.map +1 -0
- package/dist/__tests__/unit/deps-sync.test.js +3 -1
- package/dist/__tests__/unit/deps-sync.test.js.map +1 -1
- package/dist/__tests__/unit/dev-loading-server.test.d.ts +2 -0
- package/dist/__tests__/unit/dev-loading-server.test.d.ts.map +1 -0
- package/dist/__tests__/unit/dev-loading-server.test.js +141 -0
- package/dist/__tests__/unit/dev-loading-server.test.js.map +1 -0
- package/dist/__tests__/unit/docs-json-writer.test.d.ts +2 -0
- package/dist/__tests__/unit/docs-json-writer.test.d.ts.map +1 -0
- package/dist/__tests__/unit/docs-json-writer.test.js +71 -0
- package/dist/__tests__/unit/docs-json-writer.test.js.map +1 -0
- package/dist/__tests__/unit/loading-page.test.d.ts +2 -0
- package/dist/__tests__/unit/loading-page.test.d.ts.map +1 -0
- package/dist/__tests__/unit/loading-page.test.js +73 -0
- package/dist/__tests__/unit/loading-page.test.js.map +1 -0
- package/dist/__tests__/unit/login.test.d.ts +2 -0
- package/dist/__tests__/unit/login.test.d.ts.map +1 -0
- package/dist/__tests__/unit/login.test.js +100 -0
- package/dist/__tests__/unit/login.test.js.map +1 -0
- package/dist/__tests__/unit/logout.test.d.ts +2 -0
- package/dist/__tests__/unit/logout.test.d.ts.map +1 -0
- package/dist/__tests__/unit/logout.test.js +39 -0
- package/dist/__tests__/unit/logout.test.js.map +1 -0
- package/dist/__tests__/unit/tarball.test.d.ts +2 -0
- package/dist/__tests__/unit/tarball.test.d.ts.map +1 -0
- package/dist/__tests__/unit/tarball.test.js +126 -0
- package/dist/__tests__/unit/tarball.test.js.map +1 -0
- package/dist/__tests__/unit/whoami.test.d.ts +2 -0
- package/dist/__tests__/unit/whoami.test.d.ts.map +1 -0
- package/dist/__tests__/unit/whoami.test.js +47 -0
- package/dist/__tests__/unit/whoami.test.js.map +1 -0
- package/dist/commands/deploy.d.ts +13 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +265 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +48 -25
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/login.d.ts +8 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +135 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +5 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logout.js +17 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/whoami.d.ts +5 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/whoami.js +24 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/index.js +50 -7
- package/dist/index.js.map +1 -1
- package/dist/lib/auth.d.ts +34 -0
- package/dist/lib/auth.d.ts.map +1 -0
- package/dist/lib/auth.js +105 -0
- package/dist/lib/auth.js.map +1 -0
- package/dist/lib/config.d.ts +9 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +7 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/dev-loading-server.d.ts +22 -0
- package/dist/lib/dev-loading-server.d.ts.map +1 -0
- package/dist/lib/dev-loading-server.js +117 -0
- package/dist/lib/dev-loading-server.js.map +1 -0
- package/dist/lib/docs-config.d.ts +1 -0
- package/dist/lib/docs-config.d.ts.map +1 -1
- package/dist/lib/docs-config.js.map +1 -1
- package/dist/lib/docs-json-writer.d.ts +2 -0
- package/dist/lib/docs-json-writer.d.ts.map +1 -0
- package/dist/lib/docs-json-writer.js +35 -0
- package/dist/lib/docs-json-writer.js.map +1 -0
- package/dist/lib/loading-page.d.ts +11 -0
- package/dist/lib/loading-page.d.ts.map +1 -0
- package/dist/lib/loading-page.js +222 -0
- package/dist/lib/loading-page.js.map +1 -0
- package/dist/lib/output.d.ts +13 -5
- package/dist/lib/output.d.ts.map +1 -1
- package/dist/lib/output.js +22 -5
- package/dist/lib/output.js.map +1 -1
- package/dist/lib/tarball.d.ts +28 -0
- package/dist/lib/tarball.d.ts.map +1 -0
- package/dist/lib/tarball.js +117 -0
- package/dist/lib/tarball.js.map +1 -0
- package/package.json +5 -2
- package/vendored/app/[[...slug]]/page.tsx +6 -20
- package/vendored/app/api/chat/[project]/route.ts +323 -0
- package/vendored/app/api/mcp/[project]/route.ts +2 -63
- package/vendored/components/chat/ChatCodeBlock.tsx +63 -0
- package/vendored/components/chat/ChatEmptyState.tsx +79 -0
- package/vendored/components/chat/ChatFAB.tsx +36 -0
- package/vendored/components/chat/ChatInput.tsx +106 -0
- package/vendored/components/chat/ChatMessage.tsx +176 -0
- package/vendored/components/chat/ChatPanel.tsx +206 -0
- package/vendored/components/chat/ChatResizeHandle.tsx +108 -0
- package/vendored/components/chat/LazyChatPanel.tsx +19 -0
- package/vendored/components/layout/LayoutWrapper.tsx +134 -44
- package/vendored/components/layout/PageColumns.tsx +40 -0
- package/vendored/components/navigation/Header.tsx +74 -29
- package/vendored/components/navigation/Sidebar.tsx +17 -2
- package/vendored/hooks/useChat.ts +335 -0
- package/vendored/hooks/useChatPanel.tsx +101 -0
- package/vendored/lib/anthropic-client.ts +19 -0
- package/vendored/lib/build/extract-tarball.ts +150 -0
- package/vendored/lib/chat-prompt.ts +56 -0
- package/vendored/lib/docs-types.ts +14 -0
- package/vendored/lib/docs.ts +22 -4
- package/vendored/lib/embedding-chunker.ts +173 -0
- package/vendored/lib/generate-starter-questions.ts +98 -0
- package/vendored/lib/isr-build-executor.ts +2 -1
- package/vendored/lib/middleware-helpers.ts +21 -0
- package/vendored/lib/route-helpers.ts +96 -0
- package/vendored/lib/snippet-loader-isr.ts +107 -1
- package/vendored/lib/static-artifacts.ts +3 -2
- package/vendored/lib/validate-config.ts +1 -0
- package/vendored/lib/vector-store.ts +213 -0
- package/vendored/schema/docs-schema.json +33 -0
- package/vendored/scripts/dev-project.cjs +6 -0
- package/vendored/shared/types.ts +6 -5
- package/vendored/tailwind.config.ts +9 -0
- package/vendored/themes/jam/variables.css +2 -2
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useRef, type KeyboardEvent } from 'react';
|
|
4
|
+
import { MIN_WIDTH, MAX_WIDTH } from '@/hooks/useChatPanel';
|
|
5
|
+
|
|
6
|
+
interface ChatResizeHandleProps {
|
|
7
|
+
chatWidth: number;
|
|
8
|
+
onResize: (width: number) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Vertical drag handle between content and chat column.
|
|
13
|
+
* Supports mouse, touch, and keyboard (ArrowLeft/ArrowRight) interaction.
|
|
14
|
+
*/
|
|
15
|
+
export function ChatResizeHandle({ chatWidth, onResize }: ChatResizeHandleProps) {
|
|
16
|
+
const rafRef = useRef<number | null>(null);
|
|
17
|
+
const isDraggingRef = useRef(false);
|
|
18
|
+
|
|
19
|
+
const startDrag = useCallback(
|
|
20
|
+
(startX: number) => {
|
|
21
|
+
const startWidth = chatWidth;
|
|
22
|
+
isDraggingRef.current = true;
|
|
23
|
+
document.body.style.userSelect = 'none';
|
|
24
|
+
document.body.style.cursor = 'col-resize';
|
|
25
|
+
|
|
26
|
+
const onMove = (clientX: number) => {
|
|
27
|
+
if (rafRef.current !== null) return;
|
|
28
|
+
rafRef.current = requestAnimationFrame(() => {
|
|
29
|
+
rafRef.current = null;
|
|
30
|
+
// Dragging left increases width (chat is on the right)
|
|
31
|
+
const delta = startX - clientX;
|
|
32
|
+
const newWidth = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, startWidth + delta));
|
|
33
|
+
onResize(newWidth);
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const onEnd = () => {
|
|
38
|
+
isDraggingRef.current = false;
|
|
39
|
+
document.body.style.userSelect = '';
|
|
40
|
+
document.body.style.cursor = '';
|
|
41
|
+
if (rafRef.current !== null) {
|
|
42
|
+
cancelAnimationFrame(rafRef.current);
|
|
43
|
+
rafRef.current = null;
|
|
44
|
+
}
|
|
45
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
46
|
+
document.removeEventListener('mouseup', onEnd);
|
|
47
|
+
document.removeEventListener('touchmove', handleTouchMove);
|
|
48
|
+
document.removeEventListener('touchend', onEnd);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const handleMouseMove = (e: MouseEvent) => onMove(e.clientX);
|
|
52
|
+
const handleTouchMove = (e: TouchEvent) => onMove(e.touches[0].clientX);
|
|
53
|
+
|
|
54
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
55
|
+
document.addEventListener('mouseup', onEnd);
|
|
56
|
+
document.addEventListener('touchmove', handleTouchMove, { passive: true });
|
|
57
|
+
document.addEventListener('touchend', onEnd);
|
|
58
|
+
},
|
|
59
|
+
[chatWidth, onResize],
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const handleMouseDown = useCallback(
|
|
63
|
+
(e: React.MouseEvent) => {
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
startDrag(e.clientX);
|
|
66
|
+
},
|
|
67
|
+
[startDrag],
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const handleTouchStart = useCallback(
|
|
71
|
+
(e: React.TouchEvent) => {
|
|
72
|
+
startDrag(e.touches[0].clientX);
|
|
73
|
+
},
|
|
74
|
+
[startDrag],
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const handleKeyDown = useCallback(
|
|
78
|
+
(e: KeyboardEvent<HTMLDivElement>) => {
|
|
79
|
+
const step = 10;
|
|
80
|
+
if (e.key === 'ArrowLeft') {
|
|
81
|
+
e.preventDefault();
|
|
82
|
+
onResize(Math.max(MIN_WIDTH, chatWidth - step));
|
|
83
|
+
} else if (e.key === 'ArrowRight') {
|
|
84
|
+
e.preventDefault();
|
|
85
|
+
onResize(Math.min(MAX_WIDTH, chatWidth + step));
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
[chatWidth, onResize],
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div
|
|
93
|
+
role="separator"
|
|
94
|
+
aria-orientation="vertical"
|
|
95
|
+
aria-valuenow={chatWidth}
|
|
96
|
+
aria-valuemin={MIN_WIDTH}
|
|
97
|
+
aria-valuemax={MAX_WIDTH}
|
|
98
|
+
aria-label="Resize chat panel"
|
|
99
|
+
tabIndex={0}
|
|
100
|
+
onMouseDown={handleMouseDown}
|
|
101
|
+
onTouchStart={handleTouchStart}
|
|
102
|
+
onKeyDown={handleKeyDown}
|
|
103
|
+
className="hidden xl:flex items-center justify-center w-1 flex-shrink-0 cursor-col-resize group focus-visible:outline-2 focus-visible:outline-[var(--color-accent)]"
|
|
104
|
+
>
|
|
105
|
+
<div className="w-px h-full bg-[var(--color-border)] group-hover:bg-[var(--color-accent)]/50 transition-colors" />
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import dynamic from 'next/dynamic';
|
|
4
|
+
import type { ComponentProps } from 'react';
|
|
5
|
+
import type { ChatPanel as ChatPanelType } from './ChatPanel';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Code-split ChatPanel without visible loading state.
|
|
9
|
+
* The panel is rendered unconditionally (with isOpen=false initially),
|
|
10
|
+
* so a loading fallback would flash on every page load.
|
|
11
|
+
*/
|
|
12
|
+
const ChatPanel = dynamic(
|
|
13
|
+
() => import('./ChatPanel').then((m) => m.ChatPanel),
|
|
14
|
+
{ ssr: false },
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
export function LazyChatPanel(props: ComponentProps<typeof ChatPanelType>) {
|
|
18
|
+
return <ChatPanel {...props} />;
|
|
19
|
+
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState,
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
4
|
import { Header } from '@/components/navigation/Header';
|
|
5
5
|
import { Sidebar } from '@/components/navigation/Sidebar';
|
|
6
6
|
import { TabsNav } from '@/components/navigation/TabsNav';
|
|
7
|
+
import { LazyChatPanel } from '@/components/chat/LazyChatPanel';
|
|
8
|
+
import { ChatResizeHandle } from '@/components/chat/ChatResizeHandle';
|
|
9
|
+
import { ChatPanel } from '@/components/chat/ChatPanel';
|
|
10
|
+
import { ChatPanelProvider, useChatPanel } from '@/hooks/useChatPanel';
|
|
7
11
|
import { useHashNavigation } from '@/hooks/useHashNavigation';
|
|
8
12
|
import type { DocsConfig } from '@/lib/docs-types';
|
|
9
13
|
import { getTheme, type ThemeName } from '@/themes';
|
|
@@ -13,8 +17,33 @@ interface LayoutWrapperProps {
|
|
|
13
17
|
children: React.ReactNode;
|
|
14
18
|
}
|
|
15
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Hook to track a CSS media query match.
|
|
22
|
+
* Returns false during SSR/initial render to avoid hydration mismatch.
|
|
23
|
+
*/
|
|
24
|
+
function useMediaQuery(query: string): boolean {
|
|
25
|
+
const [matches, setMatches] = useState(false);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const mql = window.matchMedia(query);
|
|
29
|
+
setMatches(mql.matches);
|
|
30
|
+
const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
|
|
31
|
+
mql.addEventListener('change', handler);
|
|
32
|
+
return () => mql.removeEventListener('change', handler);
|
|
33
|
+
}, [query]);
|
|
34
|
+
|
|
35
|
+
return matches;
|
|
36
|
+
}
|
|
37
|
+
|
|
16
38
|
export function LayoutWrapper({ config, children }: LayoutWrapperProps) {
|
|
17
39
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
|
40
|
+
// Chat is on by default in production; projects can opt out with chat.enabled: false.
|
|
41
|
+
// Hidden in local dev — the chat API route needs Upstash Vector + Anthropic API.
|
|
42
|
+
const chatEnabled = process.env.NODE_ENV === 'production'
|
|
43
|
+
&& (config.chat?.enabled !== false);
|
|
44
|
+
|
|
45
|
+
// xl breakpoint (1280px) — chat renders inline on desktop, overlay on mobile
|
|
46
|
+
const isDesktop = useMediaQuery('(min-width: 1280px)');
|
|
18
47
|
|
|
19
48
|
// Handle hash navigation for three-column independent scroll layout
|
|
20
49
|
useHashNavigation();
|
|
@@ -42,22 +71,36 @@ export function LayoutWrapper({ config, children }: LayoutWrapperProps) {
|
|
|
42
71
|
}, []);
|
|
43
72
|
|
|
44
73
|
// Get theme config to determine layout
|
|
45
|
-
const themeConfig =
|
|
46
|
-
return getTheme(config.theme as ThemeName | undefined);
|
|
47
|
-
}, [config.theme]);
|
|
74
|
+
const themeConfig = getTheme(config.theme as ThemeName | undefined);
|
|
48
75
|
|
|
49
76
|
const layout = themeConfig.layout;
|
|
50
77
|
|
|
51
78
|
const toggleSidebar = () => setIsSidebarOpen(!isSidebarOpen);
|
|
52
79
|
const closeSidebar = () => setIsSidebarOpen(false);
|
|
53
80
|
|
|
81
|
+
// In local dev (no ISR_MODE), middleware doesn't rewrite /_chat, so use the direct API path
|
|
82
|
+
const chatEndpoint = process.env.NEXT_PUBLIC_PROJECT_SLUG
|
|
83
|
+
? `/api/chat/${process.env.NEXT_PUBLIC_PROJECT_SLUG}`
|
|
84
|
+
: '/_chat';
|
|
85
|
+
|
|
86
|
+
// Mobile (<xl) overlay — only when chat is NOT shown inline on desktop
|
|
87
|
+
const chatOverlay = chatEnabled && !isDesktop && <MobileChatOverlay />;
|
|
88
|
+
|
|
89
|
+
// Wrap in ChatPanelProvider so Header, PageColumns, and overlay can share state
|
|
90
|
+
const wrapWithProvider = (content: React.ReactNode) => (
|
|
91
|
+
<ChatPanelProvider
|
|
92
|
+
chatEnabled={chatEnabled}
|
|
93
|
+
chatEndpoint={chatEndpoint}
|
|
94
|
+
starterQuestions={config.chat?.starterQuestions}
|
|
95
|
+
>
|
|
96
|
+
{content}
|
|
97
|
+
</ChatPanelProvider>
|
|
98
|
+
);
|
|
99
|
+
|
|
54
100
|
// Sidebar-logo layout: Sidebar is full height, header only above content
|
|
55
|
-
// Tabs are rendered inline in the header for this layout
|
|
56
|
-
// Uses fixed viewport height on desktop with independent scroll for each column
|
|
57
101
|
if (layout === 'sidebar-logo') {
|
|
58
|
-
return (
|
|
102
|
+
return wrapWithProvider(
|
|
59
103
|
<div className="flex min-h-screen lg:h-screen lg:overflow-hidden">
|
|
60
|
-
{/* Full-height sidebar - already has its own scroll */}
|
|
61
104
|
<Sidebar
|
|
62
105
|
config={config}
|
|
63
106
|
layout={layout}
|
|
@@ -65,7 +108,6 @@ export function LayoutWrapper({ config, children }: LayoutWrapperProps) {
|
|
|
65
108
|
onClose={closeSidebar}
|
|
66
109
|
/>
|
|
67
110
|
|
|
68
|
-
{/* Content area with header (tabs are inline in header) */}
|
|
69
111
|
<div className="flex-1 lg:ml-[295px] flex flex-col lg:h-screen bg-[var(--color-bg-content,var(--color-bg-primary))]">
|
|
70
112
|
<Header
|
|
71
113
|
config={config}
|
|
@@ -73,7 +115,6 @@ export function LayoutWrapper({ config, children }: LayoutWrapperProps) {
|
|
|
73
115
|
isSidebarOpen={isSidebarOpen}
|
|
74
116
|
onToggleSidebar={toggleSidebar}
|
|
75
117
|
/>
|
|
76
|
-
{/* Main content area - contains content scroll container and TOC */}
|
|
77
118
|
<main
|
|
78
119
|
id="main-content"
|
|
79
120
|
className="flex-1 flex flex-col lg:min-h-0 transition-colors overflow-x-hidden"
|
|
@@ -81,48 +122,97 @@ export function LayoutWrapper({ config, children }: LayoutWrapperProps) {
|
|
|
81
122
|
{children}
|
|
82
123
|
</main>
|
|
83
124
|
</div>
|
|
84
|
-
|
|
125
|
+
|
|
126
|
+
{isDesktop && <DesktopChatColumn />}
|
|
127
|
+
{chatOverlay}
|
|
128
|
+
</div>,
|
|
85
129
|
);
|
|
86
130
|
}
|
|
87
131
|
|
|
88
|
-
// Header-logo layout (Jam, Nebula)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
{/* Header - stays at top, does not scroll */}
|
|
97
|
-
<Header
|
|
98
|
-
config={config}
|
|
99
|
-
layout={layout}
|
|
100
|
-
isSidebarOpen={isSidebarOpen}
|
|
101
|
-
onToggleSidebar={toggleSidebar}
|
|
102
|
-
/>
|
|
103
|
-
|
|
104
|
-
{/* Tabs bar - below header, same height */}
|
|
105
|
-
<TabsNav config={config} />
|
|
106
|
-
|
|
107
|
-
{/* Main layout area with sidebar and content - fills remaining height */}
|
|
108
|
-
<div className="flex flex-1 lg:min-h-0">
|
|
109
|
-
{/* Sidebar - has its own scroll via sidebar-scroll class */}
|
|
110
|
-
<Sidebar
|
|
132
|
+
// Header-logo layout (Jam, Nebula) — outer flex row for layout + chat column
|
|
133
|
+
return wrapWithProvider(
|
|
134
|
+
<div className="flex min-h-screen lg:h-screen lg:overflow-hidden">
|
|
135
|
+
<div
|
|
136
|
+
className="flex-1 min-w-0 flex flex-col lg:overflow-hidden mx-auto px-4 lg:px-6"
|
|
137
|
+
style={{ maxWidth: 'var(--layout-max-width, none)' }}
|
|
138
|
+
>
|
|
139
|
+
<Header
|
|
111
140
|
config={config}
|
|
112
141
|
layout={layout}
|
|
113
|
-
|
|
114
|
-
|
|
142
|
+
isSidebarOpen={isSidebarOpen}
|
|
143
|
+
onToggleSidebar={toggleSidebar}
|
|
115
144
|
/>
|
|
116
145
|
|
|
117
|
-
{
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
146
|
+
<TabsNav config={config} />
|
|
147
|
+
|
|
148
|
+
<div className="flex flex-1 lg:min-h-0">
|
|
149
|
+
<Sidebar
|
|
150
|
+
config={config}
|
|
151
|
+
layout={layout}
|
|
152
|
+
isOpen={isSidebarOpen}
|
|
153
|
+
onClose={closeSidebar}
|
|
154
|
+
/>
|
|
155
|
+
|
|
156
|
+
<main
|
|
157
|
+
id="main-content"
|
|
158
|
+
className="flex-1 flex flex-col lg:min-h-0 bg-[var(--color-bg-primary)] transition-colors overflow-x-hidden"
|
|
159
|
+
>
|
|
160
|
+
{children}
|
|
161
|
+
</main>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
{isDesktop && <DesktopChatColumn />}
|
|
166
|
+
{chatOverlay}
|
|
167
|
+
</div>,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Desktop chat column — renders at the outermost layout level for full page height.
|
|
173
|
+
* Uses ChatPanelProvider context for open/close state and width.
|
|
174
|
+
*/
|
|
175
|
+
function DesktopChatColumn() {
|
|
176
|
+
const { isChatOpen, setIsChatOpen, chatWidth, setChatWidth, chatEndpoint, starterQuestions } = useChatPanel();
|
|
177
|
+
|
|
178
|
+
if (!isChatOpen) return null;
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<>
|
|
182
|
+
<ChatResizeHandle chatWidth={chatWidth} onResize={setChatWidth} />
|
|
183
|
+
<div
|
|
184
|
+
className="flex flex-col flex-shrink-0"
|
|
185
|
+
style={{
|
|
186
|
+
width: chatWidth,
|
|
187
|
+
backgroundColor: 'var(--color-bg-primary)',
|
|
188
|
+
}}
|
|
189
|
+
data-chat-panel
|
|
190
|
+
>
|
|
191
|
+
<ChatPanel
|
|
192
|
+
mode="inline"
|
|
193
|
+
isOpen={true}
|
|
194
|
+
onClose={() => setIsChatOpen(false)}
|
|
195
|
+
starterQuestions={starterQuestions}
|
|
196
|
+
chatEndpoint={chatEndpoint}
|
|
197
|
+
/>
|
|
124
198
|
</div>
|
|
125
|
-
|
|
199
|
+
</>
|
|
126
200
|
);
|
|
127
201
|
}
|
|
128
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Mobile chat overlay — uses ChatPanelProvider context for open/close state.
|
|
205
|
+
* Separated so it can access the provider context.
|
|
206
|
+
*/
|
|
207
|
+
function MobileChatOverlay() {
|
|
208
|
+
const { isChatOpen, setIsChatOpen, chatEndpoint, starterQuestions } = useChatPanel();
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<LazyChatPanel
|
|
212
|
+
isOpen={isChatOpen}
|
|
213
|
+
onClose={() => setIsChatOpen(false)}
|
|
214
|
+
starterQuestions={starterQuestions}
|
|
215
|
+
chatEndpoint={chatEndpoint}
|
|
216
|
+
/>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { type ReactNode } from 'react';
|
|
4
|
+
import { useChatPanel } from '@/hooks/useChatPanel';
|
|
5
|
+
|
|
6
|
+
interface PageColumnsProps {
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
toc: ReactNode;
|
|
9
|
+
isWideMode?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Two-column page layout: content | TOC.
|
|
14
|
+
* On xl+ screens, the TOC is hidden when chat is open (chat renders at layout level).
|
|
15
|
+
* On <xl screens, only content is rendered (TOC hidden, chat is a mobile overlay).
|
|
16
|
+
*/
|
|
17
|
+
export function PageColumns({ children, toc, isWideMode }: PageColumnsProps) {
|
|
18
|
+
const { isChatOpen } = useChatPanel();
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="flex h-full">
|
|
22
|
+
{/* Content column - scrolls independently */}
|
|
23
|
+
<div
|
|
24
|
+
id="content-scroll-container"
|
|
25
|
+
className="flex-1 min-w-0 lg:overflow-y-auto lg:h-full content-scroll"
|
|
26
|
+
>
|
|
27
|
+
{children}
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
{/* TOC — shown when chat is closed and not in wide mode */}
|
|
31
|
+
{!isChatOpen && !isWideMode && (
|
|
32
|
+
<aside className="hidden xl:block w-72 flex-shrink-0 xl:h-full xl:overflow-y-auto toc-scroll xl:ml-0.5 pr-2">
|
|
33
|
+
<div className="py-6 sm:py-10 pr-4">
|
|
34
|
+
{toc}
|
|
35
|
+
</div>
|
|
36
|
+
</aside>
|
|
37
|
+
)}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -15,6 +15,7 @@ import { LanguageSelector } from './LanguageSelector';
|
|
|
15
15
|
import { resolveNavigation } from '@/lib/navigation-resolver';
|
|
16
16
|
import { getIconClass } from '@/lib/icon-utils';
|
|
17
17
|
import { useLinkPrefix } from '@/lib/link-prefix-context';
|
|
18
|
+
import { useChatPanel } from '@/hooks/useChatPanel';
|
|
18
19
|
|
|
19
20
|
interface HeaderProps {
|
|
20
21
|
config: DocsConfig;
|
|
@@ -27,7 +28,15 @@ interface HeaderProps {
|
|
|
27
28
|
// Max tabs to show before overflow dropdown
|
|
28
29
|
const MAX_VISIBLE_TABS = 4;
|
|
29
30
|
|
|
31
|
+
// Shared button classes for mobile search/AI icons
|
|
32
|
+
const MOBILE_ICON_BUTTON = 'lg:hidden p-2 text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] bg-[var(--color-bg-secondary)]/50 border border-[var(--color-border)]/50 hover:bg-[var(--color-bg-secondary)] rounded-md transition-colors cursor-pointer';
|
|
33
|
+
|
|
34
|
+
// Shared button classes for compact (Nebula) header icon buttons
|
|
35
|
+
const COMPACT_ICON_BUTTON = 'hidden md:flex p-2 hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] rounded-lg transition-colors cursor-pointer';
|
|
36
|
+
|
|
30
37
|
export function Header({ config, layout = 'header-logo', tabsPosition: tabsPositionProp, isSidebarOpen, onToggleSidebar }: HeaderProps) {
|
|
38
|
+
const { chatEnabled: chatContextEnabled, setIsChatOpen } = useChatPanel();
|
|
39
|
+
const onChatOpen = chatContextEnabled ? () => setIsChatOpen(true) : undefined;
|
|
31
40
|
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
|
32
41
|
const [logoError, setLogoError] = useState(false);
|
|
33
42
|
const [isDark, setIsDark] = useState(false);
|
|
@@ -275,7 +284,7 @@ export function Header({ config, layout = 'header-logo', tabsPosition: tabsPosit
|
|
|
275
284
|
{/* Hamburger menu - mobile only */}
|
|
276
285
|
<button
|
|
277
286
|
onClick={onToggleSidebar}
|
|
278
|
-
className="lg:hidden p-1.5 -ml-1.5 text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] rounded-lg transition-colors"
|
|
287
|
+
className="lg:hidden p-1.5 -ml-1.5 text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] rounded-lg transition-colors cursor-pointer"
|
|
279
288
|
aria-label="Toggle menu"
|
|
280
289
|
>
|
|
281
290
|
{isSidebarOpen ? (
|
|
@@ -297,28 +306,43 @@ export function Header({ config, layout = 'header-logo', tabsPosition: tabsPosit
|
|
|
297
306
|
)}
|
|
298
307
|
</div>
|
|
299
308
|
|
|
300
|
-
{/* Center: Search Bar - only in header-logo layout, not for compact search themes */}
|
|
309
|
+
{/* Center: Search Bar + Ask AI Button - only in header-logo layout, not for compact search themes */}
|
|
301
310
|
{showLogoInHeader && !useCompactSearch && (
|
|
302
311
|
<div className="flex-1 flex justify-center px-4">
|
|
303
|
-
<
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
<span
|
|
311
|
-
<
|
|
312
|
-
|
|
313
|
-
|
|
312
|
+
<div className="hidden md:flex items-center gap-2">
|
|
313
|
+
<button
|
|
314
|
+
onClick={() => setIsSearchOpen(true)}
|
|
315
|
+
className="flex items-center gap-3 px-3 py-2 bg-[var(--color-bg-secondary)] border border-[var(--color-border)] rounded-lg text-[var(--color-text-muted)] text-sm hover:border-[var(--color-border-hover)] transition-colors cursor-pointer"
|
|
316
|
+
aria-label="Search documentation"
|
|
317
|
+
>
|
|
318
|
+
<i className="fa-solid fa-magnifying-glass text-[14px] flex-shrink-0" aria-hidden="true" />
|
|
319
|
+
<span>Search</span>
|
|
320
|
+
<kbd className="hidden sm:inline-flex items-center gap-[2px] px-2 py-0.5 text-xs text-[var(--color-text-muted)] bg-[var(--color-bg-primary)] border border-[var(--color-border)] rounded">
|
|
321
|
+
<span className="text-sm leading-none">⌘</span>
|
|
322
|
+
<span>K</span>
|
|
323
|
+
</kbd>
|
|
324
|
+
</button>
|
|
325
|
+
{onChatOpen && (
|
|
326
|
+
<button
|
|
327
|
+
onClick={onChatOpen}
|
|
328
|
+
className="flex items-center gap-3 px-3 py-2 bg-[var(--color-bg-secondary)] border border-[var(--color-border)] rounded-lg hover:border-[var(--color-border-hover)] cursor-pointer transition-colors text-sm text-[var(--color-text-muted)]"
|
|
329
|
+
aria-label="Ask AI"
|
|
330
|
+
title="Ask AI (⌘I)"
|
|
331
|
+
>
|
|
332
|
+
<i className="fa-solid fa-sparkles text-[14px] text-[var(--color-accent)]" aria-hidden="true" />
|
|
333
|
+
<span>Ask AI</span>
|
|
334
|
+
<kbd className="hidden sm:inline-flex items-center gap-[2px] px-2 py-0.5 text-xs text-[var(--color-text-muted)] bg-[var(--color-bg-primary)] border border-[var(--color-border)] rounded">
|
|
335
|
+
<span className="text-sm leading-none">⌘</span>
|
|
336
|
+
<span>I</span>
|
|
337
|
+
</kbd>
|
|
338
|
+
</button>
|
|
339
|
+
)}
|
|
340
|
+
</div>
|
|
314
341
|
</div>
|
|
315
342
|
)}
|
|
316
343
|
|
|
317
|
-
{/* Spacer
|
|
318
|
-
{showLogoInHeader
|
|
319
|
-
|
|
320
|
-
{/* Spacer for sidebar-logo layout */}
|
|
321
|
-
{!showLogoInHeader && <div className="flex-1" />}
|
|
344
|
+
{/* Spacer — needed when the center search bar is not shown (compact themes or sidebar-logo layout) */}
|
|
345
|
+
{(!showLogoInHeader || useCompactSearch) && <div className="flex-1" />}
|
|
322
346
|
|
|
323
347
|
{/* Right side: Search icon (mobile) + Theme toggle */}
|
|
324
348
|
<div className="flex items-center gap-2 flex-shrink-0">
|
|
@@ -425,14 +449,23 @@ export function Header({ config, layout = 'header-logo', tabsPosition: tabsPosit
|
|
|
425
449
|
</nav>
|
|
426
450
|
)}
|
|
427
451
|
|
|
428
|
-
{/* Search
|
|
452
|
+
{/* Search + AI icons - mobile only */}
|
|
429
453
|
<button
|
|
430
454
|
onClick={() => setIsSearchOpen(true)}
|
|
431
|
-
className=
|
|
455
|
+
className={MOBILE_ICON_BUTTON}
|
|
432
456
|
aria-label="Search"
|
|
433
457
|
>
|
|
434
458
|
<i className="fa-solid fa-magnifying-glass text-[16px]" aria-hidden="true" />
|
|
435
459
|
</button>
|
|
460
|
+
{onChatOpen && (
|
|
461
|
+
<button
|
|
462
|
+
onClick={onChatOpen}
|
|
463
|
+
className={MOBILE_ICON_BUTTON}
|
|
464
|
+
aria-label="Ask AI"
|
|
465
|
+
>
|
|
466
|
+
<i className="fa-solid fa-sparkles text-[16px] text-[var(--color-accent)]" aria-hidden="true" />
|
|
467
|
+
</button>
|
|
468
|
+
)}
|
|
436
469
|
|
|
437
470
|
{/* Navbar Links - shown to left of theme toggle in header-logo layout */}
|
|
438
471
|
{showLogoInHeader && config.navbar?.links && config.navbar.links.length > 0 && (
|
|
@@ -481,16 +514,28 @@ export function Header({ config, layout = 'header-logo', tabsPosition: tabsPosit
|
|
|
481
514
|
/>
|
|
482
515
|
)}
|
|
483
516
|
|
|
484
|
-
{/* Compact search
|
|
517
|
+
{/* Compact search + AI icons - for themes like Nebula that use icon-only buttons */}
|
|
485
518
|
{showLogoInHeader && useCompactSearch && (
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
519
|
+
<>
|
|
520
|
+
<button
|
|
521
|
+
onClick={() => setIsSearchOpen(true)}
|
|
522
|
+
className={`${COMPACT_ICON_BUTTON} text-[var(--color-text-secondary)]`}
|
|
523
|
+
aria-label="Search"
|
|
524
|
+
title="Search (⌘K)"
|
|
525
|
+
>
|
|
526
|
+
<i className="fa-solid fa-magnifying-glass text-[16px]" aria-hidden="true" />
|
|
527
|
+
</button>
|
|
528
|
+
{onChatOpen && (
|
|
529
|
+
<button
|
|
530
|
+
onClick={onChatOpen}
|
|
531
|
+
className={`${COMPACT_ICON_BUTTON} text-[var(--color-accent)]`}
|
|
532
|
+
aria-label="Ask AI"
|
|
533
|
+
title="Ask AI (⌘I)"
|
|
534
|
+
>
|
|
535
|
+
<i className="fa-solid fa-sparkles text-[16px]" aria-hidden="true" />
|
|
536
|
+
</button>
|
|
537
|
+
)}
|
|
538
|
+
</>
|
|
494
539
|
)}
|
|
495
540
|
|
|
496
541
|
{/* Theme toggle - hidden on mobile since it's in the sidebar menu */}
|
|
@@ -29,6 +29,7 @@ import { ThemeToggle, ThemeToggleCycle } from '@/components/theme/ThemeToggle';
|
|
|
29
29
|
import { getIconClass } from '@/lib/icon-utils';
|
|
30
30
|
import { getTabsFromConfig } from '@/lib/navigation-utils';
|
|
31
31
|
import { useLinkPrefix } from '@/lib/link-prefix-context';
|
|
32
|
+
import { useChatPanel } from '@/hooks/useChatPanel';
|
|
32
33
|
|
|
33
34
|
interface SidebarProps {
|
|
34
35
|
config: DocsConfig;
|
|
@@ -147,6 +148,10 @@ export function Sidebar({ config, layout = 'header-logo', tabsPosition: tabsPosi
|
|
|
147
148
|
const sidebarRef = useRef<HTMLElement>(null);
|
|
148
149
|
const linkPrefix = useLinkPrefix();
|
|
149
150
|
|
|
151
|
+
// Chat panel state — used for Pulsar sidebar AI button
|
|
152
|
+
const { chatEnabled, setIsChatOpen } = useChatPanel();
|
|
153
|
+
const onChatOpen = chatEnabled ? () => setIsChatOpen(true) : undefined;
|
|
154
|
+
|
|
150
155
|
// Optimistic navigation: immediately highlight clicked link while page loads.
|
|
151
156
|
// Strips hash fragments so anchor links (e.g. /page#section) match pathname.
|
|
152
157
|
const handleNavigate = useCallback((url: string) => {
|
|
@@ -692,10 +697,10 @@ export function Sidebar({ config, layout = 'header-logo', tabsPosition: tabsPosi
|
|
|
692
697
|
|
|
693
698
|
{/* Search button in sidebar - for sidebar-logo layout (desktop only, mobile uses header search) */}
|
|
694
699
|
{showSearchInSidebar && (
|
|
695
|
-
<div className="hidden lg:
|
|
700
|
+
<div className="hidden lg:flex items-center gap-2 px-4 pt-4 pb-4">
|
|
696
701
|
<button
|
|
697
702
|
onClick={() => setIsSearchOpen(true)}
|
|
698
|
-
className="
|
|
703
|
+
className="flex-1 flex items-center gap-2.5 px-3 py-2 bg-[var(--color-bg-secondary)] border border-[var(--color-border)] rounded-lg text-[var(--color-text-muted)] text-sm hover:border-[var(--color-border-hover)] hover:bg-[var(--color-bg-tertiary)] transition-colors cursor-pointer"
|
|
699
704
|
>
|
|
700
705
|
<i className="fa-solid fa-magnifying-glass text-[14px] flex-shrink-0" aria-hidden="true" />
|
|
701
706
|
<span className="flex-1 text-left">Search...</span>
|
|
@@ -704,6 +709,16 @@ export function Sidebar({ config, layout = 'header-logo', tabsPosition: tabsPosi
|
|
|
704
709
|
<span>K</span>
|
|
705
710
|
</kbd>
|
|
706
711
|
</button>
|
|
712
|
+
{onChatOpen && (
|
|
713
|
+
<button
|
|
714
|
+
onClick={onChatOpen}
|
|
715
|
+
className="flex-shrink-0 flex items-center justify-center w-9 h-9 bg-[var(--color-bg-secondary)] border border-[var(--color-border)] rounded-lg text-[var(--color-text-muted)] hover:border-[var(--color-border-hover)] hover:bg-[var(--color-bg-tertiary)] transition-colors cursor-pointer"
|
|
716
|
+
aria-label="Ask AI"
|
|
717
|
+
title="Ask AI (⌘I)"
|
|
718
|
+
>
|
|
719
|
+
<i className="fa-solid fa-sparkles text-[14px] text-[var(--color-accent)]" aria-hidden="true" />
|
|
720
|
+
</button>
|
|
721
|
+
)}
|
|
707
722
|
</div>
|
|
708
723
|
)}
|
|
709
724
|
|