@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,4 +1,4 @@
|
|
|
1
|
-
import { useState, useRef, useCallback, useEffect
|
|
1
|
+
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
DOCS_DEFAULT_BASE_URL,
|
|
4
4
|
useDocBrowser,
|
|
@@ -20,30 +20,22 @@ type DocBrowserProps = {
|
|
|
20
20
|
displayMode?: 'desktop' | 'fullscreen';
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
type FloatingPanelRect = { x: number; y: number; w: number; h: number };
|
|
24
|
+
type FloatingPanelResizeEdge = 'left' | 'right' | 'top' | 'bottom' | 'bottom-right';
|
|
25
|
+
type FloatingPanelInteraction = {
|
|
26
|
+
kind: 'drag' | 'resize';
|
|
27
|
+
edge?: FloatingPanelResizeEdge;
|
|
28
|
+
startX: number;
|
|
29
|
+
startY: number;
|
|
30
|
+
startRect: FloatingPanelRect;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const FLOATING_PANEL_MARGIN = 40;
|
|
34
|
+
const FLOATING_PANEL_MIN_WIDTH = 360;
|
|
35
|
+
const FLOATING_PANEL_MIN_HEIGHT = 400;
|
|
31
36
|
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
floatPos: { x: number; y: number };
|
|
35
|
-
floatSize: { w: number; h: number };
|
|
36
|
-
}): CSSProperties | undefined {
|
|
37
|
-
const { isFullscreen, floatPos, floatSize } = params;
|
|
38
|
-
if (isFullscreen) return undefined;
|
|
39
|
-
return {
|
|
40
|
-
position: 'fixed',
|
|
41
|
-
left: floatPos.x,
|
|
42
|
-
top: floatPos.y,
|
|
43
|
-
width: floatSize.w,
|
|
44
|
-
height: floatSize.h,
|
|
45
|
-
zIndex: 9999,
|
|
46
|
-
};
|
|
37
|
+
function clamp(value: number, min: number, max: number): number {
|
|
38
|
+
return Math.min(max, Math.max(min, value));
|
|
47
39
|
}
|
|
48
40
|
|
|
49
41
|
export function DocBrowser({
|
|
@@ -68,21 +60,20 @@ export function DocBrowser({
|
|
|
68
60
|
} = useDocBrowser();
|
|
69
61
|
|
|
70
62
|
const [urlInput, setUrlInput] = useState('');
|
|
71
|
-
const [isDragging, setIsDragging] = useState(false);
|
|
72
|
-
const [isResizing, setIsResizing] = useState(false);
|
|
73
63
|
const [iframeReloadVersion, setIframeReloadVersion] = useState(0);
|
|
74
|
-
const [
|
|
75
|
-
x: Math.max(
|
|
64
|
+
const [floatRect, setFloatRect] = useState<FloatingPanelRect>(() => ({
|
|
65
|
+
x: Math.max(FLOATING_PANEL_MARGIN, window.innerWidth - 520),
|
|
76
66
|
y: 80,
|
|
67
|
+
w: 480,
|
|
68
|
+
h: 600,
|
|
77
69
|
}));
|
|
78
|
-
const [
|
|
79
|
-
const dragRef = useRef<{ startX: number; startY: number; startPosX: number; startPosY: number } | null>(null);
|
|
80
|
-
const resizeRef = useRef<{ startX: number; startY: number; startW: number; startH: number } | null>(null);
|
|
70
|
+
const [floatInteraction, setFloatInteraction] = useState<FloatingPanelInteraction | null>(null);
|
|
81
71
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
82
72
|
const currentUrl = currentTab?.currentUrl ?? DOCS_DEFAULT_BASE_URL;
|
|
83
73
|
const navVersion = currentTab?.navVersion ?? 0;
|
|
84
74
|
const prevNavVersionRef = useRef(navVersion);
|
|
85
75
|
const isDocsTab = currentTab?.kind === 'docs';
|
|
76
|
+
const customRenderer = currentTab ? customTabRenderers[currentTab.kind] : undefined;
|
|
86
77
|
|
|
87
78
|
useEffect(() => {
|
|
88
79
|
if (!isDocsTab) {
|
|
@@ -118,15 +109,6 @@ export function DocBrowser({
|
|
|
118
109
|
}
|
|
119
110
|
}, [currentUrl, navVersion, isDocsTab]);
|
|
120
111
|
|
|
121
|
-
useEffect(() => {
|
|
122
|
-
if (mode === 'floating') {
|
|
123
|
-
setFloatPos((prev) => ({
|
|
124
|
-
x: Math.max(40, window.innerWidth - floatSize.w - 40),
|
|
125
|
-
y: prev.y,
|
|
126
|
-
}));
|
|
127
|
-
}
|
|
128
|
-
}, [mode, floatSize.w]);
|
|
129
|
-
|
|
130
112
|
useEffect(() => {
|
|
131
113
|
const handler = (e: MessageEvent) => {
|
|
132
114
|
if (!isDocsTab) {
|
|
@@ -140,6 +122,91 @@ export function DocBrowser({
|
|
|
140
122
|
return () => window.removeEventListener('message', handler);
|
|
141
123
|
}, [syncUrl, isDocsTab]);
|
|
142
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
|
+
|
|
140
|
+
const startFloatDrag = useCallback((event: React.PointerEvent<HTMLElement>) => {
|
|
141
|
+
event.preventDefault();
|
|
142
|
+
setFloatInteraction({
|
|
143
|
+
kind: 'drag',
|
|
144
|
+
startX: event.clientX,
|
|
145
|
+
startY: event.clientY,
|
|
146
|
+
startRect: floatRect,
|
|
147
|
+
});
|
|
148
|
+
}, [floatRect]);
|
|
149
|
+
|
|
150
|
+
const startFloatResize = useCallback((edge: FloatingPanelResizeEdge) => (event: React.PointerEvent<HTMLElement>) => {
|
|
151
|
+
event.preventDefault();
|
|
152
|
+
event.stopPropagation();
|
|
153
|
+
setFloatInteraction({
|
|
154
|
+
kind: 'resize',
|
|
155
|
+
edge,
|
|
156
|
+
startX: event.clientX,
|
|
157
|
+
startY: event.clientY,
|
|
158
|
+
startRect: floatRect,
|
|
159
|
+
});
|
|
160
|
+
}, [floatRect]);
|
|
161
|
+
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
if (!floatInteraction) return;
|
|
164
|
+
|
|
165
|
+
const onMove = (event: PointerEvent) => {
|
|
166
|
+
const { startRect, startX, startY } = floatInteraction;
|
|
167
|
+
const dx = event.clientX - startX;
|
|
168
|
+
const dy = event.clientY - startY;
|
|
169
|
+
|
|
170
|
+
if (floatInteraction.kind === 'drag') {
|
|
171
|
+
setFloatRect({
|
|
172
|
+
...startRect,
|
|
173
|
+
x: clamp(startRect.x + dx, FLOATING_PANEL_MARGIN, window.innerWidth - FLOATING_PANEL_MARGIN - startRect.w),
|
|
174
|
+
y: clamp(startRect.y + dy, FLOATING_PANEL_MARGIN, window.innerHeight - FLOATING_PANEL_MARGIN - startRect.h),
|
|
175
|
+
});
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (floatInteraction.edge === 'left' || floatInteraction.edge === 'top') {
|
|
180
|
+
const isLeftEdge = floatInteraction.edge === 'left';
|
|
181
|
+
const fixedEdge = isLeftEdge ? startRect.x + startRect.w : startRect.y + startRect.h;
|
|
182
|
+
const movingEdge = isLeftEdge ? startRect.x + dx : startRect.y + dy;
|
|
183
|
+
const minSize = isLeftEdge ? FLOATING_PANEL_MIN_WIDTH : FLOATING_PANEL_MIN_HEIGHT;
|
|
184
|
+
const nextEdge = clamp(movingEdge, FLOATING_PANEL_MARGIN, fixedEdge - minSize);
|
|
185
|
+
setFloatRect(isLeftEdge ? { ...startRect, x: nextEdge, w: fixedEdge - nextEdge } : { ...startRect, y: nextEdge, h: fixedEdge - nextEdge });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const right = clamp(startRect.x + startRect.w + dx, startRect.x + FLOATING_PANEL_MIN_WIDTH, window.innerWidth - FLOATING_PANEL_MARGIN);
|
|
190
|
+
const bottom = clamp(startRect.y + startRect.h + dy, startRect.y + FLOATING_PANEL_MIN_HEIGHT, window.innerHeight - FLOATING_PANEL_MARGIN);
|
|
191
|
+
setFloatRect({
|
|
192
|
+
...startRect,
|
|
193
|
+
w: floatInteraction.edge === 'bottom' ? startRect.w : right - startRect.x,
|
|
194
|
+
h: floatInteraction.edge === 'right' ? startRect.h : bottom - startRect.y,
|
|
195
|
+
});
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const onEnd = () => setFloatInteraction(null);
|
|
199
|
+
|
|
200
|
+
window.addEventListener('pointermove', onMove);
|
|
201
|
+
window.addEventListener('pointerup', onEnd);
|
|
202
|
+
window.addEventListener('pointercancel', onEnd);
|
|
203
|
+
return () => {
|
|
204
|
+
window.removeEventListener('pointermove', onMove);
|
|
205
|
+
window.removeEventListener('pointerup', onEnd);
|
|
206
|
+
window.removeEventListener('pointercancel', onEnd);
|
|
207
|
+
};
|
|
208
|
+
}, [floatInteraction]);
|
|
209
|
+
|
|
143
210
|
const handleUrlSubmit = useCallback((e: React.FormEvent) => {
|
|
144
211
|
e.preventDefault();
|
|
145
212
|
if (!isDocsTab) return;
|
|
@@ -154,88 +221,6 @@ export function DocBrowser({
|
|
|
154
221
|
}
|
|
155
222
|
}, [urlInput, navigate, isDocsTab]);
|
|
156
223
|
|
|
157
|
-
const onDragStart = useCallback((e: React.MouseEvent) => {
|
|
158
|
-
if (mode !== 'floating') return;
|
|
159
|
-
setIsDragging(true);
|
|
160
|
-
dragRef.current = {
|
|
161
|
-
startX: e.clientX,
|
|
162
|
-
startY: e.clientY,
|
|
163
|
-
startPosX: floatPos.x,
|
|
164
|
-
startPosY: floatPos.y,
|
|
165
|
-
};
|
|
166
|
-
}, [mode, floatPos]);
|
|
167
|
-
|
|
168
|
-
useEffect(() => {
|
|
169
|
-
if (!isDragging) return;
|
|
170
|
-
const onMove = (e: MouseEvent) => {
|
|
171
|
-
if (!dragRef.current) return;
|
|
172
|
-
setFloatPos({
|
|
173
|
-
x: dragRef.current.startPosX + (e.clientX - dragRef.current.startX),
|
|
174
|
-
y: dragRef.current.startPosY + (e.clientY - dragRef.current.startY),
|
|
175
|
-
});
|
|
176
|
-
};
|
|
177
|
-
const onUp = () => {
|
|
178
|
-
setIsDragging(false);
|
|
179
|
-
dragRef.current = null;
|
|
180
|
-
};
|
|
181
|
-
window.addEventListener('mousemove', onMove);
|
|
182
|
-
window.addEventListener('mouseup', onUp);
|
|
183
|
-
return () => {
|
|
184
|
-
window.removeEventListener('mousemove', onMove);
|
|
185
|
-
window.removeEventListener('mouseup', onUp);
|
|
186
|
-
};
|
|
187
|
-
}, [isDragging]);
|
|
188
|
-
|
|
189
|
-
const onResizeStart = useCallback((e: React.MouseEvent) => {
|
|
190
|
-
e.preventDefault();
|
|
191
|
-
e.stopPropagation();
|
|
192
|
-
setIsResizing(true);
|
|
193
|
-
const { axis } = (e.currentTarget as HTMLElement).dataset;
|
|
194
|
-
resizeRef.current = {
|
|
195
|
-
startX: e.clientX,
|
|
196
|
-
startY: e.clientY,
|
|
197
|
-
startW: floatSize.w,
|
|
198
|
-
startH: floatSize.h,
|
|
199
|
-
};
|
|
200
|
-
const onMove = (ev: MouseEvent) => {
|
|
201
|
-
if (!resizeRef.current) return;
|
|
202
|
-
setFloatSize((prev) => ({
|
|
203
|
-
w: axis === 'y' ? prev.w : Math.max(360, resizeRef.current!.startW + (ev.clientX - resizeRef.current!.startX)),
|
|
204
|
-
h: axis === 'x' ? prev.h : Math.max(400, resizeRef.current!.startH + (ev.clientY - resizeRef.current!.startY)),
|
|
205
|
-
}));
|
|
206
|
-
};
|
|
207
|
-
const onUp = () => {
|
|
208
|
-
setIsResizing(false);
|
|
209
|
-
resizeRef.current = null;
|
|
210
|
-
window.removeEventListener('mousemove', onMove);
|
|
211
|
-
window.removeEventListener('mouseup', onUp);
|
|
212
|
-
};
|
|
213
|
-
window.addEventListener('mousemove', onMove);
|
|
214
|
-
window.addEventListener('mouseup', onUp);
|
|
215
|
-
}, [floatSize]);
|
|
216
|
-
|
|
217
|
-
const onLeftResizeStart = useCallback((e: React.MouseEvent) => {
|
|
218
|
-
e.preventDefault();
|
|
219
|
-
e.stopPropagation();
|
|
220
|
-
setIsResizing(true);
|
|
221
|
-
const startX = e.clientX;
|
|
222
|
-
const startW = floatSize.w;
|
|
223
|
-
const startPosX = floatPos.x;
|
|
224
|
-
const onMove = (ev: MouseEvent) => {
|
|
225
|
-
const delta = startX - ev.clientX;
|
|
226
|
-
const newW = Math.max(360, startW + delta);
|
|
227
|
-
setFloatSize((prev) => ({ ...prev, w: newW }));
|
|
228
|
-
setFloatPos((prev) => ({ ...prev, x: startPosX - (newW - startW) }));
|
|
229
|
-
};
|
|
230
|
-
const onUp = () => {
|
|
231
|
-
setIsResizing(false);
|
|
232
|
-
window.removeEventListener('mousemove', onMove);
|
|
233
|
-
window.removeEventListener('mouseup', onUp);
|
|
234
|
-
};
|
|
235
|
-
window.addEventListener('mousemove', onMove);
|
|
236
|
-
window.addEventListener('mouseup', onUp);
|
|
237
|
-
}, [floatSize.w, floatPos.x]);
|
|
238
|
-
|
|
239
224
|
const refreshIframe = useCallback(() => {
|
|
240
225
|
setIframeReloadVersion((version) => version + 1);
|
|
241
226
|
}, []);
|
|
@@ -244,7 +229,6 @@ export function DocBrowser({
|
|
|
244
229
|
|
|
245
230
|
const isDocked = mode === 'docked';
|
|
246
231
|
const isFullscreen = displayMode === 'fullscreen';
|
|
247
|
-
const customRenderer = currentTab ? customTabRenderers[currentTab.kind] : undefined;
|
|
248
232
|
const customRenderParams = currentTab ? {
|
|
249
233
|
currentUrl,
|
|
250
234
|
open,
|
|
@@ -265,7 +249,7 @@ export function DocBrowser({
|
|
|
265
249
|
isDocked={isDocked}
|
|
266
250
|
isFullscreen={isFullscreen}
|
|
267
251
|
onClose={close}
|
|
268
|
-
onDragStart={
|
|
252
|
+
onDragStart={startFloatDrag}
|
|
269
253
|
onToggleMode={toggleMode}
|
|
270
254
|
/>
|
|
271
255
|
|
|
@@ -297,8 +281,8 @@ export function DocBrowser({
|
|
|
297
281
|
iframeRef={iframeRef}
|
|
298
282
|
iframeReloadVersion={iframeReloadVersion}
|
|
299
283
|
iframeSandbox={iframeSandbox}
|
|
300
|
-
isDragging={
|
|
301
|
-
isResizing={
|
|
284
|
+
isDragging={floatInteraction?.kind === 'drag'}
|
|
285
|
+
isResizing={floatInteraction?.kind === 'resize'}
|
|
302
286
|
navVersion={navVersion}
|
|
303
287
|
/>
|
|
304
288
|
|
|
@@ -319,22 +303,52 @@ export function DocBrowser({
|
|
|
319
303
|
);
|
|
320
304
|
}
|
|
321
305
|
|
|
322
|
-
|
|
306
|
+
return (
|
|
323
307
|
<div
|
|
324
308
|
data-testid="doc-browser-panel"
|
|
325
|
-
className={
|
|
326
|
-
|
|
309
|
+
className={cn(
|
|
310
|
+
'flex flex-col bg-white overflow-hidden relative',
|
|
311
|
+
isFullscreen
|
|
312
|
+
? 'fixed inset-0 z-[9999] h-[100dvh] w-screen rounded-none border-0 shadow-2xl'
|
|
313
|
+
: 'rounded-2xl shadow-2xl border border-gray-200',
|
|
314
|
+
)}
|
|
315
|
+
style={
|
|
316
|
+
isFullscreen
|
|
317
|
+
? undefined
|
|
318
|
+
: {
|
|
319
|
+
position: 'fixed',
|
|
320
|
+
left: floatRect.x,
|
|
321
|
+
top: floatRect.y,
|
|
322
|
+
width: floatRect.w,
|
|
323
|
+
height: floatRect.h,
|
|
324
|
+
zIndex: 9999,
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
327
|
>
|
|
328
328
|
{panelContent}
|
|
329
329
|
|
|
330
330
|
{!isDocked && !isFullscreen && (
|
|
331
331
|
<>
|
|
332
|
-
<div className="absolute top-0 left-0
|
|
333
|
-
<div
|
|
334
|
-
|
|
332
|
+
<div className="absolute top-0 left-0 h-1.5 w-full cursor-ns-resize z-20 hover:bg-primary/10 transition-colors" data-testid="doc-browser-resize-top" onPointerDown={startFloatResize('top')} />
|
|
333
|
+
<div
|
|
334
|
+
className="absolute top-0 left-0 w-1.5 h-full cursor-ew-resize z-20 hover:bg-primary/10 transition-colors"
|
|
335
|
+
data-testid="doc-browser-resize-left"
|
|
336
|
+
onPointerDown={startFloatResize('left')}
|
|
337
|
+
/>
|
|
338
|
+
<div
|
|
339
|
+
className="absolute top-0 right-0 w-1.5 h-full cursor-ew-resize z-20 hover:bg-primary/10 transition-colors"
|
|
340
|
+
data-testid="doc-browser-resize-right"
|
|
341
|
+
onPointerDown={startFloatResize('right')}
|
|
342
|
+
/>
|
|
343
|
+
<div
|
|
344
|
+
className="absolute bottom-0 left-0 h-1.5 w-full cursor-ns-resize z-20 hover:bg-primary/10 transition-colors"
|
|
345
|
+
data-testid="doc-browser-resize-bottom"
|
|
346
|
+
onPointerDown={startFloatResize('bottom')}
|
|
347
|
+
/>
|
|
335
348
|
<div
|
|
336
349
|
className="absolute bottom-0 right-0 w-4 h-4 cursor-se-resize z-30 flex items-center justify-center text-gray-300 hover:text-gray-500 transition-colors"
|
|
337
|
-
|
|
350
|
+
data-testid="doc-browser-resize-bottom-right"
|
|
351
|
+
onPointerDown={startFloatResize('bottom-right')}
|
|
338
352
|
>
|
|
339
353
|
<GripVertical className="w-3 h-3 rotate-[-45deg]" />
|
|
340
354
|
</div>
|
|
@@ -342,6 +356,4 @@ export function DocBrowser({
|
|
|
342
356
|
)}
|
|
343
357
|
</div>
|
|
344
358
|
);
|
|
345
|
-
|
|
346
|
-
return panel;
|
|
347
359
|
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DocBrowserOpenOptions,
|
|
3
|
+
DocBrowserState,
|
|
4
|
+
DocBrowserStateUpdate,
|
|
5
|
+
DocBrowserTab,
|
|
6
|
+
DocBrowserTabKind,
|
|
7
|
+
} from '@/shared/components/doc-browser/types/doc-browser.types';
|
|
8
|
+
import {
|
|
9
|
+
createDefaultDocBrowserState,
|
|
10
|
+
createDocBrowserTab,
|
|
11
|
+
} from '@/shared/components/doc-browser/utils/doc-browser-state.utils';
|
|
12
|
+
import {
|
|
13
|
+
getDefaultDocsUrl,
|
|
14
|
+
inferTabKind,
|
|
15
|
+
normalizeDocUrl,
|
|
16
|
+
normalizeUrlByKind,
|
|
17
|
+
} from '@/shared/components/doc-browser/utils/doc-browser-url.utils';
|
|
18
|
+
import { useDocBrowserStore } from '@/shared/components/doc-browser/stores/doc-browser.store';
|
|
19
|
+
|
|
20
|
+
function updateTab(
|
|
21
|
+
state: DocBrowserState,
|
|
22
|
+
tabId: string,
|
|
23
|
+
updater: (tab: DocBrowserTab) => DocBrowserTab,
|
|
24
|
+
): DocBrowserState {
|
|
25
|
+
return {
|
|
26
|
+
...state,
|
|
27
|
+
tabs: state.tabs.map((tab) => (tab.id === tabId ? updater(tab) : tab)),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function updateActiveTab(state: DocBrowserState, updater: (tab: DocBrowserTab) => DocBrowserTab): DocBrowserState {
|
|
32
|
+
return updateTab(state, state.activeTabId, updater);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function areOpenUrlsEquivalent(
|
|
36
|
+
currentUrl: string,
|
|
37
|
+
nextUrl: string,
|
|
38
|
+
currentKind: DocBrowserTabKind,
|
|
39
|
+
nextKind: DocBrowserTabKind,
|
|
40
|
+
): boolean {
|
|
41
|
+
if (currentKind === 'docs' && nextKind === 'docs') {
|
|
42
|
+
return normalizeDocUrl(currentUrl) === normalizeDocUrl(nextUrl);
|
|
43
|
+
}
|
|
44
|
+
return currentUrl === nextUrl;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function updateTabForOpen(
|
|
48
|
+
tab: DocBrowserTab,
|
|
49
|
+
url: string,
|
|
50
|
+
kind: DocBrowserTabKind,
|
|
51
|
+
options?: DocBrowserOpenOptions,
|
|
52
|
+
dedupeKey?: string,
|
|
53
|
+
): DocBrowserTab {
|
|
54
|
+
const baseTab = {
|
|
55
|
+
...tab,
|
|
56
|
+
title: options?.title || tab.title,
|
|
57
|
+
kind,
|
|
58
|
+
dedupeKey,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (areOpenUrlsEquivalent(tab.currentUrl, url, tab.kind, kind)) {
|
|
62
|
+
return baseTab;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
...baseTab,
|
|
67
|
+
currentUrl: url,
|
|
68
|
+
history: [...tab.history.slice(0, tab.historyIndex + 1), url],
|
|
69
|
+
historyIndex: tab.historyIndex + 1,
|
|
70
|
+
navVersion: tab.navVersion + 1,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function resolveOpenTargetUrl(params: {
|
|
75
|
+
url?: string;
|
|
76
|
+
kind: DocBrowserTabKind;
|
|
77
|
+
activeTab?: DocBrowserTab;
|
|
78
|
+
}): string {
|
|
79
|
+
const { activeTab, kind, url } = params;
|
|
80
|
+
if (url && url.trim().length > 0) {
|
|
81
|
+
return normalizeUrlByKind(url, kind);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (kind === 'docs') {
|
|
85
|
+
return getDefaultDocsUrl();
|
|
86
|
+
}
|
|
87
|
+
return activeTab?.currentUrl ?? getDefaultDocsUrl();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function openDocBrowserState(
|
|
91
|
+
prev: DocBrowserState,
|
|
92
|
+
url?: string,
|
|
93
|
+
options?: DocBrowserOpenOptions,
|
|
94
|
+
): DocBrowserState {
|
|
95
|
+
const {
|
|
96
|
+
activate,
|
|
97
|
+
dedupeKey: rawDedupeKey,
|
|
98
|
+
kind,
|
|
99
|
+
newTab: shouldForceNewTab,
|
|
100
|
+
title,
|
|
101
|
+
} = options ?? {};
|
|
102
|
+
const activeTab = prev.tabs.find((tab) => tab.id === prev.activeTabId) ?? prev.tabs[0];
|
|
103
|
+
const targetKind = kind ?? (url ? inferTabKind(url) : activeTab?.kind ?? 'docs');
|
|
104
|
+
const targetUrl = resolveOpenTargetUrl({ url, kind: targetKind, activeTab });
|
|
105
|
+
const dedupeKey = rawDedupeKey?.trim() || undefined;
|
|
106
|
+
const matchedTab = dedupeKey
|
|
107
|
+
? prev.tabs.find((tab) => tab.dedupeKey === dedupeKey)
|
|
108
|
+
: undefined;
|
|
109
|
+
|
|
110
|
+
if (matchedTab) {
|
|
111
|
+
const next = updateTab(
|
|
112
|
+
prev,
|
|
113
|
+
matchedTab.id,
|
|
114
|
+
(tab) => updateTabForOpen(tab, targetUrl, targetKind, options, dedupeKey),
|
|
115
|
+
);
|
|
116
|
+
return {
|
|
117
|
+
...next,
|
|
118
|
+
activeTabId: activate === false ? prev.activeTabId : matchedTab.id,
|
|
119
|
+
isOpen: true,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (shouldForceNewTab || dedupeKey || !activeTab || activeTab.kind !== targetKind) {
|
|
124
|
+
const newTab = createDocBrowserTab(targetUrl, targetKind, title, dedupeKey);
|
|
125
|
+
return {
|
|
126
|
+
...prev,
|
|
127
|
+
isOpen: true,
|
|
128
|
+
tabs: [...prev.tabs, newTab],
|
|
129
|
+
activeTabId: newTab.id,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const next = updateActiveTab(
|
|
134
|
+
prev,
|
|
135
|
+
(tab) => updateTabForOpen(tab, targetUrl, targetKind, options, dedupeKey),
|
|
136
|
+
);
|
|
137
|
+
return { ...next, isOpen: true };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export class DocBrowserManager {
|
|
141
|
+
private readonly setSnapshot = (next: DocBrowserStateUpdate): void => {
|
|
142
|
+
useDocBrowserStore.getState().setSnapshot(next);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
readonly open = (url?: string, options?: DocBrowserOpenOptions): void => {
|
|
146
|
+
this.setSnapshot((prev) => openDocBrowserState(prev, url, options));
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
readonly close = (): void => {
|
|
150
|
+
this.setSnapshot((prev) => ({ ...prev, isOpen: false }));
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
readonly toggleMode = (): void => {
|
|
154
|
+
this.setSnapshot((prev) => ({ ...prev, mode: prev.mode === 'floating' ? 'docked' : 'floating' }));
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
readonly navigate = (url: string): void => {
|
|
158
|
+
this.setSnapshot((prev) => {
|
|
159
|
+
if (!prev.tabs.length) {
|
|
160
|
+
const fallbackTab = createDocBrowserTab(getDefaultDocsUrl(), 'docs', 'Docs');
|
|
161
|
+
return {
|
|
162
|
+
...prev,
|
|
163
|
+
tabs: [fallbackTab],
|
|
164
|
+
activeTabId: fallbackTab.id,
|
|
165
|
+
isOpen: true,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return updateActiveTab(prev, (tab) => {
|
|
170
|
+
if (tab.kind !== 'docs') {
|
|
171
|
+
return tab;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const targetUrl = normalizeUrlByKind(url, 'docs');
|
|
175
|
+
if (normalizeDocUrl(targetUrl) === normalizeDocUrl(tab.currentUrl)) {
|
|
176
|
+
return tab;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
...tab,
|
|
181
|
+
currentUrl: targetUrl,
|
|
182
|
+
history: [...tab.history.slice(0, tab.historyIndex + 1), targetUrl],
|
|
183
|
+
historyIndex: tab.historyIndex + 1,
|
|
184
|
+
navVersion: tab.navVersion + 1,
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
readonly syncUrl = (url: string): void => {
|
|
191
|
+
this.setSnapshot((prev) => {
|
|
192
|
+
if (!prev.tabs.length) {
|
|
193
|
+
const fallbackTab = createDocBrowserTab(getDefaultDocsUrl(), 'docs', 'Docs');
|
|
194
|
+
return {
|
|
195
|
+
...prev,
|
|
196
|
+
tabs: [fallbackTab],
|
|
197
|
+
activeTabId: fallbackTab.id,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return updateActiveTab(prev, (tab) => {
|
|
202
|
+
if (tab.kind !== 'docs') {
|
|
203
|
+
return tab;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (normalizeDocUrl(url) === normalizeDocUrl(tab.currentUrl)) {
|
|
207
|
+
return tab;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
...tab,
|
|
212
|
+
currentUrl: url,
|
|
213
|
+
history: [...tab.history.slice(0, tab.historyIndex + 1), url],
|
|
214
|
+
historyIndex: tab.historyIndex + 1,
|
|
215
|
+
};
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
readonly goBack = (): void => {
|
|
221
|
+
this.setSnapshot((prev) => updateActiveTab(prev, (tab) => {
|
|
222
|
+
if (tab.kind !== 'docs' || tab.historyIndex <= 0) return tab;
|
|
223
|
+
const newIndex = tab.historyIndex - 1;
|
|
224
|
+
return { ...tab, historyIndex: newIndex, currentUrl: tab.history[newIndex] };
|
|
225
|
+
}));
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
readonly goForward = (): void => {
|
|
229
|
+
this.setSnapshot((prev) => updateActiveTab(prev, (tab) => {
|
|
230
|
+
if (tab.kind !== 'docs' || tab.historyIndex >= tab.history.length - 1) return tab;
|
|
231
|
+
const newIndex = tab.historyIndex + 1;
|
|
232
|
+
return { ...tab, historyIndex: newIndex, currentUrl: tab.history[newIndex] };
|
|
233
|
+
}));
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
readonly closeTab = (tabId: string): void => {
|
|
237
|
+
this.setSnapshot((prev) => {
|
|
238
|
+
if (prev.tabs.length <= 1) {
|
|
239
|
+
const fallbackState = createDefaultDocBrowserState();
|
|
240
|
+
return {
|
|
241
|
+
...fallbackState,
|
|
242
|
+
isOpen: false,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const index = prev.tabs.findIndex((tab) => tab.id === tabId);
|
|
247
|
+
if (index < 0) {
|
|
248
|
+
return prev;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const nextTabs = prev.tabs.filter((tab) => tab.id !== tabId);
|
|
252
|
+
const nextActiveId = prev.activeTabId === tabId
|
|
253
|
+
? nextTabs[Math.max(0, index - 1)]?.id ?? nextTabs[0].id
|
|
254
|
+
: prev.activeTabId;
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
...prev,
|
|
258
|
+
tabs: nextTabs,
|
|
259
|
+
activeTabId: nextActiveId,
|
|
260
|
+
};
|
|
261
|
+
});
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
readonly setActiveTab = (tabId: string): void => {
|
|
265
|
+
this.setSnapshot((prev) => {
|
|
266
|
+
if (!prev.tabs.some((tab) => tab.id === tabId)) {
|
|
267
|
+
return prev;
|
|
268
|
+
}
|
|
269
|
+
return { ...prev, activeTabId: tabId, isOpen: true };
|
|
270
|
+
});
|
|
271
|
+
};
|
|
272
|
+
}
|