one-design-next 0.0.91 → 0.0.96
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 +40 -40
- package/dist/_util/layoutBatch.d.ts +14 -0
- package/dist/_util/layoutBatch.js +47 -0
- package/dist/_util/sharedResizeObserver.d.ts +11 -0
- package/dist/_util/sharedResizeObserver.js +65 -0
- package/dist/_util/textFit.d.ts +48 -0
- package/dist/_util/textFit.js +123 -0
- package/dist/chat-item/index.d.ts +8 -4
- package/dist/chat-item/index.js +156 -120
- package/dist/hover-fill/index.js +9 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# One Design Next
|
|
4
4
|
|
|
5
|
-
基于 **TAD One Design &
|
|
5
|
+
基于 **TAD One Design & WXAD AD UI** 设计语言的 React 组件库
|
|
6
6
|
|
|
7
7
|
通用业务控件 / 平台布局 / GenUI 对话组件 / AI Skill 工程化流水线
|
|
8
8
|
|
|
@@ -52,16 +52,16 @@ import { Button, Table, Dialog, Composer } from 'one-design-next';
|
|
|
52
52
|
|
|
53
53
|
完整导出清单以 [`components/index.ts`](components/index.ts) 为准,分类说明与 Props 索引见 [component-list.md](.cursor/skills/p2-one-design/references/component-list.md)。
|
|
54
54
|
|
|
55
|
-
| 分类
|
|
56
|
-
|
|
57
|
-
| **通用**
|
|
58
|
-
| **布局**
|
|
59
|
-
| **导航**
|
|
60
|
-
| **数据录入** | `Form` `Input` `InputNumber` `InputOtp` `Select` `AutoComplete` `Checkbox` `Radio` `Switch` `Slider` `ColorPicker` `DatePicker` `DateRangePicker` `TimePicker` `Calendar`
|
|
61
|
-
| **数据展示** | `Table` `Chart` `VirtualList` `NumberFlow` `EllipsisText` `TextSwap` `Empty`
|
|
62
|
-
| **反馈**
|
|
63
|
-
| **GenUI**
|
|
64
|
-
| **底层**
|
|
55
|
+
| 分类 | 组件 |
|
|
56
|
+
| :----------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
57
|
+
| **通用** | `Button` `Icon` `Link` `Avatar` `Image` `HoverFill` `ConfigProvider` |
|
|
58
|
+
| **布局** | `Card` `Collapse` `ScrollArea` `RuyiLayout` `MpLayout` `HuxuanLayout` |
|
|
59
|
+
| **导航** | `Tabs` `Dropdown` `Pagination` `Cascader` `TreeSelect` |
|
|
60
|
+
| **数据录入** | `Form` `Input` `InputNumber` `InputOtp` `Select` `AutoComplete` `Checkbox` `Radio` `Switch` `Slider` `ColorPicker` `DatePicker` `DateRangePicker` `TimePicker` `Calendar` |
|
|
61
|
+
| **数据展示** | `Table` `Chart` `VirtualList` `NumberFlow` `EllipsisText` `TextSwap` `Empty` |
|
|
62
|
+
| **反馈** | `Dialog` `Drawer` `Message` `Alert` `Tooltip` `Popover` `Loading` |
|
|
63
|
+
| **GenUI** | `Composer` `StreamText` `ChatItem` `AgentStep` `AgentThink` `Artifact` `Invocation` `Mention` `CodeBlock` `PreviewPanel` `ActionBar` `Attachments` `MessageImage` `Sources` `Provenance` `Suggestions` `SkillSlot` `UserBubble` `Welcome` `ErrorBlock` `Fab` |
|
|
64
|
+
| **底层** | `ModalBase` 系列 `PopBase` 系列 `Collapsible` 日期 / 时间工具函数 |
|
|
65
65
|
|
|
66
66
|
> GenUI 共享类型(`ToolCall`、`ChatMessage`、`Conversation` 等)通过 `_genui-types` 统一导出。
|
|
67
67
|
|
|
@@ -69,10 +69,10 @@ import { Button, Table, Dialog, Composer } from 'one-design-next';
|
|
|
69
69
|
|
|
70
70
|
One Design Next 提供两种主题定制方式:
|
|
71
71
|
|
|
72
|
-
| 方式
|
|
73
|
-
|
|
74
|
-
| **CSS Variables**
|
|
75
|
-
| **ConfigProvider** | 需要 React 上下文的定制(图标解析、按钮行为等) | 通过 Provider 注入配置
|
|
72
|
+
| 方式 | 适用场景 | 说明 |
|
|
73
|
+
| :----------------- | :---------------------------------------------- | :------------------------------------------ |
|
|
74
|
+
| **CSS Variables** | 全局主题、脱离 React 树的场景(如 `Message`) | `base.css` 提供基础 token,组件级变量可覆盖 |
|
|
75
|
+
| **ConfigProvider** | 需要 React 上下文的定制(图标解析、按钮行为等) | 通过 Provider 注入配置 |
|
|
76
76
|
|
|
77
77
|
## 本地开发
|
|
78
78
|
|
|
@@ -94,24 +94,24 @@ pnpm run docs:build
|
|
|
94
94
|
|
|
95
95
|
### 常用脚本
|
|
96
96
|
|
|
97
|
-
| 命令
|
|
98
|
-
|
|
99
|
-
| `pnpm run dev`
|
|
100
|
-
| `pnpm run build`
|
|
101
|
-
| `pnpm run docs:build`
|
|
102
|
-
| `pnpm run docs:preview`
|
|
103
|
-
| `pnpm run build:skills`
|
|
104
|
-
| `pnpm run create-component`
|
|
105
|
-
| `pnpm run create-skill`
|
|
106
|
-
| `pnpm run test`
|
|
107
|
-
| `pnpm run test:ci`
|
|
108
|
-
| `pnpm run test:changed`
|
|
109
|
-
| `pnpm run typecheck`
|
|
110
|
-
| `pnpm run benchmark`
|
|
111
|
-
| `pnpm run benchmark:compare` | 对比性能基准
|
|
112
|
-
| `pnpm run benchmark:auto`
|
|
113
|
-
| `pnpm run lint`
|
|
114
|
-
| `pnpm run release`
|
|
97
|
+
| 命令 | 说明 |
|
|
98
|
+
| :--------------------------- | :-------------------------------- |
|
|
99
|
+
| `pnpm run dev` | 启动本地开发文档站 |
|
|
100
|
+
| `pnpm run build` | 构建组件库(father + 样式) |
|
|
101
|
+
| `pnpm run docs:build` | 静态构建文档站(含 LLM 知识生成) |
|
|
102
|
+
| `pnpm run docs:preview` | 本地预览文档产物 |
|
|
103
|
+
| `pnpm run build:skills` | 构建多产品 Skill 发布包 |
|
|
104
|
+
| `pnpm run create-component` | 创建新组件脚手架 |
|
|
105
|
+
| `pnpm run create-skill` | 创建新 AI Skill |
|
|
106
|
+
| `pnpm run test` | 运行 Vitest(watch 模式) |
|
|
107
|
+
| `pnpm run test:ci` | 运行 Vitest(CI 模式) |
|
|
108
|
+
| `pnpm run test:changed` | 仅测试改动相关用例 |
|
|
109
|
+
| `pnpm run typecheck` | TypeScript 类型检查 |
|
|
110
|
+
| `pnpm run benchmark` | 运行性能基准测试 |
|
|
111
|
+
| `pnpm run benchmark:compare` | 对比性能基准 |
|
|
112
|
+
| `pnpm run benchmark:auto` | 仅运行自动基准任务 |
|
|
113
|
+
| `pnpm run lint` | ESLint + Stylelint 代码检查 |
|
|
114
|
+
| `pnpm run release` | 版本发布 |
|
|
115
115
|
|
|
116
116
|
## 仓库结构
|
|
117
117
|
|
|
@@ -161,14 +161,14 @@ one-design-next/
|
|
|
161
161
|
|
|
162
162
|
### 阶段说明
|
|
163
163
|
|
|
164
|
-
| 阶段
|
|
165
|
-
|
|
166
|
-
| **P1** 设计
|
|
167
|
-
| **P2** 知识
|
|
168
|
-
| **P3** 执行
|
|
169
|
-
| **P4** 质量
|
|
164
|
+
| 阶段 | Skill | 职责 |
|
|
165
|
+
| :----------- | :--------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------- |
|
|
166
|
+
| **P1** 设计 | `p1-design-analyst` · `p1-design-spec` | 需求结构化分析、组件级设计说明 |
|
|
167
|
+
| **P2** 知识 | `p2-one-design` · `p2-block-catalog` · `p2-business-knowledge` · `p2-page-catalog` | 组件库知识、区块目录、业务页面知识 |
|
|
168
|
+
| **P3** 执行 | `p3-page-implement` | 用 ODN 组件 + 区块拼装页面代码 |
|
|
169
|
+
| **P4** 质量 | `p4-audit` · `p4-polish` | 代码质量审查、发布前精修 |
|
|
170
170
|
| **内部维护** | `p4-figma-to-block` · `p5-figma-to-page` · `p4-component-update-process` · `p5-dmp-web-restore` · `p5-fe-free-trade-restore` | 设计稿转区块/整页、组件更新、线上页面还原(仅维护者本地用) |
|
|
171
|
-
| **辅助**
|
|
171
|
+
| **辅助** | `ten-questions` · `philosophy-talks` | 方法论讨论与设计哲学 |
|
|
172
172
|
|
|
173
173
|
### 多产品 Skill 发布
|
|
174
174
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 读阶段:只允许读取布局(getBoundingClientRect / getComputedStyle 等)。
|
|
3
|
+
* 返回写阶段回调,或返回 undefined 表示无需写入。
|
|
4
|
+
*/
|
|
5
|
+
type LayoutRead = () => (() => void) | void;
|
|
6
|
+
/**
|
|
7
|
+
* 把「读布局 - 改 DOM」拆成两阶段,同一帧内的所有调用先全部读、再全部写。
|
|
8
|
+
*
|
|
9
|
+
* 长列表里每个实例各自「读 - 写 - 读 - 写」时,每次写都会让布局失效,
|
|
10
|
+
* 下一个读又触发一次完整排版(layout thrashing)。500 项时这是一帧
|
|
11
|
+
* 500 次排版与一次排版的差别。
|
|
12
|
+
*/
|
|
13
|
+
export declare function scheduleLayoutBatch(read: LayoutRead): void;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 读阶段:只允许读取布局(getBoundingClientRect / getComputedStyle 等)。
|
|
3
|
+
* 返回写阶段回调,或返回 undefined 表示无需写入。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
var pending = [];
|
|
7
|
+
var rafId = 0;
|
|
8
|
+
function flush() {
|
|
9
|
+
rafId = 0;
|
|
10
|
+
var batch = pending;
|
|
11
|
+
pending = [];
|
|
12
|
+
|
|
13
|
+
// 阶段一:集中读。期间无任何写入,浏览器只需排版一次。
|
|
14
|
+
var writes = [];
|
|
15
|
+
for (var i = 0; i < batch.length; i++) {
|
|
16
|
+
try {
|
|
17
|
+
var write = batch[i]();
|
|
18
|
+
if (write) writes.push(write);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
// eslint-disable-next-line no-console
|
|
21
|
+
console.error('[odn] layout batch read error:', error);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 阶段二:集中写。布局失效只发生在全部读取之后。
|
|
26
|
+
for (var _i = 0; _i < writes.length; _i++) {
|
|
27
|
+
try {
|
|
28
|
+
writes[_i]();
|
|
29
|
+
} catch (error) {
|
|
30
|
+
// eslint-disable-next-line no-console
|
|
31
|
+
console.error('[odn] layout batch write error:', error);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 把「读布局 - 改 DOM」拆成两阶段,同一帧内的所有调用先全部读、再全部写。
|
|
38
|
+
*
|
|
39
|
+
* 长列表里每个实例各自「读 - 写 - 读 - 写」时,每次写都会让布局失效,
|
|
40
|
+
* 下一个读又触发一次完整排版(layout thrashing)。500 项时这是一帧
|
|
41
|
+
* 500 次排版与一次排版的差别。
|
|
42
|
+
*/
|
|
43
|
+
export function scheduleLayoutBatch(read) {
|
|
44
|
+
pending.push(read);
|
|
45
|
+
if (rafId) return;
|
|
46
|
+
rafId = requestAnimationFrame(flush);
|
|
47
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type SharedResizeCallback = (entry: ResizeObserverEntry) => void;
|
|
2
|
+
/**
|
|
3
|
+
* 共享 ResizeObserver:模块级单例观察者 + WeakMap 注册表。
|
|
4
|
+
* 大量组件实例共用同一个观察者,避免每个实例各 new 一个,
|
|
5
|
+
* 也避免浏览器每帧遍历成倍增长的观察目标。
|
|
6
|
+
*
|
|
7
|
+
* 返回取消观察的清理函数,应在 effect cleanup 中调用。
|
|
8
|
+
* 非浏览器环境 / 无 ResizeObserver 时安全降级为空操作。
|
|
9
|
+
*/
|
|
10
|
+
export declare function observeResize(target: Element, callback: SharedResizeCallback): () => void;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import canUseDom from "./canUseDom";
|
|
2
|
+
var sharedRO = null;
|
|
3
|
+
/**
|
|
4
|
+
* 同一元素可能被多方观察(如行容器既被标题拟合监听、又被背景同步监听)。
|
|
5
|
+
* 值用 Set 而非单个回调,避免后注册者覆盖先注册者、
|
|
6
|
+
* 以及任一方取消观察时把另一方也一并停掉。
|
|
7
|
+
*/
|
|
8
|
+
var registry = new WeakMap();
|
|
9
|
+
function ensureSharedRO() {
|
|
10
|
+
if (sharedRO) return;
|
|
11
|
+
sharedRO = new ResizeObserver(function (entries) {
|
|
12
|
+
var _loop = function _loop() {
|
|
13
|
+
var entry = entries[i];
|
|
14
|
+
var callbacks = registry.get(entry.target);
|
|
15
|
+
if (!callbacks) return 1; // continue
|
|
16
|
+
callbacks.forEach(function (cb) {
|
|
17
|
+
// 单个回调异常不应中断同批其他实例的派发(共享观察者的隔离保证)
|
|
18
|
+
try {
|
|
19
|
+
cb(entry);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
// eslint-disable-next-line no-console
|
|
22
|
+
console.error('[odn] shared ResizeObserver callback error:', error);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
for (var i = 0; i < entries.length; i++) {
|
|
27
|
+
if (_loop()) continue;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 共享 ResizeObserver:模块级单例观察者 + WeakMap 注册表。
|
|
34
|
+
* 大量组件实例共用同一个观察者,避免每个实例各 new 一个,
|
|
35
|
+
* 也避免浏览器每帧遍历成倍增长的观察目标。
|
|
36
|
+
*
|
|
37
|
+
* 返回取消观察的清理函数,应在 effect cleanup 中调用。
|
|
38
|
+
* 非浏览器环境 / 无 ResizeObserver 时安全降级为空操作。
|
|
39
|
+
*/
|
|
40
|
+
export function observeResize(target, callback) {
|
|
41
|
+
if (!canUseDom() || typeof ResizeObserver === 'undefined') {
|
|
42
|
+
return function () {};
|
|
43
|
+
}
|
|
44
|
+
ensureSharedRO();
|
|
45
|
+
var callbacks = registry.get(target);
|
|
46
|
+
if (!callbacks) {
|
|
47
|
+
callbacks = new Set();
|
|
48
|
+
registry.set(target, callbacks);
|
|
49
|
+
sharedRO.observe(target);
|
|
50
|
+
}
|
|
51
|
+
callbacks.add(callback);
|
|
52
|
+
var stopped = false;
|
|
53
|
+
return function () {
|
|
54
|
+
if (stopped) return;
|
|
55
|
+
stopped = true;
|
|
56
|
+
var current = registry.get(target);
|
|
57
|
+
if (!current) return;
|
|
58
|
+
current.delete(callback);
|
|
59
|
+
// 最后一个观察方撤销时才真正停止观察
|
|
60
|
+
if (current.size === 0) {
|
|
61
|
+
registry.delete(target);
|
|
62
|
+
sharedRO.unobserve(target);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface FitMetrics {
|
|
2
|
+
/** 按字形簇切分的文本段 */
|
|
3
|
+
segments: string[];
|
|
4
|
+
/**
|
|
5
|
+
* prefixWidths[i] = segments[0..i] 拼接后再加省略号的宽度,严格单调递增。
|
|
6
|
+
* 完整文本宽度不放进来(省略号可能比末位字形宽,混入会破坏单调性)。
|
|
7
|
+
*/
|
|
8
|
+
prefixWidths: number[];
|
|
9
|
+
/** 完整文本宽度,不含省略号 */
|
|
10
|
+
fullWidth: number;
|
|
11
|
+
}
|
|
12
|
+
export interface FontSpec {
|
|
13
|
+
/** canvas font 简写,取自 resolveFont */
|
|
14
|
+
font: string;
|
|
15
|
+
/** CSS letter-spacing 的像素值;canvas measureText 不计字间距,需手工补偿 */
|
|
16
|
+
letterSpacing: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 按字形簇切分。emoji、肤色修饰符、ZWJ 序列、带组合符的字母都算一个整体,
|
|
20
|
+
* 截断落在其中任一位置都会产生乱码,所以不能按 UTF-16 码元切。
|
|
21
|
+
* 无 Intl.Segmenter 时退化到码点切分,至少保证代理对不裂。
|
|
22
|
+
*/
|
|
23
|
+
export declare function splitGraphemes(text: string): string[];
|
|
24
|
+
/**
|
|
25
|
+
* getComputedStyle().font 在部分浏览器下返回空串(如 font-family 走 CSS 变量),
|
|
26
|
+
* 此时把空串赋给 canvas 会静默 fallback 到 10px sans-serif,测量整体失真。
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveFont(cs: CSSStyleDeclaration): string;
|
|
29
|
+
/**
|
|
30
|
+
* 预计算所有截断位置的宽度。用 canvas measureText,不触发 DOM 排版,
|
|
31
|
+
* 因此单次挂载即可算完,后续宽度变化只需在结果上二分。
|
|
32
|
+
*/
|
|
33
|
+
export declare function measureFit(text: string, ellipsis: string, spec: FontSpec): FitMetrics;
|
|
34
|
+
export interface FitResult {
|
|
35
|
+
text: string;
|
|
36
|
+
/**
|
|
37
|
+
* 结果是否确实在 available 内。为 false 表示窄到连「一个字形 + 省略号」
|
|
38
|
+
* 都放不下,只能原样返回;调用方此时必须让 CSS text-overflow 接管截断,
|
|
39
|
+
* 否则文本会溢出容器。
|
|
40
|
+
*/
|
|
41
|
+
accurate: boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 按可用宽度选出应显示的文本。
|
|
45
|
+
* 极窄时返回原文而非单独的省略号:宽度本身可能是布局未就绪的假值,
|
|
46
|
+
* 裁成「…」后若行宽不再变化就永远恢复不了,交给 CSS text-overflow 兜底更稳。
|
|
47
|
+
*/
|
|
48
|
+
export declare function fitToWidth(metrics: FitMetrics, available: number, text: string, ellipsis: string): FitResult;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import canUseDom from "./canUseDom";
|
|
2
|
+
var sharedCanvas = null;
|
|
3
|
+
function getCtx() {
|
|
4
|
+
if (!canUseDom()) return null;
|
|
5
|
+
if (!sharedCanvas) sharedCanvas = document.createElement('canvas');
|
|
6
|
+
return sharedCanvas.getContext('2d');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 本地 TS lib 目标尚未包含 Intl.Segmenter 的声明,这里按用到的最小面手写。
|
|
11
|
+
* 运行时通过 'Segmenter' in Intl 判定可用性,不可用时降级到码点切分。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
var segmenter = function () {
|
|
15
|
+
if (typeof Intl === 'undefined' || !('Segmenter' in Intl)) return null;
|
|
16
|
+
var Ctor = Intl.Segmenter;
|
|
17
|
+
return new Ctor(undefined, {
|
|
18
|
+
granularity: 'grapheme'
|
|
19
|
+
});
|
|
20
|
+
}();
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 按字形簇切分。emoji、肤色修饰符、ZWJ 序列、带组合符的字母都算一个整体,
|
|
24
|
+
* 截断落在其中任一位置都会产生乱码,所以不能按 UTF-16 码元切。
|
|
25
|
+
* 无 Intl.Segmenter 时退化到码点切分,至少保证代理对不裂。
|
|
26
|
+
*/
|
|
27
|
+
export function splitGraphemes(text) {
|
|
28
|
+
if (!text) return [];
|
|
29
|
+
if (segmenter) {
|
|
30
|
+
return Array.from(segmenter.segment(text), function (s) {
|
|
31
|
+
return s.segment;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return Array.from(text);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* getComputedStyle().font 在部分浏览器下返回空串(如 font-family 走 CSS 变量),
|
|
39
|
+
* 此时把空串赋给 canvas 会静默 fallback 到 10px sans-serif,测量整体失真。
|
|
40
|
+
*/
|
|
41
|
+
export function resolveFont(cs) {
|
|
42
|
+
if (cs.font) return cs.font;
|
|
43
|
+
return "".concat(cs.fontStyle, " ").concat(cs.fontWeight, " ").concat(cs.fontSize, " / ").concat(cs.lineHeight, " ").concat(cs.fontFamily);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 预计算所有截断位置的宽度。用 canvas measureText,不触发 DOM 排版,
|
|
48
|
+
* 因此单次挂载即可算完,后续宽度变化只需在结果上二分。
|
|
49
|
+
*/
|
|
50
|
+
export function measureFit(text, ellipsis, spec) {
|
|
51
|
+
var segments = splitGraphemes(text);
|
|
52
|
+
if (!segments.length) {
|
|
53
|
+
return {
|
|
54
|
+
segments: segments,
|
|
55
|
+
prefixWidths: [],
|
|
56
|
+
fullWidth: 0
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
var ctx = getCtx();
|
|
60
|
+
if (!ctx) {
|
|
61
|
+
// 拿不到 canvas 时给出「永远放不下」的度量,调用方据此保留原文
|
|
62
|
+
return {
|
|
63
|
+
segments: segments,
|
|
64
|
+
prefixWidths: [],
|
|
65
|
+
fullWidth: Number.POSITIVE_INFINITY
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
ctx.font = spec.font;
|
|
69
|
+
var letterSpacing = spec.letterSpacing;
|
|
70
|
+
var ellipsisWidth = ctx.measureText(ellipsis).width + letterSpacing * splitGraphemes(ellipsis).length;
|
|
71
|
+
var prefixWidths = [];
|
|
72
|
+
var prefix = '';
|
|
73
|
+
for (var i = 0; i < segments.length - 1; i++) {
|
|
74
|
+
prefix += segments[i];
|
|
75
|
+
prefixWidths.push(ctx.measureText(prefix).width + letterSpacing * (i + 1) + ellipsisWidth);
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
segments: segments,
|
|
79
|
+
prefixWidths: prefixWidths,
|
|
80
|
+
fullWidth: ctx.measureText(text).width + letterSpacing * segments.length
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 最后一个 <= available 的下标,-1 表示一个字形加省略号都放不下 */
|
|
85
|
+
function bisect(widths, available) {
|
|
86
|
+
var lo = 0;
|
|
87
|
+
var hi = widths.length - 1;
|
|
88
|
+
var best = -1;
|
|
89
|
+
while (lo <= hi) {
|
|
90
|
+
var mid = lo + hi >> 1;
|
|
91
|
+
if (widths[mid] <= available) {
|
|
92
|
+
best = mid;
|
|
93
|
+
lo = mid + 1;
|
|
94
|
+
} else {
|
|
95
|
+
hi = mid - 1;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return best;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* 按可用宽度选出应显示的文本。
|
|
102
|
+
* 极窄时返回原文而非单独的省略号:宽度本身可能是布局未就绪的假值,
|
|
103
|
+
* 裁成「…」后若行宽不再变化就永远恢复不了,交给 CSS text-overflow 兜底更稳。
|
|
104
|
+
*/
|
|
105
|
+
export function fitToWidth(metrics, available, text, ellipsis) {
|
|
106
|
+
if (!metrics.segments.length) return {
|
|
107
|
+
text: text,
|
|
108
|
+
accurate: true
|
|
109
|
+
};
|
|
110
|
+
if (metrics.fullWidth <= available) return {
|
|
111
|
+
text: text,
|
|
112
|
+
accurate: true
|
|
113
|
+
};
|
|
114
|
+
var best = bisect(metrics.prefixWidths, available);
|
|
115
|
+
if (best < 0) return {
|
|
116
|
+
text: text,
|
|
117
|
+
accurate: false
|
|
118
|
+
};
|
|
119
|
+
return {
|
|
120
|
+
text: metrics.segments.slice(0, best + 1).join('') + ellipsis,
|
|
121
|
+
accurate: true
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -38,9 +38,13 @@ export interface ChatItemProps {
|
|
|
38
38
|
defaultDeleteOpen?: boolean;
|
|
39
39
|
onDeleteOpenChange?: (open: boolean) => void;
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
declare function ChatItemInner({ id, title, active, type, status, pinned, time, onClick, onRename, onPin, onDelete, actionMenuZIndex, renaming: renamingProp, defaultRenaming, onRenamingChange, menuOpen: menuOpenProp, defaultMenuOpen, onMenuOpenChange, deleteOpen: deleteOpenProp, defaultDeleteOpen, onDeleteOpenChange, }: ChatItemProps): import("react").JSX.Element;
|
|
42
|
+
/**
|
|
43
|
+
* 会话列表项。
|
|
44
|
+
*
|
|
45
|
+
* memo:列表里单项 hover / 选中会触发父级重渲染,未 memo 时其余项会一并
|
|
46
|
+
* 重跑 render 与相对时间计算。父级需传入稳定的回调引用才能生效。
|
|
47
|
+
*/
|
|
48
|
+
export declare const ChatItem: import("react").MemoExoticComponent<typeof ChatItemInner>;
|
|
45
49
|
export default ChatItem;
|
|
46
50
|
export { formatRelativeTime } from './relative-time';
|
package/dist/chat-item/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import _toConsumableArray from "@babel/runtime/helpers/esm/toConsumableArray";
|
|
2
2
|
import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray";
|
|
3
|
-
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { scheduleLayoutBatch } from "../_util/layoutBatch";
|
|
5
|
+
import { observeResize } from "../_util/sharedResizeObserver";
|
|
6
|
+
import { fitToWidth, measureFit, resolveFont } from "../_util/textFit";
|
|
4
7
|
import Dialog from "../dialog";
|
|
5
8
|
import Dropdown from "../dropdown";
|
|
6
9
|
import HoverFill from "../hover-fill";
|
|
@@ -15,6 +18,12 @@ var STATUS_ARIA_LABEL = {
|
|
|
15
18
|
};
|
|
16
19
|
var ELLIPSIS = '…';
|
|
17
20
|
|
|
21
|
+
/**
|
|
22
|
+
* 标记「JS 已把标题裁到可用宽度内」,由 useFittedTitle 在写入时同步,
|
|
23
|
+
* 不由 React 渲染:它表达的是某一帧的测量结果,而非「是否启用拟合」。
|
|
24
|
+
*/
|
|
25
|
+
var TITLE_FITTED_ATTR = 'data-odn-chat-item-title-fitted';
|
|
26
|
+
|
|
18
27
|
/** 可用宽度变化小于该值时跳过重算(与二分容差同量级) */
|
|
19
28
|
var FIT_WIDTH_EPS = 0.5;
|
|
20
29
|
/**
|
|
@@ -24,65 +33,85 @@ var FIT_WIDTH_EPS = 0.5;
|
|
|
24
33
|
var FIT_MIN_RELIABLE_WIDTH = 24;
|
|
25
34
|
|
|
26
35
|
/**
|
|
27
|
-
* 有 trailing(客服 icon / status /
|
|
36
|
+
* 有 trailing(客服 icon / status / 时间)时,把标题裁到可用宽度。
|
|
28
37
|
* 解决 CSS text-overflow 按字符边界截断后,省略号右侧留白把 trailing 顶开的问题。
|
|
29
38
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
39
|
+
* 测量走 textFit 的 Canvas 预算:挂载时一次算完所有截断位置的宽度,
|
|
40
|
+
* 之后每次宽度变化只做二分,不再「填字 - 排版 - 量宽」地反复试探。
|
|
41
|
+
*
|
|
42
|
+
* 裁切结果直接写进 titleEl,不走 state:长列表拖宽时零 re-render。
|
|
43
|
+
* 标题本身由 React 渲染,title 变化时 React 会把完整标题写回 DOM,
|
|
44
|
+
* 随后本 effect 重跑重新裁切,顺序天然正确。
|
|
45
|
+
*
|
|
46
|
+
* 性能:RO 只监听整行;回调经 rAF 合并;缓存 maxWidth / trailingWidth / 裁切结果。
|
|
47
|
+
* trailing 宽度变化(时间文案 / 状态 / 让位)由调用方经 layoutKey 触发重测。
|
|
32
48
|
*/
|
|
33
49
|
function useFittedTitle(title, enabled, labelRef, titleRef, /** 右对齐区(状态 / 时间),量宽预留 */
|
|
34
50
|
trailingRef, /** 紧跟标题的类型 icon,量宽预留 */
|
|
35
|
-
typeIconRef, /**
|
|
51
|
+
typeIconRef, /** trailing 宽度 / 字重变化,需强制重测 */
|
|
36
52
|
layoutKey) {
|
|
37
|
-
var _useState = useState(title),
|
|
38
|
-
_useState2 = _slicedToArray(_useState, 2),
|
|
39
|
-
fitted = _useState2[0],
|
|
40
|
-
setFitted = _useState2[1];
|
|
41
53
|
useLayoutEffect(function () {
|
|
42
|
-
var _document$fonts, _label$closest;
|
|
54
|
+
var _measure, _document$fonts, _label$closest;
|
|
55
|
+
var titleEl = titleRef.current;
|
|
56
|
+
if (!titleEl) return;
|
|
57
|
+
|
|
58
|
+
// React 渲染的是完整标题;未启用拟合时保持原样,交给 CSS text-overflow
|
|
43
59
|
if (!enabled) {
|
|
44
|
-
|
|
60
|
+
titleEl.textContent = title;
|
|
61
|
+
titleEl.removeAttribute(TITLE_FITTED_ATTR);
|
|
45
62
|
return;
|
|
46
63
|
}
|
|
47
64
|
var label = labelRef.current;
|
|
48
|
-
|
|
49
|
-
if (!label || !titleEl) return;
|
|
65
|
+
if (!label) return;
|
|
50
66
|
|
|
51
|
-
//
|
|
52
|
-
|
|
67
|
+
// 先回到原文,避免沿用上一会话裁成的「…」,也保证按完整标题取字体
|
|
68
|
+
titleEl.textContent = title;
|
|
69
|
+
var titleCs = getComputedStyle(titleEl);
|
|
70
|
+
var metrics = measureFit(title, ELLIPSIS, {
|
|
71
|
+
font: resolveFont(titleCs),
|
|
72
|
+
letterSpacing: Number.parseFloat(titleCs.letterSpacing) || 0
|
|
73
|
+
});
|
|
53
74
|
var cancelled = false;
|
|
54
|
-
var
|
|
75
|
+
var scheduled = false;
|
|
55
76
|
var lastMaxWidth = Number.NaN;
|
|
56
77
|
var lastTrailingWidth = Number.NaN;
|
|
57
78
|
var lastFitted = title;
|
|
58
|
-
var
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
probe.style.fontFeatureSettings = cs.fontFeatureSettings;
|
|
69
|
-
probe.style.fontVariationSettings = cs.fontVariationSettings;
|
|
79
|
+
var lastFitAccurate = false;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* accurate 表示「当前文本确实在可用宽度内」,此时才挂 fitted 标记让
|
|
83
|
+
* CSS 停止截断(否则省略号右侧会留出字符边界的空白,把 trailing 顶开)。
|
|
84
|
+
* 放弃裁切时必须摘掉标记,把截断交回 CSS,否则标题直接溢出行外。
|
|
85
|
+
*/
|
|
86
|
+
var applyFit = function applyFit(text, accurate) {
|
|
87
|
+
titleEl.textContent = text;
|
|
88
|
+
if (accurate) titleEl.setAttribute(TITLE_FITTED_ATTR, '');else titleEl.removeAttribute(TITLE_FITTED_ATTR);
|
|
70
89
|
};
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* gap 与 trailing / typeIcon 的宽度在同一个 effect 周期内是常量:
|
|
93
|
+
* 所有会改变它们的条件(选中、菜单开合、时间文案、状态、类型 icon)
|
|
94
|
+
* 都在 layoutKey 里,变化时 effect 会整体重跑。
|
|
95
|
+
* 拖拽时因此只需读行内标题区的宽度,省掉每帧每项的 getComputedStyle。
|
|
96
|
+
*/
|
|
97
|
+
var gap = Number.NaN;
|
|
98
|
+
var trailingWidth = 0;
|
|
99
|
+
var typeWidth = 0;
|
|
100
|
+
var readStatics = function readStatics() {
|
|
101
|
+
var _trailingRef$current$, _trailingRef$current, _typeIconRef$current$, _typeIconRef$current;
|
|
102
|
+
var cs = getComputedStyle(label);
|
|
103
|
+
gap = Number.parseFloat(cs.columnGap || cs.gap) || 0;
|
|
104
|
+
trailingWidth = (_trailingRef$current$ = (_trailingRef$current = trailingRef.current) === null || _trailingRef$current === void 0 ? void 0 : _trailingRef$current.getBoundingClientRect().width) !== null && _trailingRef$current$ !== void 0 ? _trailingRef$current$ : 0;
|
|
105
|
+
typeWidth = (_typeIconRef$current$ = (_typeIconRef$current = typeIconRef.current) === null || _typeIconRef$current === void 0 ? void 0 : _typeIconRef$current.getBoundingClientRect().width) !== null && _typeIconRef$current$ !== void 0 ? _typeIconRef$current$ : 0;
|
|
74
106
|
};
|
|
75
|
-
|
|
76
|
-
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 只读布局,返回写入回调交由批处理统一执行。
|
|
110
|
+
* 这里不能直接改 DOM:同帧内还有其他实例要读,写入会让它们的读重新排版。
|
|
111
|
+
*/
|
|
112
|
+
var measure = function measure() {
|
|
77
113
|
if (cancelled) return;
|
|
78
|
-
|
|
79
|
-
if (!probe.isConnected) document.body.appendChild(probe);
|
|
80
|
-
var cs = getComputedStyle(label);
|
|
81
|
-
var gap = Number.parseFloat(cs.columnGap || cs.gap) || 0;
|
|
82
|
-
var trailing = trailingRef.current;
|
|
83
|
-
var typeIcon = typeIconRef.current;
|
|
84
|
-
var trailingWidth = (_trailing$getBounding = trailing === null || trailing === void 0 ? void 0 : trailing.getBoundingClientRect().width) !== null && _trailing$getBounding !== void 0 ? _trailing$getBounding : 0;
|
|
85
|
-
var typeWidth = (_typeIcon$getBounding = typeIcon === null || typeIcon === void 0 ? void 0 : typeIcon.getBoundingClientRect().width) !== null && _typeIcon$getBounding !== void 0 ? _typeIcon$getBounding : 0;
|
|
114
|
+
if (!Number.isFinite(gap)) readStatics();
|
|
86
115
|
// 标题两侧可能各有一段 gap:title↔type、type/title↔trailing
|
|
87
116
|
var gapCount = (typeWidth > 0 ? 1 : 0) + (trailingWidth > 0 ? 1 : 0);
|
|
88
117
|
var maxWidth = Math.max(0, label.getBoundingClientRect().width - trailingWidth - typeWidth - gap * gapCount);
|
|
@@ -94,71 +123,58 @@ layoutKey) {
|
|
|
94
123
|
if (maxWidth < FIT_MIN_RELIABLE_WIDTH) {
|
|
95
124
|
lastMaxWidth = Number.NaN;
|
|
96
125
|
lastTrailingWidth = Number.NaN;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
126
|
+
// 此刻 trailing / typeIcon 多半也没量准,下次重读
|
|
127
|
+
gap = Number.NaN;
|
|
128
|
+
if (lastFitted === title && !lastFitAccurate) return;
|
|
129
|
+
lastFitted = title;
|
|
130
|
+
lastFitAccurate = false;
|
|
131
|
+
return function () {
|
|
132
|
+
return applyFit(title, false);
|
|
133
|
+
};
|
|
102
134
|
}
|
|
103
135
|
lastMaxWidth = maxWidth;
|
|
104
136
|
lastTrailingWidth = trailingWidth;
|
|
105
|
-
var
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
next = title;
|
|
110
|
-
while (lo <= hi) {
|
|
111
|
-
var mid = lo + hi >> 1;
|
|
112
|
-
var candidate = "".concat(title.slice(0, mid)).concat(ELLIPSIS);
|
|
113
|
-
if (measure(candidate) <= maxWidth + FIT_WIDTH_EPS) {
|
|
114
|
-
next = candidate;
|
|
115
|
-
lo = mid + 1;
|
|
116
|
-
} else {
|
|
117
|
-
hi = mid - 1;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
// 极端窄宽仍放不下「字+…」时交给 CSS ellipsis,保留原文
|
|
121
|
-
if (next === ELLIPSIS || next === title && measure(title) > maxWidth + FIT_WIDTH_EPS) {
|
|
122
|
-
var withEllipsis = "".concat(title.slice(0, 1)).concat(ELLIPSIS);
|
|
123
|
-
next = measure(withEllipsis) <= maxWidth + FIT_WIDTH_EPS ? withEllipsis : title;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
if (next === lastFitted) return;
|
|
137
|
+
var _fitToWidth = fitToWidth(metrics, maxWidth + FIT_WIDTH_EPS, title, ELLIPSIS),
|
|
138
|
+
next = _fitToWidth.text,
|
|
139
|
+
accurate = _fitToWidth.accurate;
|
|
140
|
+
if (next === lastFitted && accurate === lastFitAccurate) return;
|
|
127
141
|
lastFitted = next;
|
|
128
|
-
|
|
142
|
+
lastFitAccurate = accurate;
|
|
143
|
+
return function () {
|
|
144
|
+
return applyFit(next, accurate);
|
|
145
|
+
};
|
|
129
146
|
};
|
|
130
147
|
var scheduleFit = function scheduleFit() {
|
|
131
|
-
if (cancelled ||
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
148
|
+
if (cancelled || scheduled) return;
|
|
149
|
+
scheduled = true;
|
|
150
|
+
scheduleLayoutBatch(function () {
|
|
151
|
+
scheduled = false;
|
|
152
|
+
return measure();
|
|
135
153
|
});
|
|
136
154
|
};
|
|
137
155
|
|
|
138
|
-
//
|
|
139
|
-
|
|
156
|
+
// 首屏同步裁切,避免首帧闪全标题;后续宽度变化走批处理合并
|
|
157
|
+
(_measure = measure()) === null || _measure === void 0 || _measure();
|
|
140
158
|
void ((_document$fonts = document.fonts) === null || _document$fonts === void 0 ? void 0 : _document$fonts.ready.then(function () {
|
|
141
159
|
if (cancelled) return;
|
|
142
|
-
//
|
|
160
|
+
// 字体就绪后字形宽度可能变,需重新预算并绕过 maxWidth 缓存
|
|
161
|
+
var readyCs = getComputedStyle(titleEl);
|
|
162
|
+
metrics = measureFit(title, ELLIPSIS, {
|
|
163
|
+
font: resolveFont(readyCs),
|
|
164
|
+
letterSpacing: Number.parseFloat(readyCs.letterSpacing) || 0
|
|
165
|
+
});
|
|
143
166
|
lastMaxWidth = Number.NaN;
|
|
167
|
+
gap = Number.NaN;
|
|
144
168
|
scheduleFit();
|
|
145
169
|
}));
|
|
146
170
|
var row = (_label$closest = label.closest('[data-odn-chat-item-btn]')) !== null && _label$closest !== void 0 ? _label$closest : label;
|
|
147
|
-
var
|
|
148
|
-
ro.observe(row);
|
|
149
|
-
ro.observe(label);
|
|
150
|
-
var trailing = trailingRef.current;
|
|
151
|
-
if (trailing) ro.observe(trailing);
|
|
152
|
-
var typeIcon = typeIconRef.current;
|
|
153
|
-
if (typeIcon) ro.observe(typeIcon);
|
|
171
|
+
var stopObserving = observeResize(row, scheduleFit);
|
|
154
172
|
return function () {
|
|
173
|
+
// 批处理队列里可能仍排着本实例的读回调,靠 cancelled 短路即可
|
|
155
174
|
cancelled = true;
|
|
156
|
-
|
|
157
|
-
ro.disconnect();
|
|
158
|
-
probe.remove();
|
|
175
|
+
stopObserving();
|
|
159
176
|
};
|
|
160
177
|
}, [title, enabled, layoutKey, labelRef, titleRef, trailingRef, typeIconRef]);
|
|
161
|
-
return enabled ? fitted : title;
|
|
162
178
|
}
|
|
163
179
|
function ChatItemStatus(_ref) {
|
|
164
180
|
var status = _ref.status;
|
|
@@ -204,18 +220,28 @@ function ChatItemStatus(_ref) {
|
|
|
204
220
|
}));
|
|
205
221
|
}
|
|
206
222
|
function useControllableState(controlled, defaultValue, onChange) {
|
|
207
|
-
var
|
|
208
|
-
|
|
209
|
-
internal =
|
|
210
|
-
setInternal =
|
|
223
|
+
var _useState = useState(defaultValue),
|
|
224
|
+
_useState2 = _slicedToArray(_useState, 2),
|
|
225
|
+
internal = _useState2[0],
|
|
226
|
+
setInternal = _useState2[1];
|
|
211
227
|
var value = controlled !== null && controlled !== void 0 ? controlled : internal;
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
228
|
+
// 每次渲染新建 setValue 会让下游 memo 组件(Dropdown / 菜单项)全部失效
|
|
229
|
+
var latest = useRef({
|
|
230
|
+
controlled: controlled,
|
|
231
|
+
onChange: onChange
|
|
232
|
+
});
|
|
233
|
+
latest.current = {
|
|
234
|
+
controlled: controlled,
|
|
235
|
+
onChange: onChange
|
|
215
236
|
};
|
|
237
|
+
var setValue = useCallback(function (next) {
|
|
238
|
+
var _latest$current$onCha, _latest$current;
|
|
239
|
+
(_latest$current$onCha = (_latest$current = latest.current).onChange) === null || _latest$current$onCha === void 0 || _latest$current$onCha.call(_latest$current, next);
|
|
240
|
+
if (latest.current.controlled === undefined) setInternal(next);
|
|
241
|
+
}, []);
|
|
216
242
|
return [value, setValue];
|
|
217
243
|
}
|
|
218
|
-
|
|
244
|
+
function ChatItemInner(_ref2) {
|
|
219
245
|
var id = _ref2.id,
|
|
220
246
|
title = _ref2.title,
|
|
221
247
|
active = _ref2.active,
|
|
@@ -246,10 +272,10 @@ export function ChatItem(_ref2) {
|
|
|
246
272
|
_useControllableState2 = _slicedToArray(_useControllableState, 2),
|
|
247
273
|
renaming = _useControllableState2[0],
|
|
248
274
|
setRenaming = _useControllableState2[1];
|
|
249
|
-
var
|
|
250
|
-
|
|
251
|
-
draft =
|
|
252
|
-
setDraft =
|
|
275
|
+
var _useState3 = useState(title),
|
|
276
|
+
_useState4 = _slicedToArray(_useState3, 2),
|
|
277
|
+
draft = _useState4[0],
|
|
278
|
+
setDraft = _useState4[1];
|
|
253
279
|
var _useControllableState3 = useControllableState(deleteOpenProp, defaultDeleteOpen, onDeleteOpenChange),
|
|
254
280
|
_useControllableState4 = _slicedToArray(_useControllableState3, 2),
|
|
255
281
|
deleteOpen = _useControllableState4[0],
|
|
@@ -258,12 +284,12 @@ export function ChatItem(_ref2) {
|
|
|
258
284
|
_useControllableState6 = _slicedToArray(_useControllableState5, 2),
|
|
259
285
|
menuOpen = _useControllableState6[0],
|
|
260
286
|
setMenuOpen = _useControllableState6[1];
|
|
261
|
-
var
|
|
287
|
+
var _useState5 = useState(function () {
|
|
262
288
|
return Date.now();
|
|
263
289
|
}),
|
|
264
|
-
|
|
265
|
-
now =
|
|
266
|
-
setNow =
|
|
290
|
+
_useState6 = _slicedToArray(_useState5, 2),
|
|
291
|
+
now = _useState6[0],
|
|
292
|
+
setNow = _useState6[1];
|
|
267
293
|
var inputRef = useRef(null);
|
|
268
294
|
var labelRef = useRef(null);
|
|
269
295
|
var titleRef = useRef(null);
|
|
@@ -278,8 +304,9 @@ export function ChatItem(_ref2) {
|
|
|
278
304
|
var showTime = Boolean(timeLabel);
|
|
279
305
|
var showTrailing = showStatus || showTime;
|
|
280
306
|
var fitTitle = showTypeIcon || showTrailing;
|
|
281
|
-
|
|
282
|
-
|
|
307
|
+
useFittedTitle(title, fitTitle, labelRef, titleRef, trailingRef, typeIconRef, // trailing / 字重的全部变化来源。这些都会改标题可用宽度而行宽不变,
|
|
308
|
+
// 提前声明即可,无需为此额外监听 label / trailing / typeIcon 的尺寸。
|
|
309
|
+
"".concat(active ? 1 : 0, ":").concat(menuOpen ? 1 : 0, ":").concat(timeLabel, ":").concat(status !== null && status !== void 0 ? status : '', ":").concat(showTypeIcon ? 1 : 0));
|
|
283
310
|
useEffect(function () {
|
|
284
311
|
if (!renaming) setDraft(title);
|
|
285
312
|
}, [title, renaming]);
|
|
@@ -316,19 +343,21 @@ export function ChatItem(_ref2) {
|
|
|
316
343
|
setDraft(title);
|
|
317
344
|
setRenaming(false);
|
|
318
345
|
};
|
|
319
|
-
var actionMenu =
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
346
|
+
var actionMenu = useMemo(function () {
|
|
347
|
+
return [].concat(_toConsumableArray(onRename ? [{
|
|
348
|
+
value: 'rename',
|
|
349
|
+
label: '修改名称',
|
|
350
|
+
icon: 'pencil-line'
|
|
351
|
+
}] : []), _toConsumableArray(onPin ? [{
|
|
352
|
+
value: 'pin',
|
|
353
|
+
label: pinned ? '取消置顶' : '置顶',
|
|
354
|
+
icon: 'arrow-up-to-line'
|
|
355
|
+
}] : []), _toConsumableArray(onDelete ? [{
|
|
356
|
+
value: 'delete',
|
|
357
|
+
label: '删除',
|
|
358
|
+
icon: 'trash-2'
|
|
359
|
+
}] : []));
|
|
360
|
+
}, [onRename, onPin, onDelete, pinned]);
|
|
332
361
|
return /*#__PURE__*/React.createElement(HoverFill, {
|
|
333
362
|
"data-odn-chat-item": true,
|
|
334
363
|
"data-odn-chat-item-id": id,
|
|
@@ -374,9 +403,8 @@ export function ChatItem(_ref2) {
|
|
|
374
403
|
ref: titleRef,
|
|
375
404
|
"data-odn-chat-item-title": true,
|
|
376
405
|
"data-odn-chat-item-title-bold": active ? '' : undefined,
|
|
377
|
-
"data-odn-chat-item-title-fitted": fitTitle ? '' : undefined,
|
|
378
406
|
title: title
|
|
379
|
-
},
|
|
407
|
+
}, title), showTypeIcon ? /*#__PURE__*/React.createElement("span", {
|
|
380
408
|
ref: typeIconRef,
|
|
381
409
|
"data-odn-chat-item-type-icon": true,
|
|
382
410
|
"aria-label": "\u4EBA\u5DE5\u5BA2\u670D\u4F1A\u8BDD",
|
|
@@ -446,6 +474,14 @@ export function ChatItem(_ref2) {
|
|
|
446
474
|
}
|
|
447
475
|
})));
|
|
448
476
|
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* 会话列表项。
|
|
480
|
+
*
|
|
481
|
+
* memo:列表里单项 hover / 选中会触发父级重渲染,未 memo 时其余项会一并
|
|
482
|
+
* 重跑 render 与相对时间计算。父级需传入稳定的回调引用才能生效。
|
|
483
|
+
*/
|
|
484
|
+
export var ChatItem = /*#__PURE__*/memo(ChatItemInner);
|
|
449
485
|
ChatItem.displayName = 'ChatItem';
|
|
450
486
|
export default ChatItem;
|
|
451
487
|
export { formatRelativeTime } from "./relative-time";
|
package/dist/hover-fill/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
|
|
|
6
6
|
import _objectWithoutProperties from "@babel/runtime/helpers/esm/objectWithoutProperties";
|
|
7
7
|
var _excluded = ["bgClassName", "bgStyle", "children", "onMouseEnter", "onMouseLeave", "onMouseDown", "hoverColor", "activeColor", "disabled", "hoverCursor"];
|
|
8
8
|
import React, { forwardRef, useCallback, useContext, useEffect, useRef } from 'react';
|
|
9
|
+
import { observeResize } from "../_util/sharedResizeObserver";
|
|
9
10
|
import { ConfigContext } from "../config-provider";
|
|
10
11
|
import "./style";
|
|
11
12
|
var DEFAULT_HOVER_COLOR = 'rgba(33, 34, 38, 0.05)';
|
|
@@ -66,8 +67,6 @@ var HoverFill = /*#__PURE__*/forwardRef(function (_ref, outerRef) {
|
|
|
66
67
|
y = _root$getBoundingClie.y;
|
|
67
68
|
if (window.odnHoverFillState && window.odnHoverFillState.bgVisible && window.odnHoverFillState.bgNode && window.odnHoverFillState.baseNode && window.odnHoverFillState.baseNode !== root && ((_window$odnHoverFillS3 = window.odnHoverFillState.baseNode) === null || _window$odnHoverFillS3 === void 0 ? void 0 : _window$odnHoverFillS3.parentNode) === root.parentNode && hoverFillConfig.sharable) {
|
|
68
69
|
var _window$odnHoverFillS4 = window.odnHoverFillState,
|
|
69
|
-
bgX = _window$odnHoverFillS4.bgX,
|
|
70
|
-
bgY = _window$odnHoverFillS4.bgY,
|
|
71
70
|
bgScale = _window$odnHoverFillS4.bgScale,
|
|
72
71
|
baseNode = _window$odnHoverFillS4.baseNode;
|
|
73
72
|
var _baseNode$getBounding = baseNode.getBoundingClientRect(),
|
|
@@ -205,6 +204,12 @@ var HoverFill = /*#__PURE__*/forwardRef(function (_ref, outerRef) {
|
|
|
205
204
|
}, 0);
|
|
206
205
|
}, 100);
|
|
207
206
|
};
|
|
207
|
+
var handleMouseUp = function handleMouseUp() {
|
|
208
|
+
window.removeEventListener('mouseup', handleMouseUp, false);
|
|
209
|
+
if (window.odnHoverFillState.bgNode) {
|
|
210
|
+
window.odnHoverFillState.bgNode.style.background = window.odnHoverFillState.bgVisible ? hoverColor : 'transparent';
|
|
211
|
+
}
|
|
212
|
+
};
|
|
208
213
|
var handleMouseDown = function handleMouseDown(e) {
|
|
209
214
|
e.persist();
|
|
210
215
|
if (disabled) {
|
|
@@ -218,12 +223,6 @@ var HoverFill = /*#__PURE__*/forwardRef(function (_ref, outerRef) {
|
|
|
218
223
|
}
|
|
219
224
|
window.addEventListener('mouseup', handleMouseUp, false);
|
|
220
225
|
};
|
|
221
|
-
var handleMouseUp = function handleMouseUp() {
|
|
222
|
-
window.removeEventListener('mouseup', handleMouseUp, false);
|
|
223
|
-
if (window.odnHoverFillState.bgNode) {
|
|
224
|
-
window.odnHoverFillState.bgNode.style.background = window.odnHoverFillState.bgVisible ? hoverColor : 'transparent';
|
|
225
|
-
}
|
|
226
|
-
};
|
|
227
226
|
useEffect(function () {
|
|
228
227
|
if (!window.odnHoverFillState) {
|
|
229
228
|
window.odnHoverFillState = {
|
|
@@ -245,16 +244,10 @@ var HoverFill = /*#__PURE__*/forwardRef(function (_ref, outerRef) {
|
|
|
245
244
|
}, []);
|
|
246
245
|
useEffect(function () {
|
|
247
246
|
var root = ref.current;
|
|
248
|
-
if (!root
|
|
247
|
+
if (!root) {
|
|
249
248
|
return;
|
|
250
249
|
}
|
|
251
|
-
|
|
252
|
-
syncBgSize();
|
|
253
|
-
});
|
|
254
|
-
observer.observe(root);
|
|
255
|
-
return function () {
|
|
256
|
-
observer.disconnect();
|
|
257
|
-
};
|
|
250
|
+
return observeResize(root, syncBgSize);
|
|
258
251
|
}, [ref, syncBgSize]);
|
|
259
252
|
return /*#__PURE__*/React.createElement("div", _extends({
|
|
260
253
|
ref: ref,
|