@nextclaw/ui 0.5.37 → 0.5.39
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 +12 -0
- package/dist/assets/{ChannelsList-DtvhbEV9.js → ChannelsList-DI_LXa6G.js} +1 -1
- package/dist/assets/{ChatPage-Bw_aXB4R.js → ChatPage-CHiqC2bZ.js} +1 -1
- package/dist/assets/{CronConfig-BZLXcDbm.js → CronConfig-BnGmNC74.js} +1 -1
- package/dist/assets/DocBrowser-B9xGvXuH.js +1 -0
- package/dist/assets/MarketplacePage-CTiHuMSY.js +49 -0
- package/dist/assets/{ModelConfig-Bi8Q4_NG.js → ModelConfig-Dz8pUTqH.js} +1 -1
- package/dist/assets/ProvidersList-Du9TbDku.js +1 -0
- package/dist/assets/{RuntimeConfig-Bz9aUkwu.js → RuntimeConfig-a7k7OTvs.js} +1 -1
- package/dist/assets/{SecretsConfig-Bqi-biOL.js → SecretsConfig-Dv3N-3XD.js} +1 -1
- package/dist/assets/{SessionsConfig-DcWT2QvI.js → SessionsConfig-DTZsKDvH.js} +1 -1
- package/dist/assets/{card-DwZkVl7S.js → card-BrerW-Nv.js} +1 -1
- package/dist/assets/index-BeI_Pucj.js +2 -0
- package/dist/assets/index-DMEuanmd.css +1 -0
- package/dist/assets/{label-BBDuC6Nm.js → label-BDeY_kN8.js} +1 -1
- package/dist/assets/{logos-DMFt4YDI.js → logos-Qxa07LFc.js} +1 -1
- package/dist/assets/{page-layout-hPFzCUTQ.js → page-layout-BRgOXrOh.js} +1 -1
- package/dist/assets/{switch-CwkcbkEs.js → switch-CblqC0lN.js} +1 -1
- package/dist/assets/{tabs-custom-TUrWRyYy.js → tabs-custom-B8x9xWoI.js} +1 -1
- package/dist/assets/{useConfig-DZVUrqQz.js → useConfig-VP_0ZVm1.js} +1 -1
- package/dist/assets/{useConfirmDialog-D5X0Iqid.js → useConfirmDialog-BxqJXdQR.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/api/marketplace.ts +24 -0
- package/src/api/types.ts +28 -0
- package/src/components/config/ModelConfig.tsx +1 -0
- package/src/components/config/ProviderForm.tsx +5 -1
- package/src/components/doc-browser/DocBrowser.tsx +382 -323
- package/src/components/doc-browser/DocBrowserContext.tsx +389 -157
- package/src/components/layout/Sidebar.tsx +1 -1
- package/src/components/marketplace/MarketplacePage.tsx +252 -12
- package/src/lib/i18n.ts +25 -2
- package/dist/assets/DocBrowser-BY0TiFOc.js +0 -1
- package/dist/assets/MarketplacePage-BDlAw7fO.js +0 -1
- package/dist/assets/ProvidersList-D2OB0siE.js +0 -1
- package/dist/assets/index-C1NAfZSm.js +0 -2
- package/dist/assets/index-DWgSvrx4.css +0 -1
|
@@ -1,359 +1,418 @@
|
|
|
1
1
|
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
2
|
-
import { DOCS_DEFAULT_BASE_URL, useDocBrowser } from './DocBrowserContext';
|
|
2
|
+
import { DOCS_DEFAULT_BASE_URL, isDocsUrl, useDocBrowser } from './DocBrowserContext';
|
|
3
3
|
import { cn } from '@/lib/utils';
|
|
4
4
|
import { t } from '@/lib/i18n';
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
6
|
+
ArrowLeft,
|
|
7
|
+
ArrowRight,
|
|
8
|
+
X,
|
|
9
|
+
ExternalLink,
|
|
10
|
+
PanelRightOpen,
|
|
11
|
+
Maximize2,
|
|
12
|
+
GripVertical,
|
|
13
|
+
Search,
|
|
14
|
+
BookOpen,
|
|
15
|
+
Plus,
|
|
15
16
|
} from 'lucide-react';
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* DocBrowser — An in-app micro-browser for documentation.
|
|
19
|
-
*
|
|
20
|
-
* Supports
|
|
21
|
-
* -
|
|
22
|
-
* - `
|
|
20
|
+
*
|
|
21
|
+
* Supports:
|
|
22
|
+
* - multi-tab browsing
|
|
23
|
+
* - `docked`: right sidebar panel (horizontally resizable)
|
|
24
|
+
* - `floating`: draggable, resizable overlay
|
|
23
25
|
*/
|
|
24
26
|
export function DocBrowser() {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
const {
|
|
28
|
+
isOpen,
|
|
29
|
+
mode,
|
|
30
|
+
tabs,
|
|
31
|
+
activeTabId,
|
|
32
|
+
currentTab,
|
|
33
|
+
currentUrl,
|
|
34
|
+
navVersion,
|
|
35
|
+
close,
|
|
36
|
+
toggleMode,
|
|
37
|
+
goBack,
|
|
38
|
+
goForward,
|
|
39
|
+
canGoBack,
|
|
40
|
+
canGoForward,
|
|
41
|
+
navigate,
|
|
42
|
+
syncUrl,
|
|
43
|
+
setActiveTab,
|
|
44
|
+
closeTab,
|
|
45
|
+
openNewTab,
|
|
46
|
+
} = useDocBrowser();
|
|
31
47
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
const [urlInput, setUrlInput] = useState('');
|
|
49
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
50
|
+
const [isResizing, setIsResizing] = useState(false);
|
|
51
|
+
const [floatPos, setFloatPos] = useState(() => ({
|
|
52
|
+
x: Math.max(40, window.innerWidth - 520),
|
|
53
|
+
y: 80,
|
|
54
|
+
}));
|
|
55
|
+
const [floatSize, setFloatSize] = useState({ w: 480, h: 600 });
|
|
56
|
+
const [dockedWidth, setDockedWidth] = useState(420);
|
|
57
|
+
const dragRef = useRef<{ startX: number; startY: number; startPosX: number; startPosY: number } | null>(null);
|
|
58
|
+
const resizeRef = useRef<{ startX: number; startY: number; startW: number; startH: number } | null>(null);
|
|
59
|
+
const dockResizeRef = useRef<{ startX: number; startW: number } | null>(null);
|
|
60
|
+
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
61
|
+
const prevNavVersionRef = useRef(navVersion);
|
|
62
|
+
const isDocsTab = currentTab?.kind === 'docs';
|
|
46
63
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (!isDocsTab) {
|
|
66
|
+
setUrlInput('');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const parsed = new URL(currentUrl);
|
|
71
|
+
setUrlInput(parsed.pathname);
|
|
72
|
+
} catch {
|
|
73
|
+
setUrlInput(currentUrl);
|
|
74
|
+
}
|
|
75
|
+
}, [currentUrl, activeTabId, isDocsTab]);
|
|
56
76
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
const path = new URL(currentUrl).pathname;
|
|
69
|
-
iframeRef.current.contentWindow.postMessage({ type: 'docs-navigate', path }, '*');
|
|
70
|
-
} catch { /* ignore */ }
|
|
71
|
-
}
|
|
72
|
-
}, [currentUrl, navVersion]);
|
|
77
|
+
// When currentUrl changes without navVersion bump (goBack/goForward),
|
|
78
|
+
// use postMessage to SPA-navigate inside the iframe instead of remounting.
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (!isDocsTab) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (navVersion !== prevNavVersionRef.current) {
|
|
84
|
+
prevNavVersionRef.current = navVersion;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
73
87
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
if (iframeRef.current?.contentWindow) {
|
|
89
|
+
try {
|
|
90
|
+
const path = new URL(currentUrl).pathname;
|
|
91
|
+
iframeRef.current.contentWindow.postMessage({ type: 'docs-navigate', path }, '*');
|
|
92
|
+
} catch {
|
|
93
|
+
// ignore postMessage errors
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}, [currentUrl, navVersion, isDocsTab]);
|
|
83
97
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return () => window.removeEventListener('message', handler);
|
|
93
|
-
}, [syncUrl]);
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (mode === 'floating') {
|
|
100
|
+
setFloatPos((prev) => ({
|
|
101
|
+
x: Math.max(40, window.innerWidth - floatSize.w - 40),
|
|
102
|
+
y: prev.y,
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
}, [mode, floatSize.w]);
|
|
94
106
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
const handler = (e: MessageEvent) => {
|
|
109
|
+
if (!isDocsTab) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (e.data?.type === 'docs-route-change' && typeof e.data.url === 'string') {
|
|
113
|
+
syncUrl(e.data.url);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
window.addEventListener('message', handler);
|
|
117
|
+
return () => window.removeEventListener('message', handler);
|
|
118
|
+
}, [syncUrl, isDocsTab]);
|
|
107
119
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
120
|
+
const handleUrlSubmit = useCallback((e: React.FormEvent) => {
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
if (!isDocsTab) return;
|
|
123
|
+
const input = urlInput.trim();
|
|
124
|
+
if (!input) return;
|
|
125
|
+
if (input.startsWith('/')) {
|
|
126
|
+
navigate(`${DOCS_DEFAULT_BASE_URL}${input}`);
|
|
127
|
+
} else if (input.startsWith('http')) {
|
|
128
|
+
navigate(input);
|
|
129
|
+
} else {
|
|
130
|
+
navigate(`${DOCS_DEFAULT_BASE_URL}/${input}`);
|
|
131
|
+
}
|
|
132
|
+
}, [urlInput, navigate, isDocsTab]);
|
|
119
133
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
setIsDragging(false);
|
|
131
|
-
dragRef.current = null;
|
|
132
|
-
};
|
|
133
|
-
window.addEventListener('mousemove', onMove);
|
|
134
|
-
window.addEventListener('mouseup', onUp);
|
|
135
|
-
return () => {
|
|
136
|
-
window.removeEventListener('mousemove', onMove);
|
|
137
|
-
window.removeEventListener('mouseup', onUp);
|
|
138
|
-
};
|
|
139
|
-
}, [isDragging]);
|
|
134
|
+
const onDragStart = useCallback((e: React.MouseEvent) => {
|
|
135
|
+
if (mode !== 'floating') return;
|
|
136
|
+
setIsDragging(true);
|
|
137
|
+
dragRef.current = {
|
|
138
|
+
startX: e.clientX,
|
|
139
|
+
startY: e.clientY,
|
|
140
|
+
startPosX: floatPos.x,
|
|
141
|
+
startPosY: floatPos.y,
|
|
142
|
+
};
|
|
143
|
+
}, [mode, floatPos]);
|
|
140
144
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
setIsResizing(false);
|
|
162
|
-
resizeRef.current = null;
|
|
163
|
-
window.removeEventListener('mousemove', onMove);
|
|
164
|
-
window.removeEventListener('mouseup', onUp);
|
|
165
|
-
};
|
|
166
|
-
window.addEventListener('mousemove', onMove);
|
|
167
|
-
window.addEventListener('mouseup', onUp);
|
|
168
|
-
}, [floatSize]);
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
if (!isDragging) return;
|
|
147
|
+
const onMove = (e: MouseEvent) => {
|
|
148
|
+
if (!dragRef.current) return;
|
|
149
|
+
setFloatPos({
|
|
150
|
+
x: dragRef.current.startPosX + (e.clientX - dragRef.current.startX),
|
|
151
|
+
y: dragRef.current.startPosY + (e.clientY - dragRef.current.startY),
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
const onUp = () => {
|
|
155
|
+
setIsDragging(false);
|
|
156
|
+
dragRef.current = null;
|
|
157
|
+
};
|
|
158
|
+
window.addEventListener('mousemove', onMove);
|
|
159
|
+
window.addEventListener('mouseup', onUp);
|
|
160
|
+
return () => {
|
|
161
|
+
window.removeEventListener('mousemove', onMove);
|
|
162
|
+
window.removeEventListener('mouseup', onUp);
|
|
163
|
+
};
|
|
164
|
+
}, [isDragging]);
|
|
169
165
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
166
|
+
const onResizeStart = useCallback((e: React.MouseEvent) => {
|
|
167
|
+
e.preventDefault();
|
|
168
|
+
e.stopPropagation();
|
|
169
|
+
setIsResizing(true);
|
|
170
|
+
const axis = (e.currentTarget as HTMLElement).dataset.axis;
|
|
171
|
+
resizeRef.current = {
|
|
172
|
+
startX: e.clientX,
|
|
173
|
+
startY: e.clientY,
|
|
174
|
+
startW: floatSize.w,
|
|
175
|
+
startH: floatSize.h,
|
|
176
|
+
};
|
|
177
|
+
const onMove = (ev: MouseEvent) => {
|
|
178
|
+
if (!resizeRef.current) return;
|
|
179
|
+
setFloatSize((prev) => ({
|
|
180
|
+
w: axis === 'y' ? prev.w : Math.max(360, resizeRef.current!.startW + (ev.clientX - resizeRef.current!.startX)),
|
|
181
|
+
h: axis === 'x' ? prev.h : Math.max(400, resizeRef.current!.startH + (ev.clientY - resizeRef.current!.startY)),
|
|
182
|
+
}));
|
|
183
|
+
};
|
|
184
|
+
const onUp = () => {
|
|
185
|
+
setIsResizing(false);
|
|
186
|
+
resizeRef.current = null;
|
|
187
|
+
window.removeEventListener('mousemove', onMove);
|
|
188
|
+
window.removeEventListener('mouseup', onUp);
|
|
189
|
+
};
|
|
190
|
+
window.addEventListener('mousemove', onMove);
|
|
191
|
+
window.addEventListener('mouseup', onUp);
|
|
192
|
+
}, [floatSize]);
|
|
190
193
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
window.addEventListener('mousemove', onMove);
|
|
211
|
-
window.addEventListener('mouseup', onUp);
|
|
212
|
-
}, [floatSize.w, floatPos.x]);
|
|
194
|
+
const onDockResizeStart = useCallback((e: React.MouseEvent) => {
|
|
195
|
+
e.preventDefault();
|
|
196
|
+
e.stopPropagation();
|
|
197
|
+
setIsResizing(true);
|
|
198
|
+
dockResizeRef.current = { startX: e.clientX, startW: dockedWidth };
|
|
199
|
+
const onMove = (ev: MouseEvent) => {
|
|
200
|
+
if (!dockResizeRef.current) return;
|
|
201
|
+
const delta = dockResizeRef.current.startX - ev.clientX;
|
|
202
|
+
setDockedWidth(Math.max(320, Math.min(860, dockResizeRef.current.startW + delta)));
|
|
203
|
+
};
|
|
204
|
+
const onUp = () => {
|
|
205
|
+
setIsResizing(false);
|
|
206
|
+
dockResizeRef.current = null;
|
|
207
|
+
window.removeEventListener('mousemove', onMove);
|
|
208
|
+
window.removeEventListener('mouseup', onUp);
|
|
209
|
+
};
|
|
210
|
+
window.addEventListener('mousemove', onMove);
|
|
211
|
+
window.addEventListener('mouseup', onUp);
|
|
212
|
+
}, [dockedWidth]);
|
|
213
213
|
|
|
214
|
-
|
|
214
|
+
const onLeftResizeStart = useCallback((e: React.MouseEvent) => {
|
|
215
|
+
e.preventDefault();
|
|
216
|
+
e.stopPropagation();
|
|
217
|
+
setIsResizing(true);
|
|
218
|
+
const startX = e.clientX;
|
|
219
|
+
const startW = floatSize.w;
|
|
220
|
+
const startPosX = floatPos.x;
|
|
221
|
+
const onMove = (ev: MouseEvent) => {
|
|
222
|
+
const delta = startX - ev.clientX;
|
|
223
|
+
const newW = Math.max(360, startW + delta);
|
|
224
|
+
setFloatSize((prev) => ({ ...prev, w: newW }));
|
|
225
|
+
setFloatPos((prev) => ({ ...prev, x: startPosX - (newW - startW) }));
|
|
226
|
+
};
|
|
227
|
+
const onUp = () => {
|
|
228
|
+
setIsResizing(false);
|
|
229
|
+
window.removeEventListener('mousemove', onMove);
|
|
230
|
+
window.removeEventListener('mouseup', onUp);
|
|
231
|
+
};
|
|
232
|
+
window.addEventListener('mousemove', onMove);
|
|
233
|
+
window.addEventListener('mouseup', onUp);
|
|
234
|
+
}, [floatSize.w, floatPos.x]);
|
|
215
235
|
|
|
216
|
-
|
|
236
|
+
if (!isOpen) return null;
|
|
217
237
|
|
|
218
|
-
|
|
238
|
+
const isDocked = mode === 'docked';
|
|
239
|
+
|
|
240
|
+
const panel = (
|
|
241
|
+
<div
|
|
242
|
+
className={cn(
|
|
243
|
+
'flex flex-col bg-white overflow-hidden relative',
|
|
244
|
+
isDocked
|
|
245
|
+
? 'h-full border-l border-gray-200 shrink-0'
|
|
246
|
+
: 'rounded-2xl shadow-2xl border border-gray-200',
|
|
247
|
+
)}
|
|
248
|
+
style={
|
|
249
|
+
isDocked
|
|
250
|
+
? { width: dockedWidth }
|
|
251
|
+
: {
|
|
252
|
+
position: 'fixed',
|
|
253
|
+
left: floatPos.x,
|
|
254
|
+
top: floatPos.y,
|
|
255
|
+
width: floatSize.w,
|
|
256
|
+
height: floatSize.h,
|
|
257
|
+
zIndex: 9999,
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
>
|
|
261
|
+
{isDocked && (
|
|
219
262
|
<div
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
{isDocked
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
263
|
+
className="absolute top-0 left-0 w-1.5 h-full cursor-ew-resize z-20 hover:bg-primary/10 transition-colors"
|
|
264
|
+
onMouseDown={onDockResizeStart}
|
|
265
|
+
/>
|
|
266
|
+
)}
|
|
267
|
+
|
|
268
|
+
<div
|
|
269
|
+
className={cn(
|
|
270
|
+
'flex items-center justify-between px-4 py-2.5 bg-gray-50 border-b border-gray-200 shrink-0 select-none',
|
|
271
|
+
!isDocked && 'cursor-grab active:cursor-grabbing',
|
|
272
|
+
)}
|
|
273
|
+
onMouseDown={!isDocked ? onDragStart : undefined}
|
|
274
|
+
>
|
|
275
|
+
<div className="flex items-center gap-2.5 min-w-0">
|
|
276
|
+
<BookOpen className="w-4 h-4 text-primary shrink-0" />
|
|
277
|
+
<span className="text-sm font-semibold text-gray-900 truncate">{t('docBrowserTitle')}</span>
|
|
278
|
+
</div>
|
|
279
|
+
<div className="flex items-center gap-1">
|
|
280
|
+
<button
|
|
281
|
+
onClick={toggleMode}
|
|
282
|
+
className="hover:bg-gray-200 rounded-md p-1.5 text-gray-500 hover:text-gray-700 transition-colors"
|
|
283
|
+
title={isDocked ? t('docBrowserFloatMode') : t('docBrowserDockMode')}
|
|
284
|
+
>
|
|
285
|
+
{isDocked ? <Maximize2 className="w-3.5 h-3.5" /> : <PanelRightOpen className="w-3.5 h-3.5" />}
|
|
286
|
+
</button>
|
|
287
|
+
<button
|
|
288
|
+
onClick={close}
|
|
289
|
+
className="hover:bg-gray-200 rounded-md p-1.5 text-gray-500 hover:text-gray-700 transition-colors"
|
|
290
|
+
title={t('docBrowserClose')}
|
|
291
|
+
>
|
|
292
|
+
<X className="w-3.5 h-3.5" />
|
|
293
|
+
</button>
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
246
296
|
|
|
247
|
-
|
|
297
|
+
<div className="flex items-center gap-1.5 px-2.5 py-2 bg-white border-b border-gray-100 overflow-x-auto custom-scrollbar">
|
|
298
|
+
{tabs.map((tab) => {
|
|
299
|
+
const isActive = tab.id === activeTabId;
|
|
300
|
+
return (
|
|
248
301
|
<div
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
302
|
+
key={tab.id}
|
|
303
|
+
className={cn(
|
|
304
|
+
'inline-flex items-center gap-1 h-7 px-1.5 rounded-lg text-xs border max-w-[220px] shrink-0 transition-colors',
|
|
305
|
+
isActive
|
|
306
|
+
? 'bg-blue-50 border-blue-300 text-blue-700'
|
|
307
|
+
: 'bg-gray-50 border-gray-200 text-gray-600 hover:bg-gray-100'
|
|
308
|
+
)}
|
|
254
309
|
>
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
</div>
|
|
310
|
+
<button
|
|
311
|
+
type="button"
|
|
312
|
+
onClick={() => setActiveTab(tab.id)}
|
|
313
|
+
className="truncate text-left px-1"
|
|
314
|
+
title={tab.title}
|
|
315
|
+
>
|
|
316
|
+
{tab.title || t('docBrowserTabUntitled')}
|
|
317
|
+
</button>
|
|
318
|
+
<button
|
|
319
|
+
type="button"
|
|
320
|
+
onClick={(event) => {
|
|
321
|
+
event.stopPropagation();
|
|
322
|
+
closeTab(tab.id);
|
|
323
|
+
}}
|
|
324
|
+
className="rounded p-0.5 hover:bg-black/10"
|
|
325
|
+
aria-label={t('docBrowserCloseTab')}
|
|
326
|
+
>
|
|
327
|
+
<X className="w-3 h-3" />
|
|
328
|
+
</button>
|
|
275
329
|
</div>
|
|
330
|
+
);
|
|
331
|
+
})}
|
|
332
|
+
<button
|
|
333
|
+
onClick={() => openNewTab(undefined, { kind: 'docs', title: 'Docs' })}
|
|
334
|
+
className="inline-flex items-center justify-center w-7 h-7 rounded-lg border border-gray-200 bg-white text-gray-600 hover:bg-gray-100 shrink-0"
|
|
335
|
+
title={t('docBrowserNewTab')}
|
|
336
|
+
>
|
|
337
|
+
<Plus className="w-3.5 h-3.5" />
|
|
338
|
+
</button>
|
|
339
|
+
</div>
|
|
276
340
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
<form onSubmit={handleUrlSubmit} className="flex-1 relative">
|
|
295
|
-
<Search className="w-3.5 h-3.5 absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
|
296
|
-
<input
|
|
297
|
-
type="text"
|
|
298
|
-
value={urlInput}
|
|
299
|
-
onChange={(e) => setUrlInput(e.target.value)}
|
|
300
|
-
placeholder={t('docBrowserSearchPlaceholder')}
|
|
301
|
-
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"
|
|
302
|
-
/>
|
|
303
|
-
</form>
|
|
304
|
-
</div>
|
|
341
|
+
{isDocsTab && (
|
|
342
|
+
<div className="flex items-center gap-2 px-3.5 py-2 bg-white border-b border-gray-100 shrink-0">
|
|
343
|
+
<button
|
|
344
|
+
onClick={goBack}
|
|
345
|
+
disabled={!canGoBack}
|
|
346
|
+
className="p-1.5 rounded-md hover:bg-gray-100 disabled:opacity-30 disabled:cursor-not-allowed text-gray-600 transition-colors"
|
|
347
|
+
>
|
|
348
|
+
<ArrowLeft className="w-4 h-4" />
|
|
349
|
+
</button>
|
|
350
|
+
<button
|
|
351
|
+
onClick={goForward}
|
|
352
|
+
disabled={!canGoForward}
|
|
353
|
+
className="p-1.5 rounded-md hover:bg-gray-100 disabled:opacity-30 disabled:cursor-not-allowed text-gray-600 transition-colors"
|
|
354
|
+
>
|
|
355
|
+
<ArrowRight className="w-4 h-4" />
|
|
356
|
+
</button>
|
|
305
357
|
|
|
306
|
-
|
|
307
|
-
<
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
{(isResizing || isDragging) && (
|
|
319
|
-
<div className="absolute inset-0 z-10" />
|
|
320
|
-
)}
|
|
321
|
-
</div>
|
|
358
|
+
<form onSubmit={handleUrlSubmit} className="flex-1 relative">
|
|
359
|
+
<Search className="w-3.5 h-3.5 absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
|
360
|
+
<input
|
|
361
|
+
type="text"
|
|
362
|
+
value={urlInput}
|
|
363
|
+
onChange={(e) => setUrlInput(e.target.value)}
|
|
364
|
+
placeholder={t('docBrowserSearchPlaceholder')}
|
|
365
|
+
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"
|
|
366
|
+
/>
|
|
367
|
+
</form>
|
|
368
|
+
</div>
|
|
369
|
+
)}
|
|
322
370
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
371
|
+
<div className="flex-1 relative overflow-hidden">
|
|
372
|
+
<iframe
|
|
373
|
+
ref={iframeRef}
|
|
374
|
+
key={`${activeTabId}:${navVersion}`}
|
|
375
|
+
src={currentUrl}
|
|
376
|
+
className="absolute inset-0 w-full h-full border-0"
|
|
377
|
+
title={currentTab?.title || 'NextClaw Docs'}
|
|
378
|
+
sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
|
|
379
|
+
allow="clipboard-read; clipboard-write"
|
|
380
|
+
/>
|
|
381
|
+
{(isResizing || isDragging) && (
|
|
382
|
+
<div className="absolute inset-0 z-10" />
|
|
383
|
+
)}
|
|
384
|
+
</div>
|
|
336
385
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
onMouseDown={onResizeStart}
|
|
350
|
-
>
|
|
351
|
-
<GripVertical className="w-3 h-3 rotate-[-45deg]" />
|
|
352
|
-
</div>
|
|
353
|
-
</>
|
|
354
|
-
)}
|
|
386
|
+
{isDocsTab && isDocsUrl(currentUrl) && (
|
|
387
|
+
<div className="flex items-center justify-between px-4 py-2 bg-gray-50 border-t border-gray-200 shrink-0">
|
|
388
|
+
<a
|
|
389
|
+
href={currentUrl}
|
|
390
|
+
target="_blank"
|
|
391
|
+
rel="noopener noreferrer"
|
|
392
|
+
data-doc-external
|
|
393
|
+
className="flex items-center gap-1.5 text-xs text-primary hover:text-primary-hover font-medium transition-colors"
|
|
394
|
+
>
|
|
395
|
+
{t('docBrowserOpenExternal')}
|
|
396
|
+
<ExternalLink className="w-3 h-3" />
|
|
397
|
+
</a>
|
|
355
398
|
</div>
|
|
356
|
-
|
|
399
|
+
)}
|
|
400
|
+
|
|
401
|
+
{!isDocked && (
|
|
402
|
+
<>
|
|
403
|
+
<div className="absolute top-0 left-0 w-1.5 h-full cursor-ew-resize z-20 hover:bg-primary/10 transition-colors" onMouseDown={onLeftResizeStart} />
|
|
404
|
+
<div className="absolute top-0 right-0 w-1.5 h-full cursor-ew-resize z-20 hover:bg-primary/10 transition-colors" onMouseDown={onResizeStart} data-axis="x" />
|
|
405
|
+
<div className="absolute bottom-0 left-0 h-1.5 w-full cursor-ns-resize z-20 hover:bg-primary/10 transition-colors" onMouseDown={onResizeStart} data-axis="y" />
|
|
406
|
+
<div
|
|
407
|
+
className="absolute bottom-0 right-0 w-4 h-4 cursor-se-resize z-30 flex items-center justify-center text-gray-300 hover:text-gray-500 transition-colors"
|
|
408
|
+
onMouseDown={onResizeStart}
|
|
409
|
+
>
|
|
410
|
+
<GripVertical className="w-3 h-3 rotate-[-45deg]" />
|
|
411
|
+
</div>
|
|
412
|
+
</>
|
|
413
|
+
)}
|
|
414
|
+
</div>
|
|
415
|
+
);
|
|
357
416
|
|
|
358
|
-
|
|
417
|
+
return panel;
|
|
359
418
|
}
|