@nextclaw/ui 0.12.35-beta.3 → 0.12.35-beta.4

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 (116) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/dist/assets/api-BTZui7nj.js +15 -0
  3. package/dist/assets/{book-open--T9dJHnp.js → book-open-qc0UVn6m.js} +1 -1
  4. package/dist/assets/channels-list-page-DBtU_M1Y.js +8 -0
  5. package/dist/assets/{chat-page-DOKLoiqI.js → chat-page-BcoJYo91.js} +2 -2
  6. package/dist/assets/config-split-page-DW8tP9Bc.js +1 -0
  7. package/dist/assets/createLucideIcon-CA0sgoMJ.js +1 -0
  8. package/dist/assets/desktop-update-config-DARSwQG8.js +1 -0
  9. package/dist/assets/dist-D5zsy7pX.js +1 -0
  10. package/dist/assets/dist-DNxIa3pe.js +41 -0
  11. package/dist/assets/doc-browser-B9skxzVo.js +1 -0
  12. package/dist/assets/doc-browser-CwyITRXx.js +1 -0
  13. package/dist/assets/doc-browser-context-Do2qSrB8.js +1 -0
  14. package/dist/assets/{external-link-CZ9bQY0L.js → external-link-C9fbyi6k.js} +1 -1
  15. package/dist/assets/index-BYRP_BMj.css +1 -0
  16. package/dist/assets/index-DckPGTdM.js +103 -0
  17. package/dist/assets/mcp-marketplace-page-BiExW5wq.js +40 -0
  18. package/dist/assets/mcp-marketplace-page-Cj6MDSkE.js +1 -0
  19. package/dist/assets/model-config-DbdY7fF1.js +1 -0
  20. package/dist/assets/notice-card-CyiVcu_Z.js +5 -0
  21. package/dist/assets/play-D-s0C4MN.js +1 -0
  22. package/dist/assets/plus-DOwjM_kF.js +1 -0
  23. package/dist/assets/provider-scoped-model-input-Bk8jlpRm.js +1 -0
  24. package/dist/assets/providers-list-CJjNStDV.js +1 -0
  25. package/dist/assets/react-BaRotFd6.js +1 -0
  26. package/dist/assets/remote-gl8QckNc.js +1 -0
  27. package/dist/assets/runtime-config-page-BMFc1yzf.js +1 -0
  28. package/dist/assets/{save-B6wbBLkU.js → save-CG4Hb1u8.js} +1 -1
  29. package/dist/assets/{search-config-BFmG4_F7.js → search-config-DUOR26if.js} +1 -1
  30. package/dist/assets/{search-Czito3Dk.js → search-lWpOIHEn.js} +1 -1
  31. package/dist/assets/secrets-config-ZbLzjIRx.js +3 -0
  32. package/dist/assets/setting-row-CNfTKDjc.js +1 -0
  33. package/dist/assets/tabs-custom-BbZoysAl.js +1 -0
  34. package/dist/assets/tag-chip-Dl8JbhC7.js +1 -0
  35. package/dist/assets/x-D6GwT_8k.js +1 -0
  36. package/dist/index.html +15 -17
  37. package/package.json +8 -9
  38. package/src/app/components/app-presenter-provider.tsx +21 -0
  39. package/src/app/components/layout/sidebar.layout.test.tsx +2 -2
  40. package/src/app/components/layout/sidebar.tsx +3 -3
  41. package/src/app/index.tsx +5 -3
  42. package/src/app/presenters/app.presenter.ts +24 -0
  43. package/src/features/account/components/account-panel.tsx +11 -11
  44. package/src/features/account/index.ts +1 -4
  45. package/src/features/account/managers/account.manager.ts +52 -27
  46. package/src/features/account/stores/account.store.ts +0 -15
  47. package/src/features/apps/components/apps-panel.tsx +43 -0
  48. package/src/features/apps/index.ts +1 -0
  49. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +8 -8
  50. package/src/features/chat/components/layout/chat-sidebar.tsx +3 -2
  51. package/src/features/chat/managers/chat-session-list.manager.test.ts +58 -0
  52. package/src/features/chat/stores/chat-session-list.store.ts +58 -7
  53. package/src/features/panel-apps/components/panel-app-toolbar.tsx +4 -4
  54. package/src/features/panel-apps/components/panel-apps-list.tsx +6 -2
  55. package/src/features/panel-apps/index.ts +3 -1
  56. package/src/features/panel-apps/managers/panel-app-bridge.manager.test.ts +111 -0
  57. package/src/features/panel-apps/managers/panel-app-bridge.manager.ts +247 -0
  58. package/src/features/panel-apps/utils/panel-app-doc-browser.utils.test.ts +23 -0
  59. package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +42 -16
  60. package/src/features/remote/components/remote-access-page.test.tsx +2 -2
  61. package/src/features/remote/components/remote-access-page.tsx +9 -9
  62. package/src/features/remote/managers/remote-access.manager.ts +24 -41
  63. package/src/features/service-apps/components/service-action-authorization-dialog.tsx +78 -0
  64. package/src/features/service-apps/components/service-apps-panel.tsx +290 -0
  65. package/src/features/service-apps/hooks/use-service-apps.ts +66 -0
  66. package/src/features/service-apps/index.ts +3 -0
  67. package/src/features/service-apps/managers/service-action-authorization.manager.ts +10 -0
  68. package/src/features/service-apps/stores/service-action-authorization.store.ts +53 -0
  69. package/src/shared/components/doc-browser/doc-browser-context.test.tsx +150 -1
  70. package/src/shared/components/doc-browser/doc-browser-context.tsx +34 -443
  71. package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +7 -0
  72. package/src/shared/components/doc-browser/doc-browser.tsx +16 -1
  73. package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +272 -0
  74. package/src/shared/components/doc-browser/stores/doc-browser.store.ts +140 -0
  75. package/src/shared/components/doc-browser/types/doc-browser.types.ts +47 -0
  76. package/src/shared/components/doc-browser/utils/doc-browser-state.utils.ts +47 -0
  77. package/src/shared/components/doc-browser/utils/doc-browser-url.utils.ts +95 -0
  78. package/src/shared/lib/api/managers/client.manager.ts +2 -1
  79. package/src/shared/lib/i18n/runtime/doc-browser-labels.utils.ts +31 -0
  80. package/src/shared/lib/transport/local-transport.service.ts +18 -2
  81. package/src/shared/lib/transport/remote-transport.service.ts +19 -3
  82. package/src/shared/lib/transport/request-raw-api-response.utils.ts +1 -1
  83. package/dist/assets/api-D3uoaOR8.js +0 -15
  84. package/dist/assets/channels-list-page-DHmz9iHg.js +0 -8
  85. package/dist/assets/config-split-page-Dutq3R5o.js +0 -1
  86. package/dist/assets/createLucideIcon-Cwr2o6Zq.js +0 -1
  87. package/dist/assets/desktop-update-config-ClbDsIOs.js +0 -1
  88. package/dist/assets/dist-e5WATjIW.js +0 -1
  89. package/dist/assets/doc-browser-ChY6hMn8.js +0 -1
  90. package/dist/assets/doc-browser-cR1vWRs1.js +0 -1
  91. package/dist/assets/doc-browser-context-CLczyX2Z.js +0 -1
  92. package/dist/assets/es2015-BJw-hrex.js +0 -41
  93. package/dist/assets/i18n-tWKGEGxB.js +0 -1
  94. package/dist/assets/index-DPz9DRZN.js +0 -103
  95. package/dist/assets/index-HvJDzfSU.css +0 -1
  96. package/dist/assets/key-round-B8tTb3ZJ.js +0 -1
  97. package/dist/assets/mcp-marketplace-page-BYPxLdUR.js +0 -40
  98. package/dist/assets/mcp-marketplace-page-DXCvk7wJ.js +0 -1
  99. package/dist/assets/model-config-DcnC_fSr.js +0 -1
  100. package/dist/assets/notice-card-DUDbOvqx.js +0 -5
  101. package/dist/assets/play-CV_kwi9X.js +0 -1
  102. package/dist/assets/plus-BqfNq7Tu.js +0 -1
  103. package/dist/assets/provider-scoped-model-input-eO4XY0iY.js +0 -1
  104. package/dist/assets/providers-list-BgvjRVmp.js +0 -1
  105. package/dist/assets/remote-BCuZ1JJe.js +0 -1
  106. package/dist/assets/rotate-cw-BKEhWuWp.js +0 -1
  107. package/dist/assets/runtime-config-page-B2XPXHrk.js +0 -1
  108. package/dist/assets/secrets-config-4uY5sk-V.js +0 -3
  109. package/dist/assets/select-CmmDgG8o.js +0 -1
  110. package/dist/assets/setting-row-BUO6FeN1.js +0 -1
  111. package/dist/assets/tabs-custom-D3IZL9WX.js +0 -1
  112. package/dist/assets/tag-chip-Dm_pOtE3.js +0 -1
  113. package/dist/assets/x-DBP3WiC9.js +0 -1
  114. package/src/app/components/app-manager-provider.tsx +0 -20
  115. package/src/app/managers/app.manager.ts +0 -12
  116. /package/dist/assets/{config-hints-DGfwbumO.js → config-hints-CTqBnCDp.js} +0 -0
@@ -1,196 +1,25 @@
1
- import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from 'react';
2
- import { getLanguage, type I18nLanguage } from '@/shared/lib/i18n';
3
-
4
- const DOCS_PRIMARY_DOMAIN = 'docs.nextclaw.io';
5
- const DOCS_PAGES_DEV = 'nextclaw-docs.pages.dev';
6
- const DOCS_HOSTS = new Set([
7
- DOCS_PRIMARY_DOMAIN,
8
- `www.${DOCS_PRIMARY_DOMAIN}`,
9
- DOCS_PAGES_DEV,
10
- `www.${DOCS_PAGES_DEV}`,
11
- ]);
12
-
13
- export const DOCS_DEFAULT_BASE_URL = `https://${DOCS_PRIMARY_DOMAIN}`;
14
- const DOCS_DEFAULT_GUIDE_PATH = '/guide/getting-started';
15
-
16
- export type DocBrowserMode = 'floating' | 'docked';
17
- export type DocBrowserTabKind = 'docs' | 'content' | (string & {});
18
-
19
- export type DocBrowserTab = {
20
- id: string;
21
- kind: DocBrowserTabKind;
22
- title: string;
23
- currentUrl: string;
24
- dedupeKey?: string;
25
- history: string[];
26
- historyIndex: number;
27
- /** Increments on parent-initiated navigation to trigger iframe remount */
28
- navVersion: number;
29
- };
30
-
31
- export type DocBrowserOpenOptions = {
32
- activate?: boolean;
33
- dedupeKey?: string;
34
- newTab?: boolean;
35
- title?: string;
36
- kind?: DocBrowserTabKind;
37
- };
38
-
39
- type DocBrowserState = {
40
- isOpen: boolean;
41
- mode: DocBrowserMode;
42
- tabs: DocBrowserTab[];
43
- activeTabId: string;
44
- };
45
-
46
- interface DocBrowserActions {
47
- open: (url?: string, options?: DocBrowserOpenOptions) => void;
48
- close: () => void;
49
- toggleMode: () => void;
50
- navigate: (url: string) => void;
51
- syncUrl: (url: string) => void;
52
- goBack: () => void;
53
- goForward: () => void;
54
- closeTab: (tabId: string) => void;
55
- setActiveTab: (tabId: string) => void;
56
- }
57
-
58
- export type DocBrowserContextValue = DocBrowserState & DocBrowserActions & {
59
- currentTab?: DocBrowserTab;
60
- };
1
+ import { createContext, useContext, useMemo, type ReactNode } from 'react';
2
+ import { DocBrowserManager } from '@/shared/components/doc-browser/managers/doc-browser.manager';
3
+ import { useDocBrowserStore } from '@/shared/components/doc-browser/stores/doc-browser.store';
4
+ import type { DocBrowserContextValue } from '@/shared/components/doc-browser/types/doc-browser.types';
5
+
6
+ export type {
7
+ DocBrowserActions,
8
+ DocBrowserContextValue,
9
+ DocBrowserMode,
10
+ DocBrowserOpenOptions,
11
+ DocBrowserState,
12
+ DocBrowserStateUpdate,
13
+ DocBrowserTab,
14
+ DocBrowserTabKind,
15
+ } from '@/shared/components/doc-browser/types/doc-browser.types';
16
+ export {
17
+ DOCS_DEFAULT_BASE_URL,
18
+ isDocsUrl,
19
+ } from '@/shared/components/doc-browser/utils/doc-browser-url.utils';
61
20
 
62
21
  const DocBrowserContext = createContext<DocBrowserContextValue | null>(null);
63
-
64
- let tabCounter = 0;
65
-
66
- function nextTabId(): string {
67
- tabCounter += 1;
68
- return `doc-tab-${Date.now()}-${tabCounter}`;
69
- }
70
-
71
- /** Normalize URL for comparison: strip .html and trailing slash */
72
- function normalizeDocUrl(u: string): string {
73
- try {
74
- return new URL(u).pathname.replace(/\.html$/, '').replace(/\/$/, '');
75
- } catch {
76
- return u;
77
- }
78
- }
79
-
80
- function toDocsLocale(language: I18nLanguage): 'en' | 'zh' {
81
- return language === 'zh' ? 'zh' : 'en';
82
- }
83
-
84
- function ensureLocalizedDocsPath(pathname: string, locale: 'en' | 'zh'): string {
85
- const normalized = pathname.startsWith('/') ? pathname : `/${pathname}`;
86
-
87
- if (normalized === '/' || normalized === '') {
88
- return `/${locale}/`;
89
- }
90
-
91
- if (/^\/(en|zh)(\/|$)/.test(normalized)) {
92
- return normalized;
93
- }
94
-
95
- return `/${locale}${normalized}`;
96
- }
97
-
98
- function resolveLocalizedDocsUrl(url: string): string {
99
- const locale = toDocsLocale(getLanguage());
100
-
101
- try {
102
- const parsed = new URL(url, DOCS_DEFAULT_BASE_URL);
103
- if (!DOCS_HOSTS.has(parsed.hostname)) {
104
- return parsed.toString();
105
- }
106
-
107
- parsed.pathname = ensureLocalizedDocsPath(parsed.pathname, locale);
108
- return parsed.toString();
109
- } catch {
110
- return new URL(`/${locale}${DOCS_DEFAULT_GUIDE_PATH}`, DOCS_DEFAULT_BASE_URL).toString();
111
- }
112
- }
113
-
114
- function getDefaultDocsUrl(): string {
115
- return resolveLocalizedDocsUrl(DOCS_DEFAULT_GUIDE_PATH);
116
- }
117
-
118
- function inferTabTitle(url: string, kind: DocBrowserTabKind, fallback = 'Docs'): string {
119
- try {
120
- const parsed = new URL(url, DOCS_DEFAULT_BASE_URL);
121
- if (parsed.protocol === 'data:') {
122
- return kind === 'docs' ? fallback : 'Detail';
123
- }
124
-
125
- const segments = parsed.pathname.split('/').filter(Boolean);
126
- const leaf = segments[segments.length - 1] ?? fallback;
127
- return decodeURIComponent(leaf).replace(/[-_]/g, ' ').slice(0, 40) || fallback;
128
- } catch {
129
- return fallback;
130
- }
131
- }
132
-
133
- function createTab(
134
- url: string,
135
- kind: DocBrowserTabKind,
136
- title?: string,
137
- dedupeKey?: string,
138
- ): DocBrowserTab {
139
- const tabTitle = title?.trim() || inferTabTitle(url, kind, kind === 'docs' ? 'Docs' : 'Detail');
140
-
141
- return {
142
- id: nextTabId(),
143
- kind,
144
- title: tabTitle,
145
- currentUrl: url,
146
- dedupeKey,
147
- history: [url],
148
- historyIndex: 0,
149
- navVersion: 0,
150
- };
151
- }
152
-
153
- function updateActiveTab(state: DocBrowserState, updater: (tab: DocBrowserTab) => DocBrowserTab): DocBrowserState {
154
- return updateTab(state, state.activeTabId, updater);
155
- }
156
-
157
- function updateTab(
158
- state: DocBrowserState,
159
- tabId: string,
160
- updater: (tab: DocBrowserTab) => DocBrowserTab,
161
- ): DocBrowserState {
162
- return {
163
- ...state,
164
- tabs: state.tabs.map((tab) => (tab.id === tabId ? updater(tab) : tab)),
165
- };
166
- }
167
-
168
- function updateTabForOpen(
169
- tab: DocBrowserTab,
170
- url: string,
171
- kind: DocBrowserTabKind,
172
- options?: DocBrowserOpenOptions,
173
- dedupeKey?: string,
174
- ): DocBrowserTab {
175
- const baseTab = {
176
- ...tab,
177
- title: options?.title || tab.title,
178
- kind,
179
- dedupeKey,
180
- };
181
-
182
- if (normalizeDocUrl(url) === normalizeDocUrl(tab.currentUrl)) {
183
- return baseTab;
184
- }
185
-
186
- return {
187
- ...baseTab,
188
- currentUrl: url,
189
- history: [...tab.history.slice(0, tab.historyIndex + 1), url],
190
- historyIndex: tab.historyIndex + 1,
191
- navVersion: tab.navVersion + 1,
192
- };
193
- }
22
+ const docBrowserManager = new DocBrowserManager();
194
23
 
195
24
  export function useDocBrowser(): DocBrowserContextValue {
196
25
  const ctx = useContext(DocBrowserContext);
@@ -198,263 +27,25 @@ export function useDocBrowser(): DocBrowserContextValue {
198
27
  return ctx;
199
28
  }
200
29
 
201
- /** Check if a URL belongs to the docs domain */
202
- export function isDocsUrl(url: string): boolean {
203
- try {
204
- const parsed = new URL(url, window.location.origin);
205
- return DOCS_HOSTS.has(parsed.hostname);
206
- } catch {
207
- return false;
208
- }
209
- }
210
-
211
- function inferTabKind(url: string): DocBrowserTabKind {
212
- return isDocsUrl(url) ? 'docs' : 'content';
213
- }
214
-
215
- function normalizeUrlByKind(url: string, kind: DocBrowserTabKind): string {
216
- if (kind === 'docs') {
217
- return resolveLocalizedDocsUrl(url);
218
- }
219
- return url;
220
- }
221
-
222
- function resolveOpenTargetUrl(params: {
223
- url?: string;
224
- kind: DocBrowserTabKind;
225
- activeTab?: DocBrowserTab;
226
- }): string {
227
- const { activeTab, kind, url } = params;
228
- if (url && url.trim().length > 0) {
229
- return normalizeUrlByKind(url, kind);
230
- }
231
-
232
- if (kind === 'docs') {
233
- return getDefaultDocsUrl();
234
- }
235
- return activeTab?.currentUrl ?? getDefaultDocsUrl();
236
- }
237
-
238
- function openDocBrowserState(
239
- prev: DocBrowserState,
240
- url?: string,
241
- options?: DocBrowserOpenOptions,
242
- ): DocBrowserState {
243
- const {
244
- activate,
245
- dedupeKey: rawDedupeKey,
246
- kind,
247
- newTab: shouldForceNewTab,
248
- title,
249
- } = options ?? {};
250
- const activeTab = prev.tabs.find((tab) => tab.id === prev.activeTabId) ?? prev.tabs[0];
251
- const targetKind = kind ?? (url ? inferTabKind(url) : activeTab?.kind ?? 'docs');
252
- const targetUrl = resolveOpenTargetUrl({ url, kind: targetKind, activeTab });
253
- const dedupeKey = rawDedupeKey?.trim() || undefined;
254
- const matchedTab = dedupeKey
255
- ? prev.tabs.find((tab) => tab.dedupeKey === dedupeKey)
256
- : undefined;
257
-
258
- if (matchedTab) {
259
- const next = updateTab(
260
- prev,
261
- matchedTab.id,
262
- (tab) => updateTabForOpen(tab, targetUrl, targetKind, options, dedupeKey),
263
- );
264
- return {
265
- ...next,
266
- activeTabId: activate === false ? prev.activeTabId : matchedTab.id,
267
- isOpen: true,
268
- };
269
- }
270
-
271
- if (shouldForceNewTab || dedupeKey || !activeTab || activeTab.kind !== targetKind) {
272
- const newTab = createTab(targetUrl, targetKind, title, dedupeKey);
273
- return {
274
- ...prev,
275
- isOpen: true,
276
- tabs: [...prev.tabs, newTab],
277
- activeTabId: newTab.id,
278
- };
279
- }
280
-
281
- const next = updateActiveTab(
282
- prev,
283
- (tab) => updateTabForOpen(tab, targetUrl, targetKind, options, dedupeKey),
284
- );
285
- return { ...next, isOpen: true };
286
- }
287
-
288
30
  export function DocBrowserProvider({ children }: { children: ReactNode }) {
289
- const initialUrl = getDefaultDocsUrl();
290
- const initialTab = createTab(initialUrl, 'docs', 'Docs');
291
-
292
- const [state, setState] = useState<DocBrowserState>({
293
- isOpen: false,
294
- mode: 'docked',
295
- tabs: [initialTab],
296
- activeTabId: initialTab.id,
297
- });
298
-
31
+ const snapshot = useDocBrowserStore((state) => state.snapshot);
299
32
  const currentTab = useMemo(() => {
300
- return state.tabs.find((tab) => tab.id === state.activeTabId) ?? state.tabs[0];
301
- }, [state.tabs, state.activeTabId]);
302
-
303
- const open = useCallback((url?: string, options?: DocBrowserOpenOptions) => {
304
- setState((prev) => openDocBrowserState(prev, url, options));
305
- }, []);
306
-
307
- const close = useCallback(() => {
308
- setState((prev) => ({ ...prev, isOpen: false }));
309
- }, []);
310
-
311
- const toggleMode = useCallback(() => {
312
- setState((prev) => ({ ...prev, mode: prev.mode === 'floating' ? 'docked' : 'floating' }));
313
- }, []);
314
-
315
- const navigate = useCallback((url: string) => {
316
- setState((prev) => {
317
- if (!prev.tabs.length) {
318
- const fallbackTab = createTab(getDefaultDocsUrl(), 'docs', 'Docs');
319
- return {
320
- ...prev,
321
- tabs: [fallbackTab],
322
- activeTabId: fallbackTab.id,
323
- isOpen: true,
324
- };
325
- }
326
-
327
- return updateActiveTab(prev, (tab) => {
328
- if (tab.kind !== 'docs') {
329
- return tab;
330
- }
331
-
332
- const targetUrl = normalizeUrlByKind(url, 'docs');
333
- if (normalizeDocUrl(targetUrl) === normalizeDocUrl(tab.currentUrl)) {
334
- return tab;
335
- }
336
-
337
- return {
338
- ...tab,
339
- currentUrl: targetUrl,
340
- history: [...tab.history.slice(0, tab.historyIndex + 1), targetUrl],
341
- historyIndex: tab.historyIndex + 1,
342
- navVersion: tab.navVersion + 1,
343
- };
344
- });
345
- });
346
- }, []);
347
-
348
- const syncUrl = useCallback((url: string) => {
349
- setState((prev) => {
350
- if (!prev.tabs.length) {
351
- const fallbackTab = createTab(getDefaultDocsUrl(), 'docs', 'Docs');
352
- return {
353
- ...prev,
354
- tabs: [fallbackTab],
355
- activeTabId: fallbackTab.id,
356
- };
357
- }
358
-
359
- return updateActiveTab(prev, (tab) => {
360
- if (tab.kind !== 'docs') {
361
- return tab;
362
- }
363
-
364
- if (normalizeDocUrl(url) === normalizeDocUrl(tab.currentUrl)) {
365
- return tab;
366
- }
367
-
368
- return {
369
- ...tab,
370
- currentUrl: url,
371
- history: [...tab.history.slice(0, tab.historyIndex + 1), url],
372
- historyIndex: tab.historyIndex + 1,
373
- };
374
- });
375
- });
376
- }, []);
377
-
378
- const goBack = useCallback(() => {
379
- setState((prev) => updateActiveTab(prev, (tab) => {
380
- if (tab.kind !== 'docs' || tab.historyIndex <= 0) return tab;
381
- const newIndex = tab.historyIndex - 1;
382
- return { ...tab, historyIndex: newIndex, currentUrl: tab.history[newIndex] };
383
- }));
384
- }, []);
385
-
386
- const goForward = useCallback(() => {
387
- setState((prev) => updateActiveTab(prev, (tab) => {
388
- if (tab.kind !== 'docs' || tab.historyIndex >= tab.history.length - 1) return tab;
389
- const newIndex = tab.historyIndex + 1;
390
- return { ...tab, historyIndex: newIndex, currentUrl: tab.history[newIndex] };
391
- }));
392
- }, []);
393
-
394
- const closeTab = useCallback((tabId: string) => {
395
- setState((prev) => {
396
- if (prev.tabs.length <= 1) {
397
- const fallbackTab = createTab(getDefaultDocsUrl(), 'docs', 'Docs');
398
- return {
399
- ...prev,
400
- isOpen: false,
401
- tabs: [fallbackTab],
402
- activeTabId: fallbackTab.id,
403
- };
404
- }
405
-
406
- const index = prev.tabs.findIndex((tab) => tab.id === tabId);
407
- if (index < 0) {
408
- return prev;
409
- }
410
-
411
- const nextTabs = prev.tabs.filter((tab) => tab.id !== tabId);
412
- const nextActiveId = prev.activeTabId === tabId
413
- ? nextTabs[Math.max(0, index - 1)]?.id ?? nextTabs[0].id
414
- : prev.activeTabId;
415
-
416
- return {
417
- ...prev,
418
- tabs: nextTabs,
419
- activeTabId: nextActiveId,
420
- };
421
- });
422
- }, []);
423
-
424
- const setActiveTab = useCallback((tabId: string) => {
425
- setState((prev) => {
426
- if (!prev.tabs.some((tab) => tab.id === tabId)) {
427
- return prev;
428
- }
429
- return { ...prev, activeTabId: tabId, isOpen: true };
430
- });
431
- }, []);
33
+ return snapshot.tabs.find((tab) => tab.id === snapshot.activeTabId) ?? snapshot.tabs[0];
34
+ }, [snapshot.activeTabId, snapshot.tabs]);
432
35
 
433
36
  const value = useMemo<DocBrowserContextValue>(() => ({
434
- ...state,
435
- currentTab,
436
- open,
437
- close,
438
- toggleMode,
439
- navigate,
440
- syncUrl,
441
- goBack,
442
- goForward,
443
- closeTab,
444
- setActiveTab,
445
- }), [
446
- state,
37
+ ...snapshot,
447
38
  currentTab,
448
- open,
449
- close,
450
- toggleMode,
451
- navigate,
452
- syncUrl,
453
- goBack,
454
- goForward,
455
- closeTab,
456
- setActiveTab,
457
- ]);
39
+ open: docBrowserManager.open,
40
+ close: docBrowserManager.close,
41
+ toggleMode: docBrowserManager.toggleMode,
42
+ navigate: docBrowserManager.navigate,
43
+ syncUrl: docBrowserManager.syncUrl,
44
+ goBack: docBrowserManager.goBack,
45
+ goForward: docBrowserManager.goForward,
46
+ closeTab: docBrowserManager.closeTab,
47
+ setActiveTab: docBrowserManager.setActiveTab,
48
+ }), [currentTab, snapshot]);
458
49
 
459
50
  return (
460
51
  <DocBrowserContext.Provider value={value}>
@@ -8,9 +8,16 @@ export type DocBrowserCustomTabRenderParams = {
8
8
  tab: DocBrowserTab;
9
9
  };
10
10
 
11
+ export type DocBrowserIframeMessageParams = {
12
+ event: MessageEvent;
13
+ iframe: HTMLIFrameElement | null;
14
+ tab: DocBrowserTab;
15
+ };
16
+
11
17
  export type DocBrowserCustomTabRenderer = {
12
18
  getIframeSandbox?: (tab: DocBrowserTab) => string;
13
19
  getTitle?: (tab: DocBrowserTab) => string;
20
+ onIframeMessage?: (params: DocBrowserIframeMessageParams) => void;
14
21
  renderContent?: (params: DocBrowserCustomTabRenderParams) => ReactNode;
15
22
  renderIcon?: (tab: DocBrowserTab) => ReactNode;
16
23
  renderToolbar?: (params: DocBrowserCustomTabRenderParams) => ReactNode;
@@ -73,6 +73,7 @@ export function DocBrowser({
73
73
  const navVersion = currentTab?.navVersion ?? 0;
74
74
  const prevNavVersionRef = useRef(navVersion);
75
75
  const isDocsTab = currentTab?.kind === 'docs';
76
+ const customRenderer = currentTab ? customTabRenderers[currentTab.kind] : undefined;
76
77
 
77
78
  useEffect(() => {
78
79
  if (!isDocsTab) {
@@ -121,6 +122,21 @@ export function DocBrowser({
121
122
  return () => window.removeEventListener('message', handler);
122
123
  }, [syncUrl, isDocsTab]);
123
124
 
125
+ useEffect(() => {
126
+ if (!currentTab || !customRenderer?.onIframeMessage) {
127
+ return;
128
+ }
129
+ const handler = (event: MessageEvent) => {
130
+ customRenderer.onIframeMessage?.({
131
+ event,
132
+ iframe: iframeRef.current,
133
+ tab: currentTab,
134
+ });
135
+ };
136
+ window.addEventListener('message', handler);
137
+ return () => window.removeEventListener('message', handler);
138
+ }, [currentTab, customRenderer]);
139
+
124
140
  const startFloatDrag = useCallback((event: React.PointerEvent<HTMLElement>) => {
125
141
  event.preventDefault();
126
142
  setFloatInteraction({
@@ -213,7 +229,6 @@ export function DocBrowser({
213
229
 
214
230
  const isDocked = mode === 'docked';
215
231
  const isFullscreen = displayMode === 'fullscreen';
216
- const customRenderer = currentTab ? customTabRenderers[currentTab.kind] : undefined;
217
232
  const customRenderParams = currentTab ? {
218
233
  currentUrl,
219
234
  open,