@nextclaw/ui 0.14.4 → 0.15.0
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 +54 -0
- package/dist/assets/api-DLtsiBKm.js +13 -0
- package/dist/assets/appearance-settings-page-CtkeYXux.js +1 -0
- package/dist/assets/book-open-BQwWBDKb.js +1 -0
- package/dist/assets/channels-list-page-Cv8o9Hxg.js +8 -0
- package/dist/assets/chat-page-BtA7H1Ea.js +1 -0
- package/dist/assets/{config-split-page-CRhlZ_pH.js → config-split-page-EidRBpP2.js} +1 -1
- package/dist/assets/confirm-dialog-iIJFFp2T.js +5 -0
- package/dist/assets/desktop-update-config-C6n_TB1r.js +1 -0
- package/dist/assets/{dist-CikkhOlq.js → dist-CBxWOazz.js} +1 -1
- package/dist/assets/{dist-PVA_xL1D.js → dist-D2eZC3OR.js} +1 -1
- package/dist/assets/doc-browser-B76XOXmW.js +1 -0
- package/dist/assets/doc-browser-DBnj9UMB.js +1 -0
- package/dist/assets/{doc-browser-context-MTw4EvKd.js → doc-browser-context-Sv-044Cl.js} +1 -1
- package/dist/assets/{ellipsis-y6vDtq4M.js → ellipsis-2uzhypw5.js} +1 -1
- package/dist/assets/es2015-DhQo_oS6.js +41 -0
- package/dist/assets/{external-link-DVJIQf_G.js → external-link-CM5dnUI6.js} +1 -1
- package/dist/assets/host-capabilities-9bB_998Z.js +1 -0
- package/dist/assets/i18n-provider-di1waTGX.js +1 -0
- package/dist/assets/index-CBRP-7-a.css +1 -0
- package/dist/assets/index-Z1fYpbNd.js +104 -0
- package/dist/assets/loader-circle-BjmN5DeJ.js +1 -0
- package/dist/assets/mcp-marketplace-page-C_MVQUbT.js +9 -0
- package/dist/assets/mcp-marketplace-page-CoZIiD6G.js +1 -0
- package/dist/assets/model-config-page-Cdr9P6TQ.js +1 -0
- package/dist/assets/plus-3ztRZD_l.js +1 -0
- package/dist/assets/provider-scoped-model-input-g1WkGiJ8.js +1 -0
- package/dist/assets/providers-config-page-CWlO7J4A.js +1 -0
- package/dist/assets/{react-VNqT5Zp7.js → react-DjpzoZXB.js} +1 -1
- package/dist/assets/refresh-cw-DaTv3zxE.js +1 -0
- package/dist/assets/remote-CKWZfVVB.js +1 -0
- package/dist/assets/rotate-cw-CHcaRUDz.js +1 -0
- package/dist/assets/runtime-config-page-CfTqh1nv.js +1 -0
- package/dist/assets/{save-BOJ477H-.js → save-BZof1uie.js} +1 -1
- package/dist/assets/{search-BEti5beA.js → search-MxAarS_g.js} +1 -1
- package/dist/assets/search-config-page-Dm8daYny.js +1 -0
- package/dist/assets/{secrets-config-page--IzrIOid.js → secrets-config-page-D8AbcR05.js} +2 -2
- package/dist/assets/security-config-Ds0LsJmo.js +1 -0
- package/dist/assets/select-D7GcNP0z.js +1 -0
- package/dist/assets/setting-row-CuozlnJd.js +1 -0
- package/dist/assets/side-dock-BcLau5S_.js +3 -0
- package/dist/assets/skeleton-shfmXuYp.js +1 -0
- package/dist/assets/switch-C1BWMU5c.js +1 -0
- package/dist/assets/tag-chip-DCUrQUCP.js +1 -0
- package/dist/assets/tooltip-DfAHHA76.js +1 -0
- package/dist/assets/use-config-DAdbT5QJ.js +1 -0
- package/dist/assets/use-confirm-dialog-eWcJ6V-s.js +1 -0
- package/dist/assets/x-DqL1mYLC.js +1 -0
- package/dist/index.html +53 -17
- package/index.html +25 -2
- package/package.json +9 -9
- package/src/app/components/layout/__tests__/app-layout.test.tsx +27 -0
- package/src/app/components/layout/__tests__/settings-entry-page.test.tsx +9 -1
- package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +6 -3
- package/src/app/components/layout/app-layout.tsx +10 -3
- package/src/app/components/layout/sidebar-items.tsx +8 -8
- package/src/app/components/layout/sidebar-rail.styles.ts +4 -2
- package/src/app/components/layout/sidebar.tsx +21 -28
- package/src/app/configs/app-navigation.config.ts +11 -5
- package/src/app/index.tsx +8 -0
- package/src/app/managers/__tests__/viewport-layout.manager.test.ts +59 -0
- package/src/app/managers/viewport-layout.manager.ts +39 -0
- package/src/app/presenters/app.presenter.ts +25 -2
- package/src/app/styles/design-system.css +91 -4
- package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx +86 -0
- package/src/features/chat/components/conversation/chat-conversation-content.tsx +47 -17
- package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +7 -0
- package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +9 -5
- package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +5 -5
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +51 -2
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +170 -1
- package/src/features/chat/features/conversation/components/session-conversation-area.tsx +23 -1
- package/src/features/chat/features/conversation/components/session-conversation-input.tsx +24 -7
- package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +96 -0
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +181 -0
- package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +351 -89
- package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +29 -4
- package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +28 -3
- package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +108 -8
- package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +50 -14
- package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +77 -6
- package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +21 -12
- package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +166 -0
- package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +19 -11
- package/src/features/chat/features/message/components/chat-message-list.container.tsx +38 -64
- package/src/features/chat/features/message/utils/__tests__/chat-message-process-summary.utils.test.ts +59 -0
- package/src/features/chat/features/message/utils/__tests__/chat-message-show-content-tool-card.utils.test.ts +56 -1
- package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +0 -2
- package/src/features/chat/features/message/utils/chat-message-process-summary.utils.ts +80 -0
- package/src/features/chat/features/message/utils/chat-message-show-content-tool-card.utils.ts +12 -3
- package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +60 -0
- package/src/features/chat/features/message/utils/chat-message.utils.ts +3 -0
- package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime.test.tsx +39 -0
- package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +2 -2
- package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +1 -1
- package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +3 -1
- package/src/features/chat/features/session/components/session-header/chat-session-metadata-dialog.tsx +1 -1
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +270 -115
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumb-browser.tsx +121 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +75 -20
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +70 -5
- package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +83 -0
- package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +79 -1
- package/src/features/chat/managers/__tests__/chat-ui.manager.test.ts +2 -0
- package/src/features/chat/managers/chat-thread.manager.ts +9 -3
- package/src/features/chat/managers/chat-ui.manager.ts +5 -1
- package/src/features/chat/presenters/chat.presenter.ts +2 -1
- package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +4 -0
- package/src/features/chat/stores/chat-thread.store.ts +6 -64
- package/src/features/chat/types/chat-message.types.ts +4 -0
- package/src/features/marketplace/components/__tests__/marketplace-page-detail.test.tsx +33 -26
- package/src/features/marketplace/components/__tests__/marketplace-page.test.tsx +1 -0
- package/src/features/marketplace/components/curated-shelves/__tests__/marketplace-curated-scene-route.test.tsx +1 -0
- package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.tsx +115 -0
- package/src/features/marketplace/components/marketplace-detail-doc.tsx +441 -0
- package/src/features/marketplace/components/mcp/__tests__/mcp-marketplace-page.test.tsx +3 -3
- package/src/features/marketplace/components/mcp/mcp-marketplace-doc.ts +0 -60
- package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +64 -29
- package/src/features/marketplace/hooks/use-marketplace-item-detail.ts +42 -31
- package/src/features/marketplace/index.ts +3 -0
- package/src/features/marketplace/stores/marketplace-detail-doc.store.ts +38 -0
- package/src/features/panel-apps/components/__tests__/panel-apps-list.test.tsx +32 -2
- package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +11 -0
- package/src/features/pwa/managers/pwa-shell-theme.manager.ts +8 -3
- package/src/features/settings/pages/__tests__/appearance-settings-page.test.tsx +25 -0
- package/src/features/settings/pages/appearance-settings-page.tsx +40 -0
- package/src/features/side-dock/components/__tests__/side-dock.test.tsx +44 -2
- package/src/features/side-dock/components/side-dock.tsx +100 -13
- package/src/features/side-dock/configs/side-dock-built-in-items.config.ts +10 -0
- package/src/features/side-dock/managers/__tests__/side-dock.manager.test.ts +32 -2
- package/src/features/side-dock/managers/side-dock.manager.ts +17 -7
- package/src/features/side-dock/stores/side-dock.store.ts +9 -0
- package/src/features/side-dock/types/side-dock.types.ts +10 -3
- package/src/features/side-dock/utils/side-dock-item.utils.ts +15 -2
- package/src/index.css +501 -27
- package/src/platforms/desktop/components/desktop-window-chrome.tsx +1 -1
- package/src/shared/components/doc-browser/__tests__/doc-browser-context.test.tsx +30 -1
- package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +206 -38
- package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +34 -11
- package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +1 -1
- package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +23 -5
- package/src/shared/components/doc-browser/doc-browser.tsx +128 -75
- package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +9 -1
- package/src/shared/components/doc-browser/utils/doc-browser-floating-panel.utils.ts +83 -0
- package/src/shared/components/ui/__tests__/dialog.test.tsx +27 -0
- package/src/shared/components/ui/actions/icon-action-button.tsx +14 -2
- package/src/shared/components/ui/dialog.tsx +2 -2
- package/src/shared/components/ui/popover.tsx +1 -1
- package/src/shared/components/ui/select.tsx +1 -1
- package/src/shared/components/ui/switch.tsx +4 -3
- package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +54 -0
- package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +4 -5
- package/src/shared/components/ui/tooltip.tsx +1 -1
- package/src/shared/lib/api/utils/server-path.utils.ts +27 -1
- package/src/shared/lib/i18n/locales/en-US/chat.json +15 -6
- package/src/shared/lib/i18n/locales/en-US/core.json +2 -0
- package/src/shared/lib/i18n/locales/en-US/doc-browser.json +11 -0
- package/src/shared/lib/i18n/locales/en-US/marketplace.json +5 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +15 -6
- package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -0
- package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +11 -0
- package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +5 -0
- package/src/shared/lib/session-project/__tests__/workspace-file-breadcrumb.test.ts +14 -0
- package/src/shared/lib/session-project/index.ts +1 -1
- package/src/shared/lib/session-project/{workspace-file-breadcrumb.ts → workspace-file-breadcrumb.utils.ts} +62 -8
- package/src/shared/lib/theme/index.ts +51 -13
- package/dist/assets/api-BP_8IaxB.js +0 -13
- package/dist/assets/channels-list-page-DEh5Jjqx.js +0 -8
- package/dist/assets/chat-page-BRB0N5WW.js +0 -105
- package/dist/assets/confirm-dialog-DsivFJ4r.js +0 -5
- package/dist/assets/desktop-update-config-B0w9p2ep.js +0 -1
- package/dist/assets/doc-browser-BoNJ7-tE.js +0 -1
- package/dist/assets/doc-browser-CdJ_d4ik.js +0 -1
- package/dist/assets/index-BiE-K8-q.css +0 -1
- package/dist/assets/index-D9BAE_L_.js +0 -105
- package/dist/assets/mcp-marketplace-page-B1SZNY5m.js +0 -40
- package/dist/assets/mcp-marketplace-page-Cq_Tw42T.js +0 -1
- package/dist/assets/model-config-page-DotaTUHb.js +0 -1
- package/dist/assets/plus-BbZqquJe.js +0 -1
- package/dist/assets/provider-scoped-model-input-DsFRuWJH.js +0 -1
- package/dist/assets/providers-config-page-DW4tQSLN.js +0 -1
- package/dist/assets/remote-B8TiUfwW.js +0 -1
- package/dist/assets/rotate-cw-DjHDTkCv.js +0 -1
- package/dist/assets/runtime-config-page-S6ErRHR8.js +0 -1
- package/dist/assets/search-config-page-DGj5am03.js +0 -1
- package/dist/assets/select-hoj8dK4K.js +0 -41
- package/dist/assets/tag-chip-T8-h-die.js +0 -1
- package/dist/assets/use-config-CjRV2hwL.js +0 -1
- package/dist/assets/x-D9PXs_xP.js +0 -1
- package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.ts +0 -40
- package/src/features/marketplace/components/detail-doc/marketplace-detail-doc-renderer.ts +0 -201
- package/src/features/marketplace/components/marketplace-detail-doc.ts +0 -142
- /package/dist/assets/{config-hints-BkYBc7z0.js → config-hints-fGnUjDe9.js} +0 -0
- /package/dist/assets/{middleware-CkFT3R4v.js → middleware-BILWbJE4.js} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { act, renderHook, waitFor } from '@testing-library/react';
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
|
-
import { DocBrowserProvider, useDocBrowser } from '
|
|
3
|
+
import { DocBrowserProvider, useDocBrowser } from '@/shared/components/doc-browser/doc-browser-context';
|
|
4
4
|
import { RightPanelResourceRouteResolver } from '@/features/right-panel-resources';
|
|
5
5
|
import { DocBrowserManager } from '@/shared/components/doc-browser/managers/doc-browser.manager';
|
|
6
6
|
import { createDefaultDocBrowserState } from '@/shared/components/doc-browser/utils/doc-browser-state.utils';
|
|
@@ -53,6 +53,35 @@ async function resetMemoryThenRehydrate(savedState: string | null) {
|
|
|
53
53
|
await rehydrateDocBrowserFromStorage();
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
describe('DocBrowserManager layout notifications', () => {
|
|
57
|
+
it('notifies the app layout coordinator after opening the docked browser', () => {
|
|
58
|
+
const onRightPanelOpened = vi.fn();
|
|
59
|
+
const manager = new DocBrowserManager(
|
|
60
|
+
new RightPanelResourceRouteResolver(),
|
|
61
|
+
onRightPanelOpened,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
manager.open('https://example.com/read', {
|
|
65
|
+
kind: 'content',
|
|
66
|
+
title: 'Example',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(onRightPanelOpened).toHaveBeenCalledTimes(1);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('notifies the app layout coordinator after changing dock mode', () => {
|
|
73
|
+
const onRightPanelOpened = vi.fn();
|
|
74
|
+
const manager = new DocBrowserManager(
|
|
75
|
+
new RightPanelResourceRouteResolver(),
|
|
76
|
+
onRightPanelOpened,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
manager.toggleMode();
|
|
80
|
+
|
|
81
|
+
expect(onRightPanelOpened).toHaveBeenCalledTimes(1);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
56
85
|
describe('DocBrowserProvider dedupe keys', () => {
|
|
57
86
|
it('opens different dedupe keys as separate content tabs', () => {
|
|
58
87
|
const { result } = renderHook(() => useDocBrowser(), { wrapper });
|
|
@@ -2,6 +2,7 @@ import { fireEvent, render, screen } from "@testing-library/react";
|
|
|
2
2
|
import userEvent from "@testing-library/user-event";
|
|
3
3
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
import { DocBrowser } from "@/shared/components/doc-browser/doc-browser";
|
|
5
|
+
import { DocBrowserTabStrip } from "@/shared/components/doc-browser/doc-browser-tab-strip";
|
|
5
6
|
import type {
|
|
6
7
|
DocBrowserContextValue,
|
|
7
8
|
DocBrowserTab,
|
|
@@ -66,6 +67,15 @@ const { docBrowserState } = vi.hoisted<{
|
|
|
66
67
|
},
|
|
67
68
|
}));
|
|
68
69
|
|
|
70
|
+
class TestPointerEvent extends MouseEvent {
|
|
71
|
+
pointerId: number;
|
|
72
|
+
|
|
73
|
+
constructor(type: string, init: PointerEventInit = {}) {
|
|
74
|
+
super(type, init);
|
|
75
|
+
this.pointerId = init.pointerId ?? 1;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
69
79
|
vi.mock("@/shared/components/doc-browser/doc-browser-context", async () => {
|
|
70
80
|
const actual = await vi.importActual(
|
|
71
81
|
"@/shared/components/doc-browser/doc-browser-context",
|
|
@@ -82,49 +92,61 @@ function firePointerEvent(
|
|
|
82
92
|
type: string,
|
|
83
93
|
point: { clientX: number; clientY?: number; pointerId?: number },
|
|
84
94
|
) {
|
|
85
|
-
const event = new
|
|
86
|
-
|
|
87
|
-
clientX:
|
|
88
|
-
clientY:
|
|
89
|
-
pointerId:
|
|
95
|
+
const event = new window.PointerEvent(type, {
|
|
96
|
+
bubbles: true,
|
|
97
|
+
clientX: point.clientX,
|
|
98
|
+
clientY: point.clientY ?? 0,
|
|
99
|
+
pointerId: point.pointerId ?? 1,
|
|
90
100
|
});
|
|
91
101
|
fireEvent(target, event);
|
|
92
102
|
}
|
|
93
103
|
|
|
104
|
+
function resetDocBrowserTestState() {
|
|
105
|
+
vi.clearAllMocks();
|
|
106
|
+
docBrowserState.mode = "docked";
|
|
107
|
+
docBrowserState.tabs = [
|
|
108
|
+
{
|
|
109
|
+
id: "docs",
|
|
110
|
+
kind: "docs" as const,
|
|
111
|
+
title: "Docs",
|
|
112
|
+
currentUrl: "https://docs.nextclaw.io/en/guide/getting-started",
|
|
113
|
+
history: ["https://docs.nextclaw.io/en/guide/getting-started"],
|
|
114
|
+
historyIndex: 0,
|
|
115
|
+
navVersion: 0,
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
docBrowserState.activeTabId = "docs";
|
|
119
|
+
docBrowserState.activeHistory = [
|
|
120
|
+
{
|
|
121
|
+
kind: "docs" as const,
|
|
122
|
+
tabId: "docs",
|
|
123
|
+
url: "https://docs.nextclaw.io/en/guide/getting-started",
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
docBrowserState.activeHistoryIndex = 0;
|
|
127
|
+
docBrowserState.currentTab = docBrowserState.tabs[0];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function installDocBrowserPointerTestEnvironment() {
|
|
131
|
+
Object.defineProperty(window, "innerWidth", {
|
|
132
|
+
configurable: true,
|
|
133
|
+
value: 1200,
|
|
134
|
+
});
|
|
135
|
+
Object.defineProperty(window, "innerHeight", {
|
|
136
|
+
configurable: true,
|
|
137
|
+
value: 900,
|
|
138
|
+
});
|
|
139
|
+
Object.defineProperty(window, "PointerEvent", {
|
|
140
|
+
configurable: true,
|
|
141
|
+
value: TestPointerEvent,
|
|
142
|
+
});
|
|
143
|
+
HTMLElement.prototype.setPointerCapture = vi.fn();
|
|
144
|
+
}
|
|
145
|
+
|
|
94
146
|
describe("DocBrowser", () => {
|
|
95
147
|
beforeEach(() => {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
docBrowserState.tabs = [
|
|
99
|
-
{
|
|
100
|
-
id: "docs",
|
|
101
|
-
kind: "docs" as const,
|
|
102
|
-
title: "Docs",
|
|
103
|
-
currentUrl: "https://docs.nextclaw.io/en/guide/getting-started",
|
|
104
|
-
history: ["https://docs.nextclaw.io/en/guide/getting-started"],
|
|
105
|
-
historyIndex: 0,
|
|
106
|
-
navVersion: 0,
|
|
107
|
-
},
|
|
108
|
-
];
|
|
109
|
-
docBrowserState.activeTabId = "docs";
|
|
110
|
-
docBrowserState.activeHistory = [
|
|
111
|
-
{
|
|
112
|
-
kind: "docs" as const,
|
|
113
|
-
tabId: "docs",
|
|
114
|
-
url: "https://docs.nextclaw.io/en/guide/getting-started",
|
|
115
|
-
},
|
|
116
|
-
];
|
|
117
|
-
docBrowserState.activeHistoryIndex = 0;
|
|
118
|
-
docBrowserState.currentTab = docBrowserState.tabs[0];
|
|
119
|
-
Object.defineProperty(window, "innerWidth", {
|
|
120
|
-
configurable: true,
|
|
121
|
-
value: 1200,
|
|
122
|
-
});
|
|
123
|
-
Object.defineProperty(window, "innerHeight", {
|
|
124
|
-
configurable: true,
|
|
125
|
-
value: 900,
|
|
126
|
-
});
|
|
127
|
-
HTMLElement.prototype.setPointerCapture = vi.fn();
|
|
148
|
+
resetDocBrowserTestState();
|
|
149
|
+
installDocBrowserPointerTestEnvironment();
|
|
128
150
|
});
|
|
129
151
|
|
|
130
152
|
it("uses the shared right panel resize handle in docked mode", () => {
|
|
@@ -158,6 +180,7 @@ describe("DocBrowser", () => {
|
|
|
158
180
|
|
|
159
181
|
const tabStrip = screen.getByTestId("doc-browser-tab-strip");
|
|
160
182
|
const tabActions = screen.getByTestId("doc-browser-tab-actions");
|
|
183
|
+
const floatButton = screen.getByRole("button", { name: "Float Window" });
|
|
161
184
|
const newTabButton = screen.getByRole("button", { name: "New Tab" });
|
|
162
185
|
|
|
163
186
|
expect(
|
|
@@ -167,8 +190,10 @@ describe("DocBrowser", () => {
|
|
|
167
190
|
tabActions.contains(screen.getByRole("button", { name: "Forward" })),
|
|
168
191
|
).toBe(true);
|
|
169
192
|
expect(
|
|
170
|
-
tabStrip.contains(
|
|
193
|
+
tabStrip.contains(floatButton),
|
|
171
194
|
).toBe(true);
|
|
195
|
+
expect(floatButton.querySelector(".lucide-picture-in-picture2")).toBeTruthy();
|
|
196
|
+
expect(floatButton.querySelector(".lucide-maximize2")).toBeNull();
|
|
172
197
|
expect(
|
|
173
198
|
tabStrip.contains(screen.getByRole("button", { name: "Close" })),
|
|
174
199
|
).toBe(true);
|
|
@@ -372,6 +397,50 @@ describe("DocBrowser", () => {
|
|
|
372
397
|
expect(screen.getByText("Help Docs")).toBeTruthy();
|
|
373
398
|
});
|
|
374
399
|
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
describe("DocBrowser floating interactions", () => {
|
|
403
|
+
beforeEach(() => {
|
|
404
|
+
resetDocBrowserTestState();
|
|
405
|
+
installDocBrowserPointerTestEnvironment();
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("starts floating drag from the header background without stealing tab actions", () => {
|
|
409
|
+
const onDragStart = vi.fn();
|
|
410
|
+
|
|
411
|
+
render(
|
|
412
|
+
<DocBrowserTabStrip
|
|
413
|
+
tabs={docBrowserState.tabs}
|
|
414
|
+
activeTabId="docs"
|
|
415
|
+
canGoBack={false}
|
|
416
|
+
canGoForward={false}
|
|
417
|
+
isDocked={false}
|
|
418
|
+
isFullscreen={false}
|
|
419
|
+
onGoBack={vi.fn()}
|
|
420
|
+
onGoForward={vi.fn()}
|
|
421
|
+
onOpenNewTab={vi.fn()}
|
|
422
|
+
onSetActiveTab={vi.fn()}
|
|
423
|
+
onCloseTab={vi.fn()}
|
|
424
|
+
onClose={vi.fn()}
|
|
425
|
+
onDragStart={onDragStart}
|
|
426
|
+
onToggleMode={vi.fn()}
|
|
427
|
+
/>,
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
fireEvent.pointerDown(screen.getByRole("button", { name: "Docs" }));
|
|
431
|
+
expect(onDragStart).not.toHaveBeenCalled();
|
|
432
|
+
|
|
433
|
+
const tabStrip = screen.getByTestId("doc-browser-tab-strip");
|
|
434
|
+
const headerDragSurface = tabStrip.querySelector(".doc-browser-tab-scrollbar");
|
|
435
|
+
expect(headerDragSurface).toBeInstanceOf(HTMLElement);
|
|
436
|
+
expect((headerDragSurface as HTMLElement).className).toContain("cursor-grab");
|
|
437
|
+
expect(screen.getByRole("button", { name: "Docs" }).parentElement?.className).toContain("cursor-pointer");
|
|
438
|
+
|
|
439
|
+
fireEvent.pointerDown(headerDragSurface as HTMLElement);
|
|
440
|
+
|
|
441
|
+
expect(onDragStart).toHaveBeenCalledTimes(1);
|
|
442
|
+
});
|
|
443
|
+
|
|
375
444
|
it("keeps the floating panel left edge stable when resizing from the right", () => {
|
|
376
445
|
docBrowserState.mode = "floating";
|
|
377
446
|
|
|
@@ -436,4 +505,103 @@ describe("DocBrowser", () => {
|
|
|
436
505
|
expect(panel.style.top).toBe("120px");
|
|
437
506
|
expect(panel.style.height).toBe("560px");
|
|
438
507
|
});
|
|
508
|
+
|
|
509
|
+
it("resizes the floating panel from the bottom-left corner", () => {
|
|
510
|
+
docBrowserState.mode = "floating";
|
|
511
|
+
|
|
512
|
+
render(<DocBrowser />);
|
|
513
|
+
|
|
514
|
+
firePointerEvent(
|
|
515
|
+
screen.getByTestId("doc-browser-resize-bottom-left"),
|
|
516
|
+
"pointerdown",
|
|
517
|
+
{
|
|
518
|
+
clientX: 680,
|
|
519
|
+
clientY: 680,
|
|
520
|
+
pointerId: 1,
|
|
521
|
+
},
|
|
522
|
+
);
|
|
523
|
+
firePointerEvent(window, "pointermove", {
|
|
524
|
+
clientX: 620,
|
|
525
|
+
clientY: 720,
|
|
526
|
+
pointerId: 1,
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
const panel = screen.getByTestId("doc-browser-panel");
|
|
530
|
+
expect(panel.style.left).toBe("620px");
|
|
531
|
+
expect(panel.style.top).toBe("80px");
|
|
532
|
+
expect(panel.style.width).toBe("540px");
|
|
533
|
+
expect(panel.style.height).toBe("640px");
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it("resizes the floating panel from the top-right corner", () => {
|
|
537
|
+
docBrowserState.mode = "floating";
|
|
538
|
+
|
|
539
|
+
render(<DocBrowser />);
|
|
540
|
+
|
|
541
|
+
firePointerEvent(
|
|
542
|
+
screen.getByTestId("doc-browser-resize-top-right"),
|
|
543
|
+
"pointerdown",
|
|
544
|
+
{
|
|
545
|
+
clientX: 1160,
|
|
546
|
+
clientY: 80,
|
|
547
|
+
pointerId: 1,
|
|
548
|
+
},
|
|
549
|
+
);
|
|
550
|
+
firePointerEvent(window, "pointermove", {
|
|
551
|
+
clientX: 1120,
|
|
552
|
+
clientY: 120,
|
|
553
|
+
pointerId: 1,
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
const panel = screen.getByTestId("doc-browser-panel");
|
|
557
|
+
expect(panel.style.left).toBe("680px");
|
|
558
|
+
expect(panel.style.top).toBe("120px");
|
|
559
|
+
expect(panel.style.width).toBe("440px");
|
|
560
|
+
expect(panel.style.height).toBe("560px");
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
describe("DocBrowser content tabs", () => {
|
|
565
|
+
beforeEach(() => {
|
|
566
|
+
resetDocBrowserTestState();
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it("shows browser controls for content iframe tabs", () => {
|
|
570
|
+
const contentTab: DocBrowserTab = {
|
|
571
|
+
id: "local-app",
|
|
572
|
+
kind: "content",
|
|
573
|
+
title: "Local App",
|
|
574
|
+
currentUrl: "http://127.0.0.1:5173/dashboard",
|
|
575
|
+
history: ["http://127.0.0.1:5173/dashboard"],
|
|
576
|
+
historyIndex: 0,
|
|
577
|
+
navVersion: 0,
|
|
578
|
+
};
|
|
579
|
+
docBrowserState.tabs = [contentTab];
|
|
580
|
+
docBrowserState.activeTabId = contentTab.id;
|
|
581
|
+
docBrowserState.currentTab = contentTab;
|
|
582
|
+
docBrowserState.activeHistory = [
|
|
583
|
+
{
|
|
584
|
+
kind: "content",
|
|
585
|
+
tabId: contentTab.id,
|
|
586
|
+
url: contentTab.currentUrl,
|
|
587
|
+
},
|
|
588
|
+
];
|
|
589
|
+
|
|
590
|
+
render(<DocBrowser />);
|
|
591
|
+
|
|
592
|
+
const addressInput = screen.getByRole("textbox", { name: "Address" }) as HTMLInputElement;
|
|
593
|
+
const iframe = screen.getByTitle("Local App") as HTMLIFrameElement;
|
|
594
|
+
const externalLink = screen.getByRole("link", { name: /Open in Browser/i }) as HTMLAnchorElement;
|
|
595
|
+
|
|
596
|
+
expect(addressInput.value).toBe("http://127.0.0.1:5173/dashboard");
|
|
597
|
+
expect(iframe.getAttribute("src")).toBe(contentTab.currentUrl);
|
|
598
|
+
expect(iframe.getAttribute("sandbox")).toBeNull();
|
|
599
|
+
expect(screen.getByRole("button", { name: "Refresh" })).toBeTruthy();
|
|
600
|
+
expect(externalLink.getAttribute("href")).toBe(contentTab.currentUrl);
|
|
601
|
+
|
|
602
|
+
fireEvent.change(addressInput, { target: { value: "localhost:5173/settings" } });
|
|
603
|
+
fireEvent.submit(addressInput.closest("form")!);
|
|
604
|
+
|
|
605
|
+
expect(docBrowserState.navigate).toHaveBeenCalledWith("http://localhost:5173/settings");
|
|
606
|
+
});
|
|
439
607
|
});
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import type { ReactNode, Ref } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
ExternalLink,
|
|
4
|
+
RefreshCw,
|
|
4
5
|
Search,
|
|
5
6
|
} from 'lucide-react';
|
|
6
7
|
import {
|
|
7
8
|
isDocsUrl,
|
|
8
9
|
type DocBrowserTab,
|
|
9
10
|
} from './doc-browser-context';
|
|
11
|
+
import { IconActionButton } from '@/shared/components/ui/actions/icon-action-button';
|
|
10
12
|
import { t } from '@/shared/lib/i18n';
|
|
11
13
|
|
|
12
|
-
type
|
|
13
|
-
|
|
14
|
+
type DocBrowserAddressToolbarProps = {
|
|
15
|
+
isVisible: boolean;
|
|
16
|
+
onRefresh: () => void;
|
|
14
17
|
onSubmit: (e: React.FormEvent) => void;
|
|
15
18
|
onUrlInputChange: (value: string) => void;
|
|
19
|
+
placeholder: string;
|
|
16
20
|
urlInput: string;
|
|
17
21
|
};
|
|
18
22
|
|
|
@@ -22,18 +26,20 @@ type DocBrowserFrameContentProps = {
|
|
|
22
26
|
customContent: ReactNode;
|
|
23
27
|
iframeRef: Ref<HTMLIFrameElement>;
|
|
24
28
|
iframeInstanceId: string;
|
|
25
|
-
iframeSandbox
|
|
29
|
+
iframeSandbox?: string;
|
|
26
30
|
isDragging: boolean;
|
|
27
31
|
isResizing: boolean;
|
|
28
32
|
};
|
|
29
33
|
|
|
30
|
-
export function
|
|
31
|
-
|
|
34
|
+
export function DocBrowserAddressToolbar({
|
|
35
|
+
isVisible,
|
|
36
|
+
onRefresh,
|
|
32
37
|
onSubmit,
|
|
33
38
|
onUrlInputChange,
|
|
39
|
+
placeholder,
|
|
34
40
|
urlInput,
|
|
35
|
-
}:
|
|
36
|
-
if (!
|
|
41
|
+
}: DocBrowserAddressToolbarProps) {
|
|
42
|
+
if (!isVisible) {
|
|
37
43
|
return null;
|
|
38
44
|
}
|
|
39
45
|
|
|
@@ -45,10 +51,16 @@ export function DocBrowserDocsToolbar({
|
|
|
45
51
|
type="text"
|
|
46
52
|
value={urlInput}
|
|
47
53
|
onChange={(e) => onUrlInputChange(e.target.value)}
|
|
48
|
-
|
|
54
|
+
aria-label={t('docBrowserAddressLabel')}
|
|
55
|
+
placeholder={placeholder}
|
|
49
56
|
className="h-8 w-full rounded-lg border border-border bg-background pl-8 pr-3 text-xs text-foreground transition-colors placeholder:text-muted-foreground/55 focus:border-primary/40 focus:outline-none focus:ring-1 focus:ring-primary/30"
|
|
50
57
|
/>
|
|
51
58
|
</form>
|
|
59
|
+
<IconActionButton
|
|
60
|
+
icon={<RefreshCw className="h-3.5 w-3.5" />}
|
|
61
|
+
label={t('docBrowserRefresh')}
|
|
62
|
+
onClick={onRefresh}
|
|
63
|
+
/>
|
|
52
64
|
</div>
|
|
53
65
|
);
|
|
54
66
|
}
|
|
@@ -76,6 +88,7 @@ export function DocBrowserFrameContent({
|
|
|
76
88
|
title={currentTab?.title || 'NextClaw Docs'}
|
|
77
89
|
sandbox={iframeSandbox}
|
|
78
90
|
allow="clipboard-read; clipboard-write"
|
|
91
|
+
allowFullScreen
|
|
79
92
|
/>
|
|
80
93
|
)}
|
|
81
94
|
{(isResizing || isDragging) && (
|
|
@@ -85,10 +98,20 @@ export function DocBrowserFrameContent({
|
|
|
85
98
|
);
|
|
86
99
|
}
|
|
87
100
|
|
|
88
|
-
|
|
89
|
-
|
|
101
|
+
function canOpenExternalUrl(url: string): boolean {
|
|
102
|
+
try {
|
|
103
|
+
const parsed = new URL(url, window.location.origin);
|
|
104
|
+
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function DocBrowserExternalLink({ currentUrl, isVisible }: { currentUrl: string; isVisible: boolean }) {
|
|
111
|
+
if (!isVisible || !canOpenExternalUrl(currentUrl)) {
|
|
90
112
|
return null;
|
|
91
113
|
}
|
|
114
|
+
const label = isDocsUrl(currentUrl) ? t('docBrowserOpenExternal') : t('docBrowserOpenInBrowser');
|
|
92
115
|
|
|
93
116
|
return (
|
|
94
117
|
<div className="flex items-center justify-between border-t border-border/70 bg-muted/55 px-4 py-2 shrink-0">
|
|
@@ -99,7 +122,7 @@ export function DocBrowserExternalLink({ currentUrl, isDocsTab }: { currentUrl:
|
|
|
99
122
|
data-doc-external
|
|
100
123
|
className="flex items-center gap-1.5 text-xs text-primary hover:text-primary-hover font-medium transition-colors"
|
|
101
124
|
>
|
|
102
|
-
{
|
|
125
|
+
{label}
|
|
103
126
|
<ExternalLink className="w-3 h-3" />
|
|
104
127
|
</a>
|
|
105
128
|
</div>
|
|
@@ -17,7 +17,7 @@ export type DocBrowserIframeMessageParams = {
|
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
export type DocBrowserCustomTabRenderer = {
|
|
20
|
-
getIframeSandbox?: (tab: DocBrowserTab) => string;
|
|
20
|
+
getIframeSandbox?: (tab: DocBrowserTab) => string | undefined;
|
|
21
21
|
getTitle?: (tab: DocBrowserTab) => string;
|
|
22
22
|
onIframeMessage?: (params: DocBrowserIframeMessageParams) => void;
|
|
23
23
|
renderContent?: (params: DocBrowserCustomTabRenderParams) => ReactNode;
|
|
@@ -2,8 +2,8 @@ import type { PointerEvent } from "react";
|
|
|
2
2
|
import {
|
|
3
3
|
ArrowLeft,
|
|
4
4
|
ArrowRight,
|
|
5
|
-
Maximize2,
|
|
6
5
|
PanelRightOpen,
|
|
6
|
+
PictureInPicture2,
|
|
7
7
|
Pin,
|
|
8
8
|
PinOff,
|
|
9
9
|
Plus,
|
|
@@ -37,6 +37,12 @@ type DocBrowserTabStripProps = {
|
|
|
37
37
|
onToggleMode: () => void;
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
+
function shouldBlockHeaderDrag(target: EventTarget | null): boolean {
|
|
41
|
+
return target instanceof Element && Boolean(
|
|
42
|
+
target.closest("button, a, input, textarea, select, [data-compact-tab-item]"),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
40
46
|
export function DocBrowserTabStrip({
|
|
41
47
|
tabs,
|
|
42
48
|
activeTabId,
|
|
@@ -95,7 +101,7 @@ export function DocBrowserTabStrip({
|
|
|
95
101
|
? [
|
|
96
102
|
{
|
|
97
103
|
key: "mode",
|
|
98
|
-
icon: isDocked ? <
|
|
104
|
+
icon: isDocked ? <PictureInPicture2 className="h-3.5 w-3.5" /> : <PanelRightOpen className="h-3.5 w-3.5" />,
|
|
99
105
|
label: modeLabel,
|
|
100
106
|
onClick: onToggleMode,
|
|
101
107
|
},
|
|
@@ -114,16 +120,28 @@ export function DocBrowserTabStrip({
|
|
|
114
120
|
"h-11 gap-2 border-border/70 bg-card px-2.5 shrink-0 select-none",
|
|
115
121
|
isFullscreen && "h-[calc(env(safe-area-inset-top,0px)+2.75rem)] pt-[env(safe-area-inset-top,0px)]",
|
|
116
122
|
)}
|
|
117
|
-
scrollClassName=
|
|
123
|
+
scrollClassName={cn(
|
|
124
|
+
"doc-browser-tab-scrollbar flex h-full items-center gap-1.5",
|
|
125
|
+
!isDocked && !isFullscreen && "cursor-grab active:cursor-grabbing",
|
|
126
|
+
)}
|
|
118
127
|
tabsClassName="items-center gap-1.5"
|
|
119
128
|
actionsClassName="h-full items-center gap-1"
|
|
120
129
|
actionButtonClassName="rounded-md p-1.5 hover:text-foreground disabled:opacity-60"
|
|
121
|
-
tabBaseClassName="group inline-flex min-w-0 items-center gap-1 h-7 px-1.5 rounded-lg text-xs border max-w-[220px] shrink-0 transition-colors"
|
|
130
|
+
tabBaseClassName="group inline-flex min-w-0 cursor-pointer items-center gap-1 h-7 px-1.5 rounded-lg text-xs border max-w-[220px] shrink-0 transition-colors"
|
|
122
131
|
activeTabClassName="border-primary/30 bg-primary-50/70 text-foreground shadow-[0_1px_2px_rgba(30,20,10,0.04)]"
|
|
123
132
|
inactiveTabClassName="border-border bg-muted/60 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
|
124
133
|
labelClassName="px-1 text-xs font-normal"
|
|
125
134
|
onPointerDown={!isDocked && !isFullscreen ? onDragStart : undefined}
|
|
126
|
-
onScrollPointerDown={(event) =>
|
|
135
|
+
onScrollPointerDown={(event) => {
|
|
136
|
+
if (shouldBlockHeaderDrag(event.target)) {
|
|
137
|
+
event.stopPropagation();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (!isDocked && !isFullscreen) {
|
|
141
|
+
event.stopPropagation();
|
|
142
|
+
onDragStart(event);
|
|
143
|
+
}
|
|
144
|
+
}}
|
|
127
145
|
/>
|
|
128
146
|
);
|
|
129
147
|
}
|