@nextclaw/ui 0.12.35-beta.0 → 0.12.35-beta.2

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 (77) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/dist/assets/api-D3uoaOR8.js +15 -0
  3. package/dist/assets/{book-open-825Pcd0z.js → book-open--T9dJHnp.js} +1 -1
  4. package/dist/assets/channels-list-page-CzDMCXR_.js +8 -0
  5. package/dist/assets/{chat-page-RsUTK_bY.js → chat-page-Doi-sasO.js} +2 -2
  6. package/dist/assets/{config-split-page-BvfxFQSb.js → config-split-page-Dutq3R5o.js} +1 -1
  7. package/dist/assets/{createLucideIcon-CeNHY6BW.js → createLucideIcon-Cwr2o6Zq.js} +1 -1
  8. package/dist/assets/{desktop-update-config-B_YIv5ZJ.js → desktop-update-config-Dd4DdPH3.js} +1 -1
  9. package/dist/assets/{dist-uH7tiW6E.js → dist-e5WATjIW.js} +1 -1
  10. package/dist/assets/doc-browser-DInzGY0W.js +1 -0
  11. package/dist/assets/doc-browser-DxVvmYQk.js +1 -0
  12. package/dist/assets/{doc-browser-context-CQAx_92e.js → doc-browser-context-CLczyX2Z.js} +1 -1
  13. package/dist/assets/{es2015-c9XYpli5.js → es2015-BJw-hrex.js} +1 -1
  14. package/dist/assets/{external-link-BE1deZ_c.js → external-link-CZ9bQY0L.js} +1 -1
  15. package/dist/assets/i18n-tWKGEGxB.js +1 -0
  16. package/dist/assets/index-HvJDzfSU.css +1 -0
  17. package/dist/assets/{index-BjH_p3R6.js → index-dHwM4pEn.js} +44 -44
  18. package/dist/assets/{key-round-CHo3gqpO.js → key-round-B8tTb3ZJ.js} +1 -1
  19. package/dist/assets/mcp-marketplace-page-D-dydvbL.js +1 -0
  20. package/dist/assets/{mcp-marketplace-page-0C214hGi.js → mcp-marketplace-page-n9Z25Cyk.js} +2 -2
  21. package/dist/assets/model-config-8BVoSVXQ.js +1 -0
  22. package/dist/assets/{notice-card-DO33_LzD.js → notice-card-DUDbOvqx.js} +1 -1
  23. package/dist/assets/play-CV_kwi9X.js +1 -0
  24. package/dist/assets/plus-BqfNq7Tu.js +1 -0
  25. package/dist/assets/{provider-scoped-model-input-B0_r_4EY.js → provider-scoped-model-input-eO4XY0iY.js} +1 -1
  26. package/dist/assets/providers-list-CjJmW-od.js +1 -0
  27. package/dist/assets/remote-DxTFnQm8.js +1 -0
  28. package/dist/assets/{rotate-cw-CYUByuqo.js → rotate-cw-BKEhWuWp.js} +1 -1
  29. package/dist/assets/runtime-config-page-Ba6atCMq.js +1 -0
  30. package/dist/assets/{save-B6Ga8JIq.js → save-B6wbBLkU.js} +1 -1
  31. package/dist/assets/{search-5kKpgNoP.js → search-Czito3Dk.js} +1 -1
  32. package/dist/assets/search-config-BtCNY431.js +1 -0
  33. package/dist/assets/{secrets-config-CrXA11bo.js → secrets-config-Ddh05-J5.js} +2 -2
  34. package/dist/assets/{select-C0hGOCOK.js → select-CmmDgG8o.js} +1 -1
  35. package/dist/assets/{setting-row-xsM_0nTd.js → setting-row-BUO6FeN1.js} +1 -1
  36. package/dist/assets/{tabs-custom-Bnqfhe4p.js → tabs-custom-D3IZL9WX.js} +1 -1
  37. package/dist/assets/{tag-chip-cktJJMzh.js → tag-chip-Dm_pOtE3.js} +1 -1
  38. package/dist/assets/x-DBP3WiC9.js +1 -0
  39. package/dist/index.html +17 -17
  40. package/package.json +9 -9
  41. package/src/app/components/layout/app-layout.tsx +3 -2
  42. package/src/features/chat/components/chat-session-workspace-panel.tsx +1 -1
  43. package/src/features/chat/components/layout/chat-sidebar-read-state.test.tsx +145 -0
  44. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +16 -0
  45. package/src/features/chat/components/layout/chat-sidebar.tsx +3 -2
  46. package/src/features/panel-apps/components/panel-app-list-item.tsx +83 -0
  47. package/src/features/panel-apps/components/panel-app-toolbar.tsx +34 -0
  48. package/src/features/panel-apps/components/panel-apps-list.tsx +109 -0
  49. package/src/features/panel-apps/hooks/use-panel-apps.ts +39 -0
  50. package/src/features/panel-apps/index.ts +4 -0
  51. package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +56 -0
  52. package/src/features/panel-apps/utils/panel-app-view.utils.ts +43 -0
  53. package/src/platforms/desktop/components/desktop-app-shell.tsx +5 -2
  54. package/src/platforms/mobile/components/mobile-app-shell.tsx +4 -1
  55. package/src/shared/components/doc-browser/doc-browser-context.tsx +7 -7
  56. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +201 -0
  57. package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +19 -0
  58. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +67 -0
  59. package/src/shared/components/doc-browser/doc-browser.tsx +74 -145
  60. package/src/shared/lib/api/types.ts +24 -0
  61. package/src/shared/lib/i18n/index.ts +2 -12
  62. package/src/shared/lib/i18n/runtime/doc-browser-labels.utils.ts +29 -0
  63. package/dist/assets/api-De1PAch0.js +0 -15
  64. package/dist/assets/channels-list-page-CGjXDSD-.js +0 -8
  65. package/dist/assets/doc-browser-CDe9yz9n.js +0 -1
  66. package/dist/assets/doc-browser-DwT64GuF.js +0 -1
  67. package/dist/assets/i18n-DnubSHoK.js +0 -1
  68. package/dist/assets/index-6WZ6JjaU.css +0 -1
  69. package/dist/assets/mcp-marketplace-page-amWcMncu.js +0 -1
  70. package/dist/assets/model-config-B8zNrkag.js +0 -1
  71. package/dist/assets/play-CqlWbVNo.js +0 -1
  72. package/dist/assets/plus-p61tQh2-.js +0 -1
  73. package/dist/assets/providers-list-B6v3foi7.js +0 -1
  74. package/dist/assets/remote-k671rLmY.js +0 -1
  75. package/dist/assets/runtime-config-page-DgH-XJwK.js +0 -1
  76. package/dist/assets/search-config-DPZ6d9y6.js +0 -1
  77. package/dist/assets/x-DDQw-C2x.js +0 -1
@@ -0,0 +1,201 @@
1
+ import type { ReactNode, Ref } from 'react';
2
+ import {
3
+ ArrowLeft,
4
+ ArrowRight,
5
+ BookOpen,
6
+ ExternalLink,
7
+ Maximize2,
8
+ PanelRightOpen,
9
+ Search,
10
+ X,
11
+ } from 'lucide-react';
12
+ import {
13
+ isDocsUrl,
14
+ type DocBrowserTab,
15
+ } from './doc-browser-context';
16
+ import type { DocBrowserCustomTabRenderer } from './doc-browser-renderer.types';
17
+ import { cn } from '@/shared/lib/utils';
18
+ import { t } from '@/shared/lib/i18n';
19
+
20
+ type DocBrowserPanelHeaderProps = {
21
+ currentTab?: DocBrowserTab;
22
+ customRenderer?: DocBrowserCustomTabRenderer;
23
+ isDocked: boolean;
24
+ isFullscreen: boolean;
25
+ onClose: () => void;
26
+ onDragStart: (e: React.MouseEvent) => void;
27
+ onToggleMode: () => void;
28
+ };
29
+
30
+ type DocBrowserDocsToolbarProps = {
31
+ currentTab?: DocBrowserTab;
32
+ isDocsTab: boolean;
33
+ onBack: () => void;
34
+ onForward: () => void;
35
+ onSubmit: (e: React.FormEvent) => void;
36
+ onUrlInputChange: (value: string) => void;
37
+ urlInput: string;
38
+ };
39
+
40
+ type DocBrowserFrameContentProps = {
41
+ activeTabId: string;
42
+ currentTab?: DocBrowserTab;
43
+ currentUrl: string;
44
+ customContent: ReactNode;
45
+ iframeRef: Ref<HTMLIFrameElement>;
46
+ iframeReloadVersion: number;
47
+ iframeSandbox: string;
48
+ isDragging: boolean;
49
+ isResizing: boolean;
50
+ navVersion: number;
51
+ };
52
+
53
+ export function DocBrowserPanelHeader({
54
+ currentTab,
55
+ customRenderer,
56
+ isDocked,
57
+ isFullscreen,
58
+ onClose,
59
+ onDragStart,
60
+ onToggleMode,
61
+ }: DocBrowserPanelHeaderProps) {
62
+ const title = currentTab && customRenderer?.getTitle
63
+ ? customRenderer.getTitle(currentTab)
64
+ : t('docBrowserTitle');
65
+ const icon = currentTab && customRenderer?.renderIcon
66
+ ? customRenderer.renderIcon(currentTab)
67
+ : <BookOpen className="w-4 h-4 text-primary shrink-0" />;
68
+
69
+ return (
70
+ <div
71
+ className={cn(
72
+ 'flex items-center justify-between px-4 py-2.5 bg-gray-50 border-b border-gray-200 shrink-0 select-none',
73
+ !isDocked && !isFullscreen && 'cursor-grab active:cursor-grabbing',
74
+ isFullscreen && 'pt-[calc(env(safe-area-inset-top,0px)+0.625rem)]',
75
+ )}
76
+ onMouseDown={!isDocked && !isFullscreen ? onDragStart : undefined}
77
+ >
78
+ <div className="flex items-center gap-2.5 min-w-0">
79
+ {icon}
80
+ <span className="text-sm font-semibold text-gray-900 truncate">{title}</span>
81
+ </div>
82
+ <div className="flex items-center gap-1">
83
+ {!isFullscreen ? (
84
+ <button
85
+ onClick={onToggleMode}
86
+ className="hover:bg-gray-200 rounded-md p-1.5 text-gray-500 hover:text-gray-700 transition-colors"
87
+ title={isDocked ? t('docBrowserFloatMode') : t('docBrowserDockMode')}
88
+ >
89
+ {isDocked ? <Maximize2 className="w-3.5 h-3.5" /> : <PanelRightOpen className="w-3.5 h-3.5" />}
90
+ </button>
91
+ ) : null}
92
+ <button
93
+ onClick={onClose}
94
+ className="hover:bg-gray-200 rounded-md p-1.5 text-gray-500 hover:text-gray-700 transition-colors"
95
+ title={t('docBrowserClose')}
96
+ >
97
+ <X className="w-3.5 h-3.5" />
98
+ </button>
99
+ </div>
100
+ </div>
101
+ );
102
+ }
103
+
104
+ export function DocBrowserDocsToolbar({
105
+ currentTab,
106
+ isDocsTab,
107
+ onBack,
108
+ onForward,
109
+ onSubmit,
110
+ onUrlInputChange,
111
+ urlInput,
112
+ }: DocBrowserDocsToolbarProps) {
113
+ if (!isDocsTab) {
114
+ return null;
115
+ }
116
+
117
+ return (
118
+ <div className="flex items-center gap-2 px-3.5 py-2 bg-white border-b border-gray-100 shrink-0">
119
+ <button
120
+ onClick={onBack}
121
+ disabled={!isDocsTab || (currentTab?.historyIndex ?? 0) <= 0}
122
+ className="p-1.5 rounded-md hover:bg-gray-100 disabled:opacity-30 disabled:cursor-not-allowed text-gray-600 transition-colors"
123
+ >
124
+ <ArrowLeft className="w-4 h-4" />
125
+ </button>
126
+ <button
127
+ onClick={onForward}
128
+ disabled={!isDocsTab || (currentTab?.historyIndex ?? 0) >= (currentTab?.history.length ?? 0) - 1}
129
+ className="p-1.5 rounded-md hover:bg-gray-100 disabled:opacity-30 disabled:cursor-not-allowed text-gray-600 transition-colors"
130
+ >
131
+ <ArrowRight className="w-4 h-4" />
132
+ </button>
133
+
134
+ <form onSubmit={onSubmit} className="flex-1 relative">
135
+ <Search className="w-3.5 h-3.5 absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
136
+ <input
137
+ type="text"
138
+ value={urlInput}
139
+ onChange={(e) => onUrlInputChange(e.target.value)}
140
+ placeholder={t('docBrowserSearchPlaceholder')}
141
+ 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"
142
+ />
143
+ </form>
144
+ </div>
145
+ );
146
+ }
147
+
148
+ export function DocBrowserFrameContent({
149
+ activeTabId,
150
+ currentTab,
151
+ currentUrl,
152
+ customContent,
153
+ iframeRef,
154
+ iframeReloadVersion,
155
+ iframeSandbox,
156
+ isDragging,
157
+ isResizing,
158
+ navVersion,
159
+ }: DocBrowserFrameContentProps) {
160
+ return (
161
+ <div className="flex-1 relative overflow-hidden">
162
+ {customContent ? (
163
+ customContent
164
+ ) : (
165
+ <iframe
166
+ ref={iframeRef}
167
+ key={`${activeTabId}:${navVersion}:${iframeReloadVersion}`}
168
+ src={currentUrl}
169
+ className="absolute inset-0 w-full h-full border-0"
170
+ title={currentTab?.title || 'NextClaw Docs'}
171
+ sandbox={iframeSandbox}
172
+ allow="clipboard-read; clipboard-write"
173
+ />
174
+ )}
175
+ {(isResizing || isDragging) && (
176
+ <div className="absolute inset-0 z-10" />
177
+ )}
178
+ </div>
179
+ );
180
+ }
181
+
182
+ export function DocBrowserExternalLink({ currentUrl, isDocsTab }: { currentUrl: string; isDocsTab: boolean }) {
183
+ if (!isDocsTab || !isDocsUrl(currentUrl)) {
184
+ return null;
185
+ }
186
+
187
+ return (
188
+ <div className="flex items-center justify-between px-4 py-2 bg-gray-50 border-t border-gray-200 shrink-0">
189
+ <a
190
+ href={currentUrl}
191
+ target="_blank"
192
+ rel="noopener noreferrer"
193
+ data-doc-external
194
+ className="flex items-center gap-1.5 text-xs text-primary hover:text-primary-hover font-medium transition-colors"
195
+ >
196
+ {t('docBrowserOpenExternal')}
197
+ <ExternalLink className="w-3 h-3" />
198
+ </a>
199
+ </div>
200
+ );
201
+ }
@@ -0,0 +1,19 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { DocBrowserContextValue, DocBrowserTab } from './doc-browser-context';
3
+
4
+ export type DocBrowserCustomTabRenderParams = {
5
+ currentUrl: string;
6
+ open: DocBrowserContextValue['open'];
7
+ refreshIframe: () => void;
8
+ tab: DocBrowserTab;
9
+ };
10
+
11
+ export type DocBrowserCustomTabRenderer = {
12
+ getIframeSandbox?: (tab: DocBrowserTab) => string;
13
+ getTitle?: (tab: DocBrowserTab) => string;
14
+ renderContent?: (params: DocBrowserCustomTabRenderParams) => ReactNode;
15
+ renderIcon?: (tab: DocBrowserTab) => ReactNode;
16
+ renderToolbar?: (params: DocBrowserCustomTabRenderParams) => ReactNode;
17
+ };
18
+
19
+ export type DocBrowserCustomTabRenderers = Record<string, DocBrowserCustomTabRenderer>;
@@ -0,0 +1,67 @@
1
+ import { Plus, X } from 'lucide-react';
2
+ import type { DocBrowserTab } from './doc-browser-context';
3
+ import { cn } from '@/shared/lib/utils';
4
+ import { t } from '@/shared/lib/i18n';
5
+
6
+ type DocBrowserTabStripProps = {
7
+ tabs: DocBrowserTab[];
8
+ activeTabId: string;
9
+ onOpenDocs: () => void;
10
+ onSetActiveTab: (tabId: string) => void;
11
+ onCloseTab: (tabId: string) => void;
12
+ };
13
+
14
+ export function DocBrowserTabStrip({
15
+ tabs,
16
+ activeTabId,
17
+ onOpenDocs,
18
+ onSetActiveTab,
19
+ onCloseTab,
20
+ }: DocBrowserTabStripProps) {
21
+ return (
22
+ <div className="flex items-center gap-1.5 px-2.5 py-2 bg-background border-b border-[#f1e7d4] overflow-x-auto custom-scrollbar">
23
+ {tabs.map((tab) => {
24
+ const isActive = tab.id === activeTabId;
25
+ return (
26
+ <div
27
+ key={tab.id}
28
+ className={cn(
29
+ 'inline-flex items-center gap-1 h-7 px-1.5 rounded-lg text-xs border max-w-[220px] shrink-0 transition-colors',
30
+ isActive
31
+ ? 'bg-amber-50/80 border-amber-200 text-amber-900 shadow-[0_1px_2px_rgba(30,20,10,0.04)]'
32
+ : 'bg-[#f9f8f5] border-[#eee3d1] text-[#78644d] hover:bg-[#fff7ea] hover:text-[#2f2212]',
33
+ )}
34
+ >
35
+ <button
36
+ type="button"
37
+ onClick={() => onSetActiveTab(tab.id)}
38
+ className="truncate text-left px-1"
39
+ title={tab.title}
40
+ >
41
+ {tab.title || t('docBrowserTabUntitled')}
42
+ </button>
43
+ <button
44
+ type="button"
45
+ onClick={(event) => {
46
+ event.stopPropagation();
47
+ onCloseTab(tab.id);
48
+ }}
49
+ className="rounded p-0.5 hover:bg-black/10"
50
+ aria-label={t('docBrowserCloseTab')}
51
+ >
52
+ <X className="w-3 h-3" />
53
+ </button>
54
+ </div>
55
+ );
56
+ })}
57
+ <button
58
+ type="button"
59
+ onClick={onOpenDocs}
60
+ className="inline-flex items-center justify-center w-7 h-7 rounded-lg border border-[#eee3d1] bg-white text-[#78644d] hover:bg-[#fff7ea] hover:text-[#2f2212] shrink-0"
61
+ title={t('docBrowserNewTab')}
62
+ >
63
+ <Plus className="w-3.5 h-3.5" />
64
+ </button>
65
+ </div>
66
+ );
67
+ }
@@ -1,22 +1,22 @@
1
1
  import { useState, useRef, useCallback, useEffect, type CSSProperties } from 'react';
2
- import { DOCS_DEFAULT_BASE_URL, isDocsUrl, useDocBrowser } from './doc-browser-context';
2
+ import {
3
+ DOCS_DEFAULT_BASE_URL,
4
+ useDocBrowser,
5
+ } from './doc-browser-context';
6
+ import type { DocBrowserCustomTabRenderers } from './doc-browser-renderer.types';
7
+ import {
8
+ DocBrowserDocsToolbar,
9
+ DocBrowserExternalLink,
10
+ DocBrowserFrameContent,
11
+ DocBrowserPanelHeader,
12
+ } from './doc-browser-panel-parts';
13
+ import { DocBrowserTabStrip } from './doc-browser-tab-strip';
3
14
  import { ResizableRightPanel } from '@/shared/components/resizable-right-panel/resizable-right-panel';
4
15
  import { cn } from '@/shared/lib/utils';
5
- import { t } from '@/shared/lib/i18n';
6
- import {
7
- ArrowLeft,
8
- ArrowRight,
9
- X,
10
- ExternalLink,
11
- PanelRightOpen,
12
- Maximize2,
13
- GripVertical,
14
- Search,
15
- BookOpen,
16
- Plus,
17
- } from 'lucide-react';
16
+ import { GripVertical } from 'lucide-react';
18
17
 
19
18
  type DocBrowserProps = {
19
+ customTabRenderers?: DocBrowserCustomTabRenderers;
20
20
  displayMode?: 'desktop' | 'fullscreen';
21
21
  };
22
22
 
@@ -46,7 +46,10 @@ function getPanelStyle(params: {
46
46
  };
47
47
  }
48
48
 
49
- export function DocBrowser({ displayMode = 'desktop' }: DocBrowserProps) {
49
+ export function DocBrowser({
50
+ customTabRenderers = {},
51
+ displayMode = 'desktop',
52
+ }: DocBrowserProps) {
50
53
  const {
51
54
  isOpen,
52
55
  mode,
@@ -67,6 +70,7 @@ export function DocBrowser({ displayMode = 'desktop' }: DocBrowserProps) {
67
70
  const [urlInput, setUrlInput] = useState('');
68
71
  const [isDragging, setIsDragging] = useState(false);
69
72
  const [isResizing, setIsResizing] = useState(false);
73
+ const [iframeReloadVersion, setIframeReloadVersion] = useState(0);
70
74
  const [floatPos, setFloatPos] = useState(() => ({
71
75
  x: Math.max(40, window.innerWidth - 520),
72
76
  y: 80,
@@ -232,148 +236,73 @@ export function DocBrowser({ displayMode = 'desktop' }: DocBrowserProps) {
232
236
  window.addEventListener('mouseup', onUp);
233
237
  }, [floatSize.w, floatPos.x]);
234
238
 
239
+ const refreshIframe = useCallback(() => {
240
+ setIframeReloadVersion((version) => version + 1);
241
+ }, []);
242
+
235
243
  if (!isOpen) return null;
236
244
 
237
245
  const isDocked = mode === 'docked';
238
246
  const isFullscreen = displayMode === 'fullscreen';
247
+ const customRenderer = currentTab ? customTabRenderers[currentTab.kind] : undefined;
248
+ const customRenderParams = currentTab ? {
249
+ currentUrl,
250
+ open,
251
+ refreshIframe,
252
+ tab: currentTab,
253
+ } : undefined;
254
+ const customToolbar = customRenderParams ? customRenderer?.renderToolbar?.(customRenderParams) : null;
255
+ const customContent = customRenderParams ? customRenderer?.renderContent?.(customRenderParams) : null;
256
+ const iframeSandbox = currentTab
257
+ ? customRenderer?.getIframeSandbox?.(currentTab) ?? 'allow-same-origin allow-scripts allow-popups allow-forms'
258
+ : 'allow-same-origin allow-scripts allow-popups allow-forms';
239
259
 
240
260
  const panelContent = (
241
261
  <>
242
- <div
243
- className={cn(
244
- 'flex items-center justify-between px-4 py-2.5 bg-gray-50 border-b border-gray-200 shrink-0 select-none',
245
- !isDocked && !isFullscreen && 'cursor-grab active:cursor-grabbing',
246
- isFullscreen && 'pt-[calc(env(safe-area-inset-top,0px)+0.625rem)]',
247
- )}
248
- onMouseDown={!isDocked && !isFullscreen ? onDragStart : undefined}
249
- >
250
- <div className="flex items-center gap-2.5 min-w-0">
251
- <BookOpen className="w-4 h-4 text-primary shrink-0" />
252
- <span className="text-sm font-semibold text-gray-900 truncate">{t('docBrowserTitle')}</span>
253
- </div>
254
- <div className="flex items-center gap-1">
255
- {!isFullscreen ? (
256
- <button
257
- onClick={toggleMode}
258
- className="hover:bg-gray-200 rounded-md p-1.5 text-gray-500 hover:text-gray-700 transition-colors"
259
- title={isDocked ? t('docBrowserFloatMode') : t('docBrowserDockMode')}
260
- >
261
- {isDocked ? <Maximize2 className="w-3.5 h-3.5" /> : <PanelRightOpen className="w-3.5 h-3.5" />}
262
- </button>
263
- ) : null}
264
- <button
265
- onClick={close}
266
- className="hover:bg-gray-200 rounded-md p-1.5 text-gray-500 hover:text-gray-700 transition-colors"
267
- title={t('docBrowserClose')}
268
- >
269
- <X className="w-3.5 h-3.5" />
270
- </button>
271
- </div>
272
- </div>
262
+ <DocBrowserPanelHeader
263
+ currentTab={currentTab}
264
+ customRenderer={customRenderer}
265
+ isDocked={isDocked}
266
+ isFullscreen={isFullscreen}
267
+ onClose={close}
268
+ onDragStart={onDragStart}
269
+ onToggleMode={toggleMode}
270
+ />
273
271
 
274
- <div className="flex items-center gap-1.5 px-2.5 py-2 bg-background border-b border-[#f1e7d4] overflow-x-auto custom-scrollbar">
275
- {tabs.map((tab) => {
276
- const isActive = tab.id === activeTabId;
277
- return (
278
- <div
279
- key={tab.id}
280
- className={cn(
281
- 'inline-flex items-center gap-1 h-7 px-1.5 rounded-lg text-xs border max-w-[220px] shrink-0 transition-colors',
282
- isActive
283
- ? 'bg-amber-50/80 border-amber-200 text-amber-900 shadow-[0_1px_2px_rgba(30,20,10,0.04)]'
284
- : 'bg-[#f9f8f5] border-[#eee3d1] text-[#78644d] hover:bg-[#fff7ea] hover:text-[#2f2212]'
285
- )}
286
- >
287
- <button
288
- type="button"
289
- onClick={() => setActiveTab(tab.id)}
290
- className="truncate text-left px-1"
291
- title={tab.title}
292
- >
293
- {tab.title || t('docBrowserTabUntitled')}
294
- </button>
295
- <button
296
- type="button"
297
- onClick={(event) => {
298
- event.stopPropagation();
299
- closeTab(tab.id);
300
- }}
301
- className="rounded p-0.5 hover:bg-black/10"
302
- aria-label={t('docBrowserCloseTab')}
303
- >
304
- <X className="w-3 h-3" />
305
- </button>
306
- </div>
307
- );
308
- })}
309
- <button
310
- onClick={() => open(undefined, { kind: 'docs', newTab: true, title: 'Docs' })}
311
- className="inline-flex items-center justify-center w-7 h-7 rounded-lg border border-[#eee3d1] bg-white text-[#78644d] hover:bg-[#fff7ea] hover:text-[#2f2212] shrink-0"
312
- title={t('docBrowserNewTab')}
313
- >
314
- <Plus className="w-3.5 h-3.5" />
315
- </button>
316
- </div>
272
+ <DocBrowserTabStrip
273
+ tabs={tabs}
274
+ activeTabId={activeTabId}
275
+ onOpenDocs={() => open(undefined, { kind: 'docs', newTab: true, title: 'Docs' })}
276
+ onSetActiveTab={setActiveTab}
277
+ onCloseTab={closeTab}
278
+ />
317
279
 
318
- {isDocsTab && (
319
- <div className="flex items-center gap-2 px-3.5 py-2 bg-white border-b border-gray-100 shrink-0">
320
- <button
321
- onClick={goBack}
322
- disabled={!isDocsTab || (currentTab?.historyIndex ?? 0) <= 0}
323
- className="p-1.5 rounded-md hover:bg-gray-100 disabled:opacity-30 disabled:cursor-not-allowed text-gray-600 transition-colors"
324
- >
325
- <ArrowLeft className="w-4 h-4" />
326
- </button>
327
- <button
328
- onClick={goForward}
329
- disabled={!isDocsTab || (currentTab?.historyIndex ?? 0) >= (currentTab?.history.length ?? 0) - 1}
330
- className="p-1.5 rounded-md hover:bg-gray-100 disabled:opacity-30 disabled:cursor-not-allowed text-gray-600 transition-colors"
331
- >
332
- <ArrowRight className="w-4 h-4" />
333
- </button>
280
+ <DocBrowserDocsToolbar
281
+ currentTab={currentTab}
282
+ isDocsTab={isDocsTab}
283
+ onBack={goBack}
284
+ onForward={goForward}
285
+ onSubmit={handleUrlSubmit}
286
+ onUrlInputChange={setUrlInput}
287
+ urlInput={urlInput}
288
+ />
334
289
 
335
- <form onSubmit={handleUrlSubmit} className="flex-1 relative">
336
- <Search className="w-3.5 h-3.5 absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
337
- <input
338
- type="text"
339
- value={urlInput}
340
- onChange={(e) => setUrlInput(e.target.value)}
341
- placeholder={t('docBrowserSearchPlaceholder')}
342
- 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"
343
- />
344
- </form>
345
- </div>
346
- )}
290
+ {customToolbar}
347
291
 
348
- <div className="flex-1 relative overflow-hidden">
349
- <iframe
350
- ref={iframeRef}
351
- key={`${activeTabId}:${navVersion}`}
352
- src={currentUrl}
353
- className="absolute inset-0 w-full h-full border-0"
354
- title={currentTab?.title || 'NextClaw Docs'}
355
- sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
356
- allow="clipboard-read; clipboard-write"
357
- />
358
- {(isResizing || isDragging) && (
359
- <div className="absolute inset-0 z-10" />
360
- )}
361
- </div>
292
+ <DocBrowserFrameContent
293
+ activeTabId={activeTabId}
294
+ currentTab={currentTab}
295
+ currentUrl={currentUrl}
296
+ customContent={customContent}
297
+ iframeRef={iframeRef}
298
+ iframeReloadVersion={iframeReloadVersion}
299
+ iframeSandbox={iframeSandbox}
300
+ isDragging={isDragging}
301
+ isResizing={isResizing}
302
+ navVersion={navVersion}
303
+ />
362
304
 
363
- {isDocsTab && isDocsUrl(currentUrl) && (
364
- <div className="flex items-center justify-between px-4 py-2 bg-gray-50 border-t border-gray-200 shrink-0">
365
- <a
366
- href={currentUrl}
367
- target="_blank"
368
- rel="noopener noreferrer"
369
- data-doc-external
370
- className="flex items-center gap-1.5 text-xs text-primary hover:text-primary-hover font-medium transition-colors"
371
- >
372
- {t('docBrowserOpenExternal')}
373
- <ExternalLink className="w-3 h-3" />
374
- </a>
375
- </div>
376
- )}
305
+ <DocBrowserExternalLink currentUrl={currentUrl} isDocsTab={isDocsTab} />
377
306
  </>
378
307
  );
379
308
 
@@ -351,6 +351,30 @@ export type ServerPathBrowseView = {
351
351
 
352
352
  export type ServerPathReadView = { requestedPath: string; resolvedPath: string; kind: "text" | "markdown" | "binary"; sizeBytes: number; truncated: boolean; text?: string; languageHint?: string | null };
353
353
 
354
+ export type PanelAppEntryView = {
355
+ id: string;
356
+ fileName: string;
357
+ title: string;
358
+ description?: string;
359
+ icon?: string;
360
+ contentPath: string;
361
+ updatedAt: string;
362
+ sizeBytes: number;
363
+ favorite: boolean;
364
+ lastOpenedAt?: string;
365
+ openCount: number;
366
+ };
367
+
368
+ export type PanelAppListView = {
369
+ workspacePath: string;
370
+ panelsPath: string;
371
+ entries: PanelAppEntryView[];
372
+ };
373
+
374
+ export type PanelAppPreferencesUpdateView = {
375
+ favorite?: boolean;
376
+ };
377
+
354
378
  export type {
355
379
  ChatSessionTypeCtaView,
356
380
  ChatSessionTypeOptionView,
@@ -13,6 +13,7 @@ import {
13
13
  type I18nLanguage
14
14
  } from './runtime/i18n-language-owner';
15
15
  import { DESKTOP_UPDATE_LABELS } from './desktop-update-labels.utils';
16
+ import { DOC_BROWSER_LABELS } from './runtime/doc-browser-labels.utils';
16
17
  import { MARKETPLACE_LABELS } from './marketplace';
17
18
  import { PATH_PICKER_LABELS } from './runtime/i18n.path-picker';
18
19
  import { PWA_LABELS } from './pwa';
@@ -476,18 +477,7 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
476
477
  enterTag: { zh: '输入后按回车...', en: 'Type and press Enter...' },
477
478
  headerName: { zh: 'Header 名称', en: 'Header Name' },
478
479
  headerValue: { zh: 'Header 值', en: 'Header Value' },
479
- // Doc Browser
480
- docBrowserTitle: { zh: '内嵌浏览器', en: 'Embedded Browser' },
481
- docBrowserSearchPlaceholder: { zh: '搜索,也可以输入文档地址直接打开', en: 'Search, or enter a doc URL to open' },
482
- docBrowserUrlPlaceholder: { zh: '输入文档路径,如 /guide/channels', en: 'Enter a doc path, e.g. /guide/channels' },
483
- docBrowserOpenExternal: { zh: '文档中心打开', en: 'Open in Docs' },
484
- docBrowserFloatMode: { zh: '悬浮窗口', en: 'Float Window' },
485
- docBrowserDockMode: { zh: '固定到侧栏', en: 'Dock to Sidebar' },
486
- docBrowserClose: { zh: '关闭', en: 'Close' },
487
- docBrowserHelp: { zh: '帮助文档', en: 'Help Docs' },
488
- docBrowserNewTab: { zh: '新建标签', en: 'New Tab' },
489
- docBrowserCloseTab: { zh: '关闭标签', en: 'Close Tab' },
490
- docBrowserTabUntitled: { zh: '未命名', en: 'Untitled' },
480
+ ...DOC_BROWSER_LABELS,
491
481
  ...PATH_PICKER_LABELS,
492
482
  ...PWA_LABELS,
493
483
  ...CHANNEL_AUTH_LABELS,
@@ -0,0 +1,29 @@
1
+ export const DOC_BROWSER_LABELS = {
2
+ docBrowserTitle: { zh: '内嵌浏览器', en: 'Embedded Browser' },
3
+ docBrowserSearchPlaceholder: { zh: '搜索,也可以输入文档地址直接打开', en: 'Search, or enter a doc URL to open' },
4
+ docBrowserUrlPlaceholder: { zh: '输入文档路径,如 /guide/channels', en: 'Enter a doc path, e.g. /guide/channels' },
5
+ docBrowserOpenExternal: { zh: '文档中心打开', en: 'Open in Docs' },
6
+ docBrowserFloatMode: { zh: '悬浮窗口', en: 'Float Window' },
7
+ docBrowserDockMode: { zh: '固定到侧栏', en: 'Dock to Sidebar' },
8
+ docBrowserClose: { zh: '关闭', en: 'Close' },
9
+ docBrowserHelp: { zh: '帮助文档', en: 'Help Docs' },
10
+ docBrowserNewTab: { zh: '新建标签', en: 'New Tab' },
11
+ docBrowserCloseTab: { zh: '关闭标签', en: 'Close Tab' },
12
+ docBrowserTabUntitled: { zh: '未命名', en: 'Untitled' },
13
+ panelAppsTitle: { zh: '面板应用', en: 'Panel Apps' },
14
+ panelAppsLoading: { zh: '加载面板应用中...', en: 'Loading panel apps...' },
15
+ panelAppsLoadFailed: { zh: '面板应用加载失败', en: 'Failed to load panel apps' },
16
+ panelAppsEmpty: { zh: '暂无面板应用', en: 'No panel apps yet' },
17
+ panelAppsRefresh: { zh: '刷新面板应用', en: 'Refresh panel apps' },
18
+ panelAppsRefreshCurrent: { zh: '刷新当前面板应用', en: 'Refresh current panel app' },
19
+ panelAppsSortLabel: { zh: '排序', en: 'Sort' },
20
+ panelAppsSortSmart: { zh: '智能排序', en: 'Smart' },
21
+ panelAppsFavorites: { zh: '收藏', en: 'Favorites' },
22
+ panelAppsSortRecentOpen: { zh: '最近打开', en: 'Recently Opened' },
23
+ panelAppsSortUpdated: { zh: '最近更新', en: 'Recently Updated' },
24
+ panelAppsSortName: { zh: '名称', en: 'Name' },
25
+ panelAppsFavorite: { zh: '收藏面板应用', en: 'Favorite panel app' },
26
+ panelAppsUnfavorite: { zh: '取消收藏面板应用', en: 'Unfavorite panel app' },
27
+ panelAppsLastOpened: { zh: '上次打开', en: 'Last opened' },
28
+ panelAppsUpdated: { zh: '更新于', en: 'Updated' },
29
+ } as const;