@nextclaw/ui 0.12.35-beta.6 → 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.
Files changed (84) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/dist/assets/api-DruAcori.js +15 -0
  3. package/dist/assets/{book-open-qc0UVn6m.js → book-open-cPX0Za7Z.js} +1 -1
  4. package/dist/assets/channels-list-page-D-tv11dc.js +8 -0
  5. package/dist/assets/chat-page-MyXEZI8R.js +105 -0
  6. package/dist/assets/{config-split-page-DW8tP9Bc.js → config-split-page-BFh_Qixx.js} +1 -1
  7. package/dist/assets/confirm-dialog-B0TP-vPd.js +5 -0
  8. package/dist/assets/{createLucideIcon-CA0sgoMJ.js → createLucideIcon-Br1EFMEr.js} +1 -1
  9. package/dist/assets/{desktop-update-config-DARSwQG8.js → desktop-update-config-oZpIKKtS.js} +1 -1
  10. package/dist/assets/{dist-D5zsy7pX.js → dist-CpwDZDtv.js} +1 -1
  11. package/dist/assets/{dist-DNxIa3pe.js → dist-rzakR_bu.js} +1 -1
  12. package/dist/assets/doc-browser-B2OFjScU.js +3 -0
  13. package/dist/assets/doc-browser-BrUQEZ1j.js +1 -0
  14. package/dist/assets/doc-browser-context-DB6TTHeE.js +1 -0
  15. package/dist/assets/doc-browser-route-registry.utils-x5ijvk0u.js +1 -0
  16. package/dist/assets/{external-link-C9fbyi6k.js → external-link-C3usm3se.js} +1 -1
  17. package/dist/assets/index-D2H2CKYR.js +101 -0
  18. package/dist/assets/index-v0fAGXQW.css +1 -0
  19. package/dist/assets/{mcp-marketplace-page-BiExW5wq.js → mcp-marketplace-page-CegP14Hl.js} +2 -2
  20. package/dist/assets/mcp-marketplace-page-MjpFEEkY.js +1 -0
  21. package/dist/assets/model-config-fIE_Asps.js +1 -0
  22. package/dist/assets/play-CqNi0u02.js +1 -0
  23. package/dist/assets/plus-BcIC8dG1.js +1 -0
  24. package/dist/assets/{provider-scoped-model-input-Bk8jlpRm.js → provider-scoped-model-input-D4zRsGZ6.js} +1 -1
  25. package/dist/assets/providers-list-CqsOk2hr.js +1 -0
  26. package/dist/assets/react-B2X_ph0a.js +1 -0
  27. package/dist/assets/remote-BMWQUPDW.js +1 -0
  28. package/dist/assets/runtime-config-page-BJ86cC8a.js +1 -0
  29. package/dist/assets/{save-CG4Hb1u8.js → save-D10M5vQg.js} +1 -1
  30. package/dist/assets/{search-lWpOIHEn.js → search-CdhgFNwf.js} +1 -1
  31. package/dist/assets/search-config-TbCPlRE7.js +1 -0
  32. package/dist/assets/{secrets-config-ZbLzjIRx.js → secrets-config-DBIEKP_V.js} +1 -1
  33. package/dist/assets/{setting-row-CNfTKDjc.js → setting-row-8VkukJoH.js} +1 -1
  34. package/dist/assets/{tabs-custom-BbZoysAl.js → tabs-custom-BBXu1OGs.js} +1 -1
  35. package/dist/assets/{tag-chip-Dl8JbhC7.js → tag-chip-ix15P-r4.js} +1 -1
  36. package/dist/assets/wrench-qVIwShTF.js +1 -0
  37. package/dist/assets/x-Chb07O-Y.js +1 -0
  38. package/dist/index.html +17 -15
  39. package/package.json +8 -8
  40. package/src/features/panel-apps/components/panel-app-list-item.test.tsx +60 -0
  41. package/src/features/panel-apps/components/panel-app-list-item.tsx +120 -30
  42. package/src/features/panel-apps/components/panel-apps-list.tsx +8 -1
  43. package/src/features/panel-apps/hooks/use-panel-apps.ts +10 -0
  44. package/src/features/panel-apps/managers/panel-app-bridge.manager.test.ts +85 -1
  45. package/src/features/panel-apps/managers/panel-app-bridge.manager.ts +123 -6
  46. package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +7 -3
  47. package/src/features/panel-apps/utils/panel-app-view.utils.test.ts +66 -0
  48. package/src/features/panel-apps/utils/panel-app-view.utils.ts +13 -2
  49. package/src/index.css +23 -0
  50. package/src/shared/components/doc-browser/doc-browser-context.test.tsx +170 -0
  51. package/src/shared/components/doc-browser/doc-browser-context.tsx +3 -0
  52. package/src/shared/components/doc-browser/doc-browser-home-page.tsx +95 -0
  53. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +0 -90
  54. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +117 -44
  55. package/src/shared/components/doc-browser/doc-browser.test.tsx +159 -1
  56. package/src/shared/components/doc-browser/doc-browser.tsx +42 -23
  57. package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +183 -71
  58. package/src/shared/components/doc-browser/stores/doc-browser.store.ts +52 -1
  59. package/src/shared/components/doc-browser/types/doc-browser.types.ts +9 -0
  60. package/src/shared/components/doc-browser/utils/doc-browser-route-registry.utils.ts +232 -0
  61. package/src/shared/components/doc-browser/utils/doc-browser-state.utils.ts +11 -0
  62. package/src/shared/components/doc-browser/utils/doc-browser-url.utils.ts +2 -0
  63. package/src/shared/lib/api/types.ts +1 -0
  64. package/src/shared/lib/i18n/runtime/doc-browser-labels.utils.ts +10 -0
  65. package/dist/assets/api-BTZui7nj.js +0 -15
  66. package/dist/assets/channels-list-page-DBtU_M1Y.js +0 -8
  67. package/dist/assets/chat-page-BcoJYo91.js +0 -105
  68. package/dist/assets/doc-browser-B9skxzVo.js +0 -1
  69. package/dist/assets/doc-browser-CwyITRXx.js +0 -1
  70. package/dist/assets/doc-browser-context-Do2qSrB8.js +0 -1
  71. package/dist/assets/index-BYRP_BMj.css +0 -1
  72. package/dist/assets/index-DckPGTdM.js +0 -103
  73. package/dist/assets/mcp-marketplace-page-Cj6MDSkE.js +0 -1
  74. package/dist/assets/model-config-DbdY7fF1.js +0 -1
  75. package/dist/assets/notice-card-CyiVcu_Z.js +0 -5
  76. package/dist/assets/play-D-s0C4MN.js +0 -1
  77. package/dist/assets/plus-DOwjM_kF.js +0 -1
  78. package/dist/assets/providers-list-CJjNStDV.js +0 -1
  79. package/dist/assets/react-BaRotFd6.js +0 -1
  80. package/dist/assets/remote-gl8QckNc.js +0 -1
  81. package/dist/assets/runtime-config-page-BMFc1yzf.js +0 -1
  82. package/dist/assets/search-config-DUOR26if.js +0 -1
  83. package/dist/assets/x-D6GwT_8k.js +0 -1
  84. /package/dist/assets/{config-hints-CTqBnCDp.js → config-hints-CY1lg9IE.js} +0 -0
@@ -1,4 +1,5 @@
1
- import { Plus, X } from 'lucide-react';
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
- onOpenDocs: () => void;
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
- onOpenDocs,
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 className="flex items-center gap-1.5 px-2.5 py-2 bg-background border-b border-[#f1e7d4] overflow-x-auto custom-scrollbar">
23
- {tabs.map((tab) => {
24
- const isActive = tab.id === activeTabId;
25
- return (
26
- <div
27
- key={tab.id}
28
- className={cn(
29
- 'inline-flex items-center gap-1 h-7 px-1.5 rounded-lg text-xs border max-w-[220px] shrink-0 transition-colors',
30
- isActive
31
- ? 'bg-amber-50/80 border-amber-200 text-amber-900 shadow-[0_1px_2px_rgba(30,20,10,0.04)]'
32
- : 'bg-[#f9f8f5] border-[#eee3d1] text-[#78644d] hover:bg-[#fff7ea] hover:text-[#2f2212]',
33
- )}
34
- >
35
- <button
36
- type="button"
37
- onClick={() => onSetActiveTab(tab.id)}
38
- className="truncate text-left px-1"
39
- title={tab.title}
40
- >
41
- {tab.title || t('docBrowserTabUntitled')}
42
- </button>
43
- <button
44
- type="button"
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
- <X className="w-3 h-3" />
53
- </button>
54
- </div>
55
- );
56
- })}
57
- <button
58
- type="button"
59
- onClick={onOpenDocs}
60
- className="inline-flex items-center justify-center w-7 h-7 rounded-lg border border-[#eee3d1] bg-white text-[#78644d] hover:bg-[#fff7ea] hover:text-[#2f2212] shrink-0"
61
- title={t('docBrowserNewTab')}
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
- <Plus className="w-3.5 h-3.5" />
64
- </button>
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 { docBrowserState } = vi.hoisted(() => ({
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 ? customRenderer?.renderContent?.(customRenderParams) : null;
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
- onOpenDocs={() => open(undefined, { kind: 'docs', newTab: true, title: 'Docs' })}
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}