@nextclaw/ui 0.12.35-beta.2 → 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.
- package/CHANGELOG.md +127 -0
- package/dist/assets/api-BTZui7nj.js +15 -0
- package/dist/assets/{book-open--T9dJHnp.js → book-open-qc0UVn6m.js} +1 -1
- package/dist/assets/channels-list-page-DBtU_M1Y.js +8 -0
- package/dist/assets/{chat-page-Doi-sasO.js → chat-page-BcoJYo91.js} +2 -2
- package/dist/assets/config-split-page-DW8tP9Bc.js +1 -0
- package/dist/assets/createLucideIcon-CA0sgoMJ.js +1 -0
- package/dist/assets/desktop-update-config-DARSwQG8.js +1 -0
- package/dist/assets/dist-D5zsy7pX.js +1 -0
- package/dist/assets/dist-DNxIa3pe.js +41 -0
- package/dist/assets/doc-browser-B9skxzVo.js +1 -0
- package/dist/assets/doc-browser-CwyITRXx.js +1 -0
- package/dist/assets/doc-browser-context-Do2qSrB8.js +1 -0
- package/dist/assets/{external-link-CZ9bQY0L.js → external-link-C9fbyi6k.js} +1 -1
- package/dist/assets/index-BYRP_BMj.css +1 -0
- package/dist/assets/index-DckPGTdM.js +103 -0
- package/dist/assets/mcp-marketplace-page-BiExW5wq.js +40 -0
- package/dist/assets/mcp-marketplace-page-Cj6MDSkE.js +1 -0
- package/dist/assets/model-config-DbdY7fF1.js +1 -0
- package/dist/assets/notice-card-CyiVcu_Z.js +5 -0
- package/dist/assets/play-D-s0C4MN.js +1 -0
- package/dist/assets/plus-DOwjM_kF.js +1 -0
- package/dist/assets/provider-scoped-model-input-Bk8jlpRm.js +1 -0
- package/dist/assets/providers-list-CJjNStDV.js +1 -0
- package/dist/assets/react-BaRotFd6.js +1 -0
- package/dist/assets/remote-gl8QckNc.js +1 -0
- package/dist/assets/runtime-config-page-BMFc1yzf.js +1 -0
- package/dist/assets/{save-B6wbBLkU.js → save-CG4Hb1u8.js} +1 -1
- package/dist/assets/{search-config-BtCNY431.js → search-config-DUOR26if.js} +1 -1
- package/dist/assets/{search-Czito3Dk.js → search-lWpOIHEn.js} +1 -1
- package/dist/assets/secrets-config-ZbLzjIRx.js +3 -0
- package/dist/assets/setting-row-CNfTKDjc.js +1 -0
- package/dist/assets/tabs-custom-BbZoysAl.js +1 -0
- package/dist/assets/tag-chip-Dl8JbhC7.js +1 -0
- package/dist/assets/x-D6GwT_8k.js +1 -0
- package/dist/index.html +15 -17
- package/package.json +8 -9
- package/src/app/components/app-presenter-provider.tsx +21 -0
- package/src/app/components/layout/sidebar.layout.test.tsx +2 -2
- package/src/app/components/layout/sidebar.tsx +3 -3
- package/src/app/index.tsx +5 -3
- package/src/app/presenters/app.presenter.ts +24 -0
- package/src/features/account/components/account-panel.tsx +11 -11
- package/src/features/account/index.ts +1 -4
- package/src/features/account/managers/account.manager.ts +52 -27
- package/src/features/account/stores/account.store.ts +0 -15
- package/src/features/apps/components/apps-panel.tsx +43 -0
- package/src/features/apps/index.ts +1 -0
- package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +8 -8
- package/src/features/chat/components/layout/chat-sidebar.tsx +3 -2
- package/src/features/chat/managers/chat-session-list.manager.test.ts +58 -0
- package/src/features/chat/stores/chat-session-list.store.ts +58 -7
- package/src/features/panel-apps/components/panel-app-toolbar.tsx +4 -4
- package/src/features/panel-apps/components/panel-apps-list.tsx +6 -2
- package/src/features/panel-apps/index.ts +3 -1
- package/src/features/panel-apps/managers/panel-app-bridge.manager.test.ts +111 -0
- package/src/features/panel-apps/managers/panel-app-bridge.manager.ts +247 -0
- package/src/features/panel-apps/utils/panel-app-doc-browser.utils.test.ts +23 -0
- package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +42 -16
- package/src/features/remote/components/remote-access-page.test.tsx +2 -2
- package/src/features/remote/components/remote-access-page.tsx +9 -9
- package/src/features/remote/managers/remote-access.manager.ts +24 -41
- package/src/features/service-apps/components/service-action-authorization-dialog.tsx +78 -0
- package/src/features/service-apps/components/service-apps-panel.tsx +290 -0
- package/src/features/service-apps/hooks/use-service-apps.ts +66 -0
- package/src/features/service-apps/index.ts +3 -0
- package/src/features/service-apps/managers/service-action-authorization.manager.ts +10 -0
- package/src/features/service-apps/stores/service-action-authorization.store.ts +53 -0
- package/src/shared/components/doc-browser/doc-browser-context.test.tsx +150 -1
- package/src/shared/components/doc-browser/doc-browser-context.tsx +34 -443
- package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +2 -2
- package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +7 -0
- package/src/shared/components/doc-browser/doc-browser.test.tsx +82 -2
- package/src/shared/components/doc-browser/doc-browser.tsx +147 -135
- package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +272 -0
- package/src/shared/components/doc-browser/stores/doc-browser.store.ts +140 -0
- package/src/shared/components/doc-browser/types/doc-browser.types.ts +47 -0
- package/src/shared/components/doc-browser/utils/doc-browser-state.utils.ts +47 -0
- package/src/shared/components/doc-browser/utils/doc-browser-url.utils.ts +95 -0
- package/src/shared/lib/api/managers/client.manager.ts +2 -1
- package/src/shared/lib/i18n/runtime/doc-browser-labels.utils.ts +31 -0
- package/src/shared/lib/transport/local-transport.service.ts +18 -2
- package/src/shared/lib/transport/remote-transport.service.ts +19 -3
- package/src/shared/lib/transport/request-raw-api-response.utils.ts +1 -1
- package/dist/assets/api-D3uoaOR8.js +0 -15
- package/dist/assets/channels-list-page-CzDMCXR_.js +0 -8
- package/dist/assets/config-split-page-Dutq3R5o.js +0 -1
- package/dist/assets/createLucideIcon-Cwr2o6Zq.js +0 -1
- package/dist/assets/desktop-update-config-Dd4DdPH3.js +0 -1
- package/dist/assets/dist-e5WATjIW.js +0 -1
- package/dist/assets/doc-browser-DInzGY0W.js +0 -1
- package/dist/assets/doc-browser-DxVvmYQk.js +0 -1
- package/dist/assets/doc-browser-context-CLczyX2Z.js +0 -1
- package/dist/assets/es2015-BJw-hrex.js +0 -41
- package/dist/assets/i18n-tWKGEGxB.js +0 -1
- package/dist/assets/index-HvJDzfSU.css +0 -1
- package/dist/assets/index-dHwM4pEn.js +0 -103
- package/dist/assets/key-round-B8tTb3ZJ.js +0 -1
- package/dist/assets/mcp-marketplace-page-D-dydvbL.js +0 -1
- package/dist/assets/mcp-marketplace-page-n9Z25Cyk.js +0 -40
- package/dist/assets/model-config-8BVoSVXQ.js +0 -1
- package/dist/assets/notice-card-DUDbOvqx.js +0 -5
- package/dist/assets/play-CV_kwi9X.js +0 -1
- package/dist/assets/plus-BqfNq7Tu.js +0 -1
- package/dist/assets/provider-scoped-model-input-eO4XY0iY.js +0 -1
- package/dist/assets/providers-list-CjJmW-od.js +0 -1
- package/dist/assets/remote-DxTFnQm8.js +0 -1
- package/dist/assets/rotate-cw-BKEhWuWp.js +0 -1
- package/dist/assets/runtime-config-page-Ba6atCMq.js +0 -1
- package/dist/assets/secrets-config-Ddh05-J5.js +0 -3
- package/dist/assets/select-CmmDgG8o.js +0 -1
- package/dist/assets/setting-row-BUO6FeN1.js +0 -1
- package/dist/assets/tabs-custom-D3IZL9WX.js +0 -1
- package/dist/assets/tag-chip-Dm_pOtE3.js +0 -1
- package/dist/assets/x-DBP3WiC9.js +0 -1
- package/src/app/components/app-manager-provider.tsx +0 -20
- package/src/app/managers/app.manager.ts +0 -12
- /package/dist/assets/{config-hints-DGfwbumO.js → config-hints-CTqBnCDp.js} +0 -0
|
@@ -1,196 +1,25 @@
|
|
|
1
|
-
import { createContext,
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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
|
|
301
|
-
}, [
|
|
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
|
-
...
|
|
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}>
|
|
@@ -23,7 +23,7 @@ type DocBrowserPanelHeaderProps = {
|
|
|
23
23
|
isDocked: boolean;
|
|
24
24
|
isFullscreen: boolean;
|
|
25
25
|
onClose: () => void;
|
|
26
|
-
onDragStart: (e: React.
|
|
26
|
+
onDragStart: (e: React.PointerEvent<HTMLElement>) => void;
|
|
27
27
|
onToggleMode: () => void;
|
|
28
28
|
};
|
|
29
29
|
|
|
@@ -73,7 +73,7 @@ export function DocBrowserPanelHeader({
|
|
|
73
73
|
!isDocked && !isFullscreen && 'cursor-grab active:cursor-grabbing',
|
|
74
74
|
isFullscreen && 'pt-[calc(env(safe-area-inset-top,0px)+0.625rem)]',
|
|
75
75
|
)}
|
|
76
|
-
|
|
76
|
+
onPointerDown={!isDocked && !isFullscreen ? onDragStart : undefined}
|
|
77
77
|
>
|
|
78
78
|
<div className="flex items-center gap-2.5 min-w-0">
|
|
79
79
|
{icon}
|
|
@@ -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;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { render, screen } from "@testing-library/react";
|
|
2
|
-
import { describe, expect, it, vi } from "vitest";
|
|
1
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import { DocBrowser } from "@/shared/components/doc-browser/doc-browser";
|
|
4
4
|
|
|
5
5
|
const { docBrowserState } = vi.hoisted(() => ({
|
|
@@ -49,7 +49,34 @@ vi.mock("@/shared/components/doc-browser/doc-browser-context", async () => {
|
|
|
49
49
|
};
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
+
function firePointerEvent(
|
|
53
|
+
target: Window | Document | Node | Element,
|
|
54
|
+
type: string,
|
|
55
|
+
point: { clientX: number; clientY?: number; pointerId?: number },
|
|
56
|
+
) {
|
|
57
|
+
const event = new Event(type, { bubbles: true });
|
|
58
|
+
Object.defineProperties(event, {
|
|
59
|
+
clientX: { value: point.clientX },
|
|
60
|
+
clientY: { value: point.clientY ?? 0 },
|
|
61
|
+
pointerId: { value: point.pointerId ?? 1 },
|
|
62
|
+
});
|
|
63
|
+
fireEvent(target, event);
|
|
64
|
+
}
|
|
65
|
+
|
|
52
66
|
describe("DocBrowser", () => {
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
docBrowserState.mode = "docked";
|
|
69
|
+
Object.defineProperty(window, "innerWidth", {
|
|
70
|
+
configurable: true,
|
|
71
|
+
value: 1200,
|
|
72
|
+
});
|
|
73
|
+
Object.defineProperty(window, "innerHeight", {
|
|
74
|
+
configurable: true,
|
|
75
|
+
value: 900,
|
|
76
|
+
});
|
|
77
|
+
HTMLElement.prototype.setPointerCapture = vi.fn();
|
|
78
|
+
});
|
|
79
|
+
|
|
53
80
|
it("uses the shared right panel resize handle in docked mode", () => {
|
|
54
81
|
render(<DocBrowser />);
|
|
55
82
|
|
|
@@ -71,4 +98,57 @@ describe("DocBrowser", () => {
|
|
|
71
98
|
expect(panel.querySelector(".cursor-ew-resize")).toBeNull();
|
|
72
99
|
expect(panel.querySelector(".cursor-se-resize")).toBeNull();
|
|
73
100
|
});
|
|
101
|
+
|
|
102
|
+
it("keeps the floating panel left edge stable when resizing from the right", () => {
|
|
103
|
+
docBrowserState.mode = "floating";
|
|
104
|
+
|
|
105
|
+
render(<DocBrowser />);
|
|
106
|
+
|
|
107
|
+
firePointerEvent(screen.getByTestId("doc-browser-resize-right"), "pointerdown", {
|
|
108
|
+
clientX: 1160,
|
|
109
|
+
pointerId: 1,
|
|
110
|
+
});
|
|
111
|
+
firePointerEvent(window, "pointermove", { clientX: 1120, pointerId: 1 });
|
|
112
|
+
|
|
113
|
+
const panel = screen.getByTestId("doc-browser-panel");
|
|
114
|
+
expect(panel.style.left).toBe("680px");
|
|
115
|
+
expect(panel.style.width).toBe("440px");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("keeps the floating panel right edge stable when resizing from the left", () => {
|
|
119
|
+
docBrowserState.mode = "floating";
|
|
120
|
+
|
|
121
|
+
render(<DocBrowser />);
|
|
122
|
+
|
|
123
|
+
firePointerEvent(screen.getByTestId("doc-browser-resize-left"), "pointerdown", {
|
|
124
|
+
clientX: 680,
|
|
125
|
+
pointerId: 1,
|
|
126
|
+
});
|
|
127
|
+
firePointerEvent(window, "pointermove", { clientX: 620, pointerId: 1 });
|
|
128
|
+
|
|
129
|
+
const panel = screen.getByTestId("doc-browser-panel");
|
|
130
|
+
expect(panel.style.left).toBe("620px");
|
|
131
|
+
expect(panel.style.width).toBe("540px");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("keeps the floating panel bottom edge stable when resizing from the top", () => {
|
|
135
|
+
docBrowserState.mode = "floating";
|
|
136
|
+
|
|
137
|
+
render(<DocBrowser />);
|
|
138
|
+
|
|
139
|
+
firePointerEvent(screen.getByTestId("doc-browser-resize-top"), "pointerdown", {
|
|
140
|
+
clientX: 900,
|
|
141
|
+
clientY: 80,
|
|
142
|
+
pointerId: 1,
|
|
143
|
+
});
|
|
144
|
+
firePointerEvent(window, "pointermove", {
|
|
145
|
+
clientX: 900,
|
|
146
|
+
clientY: 120,
|
|
147
|
+
pointerId: 1,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const panel = screen.getByTestId("doc-browser-panel");
|
|
151
|
+
expect(panel.style.top).toBe("120px");
|
|
152
|
+
expect(panel.style.height).toBe("560px");
|
|
153
|
+
});
|
|
74
154
|
});
|