@nextclaw/ui 0.5.37 → 0.5.38
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 +6 -0
- package/dist/assets/{ChannelsList-DtvhbEV9.js → ChannelsList-3B_zyiKA.js} +1 -1
- package/dist/assets/{ChatPage-Bw_aXB4R.js → ChatPage-DusH09PT.js} +1 -1
- package/dist/assets/{CronConfig-BZLXcDbm.js → CronConfig-5GTz5wPt.js} +1 -1
- package/dist/assets/DocBrowser-BtqGmg0N.js +1 -0
- package/dist/assets/MarketplacePage-BEW4M9BT.js +49 -0
- package/dist/assets/{ModelConfig-Bi8Q4_NG.js → ModelConfig-CwxXYqME.js} +1 -1
- package/dist/assets/{ProvidersList-D2OB0siE.js → ProvidersList-D4oaYHpJ.js} +1 -1
- package/dist/assets/{RuntimeConfig-Bz9aUkwu.js → RuntimeConfig-BwTxGi_U.js} +1 -1
- package/dist/assets/{SecretsConfig-Bqi-biOL.js → SecretsConfig-x36MY4ym.js} +1 -1
- package/dist/assets/{SessionsConfig-DcWT2QvI.js → SessionsConfig-qEffYDZ0.js} +1 -1
- package/dist/assets/{card-DwZkVl7S.js → card-Bq6uwDJQ.js} +1 -1
- package/dist/assets/index-DMEuanmd.css +1 -0
- package/dist/assets/index-wB2uPrKu.js +2 -0
- package/dist/assets/{label-BBDuC6Nm.js → label-Cq1vSfWg.js} +1 -1
- package/dist/assets/{logos-DMFt4YDI.js → logos-BKBMs40Q.js} +1 -1
- package/dist/assets/{page-layout-hPFzCUTQ.js → page-layout-D8MW2vP-.js} +1 -1
- package/dist/assets/{switch-CwkcbkEs.js → switch-CycMxy31.js} +1 -1
- package/dist/assets/{tabs-custom-TUrWRyYy.js → tabs-custom-N4olWJSw.js} +1 -1
- package/dist/assets/{useConfig-DZVUrqQz.js → useConfig-tR_KAfMV.js} +1 -1
- package/dist/assets/{useConfirmDialog-D5X0Iqid.js → useConfirmDialog-DE0Yp8Ai.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 +1 -0
- 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 +21 -2
- package/dist/assets/DocBrowser-BY0TiFOc.js +0 -1
- package/dist/assets/MarketplacePage-BDlAw7fO.js +0 -1
- package/dist/assets/index-C1NAfZSm.js +0 -2
- package/dist/assets/index-DWgSvrx4.css +0 -1
|
@@ -4,203 +4,435 @@ import { getLanguage, type I18nLanguage } from '@/lib/i18n';
|
|
|
4
4
|
const DOCS_PRIMARY_DOMAIN = 'docs.nextclaw.io';
|
|
5
5
|
const DOCS_PAGES_DEV = 'nextclaw-docs.pages.dev';
|
|
6
6
|
const DOCS_HOSTS = new Set([
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
DOCS_PRIMARY_DOMAIN,
|
|
8
|
+
`www.${DOCS_PRIMARY_DOMAIN}`,
|
|
9
|
+
DOCS_PAGES_DEV,
|
|
10
|
+
`www.${DOCS_PAGES_DEV}`,
|
|
11
11
|
]);
|
|
12
12
|
|
|
13
13
|
export const DOCS_DEFAULT_BASE_URL = `https://${DOCS_PRIMARY_DOMAIN}`;
|
|
14
14
|
const DOCS_DEFAULT_GUIDE_PATH = '/guide/getting-started';
|
|
15
15
|
|
|
16
16
|
export type DocBrowserMode = 'floating' | 'docked';
|
|
17
|
+
export type DocBrowserTabKind = 'docs' | 'content';
|
|
18
|
+
|
|
19
|
+
export type DocBrowserTab = {
|
|
20
|
+
id: string;
|
|
21
|
+
kind: DocBrowserTabKind;
|
|
22
|
+
title: string;
|
|
23
|
+
currentUrl: string;
|
|
24
|
+
history: string[];
|
|
25
|
+
historyIndex: number;
|
|
26
|
+
/** Increments on parent-initiated navigation to trigger iframe remount */
|
|
27
|
+
navVersion: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type DocBrowserOpenOptions = {
|
|
31
|
+
newTab?: boolean;
|
|
32
|
+
title?: string;
|
|
33
|
+
kind?: DocBrowserTabKind;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type DocBrowserState = {
|
|
37
|
+
isOpen: boolean;
|
|
38
|
+
mode: DocBrowserMode;
|
|
39
|
+
tabs: DocBrowserTab[];
|
|
40
|
+
activeTabId: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
interface DocBrowserActions {
|
|
44
|
+
open: (url?: string, options?: DocBrowserOpenOptions) => void;
|
|
45
|
+
close: () => void;
|
|
46
|
+
toggleMode: () => void;
|
|
47
|
+
setMode: (mode: DocBrowserMode) => void;
|
|
48
|
+
navigate: (url: string) => void;
|
|
49
|
+
syncUrl: (url: string) => void;
|
|
50
|
+
goBack: () => void;
|
|
51
|
+
goForward: () => void;
|
|
52
|
+
openNewTab: (url?: string, options?: Omit<DocBrowserOpenOptions, 'newTab'>) => void;
|
|
53
|
+
closeTab: (tabId: string) => void;
|
|
54
|
+
setActiveTab: (tabId: string) => void;
|
|
55
|
+
canGoBack: boolean;
|
|
56
|
+
canGoForward: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type DocBrowserContextValue = DocBrowserState & DocBrowserActions & {
|
|
60
|
+
currentUrl: string;
|
|
61
|
+
currentTab?: DocBrowserTab;
|
|
62
|
+
navVersion: number;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const DocBrowserContext = createContext<DocBrowserContextValue | null>(null);
|
|
66
|
+
|
|
67
|
+
let tabCounter = 0;
|
|
68
|
+
|
|
69
|
+
function nextTabId(): string {
|
|
70
|
+
tabCounter += 1;
|
|
71
|
+
return `doc-tab-${Date.now()}-${tabCounter}`;
|
|
72
|
+
}
|
|
17
73
|
|
|
18
74
|
/** Normalize URL for comparison: strip .html and trailing slash */
|
|
19
75
|
function normalizeDocUrl(u: string): string {
|
|
20
|
-
|
|
76
|
+
try {
|
|
77
|
+
return new URL(u).pathname.replace(/\.html$/, '').replace(/\/$/, '');
|
|
78
|
+
} catch {
|
|
79
|
+
return u;
|
|
80
|
+
}
|
|
21
81
|
}
|
|
22
82
|
|
|
23
83
|
function toDocsLocale(language: I18nLanguage): 'en' | 'zh' {
|
|
24
|
-
|
|
84
|
+
return language === 'zh' ? 'zh' : 'en';
|
|
25
85
|
}
|
|
26
86
|
|
|
27
87
|
function ensureLocalizedDocsPath(pathname: string, locale: 'en' | 'zh'): string {
|
|
28
|
-
|
|
88
|
+
const normalized = pathname.startsWith('/') ? pathname : `/${pathname}`;
|
|
29
89
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
90
|
+
if (normalized === '/' || normalized === '') {
|
|
91
|
+
return `/${locale}/`;
|
|
92
|
+
}
|
|
33
93
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
94
|
+
if (/^\/(en|zh)(\/|$)/.test(normalized)) {
|
|
95
|
+
return normalized;
|
|
96
|
+
}
|
|
37
97
|
|
|
38
|
-
|
|
98
|
+
return `/${locale}${normalized}`;
|
|
39
99
|
}
|
|
40
100
|
|
|
41
101
|
function resolveLocalizedDocsUrl(url: string): string {
|
|
42
|
-
|
|
102
|
+
const locale = toDocsLocale(getLanguage());
|
|
43
103
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
parsed.pathname = ensureLocalizedDocsPath(parsed.pathname, locale);
|
|
51
|
-
return parsed.toString();
|
|
52
|
-
} catch {
|
|
53
|
-
return new URL(`/${locale}${DOCS_DEFAULT_GUIDE_PATH}`, DOCS_DEFAULT_BASE_URL).toString();
|
|
104
|
+
try {
|
|
105
|
+
const parsed = new URL(url, DOCS_DEFAULT_BASE_URL);
|
|
106
|
+
if (!DOCS_HOSTS.has(parsed.hostname)) {
|
|
107
|
+
return parsed.toString();
|
|
54
108
|
}
|
|
109
|
+
|
|
110
|
+
parsed.pathname = ensureLocalizedDocsPath(parsed.pathname, locale);
|
|
111
|
+
return parsed.toString();
|
|
112
|
+
} catch {
|
|
113
|
+
return new URL(`/${locale}${DOCS_DEFAULT_GUIDE_PATH}`, DOCS_DEFAULT_BASE_URL).toString();
|
|
114
|
+
}
|
|
55
115
|
}
|
|
56
116
|
|
|
57
117
|
function getDefaultDocsUrl(): string {
|
|
58
|
-
|
|
118
|
+
return resolveLocalizedDocsUrl(DOCS_DEFAULT_GUIDE_PATH);
|
|
59
119
|
}
|
|
60
120
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
121
|
+
function inferTabTitle(url: string, kind: DocBrowserTabKind, fallback = 'Docs'): string {
|
|
122
|
+
try {
|
|
123
|
+
const parsed = new URL(url, DOCS_DEFAULT_BASE_URL);
|
|
124
|
+
if (parsed.protocol === 'data:') {
|
|
125
|
+
return kind === 'docs' ? fallback : 'Detail';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const segments = parsed.pathname.split('/').filter(Boolean);
|
|
129
|
+
const leaf = segments[segments.length - 1] ?? fallback;
|
|
130
|
+
return decodeURIComponent(leaf).replace(/[-_]/g, ' ').slice(0, 40) || fallback;
|
|
131
|
+
} catch {
|
|
132
|
+
return fallback;
|
|
133
|
+
}
|
|
69
134
|
}
|
|
70
135
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
close: () => void;
|
|
74
|
-
toggleMode: () => void;
|
|
75
|
-
setMode: (mode: DocBrowserMode) => void;
|
|
76
|
-
/** Parent-initiated navigation — will cause iframe to reload to this URL */
|
|
77
|
-
navigate: (url: string) => void;
|
|
78
|
-
/** Iframe-initiated sync — records URL to history without reloading iframe */
|
|
79
|
-
syncUrl: (url: string) => void;
|
|
80
|
-
goBack: () => void;
|
|
81
|
-
goForward: () => void;
|
|
82
|
-
canGoBack: boolean;
|
|
83
|
-
canGoForward: boolean;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
type DocBrowserContextValue = DocBrowserState & DocBrowserActions;
|
|
136
|
+
function createTab(url: string, kind: DocBrowserTabKind, title?: string): DocBrowserTab {
|
|
137
|
+
const tabTitle = title?.trim() || inferTabTitle(url, kind, kind === 'docs' ? 'Docs' : 'Detail');
|
|
87
138
|
|
|
88
|
-
|
|
139
|
+
return {
|
|
140
|
+
id: nextTabId(),
|
|
141
|
+
kind,
|
|
142
|
+
title: tabTitle,
|
|
143
|
+
currentUrl: url,
|
|
144
|
+
history: [url],
|
|
145
|
+
historyIndex: 0,
|
|
146
|
+
navVersion: 0,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function updateActiveTab(state: DocBrowserState, updater: (tab: DocBrowserTab) => DocBrowserTab): DocBrowserState {
|
|
151
|
+
return {
|
|
152
|
+
...state,
|
|
153
|
+
tabs: state.tabs.map((tab) => (tab.id === state.activeTabId ? updater(tab) : tab)),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
89
156
|
|
|
90
157
|
export function useDocBrowser(): DocBrowserContextValue {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
158
|
+
const ctx = useContext(DocBrowserContext);
|
|
159
|
+
if (!ctx) throw new Error('useDocBrowser must be used within DocBrowserProvider');
|
|
160
|
+
return ctx;
|
|
94
161
|
}
|
|
95
162
|
|
|
96
163
|
/** Check if a URL belongs to the docs domain */
|
|
97
164
|
export function isDocsUrl(url: string): boolean {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
165
|
+
try {
|
|
166
|
+
const parsed = new URL(url, window.location.origin);
|
|
167
|
+
return DOCS_HOSTS.has(parsed.hostname);
|
|
168
|
+
} catch {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function inferTabKind(url: string): DocBrowserTabKind {
|
|
174
|
+
return isDocsUrl(url) ? 'docs' : 'content';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function normalizeUrlByKind(url: string, kind: DocBrowserTabKind): string {
|
|
178
|
+
if (kind === 'docs') {
|
|
179
|
+
return resolveLocalizedDocsUrl(url);
|
|
180
|
+
}
|
|
181
|
+
return url;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function resolveOpenTargetUrl(params: {
|
|
185
|
+
url?: string;
|
|
186
|
+
kind: DocBrowserTabKind;
|
|
187
|
+
activeTab?: DocBrowserTab;
|
|
188
|
+
}): string {
|
|
189
|
+
if (params.url && params.url.trim().length > 0) {
|
|
190
|
+
return normalizeUrlByKind(params.url, params.kind);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (params.kind === 'docs') {
|
|
194
|
+
return getDefaultDocsUrl();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return params.activeTab?.currentUrl ?? getDefaultDocsUrl();
|
|
104
198
|
}
|
|
105
199
|
|
|
106
200
|
export function DocBrowserProvider({ children }: { children: ReactNode }) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
201
|
+
const initialUrl = getDefaultDocsUrl();
|
|
202
|
+
const initialTab = createTab(initialUrl, 'docs', 'Docs');
|
|
203
|
+
|
|
204
|
+
const [state, setState] = useState<DocBrowserState>({
|
|
205
|
+
isOpen: false,
|
|
206
|
+
mode: 'docked',
|
|
207
|
+
tabs: [initialTab],
|
|
208
|
+
activeTabId: initialTab.id,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const currentTab = useMemo(() => {
|
|
212
|
+
return state.tabs.find((tab) => tab.id === state.activeTabId) ?? state.tabs[0];
|
|
213
|
+
}, [state.tabs, state.activeTabId]);
|
|
214
|
+
|
|
215
|
+
const open = useCallback((url?: string, options?: DocBrowserOpenOptions) => {
|
|
216
|
+
setState((prev) => {
|
|
217
|
+
const activeTab = prev.tabs.find((tab) => tab.id === prev.activeTabId) ?? prev.tabs[0];
|
|
218
|
+
const targetKind = options?.kind ?? (url ? inferTabKind(url) : activeTab?.kind ?? 'docs');
|
|
219
|
+
const targetUrl = resolveOpenTargetUrl({
|
|
220
|
+
url,
|
|
221
|
+
kind: targetKind,
|
|
222
|
+
activeTab
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const shouldOpenNewTab = Boolean(options?.newTab || !activeTab || activeTab.kind !== targetKind);
|
|
226
|
+
|
|
227
|
+
if (shouldOpenNewTab) {
|
|
228
|
+
const newTab = createTab(targetUrl, targetKind, options?.title);
|
|
229
|
+
return {
|
|
230
|
+
...prev,
|
|
231
|
+
isOpen: true,
|
|
232
|
+
tabs: [...prev.tabs, newTab],
|
|
233
|
+
activeTabId: newTab.id,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const next = updateActiveTab(prev, (tab) => {
|
|
238
|
+
if (normalizeDocUrl(targetUrl) === normalizeDocUrl(tab.currentUrl)) {
|
|
239
|
+
return options?.title ? { ...tab, title: options.title } : tab;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
...tab,
|
|
244
|
+
title: options?.title || tab.title,
|
|
245
|
+
kind: targetKind,
|
|
246
|
+
currentUrl: targetUrl,
|
|
247
|
+
history: [...tab.history.slice(0, tab.historyIndex + 1), targetUrl],
|
|
248
|
+
historyIndex: tab.historyIndex + 1,
|
|
249
|
+
navVersion: tab.navVersion + 1,
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
...next,
|
|
255
|
+
isOpen: true,
|
|
256
|
+
};
|
|
257
|
+
});
|
|
258
|
+
}, []);
|
|
259
|
+
|
|
260
|
+
const close = useCallback(() => {
|
|
261
|
+
setState((prev) => ({ ...prev, isOpen: false }));
|
|
262
|
+
}, []);
|
|
263
|
+
|
|
264
|
+
const toggleMode = useCallback(() => {
|
|
265
|
+
setState((prev) => ({ ...prev, mode: prev.mode === 'floating' ? 'docked' : 'floating' }));
|
|
266
|
+
}, []);
|
|
267
|
+
|
|
268
|
+
const setMode = useCallback((mode: DocBrowserMode) => {
|
|
269
|
+
setState((prev) => ({ ...prev, mode }));
|
|
270
|
+
}, []);
|
|
271
|
+
|
|
272
|
+
const navigate = useCallback((url: string) => {
|
|
273
|
+
setState((prev) => {
|
|
274
|
+
if (!prev.tabs.length) {
|
|
275
|
+
const fallbackTab = createTab(getDefaultDocsUrl(), 'docs', 'Docs');
|
|
276
|
+
return {
|
|
277
|
+
...prev,
|
|
278
|
+
tabs: [fallbackTab],
|
|
279
|
+
activeTabId: fallbackTab.id,
|
|
280
|
+
isOpen: true,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return updateActiveTab(prev, (tab) => {
|
|
285
|
+
if (tab.kind !== 'docs') {
|
|
286
|
+
return tab;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const targetUrl = normalizeUrlByKind(url, 'docs');
|
|
290
|
+
if (normalizeDocUrl(targetUrl) === normalizeDocUrl(tab.currentUrl)) {
|
|
291
|
+
return tab;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
...tab,
|
|
296
|
+
currentUrl: targetUrl,
|
|
297
|
+
history: [...tab.history.slice(0, tab.historyIndex + 1), targetUrl],
|
|
298
|
+
historyIndex: tab.historyIndex + 1,
|
|
299
|
+
navVersion: tab.navVersion + 1,
|
|
300
|
+
};
|
|
301
|
+
});
|
|
114
302
|
});
|
|
303
|
+
}, []);
|
|
304
|
+
|
|
305
|
+
const syncUrl = useCallback((url: string) => {
|
|
306
|
+
setState((prev) => {
|
|
307
|
+
if (!prev.tabs.length) {
|
|
308
|
+
const fallbackTab = createTab(getDefaultDocsUrl(), 'docs', 'Docs');
|
|
309
|
+
return {
|
|
310
|
+
...prev,
|
|
311
|
+
tabs: [fallbackTab],
|
|
312
|
+
activeTabId: fallbackTab.id,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return updateActiveTab(prev, (tab) => {
|
|
317
|
+
if (tab.kind !== 'docs') {
|
|
318
|
+
return tab;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (normalizeDocUrl(url) === normalizeDocUrl(tab.currentUrl)) {
|
|
322
|
+
return tab;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
...tab,
|
|
327
|
+
currentUrl: url,
|
|
328
|
+
history: [...tab.history.slice(0, tab.historyIndex + 1), url],
|
|
329
|
+
historyIndex: tab.historyIndex + 1,
|
|
330
|
+
};
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
}, []);
|
|
334
|
+
|
|
335
|
+
const goBack = useCallback(() => {
|
|
336
|
+
setState((prev) => updateActiveTab(prev, (tab) => {
|
|
337
|
+
if (tab.kind !== 'docs' || tab.historyIndex <= 0) return tab;
|
|
338
|
+
const newIndex = tab.historyIndex - 1;
|
|
339
|
+
return { ...tab, historyIndex: newIndex, currentUrl: tab.history[newIndex] };
|
|
340
|
+
}));
|
|
341
|
+
}, []);
|
|
342
|
+
|
|
343
|
+
const goForward = useCallback(() => {
|
|
344
|
+
setState((prev) => updateActiveTab(prev, (tab) => {
|
|
345
|
+
if (tab.kind !== 'docs' || tab.historyIndex >= tab.history.length - 1) return tab;
|
|
346
|
+
const newIndex = tab.historyIndex + 1;
|
|
347
|
+
return { ...tab, historyIndex: newIndex, currentUrl: tab.history[newIndex] };
|
|
348
|
+
}));
|
|
349
|
+
}, []);
|
|
350
|
+
|
|
351
|
+
const openNewTab = useCallback((url?: string, options?: Omit<DocBrowserOpenOptions, 'newTab'>) => {
|
|
352
|
+
open(url, { ...(options ?? {}), newTab: true });
|
|
353
|
+
}, [open]);
|
|
354
|
+
|
|
355
|
+
const closeTab = useCallback((tabId: string) => {
|
|
356
|
+
setState((prev) => {
|
|
357
|
+
if (prev.tabs.length <= 1) {
|
|
358
|
+
const fallbackTab = createTab(getDefaultDocsUrl(), 'docs', 'Docs');
|
|
359
|
+
return {
|
|
360
|
+
...prev,
|
|
361
|
+
isOpen: false,
|
|
362
|
+
tabs: [fallbackTab],
|
|
363
|
+
activeTabId: fallbackTab.id,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const index = prev.tabs.findIndex((tab) => tab.id === tabId);
|
|
368
|
+
if (index < 0) {
|
|
369
|
+
return prev;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const nextTabs = prev.tabs.filter((tab) => tab.id !== tabId);
|
|
373
|
+
const nextActiveId = prev.activeTabId === tabId
|
|
374
|
+
? nextTabs[Math.max(0, index - 1)]?.id ?? nextTabs[0].id
|
|
375
|
+
: prev.activeTabId;
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
...prev,
|
|
379
|
+
tabs: nextTabs,
|
|
380
|
+
activeTabId: nextActiveId,
|
|
381
|
+
};
|
|
382
|
+
});
|
|
383
|
+
}, []);
|
|
384
|
+
|
|
385
|
+
const setActiveTab = useCallback((tabId: string) => {
|
|
386
|
+
setState((prev) => {
|
|
387
|
+
if (!prev.tabs.some((tab) => tab.id === tabId)) {
|
|
388
|
+
return prev;
|
|
389
|
+
}
|
|
390
|
+
return { ...prev, activeTabId: tabId, isOpen: true };
|
|
391
|
+
});
|
|
392
|
+
}, []);
|
|
393
|
+
|
|
394
|
+
const canGoBack = Boolean(currentTab && currentTab.kind === 'docs' && currentTab.historyIndex > 0);
|
|
395
|
+
const canGoForward = Boolean(currentTab && currentTab.kind === 'docs' && currentTab.historyIndex < currentTab.history.length - 1);
|
|
396
|
+
|
|
397
|
+
const value = useMemo<DocBrowserContextValue>(() => ({
|
|
398
|
+
...state,
|
|
399
|
+
currentTab,
|
|
400
|
+
currentUrl: currentTab?.currentUrl ?? getDefaultDocsUrl(),
|
|
401
|
+
navVersion: currentTab?.navVersion ?? 0,
|
|
402
|
+
open,
|
|
403
|
+
close,
|
|
404
|
+
toggleMode,
|
|
405
|
+
setMode,
|
|
406
|
+
navigate,
|
|
407
|
+
syncUrl,
|
|
408
|
+
goBack,
|
|
409
|
+
goForward,
|
|
410
|
+
openNewTab,
|
|
411
|
+
closeTab,
|
|
412
|
+
setActiveTab,
|
|
413
|
+
canGoBack,
|
|
414
|
+
canGoForward,
|
|
415
|
+
}), [
|
|
416
|
+
state,
|
|
417
|
+
currentTab,
|
|
418
|
+
open,
|
|
419
|
+
close,
|
|
420
|
+
toggleMode,
|
|
421
|
+
setMode,
|
|
422
|
+
navigate,
|
|
423
|
+
syncUrl,
|
|
424
|
+
goBack,
|
|
425
|
+
goForward,
|
|
426
|
+
openNewTab,
|
|
427
|
+
closeTab,
|
|
428
|
+
setActiveTab,
|
|
429
|
+
canGoBack,
|
|
430
|
+
canGoForward,
|
|
431
|
+
]);
|
|
115
432
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
currentUrl: targetUrl,
|
|
122
|
-
history: [...prev.history.slice(0, prev.historyIndex + 1), targetUrl],
|
|
123
|
-
historyIndex: prev.historyIndex + 1,
|
|
124
|
-
navVersion: prev.navVersion + 1,
|
|
125
|
-
}));
|
|
126
|
-
}, [state.currentUrl]);
|
|
127
|
-
|
|
128
|
-
const close = useCallback(() => {
|
|
129
|
-
setState(prev => ({ ...prev, isOpen: false }));
|
|
130
|
-
}, []);
|
|
131
|
-
|
|
132
|
-
const toggleMode = useCallback(() => {
|
|
133
|
-
setState(prev => ({ ...prev, mode: prev.mode === 'floating' ? 'docked' : 'floating' }));
|
|
134
|
-
}, []);
|
|
135
|
-
|
|
136
|
-
const setMode = useCallback((mode: DocBrowserMode) => {
|
|
137
|
-
setState(prev => ({ ...prev, mode }));
|
|
138
|
-
}, []);
|
|
139
|
-
|
|
140
|
-
/** Parent-initiated: push to history AND bump navVersion so iframe reloads */
|
|
141
|
-
const navigate = useCallback((url: string) => {
|
|
142
|
-
const targetUrl = resolveLocalizedDocsUrl(url);
|
|
143
|
-
setState(prev => {
|
|
144
|
-
if (normalizeDocUrl(targetUrl) === normalizeDocUrl(prev.currentUrl)) return prev;
|
|
145
|
-
return {
|
|
146
|
-
...prev,
|
|
147
|
-
currentUrl: targetUrl,
|
|
148
|
-
history: [...prev.history.slice(0, prev.historyIndex + 1), targetUrl],
|
|
149
|
-
historyIndex: prev.historyIndex + 1,
|
|
150
|
-
navVersion: prev.navVersion + 1,
|
|
151
|
-
};
|
|
152
|
-
});
|
|
153
|
-
}, []);
|
|
154
|
-
|
|
155
|
-
/** Iframe-initiated: push to history but do NOT bump navVersion (no iframe reload) */
|
|
156
|
-
const syncUrl = useCallback((url: string) => {
|
|
157
|
-
setState(prev => {
|
|
158
|
-
if (normalizeDocUrl(url) === normalizeDocUrl(prev.currentUrl)) return prev;
|
|
159
|
-
return {
|
|
160
|
-
...prev,
|
|
161
|
-
currentUrl: url,
|
|
162
|
-
history: [...prev.history.slice(0, prev.historyIndex + 1), url],
|
|
163
|
-
historyIndex: prev.historyIndex + 1,
|
|
164
|
-
};
|
|
165
|
-
});
|
|
166
|
-
}, []);
|
|
167
|
-
|
|
168
|
-
const goBack = useCallback(() => {
|
|
169
|
-
setState(prev => {
|
|
170
|
-
if (prev.historyIndex <= 0) return prev;
|
|
171
|
-
const newIndex = prev.historyIndex - 1;
|
|
172
|
-
return { ...prev, historyIndex: newIndex, currentUrl: prev.history[newIndex] };
|
|
173
|
-
});
|
|
174
|
-
}, []);
|
|
175
|
-
|
|
176
|
-
const goForward = useCallback(() => {
|
|
177
|
-
setState(prev => {
|
|
178
|
-
if (prev.historyIndex >= prev.history.length - 1) return prev;
|
|
179
|
-
const newIndex = prev.historyIndex + 1;
|
|
180
|
-
return { ...prev, historyIndex: newIndex, currentUrl: prev.history[newIndex] };
|
|
181
|
-
});
|
|
182
|
-
}, []);
|
|
183
|
-
|
|
184
|
-
const canGoBack = state.historyIndex > 0;
|
|
185
|
-
const canGoForward = state.historyIndex < state.history.length - 1;
|
|
186
|
-
|
|
187
|
-
const value = useMemo<DocBrowserContextValue>(() => ({
|
|
188
|
-
...state,
|
|
189
|
-
open,
|
|
190
|
-
close,
|
|
191
|
-
toggleMode,
|
|
192
|
-
setMode,
|
|
193
|
-
navigate,
|
|
194
|
-
syncUrl,
|
|
195
|
-
goBack,
|
|
196
|
-
goForward,
|
|
197
|
-
canGoBack,
|
|
198
|
-
canGoForward,
|
|
199
|
-
}), [state, open, close, toggleMode, setMode, navigate, syncUrl, goBack, goForward, canGoBack, canGoForward]);
|
|
200
|
-
|
|
201
|
-
return (
|
|
202
|
-
<DocBrowserContext.Provider value={value}>
|
|
203
|
-
{children}
|
|
204
|
-
</DocBrowserContext.Provider>
|
|
205
|
-
);
|
|
433
|
+
return (
|
|
434
|
+
<DocBrowserContext.Provider value={value}>
|
|
435
|
+
{children}
|
|
436
|
+
</DocBrowserContext.Provider>
|
|
437
|
+
);
|
|
206
438
|
}
|
|
@@ -167,7 +167,7 @@ export function Sidebar() {
|
|
|
167
167
|
</Select>
|
|
168
168
|
</div>
|
|
169
169
|
<button
|
|
170
|
-
onClick={() => docBrowser.open()}
|
|
170
|
+
onClick={() => docBrowser.open(undefined, { kind: 'docs', newTab: true, title: 'Docs' })}
|
|
171
171
|
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-[14px] font-medium transition-all duration-base text-gray-600 hover:bg-[#e4e7ef] hover:text-gray-800"
|
|
172
172
|
>
|
|
173
173
|
<BookOpen className="h-[17px] w-[17px] text-gray-400" />
|