@aura-react-comp/x 0.0.0-stage → 0.1.1

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 (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -2
  3. package/esm/actions/index.d.ts +28 -0
  4. package/esm/actions/index.js +37 -0
  5. package/esm/actions/index.less +59 -0
  6. package/esm/attachments/index.d.ts +41 -0
  7. package/esm/attachments/index.js +48 -0
  8. package/esm/attachments/index.less +22 -0
  9. package/esm/bubble/index.d.ts +42 -0
  10. package/esm/bubble/index.js +80 -0
  11. package/esm/bubble/index.less +164 -0
  12. package/esm/code-highlighter/index.d.ts +39 -0
  13. package/esm/code-highlighter/index.js +165 -0
  14. package/esm/code-highlighter/index.less +76 -0
  15. package/esm/code-highlighter/prism-theme.d.ts +13 -0
  16. package/esm/code-highlighter/prism-theme.js +75 -0
  17. package/esm/conversations/index.d.ts +35 -0
  18. package/esm/conversations/index.js +89 -0
  19. package/esm/conversations/index.less +138 -0
  20. package/esm/file-card/index.d.ts +40 -0
  21. package/esm/file-card/index.js +126 -0
  22. package/esm/file-card/index.less +161 -0
  23. package/esm/folder/index.d.ts +66 -0
  24. package/esm/folder/index.js +345 -0
  25. package/esm/folder/index.less +158 -0
  26. package/esm/folder/tree-utils.d.ts +30 -0
  27. package/esm/folder/tree-utils.js +147 -0
  28. package/esm/folder/types.d.ts +51 -0
  29. package/esm/folder/types.js +1 -0
  30. package/esm/index.d.ts +40 -0
  31. package/esm/index.js +20 -0
  32. package/esm/markdown-content/index.d.ts +34 -0
  33. package/esm/markdown-content/index.js +117 -0
  34. package/esm/markdown-content/index.less +124 -0
  35. package/esm/mermaid/index.d.ts +47 -0
  36. package/esm/mermaid/index.js +235 -0
  37. package/esm/mermaid/index.less +155 -0
  38. package/esm/notification/index.d.ts +59 -0
  39. package/esm/notification/index.js +148 -0
  40. package/esm/prompts/index.d.ts +27 -0
  41. package/esm/prompts/index.js +50 -0
  42. package/esm/prompts/index.less +95 -0
  43. package/esm/sender/index.d.ts +48 -0
  44. package/esm/sender/index.js +88 -0
  45. package/esm/sender/index.less +75 -0
  46. package/esm/sources/index.d.ts +40 -0
  47. package/esm/sources/index.js +154 -0
  48. package/esm/sources/index.less +219 -0
  49. package/esm/style.css +1565 -0
  50. package/esm/suggestion/index.d.ts +21 -0
  51. package/esm/suggestion/index.js +36 -0
  52. package/esm/suggestion/index.less +44 -0
  53. package/esm/think/index.d.ts +20 -0
  54. package/esm/think/index.js +48 -0
  55. package/esm/think/index.less +92 -0
  56. package/esm/thought-chain/index.d.ts +34 -0
  57. package/esm/thought-chain/index.js +105 -0
  58. package/esm/thought-chain/index.less +174 -0
  59. package/esm/use-x-chat/index.d.ts +85 -0
  60. package/esm/use-x-chat/index.js +178 -0
  61. package/esm/use-x-stream/index.d.ts +43 -0
  62. package/esm/use-x-stream/index.js +112 -0
  63. package/esm/use-x-stream/parse-sse.d.ts +19 -0
  64. package/esm/use-x-stream/parse-sse.js +91 -0
  65. package/esm/utils/color.d.ts +9 -0
  66. package/esm/utils/color.js +45 -0
  67. package/esm/welcome/index.d.ts +22 -0
  68. package/esm/welcome/index.js +36 -0
  69. package/esm/welcome/index.less +57 -0
  70. package/esm/x-provider/index.d.ts +48 -0
  71. package/esm/x-provider/index.js +66 -0
  72. package/package.json +70 -3
@@ -0,0 +1,126 @@
1
+ import React from 'react';
2
+ import { classNames, prefixCls } from '@aura-react-comp/shared';
3
+ import { jsx as _jsx } from "react/jsx-runtime";
4
+ import { jsxs as _jsxs } from "react/jsx-runtime";
5
+ const SIZE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB'];
6
+
7
+ /** 字节数 → 人类可读字符串(B 取整,其余保留一位小数) */
8
+ export function formatFileSize(bytes) {
9
+ if (!Number.isFinite(bytes) || bytes <= 0) return '0 B';
10
+ const exp = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), SIZE_UNITS.length - 1);
11
+ const value = bytes / 1024 ** exp;
12
+ return `${exp === 0 ? Math.round(value) : value.toFixed(1)} ${SIZE_UNITS[exp]}`;
13
+ }
14
+
15
+ /** 取小写扩展名;无扩展名返回空串 */
16
+ export function getFileExt(name) {
17
+ const dot = name.lastIndexOf('.');
18
+ if (dot <= 0 || dot === name.length - 1) return '';
19
+ return name.slice(dot + 1).toLowerCase();
20
+ }
21
+ const clampPercent = value => Math.min(100, Math.max(0, Math.round(value)));
22
+
23
+ /** 内置文件图标(继承 currentColor) */
24
+ const FileGlyph = () => /*#__PURE__*/_jsxs("svg", {
25
+ viewBox: "0 0 20 20",
26
+ fill: "none",
27
+ stroke: "currentColor",
28
+ strokeWidth: "1.4",
29
+ strokeLinecap: "round",
30
+ strokeLinejoin: "round",
31
+ "aria-hidden": "true",
32
+ focusable: "false",
33
+ children: [/*#__PURE__*/_jsx("path", {
34
+ d: "M11.5 2.5H5.5A1.5 1.5 0 0 0 4 4v12a1.5 1.5 0 0 0 1.5 1.5h9A1.5 1.5 0 0 0 16 16V7l-4.5-4.5Z"
35
+ }), /*#__PURE__*/_jsx("path", {
36
+ d: "M11.5 2.5V7H16"
37
+ })]
38
+ });
39
+
40
+ /**
41
+ * FileCard — 文件卡片。
42
+ *
43
+ * 附件列表、引用文件、上传结果的基本展示单元:图标、文件名、大小、
44
+ * 扩展名徽标与状态(上传进度 / 完成 / 失败)。通常配合
45
+ * [`Attachments`](/x-components/attachments) 组成输入框上方的附件条。
46
+ */
47
+ export const FileCard = ({
48
+ name,
49
+ size,
50
+ status = 'init',
51
+ percent,
52
+ description,
53
+ errorTip,
54
+ icon,
55
+ onRemove,
56
+ className,
57
+ style
58
+ }) => {
59
+ const ext = getFileExt(name);
60
+ const clamped = clampPercent(percent ?? 0);
61
+ const showProgress = status === 'uploading' && percent !== undefined;
62
+ return /*#__PURE__*/_jsxs("div", {
63
+ className: classNames(prefixCls('x-file-card'), status !== 'init' && prefixCls(`x-file-card--${status}`), className),
64
+ style: style,
65
+ children: [/*#__PURE__*/_jsx("span", {
66
+ className: prefixCls('x-file-card-icon'),
67
+ "aria-hidden": "true",
68
+ children: icon ?? /*#__PURE__*/_jsx(FileGlyph, {})
69
+ }), /*#__PURE__*/_jsxs("div", {
70
+ className: prefixCls('x-file-card-body'),
71
+ children: [/*#__PURE__*/_jsxs("div", {
72
+ className: prefixCls('x-file-card-main'),
73
+ children: [/*#__PURE__*/_jsx("span", {
74
+ className: prefixCls('x-file-card-name'),
75
+ title: name,
76
+ children: name
77
+ }), status === 'done' ? /*#__PURE__*/_jsx("span", {
78
+ className: prefixCls('x-file-card-check'),
79
+ "aria-hidden": "true",
80
+ children: "\u2713"
81
+ }) : null, onRemove ? /*#__PURE__*/_jsx("button", {
82
+ type: "button",
83
+ className: prefixCls('x-file-card-remove'),
84
+ "aria-label": `移除 ${name}`,
85
+ onClick: () => onRemove(name),
86
+ children: /*#__PURE__*/_jsx("span", {
87
+ "aria-hidden": "true",
88
+ children: "\xD7"
89
+ })
90
+ }) : null]
91
+ }), /*#__PURE__*/_jsxs("div", {
92
+ className: prefixCls('x-file-card-meta'),
93
+ children: [size !== undefined ? /*#__PURE__*/_jsx("span", {
94
+ className: prefixCls('x-file-card-size'),
95
+ children: formatFileSize(size)
96
+ }) : null, ext ? /*#__PURE__*/_jsx("span", {
97
+ className: prefixCls('x-file-card-ext'),
98
+ children: ext
99
+ }) : null, showProgress ? /*#__PURE__*/_jsxs("span", {
100
+ className: prefixCls('x-file-card-percent'),
101
+ children: [clamped, "%"]
102
+ }) : null]
103
+ }), showProgress ? /*#__PURE__*/_jsx("div", {
104
+ className: prefixCls('x-file-card-progress'),
105
+ role: "progressbar",
106
+ "aria-label": `${name} 上传进度`,
107
+ "aria-valuenow": clamped,
108
+ "aria-valuemin": 0,
109
+ "aria-valuemax": 100,
110
+ children: /*#__PURE__*/_jsx("div", {
111
+ className: prefixCls('x-file-card-progress-bar'),
112
+ style: {
113
+ width: `${clamped}%`
114
+ }
115
+ })
116
+ }) : null, status === 'error' && errorTip ? /*#__PURE__*/_jsx("div", {
117
+ className: prefixCls('x-file-card-error-tip'),
118
+ children: errorTip
119
+ }) : description ? /*#__PURE__*/_jsx("div", {
120
+ className: prefixCls('x-file-card-desc'),
121
+ children: description
122
+ }) : null]
123
+ })]
124
+ });
125
+ };
126
+ FileCard.displayName = 'FileCard';
@@ -0,0 +1,161 @@
1
+ /* FileCard — 文件卡片(附件 / 引用文件的展示单元) */
2
+
3
+ .aura-x-file-card {
4
+ display: flex;
5
+ gap: var(--aura-spacing-2);
6
+ align-items: flex-start;
7
+ width: 264px;
8
+ padding: var(--aura-spacing-2) var(--aura-spacing-3);
9
+ border: 1px solid var(--aura-border);
10
+ border-radius: var(--aura-radius-md);
11
+ background: var(--aura-x-bubble-ai-bg);
12
+ transition:
13
+ border-color var(--aura-duration-fast) var(--aura-easing),
14
+ box-shadow var(--aura-duration-fast) var(--aura-easing);
15
+ }
16
+
17
+ .aura-x-file-card:hover {
18
+ border-color: var(--aura-x-bubble-border);
19
+ box-shadow: var(--aura-shadow-sm);
20
+ }
21
+
22
+ .aura-x-file-card-icon {
23
+ display: flex;
24
+ flex: none;
25
+ align-items: center;
26
+ justify-content: center;
27
+ width: 36px;
28
+ height: 36px;
29
+ border-radius: var(--aura-radius-sm);
30
+ background: var(--aura-x-accent-soft);
31
+ color: var(--aura-x-accent);
32
+ }
33
+
34
+ .aura-x-file-card-icon > svg {
35
+ width: 18px;
36
+ height: 18px;
37
+ }
38
+
39
+ .aura-x-file-card-body {
40
+ flex: 1;
41
+ min-width: 0;
42
+ }
43
+
44
+ .aura-x-file-card-main {
45
+ display: flex;
46
+ gap: var(--aura-spacing-1);
47
+ align-items: center;
48
+ }
49
+
50
+ .aura-x-file-card-name {
51
+ overflow: hidden;
52
+ color: var(--aura-text);
53
+ font-size: var(--aura-font-size-sm);
54
+ line-height: 1.5;
55
+ text-overflow: ellipsis;
56
+ white-space: nowrap;
57
+ }
58
+
59
+ .aura-x-file-card-check {
60
+ flex: none;
61
+ color: var(--aura-success);
62
+ font-size: var(--aura-font-size-xs);
63
+ line-height: 1.5;
64
+ }
65
+
66
+ .aura-x-file-card-remove {
67
+ display: flex;
68
+ flex: none;
69
+ align-items: center;
70
+ justify-content: center;
71
+ width: 20px;
72
+ height: 20px;
73
+ margin-inline-start: auto;
74
+ padding: 0;
75
+ border: none;
76
+ border-radius: var(--aura-radius-sm);
77
+ background: transparent;
78
+ color: var(--aura-text-tertiary);
79
+ font-size: var(--aura-font-size-md);
80
+ line-height: 1;
81
+ cursor: pointer;
82
+ transition:
83
+ background var(--aura-duration-fast) var(--aura-easing),
84
+ color var(--aura-duration-fast) var(--aura-easing);
85
+ }
86
+
87
+ .aura-x-file-card-remove:hover {
88
+ background: var(--aura-x-accent-soft);
89
+ color: var(--aura-x-accent);
90
+ }
91
+
92
+ .aura-x-file-card-remove:focus-visible {
93
+ outline: none;
94
+ box-shadow: var(--aura-x-focus-ring);
95
+ }
96
+
97
+ .aura-x-file-card-meta {
98
+ display: flex;
99
+ gap: var(--aura-spacing-2);
100
+ align-items: center;
101
+ margin-top: 2px;
102
+ color: var(--aura-text-tertiary);
103
+ font-size: var(--aura-font-size-xs);
104
+ line-height: 1.5;
105
+ }
106
+
107
+ .aura-x-file-card-ext {
108
+ padding: 0 4px;
109
+ border-radius: var(--aura-radius-full);
110
+ background: var(--aura-bg-tertiary);
111
+ text-transform: uppercase;
112
+ }
113
+
114
+ .aura-x-file-card-progress {
115
+ height: 4px;
116
+ margin-top: var(--aura-spacing-1);
117
+ border-radius: var(--aura-radius-full);
118
+ background: var(--aura-bg-tertiary);
119
+ overflow: hidden;
120
+ }
121
+
122
+ .aura-x-file-card-progress-bar {
123
+ height: 100%;
124
+ border-radius: inherit;
125
+ background: var(--aura-x-accent-gradient);
126
+ transition: width var(--aura-duration-normal) var(--aura-easing);
127
+ }
128
+
129
+ .aura-x-file-card-desc {
130
+ margin-top: var(--aura-spacing-1);
131
+ color: var(--aura-text-secondary);
132
+ font-size: var(--aura-font-size-xs);
133
+ line-height: 1.5;
134
+ }
135
+
136
+ /* 失败态 */
137
+ .aura-x-file-card--error {
138
+ border-color: var(--aura-error);
139
+ }
140
+
141
+ .aura-x-file-card--error .aura-x-file-card-icon {
142
+ background: var(--aura-error-light);
143
+ color: var(--aura-error);
144
+ }
145
+
146
+ .aura-x-file-card--error .aura-x-file-card-size {
147
+ color: var(--aura-error);
148
+ }
149
+
150
+ .aura-x-file-card-error-tip {
151
+ margin-top: var(--aura-spacing-1);
152
+ color: var(--aura-error);
153
+ font-size: var(--aura-font-size-xs);
154
+ line-height: 1.5;
155
+ }
156
+
157
+ /* 成功态 */
158
+ .aura-x-file-card--done .aura-x-file-card-icon {
159
+ background: var(--aura-success-light);
160
+ color: var(--aura-success);
161
+ }
@@ -0,0 +1,66 @@
1
+ import React from 'react';
2
+ import type { MenuProps } from 'antd';
3
+ import type { FileContentService, FolderDirectoryIcons, FolderPreviewFile, FolderPreviewRenderInfo, FolderRef, FolderSelectedFile, FolderTreeData } from './types';
4
+ export type { FileContentService, FolderContextMenuItem, FolderDirectoryIcons, FolderPreviewFile, FolderPreviewRenderInfo, FolderRef, FolderSelectedFile, FolderTreeData, } from './types';
5
+ type FolderMenuSpec = MenuProps['items'] | ((node: FolderTreeData, key: string) => MenuProps['items']);
6
+ export interface FolderProps {
7
+ /** 文件树数据 */
8
+ treeData?: FolderTreeData[];
9
+ /** 是否开启选择 @default true */
10
+ selectable?: boolean;
11
+ /** 选中的文件路径(受控) */
12
+ selectedFile?: string[];
13
+ /** 默认选中的文件路径 @default [] */
14
+ defaultSelectedFile?: string[];
15
+ onSelectedFileChange?: (file: FolderSelectedFile) => void;
16
+ /** 目录树宽度 @default 278 */
17
+ directoryTreeWith?: number | string;
18
+ /**
19
+ * 空状态展示内容:
20
+ * - 覆盖「树为空」与「未选中任何文件」两处默认提示
21
+ * - 传 `false` 则不展示
22
+ */
23
+ emptyRender?: false | React.ReactNode | (() => React.ReactNode);
24
+ /** 自定义预览内容;函数形式可拿到文件信息与默认预览节点 */
25
+ previewRender?: React.ReactNode | ((file: FolderPreviewFile, info: FolderPreviewRenderInfo) => React.ReactNode);
26
+ /** 展开的节点 key 数组(受控);key = path 数组以 `/` 连接 */
27
+ expandedPaths?: string[];
28
+ /** 默认展开的节点 key 数组 */
29
+ defaultExpandedPaths?: string[];
30
+ /** 是否默认展开全部文件夹 @default true */
31
+ defaultExpandAll?: boolean;
32
+ onExpandedPathsChange?: (paths: string[]) => void;
33
+ /** 文件内容服务:文件没有内联 `content` 时按需拉取 */
34
+ fileContentService?: FileContentService;
35
+ onFileClick?: (filePath: string, content?: string) => void;
36
+ onFolderClick?: (folderPath: string) => void;
37
+ /** 目录树标题;传 `false` 不展示 */
38
+ directoryTitle?: false | React.ReactNode | (() => React.ReactNode);
39
+ /** 预览区标题;缺省用文件名,函数形式可自定义 */
40
+ previewTitle?: string | ((info: {
41
+ title: string;
42
+ path: string[];
43
+ content: string;
44
+ }) => React.ReactNode);
45
+ /**
46
+ * 图标配置:`false` 不展示图标;`directory` 键对应文件夹,
47
+ * 其余键按**扩展名**匹配文件(如 `tsx`),未命中回退内置图标
48
+ */
49
+ directoryIcons?: FolderDirectoryIcons;
50
+ /** 右键菜单:全局配置或按节点返回;节点自身的 `contextMenu` 优先级更高 */
51
+ contextMenu?: FolderMenuSpec;
52
+ onRightClick?: (info: {
53
+ event: React.MouseEvent;
54
+ node: FolderTreeData;
55
+ }) => void;
56
+ className?: string;
57
+ style?: React.CSSProperties;
58
+ }
59
+ /**
60
+ * Folder — 文件树。
61
+ *
62
+ * 左侧目录树 + 右侧文件预览:目录树支持展开 / 收起、选中、右键菜单与图标定制;
63
+ * 预览区可选择内联 `content`,或通过 `fileContentService` 按需拉取。
64
+ * 树的增删改用 `ref` 上的不可变换算方法完成(返回新 treeData,由调用方接管)。
65
+ */
66
+ export declare const Folder: React.ForwardRefExoticComponent<FolderProps & React.RefAttributes<FolderRef>>;
@@ -0,0 +1,345 @@
1
+ import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react';
2
+ import { Dropdown } from 'antd';
3
+ import { classNames, prefixCls } from '@aura-react-comp/shared';
4
+ import { CodeHighlighter } from "../code-highlighter";
5
+ import { addNodeAt, collectFolderKeys, extOf, findNode, isFolder, isSamePath, keyOfPath, languageOf, removeNodeAt, updateNodeAt } from "./tree-utils";
6
+ import { jsx as _jsx } from "react/jsx-runtime";
7
+ import { jsxs as _jsxs } from "react/jsx-runtime";
8
+ /** 默认预览区在「未选中 / 选中文件夹」时的提示 */
9
+ const DEFAULT_PLACEHOLDER = '请选择一个文件';
10
+ /** 文件夹图标(继承 currentColor) */
11
+ const FolderGlyph = () => /*#__PURE__*/_jsx("svg", {
12
+ viewBox: "0 0 16 16",
13
+ fill: "none",
14
+ stroke: "currentColor",
15
+ strokeWidth: "1.3",
16
+ "aria-hidden": "true",
17
+ focusable: "false",
18
+ children: /*#__PURE__*/_jsx("path", {
19
+ d: "M1.5 12.5v-9A1 1 0 0 1 2.5 2.5h3.2l1.3 1.6h6.5a1 1 0 0 1 1 1v7.4a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1Z"
20
+ })
21
+ });
22
+
23
+ /** 文件图标(继承 currentColor) */
24
+ const FileGlyph = () => /*#__PURE__*/_jsxs("svg", {
25
+ viewBox: "0 0 16 16",
26
+ fill: "none",
27
+ stroke: "currentColor",
28
+ strokeWidth: "1.3",
29
+ strokeLinejoin: "round",
30
+ "aria-hidden": "true",
31
+ focusable: "false",
32
+ children: [/*#__PURE__*/_jsx("path", {
33
+ d: "M9 1.8H4.4a1 1 0 0 0-1 1v10.4a1 1 0 0 0 1 1h7.2a1 1 0 0 0 1-1V5.2L9 1.8Z"
34
+ }), /*#__PURE__*/_jsx("path", {
35
+ d: "M9 1.8v3.4h3.6"
36
+ })]
37
+ });
38
+ function renderSlot(slot) {
39
+ if (slot === false || slot === undefined) return null;
40
+ return typeof slot === 'function' ? slot() : slot;
41
+ }
42
+
43
+ /**
44
+ * Folder — 文件树。
45
+ *
46
+ * 左侧目录树 + 右侧文件预览:目录树支持展开 / 收起、选中、右键菜单与图标定制;
47
+ * 预览区可选择内联 `content`,或通过 `fileContentService` 按需拉取。
48
+ * 树的增删改用 `ref` 上的不可变换算方法完成(返回新 treeData,由调用方接管)。
49
+ */
50
+ export const Folder = /*#__PURE__*/forwardRef(({
51
+ treeData = [],
52
+ selectable = true,
53
+ selectedFile,
54
+ defaultSelectedFile = [],
55
+ onSelectedFileChange,
56
+ directoryTreeWith = 278,
57
+ emptyRender,
58
+ previewRender,
59
+ expandedPaths,
60
+ defaultExpandedPaths,
61
+ defaultExpandAll = true,
62
+ onExpandedPathsChange,
63
+ fileContentService,
64
+ onFileClick,
65
+ onFolderClick,
66
+ directoryTitle,
67
+ previewTitle,
68
+ directoryIcons,
69
+ contextMenu,
70
+ onRightClick,
71
+ className,
72
+ style
73
+ }, ref) => {
74
+ const [innerSelected, setInnerSelected] = useState(defaultSelectedFile);
75
+ const [innerExpanded, setInnerExpanded] = useState(() => {
76
+ if (defaultExpandedPaths) return new Set(defaultExpandedPaths);
77
+ return defaultExpandAll ? new Set(collectFolderKeys(treeData)) : new Set();
78
+ });
79
+ const [content, setContent] = useState({
80
+ status: 'idle'
81
+ });
82
+ const isSelectedControlled = selectedFile !== undefined;
83
+ const activePath = isSelectedControlled ? selectedFile : innerSelected;
84
+ const isExpandedControlled = expandedPaths !== undefined;
85
+ const expanded = useMemo(() => isExpandedControlled ? new Set(expandedPaths) : innerExpanded, [isExpandedControlled, expandedPaths, innerExpanded]);
86
+ const selectedNode = useMemo(() => findNode(treeData, activePath), [treeData, activePath]);
87
+ const selectedKey = keyOfPath(activePath);
88
+ const selectedName = selectedNode?.title ?? '';
89
+
90
+ // ===== 预览内容解析:内联 content 优先,其次交给 fileContentService =====
91
+ useEffect(() => {
92
+ if (!selectedNode || isFolder(selectedNode)) {
93
+ setContent({
94
+ status: 'idle'
95
+ });
96
+ return undefined;
97
+ }
98
+ if (selectedNode.content !== undefined) {
99
+ setContent({
100
+ status: 'ready',
101
+ text: selectedNode.content
102
+ });
103
+ return undefined;
104
+ }
105
+ if (!fileContentService) {
106
+ setContent({
107
+ status: 'ready',
108
+ text: ''
109
+ });
110
+ return undefined;
111
+ }
112
+ let cancelled = false;
113
+ setContent({
114
+ status: 'loading'
115
+ });
116
+ fileContentService.loadFileContent(selectedKey).then(text => {
117
+ if (!cancelled) setContent({
118
+ status: 'ready',
119
+ text
120
+ });
121
+ }).catch(error => {
122
+ if (cancelled) return;
123
+ setContent({
124
+ status: 'error',
125
+ error: error instanceof Error ? error.message : String(error)
126
+ });
127
+ });
128
+ return () => {
129
+ cancelled = true;
130
+ };
131
+ }, [selectedNode, selectedKey, fileContentService]);
132
+ const toggleExpand = useCallback(key => {
133
+ const next = new Set(expanded);
134
+ if (next.has(key)) next.delete(key);else next.add(key);
135
+ if (!isExpandedControlled) setInnerExpanded(next);
136
+ onExpandedPathsChange?.([...next]);
137
+ }, [expanded, isExpandedControlled, onExpandedPathsChange]);
138
+ const select = useCallback((node, path) => {
139
+ if (!selectable) return;
140
+ const folder = isFolder(node);
141
+ if (!isSelectedControlled) setInnerSelected(path);
142
+ if (folder) onFolderClick?.(keyOfPath(path));else onFileClick?.(keyOfPath(path), node.content);
143
+ onSelectedFileChange?.({
144
+ path,
145
+ name: node.title,
146
+ ...(folder ? {} : {
147
+ content: node.content
148
+ })
149
+ });
150
+ }, [selectable, isSelectedControlled, onFolderClick, onFileClick, onSelectedFileChange]);
151
+ const activate = useCallback((node, path) => {
152
+ if (isFolder(node)) toggleExpand(keyOfPath(path));
153
+ select(node, path);
154
+ }, [toggleExpand, select]);
155
+ useImperativeHandle(ref, () => ({
156
+ getNode: path => findNode(treeData, path),
157
+ updateNode: (path, data) => updateNodeAt(treeData, path, data),
158
+ deleteNode: path => removeNodeAt(treeData, path),
159
+ addNode: (parentPath, node) => addNodeAt(treeData, parentPath, node)
160
+ }), [treeData]);
161
+ const resolveIcon = node => {
162
+ if (directoryIcons === false) return null;
163
+ const folder = isFolder(node);
164
+ if (directoryIcons) {
165
+ const spec = folder ? directoryIcons.directory : directoryIcons[extOf(node.title)];
166
+ if (spec !== undefined) {
167
+ return typeof spec === 'function' ? spec() : spec;
168
+ }
169
+ }
170
+ return folder ? /*#__PURE__*/_jsx(FolderGlyph, {}) : /*#__PURE__*/_jsx(FileGlyph, {});
171
+ };
172
+ const resolveMenu = (node, key) => {
173
+ if (node.contextMenu === false) return undefined;
174
+ const spec = node.contextMenu ?? contextMenu;
175
+ if (!spec) return undefined;
176
+ if (typeof spec === 'function') {
177
+ // antd 的 ItemType 本身也可能是函数(自定义渲染),TS 无法据此区分出
178
+ // 我们的自定义签名,这里显式标注后调用,避免把两者求成交集类型。
179
+ const resolve = spec;
180
+ return resolve(node, key) ?? undefined;
181
+ }
182
+ return spec;
183
+ };
184
+ const renderNodes = (nodes, parentPath, level) => nodes.map(node => {
185
+ const path = [...parentPath, node.path];
186
+ const key = keyOfPath(path);
187
+ const folder = isFolder(node);
188
+ const open = expanded.has(key);
189
+ const selected = isSamePath(path, activePath);
190
+ const menu = resolveMenu(node, key);
191
+ const row = /*#__PURE__*/_jsxs("div", {
192
+ role: "treeitem",
193
+ "aria-level": level,
194
+ "aria-selected": selected,
195
+ "aria-expanded": folder ? open : undefined,
196
+ tabIndex: 0,
197
+ className: classNames(prefixCls('x-folder-node'), selected && prefixCls('x-folder-node--selected'), folder && prefixCls('x-folder-node--folder')),
198
+ onClick: () => activate(node, path),
199
+ onContextMenu: event => {
200
+ onRightClick?.({
201
+ event,
202
+ node
203
+ });
204
+ },
205
+ onKeyDown: event => {
206
+ if (event.key === 'Enter' || event.key === ' ') {
207
+ event.preventDefault();
208
+ activate(node, path);
209
+ return;
210
+ }
211
+ if (!folder) return;
212
+ if (event.key === 'ArrowRight' && !open) {
213
+ event.preventDefault();
214
+ toggleExpand(key);
215
+ }
216
+ if (event.key === 'ArrowLeft' && open) {
217
+ event.preventDefault();
218
+ toggleExpand(key);
219
+ }
220
+ },
221
+ children: [/*#__PURE__*/_jsx("span", {
222
+ className: prefixCls('x-folder-caret'),
223
+ "data-open": folder ? open : undefined,
224
+ "aria-hidden": "true",
225
+ children: folder ? open ? '▾' : '▸' : ''
226
+ }), /*#__PURE__*/_jsx("span", {
227
+ className: prefixCls('x-folder-node-icon'),
228
+ "aria-hidden": "true",
229
+ children: resolveIcon(node)
230
+ }), /*#__PURE__*/_jsx("span", {
231
+ className: prefixCls('x-folder-node-title'),
232
+ children: node.title
233
+ })]
234
+ });
235
+ return /*#__PURE__*/_jsxs(React.Fragment, {
236
+ children: [menu ? /*#__PURE__*/_jsx(Dropdown, {
237
+ menu: {
238
+ items: menu
239
+ },
240
+ trigger: ['contextMenu'],
241
+ children: row
242
+ }) : row, folder && open && (node.children?.length ?? 0) > 0 ? /*#__PURE__*/_jsx("div", {
243
+ role: "group",
244
+ children: renderNodes(node.children ?? [], path, level + 1)
245
+ }) : null]
246
+ }, key);
247
+ });
248
+ const fileInfo = {
249
+ content: content.text,
250
+ path: activePath,
251
+ title: selectedName || undefined,
252
+ language: languageOf(selectedName)
253
+ };
254
+ const emptyNode = (() => {
255
+ if (emptyRender === false) return null;
256
+ if (emptyRender !== undefined) return renderSlot(emptyRender);
257
+ return /*#__PURE__*/_jsx("div", {
258
+ className: prefixCls('x-folder-empty'),
259
+ children: DEFAULT_PLACEHOLDER
260
+ });
261
+ })();
262
+ const originPreview = (() => {
263
+ if (!selectedNode) return emptyNode;
264
+ if (isFolder(selectedNode)) {
265
+ if (emptyRender === false) return null;
266
+ return /*#__PURE__*/_jsx("div", {
267
+ className: prefixCls('x-folder-empty'),
268
+ children: emptyRender !== undefined ? renderSlot(emptyRender) : `文件夹 · ${selectedNode.children?.length ?? 0} 项`
269
+ });
270
+ }
271
+ if (content.status === 'loading') {
272
+ return /*#__PURE__*/_jsx("div", {
273
+ className: prefixCls('x-folder-loading'),
274
+ "aria-busy": "true",
275
+ children: "\u6B63\u5728\u52A0\u8F7D\u6587\u4EF6\u5185\u5BB9\u2026"
276
+ });
277
+ }
278
+ if (content.status === 'error') {
279
+ return /*#__PURE__*/_jsx("div", {
280
+ className: prefixCls('x-folder-error'),
281
+ role: "alert",
282
+ children: content.error
283
+ });
284
+ }
285
+ return /*#__PURE__*/_jsx(CodeHighlighter, {
286
+ lang: fileInfo.language,
287
+ header: false,
288
+ children: content.text ?? ''
289
+ });
290
+ })();
291
+ const previewBody = previewRender === undefined ? originPreview : typeof previewRender === 'function' ? previewRender(fileInfo, {
292
+ originNode: originPreview
293
+ }) : previewRender;
294
+ const previewTitleNode = (() => {
295
+ if (previewTitle === undefined) {
296
+ return selectedName ? /*#__PURE__*/_jsx("span", {
297
+ className: prefixCls('x-folder-preview-title'),
298
+ children: selectedName
299
+ }) : null;
300
+ }
301
+ if (typeof previewTitle === 'function') {
302
+ return previewTitle({
303
+ title: selectedName,
304
+ path: activePath,
305
+ content: content.text ?? ''
306
+ });
307
+ }
308
+ return /*#__PURE__*/_jsx("span", {
309
+ className: prefixCls('x-folder-preview-title'),
310
+ children: previewTitle
311
+ });
312
+ })();
313
+ return /*#__PURE__*/_jsxs("div", {
314
+ className: classNames(prefixCls('x-folder'), className),
315
+ style: style,
316
+ children: [/*#__PURE__*/_jsxs("div", {
317
+ className: prefixCls('x-folder-directory'),
318
+ style: {
319
+ width: directoryTreeWith
320
+ },
321
+ children: [directoryTitle !== false ? /*#__PURE__*/_jsx("div", {
322
+ className: prefixCls('x-folder-directory-title'),
323
+ children: directoryTitle === undefined ? '文件' : renderSlot(directoryTitle)
324
+ }) : null, treeData.length === 0 ? emptyRender === false ? null : /*#__PURE__*/_jsx("div", {
325
+ className: prefixCls('x-folder-empty'),
326
+ children: emptyRender !== undefined ? renderSlot(emptyRender) : '暂无文件'
327
+ }) : /*#__PURE__*/_jsx("div", {
328
+ role: "tree",
329
+ "aria-label": "\u6587\u4EF6\u6811",
330
+ className: prefixCls('x-folder-tree'),
331
+ children: renderNodes(treeData, [], 1)
332
+ })]
333
+ }), /*#__PURE__*/_jsxs("div", {
334
+ className: prefixCls('x-folder-preview'),
335
+ children: [previewTitleNode ? /*#__PURE__*/_jsx("div", {
336
+ className: prefixCls('x-folder-preview-header'),
337
+ children: previewTitleNode
338
+ }) : null, /*#__PURE__*/_jsx("div", {
339
+ className: prefixCls('x-folder-preview-body'),
340
+ children: previewBody
341
+ })]
342
+ })]
343
+ });
344
+ });
345
+ Folder.displayName = 'Folder';