knight-ui 1.2.5 → 1.2.6

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/README.md CHANGED
@@ -83,6 +83,7 @@
83
83
  - [Spin 加载中](#spin-加载中)
84
84
  - [Feedback 反馈组件](#feedback-反馈组件)
85
85
  - Plus 增强
86
+ - [AttachmentPicker 附件选择器](#attachmentpicker-附件选择器)
86
87
  - [Markdown](#markdown)
87
88
  - [CodeEditor 代码编辑器](#codeeditor-代码编辑器)
88
89
  - [CodeHighlight 代码高亮](#codehighlight-代码高亮)
@@ -97,6 +98,8 @@
97
98
  - [AIChatInput AI 对话输入](#aichatinput-ai-对话输入)
98
99
  - [AIChatDialogue AI 对话展示](#aichatdialogue-ai-对话展示)
99
100
  - [Sidebar AI 侧栏](#sidebar-ai-侧栏)
101
+ - Utility 工具
102
+ - [AttachmentUtility 附件工具](#attachmentutility-附件工具)
100
103
  - Other 其他
101
104
  - [ConfigProvider 全局配置](#configprovider-全局配置)
102
105
  - [LocaleProvider 国际化](#localeprovider-国际化)
@@ -250,8 +253,8 @@ Token(`--ku-color-glass` / `-strong` / `-head`、`--ku-blur-base` … `-xl`)
250
253
  |---|---|---|
251
254
  | `ku-glass` | 容器级(侧栏、卡片、面板) | `--ku-color-glass` / `--ku-blur-lg` |
252
255
  | `ku-glass-head` | 头部、顶栏 | `--ku-color-glass-head` / `--ku-blur-lg` |
253
- | `ku-glass-overlay` | 浮层(弹窗、抽屉、筛选面板、Toast / Notification / Feedback,以及 Cascader / AutoComplete / TreeSelect / DatePicker / TimePicker / ColorPicker 的弹层) | `--ku-color-glass-strong` / `--ku-blur-xl` |
254
- | `ku-glass-menu` | 选项面板(`Dropdown` 与 `Select` 的菜单;其余下拉类弹层尚未换档) | `--ku-color-glass-menu` / `--ku-blur-menu` |
256
+ | `ku-glass-overlay` | 浮层(弹窗、抽屉、筛选面板、Toast / Notification / Feedback,以及 DatePicker / TimePicker / ColorPicker 的弹层) | `--ku-color-glass-strong` / `--ku-blur-xl` |
257
+ | `ku-glass-menu` | 选项面板(`Dropdown` / `Select` / `AutoComplete` 的菜单,以及 `Cascader` / `TreeSelect` 的下拉)—— 判据是"要逐行读的密集文字选项面" | `--ku-color-glass-menu` / `--ku-blur-menu` |
255
258
  | `ku-glass-tip` | 小浮层气泡(Popover / Tooltip / Popconfirm / AudioPlayer 与 VideoPlayer 的音量条与倍速菜单 / Chat 的表情面板;`FloatButton` 的按钮也走这一档) | `--ku-color-glass-tip` / `--ku-blur-tip` / `--ku-glass-grain`(`Popconfirm` 再叠一层 `ku-glass-tip-popconfirm` 换膜) |
256
259
  | `ku-glass-subtle` | 表格级(面积大,糊太重会糊掉内容) | `--ku-color-glass` / `--ku-blur-base` |
257
260
  | `ku-mask` | 遮罩(Modal / SideSheet / Image 预览 / UserGuide / Cropper 裁剪蒙版 / Spin 覆盖层) | `--ku-color-mask` / `--ku-blur-mask` |
@@ -468,7 +471,7 @@ Token(`--ku-color-glass` / `-strong` / `-head`、`--ku-blur-base` … `-xl`)
468
471
  | `ku-glass*` / `ku-mask` | 玻璃底色 + 模糊 + `@supports` 回退 | `backdrop-filter` 与 `@supports` 都写不进内联 |
469
472
  | `ku-table-*`(行 / 表头 / 排序 / 筛选) | 条纹、悬停、选中、表头吸顶 | 行与表头的内联不含底色 |
470
473
  | `ku-calendar-cell*` | 悬停、今天、选中 | 单元格内联刻意只留布局属性 |
471
- | `ku-select-option*` / `ku-dropdown-item*` / `ku-collapse-header` / `ku-table-filter-item` | 悬停底、选中底、展开过渡 | 内联没写底色(只有 `cursor` 被内联占了) |
474
+ | `ku-select-option*` / `ku-dropdown-item*` / `ku-menu-option*`(`Cascader` / `TreeSelect` 的选项行)/ `ku-collapse-header` / `ku-table-filter-item` | 悬停底、选中底、展开过渡 | 内联没写底色(只有 `cursor` 被内联占了) |
472
475
  | `ku-tag` / `ku-scrollbar` / `ku-float-button` | 过渡、滚动条伪元素、悬浮/展开态、玻璃档 | 内联未涉及这些属性;`ku-float-button` 还独占了三项:**底色**(内联已不再写 `background-color`)、**UA 重置**(`appearance: none` / `border: none` —— 少了下标就会露出 UA 的原生 `outset` 斜角环)、以及**阴影的两个状态**(`:hover` 的 `-lg` 与 `box-shadow` 过渡都写不了内联,组件里那句内联 `boxShadow` 因此是删掉的) |
473
476
  | `ku-layout-sider-trigger` | 折叠钮的**几何**(绝对定位骑在侧栏右边缘、24px 圆钮)、**UA 重置**(`appearance: none` 等)与不透明底色 | 这些属性内联没写——钮要压出侧栏边缘,位置只能由类给;UA 重置同理,写不进内联的语义里 |
474
477
  | `ku-userguide-card` | UserGuide 引导卡片的**不透明**底(`--ku-color-bg-3`) | 卡片不挂玻璃,底色改由类提供(内联原本就没写 `background-color`) |
@@ -1212,12 +1215,59 @@ const data = ["Apple", "Banana", "Orange", "Grape"];
1212
1215
  />
1213
1216
  ```
1214
1217
 
1218
+ **"可选可输"**(候选里挑一个,或手打一个候选外的值):
1219
+
1220
+ ```tsx
1221
+ const models = ["deepseek-chat", "deepseek-reasoner", "qwen-plus"];
1222
+ const [model, setModel] = React.useState("");
1223
+
1224
+ <AutoComplete
1225
+ data={models}
1226
+ value={model}
1227
+ onChange={setModel} // 手输与选中都走这一个
1228
+ onBlur={(value) => setModel(value.trim())} // 失焦时收尾
1229
+ />
1230
+ ```
1231
+
1232
+ **异步拉候选**(聚焦即拉,敲键再按关键词拉):
1233
+
1234
+ ```tsx
1235
+ const [data, setData] = React.useState<string[]>([]);
1236
+ const [loading, setLoading] = React.useState(false);
1237
+
1238
+ <AutoComplete
1239
+ data={data}
1240
+ loading={loading} // 必须: 拉取期间置 true
1241
+ filter={false} // 必须: 由服务端过滤, 本地别再筛一遍
1242
+ onFocus={() => fetchCandidates("")}
1243
+ onSearch={(keyword) => fetchCandidates(keyword)}
1244
+ onChange={setValue}
1245
+ />
1246
+ ```
1247
+
1215
1248
  | 属性 | 类型 | 默认值 | 说明 |
1216
1249
  |---|---|---|---|
1217
1250
  | `data` | `(string \| AutoCompleteOption)[]` | — | 数据源 |
1218
- | `filter` | `boolean` | — | 本地过滤 |
1219
- | `onSearch` | `(inputValue) => void` | — | 搜索回调 |
1220
- | `onSelect` | `(option) => void` | — | 选中回调 |
1251
+ | `value / defaultValue` | `string` | — | 值(受控 / 非受控) |
1252
+ | `filter` | `boolean` | `true` | 本地过滤;关掉即"输入什么就显示什么"(服务端过滤时用) |
1253
+ | `onChange` | `(value, option?) => void` | — | 值变化回调,**手输与选中都触发**(手输时 `option` 为空) |
1254
+ | `onSearch` | `(inputValue) => void` | — | 搜索回调,敲键时与 `onChange` 同一时刻触发 |
1255
+ | `onFocus` | `(value) => void` | — | 聚焦,交出聚焦那一刻的文本 |
1256
+ | `onBlur` | `(value) => void` | — | 失焦,交出离开时的文本 |
1257
+ | `onSelect` | `(option) => void` | — | 选中回调(要选项对象、含 `extra` 时用它) |
1258
+ | `renderItem` | `(option) => ReactNode` | — | 自定义渲染候选项 |
1259
+ | `emptyContent` | `ReactNode` | `"无匹配结果"` | 空数据提示 |
1260
+ | `loading` | `boolean` | — | 加载中 |
1261
+ | `size` | `"small" \| "default" \| "large"` | `"default"` | 尺寸 |
1262
+ | `placeholder` | `string` | — | 占位符 |
1263
+
1264
+ > **一个"值来源",三个语义不同的回调**:`onChange` 管"值变了"(手输与选中都触发,**要拿当前值只接它就够**);`onSearch` 管"该搜了"(敲键时与 `onChange` 同一时刻触发,挂异步请求用);`onBlur` 管"输完了"(失焦一次,用来 `trim` / 规范化 / 校验这个值是否已知)。三个都留着各接各的 —— 合并会逼着异步场景和值同步场景共用一个口子。
1265
+ >
1266
+ > **"可选可输"就是 `onChange` + `onBlur` 两个口子**:`onChange` 保证手打一个候选外的值也有人告诉你(这是这个用法唯一容易踩的地方),`onBlur` 给你一个收尾的落点。要选项对象(例如带 `extra`)时另接 `onSelect`。
1267
+ >
1268
+ > **异步拉候选的两个约定**:① 拉取期间把 `loading` 置 `true` —— 不置的话 `data` 还空着,下拉会先闪一下"无匹配结果";② 由服务端过滤时 `filter` 传 `false`,否则本地会把服务端返回的结果再筛一遍。
1269
+ >
1270
+ > 聚焦与失焦都是**先回调再动下拉**,且给的是**那一刻的文本**,不随下拉的开合变化。下拉本身的收起有 150ms 延迟(选项的 `mousedown` 要抢在 `blur` 之前生效,否则点不中候选项)。
1221
1271
 
1222
1272
  ### Transfer 穿梭框
1223
1273
 
@@ -2551,6 +2601,39 @@ import { Feedback } from "knight-ui";
2551
2601
 
2552
2602
  ## Plus 增强
2553
2603
 
2604
+ ### AttachmentPicker 附件选择器
2605
+
2606
+ ```tsx
2607
+ import { AttachmentPicker, IconPaperclip } from "knight-ui";
2608
+
2609
+ <AttachmentPicker
2610
+ accept="image/*,.pdf"
2611
+ maxSize={5 * 1024 * 1024}
2612
+ maxCount={3}
2613
+ count={attachments.length}
2614
+ onPick={(files) => addFiles(files)}
2615
+ onValidateFail={(reason) => Toast.warning(reason.message)}
2616
+ >
2617
+ <IconPaperclip size="small" />
2618
+ </AttachmentPicker>
2619
+ ```
2620
+
2621
+ | 属性 | 类型 | 默认值 | 说明 |
2622
+ |---|---|---|---|
2623
+ | `accept` | `string` | — | 可收类型,与 `<input accept>` 同语法(如 `"image/*,.pdf"`) |
2624
+ | `multiple` | `boolean` | `true` | 是否多选 |
2625
+ | `maxCount` | `number` | — | 数量上限,与 `count` 一起算剩余名额 |
2626
+ | `maxSize` | `number` | — | 单个文件字节上限 |
2627
+ | `count` | `number` | — | 已有数量 —— 由调用方喂当前附件数 |
2628
+ | `onPick` | `(files: Array<File>) => void` | — | 通过校验的文件 |
2629
+ | `onValidateFail` | `(reason: RejectReason) => void` | — | 未通过校验,见 [AttachmentUtility](#attachmentutility-附件工具) |
2630
+
2631
+ > **只管"弹框 + 校验"**:点 `children` 触发系统选文件框,选完按 `accept` / `maxSize` / `maxCount` 过滤,通过的交 `onPick`、没通过的交 `onValidateFail`(**提示语由调用方决定,库不自己弹**)。触发体长什么样完全由 `children` 决定,好让各处沿用自己那套按钮样式 —— 上面的 `IconPaperclip` 只是一例,换成一个 `Button` 或一枚图标按钮都行。
2632
+ >
2633
+ > **不做去重、也不转成 `Attachment`**:交出来的是原始 `File[]`。要不要生成缩略图(`URL.createObjectURL`)、要不要顺带建对象 URL,都是调用方的事;这两步在 `AttachmentUtility.fromFiles` 里有现成的。
2634
+ >
2635
+ > 每次 `change` 之后会把 input 的值置空,否则**连续选同一个文件不会再触发 change** —— 浏览器只在值发生变化时才派发事件。
2636
+
2554
2637
  ### Markdown
2555
2638
 
2556
2639
  ```tsx
@@ -2880,15 +2963,78 @@ import { AIChatInput } from "knight-ui";
2880
2963
  | `loading` | `boolean` | `false` | 加载中 |
2881
2964
  | `minRows` | `number` | `2` | 最小(初始)行数,不足时留白;被 `style` 撑高也不塌 |
2882
2965
  | `maxRows` | `number` | `4` | 最大行数,超出后框内竖直滚动 |
2883
- | `attachments` | `Attachment[]` | — | 附件列表 |
2966
+ | `attachments` | `Attachment[]` | — | 附件列表。**受控**:传了就以它为准,增删只回调(见下方引用块) |
2967
+ | `toolbar` | `React.ReactNode` | — | 底部工具条左端内容。**原名 `bottomBar`**,连同新增的 `toolbarEnd` 一起改名。不传则保留 20px 空白垫高 |
2968
+ | `toolbarEnd` | `React.ReactNode` | — | 底部工具条右端内容,与 `toolbar` 同一行、自动靠右 |
2969
+ | `accept` | `string` | — | 可收附件类型,与 `<input accept>` 同语法(如 `"image/*,.pdf"`),不传则不限 |
2970
+ | `multiple` | `boolean` | `true` | 是否允许多选 |
2971
+ | `maxCount` | `number` | — | 附件数量上限,不传则不限 |
2972
+ | `maxSize` | `number` | — | 单个附件字节上限,不传则不限 |
2973
+ | `onAddAttachments` | `(files: Array<File>) => void` | — | 附件通过校验 |
2974
+ | `onRemoveAttachment` | `(attachment: Attachment, index: number) => void` | — | 附件被移除 |
2975
+ | `onValidateFail` | `(reason: RejectReason) => void` | — | 附件未通过校验,见 [AttachmentUtility](#attachmentutility-附件工具) |
2884
2976
  | `onSend` | `(value, attachments) => void` | — | 发送回调 |
2885
2977
  | `backgroundColor` | `string` | — | 输入框底色。缺省走 `.ku-chat-surface`(暗 `#131620` / 亮 `#f5f6f8`)。**要与 `AIChatDialogue` 一起改** |
2886
2978
 
2979
+ 多模态(工具栏放附件按钮,附件状态由调用方持有):
2980
+
2981
+ ```tsx
2982
+ import { AIChatInput, AttachmentPicker, AttachmentUtility, IconPaperclip, Toast } from "knight-ui";
2983
+
2984
+ const [attachments, setAttachments] = React.useState<Attachment[]>([]);
2985
+
2986
+ const addFiles = (files: Array<File>) =>
2987
+ setAttachments((prev) => [...prev, ...AttachmentUtility.fromFiles(files)]);
2988
+
2989
+ <AIChatInput
2990
+ attachments={attachments}
2991
+ accept="image/*"
2992
+ maxCount={4}
2993
+ toolbar={
2994
+ <AttachmentPicker accept="image/*" count={attachments.length} maxCount={4}
2995
+ onPick={addFiles} onValidateFail={(r) => Toast.warning(r.message)}>
2996
+ <IconPaperclip size="small" />
2997
+ </AttachmentPicker>
2998
+ }
2999
+ toolbarEnd={<McpSelect servers={servers} />}
3000
+ onAddAttachments={addFiles}
3001
+ onRemoveAttachment={(a) => setAttachments((prev) => prev.filter((item) => item !== a))}
3002
+ onSend={(text, files) => {
3003
+ // 编码(转 base64 / 拼 FormData)由上层做, 组件只把 File 原样递出来
3004
+ files.forEach(AttachmentUtility.release);
3005
+ setAttachments([]);
3006
+ }}
3007
+ />
3008
+ ```
3009
+
3010
+ **`Attachment` 字段**:
3011
+
3012
+ | 字段 | 类型 | 说明 |
3013
+ |---|---|---|
3014
+ | `id` | `string` | 稳定标识 —— 由 `AttachmentUtility.fromFiles` 生成,也允许调用方自带 |
3015
+ | `name` | `string` | 文件名 |
3016
+ | `size` | `number` | 字节数 |
3017
+ | `type` | `string` | MIME |
3018
+ | `kind` | `"image" \| "file" \| "audio" \| "video"` | 类别 —— 决定缩略图形状与发送时对应的 part 形态 |
3019
+ | `thumbnail` | `string` | 图片缩略图(objectURL / dataURL) |
3020
+ | `url` | `string` | 远程地址(已有附件回填时用) |
3021
+ | `file` | `File` | 原始文件 —— **编码责任在上层** |
3022
+
3023
+ > **三种添加入口**:点 `toolbar` 里的 `AttachmentPicker`、直接 `Ctrl+V` 粘贴、把文件拖到输入框上。三条都按同一套规则校验(`accept` / `maxSize` / `maxCount`,都走 `AttachmentUtility.filter`),但**回调分两个口子**:选择器那条从 `AttachmentPicker.onPick` 出来,粘贴与拖拽那条从 `AIChatInput.onAddAttachments` 出来 —— 因为选择器是独立组件、有自己的 `<input>`,它的 change 不经过 `AIChatInput`。示例里两个口子都接到同一个 `addFiles`,所以不会重复添加;选择器那边记得把当前附件数喂给它的 `count`,否则两条入口各自算名额,`maxCount` 会被突破。粘贴时**以文件为准**:截图粘贴是主场景,剪贴板里顺带的文字与地址不会被一并塞进正文。
3024
+ >
3025
+ > 拖拽悬停时输入框上会盖一层磨砂遮罩并提示「拖拽可添加附件」,此时描边也换成主色。遮罩走 `.ku-mask`(遮罩档:压暗 34% + `blur(10px) saturate(120%)`),不用玻璃档 —— 带颗粒的 `ku-glass-overlay` / `-tip` / `-menu` **三档叠的是同一颗 `--ku-glass-grain`**,铺满整块输入框时颗粒感明显;遮罩档不含颗粒,且"压暗"本就是遮罩要表达的语义。
3026
+ >
3027
+ > **纯附件也能发**:`hasContent = 正文非空 || 附件数 > 0`,发送键的可用态与 Enter 的判定用的是同一个值,所以只贴图不写字照样发得出去。
3028
+ >
3029
+ > **`attachments` 受控 / 非受控**:**不传** = 库自己管 —— 拖拽 / 粘贴 / 选择器直接可用,发送后自动清空,卸载时自动 `AttachmentUtility.release` 归还缩略图;**传了** = 以你为准,库不再改动这个数组,增删只回调(`onAddAttachments` / `onRemoveAttachment`),**缩略图的 `release` 与发送后的清空都由调用方负责** —— 示例里那句 `files.forEach(AttachmentUtility.release)` 干的就是这件事。
3030
+ >
3031
+ > **组件只收集、不编码**:`onSend` 递出来的是原始 `File`。转 base64、拼 `FormData`、还是先传到对象存储换一个 URL,各家有各家的做法,所以这一层留在外面。
3032
+ >
2887
3033
  > **面**:面板挂 `.ku-chat-surface`(**不透明**底,暗 `#131620` / 亮 `#f5f6f8`),与 `AIChatDialogue` / `Chat` **共用这一条类规则**。它是**同一个对话区的下半块**——两块底色必须一致,只改一边会在中间露出一条分界线,所以它跟着对话面板走,而不是像别的输入类控件那样走 `fill-0`(见 `input/Input.tsx`)。它仍是一个**面板**——自带内边距,里面装着 textarea + 附件条 + 发送键——不是 `Input` 那种单字段控件,那个分野没变。内部 textarea 本来就是透明的,附件标签走不透明的 `fill-1`(在这块面上是**更亮**的一档,读起来是一枚筹码)。
2888
3034
  >
2889
3035
  > **`backgroundColor` 只在传了的时候落内联**(`...(backgroundColor ? { backgroundColor } : {})`)。**不能无条件写内联 `backgroundColor`**——内联优先级高于类,写死就把 `.ku-chat-surface` 那一档盖死了。要与 `AIChatDialogue` 一起改,理由同上一段。展开见「对话区的面」。
2890
3036
  >
2891
- > 内边距 `10px 12px`,**横向略大于纵向**:纵向那 10px 会跟 textarea 下面 20px 的垫高叠在一起(发送键就落在右下角那个垫高层里),再大就把键从右下角推离边框;横向是文字起笔的位置,12px 才不显得贴在框上。附件条的横向缩进交给外框提供——它原来自己带 12px,于是比 textarea 多缩进一层、两个左边缘对不齐,现在只留底边距把附件和正文隔开。
3037
+ > 内边距 `10px 12px`,**横向略大于纵向**:纵向那 10px 会跟 textarea 下面 20px 的垫高叠在一起(发送键就落在右下角那个垫高层里,那 20px 如今正是 `toolbar` 那一行的 `minHeight`,放了内容就随内容长高),再大就把键从右下角推离边框;横向是文字起笔的位置,12px 才不显得贴在框上。附件条的横向缩进交给外框提供——它原来自己带 12px,于是比 textarea 多缩进一层、两个左边缘对不齐,现在只留底边距把附件和正文隔开。
2892
3038
  >
2893
3039
  > `minRows` 默认 `2`、`maxRows` 默认 `4`:即初始两行,最多长到四行,再超就在框内滚动。要更宽松的书写区就自己传 `maxRows`(`minRows` 与它拉开几行才有余地)。
2894
3040
  >
@@ -2982,6 +3128,53 @@ const items = [
2982
3128
 
2983
3129
  ---
2984
3130
 
3131
+ ## Utility 工具
3132
+
3133
+ ### AttachmentUtility 附件工具
3134
+
3135
+ 附件相关的纯函数集合,命名空间导出(`AttachmentUtility.xxx`)。**无状态、不碰 React** —— 组件里的附件逻辑都建立在这几个函数上,别的项目要单独用也可以直接引。
3136
+
3137
+ ```tsx
3138
+ import { AttachmentUtility } from "knight-ui";
3139
+
3140
+ // File[] → Attachment[],图片顺带生成 objectURL 缩略图
3141
+ const items = AttachmentUtility.fromFiles(files);
3142
+
3143
+ // 按条件过滤,通过的与没通过的分开交回
3144
+ const { accepted, rejected } = AttachmentUtility.filter(files, {
3145
+ accept: "image/*,.pdf", maxSize: 5 * 1024 * 1024, maxCount: 3, count: 2,
3146
+ });
3147
+
3148
+ // 归还缩略图(只 revoke blob: 开头的 objectURL,dataURL 不动)
3149
+ items.forEach(AttachmentUtility.release);
3150
+
3151
+ AttachmentUtility.kindOf(file); // "image" | "file" | "audio" | "video"
3152
+ AttachmentUtility.readSize(1536); // "1.5 KB"
3153
+ ```
3154
+
3155
+ | 成员 | 签名 | 说明 |
3156
+ |---|---|---|
3157
+ | `fromFiles` | `(files: Array<File>) => Array<Item>` | 转成附件对象,图片顺带建 `thumbnail` |
3158
+ | `filter` | `(files, options?) => { accepted, rejected }` | 按 `accept` / `maxSize` / `maxCount`(+`count`) 分类 |
3159
+ | `release` | `(attachment) => void` | 归还缩略图占的 objectURL |
3160
+ | `kindOf` | `(file: File) => Kind` | 判类别,**MIME 优先、退回后缀** |
3161
+ | `readSize` | `(size: number) => string` | 字节数转可读文本(`B` / `KB` / `MB`) |
3162
+
3163
+ | 类型 | 说明 |
3164
+ |---|---|
3165
+ | `Kind` | `"image" \| "file" \| "audio" \| "video"` |
3166
+ | `Item` | 附件的内部形态:`id` / `name` / `size` / `type` / `kind` / `thumbnail?` / `file` |
3167
+ | `FilterOptions` | `accept?` / `multiple?` / `maxCount?` / `maxSize?` / `count?` |
3168
+ | `RejectReason` | 未通过的原因:`file` / `code`(`"type"` 类型不符、`"size"` 超体积、`"count"` 超数量或多选受限)/ `message` |
3169
+
3170
+ > **`release` 只 revoke `blob:`**:`fromFiles` 生成的缩略图是 objectURL,不归还就是一次内存泄漏;而回填的远程附件(`url`)或调用方自己塞的 dataURL 都没有可归还的东西,所以按前缀甄别,不误伤。
3171
+ >
3172
+ > **`filter` 的 `count` 是"已有多少"**:`maxCount` 是总数上限而不是"这次能选几个",所以剩余名额 = `maxCount - count`。不传 `count` 就当作从零开始。
3173
+ >
3174
+ > **`kindOf` 先看 MIME 再看后缀**:浏览器给的 `file.type` 有时是空的(少见扩展名尤其容易),此时退回按扩展名判;两样都认不出就归 `"file"`。
3175
+
3176
+ ---
3177
+
2985
3178
  ## Other 其他
2986
3179
 
2987
3180
  ### ConfigProvider 全局配置
@@ -1,27 +1,46 @@
1
1
  import React from "react";
2
2
  import { AbstractUI, type UIProps, type UIState } from "../../base";
3
- export interface Attachment {
4
- id?: string;
5
- name: string;
6
- size?: number;
7
- type?: string;
8
- url?: string;
9
- }
3
+ import { AttachmentUtility } from "../../utility";
10
4
  /**
11
5
  * AI 聊天输入框。
12
6
  */
13
7
  export declare class AIChatInput extends AbstractUI<AIChatInputProps, AIChatInputState> {
8
+ /** 行高(px):textarea 的 min/max 高度、以及行数换算都以此为单位 */
9
+ private static readonly LINE_HEIGHT;
10
+ /** 最小行数默认值:一处定义、三处消费(measure / 发送后复位 / 渲染),免得默认值改一处漏两处 */
11
+ private static readonly DEFAULT_MIN_ROWS;
12
+ /** 最大行数默认值 */
13
+ private static readonly DEFAULT_MAX_ROWS;
14
14
  private textareaRef;
15
15
  constructor(props: AIChatInputProps);
16
16
  componentDidUpdate(prevProps: Readonly<AIChatInputProps>, prevState: Readonly<AIChatInputState>): void;
17
17
  componentDidMount(): void;
18
18
  componentWillUnmount(): void;
19
19
  private onTextareaWheel;
20
+ /** 归还一批附件的缩略图 */
21
+ private releaseAll;
20
22
  private autoResize;
21
23
  private handleSend;
22
24
  private handleKeyDown;
23
25
  private handleChange;
24
26
  private handleRemoveAttachment;
27
+ /** 新文件统一入口 — 选文件 / 粘贴 / 拖拽都走这里。
28
+ * 校验按 props 的 accept/maxSize/maxCount 做, 未通过的交回 onValidateFail;
29
+ * 未受控时自己转成 Attachment 存下, 受控时只把原始 File[] 抛给 onAddAttachments */
30
+ private acceptFiles;
31
+ /** 粘贴带文件时以文件为准 —— 截图粘贴是主场景, 不该把剪贴板里的文字/地址一并塞进正文
32
+ * 以下挂到 DOM 上的处理器一律写成箭头属性: React 是裸调用, 原型方法进去 this 就没了 */
33
+ private handlePaste;
34
+ /** 拖拽深度 — dragenter / dragleave 在子元素之间来回成对触发, 靠计数才分得清"真离开" */
35
+ private dragDepth;
36
+ private handleDragEnter;
37
+ private handleDragOver;
38
+ private handleDragLeave;
39
+ private handleDrop;
40
+ /** 悬停高亮 — 判得出不是文件就不亮; 判不出来(浏览器给的信息不规整)宁可亮, 别让人以为拖不进来 */
41
+ private markDragging;
42
+ /** 拖的是不是文件 — 认不出来按文件算 */
43
+ private static looksLikeFiles;
25
44
  protected OnRenderContent(): React.ReactElement;
26
45
  }
27
46
  export interface AIChatInputProps extends UIProps {
@@ -35,16 +54,54 @@ export interface AIChatInputProps extends UIProps {
35
54
  backgroundColor?: string;
36
55
  disabled?: boolean;
37
56
  loading?: boolean;
57
+ /** 附件列表。**受控**: 传了就以它为准, 增删只回调(缩略图的 release、发送后清空都由调用方负责);
58
+ * 不传则库自己管 —— 拖拽/粘贴/选择器直接可用, 发送后自动清空、卸载时自动归还缩略图 */
38
59
  attachments?: Attachment[];
39
60
  /** 最小(初始)行数 @default 2 */
40
61
  minRows?: number;
41
62
  /** 最大行数,超出后滚动 @default 4 */
42
63
  maxRows?: number;
43
- /** 底部工具条内容(输入框内、textarea 下方、发送键左侧) —— 库不关心放什么,由调用方组装;不传则保留 20px 空白垫高 */
44
- bottomBar?: React.ReactNode;
64
+ /** 底部工具条左端内容(输入框内、textarea 下方) —— 库不关心放什么,由调用方组装;不传则保留 20px 空白垫高 */
65
+ toolbar?: React.ReactNode;
66
+ /** 底部工具条右端内容 —— 与 toolbar 同一行, 自动靠右 */
67
+ toolbarEnd?: React.ReactNode;
68
+ /** 可收附件类型, 与 <input accept> 同语法(如 "image/*,.pdf"), 不传则不限 */
69
+ accept?: string;
70
+ /** 是否允许多选 @default true */
71
+ multiple?: boolean;
72
+ /** 附件数量上限, 不传则不限 */
73
+ maxCount?: number;
74
+ /** 单个附件字节上限, 不传则不限 */
75
+ maxSize?: number;
76
+ /** 附件通过校验 —— 受控时调用方据此把 File 转成 Attachment 回传 attachments */
77
+ onAddAttachments?: (files: Array<File>) => void;
78
+ /** 附件被移除 */
79
+ onRemoveAttachment?: (attachment: Attachment, index: number) => void;
80
+ /** 附件未通过校验 —— 提示语由调用方决定, 库不自己弹 */
81
+ onValidateFail?: (reason: AttachmentUtility.RejectReason) => void;
45
82
  }
46
83
  interface AIChatInputState extends UIState {
47
84
  value: string;
48
85
  attachments: Attachment[];
86
+ /** 有文件拖到框上 — 仅用于描边高亮 */
87
+ dragging: boolean;
88
+ }
89
+ export interface Attachment {
90
+ /** 稳定标识 — 由 AttachmentUtility.fromFiles 生成, 也允许调用方自带 */
91
+ id?: string;
92
+ /** 文件名 */
93
+ name: string;
94
+ /** 字节数 */
95
+ size?: number;
96
+ /** MIME */
97
+ type?: string;
98
+ /** 远程地址(已有附件回填时用) */
99
+ url?: string;
100
+ /** 类别 — 决定缩略图形状与发送时对应的 part 形态 */
101
+ kind?: AttachmentUtility.Kind;
102
+ /** 图片缩略图(objectURL / dataURL) */
103
+ thumbnail?: string;
104
+ /** 原始文件 — 编码责任在上层 */
105
+ file?: File;
49
106
  }
50
107
  export {};
@@ -1,6 +1,5 @@
1
1
  import React from "react";
2
2
  import { AbstractUI, type UIProps, type UIState } from "../../base";
3
- import type { ValidateStatus } from "../types";
4
3
  export interface AutoCompleteOption {
5
4
  value: string;
6
5
  label?: React.ReactNode;
@@ -19,7 +18,13 @@ export declare class AutoComplete extends AbstractUI<AutoCompleteProps, AutoComp
19
18
  focus(): void;
20
19
  protected OnRenderContent(): React.ReactElement;
21
20
  private getFilteredData;
22
- private onInputChange;
21
+ private handleInputChange;
22
+ /** 聚焦 — 先把当前文本交出去(在展开下拉之前), 再展开。
23
+ * 异步拉候选的入口: 聚焦时去服务端要一份候选, 期间把 loading 置 true */
24
+ private handleFocus;
25
+ /** 失焦 — 先把当前文本交出去(在收起下拉之前), 再收下拉。
26
+ * 有了它, "手输一个列表里没有的值"才有落点: 单靠 onChange 只说明值变过, 说不清"输完了" */
27
+ private handleBlur;
23
28
  private openDropdown;
24
29
  private closeDropdown;
25
30
  private onDocClick;
@@ -35,9 +40,15 @@ export interface AutoCompleteProps extends UIProps {
35
40
  defaultValue?: string;
36
41
  /** 搜索回调(用于异步请求) */
37
42
  onSearch?: (inputValue: string) => void;
43
+ /** 聚焦 — 交出聚焦那一刻的文本。异步拉候选的入口: 聚焦时去服务端要一份候选,
44
+ * 期间把 loading 置 true(否则 data 还空着会闪一下"无匹配结果") */
45
+ onFocus?: (value: string) => void;
46
+ /** 失焦 — 交出离开时的文本, 给调用方一个"输完了"的边界(trim / 规范化 / 校验是否已知值) */
47
+ onBlur?: (value: string) => void;
38
48
  /** 选中回调 */
39
49
  onSelect?: (option: AutoCompleteOption) => void;
40
- /** 值变化回调 */
50
+ /** 值变化回调 — **手输与选中都触发**: 手输时 option 为空, 选中时带上。
51
+ * 与 onSearch 在敲键时同一时刻触发, 但语义不同 —— 这个是"值变了", 那个是"该搜了" */
41
52
  onChange?: (value: string, option?: AutoCompleteOption) => void;
42
53
  /** 是否本地过滤 */
43
54
  filter?: boolean;
@@ -49,7 +60,6 @@ export interface AutoCompleteProps extends UIProps {
49
60
  loading?: boolean;
50
61
  size?: "small" | "default" | "large";
51
62
  placeholder?: string;
52
- validateStatus?: ValidateStatus;
53
63
  }
54
64
  interface AutoCompleteState extends UIState {
55
65
  value: string;
@@ -26,6 +26,9 @@ export declare class TreeSelect extends AbstractUI<TreeSelectProps, TreeSelectSt
26
26
  componentWillUnmount(): void;
27
27
  private onClickOutside;
28
28
  protected OnRenderContent(): React.ReactElement;
29
+ /** 选项行的类: 悬停 / 选中 / 禁用三态都交给 .ku-menu-option 那几档类。
30
+ * 底色**不能写内联** —— 内联优先级高于类, 写了悬停就没反应(与 Select / Cascader 同一条规矩) */
31
+ private optionClass;
29
32
  private flatItemStyle;
30
33
  private renderCheckbox;
31
34
  private renderTreeNode;
@@ -0,0 +1,30 @@
1
+ import React from "react";
2
+ import { AbstractUI, type UIProps, type UIState } from "../../base";
3
+ import { AttachmentUtility } from "../../utility";
4
+ /**
5
+ * 附件选择器 — 点击触发体弹系统选文件框, 选完按 accept / maxSize / maxCount 校验后回调。
6
+ * 只管"弹框 + 校验"; 触发体长什么样由 children 决定, 好让各处沿用自己那套按钮样式。
7
+ */
8
+ export declare class AttachmentPicker extends AbstractUI<AttachmentPickerProps, UIState> {
9
+ private inputRef;
10
+ constructor(props: AttachmentPickerProps);
11
+ private handleClick;
12
+ private handleChange;
13
+ protected OnRenderContent(): React.ReactElement;
14
+ }
15
+ export interface AttachmentPickerProps extends UIProps {
16
+ /** 可收类型, 与 <input accept> 同语法(如 "image/*,.pdf") */
17
+ accept?: string;
18
+ /** 是否多选 @default true */
19
+ multiple?: boolean;
20
+ /** 数量上限, 与 count 一起算剩余名额 */
21
+ maxCount?: number;
22
+ /** 单个文件字节上限 */
23
+ maxSize?: number;
24
+ /** 已有数量 — 由调用方喂当前附件数 */
25
+ count?: number;
26
+ /** 通过校验的文件 */
27
+ onPick?: (files: Array<File>) => void;
28
+ /** 未通过校验 — 提示语由调用方决定 */
29
+ onValidateFail?: (reason: AttachmentUtility.RejectReason) => void;
30
+ }
@@ -1,3 +1,5 @@
1
+ export { AttachmentPicker } from "./AttachmentPicker";
2
+ export type { AttachmentPickerProps } from "./AttachmentPicker";
1
3
  export { AudioPlayer } from "./AudioPlayer";
2
4
  export type { AudioPlayerProps } from "./AudioPlayer";
3
5
  export { Chat } from "./Chat";
package/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from "./base";
2
2
  export * from "./component";
3
+ export * from "./utility";