@nextclaw/ui 0.15.3 → 0.15.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/dist/assets/aiden0z-pptx-renderer.es-Bd99vut2.js +94 -0
- package/dist/assets/api-BOXOrpcJ.js +13 -0
- package/dist/assets/app-presenter-provider-CkHLbvZQ.js +106 -0
- package/dist/assets/app-presenter-provider-D3BuuOyZ.css +1 -0
- package/dist/assets/appearance-settings-page-fLIJbx1C.js +1 -0
- package/dist/assets/{book-open-C3mJGwiy.js → book-open-DWUjS6aW.js} +1 -1
- package/dist/assets/channels-list-page-CvCTJraa.js +8 -0
- package/dist/assets/chat-page-B42UUIFR.js +1 -0
- package/dist/assets/chunk-DseTPa7n.js +1 -0
- package/dist/assets/config-split-page-DBgt9S8b.js +1 -0
- package/dist/assets/confirm-dialog-GSXIvoRU.js +5 -0
- package/dist/assets/desktop-update-config-7bmNeWIC.js +1 -0
- package/dist/assets/dist-DC7f4TJH.js +41 -0
- package/dist/assets/doc-browser-CgdoLUT92.js +1 -0
- package/dist/assets/doc-browser-D0monHjG.js +1 -0
- package/dist/assets/doc-browser-context-CGwEOhBy.js +1 -0
- package/dist/assets/docx-preview-eFH9DRsS.js +20 -0
- package/dist/assets/download-DiP9zx-J.js +1 -0
- package/dist/assets/{external-link-CU0kzzIP.js → external-link-C6ZeSPef.js} +1 -1
- package/dist/assets/eye-suvKmtwn.js +1 -0
- package/dist/assets/folder-CMELbQGG.js +1 -0
- package/dist/assets/{form-actions-BvCp_0lS.js → form-actions-CUAzpNe_.js} +1 -1
- package/dist/assets/index-BFeNDQ4R.css +1 -0
- package/dist/assets/index-D_6U-M94.js +2 -0
- package/dist/assets/jszip.min-Das2IAnb.js +2 -0
- package/dist/assets/loader-circle-tYHStefV.js +1 -0
- package/dist/assets/logo-badge-Cu5UGH_4.js +1 -0
- package/dist/assets/mcp-marketplace-page-BUzHDwW6.js +9 -0
- package/dist/assets/mcp-marketplace-page-ohUAdfqJ.js +1 -0
- package/dist/assets/model-config-page-CWP6pyBm.js +1 -0
- package/dist/assets/plus-tywizkeY.js +1 -0
- package/dist/assets/popover-DImguRok.js +1 -0
- package/dist/assets/provider-models-C_yOh6DE.js +1 -0
- package/dist/assets/provider-scoped-model-input-CE7p4SbJ.js +1 -0
- package/dist/assets/providers-config-page-BKPo8SjH.js +1 -0
- package/dist/assets/react-5sjAT_6d.js +8 -0
- package/dist/assets/{refresh-cw-CILd1vAP.js → refresh-cw-CHLBzWWj.js} +1 -1
- package/dist/assets/remote-DXJryUxB.js +1 -0
- package/dist/assets/rotate-cw-BefO5brU.js +1 -0
- package/dist/assets/runtime-config-page-1Axv4QGu.js +1 -0
- package/dist/assets/{save-ByJqcPTP.js → save-BABzyrjB.js} +1 -1
- package/dist/assets/{search-DVIV6pfD.js → search-BTqxG3ZG.js} +1 -1
- package/dist/assets/search-config-page-BkKKmxmS.js +1 -0
- package/dist/assets/secrets-config-page-CWJMyu-k.js +3 -0
- package/dist/assets/security-config-DeQUIXfi.js +1 -0
- package/dist/assets/select-TYZDBNmq.js +1 -0
- package/dist/assets/setting-row-DR23Z_5I.js +1 -0
- package/dist/assets/{settings-2-CH5shGg_.js → settings-2-D8WWcBnw.js} +1 -1
- package/dist/assets/{skeleton-DeX1FKw7.js → skeleton-CloHeg0S.js} +1 -1
- package/dist/assets/tabs-CX-ckWoo.js +1 -0
- package/dist/assets/tag-chip-mCc23Rhb.js +1 -0
- package/dist/assets/trash-2-SAW1Z4Po.js +1 -0
- package/dist/assets/use-config-BucdaK23.js +1 -0
- package/dist/assets/use-confirm-dialog-C95F6sT1.js +1 -0
- package/dist/assets/use-viewport-layout-C6m3Nte7.js +1 -0
- package/dist/assets/x-DtCPHwCu.js +1 -0
- package/dist/assets/xlsx-B6zVt1RK.js +64 -0
- package/dist/index.html +34 -28
- package/package.json +11 -6
- package/src/features/chat/components/__tests__/chat-popover-content.test.tsx +41 -0
- package/src/features/chat/components/chat-popover-content.tsx +26 -0
- package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx +13 -0
- package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +21 -11
- package/src/features/chat/components/conversation/__tests__/chat-conversation-header.test.tsx +2 -2
- package/src/features/chat/components/conversation/__tests__/chat-conversation-workspace-section.test.tsx +28 -1
- package/src/features/chat/components/conversation/chat-conversation-content.tsx +5 -4
- package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +9 -18
- package/src/features/chat/components/conversation/chat-conversation-track.tsx +35 -0
- package/src/features/chat/components/conversation/chat-conversation-workspace-section.tsx +1 -0
- package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +7 -7
- package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +3 -14
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +20 -1
- package/src/features/chat/features/conversation/components/session-conversation-input.tsx +69 -54
- package/src/features/chat/features/message/components/chat-message-list.container.tsx +16 -1
- package/src/features/chat/features/message/utils/__tests__/chat-message-file-operation.utils.test.ts +30 -0
- package/src/features/chat/features/session/components/__tests__/chat-sidebar-session-item.test.tsx +37 -0
- package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +4 -3
- package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +3 -8
- package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +65 -20
- package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +26 -28
- package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +3 -3
- package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +3 -3
- package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +3 -3
- package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +3 -3
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +403 -106
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +113 -2
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel.test.tsx +1 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +197 -44
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumb-browser.tsx +9 -3
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +7 -5
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +57 -177
- package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +239 -1
- package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +56 -6
- package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +29 -7
- package/src/features/chat/features/workspace/components/file-content-preview/use-workspace-file-buffer.ts +42 -0
- package/src/features/chat/features/workspace/components/file-content-preview/workspace-document-preview-state.tsx +18 -0
- package/src/features/chat/features/workspace/components/file-content-preview/workspace-docx-preview.css +89 -0
- package/src/features/chat/features/workspace/components/file-content-preview/workspace-docx-preview.tsx +87 -0
- package/src/features/chat/features/workspace/components/file-content-preview/workspace-presentation-preview.tsx +82 -0
- package/src/features/chat/features/workspace/components/file-content-preview/workspace-spreadsheet-preview.tsx +180 -0
- package/src/features/chat/features/workspace/components/workspace-file-content-preview.tsx +191 -0
- package/src/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state.ts +1 -4
- package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +53 -10
- package/src/features/chat/features/workspace/utils/chat-thread-workspace-session.utils.ts +68 -2
- package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +17 -0
- package/src/features/chat/features/workspace/utils/chat-workspace-file-tab.utils.ts +63 -0
- package/src/features/chat/features/workspace/utils/chat-workspace-file-viewer.utils.ts +49 -0
- package/src/features/chat/features/workspace/utils/chat-workspace-panel-layout.utils.ts +13 -0
- package/src/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils.ts +120 -38
- package/src/features/chat/index.ts +1 -0
- package/src/features/chat/managers/__tests__/chat-thread-workspace.manager.test.ts +76 -0
- package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +44 -60
- package/src/features/chat/managers/chat-thread.manager.ts +123 -126
- package/src/features/chat/pages/ncp-chat-page.tsx +1 -2
- package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +78 -1
- package/src/features/chat/stores/chat-message-layout.store.test.ts +37 -0
- package/src/features/chat/stores/chat-message-layout.store.ts +41 -0
- package/src/features/chat/stores/chat-thread.store.ts +70 -13
- package/src/features/settings/pages/__tests__/appearance-settings-page.test.tsx +25 -8
- package/src/features/settings/pages/appearance-settings-page.tsx +92 -13
- package/src/index.css +36 -2
- package/src/shared/components/__tests__/file-type-icon.test.tsx +61 -0
- package/src/shared/components/doc-browser/__tests__/doc-browser-context.test.tsx +19 -0
- package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +16 -0
- package/src/shared/components/doc-browser/doc-browser-context.tsx +1 -0
- package/src/shared/components/doc-browser/doc-browser.tsx +11 -3
- package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +5 -0
- package/src/shared/components/doc-browser/stores/doc-browser.store.ts +6 -1
- package/src/shared/components/doc-browser/types/doc-browser.types.ts +2 -0
- package/src/shared/components/doc-browser/utils/doc-browser-state.utils.ts +11 -0
- package/src/shared/components/file-type-icon.tsx +272 -0
- package/src/shared/components/resizable-right-panel/__tests__/resizable-right-panel.test.tsx +22 -0
- package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +17 -2
- package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +38 -0
- package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +82 -2
- package/src/shared/lib/api/__tests__/server-path.utils.test.ts +28 -0
- package/src/shared/lib/api/utils/server-path.utils.ts +19 -20
- package/src/shared/lib/i18n/locales/en-US/chat.json +17 -1
- package/src/shared/lib/i18n/locales/en-US/core.json +6 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +17 -1
- package/src/shared/lib/i18n/locales/zh-CN/core.json +6 -0
- package/dist/assets/api-iNBpppzj.js +0 -13
- package/dist/assets/appearance-settings-page-DMffQFR3.js +0 -1
- package/dist/assets/channels-list-page-CCtp0BRS.js +0 -8
- package/dist/assets/chat-page-H5a0dWvO.js +0 -1
- package/dist/assets/config-split-page-CrOJgB-Y.js +0 -1
- package/dist/assets/confirm-dialog-Chv50USy.js +0 -5
- package/dist/assets/desktop-update-config-KzTuXRf_.js +0 -1
- package/dist/assets/dist-BunzmC4B.js +0 -1
- package/dist/assets/dist-ChmGwvtK.js +0 -1
- package/dist/assets/doc-browser-DNRL4Zvd.js +0 -1
- package/dist/assets/doc-browser-Dg0pmkOg.js +0 -1
- package/dist/assets/doc-browser-context-BHY-0FSD.js +0 -1
- package/dist/assets/es2015-DQZ5JWwK.js +0 -41
- package/dist/assets/i18n-provider-Cu8XyZb_.js +0 -1
- package/dist/assets/index-CzfBdvK1.css +0 -1
- package/dist/assets/index-DGqHne1Q.js +0 -104
- package/dist/assets/loader-circle-CDSFuzmk.js +0 -1
- package/dist/assets/mcp-marketplace-page-BeEQK0DL.js +0 -9
- package/dist/assets/mcp-marketplace-page-D6idOTLp.js +0 -1
- package/dist/assets/model-config-page-bNNo-XyG.js +0 -1
- package/dist/assets/plus-D4DsdLN-.js +0 -1
- package/dist/assets/provider-scoped-model-input-IlgQCsoT.js +0 -1
- package/dist/assets/providers-config-page-wLo6CAJw.js +0 -1
- package/dist/assets/react-BPIsQIlO.js +0 -8
- package/dist/assets/remote-uKEYp7AR.js +0 -1
- package/dist/assets/rotate-cw-wPFPg1kH.js +0 -1
- package/dist/assets/runtime-config-page-DDn1sP4G.js +0 -1
- package/dist/assets/search-config-page-B-Cf4iiB.js +0 -1
- package/dist/assets/secrets-config-page-DPKsY8Iw.js +0 -3
- package/dist/assets/security-config-e9SSBlKr.js +0 -1
- package/dist/assets/select-BdgczSNw.js +0 -1
- package/dist/assets/setting-row-BkTVUTKx.js +0 -1
- package/dist/assets/side-dock-BsBvcJ9i.js +0 -3
- package/dist/assets/switch-Cv8Ekb3x.js +0 -1
- package/dist/assets/tag-chip-RB9uKcoC.js +0 -1
- package/dist/assets/tooltip-oQlEN-l6.js +0 -1
- package/dist/assets/use-config-l3W7y8Qz.js +0 -1
- package/dist/assets/use-confirm-dialog-BAxaVOJI.js +0 -1
- package/dist/assets/x-BBMffEN0.js +0 -1
- /package/dist/assets/{config-hints-C_iP1zl_.js → config-hints-fDrYfl0l.js} +0 -0
- /package/dist/assets/{host-capabilities-9bB_998Z.js → host-capabilities-zZvY9U63.js} +0 -0
- /package/dist/assets/{middleware-BILWbJE4.js → middleware-BLMHv9YF.js} +0 -0
|
@@ -12,8 +12,18 @@ import {
|
|
|
12
12
|
} from './doc-browser-url.utils';
|
|
13
13
|
import { t } from '@/shared/lib/i18n';
|
|
14
14
|
|
|
15
|
+
export const DOC_BROWSER_DOCKED_DEFAULT_WIDTH = 420;
|
|
16
|
+
export const DOC_BROWSER_DOCKED_MIN_WIDTH = 320;
|
|
17
|
+
export const DOC_BROWSER_DOCKED_MAX_WIDTH = 860;
|
|
18
|
+
|
|
15
19
|
let tabCounter = 0;
|
|
16
20
|
|
|
21
|
+
export function normalizeDocBrowserDockedWidth(value: unknown): number {
|
|
22
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
23
|
+
? Math.min(DOC_BROWSER_DOCKED_MAX_WIDTH, Math.max(DOC_BROWSER_DOCKED_MIN_WIDTH, value))
|
|
24
|
+
: DOC_BROWSER_DOCKED_DEFAULT_WIDTH;
|
|
25
|
+
}
|
|
26
|
+
|
|
17
27
|
function nextTabId(): string {
|
|
18
28
|
tabCounter += 1;
|
|
19
29
|
return `doc-tab-${Date.now()}-${tabCounter}`;
|
|
@@ -58,6 +68,7 @@ export function createDefaultDocBrowserState(): DocBrowserState {
|
|
|
58
68
|
return {
|
|
59
69
|
isOpen: false,
|
|
60
70
|
mode: 'docked',
|
|
71
|
+
dockedWidth: DOC_BROWSER_DOCKED_DEFAULT_WIDTH,
|
|
61
72
|
tabs: [initialTab],
|
|
62
73
|
activeTabId: initialTab.id,
|
|
63
74
|
activeHistory: [createDocBrowserActiveHistoryEntry(initialTab)],
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { Icon, type IconifyIcon } from "@iconify/react";
|
|
2
|
+
import defaultFileIcon from "@iconify/icons-vscode-icons/default-file";
|
|
3
|
+
import audioIcon from "@iconify/icons-vscode-icons/file-type-audio";
|
|
4
|
+
import bunIcon from "@iconify/icons-vscode-icons/file-type-bun";
|
|
5
|
+
import configIcon from "@iconify/icons-vscode-icons/file-type-config";
|
|
6
|
+
import cppIcon from "@iconify/icons-vscode-icons/file-type-cpp";
|
|
7
|
+
import csharpIcon from "@iconify/icons-vscode-icons/file-type-csharp";
|
|
8
|
+
import cssIcon from "@iconify/icons-vscode-icons/file-type-css";
|
|
9
|
+
import dockerIcon from "@iconify/icons-vscode-icons/file-type-docker";
|
|
10
|
+
import dotenvIcon from "@iconify/icons-vscode-icons/file-type-dotenv";
|
|
11
|
+
import eslintIcon from "@iconify/icons-vscode-icons/file-type-eslint";
|
|
12
|
+
import excelIcon from "@iconify/icons-vscode-icons/file-type-excel";
|
|
13
|
+
import fontIcon from "@iconify/icons-vscode-icons/file-type-font";
|
|
14
|
+
import gitIcon from "@iconify/icons-vscode-icons/file-type-git";
|
|
15
|
+
import goIcon from "@iconify/icons-vscode-icons/file-type-go";
|
|
16
|
+
import graphqlIcon from "@iconify/icons-vscode-icons/file-type-graphql";
|
|
17
|
+
import htmlIcon from "@iconify/icons-vscode-icons/file-type-html";
|
|
18
|
+
import imageIcon from "@iconify/icons-vscode-icons/file-type-image";
|
|
19
|
+
import javaIcon from "@iconify/icons-vscode-icons/file-type-java";
|
|
20
|
+
import javascriptIcon from "@iconify/icons-vscode-icons/file-type-js";
|
|
21
|
+
import jsonIcon from "@iconify/icons-vscode-icons/file-type-json";
|
|
22
|
+
import kotlinIcon from "@iconify/icons-vscode-icons/file-type-kotlin";
|
|
23
|
+
import licenseIcon from "@iconify/icons-vscode-icons/file-type-license";
|
|
24
|
+
import luaIcon from "@iconify/icons-vscode-icons/file-type-lua";
|
|
25
|
+
import makefileIcon from "@iconify/icons-vscode-icons/file-type-makefile";
|
|
26
|
+
import markdownIcon from "@iconify/icons-vscode-icons/file-type-markdown";
|
|
27
|
+
import npmIcon from "@iconify/icons-vscode-icons/file-type-npm";
|
|
28
|
+
import pdfIcon from "@iconify/icons-vscode-icons/file-type-pdf2";
|
|
29
|
+
import phpIcon from "@iconify/icons-vscode-icons/file-type-php";
|
|
30
|
+
import pnpmIcon from "@iconify/icons-vscode-icons/file-type-pnpm";
|
|
31
|
+
import powerpointIcon from "@iconify/icons-vscode-icons/file-type-powerpoint";
|
|
32
|
+
import prettierIcon from "@iconify/icons-vscode-icons/file-type-prettier";
|
|
33
|
+
import pythonIcon from "@iconify/icons-vscode-icons/file-type-python";
|
|
34
|
+
import reactJavascriptIcon from "@iconify/icons-vscode-icons/file-type-reactjs";
|
|
35
|
+
import reactTypescriptIcon from "@iconify/icons-vscode-icons/file-type-reactts";
|
|
36
|
+
import rubyIcon from "@iconify/icons-vscode-icons/file-type-ruby";
|
|
37
|
+
import rustIcon from "@iconify/icons-vscode-icons/file-type-rust";
|
|
38
|
+
import sassIcon from "@iconify/icons-vscode-icons/file-type-sass";
|
|
39
|
+
import shellIcon from "@iconify/icons-vscode-icons/file-type-shell";
|
|
40
|
+
import sqlIcon from "@iconify/icons-vscode-icons/file-type-sql";
|
|
41
|
+
import svelteIcon from "@iconify/icons-vscode-icons/file-type-svelte";
|
|
42
|
+
import svgIcon from "@iconify/icons-vscode-icons/file-type-svg";
|
|
43
|
+
import swiftIcon from "@iconify/icons-vscode-icons/file-type-swift";
|
|
44
|
+
import tailwindIcon from "@iconify/icons-vscode-icons/file-type-tailwind";
|
|
45
|
+
import textIcon from "@iconify/icons-vscode-icons/file-type-text";
|
|
46
|
+
import tomlIcon from "@iconify/icons-vscode-icons/file-type-toml";
|
|
47
|
+
import tsconfigIcon from "@iconify/icons-vscode-icons/file-type-tsconfig";
|
|
48
|
+
import typescriptIcon from "@iconify/icons-vscode-icons/file-type-typescript";
|
|
49
|
+
import videoIcon from "@iconify/icons-vscode-icons/file-type-video";
|
|
50
|
+
import viteIcon from "@iconify/icons-vscode-icons/file-type-vite";
|
|
51
|
+
import vitestIcon from "@iconify/icons-vscode-icons/file-type-vitest";
|
|
52
|
+
import vueIcon from "@iconify/icons-vscode-icons/file-type-vue";
|
|
53
|
+
import wordIcon from "@iconify/icons-vscode-icons/file-type-word";
|
|
54
|
+
import xmlIcon from "@iconify/icons-vscode-icons/file-type-xml";
|
|
55
|
+
import yamlIcon from "@iconify/icons-vscode-icons/file-type-yaml";
|
|
56
|
+
import yarnIcon from "@iconify/icons-vscode-icons/file-type-yarn";
|
|
57
|
+
import zipIcon from "@iconify/icons-vscode-icons/file-type-zip";
|
|
58
|
+
import { cn } from "@/shared/lib/utils";
|
|
59
|
+
|
|
60
|
+
type FileTypeIconProps = {
|
|
61
|
+
className?: string;
|
|
62
|
+
fileName: string;
|
|
63
|
+
size?: "compact" | "default";
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type FileIconVisual = {
|
|
67
|
+
icon: IconifyIcon;
|
|
68
|
+
name: string;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
type BundledIcon = IconifyIcon | { default: IconifyIcon };
|
|
72
|
+
|
|
73
|
+
type FileIconRule = FileIconVisual & {
|
|
74
|
+
extensions: readonly string[];
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const SPECIAL_FILE_RULES: Array<FileIconVisual & { pattern: RegExp }> = [
|
|
78
|
+
{
|
|
79
|
+
icon: dockerIcon,
|
|
80
|
+
name: "docker",
|
|
81
|
+
pattern: /^(dockerfile|\.dockerignore)$/,
|
|
82
|
+
},
|
|
83
|
+
{ icon: dotenvIcon, name: "dotenv", pattern: /^\.env(?:\.|$)/ },
|
|
84
|
+
{
|
|
85
|
+
icon: gitIcon,
|
|
86
|
+
name: "git",
|
|
87
|
+
pattern: /^\.git(?:attributes|ignore|modules)$/,
|
|
88
|
+
},
|
|
89
|
+
{ icon: makefileIcon, name: "makefile", pattern: /^makefile$/ },
|
|
90
|
+
{ icon: licenseIcon, name: "license", pattern: /^licen[cs]e(?:\.|$)/ },
|
|
91
|
+
{ icon: markdownIcon, name: "markdown", pattern: /^readme(?:\.|$)/ },
|
|
92
|
+
{ icon: npmIcon, name: "npm", pattern: /^package(?:-lock)?\.json$/ },
|
|
93
|
+
{
|
|
94
|
+
icon: pnpmIcon,
|
|
95
|
+
name: "pnpm",
|
|
96
|
+
pattern: /^(pnpm-lock\.yaml|pnpm-workspace\.yaml)$/,
|
|
97
|
+
},
|
|
98
|
+
{ icon: yarnIcon, name: "yarn", pattern: /^yarn\.lock$/ },
|
|
99
|
+
{ icon: bunIcon, name: "bun", pattern: /^bun\.lockb?$/ },
|
|
100
|
+
{
|
|
101
|
+
icon: tsconfigIcon,
|
|
102
|
+
name: "tsconfig",
|
|
103
|
+
pattern: /^tsconfig(?:\..+)?\.json$/,
|
|
104
|
+
},
|
|
105
|
+
{ icon: viteIcon, name: "vite", pattern: /^vite\.config\./ },
|
|
106
|
+
{ icon: vitestIcon, name: "vitest", pattern: /^vitest(?:\..+)?\.config\./ },
|
|
107
|
+
{ icon: tailwindIcon, name: "tailwind", pattern: /^tailwind\.config\./ },
|
|
108
|
+
{
|
|
109
|
+
icon: eslintIcon,
|
|
110
|
+
name: "eslint",
|
|
111
|
+
pattern: /^(eslint\.config\.|\.eslintrc)/,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
icon: prettierIcon,
|
|
115
|
+
name: "prettier",
|
|
116
|
+
pattern: /^(prettier\.config\.|\.prettierrc)/,
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
const FILE_ICON_RULES: FileIconRule[] = [
|
|
121
|
+
{ icon: reactTypescriptIcon, name: "react-typescript", extensions: ["tsx"] },
|
|
122
|
+
{ icon: reactJavascriptIcon, name: "react-javascript", extensions: ["jsx"] },
|
|
123
|
+
{
|
|
124
|
+
icon: typescriptIcon,
|
|
125
|
+
name: "typescript",
|
|
126
|
+
extensions: ["ts", "mts", "cts"],
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
icon: javascriptIcon,
|
|
130
|
+
name: "javascript",
|
|
131
|
+
extensions: ["js", "mjs", "cjs"],
|
|
132
|
+
},
|
|
133
|
+
{ icon: jsonIcon, name: "json", extensions: ["json", "json5", "jsonc"] },
|
|
134
|
+
{ icon: markdownIcon, name: "markdown", extensions: ["md", "mdx"] },
|
|
135
|
+
{ icon: htmlIcon, name: "html", extensions: ["html", "htm"] },
|
|
136
|
+
{ icon: cssIcon, name: "css", extensions: ["css", "pcss", "less"] },
|
|
137
|
+
{ icon: sassIcon, name: "sass", extensions: ["sass", "scss"] },
|
|
138
|
+
{ icon: vueIcon, name: "vue", extensions: ["vue"] },
|
|
139
|
+
{ icon: svelteIcon, name: "svelte", extensions: ["svelte"] },
|
|
140
|
+
{ icon: pythonIcon, name: "python", extensions: ["py", "pyw"] },
|
|
141
|
+
{ icon: goIcon, name: "go", extensions: ["go"] },
|
|
142
|
+
{ icon: rustIcon, name: "rust", extensions: ["rs"] },
|
|
143
|
+
{ icon: javaIcon, name: "java", extensions: ["java"] },
|
|
144
|
+
{ icon: kotlinIcon, name: "kotlin", extensions: ["kt", "kts"] },
|
|
145
|
+
{ icon: swiftIcon, name: "swift", extensions: ["swift"] },
|
|
146
|
+
{ icon: cppIcon, name: "cpp", extensions: ["c", "cc", "cpp", "h", "hpp"] },
|
|
147
|
+
{ icon: csharpIcon, name: "csharp", extensions: ["cs"] },
|
|
148
|
+
{ icon: phpIcon, name: "php", extensions: ["php"] },
|
|
149
|
+
{ icon: rubyIcon, name: "ruby", extensions: ["rb"] },
|
|
150
|
+
{ icon: luaIcon, name: "lua", extensions: ["lua"] },
|
|
151
|
+
{
|
|
152
|
+
icon: shellIcon,
|
|
153
|
+
name: "shell",
|
|
154
|
+
extensions: ["sh", "bash", "zsh", "fish", "ps1"],
|
|
155
|
+
},
|
|
156
|
+
{ icon: graphqlIcon, name: "graphql", extensions: ["graphql", "gql"] },
|
|
157
|
+
{ icon: yamlIcon, name: "yaml", extensions: ["yaml", "yml"] },
|
|
158
|
+
{ icon: tomlIcon, name: "toml", extensions: ["toml"] },
|
|
159
|
+
{ icon: xmlIcon, name: "xml", extensions: ["xml"] },
|
|
160
|
+
{ icon: sqlIcon, name: "sql", extensions: ["sql", "sqlite", "db"] },
|
|
161
|
+
{ icon: svgIcon, name: "svg", extensions: ["svg"] },
|
|
162
|
+
{
|
|
163
|
+
icon: imageIcon,
|
|
164
|
+
name: "image",
|
|
165
|
+
extensions: [
|
|
166
|
+
"avif",
|
|
167
|
+
"bmp",
|
|
168
|
+
"gif",
|
|
169
|
+
"heic",
|
|
170
|
+
"ico",
|
|
171
|
+
"jpeg",
|
|
172
|
+
"jpg",
|
|
173
|
+
"png",
|
|
174
|
+
"tif",
|
|
175
|
+
"tiff",
|
|
176
|
+
"webp",
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
icon: audioIcon,
|
|
181
|
+
name: "audio",
|
|
182
|
+
extensions: ["aac", "flac", "m4a", "mp3", "ogg", "opus", "wav", "weba"],
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
icon: videoIcon,
|
|
186
|
+
name: "video",
|
|
187
|
+
extensions: ["avi", "m4v", "mkv", "mov", "mp4", "webm", "wmv"],
|
|
188
|
+
},
|
|
189
|
+
{ icon: pdfIcon, name: "pdf", extensions: ["pdf"] },
|
|
190
|
+
{ icon: wordIcon, name: "word", extensions: ["doc", "docx", "odt", "rtf"] },
|
|
191
|
+
{
|
|
192
|
+
icon: excelIcon,
|
|
193
|
+
name: "excel",
|
|
194
|
+
extensions: ["csv", "numbers", "ods", "tsv", "xls", "xlsx"],
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
icon: powerpointIcon,
|
|
198
|
+
name: "powerpoint",
|
|
199
|
+
extensions: ["odp", "ppt", "pptx"],
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
icon: zipIcon,
|
|
203
|
+
name: "archive",
|
|
204
|
+
extensions: ["7z", "bz2", "gz", "rar", "tar", "tgz", "zip"],
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
icon: fontIcon,
|
|
208
|
+
name: "font",
|
|
209
|
+
extensions: ["eot", "otf", "ttf", "woff", "woff2"],
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
icon: configIcon,
|
|
213
|
+
name: "config",
|
|
214
|
+
extensions: ["cfg", "conf", "ini", "properties"],
|
|
215
|
+
},
|
|
216
|
+
{ icon: textIcon, name: "text", extensions: ["log", "txt"] },
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
const DEFAULT_FILE_VISUAL: FileIconVisual = {
|
|
220
|
+
icon: defaultFileIcon,
|
|
221
|
+
name: "default",
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
function readFileName(value: string): string {
|
|
225
|
+
return value.trim().split(/[\\/]/).filter(Boolean).pop() ?? value.trim();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function readFileExtension(fileName: string): string {
|
|
229
|
+
return /\.([a-z0-9]{1,16})$/i.exec(fileName)?.[1]?.toLowerCase() ?? "";
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function resolveFileIconVisual(value: string): FileIconVisual {
|
|
233
|
+
const fileName = readFileName(value).toLowerCase();
|
|
234
|
+
const specialVisual = SPECIAL_FILE_RULES.find(({ pattern }) =>
|
|
235
|
+
pattern.test(fileName),
|
|
236
|
+
);
|
|
237
|
+
if (specialVisual) {
|
|
238
|
+
return specialVisual;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const extension = readFileExtension(fileName);
|
|
242
|
+
return (
|
|
243
|
+
FILE_ICON_RULES.find(({ extensions }) => extensions.includes(extension)) ??
|
|
244
|
+
DEFAULT_FILE_VISUAL
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function resolveBundledIcon(icon: IconifyIcon): IconifyIcon {
|
|
249
|
+
const bundledIcon = icon as unknown as BundledIcon;
|
|
250
|
+
return "body" in bundledIcon ? bundledIcon : bundledIcon.default;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function FileTypeIcon({
|
|
254
|
+
className,
|
|
255
|
+
fileName,
|
|
256
|
+
size = "default",
|
|
257
|
+
}: FileTypeIconProps) {
|
|
258
|
+
const visual = resolveFileIconVisual(fileName);
|
|
259
|
+
|
|
260
|
+
return (
|
|
261
|
+
<Icon
|
|
262
|
+
aria-hidden="true"
|
|
263
|
+
data-file-type-icon={visual.name}
|
|
264
|
+
icon={resolveBundledIcon(visual.icon)}
|
|
265
|
+
className={cn(
|
|
266
|
+
"shrink-0",
|
|
267
|
+
size === "compact" ? "h-3.5 w-3.5" : "h-4 w-4",
|
|
268
|
+
className,
|
|
269
|
+
)}
|
|
270
|
+
/>
|
|
271
|
+
);
|
|
272
|
+
}
|
package/src/shared/components/resizable-right-panel/__tests__/resizable-right-panel.test.tsx
CHANGED
|
@@ -21,12 +21,14 @@ describe("ResizableRightPanel", () => {
|
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
it("resizes horizontally from the left handle and clamps to max width", () => {
|
|
24
|
+
const onWidthCommit = vi.fn();
|
|
24
25
|
render(
|
|
25
26
|
<ResizableRightPanel
|
|
26
27
|
data-testid="right-panel"
|
|
27
28
|
defaultWidth={420}
|
|
28
29
|
minWidth={320}
|
|
29
30
|
maxWidth={500}
|
|
31
|
+
onWidthCommit={onWidthCommit}
|
|
30
32
|
>
|
|
31
33
|
Content
|
|
32
34
|
</ResizableRightPanel>,
|
|
@@ -46,6 +48,8 @@ describe("ResizableRightPanel", () => {
|
|
|
46
48
|
|
|
47
49
|
firePointerEvent(window, "pointerup", { clientX: 600, pointerId: 1 });
|
|
48
50
|
|
|
51
|
+
expect(onWidthCommit).toHaveBeenCalledWith(500);
|
|
52
|
+
|
|
49
53
|
expect(
|
|
50
54
|
screen.queryByTestId("resizable-right-panel-resize-shield"),
|
|
51
55
|
).toBeNull();
|
|
@@ -64,6 +68,24 @@ describe("ResizableRightPanel", () => {
|
|
|
64
68
|
expect(screen.getByTestId("right-panel").className).toContain("fixed");
|
|
65
69
|
});
|
|
66
70
|
|
|
71
|
+
it("applies a persisted controlled width after store rehydration", () => {
|
|
72
|
+
const view = render(
|
|
73
|
+
<ResizableRightPanel data-testid="right-panel" width={480}>
|
|
74
|
+
Content
|
|
75
|
+
</ResizableRightPanel>,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
expect(screen.getByTestId("right-panel").style.width).toBe("480px");
|
|
79
|
+
|
|
80
|
+
view.rerender(
|
|
81
|
+
<ResizableRightPanel data-testid="right-panel" width={620}>
|
|
82
|
+
Content
|
|
83
|
+
</ResizableRightPanel>,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
expect(screen.getByTestId("right-panel").style.width).toBe("620px");
|
|
87
|
+
});
|
|
88
|
+
|
|
67
89
|
it("can scope overlay positioning to its container", () => {
|
|
68
90
|
render(
|
|
69
91
|
<ResizableRightPanel
|
|
@@ -5,8 +5,10 @@ type ResizableRightPanelProps = ComponentPropsWithoutRef<"aside"> & {
|
|
|
5
5
|
defaultWidth?: number;
|
|
6
6
|
minWidth?: number;
|
|
7
7
|
maxWidth?: number;
|
|
8
|
+
onWidthCommit?: (width: number) => void;
|
|
8
9
|
overlay?: boolean;
|
|
9
10
|
overlayScope?: "viewport" | "container";
|
|
11
|
+
width?: number;
|
|
10
12
|
};
|
|
11
13
|
|
|
12
14
|
export function ResizableRightPanel({
|
|
@@ -16,13 +18,18 @@ export function ResizableRightPanel({
|
|
|
16
18
|
defaultWidth = 420,
|
|
17
19
|
minWidth = 320,
|
|
18
20
|
maxWidth = 860,
|
|
21
|
+
onWidthCommit,
|
|
19
22
|
overlay = false,
|
|
20
23
|
overlayScope = "viewport",
|
|
24
|
+
width: controlledWidth,
|
|
21
25
|
...props
|
|
22
26
|
}: ResizableRightPanelProps) {
|
|
23
27
|
const resizeRef = useRef<{ startX: number; startWidth: number } | null>(null);
|
|
28
|
+
const widthRef = useRef(controlledWidth ?? defaultWidth);
|
|
24
29
|
const [isResizing, setIsResizing] = useState(false);
|
|
25
|
-
const [
|
|
30
|
+
const [dragWidth, setDragWidth] = useState<number | null>(null);
|
|
31
|
+
const [uncontrolledWidth, setUncontrolledWidth] = useState(defaultWidth);
|
|
32
|
+
const width = dragWidth ?? controlledWidth ?? uncontrolledWidth;
|
|
26
33
|
const overlayClassName = overlay
|
|
27
34
|
? overlayScope === "container"
|
|
28
35
|
? "absolute inset-0 z-30"
|
|
@@ -35,14 +42,22 @@ export function ResizableRightPanel({
|
|
|
35
42
|
event.stopPropagation();
|
|
36
43
|
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
37
44
|
setIsResizing(true);
|
|
45
|
+
widthRef.current = width;
|
|
38
46
|
resizeRef.current = { startX: event.clientX, startWidth: width };
|
|
39
47
|
const onMove = (moveEvent: PointerEvent) => {
|
|
40
48
|
const resizing = resizeRef.current;
|
|
41
49
|
if (!resizing) return;
|
|
42
50
|
const nextWidth = resizing.startWidth + resizing.startX - moveEvent.clientX;
|
|
43
|
-
|
|
51
|
+
const clampedWidth = Math.max(minWidth, Math.min(maxWidth, nextWidth));
|
|
52
|
+
widthRef.current = clampedWidth;
|
|
53
|
+
setDragWidth(clampedWidth);
|
|
44
54
|
};
|
|
45
55
|
const onUp = () => {
|
|
56
|
+
if (controlledWidth === undefined) {
|
|
57
|
+
setUncontrolledWidth(widthRef.current);
|
|
58
|
+
}
|
|
59
|
+
onWidthCommit?.(widthRef.current);
|
|
60
|
+
setDragWidth(null);
|
|
46
61
|
setIsResizing(false);
|
|
47
62
|
resizeRef.current = null;
|
|
48
63
|
window.removeEventListener("pointermove", onMove);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
2
3
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
4
|
import {
|
|
4
5
|
CompactTabStrip,
|
|
@@ -126,4 +127,41 @@ describe("CompactTabStrip", () => {
|
|
|
126
127
|
expect(onClose).toHaveBeenCalledTimes(1);
|
|
127
128
|
expect(onSelect).not.toHaveBeenCalled();
|
|
128
129
|
});
|
|
130
|
+
|
|
131
|
+
it("opens a tab action menu without selecting the tab or restoring stale focus after an action", async () => {
|
|
132
|
+
const user = userEvent.setup();
|
|
133
|
+
const onAction = vi.fn();
|
|
134
|
+
const onSelect = vi.fn();
|
|
135
|
+
|
|
136
|
+
render(
|
|
137
|
+
<CompactTabStrip
|
|
138
|
+
tabs={[
|
|
139
|
+
{
|
|
140
|
+
key: "source-tab",
|
|
141
|
+
label: "index.html",
|
|
142
|
+
active: true,
|
|
143
|
+
menuLabel: "File actions",
|
|
144
|
+
menuActions: [
|
|
145
|
+
{
|
|
146
|
+
key: "preview",
|
|
147
|
+
icon: <span />,
|
|
148
|
+
label: "Open preview",
|
|
149
|
+
onClick: onAction,
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
onSelect,
|
|
153
|
+
},
|
|
154
|
+
]}
|
|
155
|
+
actions={[]}
|
|
156
|
+
/>,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const menuTrigger = screen.getByRole("button", { name: "File actions" });
|
|
160
|
+
await user.click(menuTrigger);
|
|
161
|
+
await user.click(screen.getByRole("menuitem", { name: "Open preview" }));
|
|
162
|
+
|
|
163
|
+
expect(onAction).toHaveBeenCalledTimes(1);
|
|
164
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
165
|
+
expect(document.activeElement).not.toBe(menuTrigger);
|
|
166
|
+
});
|
|
129
167
|
});
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import { X } from "lucide-react";
|
|
2
|
+
import { MoreVertical, X } from "lucide-react";
|
|
3
3
|
import { IconActionButton } from "@/shared/components/ui/actions/icon-action-button";
|
|
4
|
+
import {
|
|
5
|
+
Popover,
|
|
6
|
+
PopoverContent,
|
|
7
|
+
PopoverTrigger,
|
|
8
|
+
} from "@/shared/components/ui/popover";
|
|
4
9
|
import {
|
|
5
10
|
Tooltip,
|
|
6
11
|
TooltipContent,
|
|
@@ -15,10 +20,13 @@ export type CompactTabStripTab = {
|
|
|
15
20
|
active: boolean;
|
|
16
21
|
tooltip?: string | null;
|
|
17
22
|
leadingIcon?: React.ReactNode;
|
|
23
|
+
labelClassName?: string;
|
|
18
24
|
badge?: React.ReactNode;
|
|
19
25
|
unreadIndicator?: React.ReactNode;
|
|
20
26
|
closeLabel?: string;
|
|
21
27
|
closePlacement?: "leading-hover" | "trailing";
|
|
28
|
+
menuLabel?: string;
|
|
29
|
+
menuActions?: readonly CompactTabStripAction[];
|
|
22
30
|
onSelect: () => void;
|
|
23
31
|
onClose?: () => void;
|
|
24
32
|
};
|
|
@@ -55,6 +63,77 @@ function closeCompactTab(event: React.MouseEvent<HTMLButtonElement>, onClose: ()
|
|
|
55
63
|
onClose();
|
|
56
64
|
}
|
|
57
65
|
|
|
66
|
+
function CompactTabMenu({ tab }: { tab: CompactTabStripTab }) {
|
|
67
|
+
const [open, setOpen] = React.useState(false);
|
|
68
|
+
const restoreTriggerFocus = React.useRef(true);
|
|
69
|
+
if (!tab.menuActions?.length) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const menuLabel = tab.menuLabel ?? tab.label;
|
|
74
|
+
return (
|
|
75
|
+
<Popover
|
|
76
|
+
open={open}
|
|
77
|
+
onOpenChange={(nextOpen) => {
|
|
78
|
+
if (nextOpen) {
|
|
79
|
+
restoreTriggerFocus.current = true;
|
|
80
|
+
}
|
|
81
|
+
setOpen(nextOpen);
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
<div
|
|
85
|
+
className={cn(
|
|
86
|
+
"-ml-1.5 flex w-0 shrink-0 overflow-hidden opacity-0 transition-[width,margin,opacity] group-hover:ml-0 group-hover:w-5 group-hover:opacity-100 group-focus-within:ml-0 group-focus-within:w-5 group-focus-within:opacity-100",
|
|
87
|
+
open ? "ml-0 w-5 opacity-100" : null,
|
|
88
|
+
)}
|
|
89
|
+
>
|
|
90
|
+
<PopoverTrigger asChild>
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
aria-label={menuLabel}
|
|
94
|
+
onClick={(event) => event.stopPropagation()}
|
|
95
|
+
className="inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-md text-gray-400 transition-colors hover:bg-gray-200 hover:text-gray-700 focus-visible:bg-gray-200 focus-visible:text-gray-700 focus-visible:outline-none"
|
|
96
|
+
>
|
|
97
|
+
<MoreVertical className="h-3.5 w-3.5" />
|
|
98
|
+
</button>
|
|
99
|
+
</PopoverTrigger>
|
|
100
|
+
</div>
|
|
101
|
+
<PopoverContent
|
|
102
|
+
align="end"
|
|
103
|
+
className="w-40 rounded-xl p-1.5"
|
|
104
|
+
onClick={(event) => event.stopPropagation()}
|
|
105
|
+
onCloseAutoFocus={(event) => {
|
|
106
|
+
if (!restoreTriggerFocus.current) {
|
|
107
|
+
event.preventDefault();
|
|
108
|
+
}
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
<div role="menu" aria-label={menuLabel}>
|
|
112
|
+
{tab.menuActions.map((action) => (
|
|
113
|
+
<button
|
|
114
|
+
key={action.key}
|
|
115
|
+
type="button"
|
|
116
|
+
role="menuitem"
|
|
117
|
+
disabled={action.disabled}
|
|
118
|
+
className="flex w-full items-center gap-2 rounded-lg px-2.5 py-2 text-left text-xs text-gray-700 transition-colors hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border disabled:cursor-not-allowed disabled:opacity-45"
|
|
119
|
+
onClick={() => {
|
|
120
|
+
restoreTriggerFocus.current = false;
|
|
121
|
+
setOpen(false);
|
|
122
|
+
action.onClick();
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
<span className="inline-flex h-4 w-4 shrink-0 items-center justify-center">
|
|
126
|
+
{action.icon}
|
|
127
|
+
</span>
|
|
128
|
+
<span>{action.label}</span>
|
|
129
|
+
</button>
|
|
130
|
+
))}
|
|
131
|
+
</div>
|
|
132
|
+
</PopoverContent>
|
|
133
|
+
</Popover>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
58
137
|
function CompactTabLabelButton({
|
|
59
138
|
labelClassName,
|
|
60
139
|
tab,
|
|
@@ -77,7 +156,7 @@ function CompactTabLabelButton({
|
|
|
77
156
|
{leadingIcon}
|
|
78
157
|
</span>
|
|
79
158
|
) : null}
|
|
80
|
-
<span className={cn("min-w-0 truncate text-[12px] font-medium", labelClassName)}>
|
|
159
|
+
<span className={cn("min-w-0 truncate text-[12px] font-medium", labelClassName, tab.labelClassName)}>
|
|
81
160
|
{tab.label}
|
|
82
161
|
</span>
|
|
83
162
|
{tab.badge}
|
|
@@ -145,6 +224,7 @@ function CompactTabItem({
|
|
|
145
224
|
</button>
|
|
146
225
|
) : null}
|
|
147
226
|
<CompactTabLabelButton tab={tab} labelClassName={labelClassName} />
|
|
227
|
+
<CompactTabMenu tab={tab} />
|
|
148
228
|
{tab.onClose && tab.closePlacement !== "leading-hover" ? (
|
|
149
229
|
<IconActionButton
|
|
150
230
|
size="sm"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildServerPathContentUrl } from "@/shared/lib/api/utils/server-path.utils";
|
|
3
|
+
|
|
4
|
+
describe("buildServerPathContentUrl", () => {
|
|
5
|
+
it("uses the query content route for absolute files", () => {
|
|
6
|
+
expect(buildServerPathContentUrl("/tmp/logo.svg")).toContain(
|
|
7
|
+
"/api/server-paths/content?path=%2Ftmp%2Flogo.svg",
|
|
8
|
+
);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("uses the same query content route for Windows files", () => {
|
|
12
|
+
expect(buildServerPathContentUrl("C:\\Users\\me\\logo.svg")).toContain(
|
|
13
|
+
"path=C%3A%5CUsers%5Cme%5Clogo.svg",
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("builds a base-path content URL for relative Markdown images", () => {
|
|
18
|
+
const url = buildServerPathContentUrl("assets/logo.svg", "/tmp/project");
|
|
19
|
+
|
|
20
|
+
expect(url).toContain("/api/server-paths/content?");
|
|
21
|
+
expect(url).toContain("path=assets%2Flogo.svg");
|
|
22
|
+
expect(url).toContain("basePath=%2Ftmp%2Fproject");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("rejects relative content paths when no base path is available", () => {
|
|
26
|
+
expect(buildServerPathContentUrl("assets/logo.svg", null)).toBeNull();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -3,17 +3,6 @@ import { nextclawClient } from '@/shared/lib/api/managers/client.manager';
|
|
|
3
3
|
import type { ServerPathBrowseView, ServerPathReadView } from '@/shared/lib/api/types';
|
|
4
4
|
|
|
5
5
|
const SERVER_PATH_CONTENT_BASE_PATH = '/api/server-paths/content';
|
|
6
|
-
const SERVER_PATH_CONTENT_ABSOLUTE_SCOPE = '__abs__';
|
|
7
|
-
const SERVER_PATH_CONTENT_WINDOWS_SCOPE = '__win__';
|
|
8
|
-
|
|
9
|
-
function encodeServerPathSegments(path: string): string {
|
|
10
|
-
return path
|
|
11
|
-
.replace(/\\/g, '/')
|
|
12
|
-
.split('/')
|
|
13
|
-
.filter(Boolean)
|
|
14
|
-
.map((segment) => encodeURIComponent(segment))
|
|
15
|
-
.join('/');
|
|
16
|
-
}
|
|
17
6
|
|
|
18
7
|
export async function fetchServerPathBrowse(params?: {
|
|
19
8
|
path?: string | null;
|
|
@@ -30,14 +19,24 @@ export async function fetchServerPathRead(params: {
|
|
|
30
19
|
return await nextclawClient.serverPaths.read(params);
|
|
31
20
|
}
|
|
32
21
|
|
|
33
|
-
export function buildServerPathContentUrl(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
22
|
+
export function buildServerPathContentUrl(path: string): string;
|
|
23
|
+
export function buildServerPathContentUrl(
|
|
24
|
+
path: string,
|
|
25
|
+
basePath: string | null,
|
|
26
|
+
): string | null;
|
|
27
|
+
export function buildServerPathContentUrl(
|
|
28
|
+
path: string,
|
|
29
|
+
basePath?: string | null,
|
|
30
|
+
): string | null {
|
|
31
|
+
const normalizedPath = path.trim();
|
|
32
|
+
const isAbsolute = normalizedPath.startsWith("/") || /^[a-z]:[\\/]/i.test(normalizedPath);
|
|
33
|
+
const normalizedBasePath = basePath?.trim() ?? "";
|
|
34
|
+
if (!isAbsolute && !normalizedBasePath) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const query = new URLSearchParams({ path: normalizedPath });
|
|
38
|
+
if (normalizedBasePath) {
|
|
39
|
+
query.set("basePath", normalizedBasePath);
|
|
41
40
|
}
|
|
42
|
-
return `${API_BASE}${SERVER_PATH_CONTENT_BASE_PATH}
|
|
41
|
+
return `${API_BASE}${SERVER_PATH_CONTENT_BASE_PATH}?${query.toString()}`;
|
|
43
42
|
}
|