@nextclaw/ui 0.3.15 → 0.3.17
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/CHANGELOG.md +16 -0
- package/dist/assets/index-CPXV1dWr.js +337 -0
- package/dist/assets/index-Wn63frSd.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/App.tsx +2 -0
- package/src/api/config.ts +3 -2
- package/src/api/marketplace.ts +110 -0
- package/src/api/types.ts +85 -0
- package/src/components/config/ChannelsList.tsx +2 -2
- package/src/components/config/ModelConfig.tsx +3 -3
- package/src/components/config/ProvidersList.tsx +3 -3
- package/src/components/config/SessionsConfig.tsx +298 -252
- package/src/components/doc-browser/DocBrowser.tsx +272 -0
- package/src/components/doc-browser/DocBrowserContext.tsx +134 -0
- package/src/components/doc-browser/index.ts +3 -0
- package/src/components/doc-browser/useDocLinkInterceptor.ts +33 -0
- package/src/components/layout/AppLayout.tsx +25 -8
- package/src/components/layout/Sidebar.tsx +32 -5
- package/src/components/marketplace/MarketplacePage.tsx +408 -0
- package/src/components/ui/select.tsx +135 -0
- package/src/hooks/useMarketplace.ts +59 -0
- package/src/index.css +11 -4
- package/src/lib/i18n.ts +10 -1
- package/src/styles/design-system.css +256 -214
- package/dist/assets/index-AVHp7I-v.js +0 -266
- package/dist/assets/index-MxqXd9ts.css +0 -1
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
2
|
+
import { DOCS_DEFAULT_BASE_URL, useDocBrowser } from './DocBrowserContext';
|
|
3
|
+
import { cn } from '@/lib/utils';
|
|
4
|
+
import { t } from '@/lib/i18n';
|
|
5
|
+
import {
|
|
6
|
+
ArrowLeft,
|
|
7
|
+
ArrowRight,
|
|
8
|
+
X,
|
|
9
|
+
ExternalLink,
|
|
10
|
+
PanelRightOpen,
|
|
11
|
+
Maximize2,
|
|
12
|
+
GripVertical,
|
|
13
|
+
Search,
|
|
14
|
+
BookOpen,
|
|
15
|
+
} from 'lucide-react';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* DocBrowser — An in-app micro-browser for documentation.
|
|
19
|
+
*
|
|
20
|
+
* Supports two modes:
|
|
21
|
+
* - `docked`: Renders as a right sidebar panel (horizontally resizable)
|
|
22
|
+
* - `floating`: Renders as a draggable, resizable overlay
|
|
23
|
+
*/
|
|
24
|
+
export function DocBrowser() {
|
|
25
|
+
const {
|
|
26
|
+
isOpen, mode, currentUrl,
|
|
27
|
+
close, toggleMode,
|
|
28
|
+
goBack, goForward, canGoBack, canGoForward,
|
|
29
|
+
navigate,
|
|
30
|
+
} = useDocBrowser();
|
|
31
|
+
|
|
32
|
+
const [urlInput, setUrlInput] = useState('');
|
|
33
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
34
|
+
const [floatPos, setFloatPos] = useState({ x: 120, y: 80 });
|
|
35
|
+
const [floatSize, setFloatSize] = useState({ w: 480, h: 600 });
|
|
36
|
+
const [dockedWidth, setDockedWidth] = useState(420);
|
|
37
|
+
const dragRef = useRef<{ startX: number; startY: number; startPosX: number; startPosY: number } | null>(null);
|
|
38
|
+
const resizeRef = useRef<{ startX: number; startY: number; startW: number; startH: number } | null>(null);
|
|
39
|
+
const dockResizeRef = useRef<{ startX: number; startW: number } | null>(null);
|
|
40
|
+
|
|
41
|
+
// Sync URL input with current URL
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
try {
|
|
44
|
+
const parsed = new URL(currentUrl);
|
|
45
|
+
setUrlInput(parsed.pathname);
|
|
46
|
+
} catch {
|
|
47
|
+
setUrlInput(currentUrl);
|
|
48
|
+
}
|
|
49
|
+
}, [currentUrl]);
|
|
50
|
+
|
|
51
|
+
const handleUrlSubmit = useCallback((e: React.FormEvent) => {
|
|
52
|
+
e.preventDefault();
|
|
53
|
+
const input = urlInput.trim();
|
|
54
|
+
if (!input) return;
|
|
55
|
+
if (input.startsWith('/')) {
|
|
56
|
+
navigate(`${DOCS_DEFAULT_BASE_URL}${input}`);
|
|
57
|
+
} else if (input.startsWith('http')) {
|
|
58
|
+
navigate(input);
|
|
59
|
+
} else {
|
|
60
|
+
navigate(`${DOCS_DEFAULT_BASE_URL}/${input}`);
|
|
61
|
+
}
|
|
62
|
+
}, [urlInput, navigate]);
|
|
63
|
+
|
|
64
|
+
// --- Dragging logic (floating mode) ---
|
|
65
|
+
const onDragStart = useCallback((e: React.MouseEvent) => {
|
|
66
|
+
if (mode !== 'floating') return;
|
|
67
|
+
setIsDragging(true);
|
|
68
|
+
dragRef.current = {
|
|
69
|
+
startX: e.clientX,
|
|
70
|
+
startY: e.clientY,
|
|
71
|
+
startPosX: floatPos.x,
|
|
72
|
+
startPosY: floatPos.y,
|
|
73
|
+
};
|
|
74
|
+
}, [mode, floatPos]);
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (!isDragging) return;
|
|
78
|
+
const onMove = (e: MouseEvent) => {
|
|
79
|
+
if (!dragRef.current) return;
|
|
80
|
+
setFloatPos({
|
|
81
|
+
x: dragRef.current.startPosX + (e.clientX - dragRef.current.startX),
|
|
82
|
+
y: dragRef.current.startPosY + (e.clientY - dragRef.current.startY),
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
const onUp = () => {
|
|
86
|
+
setIsDragging(false);
|
|
87
|
+
dragRef.current = null;
|
|
88
|
+
};
|
|
89
|
+
window.addEventListener('mousemove', onMove);
|
|
90
|
+
window.addEventListener('mouseup', onUp);
|
|
91
|
+
return () => {
|
|
92
|
+
window.removeEventListener('mousemove', onMove);
|
|
93
|
+
window.removeEventListener('mouseup', onUp);
|
|
94
|
+
};
|
|
95
|
+
}, [isDragging]);
|
|
96
|
+
|
|
97
|
+
// --- Resize logic (floating mode — bottom-right corner) ---
|
|
98
|
+
const onResizeStart = useCallback((e: React.MouseEvent) => {
|
|
99
|
+
e.preventDefault();
|
|
100
|
+
e.stopPropagation();
|
|
101
|
+
resizeRef.current = {
|
|
102
|
+
startX: e.clientX,
|
|
103
|
+
startY: e.clientY,
|
|
104
|
+
startW: floatSize.w,
|
|
105
|
+
startH: floatSize.h,
|
|
106
|
+
};
|
|
107
|
+
const onMove = (ev: MouseEvent) => {
|
|
108
|
+
if (!resizeRef.current) return;
|
|
109
|
+
setFloatSize({
|
|
110
|
+
w: Math.max(360, resizeRef.current.startW + (ev.clientX - resizeRef.current.startX)),
|
|
111
|
+
h: Math.max(400, resizeRef.current.startH + (ev.clientY - resizeRef.current.startY)),
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
const onUp = () => {
|
|
115
|
+
resizeRef.current = null;
|
|
116
|
+
window.removeEventListener('mousemove', onMove);
|
|
117
|
+
window.removeEventListener('mouseup', onUp);
|
|
118
|
+
};
|
|
119
|
+
window.addEventListener('mousemove', onMove);
|
|
120
|
+
window.addEventListener('mouseup', onUp);
|
|
121
|
+
}, [floatSize]);
|
|
122
|
+
|
|
123
|
+
// --- Horizontal resize logic (docked mode — left edge) ---
|
|
124
|
+
const onDockResizeStart = useCallback((e: React.MouseEvent) => {
|
|
125
|
+
e.preventDefault();
|
|
126
|
+
e.stopPropagation();
|
|
127
|
+
dockResizeRef.current = { startX: e.clientX, startW: dockedWidth };
|
|
128
|
+
const onMove = (ev: MouseEvent) => {
|
|
129
|
+
if (!dockResizeRef.current) return;
|
|
130
|
+
// Dragging left should increase width (since resize handle is on the left edge)
|
|
131
|
+
const delta = dockResizeRef.current.startX - ev.clientX;
|
|
132
|
+
setDockedWidth(Math.max(320, Math.min(800, dockResizeRef.current.startW + delta)));
|
|
133
|
+
};
|
|
134
|
+
const onUp = () => {
|
|
135
|
+
dockResizeRef.current = null;
|
|
136
|
+
window.removeEventListener('mousemove', onMove);
|
|
137
|
+
window.removeEventListener('mouseup', onUp);
|
|
138
|
+
};
|
|
139
|
+
window.addEventListener('mousemove', onMove);
|
|
140
|
+
window.addEventListener('mouseup', onUp);
|
|
141
|
+
}, [dockedWidth]);
|
|
142
|
+
|
|
143
|
+
if (!isOpen) return null;
|
|
144
|
+
|
|
145
|
+
const isDocked = mode === 'docked';
|
|
146
|
+
|
|
147
|
+
const panel = (
|
|
148
|
+
<div
|
|
149
|
+
className={cn(
|
|
150
|
+
'flex flex-col bg-white overflow-hidden relative',
|
|
151
|
+
isDocked
|
|
152
|
+
? 'h-full border-l border-gray-200 shrink-0'
|
|
153
|
+
: 'rounded-2xl shadow-2xl border border-gray-200',
|
|
154
|
+
)}
|
|
155
|
+
style={
|
|
156
|
+
isDocked
|
|
157
|
+
? { width: dockedWidth }
|
|
158
|
+
: {
|
|
159
|
+
position: 'fixed',
|
|
160
|
+
left: floatPos.x,
|
|
161
|
+
top: floatPos.y,
|
|
162
|
+
width: floatSize.w,
|
|
163
|
+
height: floatSize.h,
|
|
164
|
+
zIndex: 9999,
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
>
|
|
168
|
+
{/* Docked mode: left-edge resize handle */}
|
|
169
|
+
{isDocked && (
|
|
170
|
+
<div
|
|
171
|
+
className="absolute top-0 left-0 w-1.5 h-full cursor-ew-resize z-20 hover:bg-primary/10 transition-colors"
|
|
172
|
+
onMouseDown={onDockResizeStart}
|
|
173
|
+
/>
|
|
174
|
+
)}
|
|
175
|
+
|
|
176
|
+
{/* Title Bar */}
|
|
177
|
+
<div
|
|
178
|
+
className={cn(
|
|
179
|
+
'flex items-center justify-between px-4 py-2.5 bg-gray-50 border-b border-gray-200 shrink-0 select-none',
|
|
180
|
+
!isDocked && 'cursor-grab active:cursor-grabbing',
|
|
181
|
+
)}
|
|
182
|
+
onMouseDown={!isDocked ? onDragStart : undefined}
|
|
183
|
+
>
|
|
184
|
+
<div className="flex items-center gap-2.5">
|
|
185
|
+
<BookOpen className="w-4 h-4 text-primary" />
|
|
186
|
+
<span className="text-sm font-semibold text-gray-900">{t('docBrowserTitle')}</span>
|
|
187
|
+
</div>
|
|
188
|
+
<div className="flex items-center gap-1">
|
|
189
|
+
<button
|
|
190
|
+
onClick={toggleMode}
|
|
191
|
+
className="hover:bg-gray-200 rounded-md p-1.5 text-gray-500 hover:text-gray-700 transition-colors"
|
|
192
|
+
title={isDocked ? t('docBrowserFloatMode') : t('docBrowserDockMode')}
|
|
193
|
+
>
|
|
194
|
+
{isDocked ? <Maximize2 className="w-3.5 h-3.5" /> : <PanelRightOpen className="w-3.5 h-3.5" />}
|
|
195
|
+
</button>
|
|
196
|
+
<button
|
|
197
|
+
onClick={close}
|
|
198
|
+
className="hover:bg-gray-200 rounded-md p-1.5 text-gray-500 hover:text-gray-700 transition-colors"
|
|
199
|
+
title={t('docBrowserClose')}
|
|
200
|
+
>
|
|
201
|
+
<X className="w-3.5 h-3.5" />
|
|
202
|
+
</button>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
{/* Navigation Bar */}
|
|
207
|
+
<div className="flex items-center gap-2 px-3.5 py-2 bg-white border-b border-gray-100 shrink-0">
|
|
208
|
+
<button
|
|
209
|
+
onClick={goBack}
|
|
210
|
+
disabled={!canGoBack}
|
|
211
|
+
className="p-1.5 rounded-md hover:bg-gray-100 disabled:opacity-30 disabled:cursor-not-allowed text-gray-600 transition-colors"
|
|
212
|
+
>
|
|
213
|
+
<ArrowLeft className="w-4 h-4" />
|
|
214
|
+
</button>
|
|
215
|
+
<button
|
|
216
|
+
onClick={goForward}
|
|
217
|
+
disabled={!canGoForward}
|
|
218
|
+
className="p-1.5 rounded-md hover:bg-gray-100 disabled:opacity-30 disabled:cursor-not-allowed text-gray-600 transition-colors"
|
|
219
|
+
>
|
|
220
|
+
<ArrowRight className="w-4 h-4" />
|
|
221
|
+
</button>
|
|
222
|
+
|
|
223
|
+
<form onSubmit={handleUrlSubmit} className="flex-1 relative">
|
|
224
|
+
<Search className="w-3.5 h-3.5 absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
|
225
|
+
<input
|
|
226
|
+
type="text"
|
|
227
|
+
value={urlInput}
|
|
228
|
+
onChange={(e) => setUrlInput(e.target.value)}
|
|
229
|
+
placeholder={t('docBrowserSearchPlaceholder')}
|
|
230
|
+
className="w-full h-8 pl-8 pr-3 rounded-lg bg-gray-50 border border-gray-200 text-xs text-gray-700 focus:outline-none focus:ring-1 focus:ring-primary/30 focus:border-primary/40 transition-colors placeholder:text-gray-400"
|
|
231
|
+
/>
|
|
232
|
+
</form>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
{/* Iframe Content */}
|
|
236
|
+
<div className="flex-1 relative overflow-hidden">
|
|
237
|
+
<iframe
|
|
238
|
+
src={currentUrl}
|
|
239
|
+
className="absolute inset-0 w-full h-full border-0"
|
|
240
|
+
title="NextClaw Documentation"
|
|
241
|
+
sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
|
|
242
|
+
allow="clipboard-read; clipboard-write"
|
|
243
|
+
/>
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
{/* Footer */}
|
|
247
|
+
<div className="flex items-center justify-between px-4 py-2 bg-gray-50 border-t border-gray-200 shrink-0">
|
|
248
|
+
<a
|
|
249
|
+
href={currentUrl}
|
|
250
|
+
target="_blank"
|
|
251
|
+
rel="noopener noreferrer"
|
|
252
|
+
className="flex items-center gap-1.5 text-xs text-primary hover:text-primary-hover font-medium transition-colors"
|
|
253
|
+
>
|
|
254
|
+
{t('docBrowserOpenExternal')}
|
|
255
|
+
<ExternalLink className="w-3 h-3" />
|
|
256
|
+
</a>
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
{/* Resize Handle (floating only — bottom-right corner) */}
|
|
260
|
+
{!isDocked && (
|
|
261
|
+
<div
|
|
262
|
+
className="absolute bottom-0 right-0 w-5 h-5 cursor-se-resize flex items-center justify-center text-gray-300 hover:text-gray-500 transition-colors"
|
|
263
|
+
onMouseDown={onResizeStart}
|
|
264
|
+
>
|
|
265
|
+
<GripVertical className="w-3 h-3 rotate-[-45deg]" />
|
|
266
|
+
</div>
|
|
267
|
+
)}
|
|
268
|
+
</div>
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
return panel;
|
|
272
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
const DOCS_PRIMARY_DOMAIN = 'docs.nextclaw.io';
|
|
4
|
+
const DOCS_FALLBACK_DOMAIN = 'nextclaw-docs.pages.dev';
|
|
5
|
+
const DOCS_HOSTS = new Set([
|
|
6
|
+
DOCS_PRIMARY_DOMAIN,
|
|
7
|
+
`www.${DOCS_PRIMARY_DOMAIN}`,
|
|
8
|
+
DOCS_FALLBACK_DOMAIN,
|
|
9
|
+
`www.${DOCS_FALLBACK_DOMAIN}`,
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
export const DOCS_DEFAULT_BASE_URL = `https://${DOCS_FALLBACK_DOMAIN}`;
|
|
13
|
+
|
|
14
|
+
export type DocBrowserMode = 'floating' | 'docked';
|
|
15
|
+
|
|
16
|
+
interface DocBrowserState {
|
|
17
|
+
isOpen: boolean;
|
|
18
|
+
mode: DocBrowserMode;
|
|
19
|
+
currentUrl: string;
|
|
20
|
+
history: string[];
|
|
21
|
+
historyIndex: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface DocBrowserActions {
|
|
25
|
+
open: (url?: string) => void;
|
|
26
|
+
close: () => void;
|
|
27
|
+
toggleMode: () => void;
|
|
28
|
+
setMode: (mode: DocBrowserMode) => void;
|
|
29
|
+
navigate: (url: string) => void;
|
|
30
|
+
goBack: () => void;
|
|
31
|
+
goForward: () => void;
|
|
32
|
+
canGoBack: boolean;
|
|
33
|
+
canGoForward: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type DocBrowserContextValue = DocBrowserState & DocBrowserActions;
|
|
37
|
+
|
|
38
|
+
const DocBrowserContext = createContext<DocBrowserContextValue | null>(null);
|
|
39
|
+
|
|
40
|
+
export function useDocBrowser(): DocBrowserContextValue {
|
|
41
|
+
const ctx = useContext(DocBrowserContext);
|
|
42
|
+
if (!ctx) throw new Error('useDocBrowser must be used within DocBrowserProvider');
|
|
43
|
+
return ctx;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Check if a URL belongs to the docs domain */
|
|
47
|
+
export function isDocsUrl(url: string): boolean {
|
|
48
|
+
try {
|
|
49
|
+
const parsed = new URL(url, window.location.origin);
|
|
50
|
+
return DOCS_HOSTS.has(parsed.hostname);
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function DocBrowserProvider({ children }: { children: ReactNode }) {
|
|
57
|
+
const [state, setState] = useState<DocBrowserState>({
|
|
58
|
+
isOpen: false,
|
|
59
|
+
mode: 'docked',
|
|
60
|
+
currentUrl: `${DOCS_DEFAULT_BASE_URL}/guide/getting-started`,
|
|
61
|
+
history: [`${DOCS_DEFAULT_BASE_URL}/guide/getting-started`],
|
|
62
|
+
historyIndex: 0,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const open = useCallback((url?: string) => {
|
|
66
|
+
const targetUrl = url || state.currentUrl || `${DOCS_DEFAULT_BASE_URL}/guide/getting-started`;
|
|
67
|
+
setState(prev => ({
|
|
68
|
+
...prev,
|
|
69
|
+
isOpen: true,
|
|
70
|
+
currentUrl: targetUrl,
|
|
71
|
+
history: [...prev.history.slice(0, prev.historyIndex + 1), targetUrl],
|
|
72
|
+
historyIndex: prev.historyIndex + 1,
|
|
73
|
+
}));
|
|
74
|
+
}, [state.currentUrl]);
|
|
75
|
+
|
|
76
|
+
const close = useCallback(() => {
|
|
77
|
+
setState(prev => ({ ...prev, isOpen: false }));
|
|
78
|
+
}, []);
|
|
79
|
+
|
|
80
|
+
const toggleMode = useCallback(() => {
|
|
81
|
+
setState(prev => ({ ...prev, mode: prev.mode === 'floating' ? 'docked' : 'floating' }));
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
const setMode = useCallback((mode: DocBrowserMode) => {
|
|
85
|
+
setState(prev => ({ ...prev, mode }));
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
const navigate = useCallback((url: string) => {
|
|
89
|
+
setState(prev => ({
|
|
90
|
+
...prev,
|
|
91
|
+
currentUrl: url,
|
|
92
|
+
history: [...prev.history.slice(0, prev.historyIndex + 1), url],
|
|
93
|
+
historyIndex: prev.historyIndex + 1,
|
|
94
|
+
}));
|
|
95
|
+
}, []);
|
|
96
|
+
|
|
97
|
+
const goBack = useCallback(() => {
|
|
98
|
+
setState(prev => {
|
|
99
|
+
if (prev.historyIndex <= 0) return prev;
|
|
100
|
+
const newIndex = prev.historyIndex - 1;
|
|
101
|
+
return { ...prev, historyIndex: newIndex, currentUrl: prev.history[newIndex] };
|
|
102
|
+
});
|
|
103
|
+
}, []);
|
|
104
|
+
|
|
105
|
+
const goForward = useCallback(() => {
|
|
106
|
+
setState(prev => {
|
|
107
|
+
if (prev.historyIndex >= prev.history.length - 1) return prev;
|
|
108
|
+
const newIndex = prev.historyIndex + 1;
|
|
109
|
+
return { ...prev, historyIndex: newIndex, currentUrl: prev.history[newIndex] };
|
|
110
|
+
});
|
|
111
|
+
}, []);
|
|
112
|
+
|
|
113
|
+
const canGoBack = state.historyIndex > 0;
|
|
114
|
+
const canGoForward = state.historyIndex < state.history.length - 1;
|
|
115
|
+
|
|
116
|
+
const value = useMemo<DocBrowserContextValue>(() => ({
|
|
117
|
+
...state,
|
|
118
|
+
open,
|
|
119
|
+
close,
|
|
120
|
+
toggleMode,
|
|
121
|
+
setMode,
|
|
122
|
+
navigate,
|
|
123
|
+
goBack,
|
|
124
|
+
goForward,
|
|
125
|
+
canGoBack,
|
|
126
|
+
canGoForward,
|
|
127
|
+
}), [state, open, close, toggleMode, setMode, navigate, goBack, goForward, canGoBack, canGoForward]);
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<DocBrowserContext.Provider value={value}>
|
|
131
|
+
{children}
|
|
132
|
+
</DocBrowserContext.Provider>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { isDocsUrl, useDocBrowser } from './DocBrowserContext';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Global click interceptor for docs links.
|
|
6
|
+
* Captures clicks on <a> tags pointing to the docs domain
|
|
7
|
+
* and opens them in the in-app micro-browser instead.
|
|
8
|
+
*/
|
|
9
|
+
export function useDocLinkInterceptor() {
|
|
10
|
+
const docBrowser = useDocBrowser();
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const handler = (e: MouseEvent) => {
|
|
14
|
+
// Walk up from the click target to find an anchor
|
|
15
|
+
const anchor = (e.target as HTMLElement).closest<HTMLAnchorElement>('a[href]');
|
|
16
|
+
if (!anchor) return;
|
|
17
|
+
|
|
18
|
+
const href = anchor.getAttribute('href') || '';
|
|
19
|
+
if (!isDocsUrl(href)) return;
|
|
20
|
+
|
|
21
|
+
// Don't intercept if modifier keys are held (user wants new tab behavior)
|
|
22
|
+
if (e.ctrlKey || e.metaKey || e.shiftKey) return;
|
|
23
|
+
|
|
24
|
+
e.preventDefault();
|
|
25
|
+
e.stopPropagation();
|
|
26
|
+
docBrowser.open(href);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Use capture phase to intercept before React's synthetic events
|
|
30
|
+
document.addEventListener('click', handler, true);
|
|
31
|
+
return () => document.removeEventListener('click', handler, true);
|
|
32
|
+
}, [docBrowser]);
|
|
33
|
+
}
|
|
@@ -1,20 +1,37 @@
|
|
|
1
1
|
import { Sidebar } from './Sidebar';
|
|
2
|
+
import { DocBrowserProvider, DocBrowser, useDocBrowser, useDocLinkInterceptor } from '@/components/doc-browser';
|
|
2
3
|
|
|
3
4
|
interface AppLayoutProps {
|
|
4
5
|
children: React.ReactNode;
|
|
5
6
|
}
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
function AppLayoutInner({ children }: AppLayoutProps) {
|
|
9
|
+
const { isOpen, mode } = useDocBrowser();
|
|
10
|
+
useDocLinkInterceptor();
|
|
11
|
+
|
|
8
12
|
return (
|
|
9
|
-
<div className="h-screen flex bg-
|
|
13
|
+
<div className="h-screen flex bg-white font-sans text-foreground">
|
|
10
14
|
<Sidebar />
|
|
11
|
-
<div className="flex-1 flex
|
|
12
|
-
<
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
<div className="flex-1 flex min-w-0 overflow-hidden relative">
|
|
16
|
+
<div className="flex-1 flex flex-col min-w-0 bg-gray-50 overflow-hidden">
|
|
17
|
+
<main className="flex-1 overflow-auto custom-scrollbar p-10">
|
|
18
|
+
<div className="max-w-6xl mx-auto animate-fade-in h-full">
|
|
19
|
+
{children}
|
|
20
|
+
</div>
|
|
21
|
+
</main>
|
|
22
|
+
</div>
|
|
23
|
+
{/* Doc Browser: docked mode renders inline, floating mode renders as overlay */}
|
|
24
|
+
{isOpen && mode === 'docked' && <DocBrowser />}
|
|
17
25
|
</div>
|
|
26
|
+
{isOpen && mode === 'floating' && <DocBrowser />}
|
|
18
27
|
</div>
|
|
19
28
|
);
|
|
20
29
|
}
|
|
30
|
+
|
|
31
|
+
export function AppLayout({ children }: AppLayoutProps) {
|
|
32
|
+
return (
|
|
33
|
+
<DocBrowserProvider>
|
|
34
|
+
<AppLayoutInner>{children}</AppLayoutInner>
|
|
35
|
+
</DocBrowserProvider>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { cn } from '@/lib/utils';
|
|
2
2
|
import { t } from '@/lib/i18n';
|
|
3
|
-
import { Cpu, GitBranch, History, MessageSquare, Sparkles } from 'lucide-react';
|
|
3
|
+
import { Cpu, GitBranch, History, MessageSquare, Sparkles, BookOpen, Store } from 'lucide-react';
|
|
4
4
|
import { NavLink } from 'react-router-dom';
|
|
5
|
+
import { useDocBrowser } from '@/components/doc-browser';
|
|
5
6
|
|
|
6
7
|
const navItems = [
|
|
7
8
|
{
|
|
@@ -28,12 +29,19 @@ const navItems = [
|
|
|
28
29
|
target: '/sessions',
|
|
29
30
|
label: t('sessions'),
|
|
30
31
|
icon: History,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
target: '/marketplace',
|
|
35
|
+
label: 'Marketplace',
|
|
36
|
+
icon: Store,
|
|
31
37
|
}
|
|
32
38
|
];
|
|
33
39
|
|
|
34
40
|
export function Sidebar() {
|
|
41
|
+
const docBrowser = useDocBrowser();
|
|
42
|
+
|
|
35
43
|
return (
|
|
36
|
-
<aside className="w-[240px] bg-
|
|
44
|
+
<aside className="w-[240px] bg-white border-r border-gray-200 flex flex-col h-full py-6 px-4">
|
|
37
45
|
{/* Logo Area */}
|
|
38
46
|
<div className="px-3 mb-8">
|
|
39
47
|
<div className="flex items-center gap-2.5 group cursor-pointer">
|
|
@@ -55,10 +63,10 @@ export function Sidebar() {
|
|
|
55
63
|
<NavLink
|
|
56
64
|
to={item.target}
|
|
57
65
|
className={({ isActive }) => cn(
|
|
58
|
-
'group w-full flex items-center gap-3 px-
|
|
66
|
+
'group w-full flex items-center gap-3 px-4 py-2.5 rounded-lg text-sm font-medium transition-all duration-base',
|
|
59
67
|
isActive
|
|
60
|
-
? 'bg-
|
|
61
|
-
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
|
|
68
|
+
? 'bg-brand-50 text-brand-700'
|
|
69
|
+
: 'text-gray-600 hover:bg-gray-100/80 hover:text-gray-900'
|
|
62
70
|
)}
|
|
63
71
|
>
|
|
64
72
|
{({ isActive }) => (
|
|
@@ -76,6 +84,25 @@ export function Sidebar() {
|
|
|
76
84
|
})}
|
|
77
85
|
</ul>
|
|
78
86
|
</nav>
|
|
87
|
+
|
|
88
|
+
{/* Help Button */}
|
|
89
|
+
<div className="pt-2 border-t border-gray-100 mt-2">
|
|
90
|
+
<button
|
|
91
|
+
onClick={() => docBrowser.open()}
|
|
92
|
+
className={cn(
|
|
93
|
+
'w-full flex items-center gap-3 px-4 py-2.5 rounded-lg text-sm font-medium transition-all duration-base',
|
|
94
|
+
docBrowser.isOpen
|
|
95
|
+
? 'bg-brand-50 text-brand-700'
|
|
96
|
+
: 'text-gray-600 hover:bg-gray-100/80 hover:text-gray-900'
|
|
97
|
+
)}
|
|
98
|
+
>
|
|
99
|
+
<BookOpen className={cn(
|
|
100
|
+
'h-4 w-4',
|
|
101
|
+
docBrowser.isOpen ? 'text-primary' : 'text-gray-500'
|
|
102
|
+
)} />
|
|
103
|
+
<span className="flex-1 text-left">{t('docBrowserHelp')}</span>
|
|
104
|
+
</button>
|
|
105
|
+
</div>
|
|
79
106
|
</aside>
|
|
80
107
|
);
|
|
81
108
|
}
|