one-design-next 0.0.90 → 0.0.92
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/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/composer/index.d.ts +4 -3
- package/dist/composer/index.js +55 -56
- package/dist/composer/utils.d.ts +20 -2
- package/dist/composer/utils.js +24 -4
- package/dist/hover-fill/index.js +9 -16
- package/dist/index.d.ts +1 -1
- package/dist/skill-slot/index.d.ts +2 -17
- package/dist/skill-slot/index.js +8 -78
- package/dist/skill-slot/style/index.css +2 -65
- package/package.json +1 -1
|
@@ -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';
|