@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.
Files changed (37) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/assets/{ChannelsList-DtvhbEV9.js → ChannelsList-DI_LXa6G.js} +1 -1
  3. package/dist/assets/{ChatPage-Bw_aXB4R.js → ChatPage-CHiqC2bZ.js} +1 -1
  4. package/dist/assets/{CronConfig-BZLXcDbm.js → CronConfig-BnGmNC74.js} +1 -1
  5. package/dist/assets/DocBrowser-B9xGvXuH.js +1 -0
  6. package/dist/assets/MarketplacePage-CTiHuMSY.js +49 -0
  7. package/dist/assets/{ModelConfig-Bi8Q4_NG.js → ModelConfig-Dz8pUTqH.js} +1 -1
  8. package/dist/assets/ProvidersList-Du9TbDku.js +1 -0
  9. package/dist/assets/{RuntimeConfig-Bz9aUkwu.js → RuntimeConfig-a7k7OTvs.js} +1 -1
  10. package/dist/assets/{SecretsConfig-Bqi-biOL.js → SecretsConfig-Dv3N-3XD.js} +1 -1
  11. package/dist/assets/{SessionsConfig-DcWT2QvI.js → SessionsConfig-DTZsKDvH.js} +1 -1
  12. package/dist/assets/{card-DwZkVl7S.js → card-BrerW-Nv.js} +1 -1
  13. package/dist/assets/index-BeI_Pucj.js +2 -0
  14. package/dist/assets/index-DMEuanmd.css +1 -0
  15. package/dist/assets/{label-BBDuC6Nm.js → label-BDeY_kN8.js} +1 -1
  16. package/dist/assets/{logos-DMFt4YDI.js → logos-Qxa07LFc.js} +1 -1
  17. package/dist/assets/{page-layout-hPFzCUTQ.js → page-layout-BRgOXrOh.js} +1 -1
  18. package/dist/assets/{switch-CwkcbkEs.js → switch-CblqC0lN.js} +1 -1
  19. package/dist/assets/{tabs-custom-TUrWRyYy.js → tabs-custom-B8x9xWoI.js} +1 -1
  20. package/dist/assets/{useConfig-DZVUrqQz.js → useConfig-VP_0ZVm1.js} +1 -1
  21. package/dist/assets/{useConfirmDialog-D5X0Iqid.js → useConfirmDialog-BxqJXdQR.js} +1 -1
  22. package/dist/index.html +2 -2
  23. package/package.json +1 -1
  24. package/src/api/marketplace.ts +24 -0
  25. package/src/api/types.ts +28 -0
  26. package/src/components/config/ModelConfig.tsx +1 -0
  27. package/src/components/config/ProviderForm.tsx +5 -1
  28. package/src/components/doc-browser/DocBrowser.tsx +382 -323
  29. package/src/components/doc-browser/DocBrowserContext.tsx +389 -157
  30. package/src/components/layout/Sidebar.tsx +1 -1
  31. package/src/components/marketplace/MarketplacePage.tsx +252 -12
  32. package/src/lib/i18n.ts +25 -2
  33. package/dist/assets/DocBrowser-BY0TiFOc.js +0 -1
  34. package/dist/assets/MarketplacePage-BDlAw7fO.js +0 -1
  35. package/dist/assets/ProvidersList-D2OB0siE.js +0 -1
  36. package/dist/assets/index-C1NAfZSm.js +0 -2
  37. 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
- ArrowLeft,
7
- ArrowRight,
8
- X,
9
- ExternalLink,
10
- PanelRightOpen,
11
- Maximize2,
12
- GripVertical,
13
- Search,
14
- BookOpen,
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 two modes:
21
- * - `docked`: Renders as a right sidebar panel (horizontally resizable)
22
- * - `floating`: Renders as a draggable, resizable overlay
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
- const {
26
- isOpen, mode, currentUrl, navVersion,
27
- close, toggleMode,
28
- goBack, goForward, canGoBack, canGoForward,
29
- navigate, syncUrl,
30
- } = useDocBrowser();
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
- const [urlInput, setUrlInput] = useState('');
33
- const [isDragging, setIsDragging] = useState(false);
34
- const [isResizing, setIsResizing] = useState(false);
35
- const [floatPos, setFloatPos] = useState(() => ({
36
- x: Math.max(40, window.innerWidth - 520),
37
- y: 80,
38
- }));
39
- const [floatSize, setFloatSize] = useState({ w: 480, h: 600 });
40
- const [dockedWidth, setDockedWidth] = useState(420);
41
- const dragRef = useRef<{ startX: number; startY: number; startPosX: number; startPosY: number } | null>(null);
42
- const resizeRef = useRef<{ startX: number; startY: number; startW: number; startH: number } | null>(null);
43
- const dockResizeRef = useRef<{ startX: number; startW: number } | null>(null);
44
- const iframeRef = useRef<HTMLIFrameElement>(null);
45
- const prevNavVersionRef = useRef(navVersion);
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
- // Sync URL input with current URL
48
- useEffect(() => {
49
- try {
50
- const parsed = new URL(currentUrl);
51
- setUrlInput(parsed.pathname);
52
- } catch {
53
- setUrlInput(currentUrl);
54
- }
55
- }, [currentUrl]);
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
- // When currentUrl changes without navVersion bump (goBack/goForward),
58
- // use postMessage to SPA-navigate inside the iframe instead of remounting
59
- useEffect(() => {
60
- if (navVersion !== prevNavVersionRef.current) {
61
- // navVersion changed — iframe will remount via key, no need to postMessage
62
- prevNavVersionRef.current = navVersion;
63
- return;
64
- }
65
- // Same navVersion — means goBack/goForward triggered
66
- if (iframeRef.current?.contentWindow) {
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
- // Reposition floating window near right edge when switching from docked
75
- useEffect(() => {
76
- if (mode === 'floating') {
77
- setFloatPos(prev => ({
78
- x: Math.max(40, window.innerWidth - floatSize.w - 40),
79
- y: prev.y,
80
- }));
81
- }
82
- }, [mode, floatSize.w]);
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
- // Listen for route changes from the iframe via postMessage
85
- useEffect(() => {
86
- const handler = (e: MessageEvent) => {
87
- if (e.data?.type === 'docs-route-change' && typeof e.data.url === 'string') {
88
- syncUrl(e.data.url);
89
- }
90
- };
91
- window.addEventListener('message', handler);
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
- const handleUrlSubmit = useCallback((e: React.FormEvent) => {
96
- e.preventDefault();
97
- const input = urlInput.trim();
98
- if (!input) return;
99
- if (input.startsWith('/')) {
100
- navigate(`${DOCS_DEFAULT_BASE_URL}${input}`);
101
- } else if (input.startsWith('http')) {
102
- navigate(input);
103
- } else {
104
- navigate(`${DOCS_DEFAULT_BASE_URL}/${input}`);
105
- }
106
- }, [urlInput, navigate]);
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
- // --- Dragging logic (floating mode) ---
109
- const onDragStart = useCallback((e: React.MouseEvent) => {
110
- if (mode !== 'floating') return;
111
- setIsDragging(true);
112
- dragRef.current = {
113
- startX: e.clientX,
114
- startY: e.clientY,
115
- startPosX: floatPos.x,
116
- startPosY: floatPos.y,
117
- };
118
- }, [mode, floatPos]);
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
- useEffect(() => {
121
- if (!isDragging) return;
122
- const onMove = (e: MouseEvent) => {
123
- if (!dragRef.current) return;
124
- setFloatPos({
125
- x: dragRef.current.startPosX + (e.clientX - dragRef.current.startX),
126
- y: dragRef.current.startPosY + (e.clientY - dragRef.current.startY),
127
- });
128
- };
129
- const onUp = () => {
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
- // --- Resize logic (floating mode) ---
142
- const onResizeStart = useCallback((e: React.MouseEvent) => {
143
- e.preventDefault();
144
- e.stopPropagation();
145
- setIsResizing(true);
146
- const axis = (e.currentTarget as HTMLElement).dataset.axis; // 'x', 'y', or undefined (both)
147
- resizeRef.current = {
148
- startX: e.clientX,
149
- startY: e.clientY,
150
- startW: floatSize.w,
151
- startH: floatSize.h,
152
- };
153
- const onMove = (ev: MouseEvent) => {
154
- if (!resizeRef.current) return;
155
- setFloatSize(prev => ({
156
- w: axis === 'y' ? prev.w : Math.max(360, resizeRef.current!.startW + (ev.clientX - resizeRef.current!.startX)),
157
- h: axis === 'x' ? prev.h : Math.max(400, resizeRef.current!.startH + (ev.clientY - resizeRef.current!.startY)),
158
- }));
159
- };
160
- const onUp = () => {
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
- // --- Horizontal resize logic (docked mode left edge) ---
171
- const onDockResizeStart = useCallback((e: React.MouseEvent) => {
172
- e.preventDefault();
173
- e.stopPropagation();
174
- setIsResizing(true);
175
- dockResizeRef.current = { startX: e.clientX, startW: dockedWidth };
176
- const onMove = (ev: MouseEvent) => {
177
- if (!dockResizeRef.current) return;
178
- const delta = dockResizeRef.current.startX - ev.clientX;
179
- setDockedWidth(Math.max(320, Math.min(800, dockResizeRef.current.startW + delta)));
180
- };
181
- const onUp = () => {
182
- setIsResizing(false);
183
- dockResizeRef.current = null;
184
- window.removeEventListener('mousemove', onMove);
185
- window.removeEventListener('mouseup', onUp);
186
- };
187
- window.addEventListener('mousemove', onMove);
188
- window.addEventListener('mouseup', onUp);
189
- }, [dockedWidth]);
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
- // --- Left-edge resize logic (floating mode adjusts both position and width) ---
192
- const onLeftResizeStart = useCallback((e: React.MouseEvent) => {
193
- e.preventDefault();
194
- e.stopPropagation();
195
- setIsResizing(true);
196
- const startX = e.clientX;
197
- const startW = floatSize.w;
198
- const startPosX = floatPos.x;
199
- const onMove = (ev: MouseEvent) => {
200
- const delta = startX - ev.clientX;
201
- const newW = Math.max(360, startW + delta);
202
- setFloatSize(prev => ({ ...prev, w: newW }));
203
- setFloatPos(prev => ({ ...prev, x: startPosX - (newW - startW) }));
204
- };
205
- const onUp = () => {
206
- setIsResizing(false);
207
- window.removeEventListener('mousemove', onMove);
208
- window.removeEventListener('mouseup', onUp);
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
- if (!isOpen) return null;
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
- const isDocked = mode === 'docked';
236
+ if (!isOpen) return null;
217
237
 
218
- const panel = (
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
- className={cn(
221
- 'flex flex-col bg-white overflow-hidden relative',
222
- isDocked
223
- ? 'h-full border-l border-gray-200 shrink-0'
224
- : 'rounded-2xl shadow-2xl border border-gray-200',
225
- )}
226
- style={
227
- isDocked
228
- ? { width: dockedWidth }
229
- : {
230
- position: 'fixed',
231
- left: floatPos.x,
232
- top: floatPos.y,
233
- width: floatSize.w,
234
- height: floatSize.h,
235
- zIndex: 9999,
236
- }
237
- }
238
- >
239
- {/* Docked mode: left-edge resize handle */}
240
- {isDocked && (
241
- <div
242
- className="absolute top-0 left-0 w-1.5 h-full cursor-ew-resize z-20 hover:bg-primary/10 transition-colors"
243
- onMouseDown={onDockResizeStart}
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
- {/* Title Bar */}
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
- className={cn(
250
- 'flex items-center justify-between px-4 py-2.5 bg-gray-50 border-b border-gray-200 shrink-0 select-none',
251
- !isDocked && 'cursor-grab active:cursor-grabbing',
252
- )}
253
- onMouseDown={!isDocked ? onDragStart : undefined}
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
- <div className="flex items-center gap-2.5">
256
- <BookOpen className="w-4 h-4 text-primary" />
257
- <span className="text-sm font-semibold text-gray-900">{t('docBrowserTitle')}</span>
258
- </div>
259
- <div className="flex items-center gap-1">
260
- <button
261
- onClick={toggleMode}
262
- className="hover:bg-gray-200 rounded-md p-1.5 text-gray-500 hover:text-gray-700 transition-colors"
263
- title={isDocked ? t('docBrowserFloatMode') : t('docBrowserDockMode')}
264
- >
265
- {isDocked ? <Maximize2 className="w-3.5 h-3.5" /> : <PanelRightOpen className="w-3.5 h-3.5" />}
266
- </button>
267
- <button
268
- onClick={close}
269
- className="hover:bg-gray-200 rounded-md p-1.5 text-gray-500 hover:text-gray-700 transition-colors"
270
- title={t('docBrowserClose')}
271
- >
272
- <X className="w-3.5 h-3.5" />
273
- </button>
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
- {/* Navigation Bar */}
278
- <div className="flex items-center gap-2 px-3.5 py-2 bg-white border-b border-gray-100 shrink-0">
279
- <button
280
- onClick={goBack}
281
- disabled={!canGoBack}
282
- className="p-1.5 rounded-md hover:bg-gray-100 disabled:opacity-30 disabled:cursor-not-allowed text-gray-600 transition-colors"
283
- >
284
- <ArrowLeft className="w-4 h-4" />
285
- </button>
286
- <button
287
- onClick={goForward}
288
- disabled={!canGoForward}
289
- className="p-1.5 rounded-md hover:bg-gray-100 disabled:opacity-30 disabled:cursor-not-allowed text-gray-600 transition-colors"
290
- >
291
- <ArrowRight className="w-4 h-4" />
292
- </button>
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
- {/* Iframe Content */}
307
- <div className="flex-1 relative overflow-hidden">
308
- <iframe
309
- ref={iframeRef}
310
- key={navVersion}
311
- src={currentUrl}
312
- className="absolute inset-0 w-full h-full border-0"
313
- title="NextClaw Documentation"
314
- sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
315
- allow="clipboard-read; clipboard-write"
316
- />
317
- {/* Transparent overlay during resize to prevent iframe from stealing mouse events */}
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
- {/* Footer */}
324
- <div className="flex items-center justify-between px-4 py-2 bg-gray-50 border-t border-gray-200 shrink-0">
325
- <a
326
- href={currentUrl}
327
- target="_blank"
328
- rel="noopener noreferrer"
329
- data-doc-external
330
- className="flex items-center gap-1.5 text-xs text-primary hover:text-primary-hover font-medium transition-colors"
331
- >
332
- {t('docBrowserOpenExternal')}
333
- <ExternalLink className="w-3 h-3" />
334
- </a>
335
- </div>
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
- {/* Resize Handles (floating only) */}
338
- {!isDocked && (
339
- <>
340
- {/* Left edge */}
341
- <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} />
342
- {/* Right edge */}
343
- <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" />
344
- {/* Bottom edge */}
345
- <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" />
346
- {/* Bottom-right corner */}
347
- <div
348
- 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"
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
- return panel;
417
+ return panel;
359
418
  }