@nextclaw/ui 0.12.35-beta.5 → 0.12.35-beta.7
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 +126 -0
- package/dist/assets/api-DruAcori.js +15 -0
- package/dist/assets/{book-open-qc0UVn6m.js → book-open-cPX0Za7Z.js} +1 -1
- package/dist/assets/channels-list-page-D-tv11dc.js +8 -0
- package/dist/assets/chat-page-MyXEZI8R.js +105 -0
- package/dist/assets/{config-split-page-DW8tP9Bc.js → config-split-page-BFh_Qixx.js} +1 -1
- package/dist/assets/confirm-dialog-B0TP-vPd.js +5 -0
- package/dist/assets/{createLucideIcon-CA0sgoMJ.js → createLucideIcon-Br1EFMEr.js} +1 -1
- package/dist/assets/{desktop-update-config-DARSwQG8.js → desktop-update-config-oZpIKKtS.js} +1 -1
- package/dist/assets/{dist-D5zsy7pX.js → dist-CpwDZDtv.js} +1 -1
- package/dist/assets/{dist-DNxIa3pe.js → dist-rzakR_bu.js} +1 -1
- package/dist/assets/doc-browser-B2OFjScU.js +3 -0
- package/dist/assets/doc-browser-BrUQEZ1j.js +1 -0
- package/dist/assets/doc-browser-context-DB6TTHeE.js +1 -0
- package/dist/assets/doc-browser-route-registry.utils-x5ijvk0u.js +1 -0
- package/dist/assets/{external-link-C9fbyi6k.js → external-link-C3usm3se.js} +1 -1
- package/dist/assets/index-D2H2CKYR.js +101 -0
- package/dist/assets/index-v0fAGXQW.css +1 -0
- package/dist/assets/{mcp-marketplace-page-BiExW5wq.js → mcp-marketplace-page-CegP14Hl.js} +2 -2
- package/dist/assets/mcp-marketplace-page-MjpFEEkY.js +1 -0
- package/dist/assets/model-config-fIE_Asps.js +1 -0
- package/dist/assets/play-CqNi0u02.js +1 -0
- package/dist/assets/plus-BcIC8dG1.js +1 -0
- package/dist/assets/{provider-scoped-model-input-Bk8jlpRm.js → provider-scoped-model-input-D4zRsGZ6.js} +1 -1
- package/dist/assets/providers-list-CqsOk2hr.js +1 -0
- package/dist/assets/react-B2X_ph0a.js +1 -0
- package/dist/assets/remote-BMWQUPDW.js +1 -0
- package/dist/assets/runtime-config-page-BJ86cC8a.js +1 -0
- package/dist/assets/{save-CG4Hb1u8.js → save-D10M5vQg.js} +1 -1
- package/dist/assets/{search-lWpOIHEn.js → search-CdhgFNwf.js} +1 -1
- package/dist/assets/search-config-TbCPlRE7.js +1 -0
- package/dist/assets/{secrets-config-ZbLzjIRx.js → secrets-config-DBIEKP_V.js} +1 -1
- package/dist/assets/{setting-row-CNfTKDjc.js → setting-row-8VkukJoH.js} +1 -1
- package/dist/assets/{tabs-custom-BbZoysAl.js → tabs-custom-BBXu1OGs.js} +1 -1
- package/dist/assets/{tag-chip-Dl8JbhC7.js → tag-chip-ix15P-r4.js} +1 -1
- package/dist/assets/wrench-qVIwShTF.js +1 -0
- package/dist/assets/x-Chb07O-Y.js +1 -0
- package/dist/index.html +17 -15
- package/package.json +8 -8
- package/src/features/panel-apps/components/panel-app-list-item.test.tsx +60 -0
- package/src/features/panel-apps/components/panel-app-list-item.tsx +120 -30
- package/src/features/panel-apps/components/panel-apps-list.tsx +8 -1
- package/src/features/panel-apps/hooks/use-panel-apps.ts +10 -0
- package/src/features/panel-apps/managers/panel-app-bridge.manager.test.ts +85 -1
- package/src/features/panel-apps/managers/panel-app-bridge.manager.ts +123 -6
- package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +7 -3
- package/src/features/panel-apps/utils/panel-app-view.utils.test.ts +66 -0
- package/src/features/panel-apps/utils/panel-app-view.utils.ts +13 -2
- package/src/index.css +23 -0
- package/src/shared/components/doc-browser/doc-browser-context.test.tsx +170 -0
- package/src/shared/components/doc-browser/doc-browser-context.tsx +3 -0
- package/src/shared/components/doc-browser/doc-browser-home-page.tsx +95 -0
- package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +0 -90
- package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +117 -44
- package/src/shared/components/doc-browser/doc-browser.test.tsx +159 -1
- package/src/shared/components/doc-browser/doc-browser.tsx +42 -23
- package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +183 -71
- package/src/shared/components/doc-browser/stores/doc-browser.store.ts +52 -1
- package/src/shared/components/doc-browser/types/doc-browser.types.ts +9 -0
- package/src/shared/components/doc-browser/utils/doc-browser-route-registry.utils.ts +232 -0
- package/src/shared/components/doc-browser/utils/doc-browser-state.utils.ts +11 -0
- package/src/shared/components/doc-browser/utils/doc-browser-url.utils.ts +2 -0
- package/src/shared/lib/api/types.ts +1 -0
- package/src/shared/lib/i18n/runtime/doc-browser-labels.utils.ts +10 -0
- package/dist/assets/api-BTZui7nj.js +0 -15
- package/dist/assets/channels-list-page-DBtU_M1Y.js +0 -8
- package/dist/assets/chat-page-BcoJYo91.js +0 -105
- package/dist/assets/doc-browser-B9skxzVo.js +0 -1
- package/dist/assets/doc-browser-CwyITRXx.js +0 -1
- package/dist/assets/doc-browser-context-Do2qSrB8.js +0 -1
- package/dist/assets/index-BYRP_BMj.css +0 -1
- package/dist/assets/index-DckPGTdM.js +0 -103
- package/dist/assets/mcp-marketplace-page-Cj6MDSkE.js +0 -1
- package/dist/assets/model-config-DbdY7fF1.js +0 -1
- package/dist/assets/notice-card-CyiVcu_Z.js +0 -5
- package/dist/assets/play-D-s0C4MN.js +0 -1
- package/dist/assets/plus-DOwjM_kF.js +0 -1
- package/dist/assets/providers-list-CJjNStDV.js +0 -1
- package/dist/assets/react-BaRotFd6.js +0 -1
- package/dist/assets/remote-gl8QckNc.js +0 -1
- package/dist/assets/runtime-config-page-BMFc1yzf.js +0 -1
- package/dist/assets/search-config-DUOR26if.js +0 -1
- package/dist/assets/x-D6GwT_8k.js +0 -1
- /package/dist/assets/{config-hints-CTqBnCDp.js → config-hints-CY1lg9IE.js} +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { PointerEvent } from 'react';
|
|
2
|
+
import { ArrowLeft, ArrowRight, Maximize2, PanelRightOpen, Plus, X } from 'lucide-react';
|
|
2
3
|
import type { DocBrowserTab } from './doc-browser-context';
|
|
3
4
|
import { cn } from '@/shared/lib/utils';
|
|
4
5
|
import { t } from '@/shared/lib/i18n';
|
|
@@ -6,62 +7,134 @@ import { t } from '@/shared/lib/i18n';
|
|
|
6
7
|
type DocBrowserTabStripProps = {
|
|
7
8
|
tabs: DocBrowserTab[];
|
|
8
9
|
activeTabId: string;
|
|
9
|
-
|
|
10
|
+
canGoBack: boolean;
|
|
11
|
+
canGoForward: boolean;
|
|
12
|
+
isDocked: boolean;
|
|
13
|
+
isFullscreen: boolean;
|
|
14
|
+
onGoBack: () => void;
|
|
15
|
+
onGoForward: () => void;
|
|
16
|
+
onOpenNewTab: () => void;
|
|
10
17
|
onSetActiveTab: (tabId: string) => void;
|
|
11
18
|
onCloseTab: (tabId: string) => void;
|
|
19
|
+
onClose: () => void;
|
|
20
|
+
onDragStart: (event: PointerEvent<HTMLElement>) => void;
|
|
21
|
+
onToggleMode: () => void;
|
|
12
22
|
};
|
|
13
23
|
|
|
14
24
|
export function DocBrowserTabStrip({
|
|
15
25
|
tabs,
|
|
16
26
|
activeTabId,
|
|
17
|
-
|
|
27
|
+
canGoBack,
|
|
28
|
+
canGoForward,
|
|
29
|
+
isDocked,
|
|
30
|
+
isFullscreen,
|
|
31
|
+
onGoBack,
|
|
32
|
+
onGoForward,
|
|
33
|
+
onOpenNewTab,
|
|
18
34
|
onSetActiveTab,
|
|
19
35
|
onCloseTab,
|
|
36
|
+
onClose,
|
|
37
|
+
onDragStart,
|
|
38
|
+
onToggleMode,
|
|
20
39
|
}: DocBrowserTabStripProps) {
|
|
21
40
|
return (
|
|
22
|
-
<div
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
onClick={(event) => {
|
|
46
|
-
event.stopPropagation();
|
|
47
|
-
onCloseTab(tab.id);
|
|
48
|
-
}}
|
|
49
|
-
className="rounded p-0.5 hover:bg-black/10"
|
|
50
|
-
aria-label={t('docBrowserCloseTab')}
|
|
41
|
+
<div
|
|
42
|
+
data-testid="doc-browser-tab-strip"
|
|
43
|
+
className={cn(
|
|
44
|
+
'flex h-11 items-stretch gap-2 px-2.5 bg-background border-b border-[#f1e7d4] shrink-0 select-none',
|
|
45
|
+
isFullscreen && 'h-[calc(env(safe-area-inset-top,0px)+2.75rem)] pt-[env(safe-area-inset-top,0px)]',
|
|
46
|
+
)}
|
|
47
|
+
onPointerDown={!isDocked && !isFullscreen ? onDragStart : undefined}
|
|
48
|
+
>
|
|
49
|
+
<div
|
|
50
|
+
className="doc-browser-tab-scrollbar flex h-full min-w-0 flex-1 items-center gap-1.5 overflow-x-auto"
|
|
51
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
52
|
+
>
|
|
53
|
+
{tabs.map((tab) => {
|
|
54
|
+
const isActive = tab.id === activeTabId;
|
|
55
|
+
return (
|
|
56
|
+
<div
|
|
57
|
+
key={tab.id}
|
|
58
|
+
className={cn(
|
|
59
|
+
'inline-flex items-center gap-1 h-7 px-1.5 rounded-lg text-xs border max-w-[220px] shrink-0 transition-colors',
|
|
60
|
+
isActive
|
|
61
|
+
? 'bg-amber-50/80 border-amber-200 text-amber-900 shadow-[0_1px_2px_rgba(30,20,10,0.04)]'
|
|
62
|
+
: 'bg-[#f9f8f5] border-[#eee3d1] text-[#78644d] hover:bg-[#fff7ea] hover:text-[#2f2212]',
|
|
63
|
+
)}
|
|
51
64
|
>
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
<button
|
|
66
|
+
type="button"
|
|
67
|
+
onClick={() => onSetActiveTab(tab.id)}
|
|
68
|
+
className="truncate text-left px-1"
|
|
69
|
+
title={tab.title}
|
|
70
|
+
>
|
|
71
|
+
{tab.title || t('docBrowserTabUntitled')}
|
|
72
|
+
</button>
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
onClick={(event) => {
|
|
76
|
+
event.stopPropagation();
|
|
77
|
+
onCloseTab(tab.id);
|
|
78
|
+
}}
|
|
79
|
+
className="rounded p-0.5 hover:bg-black/10"
|
|
80
|
+
aria-label={t('docBrowserCloseTab')}
|
|
81
|
+
>
|
|
82
|
+
<X className="w-3 h-3" />
|
|
83
|
+
</button>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
})}
|
|
87
|
+
</div>
|
|
88
|
+
<div
|
|
89
|
+
className="flex h-full items-center gap-1 shrink-0"
|
|
90
|
+
data-testid="doc-browser-tab-actions"
|
|
91
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
62
92
|
>
|
|
63
|
-
<
|
|
64
|
-
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
onClick={onGoBack}
|
|
96
|
+
disabled={!canGoBack}
|
|
97
|
+
className="rounded-md p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 disabled:cursor-not-allowed disabled:text-gray-300 disabled:opacity-60 disabled:hover:bg-transparent disabled:hover:text-gray-300"
|
|
98
|
+
title={t('docBrowserBack')}
|
|
99
|
+
>
|
|
100
|
+
<ArrowLeft className="w-3.5 h-3.5" />
|
|
101
|
+
</button>
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
onClick={onGoForward}
|
|
105
|
+
disabled={!canGoForward}
|
|
106
|
+
className="rounded-md p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 disabled:cursor-not-allowed disabled:text-gray-300 disabled:opacity-60 disabled:hover:bg-transparent disabled:hover:text-gray-300"
|
|
107
|
+
title={t('docBrowserForward')}
|
|
108
|
+
>
|
|
109
|
+
<ArrowRight className="w-3.5 h-3.5" />
|
|
110
|
+
</button>
|
|
111
|
+
<button
|
|
112
|
+
type="button"
|
|
113
|
+
onClick={onOpenNewTab}
|
|
114
|
+
className="hover:bg-gray-100 rounded-md p-1.5 text-gray-500 hover:text-gray-700 transition-colors"
|
|
115
|
+
title={t('docBrowserNewTab')}
|
|
116
|
+
>
|
|
117
|
+
<Plus className="w-3.5 h-3.5" />
|
|
118
|
+
</button>
|
|
119
|
+
{!isFullscreen ? (
|
|
120
|
+
<button
|
|
121
|
+
type="button"
|
|
122
|
+
onClick={onToggleMode}
|
|
123
|
+
className="hover:bg-gray-100 rounded-md p-1.5 text-gray-500 hover:text-gray-700 transition-colors"
|
|
124
|
+
title={isDocked ? t('docBrowserFloatMode') : t('docBrowserDockMode')}
|
|
125
|
+
>
|
|
126
|
+
{isDocked ? <Maximize2 className="w-3.5 h-3.5" /> : <PanelRightOpen className="w-3.5 h-3.5" />}
|
|
127
|
+
</button>
|
|
128
|
+
) : null}
|
|
129
|
+
<button
|
|
130
|
+
type="button"
|
|
131
|
+
onClick={onClose}
|
|
132
|
+
className="hover:bg-gray-100 rounded-md p-1.5 text-gray-500 hover:text-gray-700 transition-colors"
|
|
133
|
+
title={t('docBrowserClose')}
|
|
134
|
+
>
|
|
135
|
+
<X className="w-3.5 h-3.5" />
|
|
136
|
+
</button>
|
|
137
|
+
</div>
|
|
65
138
|
</div>
|
|
66
139
|
);
|
|
67
140
|
}
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import { DocBrowser } from "@/shared/components/doc-browser/doc-browser";
|
|
4
|
+
import type { DocBrowserContextValue, DocBrowserTab } from "@/shared/components/doc-browser/doc-browser-context";
|
|
4
5
|
|
|
5
|
-
const {
|
|
6
|
+
const { navigateMock } = vi.hoisted(() => ({
|
|
7
|
+
navigateMock: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock("react-router-dom", async () => ({
|
|
11
|
+
...(await vi.importActual("react-router-dom") as object),
|
|
12
|
+
useNavigate: () => navigateMock,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
const { docBrowserState } = vi.hoisted<{ docBrowserState: DocBrowserContextValue }>(() => ({
|
|
6
16
|
docBrowserState: {
|
|
7
17
|
isOpen: true,
|
|
8
18
|
mode: "docked" as "docked" | "floating",
|
|
@@ -18,6 +28,12 @@ const { docBrowserState } = vi.hoisted(() => ({
|
|
|
18
28
|
},
|
|
19
29
|
],
|
|
20
30
|
activeTabId: "docs",
|
|
31
|
+
activeHistory: [{
|
|
32
|
+
kind: "docs" as const,
|
|
33
|
+
tabId: "docs",
|
|
34
|
+
url: "https://docs.nextclaw.io/en/guide/getting-started",
|
|
35
|
+
}],
|
|
36
|
+
activeHistoryIndex: 0,
|
|
21
37
|
currentTab: {
|
|
22
38
|
id: "docs",
|
|
23
39
|
kind: "docs" as const,
|
|
@@ -27,6 +43,8 @@ const { docBrowserState } = vi.hoisted(() => ({
|
|
|
27
43
|
historyIndex: 0,
|
|
28
44
|
navVersion: 0,
|
|
29
45
|
},
|
|
46
|
+
open: vi.fn(),
|
|
47
|
+
openNewTab: vi.fn(),
|
|
30
48
|
close: vi.fn(),
|
|
31
49
|
toggleMode: vi.fn(),
|
|
32
50
|
goBack: vi.fn(),
|
|
@@ -65,7 +83,27 @@ function firePointerEvent(
|
|
|
65
83
|
|
|
66
84
|
describe("DocBrowser", () => {
|
|
67
85
|
beforeEach(() => {
|
|
86
|
+
vi.clearAllMocks();
|
|
68
87
|
docBrowserState.mode = "docked";
|
|
88
|
+
docBrowserState.tabs = [
|
|
89
|
+
{
|
|
90
|
+
id: "docs",
|
|
91
|
+
kind: "docs" as const,
|
|
92
|
+
title: "Docs",
|
|
93
|
+
currentUrl: "https://docs.nextclaw.io/en/guide/getting-started",
|
|
94
|
+
history: ["https://docs.nextclaw.io/en/guide/getting-started"],
|
|
95
|
+
historyIndex: 0,
|
|
96
|
+
navVersion: 0,
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
docBrowserState.activeTabId = "docs";
|
|
100
|
+
docBrowserState.activeHistory = [{
|
|
101
|
+
kind: "docs" as const,
|
|
102
|
+
tabId: "docs",
|
|
103
|
+
url: "https://docs.nextclaw.io/en/guide/getting-started",
|
|
104
|
+
}];
|
|
105
|
+
docBrowserState.activeHistoryIndex = 0;
|
|
106
|
+
docBrowserState.currentTab = docBrowserState.tabs[0];
|
|
69
107
|
Object.defineProperty(window, "innerWidth", {
|
|
70
108
|
configurable: true,
|
|
71
109
|
value: 1200,
|
|
@@ -99,6 +137,126 @@ describe("DocBrowser", () => {
|
|
|
99
137
|
expect(panel.querySelector(".cursor-se-resize")).toBeNull();
|
|
100
138
|
});
|
|
101
139
|
|
|
140
|
+
it("keeps browser window controls on the tab strip", () => {
|
|
141
|
+
render(<DocBrowser />);
|
|
142
|
+
|
|
143
|
+
const tabStrip = screen.getByTestId("doc-browser-tab-strip");
|
|
144
|
+
const tabActions = screen.getByTestId("doc-browser-tab-actions");
|
|
145
|
+
|
|
146
|
+
expect(tabActions.contains(screen.getByTitle("Back"))).toBe(true);
|
|
147
|
+
expect(tabActions.contains(screen.getByTitle("Forward"))).toBe(true);
|
|
148
|
+
expect(tabStrip.contains(screen.getByTitle("Float Window"))).toBe(true);
|
|
149
|
+
expect(tabStrip.contains(screen.getByTitle("Close"))).toBe(true);
|
|
150
|
+
expect(tabActions.contains(screen.getByTitle("New Tab"))).toBe(true);
|
|
151
|
+
expect(screen.queryByText("Embedded Browser")).toBeNull();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("uses active history to enable browser back and forward actions", () => {
|
|
155
|
+
const history = [
|
|
156
|
+
"https://docs.nextclaw.io/en/guide/getting-started",
|
|
157
|
+
"https://docs.nextclaw.io/en/guide/channels",
|
|
158
|
+
"https://docs.nextclaw.io/en/guide/apps",
|
|
159
|
+
];
|
|
160
|
+
const historyTab: DocBrowserTab = {
|
|
161
|
+
id: "docs",
|
|
162
|
+
kind: "docs",
|
|
163
|
+
title: "Docs",
|
|
164
|
+
currentUrl: history[1],
|
|
165
|
+
history,
|
|
166
|
+
historyIndex: 1,
|
|
167
|
+
navVersion: 0,
|
|
168
|
+
};
|
|
169
|
+
docBrowserState.tabs = [historyTab];
|
|
170
|
+
docBrowserState.activeTabId = historyTab.id;
|
|
171
|
+
docBrowserState.activeHistory = [
|
|
172
|
+
{ kind: "home" as const, tabId: "home", url: "nextclaw://new-tab" },
|
|
173
|
+
{ kind: "docs" as const, tabId: historyTab.id, url: history[1] },
|
|
174
|
+
{ kind: "apps" as const, tabId: "apps", url: "nextclaw://apps" },
|
|
175
|
+
];
|
|
176
|
+
docBrowserState.activeHistoryIndex = 1;
|
|
177
|
+
docBrowserState.currentTab = historyTab;
|
|
178
|
+
|
|
179
|
+
render(<DocBrowser />);
|
|
180
|
+
|
|
181
|
+
fireEvent.click(screen.getByTitle("Back"));
|
|
182
|
+
fireEvent.click(screen.getByTitle("Forward"));
|
|
183
|
+
|
|
184
|
+
expect(docBrowserState.goBack).toHaveBeenCalled();
|
|
185
|
+
expect(docBrowserState.goForward).toHaveBeenCalled();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("disables browser history actions at the active history edges", () => {
|
|
189
|
+
const history = [
|
|
190
|
+
"https://docs.nextclaw.io/en/guide/getting-started",
|
|
191
|
+
"https://docs.nextclaw.io/en/guide/apps",
|
|
192
|
+
];
|
|
193
|
+
const latestTab: DocBrowserTab = {
|
|
194
|
+
id: "docs",
|
|
195
|
+
kind: "docs",
|
|
196
|
+
title: "Docs",
|
|
197
|
+
currentUrl: history[1],
|
|
198
|
+
history,
|
|
199
|
+
historyIndex: 1,
|
|
200
|
+
navVersion: 0,
|
|
201
|
+
};
|
|
202
|
+
docBrowserState.tabs = [latestTab];
|
|
203
|
+
docBrowserState.activeTabId = latestTab.id;
|
|
204
|
+
docBrowserState.activeHistory = [
|
|
205
|
+
{ kind: "docs" as const, tabId: latestTab.id, url: history[0] },
|
|
206
|
+
{ kind: "docs" as const, tabId: latestTab.id, url: history[1] },
|
|
207
|
+
];
|
|
208
|
+
docBrowserState.activeHistoryIndex = 1;
|
|
209
|
+
docBrowserState.currentTab = latestTab;
|
|
210
|
+
|
|
211
|
+
const { rerender } = render(<DocBrowser />);
|
|
212
|
+
|
|
213
|
+
expect(screen.getByTitle("Back")).toHaveProperty("disabled", false);
|
|
214
|
+
expect(screen.getByTitle("Forward")).toHaveProperty("disabled", true);
|
|
215
|
+
|
|
216
|
+
const oldestTab: DocBrowserTab = {
|
|
217
|
+
...latestTab,
|
|
218
|
+
currentUrl: history[0],
|
|
219
|
+
historyIndex: 0,
|
|
220
|
+
};
|
|
221
|
+
docBrowserState.tabs = [oldestTab];
|
|
222
|
+
docBrowserState.currentTab = oldestTab;
|
|
223
|
+
docBrowserState.activeHistoryIndex = 0;
|
|
224
|
+
rerender(<DocBrowser />);
|
|
225
|
+
|
|
226
|
+
expect(screen.getByTitle("Back")).toHaveProperty("disabled", true);
|
|
227
|
+
expect(screen.getByTitle("Forward")).toHaveProperty("disabled", false);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("opens the start page from the fixed new tab action", () => {
|
|
231
|
+
render(<DocBrowser />);
|
|
232
|
+
|
|
233
|
+
fireEvent.click(screen.getByTitle("New Tab"));
|
|
234
|
+
|
|
235
|
+
expect(docBrowserState.openNewTab).toHaveBeenCalled();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("renders a start page for home tabs", () => {
|
|
239
|
+
const homeTab: DocBrowserTab = {
|
|
240
|
+
id: "home",
|
|
241
|
+
kind: "home" as const,
|
|
242
|
+
title: "Start Page",
|
|
243
|
+
currentUrl: "nextclaw://new-tab",
|
|
244
|
+
history: ["nextclaw://new-tab"],
|
|
245
|
+
historyIndex: 0,
|
|
246
|
+
navVersion: 0,
|
|
247
|
+
};
|
|
248
|
+
docBrowserState.tabs = [homeTab];
|
|
249
|
+
docBrowserState.activeTabId = homeTab.id;
|
|
250
|
+
docBrowserState.currentTab = homeTab;
|
|
251
|
+
|
|
252
|
+
render(<DocBrowser />);
|
|
253
|
+
|
|
254
|
+
expect(screen.getAllByText("Start Page").length).toBeGreaterThan(0);
|
|
255
|
+
expect(screen.getByText("Apps")).toBeTruthy();
|
|
256
|
+
expect(screen.getByText("Service Apps")).toBeTruthy();
|
|
257
|
+
expect(screen.getByText("Help Docs")).toBeTruthy();
|
|
258
|
+
});
|
|
259
|
+
|
|
102
260
|
it("keeps the floating panel left edge stable when resizing from the right", () => {
|
|
103
261
|
docBrowserState.mode = "floating";
|
|
104
262
|
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
DOCS_DEFAULT_BASE_URL,
|
|
4
|
+
DOC_BROWSER_HOME_TAB_KIND,
|
|
4
5
|
useDocBrowser,
|
|
5
6
|
} from './doc-browser-context';
|
|
7
|
+
import { normalizeDocUrl } from './utils/doc-browser-url.utils';
|
|
8
|
+
import { DocBrowserHomePage } from './doc-browser-home-page';
|
|
6
9
|
import type { DocBrowserCustomTabRenderers } from './doc-browser-renderer.types';
|
|
7
10
|
import {
|
|
8
11
|
DocBrowserDocsToolbar,
|
|
9
12
|
DocBrowserExternalLink,
|
|
10
13
|
DocBrowserFrameContent,
|
|
11
|
-
DocBrowserPanelHeader,
|
|
12
14
|
} from './doc-browser-panel-parts';
|
|
13
15
|
import { DocBrowserTabStrip } from './doc-browser-tab-strip';
|
|
14
16
|
import { ResizableRightPanel } from '@/shared/components/resizable-right-panel/resizable-right-panel';
|
|
@@ -38,6 +40,15 @@ function clamp(value: number, min: number, max: number): number {
|
|
|
38
40
|
return Math.min(max, Math.max(min, value));
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
function createInitialFloatingPanelRect(): FloatingPanelRect {
|
|
44
|
+
return {
|
|
45
|
+
x: Math.max(FLOATING_PANEL_MARGIN, window.innerWidth - 520),
|
|
46
|
+
y: 80,
|
|
47
|
+
w: 480,
|
|
48
|
+
h: 600,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
41
52
|
export function DocBrowser({
|
|
42
53
|
customTabRenderers = {},
|
|
43
54
|
displayMode = 'desktop',
|
|
@@ -47,8 +58,11 @@ export function DocBrowser({
|
|
|
47
58
|
mode,
|
|
48
59
|
tabs,
|
|
49
60
|
activeTabId,
|
|
61
|
+
activeHistory,
|
|
62
|
+
activeHistoryIndex,
|
|
50
63
|
currentTab,
|
|
51
64
|
open,
|
|
65
|
+
openNewTab,
|
|
52
66
|
close,
|
|
53
67
|
toggleMode,
|
|
54
68
|
goBack,
|
|
@@ -61,18 +75,17 @@ export function DocBrowser({
|
|
|
61
75
|
|
|
62
76
|
const [urlInput, setUrlInput] = useState('');
|
|
63
77
|
const [iframeReloadVersion, setIframeReloadVersion] = useState(0);
|
|
64
|
-
const [floatRect, setFloatRect] = useState<FloatingPanelRect>(
|
|
65
|
-
x: Math.max(FLOATING_PANEL_MARGIN, window.innerWidth - 520),
|
|
66
|
-
y: 80,
|
|
67
|
-
w: 480,
|
|
68
|
-
h: 600,
|
|
69
|
-
}));
|
|
78
|
+
const [floatRect, setFloatRect] = useState<FloatingPanelRect>(createInitialFloatingPanelRect);
|
|
70
79
|
const [floatInteraction, setFloatInteraction] = useState<FloatingPanelInteraction | null>(null);
|
|
71
80
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
72
81
|
const currentUrl = currentTab?.currentUrl ?? DOCS_DEFAULT_BASE_URL;
|
|
73
82
|
const navVersion = currentTab?.navVersion ?? 0;
|
|
83
|
+
const pendingParentDocsUrlRef = useRef<string | null>(null);
|
|
74
84
|
const prevNavVersionRef = useRef(navVersion);
|
|
75
85
|
const isDocsTab = currentTab?.kind === 'docs';
|
|
86
|
+
const isHomeTab = currentTab?.kind === DOC_BROWSER_HOME_TAB_KIND;
|
|
87
|
+
const canGoBack = activeHistoryIndex > 0;
|
|
88
|
+
const canGoForward = activeHistoryIndex < activeHistory.length - 1;
|
|
76
89
|
const customRenderer = currentTab ? customTabRenderers[currentTab.kind] : undefined;
|
|
77
90
|
|
|
78
91
|
useEffect(() => {
|
|
@@ -92,16 +105,19 @@ export function DocBrowser({
|
|
|
92
105
|
// use postMessage to SPA-navigate inside the iframe instead of remounting.
|
|
93
106
|
useEffect(() => {
|
|
94
107
|
if (!isDocsTab) {
|
|
108
|
+
pendingParentDocsUrlRef.current = null;
|
|
95
109
|
return;
|
|
96
110
|
}
|
|
97
111
|
if (navVersion !== prevNavVersionRef.current) {
|
|
98
112
|
prevNavVersionRef.current = navVersion;
|
|
113
|
+
pendingParentDocsUrlRef.current = normalizeDocUrl(currentUrl);
|
|
99
114
|
return;
|
|
100
115
|
}
|
|
101
116
|
|
|
102
117
|
if (iframeRef.current?.contentWindow) {
|
|
103
118
|
try {
|
|
104
119
|
const path = new URL(currentUrl).pathname;
|
|
120
|
+
pendingParentDocsUrlRef.current = normalizeDocUrl(currentUrl);
|
|
105
121
|
iframeRef.current.contentWindow.postMessage({ type: 'docs-navigate', path }, '*');
|
|
106
122
|
} catch {
|
|
107
123
|
// ignore postMessage errors
|
|
@@ -115,12 +131,17 @@ export function DocBrowser({
|
|
|
115
131
|
return;
|
|
116
132
|
}
|
|
117
133
|
if (e.data?.type === 'docs-route-change' && typeof e.data.url === 'string') {
|
|
134
|
+
const eventUrl = normalizeDocUrl(e.data.url);
|
|
135
|
+
if (pendingParentDocsUrlRef.current && eventUrl !== pendingParentDocsUrlRef.current) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
pendingParentDocsUrlRef.current = null;
|
|
118
139
|
syncUrl(e.data.url);
|
|
119
140
|
}
|
|
120
141
|
};
|
|
121
142
|
window.addEventListener('message', handler);
|
|
122
143
|
return () => window.removeEventListener('message', handler);
|
|
123
|
-
}, [syncUrl, isDocsTab]);
|
|
144
|
+
}, [currentUrl, syncUrl, isDocsTab]);
|
|
124
145
|
|
|
125
146
|
useEffect(() => {
|
|
126
147
|
if (!currentTab || !customRenderer?.onIframeMessage) {
|
|
@@ -236,36 +257,34 @@ export function DocBrowser({
|
|
|
236
257
|
tab: currentTab,
|
|
237
258
|
} : undefined;
|
|
238
259
|
const customToolbar = customRenderParams ? customRenderer?.renderToolbar?.(customRenderParams) : null;
|
|
239
|
-
const customContent = customRenderParams
|
|
260
|
+
const customContent = customRenderParams
|
|
261
|
+
? customRenderer?.renderContent?.(customRenderParams) ?? (isHomeTab ? <DocBrowserHomePage open={open} /> : null)
|
|
262
|
+
: null;
|
|
240
263
|
const iframeSandbox = currentTab
|
|
241
264
|
? customRenderer?.getIframeSandbox?.(currentTab) ?? 'allow-same-origin allow-scripts allow-popups allow-forms'
|
|
242
265
|
: 'allow-same-origin allow-scripts allow-popups allow-forms';
|
|
243
266
|
|
|
244
267
|
const panelContent = (
|
|
245
268
|
<>
|
|
246
|
-
<DocBrowserPanelHeader
|
|
247
|
-
currentTab={currentTab}
|
|
248
|
-
customRenderer={customRenderer}
|
|
249
|
-
isDocked={isDocked}
|
|
250
|
-
isFullscreen={isFullscreen}
|
|
251
|
-
onClose={close}
|
|
252
|
-
onDragStart={startFloatDrag}
|
|
253
|
-
onToggleMode={toggleMode}
|
|
254
|
-
/>
|
|
255
|
-
|
|
256
269
|
<DocBrowserTabStrip
|
|
257
270
|
tabs={tabs}
|
|
258
271
|
activeTabId={activeTabId}
|
|
259
|
-
|
|
272
|
+
canGoBack={canGoBack}
|
|
273
|
+
canGoForward={canGoForward}
|
|
274
|
+
isDocked={isDocked}
|
|
275
|
+
isFullscreen={isFullscreen}
|
|
276
|
+
onGoBack={goBack}
|
|
277
|
+
onGoForward={goForward}
|
|
278
|
+
onOpenNewTab={openNewTab}
|
|
260
279
|
onSetActiveTab={setActiveTab}
|
|
261
280
|
onCloseTab={closeTab}
|
|
281
|
+
onClose={close}
|
|
282
|
+
onDragStart={startFloatDrag}
|
|
283
|
+
onToggleMode={toggleMode}
|
|
262
284
|
/>
|
|
263
285
|
|
|
264
286
|
<DocBrowserDocsToolbar
|
|
265
|
-
currentTab={currentTab}
|
|
266
287
|
isDocsTab={isDocsTab}
|
|
267
|
-
onBack={goBack}
|
|
268
|
-
onForward={goForward}
|
|
269
288
|
onSubmit={handleUrlSubmit}
|
|
270
289
|
onUrlInputChange={setUrlInput}
|
|
271
290
|
urlInput={urlInput}
|