@nextclaw/ui 0.13.1 → 0.13.3

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 (96) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/dist/assets/app-presenter-provider-DIcnNB9s.js +3 -0
  3. package/dist/assets/{wrench-qVIwShTF.js → boxes-BvA-I63l.js} +1 -1
  4. package/dist/assets/channels-list-page-wK3l7p5R.js +8 -0
  5. package/dist/assets/chat-page-DgehvdJT.js +105 -0
  6. package/dist/assets/{confirm-dialog-B0TP-vPd.js → confirm-dialog-CGrKHbAW.js} +1 -1
  7. package/dist/assets/desktop-update-config-CehWxSSg.js +1 -0
  8. package/dist/assets/doc-browser-BgxmVsTx.js +1 -0
  9. package/dist/assets/doc-browser-CZn5-Qbu.js +1 -0
  10. package/dist/assets/doc-browser-context-B8gk7HMr.js +1 -0
  11. package/dist/assets/index-DORmh0Cy.css +1 -0
  12. package/dist/assets/index-DcFpvnge.js +101 -0
  13. package/dist/assets/key-round-D41Oe35x.js +1 -0
  14. package/dist/assets/mcp-marketplace-page-DBLhJWOJ.js +1 -0
  15. package/dist/assets/{mcp-marketplace-page-DDjeHd1I.js → mcp-marketplace-page-Dvi8mCBd.js} +2 -2
  16. package/dist/assets/model-config-B-EJXGtO.js +1 -0
  17. package/dist/assets/popover-YieElO8c.js +1 -0
  18. package/dist/assets/{provider-scoped-model-input-t1tCP5y5.js → provider-scoped-model-input-BPRNGqYn.js} +1 -1
  19. package/dist/assets/providers-list-CtBpL1ZY.js +1 -0
  20. package/dist/assets/remote-BGHbHoHf.js +1 -0
  21. package/dist/assets/rotate-cw-jueVCNJQ.js +1 -0
  22. package/dist/assets/runtime-config-page-C1Hv7mad.js +1 -0
  23. package/dist/assets/search-config-G2O1fcRb.js +1 -0
  24. package/dist/assets/{secrets-config-D1AUSEXX.js → secrets-config-TenzxYbE.js} +2 -2
  25. package/dist/assets/status-dot-BSMG7ZaC.js +1 -0
  26. package/dist/assets/tabs-D1CjpWNX.js +1 -0
  27. package/dist/assets/trash-2-DSWTsARW.js +1 -0
  28. package/dist/index.html +19 -13
  29. package/package.json +8 -8
  30. package/src/app/components/layout/app-layout.test.tsx +2 -1
  31. package/src/app/components/layout/app-layout.tsx +18 -4
  32. package/src/app/components/layout/sidebar.tsx +1 -1
  33. package/src/app/presenters/app.presenter.ts +6 -0
  34. package/src/features/chat/components/layout/chat-sidebar.test.tsx +1 -2
  35. package/src/features/chat/components/layout/chat-sidebar.tsx +1 -1
  36. package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +17 -13
  37. package/src/features/right-panel-resources/components/right-panel-resource-home-page.tsx +96 -0
  38. package/src/features/right-panel-resources/configs/right-panel-resource-routes.config.ts +171 -0
  39. package/src/features/right-panel-resources/index.ts +18 -0
  40. package/src/features/right-panel-resources/types/right-panel-resource.types.ts +36 -0
  41. package/src/features/right-panel-resources/utils/right-panel-resource-route-resolver.utils.test.ts +38 -0
  42. package/src/features/right-panel-resources/utils/right-panel-resource-route-resolver.utils.ts +89 -0
  43. package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.ts +48 -0
  44. package/src/features/side-dock/components/side-dock.test.tsx +58 -0
  45. package/src/features/side-dock/components/side-dock.tsx +117 -0
  46. package/src/features/side-dock/configs/side-dock-built-in-items.config.ts +43 -0
  47. package/src/features/side-dock/index.ts +10 -0
  48. package/src/features/side-dock/managers/side-dock.manager.test.ts +62 -0
  49. package/src/features/side-dock/managers/side-dock.manager.ts +58 -0
  50. package/src/features/side-dock/stores/side-dock.store.ts +42 -0
  51. package/src/features/side-dock/types/side-dock.types.ts +29 -0
  52. package/src/features/side-dock/utils/side-dock-item.utils.test.ts +77 -0
  53. package/src/features/side-dock/utils/side-dock-item.utils.ts +126 -0
  54. package/src/platforms/desktop/components/desktop-app-shell.tsx +3 -0
  55. package/src/shared/components/doc-browser/doc-browser-context.test.tsx +52 -5
  56. package/src/shared/components/doc-browser/doc-browser-context.tsx +22 -14
  57. package/src/shared/components/doc-browser/doc-browser-home-page.tsx +9 -89
  58. package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +1 -0
  59. package/src/shared/components/doc-browser/doc-browser.test.tsx +5 -3
  60. package/src/shared/components/doc-browser/doc-browser.tsx +3 -1
  61. package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +92 -43
  62. package/src/shared/components/doc-browser/types/doc-browser.types.ts +24 -0
  63. package/src/shared/components/doc-browser/utils/doc-browser-route-resolver.utils.ts +65 -0
  64. package/src/shared/components/doc-browser/utils/doc-browser-state.utils.ts +5 -4
  65. package/src/shared/lib/resource-uri/index.ts +11 -0
  66. package/src/shared/lib/resource-uri/types/resource-uri.types.ts +19 -0
  67. package/src/shared/lib/resource-uri/utils/resource-uri-resolver.utils.test.ts +49 -0
  68. package/src/shared/lib/resource-uri/utils/resource-uri-resolver.utils.ts +35 -0
  69. package/src/shared/lib/resource-uri/utils/resource-uri.utils.ts +48 -0
  70. package/dist/assets/channels-list-page-DXH1qYLs.js +0 -8
  71. package/dist/assets/chat-page-Cpo0AFHb.js +0 -105
  72. package/dist/assets/desktop-update-config-Be8x1jNy.js +0 -1
  73. package/dist/assets/doc-browser-BI7HSaMK.js +0 -3
  74. package/dist/assets/doc-browser-context-DB6TTHeE.js +0 -1
  75. package/dist/assets/doc-browser-route-registry.utils-x5ijvk0u.js +0 -1
  76. package/dist/assets/doc-browser-yTfN7UBH.js +0 -1
  77. package/dist/assets/index-GVTiCf1s.js +0 -101
  78. package/dist/assets/index-v0fAGXQW.css +0 -1
  79. package/dist/assets/mcp-marketplace-page-WUkc7n94.js +0 -1
  80. package/dist/assets/model-config-BjXMpU9U.js +0 -1
  81. package/dist/assets/providers-list-C1D5p9kK.js +0 -1
  82. package/dist/assets/remote-BHeENKxL.js +0 -1
  83. package/dist/assets/runtime-config-page--O0Wq01S.js +0 -1
  84. package/dist/assets/search-config-Dzpizo-_.js +0 -1
  85. package/src/shared/components/doc-browser/utils/doc-browser-route-registry.utils.ts +0 -232
  86. /package/dist/assets/{book-open-cPX0Za7Z.js → book-open-CRotBOF2.js} +0 -0
  87. /package/dist/assets/{config-hints-CY1lg9IE.js → config-hints-A7G_jMTU.js} +0 -0
  88. /package/dist/assets/{config-split-page-BFh_Qixx.js → config-split-page-BnTO1mAM.js} +0 -0
  89. /package/dist/assets/{external-link-C3usm3se.js → external-link-DHjFdCpy.js} +0 -0
  90. /package/dist/assets/{play-CqNi0u02.js → play-C-0vo7hi.js} +0 -0
  91. /package/dist/assets/{plus-BcIC8dG1.js → plus-Cr_akLp2.js} +0 -0
  92. /package/dist/assets/{save-D10M5vQg.js → save-aYdSdK97.js} +0 -0
  93. /package/dist/assets/{setting-row-8VkukJoH.js → setting-row-BNu0qhOM.js} +0 -0
  94. /package/dist/assets/{tabs-custom-BBXu1OGs.js → tabs-custom-CRSN1ekg.js} +0 -0
  95. /package/dist/assets/{tag-chip-ix15P-r4.js → tag-chip-DEashiw6.js} +0 -0
  96. /package/dist/assets/{x-Chb07O-Y.js → x-D1v_vb54.js} +0 -0
@@ -1,6 +1,8 @@
1
1
  import { act, renderHook, waitFor } from '@testing-library/react';
2
2
  import type { ReactNode } from 'react';
3
3
  import { DocBrowserProvider, useDocBrowser } from './doc-browser-context';
4
+ import { RightPanelResourceRouteResolver } from '@/features/right-panel-resources';
5
+ import { DocBrowserManager } from '@/shared/components/doc-browser/managers/doc-browser.manager';
4
6
  import { createDefaultDocBrowserState } from '@/shared/components/doc-browser/utils/doc-browser-state.utils';
5
7
  import { useDocBrowserStore } from '@/shared/components/doc-browser/stores/doc-browser.store';
6
8
 
@@ -13,9 +15,11 @@ vi.mock('@/shared/lib/i18n', async () => {
13
15
  });
14
16
 
15
17
  const wrapper = ({ children }: { children: ReactNode }) => (
16
- <DocBrowserProvider>{children}</DocBrowserProvider>
18
+ <DocBrowserProvider manager={testDocBrowserManager}>{children}</DocBrowserProvider>
17
19
  );
18
20
 
21
+ const testDocBrowserManager = new DocBrowserManager(new RightPanelResourceRouteResolver());
22
+
19
23
  const docBrowserStorageKey = 'nextclaw.doc-browser.state';
20
24
 
21
25
  beforeEach(() => {
@@ -175,6 +179,24 @@ describe('DocBrowserProvider dedupe keys', () => {
175
179
  });
176
180
  });
177
181
 
182
+ it('replaces the closed default start page when opening docs as a new tab', () => {
183
+ const { result } = renderHook(() => useDocBrowser(), { wrapper });
184
+
185
+ act(() => {
186
+ result.current.open(undefined, {
187
+ kind: 'docs',
188
+ newTab: true,
189
+ title: 'Help Docs',
190
+ });
191
+ });
192
+
193
+ expect(result.current.tabs).toHaveLength(1);
194
+ expect(result.current.currentTab).toMatchObject({
195
+ kind: 'docs',
196
+ title: 'Help Docs',
197
+ });
198
+ });
199
+
178
200
  it('infers built-in apps routes without component-level options', () => {
179
201
  const { result } = renderHook(() => useDocBrowser(), { wrapper });
180
202
 
@@ -190,6 +212,27 @@ describe('DocBrowserProvider dedupe keys', () => {
190
212
  });
191
213
  });
192
214
 
215
+ it('opens an already resolved panel app target without reparsing its content URL', () => {
216
+ const { result } = renderHook(() => useDocBrowser(), { wrapper });
217
+
218
+ act(() => {
219
+ result.current.openTarget({
220
+ dedupeKey: 'panel-app:demo',
221
+ historyPolicy: 'managed',
222
+ kind: 'panel-app',
223
+ title: 'Demo Panel App',
224
+ url: '/api/panel-apps/demo/content',
225
+ });
226
+ });
227
+
228
+ expect(result.current.currentTab).toMatchObject({
229
+ currentUrl: '/api/panel-apps/demo/content',
230
+ dedupeKey: 'panel-app:demo',
231
+ kind: 'panel-app',
232
+ title: 'Demo Panel App',
233
+ });
234
+ });
235
+
193
236
  it('uses managed history for non-doc routes', () => {
194
237
  const { result } = renderHook(() => useDocBrowser(), { wrapper });
195
238
 
@@ -238,7 +281,8 @@ describe('DocBrowserProvider dedupe keys', () => {
238
281
  });
239
282
 
240
283
  expect(result.current.currentTab).toMatchObject({
241
- kind: 'docs',
284
+ currentUrl: 'data:text/html,A',
285
+ kind: 'content',
242
286
  });
243
287
  expect(result.current.activeHistoryIndex).toBe(0);
244
288
 
@@ -253,7 +297,7 @@ describe('DocBrowserProvider dedupe keys', () => {
253
297
  historyIndex: 1,
254
298
  history: ['data:text/html,A', 'data:text/html,B'],
255
299
  });
256
- expect(result.current.activeHistoryIndex).toBe(2);
300
+ expect(result.current.activeHistoryIndex).toBe(1);
257
301
  });
258
302
 
259
303
  it('records active tab switching in the top-level browser history', () => {
@@ -400,7 +444,7 @@ describe('DocBrowserProvider persistence', () => {
400
444
  });
401
445
  });
402
446
 
403
- it('falls back to the default docs tab when persisted state is malformed', async () => {
447
+ it('falls back to the start page when persisted state is malformed', async () => {
404
448
  window.localStorage.setItem(docBrowserStorageKey, JSON.stringify({
405
449
  version: 1,
406
450
  state: {
@@ -416,6 +460,9 @@ describe('DocBrowserProvider persistence', () => {
416
460
  const { result } = renderHook(() => useDocBrowser(), { wrapper });
417
461
 
418
462
  expect(result.current.isOpen).toBe(false);
419
- expect(result.current.currentTab?.kind).toBe('docs');
463
+ expect(result.current.currentTab).toMatchObject({
464
+ currentUrl: 'nextclaw://new-tab',
465
+ kind: 'home',
466
+ });
420
467
  });
421
468
  });
@@ -1,5 +1,5 @@
1
1
  import { createContext, useContext, useMemo, type ReactNode } from 'react';
2
- import { DocBrowserManager } from '@/shared/components/doc-browser/managers/doc-browser.manager';
2
+ import type { DocBrowserManager } from '@/shared/components/doc-browser/managers/doc-browser.manager';
3
3
  import { useDocBrowserStore } from '@/shared/components/doc-browser/stores/doc-browser.store';
4
4
  import type { DocBrowserContextValue } from '@/shared/components/doc-browser/types/doc-browser.types';
5
5
 
@@ -8,6 +8,8 @@ export type {
8
8
  DocBrowserContextValue,
9
9
  DocBrowserMode,
10
10
  DocBrowserOpenOptions,
11
+ DocBrowserRouteResolver,
12
+ DocBrowserRouteTarget,
11
13
  DocBrowserState,
12
14
  DocBrowserStateUpdate,
13
15
  DocBrowserTab,
@@ -21,7 +23,6 @@ export {
21
23
  } from '@/shared/components/doc-browser/utils/doc-browser-url.utils';
22
24
 
23
25
  const DocBrowserContext = createContext<DocBrowserContextValue | null>(null);
24
- const docBrowserManager = new DocBrowserManager();
25
26
 
26
27
  export function useDocBrowser(): DocBrowserContextValue {
27
28
  const ctx = useContext(DocBrowserContext);
@@ -29,7 +30,13 @@ export function useDocBrowser(): DocBrowserContextValue {
29
30
  return ctx;
30
31
  }
31
32
 
32
- export function DocBrowserProvider({ children }: { children: ReactNode }) {
33
+ export function DocBrowserProvider({
34
+ children,
35
+ manager,
36
+ }: {
37
+ children: ReactNode;
38
+ manager: DocBrowserManager;
39
+ }) {
33
40
  const snapshot = useDocBrowserStore((state) => state.snapshot);
34
41
  const currentTab = useMemo(() => {
35
42
  return snapshot.tabs.find((tab) => tab.id === snapshot.activeTabId) ?? snapshot.tabs[0];
@@ -38,17 +45,18 @@ export function DocBrowserProvider({ children }: { children: ReactNode }) {
38
45
  const value = useMemo<DocBrowserContextValue>(() => ({
39
46
  ...snapshot,
40
47
  currentTab,
41
- open: docBrowserManager.open,
42
- openNewTab: docBrowserManager.openNewTab,
43
- close: docBrowserManager.close,
44
- toggleMode: docBrowserManager.toggleMode,
45
- navigate: docBrowserManager.navigate,
46
- syncUrl: docBrowserManager.syncUrl,
47
- goBack: docBrowserManager.goBack,
48
- goForward: docBrowserManager.goForward,
49
- closeTab: docBrowserManager.closeTab,
50
- setActiveTab: docBrowserManager.setActiveTab,
51
- }), [currentTab, snapshot]);
48
+ open: manager.open,
49
+ openTarget: manager.openTarget,
50
+ openNewTab: manager.openNewTab,
51
+ close: manager.close,
52
+ toggleMode: manager.toggleMode,
53
+ navigate: manager.navigate,
54
+ syncUrl: manager.syncUrl,
55
+ goBack: manager.goBack,
56
+ goForward: manager.goForward,
57
+ closeTab: manager.closeTab,
58
+ setActiveTab: manager.setActiveTab,
59
+ }), [currentTab, manager, snapshot]);
52
60
 
53
61
  return (
54
62
  <DocBrowserContext.Provider value={value}>
@@ -1,94 +1,14 @@
1
- import type { ReactNode } from 'react';
2
- import {
3
- BookOpen,
4
- Boxes,
5
- BrainCircuit,
6
- Grid3X3,
7
- Wrench,
8
- } from 'lucide-react';
9
- import { useNavigate } from 'react-router-dom';
10
- import type { DocBrowserContextValue } from './doc-browser-context';
11
- import {
12
- docBrowserRouteRegistry,
13
- type DocBrowserHomeNavigationItem,
14
- type DocBrowserNavigationTarget,
15
- } from './utils/doc-browser-route-registry.utils';
16
- import { cn } from '@/shared/lib/utils';
17
-
18
- type DocBrowserHomePageProps = {
19
- open: DocBrowserContextValue['open'];
20
- };
21
-
22
- type DocBrowserHomeItem = {
23
- accentClassName: string;
24
- icon: ReactNode;
25
- };
26
-
27
- const HOME_ITEM_VIEW_BY_ID: Record<string, DocBrowserHomeItem> = {
28
- apps: {
29
- accentClassName: 'bg-emerald-50 text-emerald-700 border-emerald-100',
30
- icon: <Boxes className="h-5 w-5" />,
31
- },
32
- 'service-apps': {
33
- accentClassName: 'bg-sky-50 text-sky-700 border-sky-100',
34
- icon: <Grid3X3 className="h-5 w-5" />,
35
- },
36
- docs: {
37
- accentClassName: 'bg-amber-50 text-amber-700 border-amber-100',
38
- icon: <BookOpen className="h-5 w-5" />,
39
- },
40
- 'skill-marketplace': {
41
- accentClassName: 'bg-rose-50 text-rose-700 border-rose-100',
42
- icon: <BrainCircuit className="h-5 w-5" />,
43
- },
44
- 'mcp-marketplace': {
45
- accentClassName: 'bg-indigo-50 text-indigo-700 border-indigo-100',
46
- icon: <Wrench className="h-5 w-5" />,
47
- },
48
- };
49
-
50
- function resolveHomeItemView(item: DocBrowserHomeNavigationItem): DocBrowserHomeItem {
51
- return HOME_ITEM_VIEW_BY_ID[item.id] ?? {
52
- accentClassName: 'bg-gray-50 text-gray-700 border-gray-100',
53
- icon: <Boxes className="h-5 w-5" />,
54
- };
55
- }
56
-
57
- export function DocBrowserHomePage({ open }: DocBrowserHomePageProps) {
58
- const navigate = useNavigate();
59
- const items = docBrowserRouteRegistry.getHomeNavigationItems();
60
-
61
- const openTarget = (target: DocBrowserNavigationTarget) => {
62
- if (target.type === 'app-route') {
63
- navigate(target.path);
64
- return;
65
- }
66
- open(target.url, target.options);
67
- };
1
+ import { Boxes } from 'lucide-react';
2
+ import { t } from '@/shared/lib/i18n';
68
3
 
4
+ export function DocBrowserHomePage() {
69
5
  return (
70
- <div className="h-full overflow-auto bg-[#fbfaf7] px-5 py-6">
71
- <div className="mx-auto w-full max-w-[640px]">
72
- <div className="grid grid-cols-[repeat(auto-fit,minmax(88px,1fr))] gap-x-3 gap-y-5">
73
- {items.map((item) => {
74
- const view = resolveHomeItemView(item);
75
- return (
76
- <button
77
- key={item.id}
78
- type="button"
79
- onClick={() => openTarget(item.target)}
80
- className="group flex min-h-[82px] flex-col items-center gap-2 rounded-lg px-2 py-2 text-center transition-colors hover:bg-white/80"
81
- >
82
- <span className={cn('flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl border shadow-[0_1px_2px_rgba(30,20,10,0.04)] transition-transform group-hover:scale-[1.03]', view.accentClassName)}>
83
- {view.icon}
84
- </span>
85
- <span className="min-w-0 max-w-full">
86
- <span className="line-clamp-2 block text-xs font-medium leading-snug text-gray-800">{item.label}</span>
87
- </span>
88
- </button>
89
- );
90
- })}
91
- </div>
6
+ <div className="flex h-full items-center justify-center bg-background px-5 py-6">
7
+ <div className="flex max-w-[260px] flex-col items-center gap-3 text-center">
8
+ <span className="flex h-12 w-12 items-center justify-center rounded-lg border border-gray-100 bg-gray-50 text-gray-600 shadow-[0_1px_2px_rgba(30,20,10,0.04)]">
9
+ <Boxes className="h-5 w-5" />
10
+ </span>
11
+ <span className="text-sm font-medium text-gray-800">{t('docBrowserHomeTitle')}</span>
92
12
  </div>
93
13
  </div>
94
14
  );
@@ -4,6 +4,7 @@ import type { DocBrowserContextValue, DocBrowserTab } from './doc-browser-contex
4
4
  export type DocBrowserCustomTabRenderParams = {
5
5
  currentUrl: string;
6
6
  open: DocBrowserContextValue['open'];
7
+ openTarget: DocBrowserContextValue['openTarget'];
7
8
  refreshIframe: () => void;
8
9
  tab: DocBrowserTab;
9
10
  };
@@ -2,6 +2,7 @@ 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
4
  import type { DocBrowserContextValue, DocBrowserTab } from "@/shared/components/doc-browser/doc-browser-context";
5
+ import { PANEL_APPS_DOC_BROWSER_RENDERERS } from "@/features/panel-apps";
5
6
 
6
7
  const { navigateMock } = vi.hoisted(() => ({
7
8
  navigateMock: vi.fn(),
@@ -44,6 +45,7 @@ const { docBrowserState } = vi.hoisted<{ docBrowserState: DocBrowserContextValue
44
45
  navVersion: 0,
45
46
  },
46
47
  open: vi.fn(),
48
+ openTarget: vi.fn(),
47
49
  openNewTab: vi.fn(),
48
50
  close: vi.fn(),
49
51
  toggleMode: vi.fn(),
@@ -138,7 +140,7 @@ describe("DocBrowser", () => {
138
140
  });
139
141
 
140
142
  it("keeps browser window controls on the tab strip", () => {
141
- render(<DocBrowser />);
143
+ render(<DocBrowser customTabRenderers={PANEL_APPS_DOC_BROWSER_RENDERERS} />);
142
144
 
143
145
  const tabStrip = screen.getByTestId("doc-browser-tab-strip");
144
146
  const tabActions = screen.getByTestId("doc-browser-tab-actions");
@@ -176,7 +178,7 @@ describe("DocBrowser", () => {
176
178
  docBrowserState.activeHistoryIndex = 1;
177
179
  docBrowserState.currentTab = historyTab;
178
180
 
179
- render(<DocBrowser />);
181
+ render(<DocBrowser customTabRenderers={PANEL_APPS_DOC_BROWSER_RENDERERS} />);
180
182
 
181
183
  fireEvent.click(screen.getByTitle("Back"));
182
184
  fireEvent.click(screen.getByTitle("Forward"));
@@ -249,7 +251,7 @@ describe("DocBrowser", () => {
249
251
  docBrowserState.activeTabId = homeTab.id;
250
252
  docBrowserState.currentTab = homeTab;
251
253
 
252
- render(<DocBrowser />);
254
+ render(<DocBrowser customTabRenderers={PANEL_APPS_DOC_BROWSER_RENDERERS} />);
253
255
 
254
256
  expect(screen.getAllByText("Start Page").length).toBeGreaterThan(0);
255
257
  expect(screen.getByText("Apps")).toBeTruthy();
@@ -62,6 +62,7 @@ export function DocBrowser({
62
62
  activeHistoryIndex,
63
63
  currentTab,
64
64
  open,
65
+ openTarget,
65
66
  openNewTab,
66
67
  close,
67
68
  toggleMode,
@@ -255,12 +256,13 @@ export function DocBrowser({
255
256
  const customRenderParams = currentTab ? {
256
257
  currentUrl,
257
258
  open,
259
+ openTarget,
258
260
  refreshIframe,
259
261
  tab: currentTab,
260
262
  } : undefined;
261
263
  const customToolbar = customRenderParams ? customRenderer?.renderToolbar?.(customRenderParams) : null;
262
264
  const customContent = customRenderParams
263
- ? customRenderer?.renderContent?.(customRenderParams) ?? (isHomeTab ? <DocBrowserHomePage open={open} /> : null)
265
+ ? customRenderer?.renderContent?.(customRenderParams) ?? (isHomeTab ? <DocBrowserHomePage /> : null)
264
266
  : null;
265
267
  const iframeSandbox = currentTab
266
268
  ? customRenderer?.getIframeSandbox?.(currentTab) ?? 'allow-same-origin allow-scripts allow-popups allow-forms'
@@ -1,10 +1,11 @@
1
1
  import type {
2
2
  DocBrowserActiveHistoryEntry,
3
3
  DocBrowserOpenOptions,
4
+ DocBrowserRouteResolver,
5
+ DocBrowserRouteTarget,
4
6
  DocBrowserState,
5
7
  DocBrowserStateUpdate,
6
8
  DocBrowserTab,
7
- DocBrowserTabKind,
8
9
  } from '@/shared/components/doc-browser/types/doc-browser.types';
9
10
  import {
10
11
  createDocBrowserActiveHistoryEntry,
@@ -13,13 +14,14 @@ import {
13
14
  } from '@/shared/components/doc-browser/utils/doc-browser-state.utils';
14
15
  import {
15
16
  DOC_BROWSER_HOME_TAB_KIND,
17
+ DOC_BROWSER_HOME_URL,
16
18
  getDefaultDocsUrl,
17
19
  } from '@/shared/components/doc-browser/utils/doc-browser-url.utils';
18
- import {
19
- docBrowserRouteRegistry,
20
- } from '@/shared/components/doc-browser/utils/doc-browser-route-registry.utils';
20
+ import { DefaultDocBrowserRouteResolver } from '@/shared/components/doc-browser/utils/doc-browser-route-resolver.utils';
21
21
  import { useDocBrowserStore } from '@/shared/components/doc-browser/stores/doc-browser.store';
22
22
 
23
+ const defaultDocBrowserRouteResolver = new DefaultDocBrowserRouteResolver();
24
+
23
25
  function updateTab(
24
26
  state: DocBrowserState,
25
27
  tabId: string,
@@ -44,17 +46,22 @@ function appendManualNavigation(tab: DocBrowserTab, url: string): Pick<DocBrowse
44
46
  }
45
47
 
46
48
  function areActiveHistoryEntriesEquivalent(
49
+ routeResolver: DocBrowserRouteResolver,
47
50
  current: DocBrowserActiveHistoryEntry,
48
51
  next: DocBrowserActiveHistoryEntry,
49
52
  ): boolean {
50
53
  return current.tabId === next.tabId
51
54
  && current.kind === next.kind
52
- && docBrowserRouteRegistry.areUrlsEquivalent(current.url, next.url, current.kind, next.kind);
55
+ && routeResolver.areUrlsEquivalent(current.url, next.url, current.kind, next.kind);
53
56
  }
54
57
 
55
- function pushActiveHistory(state: DocBrowserState, entry: DocBrowserActiveHistoryEntry): DocBrowserState {
58
+ function pushActiveHistory(
59
+ routeResolver: DocBrowserRouteResolver,
60
+ state: DocBrowserState,
61
+ entry: DocBrowserActiveHistoryEntry,
62
+ ): DocBrowserState {
56
63
  const current = state.activeHistory[state.activeHistoryIndex];
57
- if (current && areActiveHistoryEntriesEquivalent(current, entry)) {
64
+ if (current && areActiveHistoryEntriesEquivalent(routeResolver, current, entry)) {
58
65
  return state;
59
66
  }
60
67
  const activeHistory = [...state.activeHistory.slice(0, state.activeHistoryIndex + 1), entry];
@@ -65,26 +72,30 @@ function pushActiveHistory(state: DocBrowserState, entry: DocBrowserActiveHistor
65
72
  };
66
73
  }
67
74
 
68
- function findTabHistoryIndex(tab: DocBrowserTab, url: string): number {
75
+ function findTabHistoryIndex(routeResolver: DocBrowserRouteResolver, tab: DocBrowserTab, url: string): number {
69
76
  for (let index = tab.history.length - 1; index >= 0; index -= 1) {
70
- if (docBrowserRouteRegistry.areUrlsEquivalent(tab.history[index], url, tab.kind, tab.kind)) {
77
+ if (routeResolver.areUrlsEquivalent(tab.history[index], url, tab.kind, tab.kind)) {
71
78
  return index;
72
79
  }
73
80
  }
74
81
  return -1;
75
82
  }
76
83
 
77
- function restoreActiveHistoryEntry(state: DocBrowserState, entry: DocBrowserActiveHistoryEntry): DocBrowserState {
84
+ function restoreActiveHistoryEntry(
85
+ routeResolver: DocBrowserRouteResolver,
86
+ state: DocBrowserState,
87
+ entry: DocBrowserActiveHistoryEntry,
88
+ ): DocBrowserState {
78
89
  const tab = state.tabs.find((item) => item.id === entry.tabId);
79
90
  if (!tab) {
80
91
  return state;
81
92
  }
82
- const target = docBrowserRouteRegistry.resolveOpenTarget({
93
+ const target = routeResolver.resolveOpenTarget({
83
94
  activeTab: tab,
84
95
  kind: entry.kind,
85
96
  url: entry.url,
86
97
  });
87
- const historyIndex = findTabHistoryIndex(tab, target.url);
98
+ const historyIndex = findTabHistoryIndex(routeResolver, tab, target.url);
88
99
  return updateTab(
89
100
  {
90
101
  ...state,
@@ -104,12 +115,9 @@ function restoreActiveHistoryEntry(state: DocBrowserState, entry: DocBrowserActi
104
115
  }
105
116
 
106
117
  function updateTabForOpen(
118
+ routeResolver: DocBrowserRouteResolver,
107
119
  tab: DocBrowserTab,
108
- target: {
109
- kind: DocBrowserTabKind;
110
- title: string;
111
- url: string;
112
- },
120
+ target: DocBrowserRouteTarget,
113
121
  options?: DocBrowserOpenOptions,
114
122
  dedupeKey?: string,
115
123
  ): DocBrowserTab {
@@ -120,7 +128,7 @@ function updateTabForOpen(
120
128
  dedupeKey,
121
129
  };
122
130
 
123
- if (docBrowserRouteRegistry.areUrlsEquivalent(tab.currentUrl, target.url, tab.kind, target.kind)) {
131
+ if (routeResolver.areUrlsEquivalent(tab.currentUrl, target.url, tab.kind, target.kind)) {
124
132
  return baseTab;
125
133
  }
126
134
 
@@ -132,25 +140,27 @@ function updateTabForOpen(
132
140
  };
133
141
  }
134
142
 
135
- function openDocBrowserState(
143
+ function isClosedDefaultHomeState(state: DocBrowserState, activeTab?: DocBrowserTab): boolean {
144
+ return !state.isOpen
145
+ && state.tabs.length === 1
146
+ && activeTab?.kind === DOC_BROWSER_HOME_TAB_KIND
147
+ && activeTab.currentUrl === DOC_BROWSER_HOME_URL;
148
+ }
149
+
150
+ function openResolvedDocBrowserState(
151
+ routeResolver: DocBrowserRouteResolver,
136
152
  prev: DocBrowserState,
137
- url?: string,
153
+ target: DocBrowserRouteTarget,
138
154
  options?: DocBrowserOpenOptions,
139
155
  ): DocBrowserState {
140
156
  const {
141
157
  activate,
142
158
  dedupeKey: rawDedupeKey,
143
- kind,
144
159
  newTab: shouldForceNewTab,
145
160
  title,
146
161
  } = options ?? {};
147
- const activeTab = prev.tabs.find((tab) => tab.id === prev.activeTabId) ?? prev.tabs[0];
148
- const target = docBrowserRouteRegistry.resolveOpenTarget({
149
- activeTab,
150
- kind,
151
- url,
152
- });
153
162
  const dedupeKey = rawDedupeKey?.trim() || target.dedupeKey;
163
+ const activeTab = prev.tabs.find((tab) => tab.id === prev.activeTabId) ?? prev.tabs[0];
154
164
  const matchedTab = dedupeKey
155
165
  ? prev.tabs.find((tab) => tab.dedupeKey === dedupeKey)
156
166
  : undefined;
@@ -159,7 +169,7 @@ function openDocBrowserState(
159
169
  const next = updateTab(
160
170
  prev,
161
171
  matchedTab.id,
162
- (tab) => updateTabForOpen(tab, target, options, dedupeKey),
172
+ (tab) => updateTabForOpen(routeResolver, tab, target, options, dedupeKey),
163
173
  );
164
174
  const shouldActivate = activate !== false;
165
175
  const activeTabId = shouldActivate ? matchedTab.id : prev.activeTabId;
@@ -170,38 +180,76 @@ function openDocBrowserState(
170
180
  };
171
181
  const shouldPushActiveHistory = shouldActivate || matchedTab.id === prev.activeTabId;
172
182
  return shouldPushActiveHistory
173
- ? pushActiveHistory(nextState, { kind: target.kind, tabId: matchedTab.id, url: target.url })
183
+ ? pushActiveHistory(routeResolver, nextState, { kind: target.kind, tabId: matchedTab.id, url: target.url })
174
184
  : nextState;
175
185
  }
176
186
 
177
187
  if (shouldForceNewTab || dedupeKey || !activeTab || activeTab.kind !== target.kind) {
178
188
  const newTab = createDocBrowserTab(target.url, target.kind, title ?? target.title, dedupeKey);
189
+ if (isClosedDefaultHomeState(prev, activeTab)) {
190
+ const nextState = {
191
+ ...prev,
192
+ isOpen: true,
193
+ tabs: [newTab],
194
+ activeTabId: newTab.id,
195
+ activeHistory: [createDocBrowserActiveHistoryEntry(newTab)],
196
+ activeHistoryIndex: 0,
197
+ };
198
+ return nextState;
199
+ }
179
200
  const nextState = {
180
201
  ...prev,
181
202
  isOpen: true,
182
203
  tabs: [...prev.tabs, newTab],
183
204
  activeTabId: newTab.id,
184
205
  };
185
- return pushActiveHistory(nextState, createDocBrowserActiveHistoryEntry(newTab));
206
+ return pushActiveHistory(routeResolver, nextState, createDocBrowserActiveHistoryEntry(newTab));
186
207
  }
187
208
 
188
209
  const next = updateActiveTab(
189
210
  prev,
190
- (tab) => updateTabForOpen(tab, target, options, dedupeKey),
211
+ (tab) => updateTabForOpen(routeResolver, tab, target, options, dedupeKey),
191
212
  );
192
213
  return pushActiveHistory(
214
+ routeResolver,
193
215
  { ...next, isOpen: true },
194
216
  { kind: target.kind, tabId: prev.activeTabId, url: target.url },
195
217
  );
196
218
  }
197
219
 
220
+ function openDocBrowserState(
221
+ routeResolver: DocBrowserRouteResolver,
222
+ prev: DocBrowserState,
223
+ url?: string,
224
+ options?: DocBrowserOpenOptions,
225
+ ): DocBrowserState {
226
+ const activeTab = prev.tabs.find((tab) => tab.id === prev.activeTabId) ?? prev.tabs[0];
227
+ const target = routeResolver.resolveOpenTarget({
228
+ activeTab,
229
+ kind: options?.kind,
230
+ url,
231
+ });
232
+ return openResolvedDocBrowserState(routeResolver, prev, target, options);
233
+ }
234
+
198
235
  export class DocBrowserManager {
236
+ constructor(private readonly routeResolver: DocBrowserRouteResolver = defaultDocBrowserRouteResolver) {}
237
+
199
238
  private readonly setSnapshot = (next: DocBrowserStateUpdate): void => {
200
239
  useDocBrowserStore.getState().setSnapshot(next);
201
240
  };
202
241
 
203
242
  readonly open = (url?: string, options?: DocBrowserOpenOptions): void => {
204
- this.setSnapshot((prev) => openDocBrowserState(prev, url, options));
243
+ this.setSnapshot((prev) => openDocBrowserState(this.routeResolver, prev, url, options));
244
+ };
245
+
246
+ readonly openTarget = (target: DocBrowserRouteTarget, options?: DocBrowserOpenOptions): void => {
247
+ this.setSnapshot((prev) => openResolvedDocBrowserState(this.routeResolver, prev, target, {
248
+ ...options,
249
+ dedupeKey: options?.dedupeKey ?? target.dedupeKey,
250
+ kind: options?.kind ?? target.kind,
251
+ title: options?.title ?? target.title,
252
+ }));
205
253
  };
206
254
 
207
255
  readonly openNewTab = (): void => {
@@ -229,12 +277,12 @@ export class DocBrowserManager {
229
277
  }
230
278
 
231
279
  const next = updateActiveTab(prev, (tab) => {
232
- if (!docBrowserRouteRegistry.usesManagedHistory(tab)) {
280
+ if (!this.routeResolver.usesManagedHistory(tab)) {
233
281
  return tab;
234
282
  }
235
- const target = docBrowserRouteRegistry.resolveOpenTarget({ activeTab: tab, url });
283
+ const target = this.routeResolver.resolveOpenTarget({ activeTab: tab, url });
236
284
 
237
- if (docBrowserRouteRegistry.areUrlsEquivalent(tab.currentUrl, target.url, tab.kind, target.kind)) {
285
+ if (this.routeResolver.areUrlsEquivalent(tab.currentUrl, target.url, tab.kind, target.kind)) {
238
286
  return tab;
239
287
  }
240
288
 
@@ -250,7 +298,7 @@ export class DocBrowserManager {
250
298
  });
251
299
  const currentTab = next.tabs.find((tab) => tab.id === next.activeTabId);
252
300
  return currentTab
253
- ? pushActiveHistory(next, createDocBrowserActiveHistoryEntry(currentTab))
301
+ ? pushActiveHistory(this.routeResolver, next, createDocBrowserActiveHistoryEntry(currentTab))
254
302
  : next;
255
303
  });
256
304
  };
@@ -267,12 +315,12 @@ export class DocBrowserManager {
267
315
  }
268
316
 
269
317
  const next = updateActiveTab(prev, (tab) => {
270
- if (!docBrowserRouteRegistry.usesManagedHistory(tab)) {
318
+ if (!this.routeResolver.usesManagedHistory(tab)) {
271
319
  return tab;
272
320
  }
273
- const target = docBrowserRouteRegistry.resolveOpenTarget({ activeTab: tab, kind: tab.kind, url });
321
+ const target = this.routeResolver.resolveOpenTarget({ activeTab: tab, kind: tab.kind, url });
274
322
 
275
- if (docBrowserRouteRegistry.areUrlsEquivalent(tab.currentUrl, target.url, tab.kind, target.kind)) {
323
+ if (this.routeResolver.areUrlsEquivalent(tab.currentUrl, target.url, tab.kind, target.kind)) {
276
324
  return tab;
277
325
  }
278
326
 
@@ -287,7 +335,7 @@ export class DocBrowserManager {
287
335
  });
288
336
  const currentTab = next.tabs.find((tab) => tab.id === next.activeTabId);
289
337
  return currentTab
290
- ? pushActiveHistory(next, createDocBrowserActiveHistoryEntry(currentTab))
338
+ ? pushActiveHistory(this.routeResolver, next, createDocBrowserActiveHistoryEntry(currentTab))
291
339
  : next;
292
340
  });
293
341
  };
@@ -302,7 +350,7 @@ export class DocBrowserManager {
302
350
  if (!entry) {
303
351
  return prev;
304
352
  }
305
- return restoreActiveHistoryEntry({ ...prev, activeHistoryIndex }, entry);
353
+ return restoreActiveHistoryEntry(this.routeResolver, { ...prev, activeHistoryIndex }, entry);
306
354
  });
307
355
  };
308
356
 
@@ -316,7 +364,7 @@ export class DocBrowserManager {
316
364
  if (!entry) {
317
365
  return prev;
318
366
  }
319
- return restoreActiveHistoryEntry({ ...prev, activeHistoryIndex }, entry);
367
+ return restoreActiveHistoryEntry(this.routeResolver, { ...prev, activeHistoryIndex }, entry);
320
368
  });
321
369
  };
322
370
 
@@ -361,7 +409,7 @@ export class DocBrowserManager {
361
409
  ),
362
410
  };
363
411
  return prev.activeTabId === tabId
364
- ? pushActiveHistory(reconciledState, createDocBrowserActiveHistoryEntry(nextActiveTab))
412
+ ? pushActiveHistory(this.routeResolver, reconciledState, createDocBrowserActiveHistoryEntry(nextActiveTab))
365
413
  : reconciledState;
366
414
  });
367
415
  };
@@ -376,6 +424,7 @@ export class DocBrowserManager {
376
424
  return prev;
377
425
  }
378
426
  return pushActiveHistory(
427
+ this.routeResolver,
379
428
  { ...prev, activeTabId: tabId, isOpen: true },
380
429
  createDocBrowserActiveHistoryEntry(tab),
381
430
  );