chat-layout 1.2.0-1 → 1.2.0-2
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 +26 -0
- package/example/chat.ts +80 -1
- package/index.d.mts +29 -2
- package/index.mjs +366 -78
- package/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -109,6 +109,32 @@ Notes:
|
|
|
109
109
|
- `overflowWrap: "break-word"` keeps the current min-content behavior; `overflowWrap: "anywhere"` lets long unspaced strings shrink inside flex layouts such as chat bubbles.
|
|
110
110
|
- Current `measureMinContent()` behavior stays compatibility-first: ellipsis affects constrained measurement/drawing, but does not lower the min-content shrink floor by itself.
|
|
111
111
|
|
|
112
|
+
## Text justification
|
|
113
|
+
|
|
114
|
+
`MultilineText` supports two-end justification (justify) as a draw-phase decoration. It does not affect measurement or layout:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
const justified = new MultilineText(paragraph, {
|
|
118
|
+
lineHeight: 20,
|
|
119
|
+
font: "16px system-ui",
|
|
120
|
+
color: "#111",
|
|
121
|
+
align: "start",
|
|
122
|
+
justify: true, // or "inter-word" | "inter-character"
|
|
123
|
+
justifyLastLine: false, // default: last line uses normal alignment
|
|
124
|
+
justifyGapThreshold: 2.0, // max gap ratio before fallback
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Notes:
|
|
129
|
+
|
|
130
|
+
- `justify: true` is equivalent to `"inter-word"` mode, which expands spaces between words via `ctx.wordSpacing`.
|
|
131
|
+
- `"inter-character"` mode distributes extra space after every character via `ctx.letterSpacing`.
|
|
132
|
+
- Requires browser support for `CanvasRenderingContext2D.wordSpacing` / `letterSpacing`. When unsupported, justify is silently disabled.
|
|
133
|
+
- Lines that exceed `justifyGapThreshold`, have no expandable gaps, or are the last line (unless `justifyLastLine: true`) fall back to `align` / `physicalAlign`.
|
|
134
|
+
- `overflow: "ellipsis"` truncated lines are never justified.
|
|
135
|
+
- `measure()` and `measureMinContent()` are not affected by justify options.
|
|
136
|
+
- Works with both plain text and `InlineSpan[]` rich text.
|
|
137
|
+
|
|
112
138
|
## Shrink behavior
|
|
113
139
|
|
|
114
140
|
- `FlexItemOptions.shrink` defaults to `0`, so old layouts keep their previous behavior unless you opt in.
|
package/example/chat.ts
CHANGED
|
@@ -37,6 +37,37 @@ const sampleWords = [
|
|
|
37
37
|
"history",
|
|
38
38
|
];
|
|
39
39
|
|
|
40
|
+
const sampleChinesePhrases = [
|
|
41
|
+
"你好",
|
|
42
|
+
"收到",
|
|
43
|
+
"没问题",
|
|
44
|
+
"等一下",
|
|
45
|
+
"马上来",
|
|
46
|
+
"这个效果不错",
|
|
47
|
+
"刚刚更新了",
|
|
48
|
+
"看起来可以",
|
|
49
|
+
"再确认一下",
|
|
50
|
+
"这里有点奇怪",
|
|
51
|
+
"我先试试",
|
|
52
|
+
"辛苦了",
|
|
53
|
+
"已经修好了",
|
|
54
|
+
"周会上再聊",
|
|
55
|
+
"我发你截图",
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const sampleMixedPhrases = [
|
|
59
|
+
"hello 这条消息现在支持中文了",
|
|
60
|
+
"chat layout 这里需要再看一下",
|
|
61
|
+
"render 完成后记得刷新 preview",
|
|
62
|
+
"这段 mixed text 会测试自动换行 behavior",
|
|
63
|
+
"reply preview 里也放一点中文 content",
|
|
64
|
+
"今天的 build 已经 green 了",
|
|
65
|
+
"这个 bubble 的 spacing 感觉更自然",
|
|
66
|
+
"virtualized 列表滚动起来还是很顺",
|
|
67
|
+
"先 push 一个 demo message 看效果",
|
|
68
|
+
"中文 English mixed 排版更接近真实聊天",
|
|
69
|
+
];
|
|
70
|
+
|
|
40
71
|
type C = CanvasRenderingContext2D;
|
|
41
72
|
|
|
42
73
|
class RoundedBox extends PaddingBox<C> {
|
|
@@ -290,6 +321,8 @@ const renderItem = memoRenderItem((item: ChatItem): Node<C> => {
|
|
|
290
321
|
color: "black",
|
|
291
322
|
align: "start",
|
|
292
323
|
overflowWrap: "anywhere",
|
|
324
|
+
justify: "inter-character",
|
|
325
|
+
justifyGapThreshold: 0.2,
|
|
293
326
|
}),
|
|
294
327
|
{ alignSelf: "start" },
|
|
295
328
|
);
|
|
@@ -510,7 +543,7 @@ canvas.addEventListener("click", (e) => {
|
|
|
510
543
|
}
|
|
511
544
|
});
|
|
512
545
|
|
|
513
|
-
function
|
|
546
|
+
function randomEnglishText(words: number): string {
|
|
514
547
|
const out: string[] = [];
|
|
515
548
|
for (let i = 0; i < words; i += 1) {
|
|
516
549
|
out.push(sampleWords[Math.floor(Math.random() * sampleWords.length)]);
|
|
@@ -518,6 +551,52 @@ function randomText(words: number): string {
|
|
|
518
551
|
return out.join(" ");
|
|
519
552
|
}
|
|
520
553
|
|
|
554
|
+
function randomChineseText(words: number): string {
|
|
555
|
+
const out: string[] = [];
|
|
556
|
+
for (let i = 0; i < words; i += 1) {
|
|
557
|
+
out.push(
|
|
558
|
+
sampleChinesePhrases[
|
|
559
|
+
Math.floor(Math.random() * sampleChinesePhrases.length)
|
|
560
|
+
],
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
return out.join("");
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function randomMixedText(words: number): string {
|
|
567
|
+
const out: string[] = [];
|
|
568
|
+
for (let i = 0; i < words; i += 1) {
|
|
569
|
+
const mode = Math.random();
|
|
570
|
+
if (mode < 0.2) {
|
|
571
|
+
out.push(sampleWords[Math.floor(Math.random() * sampleWords.length)]);
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
if (mode < 0.65) {
|
|
575
|
+
out.push(
|
|
576
|
+
sampleChinesePhrases[
|
|
577
|
+
Math.floor(Math.random() * sampleChinesePhrases.length)
|
|
578
|
+
],
|
|
579
|
+
);
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
out.push(
|
|
583
|
+
sampleMixedPhrases[Math.floor(Math.random() * sampleMixedPhrases.length)],
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
return out.join(Math.random() < 0.5 ? " " : "");
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function randomText(words: number): string {
|
|
590
|
+
const mode = Math.random();
|
|
591
|
+
if (mode < 0.3) {
|
|
592
|
+
return randomEnglishText(words);
|
|
593
|
+
}
|
|
594
|
+
if (mode < 0.6) {
|
|
595
|
+
return randomChineseText(words);
|
|
596
|
+
}
|
|
597
|
+
return randomMixedText(words);
|
|
598
|
+
}
|
|
599
|
+
|
|
521
600
|
button("unshift", () => {
|
|
522
601
|
list.unshift({
|
|
523
602
|
id: nextMessageId++,
|
package/index.d.mts
CHANGED
|
@@ -96,10 +96,37 @@ interface InlineSpan<C extends CanvasRenderingContext2D> {
|
|
|
96
96
|
/** Optional extra occupied width appended after the span's rendered text. */
|
|
97
97
|
extraWidth?: number;
|
|
98
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Two-end justification mode for multi-line text.
|
|
101
|
+
* `"inter-word"` expands only collapsible spaces.
|
|
102
|
+
* `"inter-character"` is script-aware and may combine `wordSpacing` with `letterSpacing`.
|
|
103
|
+
*/
|
|
104
|
+
type TextJustifyMode = "inter-word" | "inter-character";
|
|
105
|
+
/**
|
|
106
|
+
* Options controlling two-end justification behavior.
|
|
107
|
+
*/
|
|
108
|
+
interface TextJustifyOptions {
|
|
109
|
+
/**
|
|
110
|
+
* Enable two-end justification. Default: false.
|
|
111
|
+
* `true` uses "inter-word" mode.
|
|
112
|
+
* `"inter-character"` may combine `wordSpacing` and `letterSpacing` internally.
|
|
113
|
+
*/
|
|
114
|
+
justify?: boolean | TextJustifyMode;
|
|
115
|
+
/**
|
|
116
|
+
* Whether to justify the last line as well. Default: false.
|
|
117
|
+
*/
|
|
118
|
+
justifyLastLine?: boolean;
|
|
119
|
+
/**
|
|
120
|
+
* Maximum ratio of a single gap relative to the average word/char width.
|
|
121
|
+
* Lines exceeding this threshold fall back to normal alignment.
|
|
122
|
+
* Default: 2.0. Set to Infinity to disable.
|
|
123
|
+
*/
|
|
124
|
+
justifyGapThreshold?: number;
|
|
125
|
+
}
|
|
99
126
|
/**
|
|
100
127
|
* Options for multi-line text nodes.
|
|
101
128
|
*/
|
|
102
|
-
interface MultilineTextOptions<C extends CanvasRenderingContext2D> extends TextStyleOptions<C
|
|
129
|
+
interface MultilineTextOptions<C extends CanvasRenderingContext2D> extends TextStyleOptions<C>, TextJustifyOptions {
|
|
103
130
|
/** Logical alignment that matches `Place.align`. */
|
|
104
131
|
align?: TextAlign;
|
|
105
132
|
/** Explicit physical alignment when left/right semantics are required. */
|
|
@@ -676,5 +703,5 @@ declare class TimelineRenderer<C extends CanvasRenderingContext2D, T extends {}>
|
|
|
676
703
|
hittest(test: HitTest): boolean;
|
|
677
704
|
}
|
|
678
705
|
//#endregion
|
|
679
|
-
export { Axis, BaseRenderer, Box, ChatRenderer, ChildLayoutResult, Context, CrossAxisAlignment, DebugRenderer, DynValue, Fixed, Flex, FlexContainerOptions, FlexItem, FlexItemOptions, FlexLayoutResult, Group, HitTest, InlineSpan, JumpToOptions, LayoutConstraints, LayoutRect, ListState, MainAxisAlignment, MainAxisSize, MultilineText, MultilineTextOptions, Node, PaddingBox, PhysicalTextAlign, Place, RenderFeedback, RendererOptions, ReplaceListItemAnimationOptions, ShrinkWrap, Text, TextAlign, TextEllipsisPosition, TextOptions, TextOverflowMode, TextOverflowWrapMode, TextStyleOptions, TextWhiteSpaceMode, TextWordBreakMode, TimelineRenderer, VirtualizedRenderer, Wrapper, memoRenderItem, memoRenderItemBy };
|
|
706
|
+
export { Axis, BaseRenderer, Box, ChatRenderer, ChildLayoutResult, Context, CrossAxisAlignment, DebugRenderer, DynValue, Fixed, Flex, FlexContainerOptions, FlexItem, FlexItemOptions, FlexLayoutResult, Group, HitTest, InlineSpan, JumpToOptions, LayoutConstraints, LayoutRect, ListState, MainAxisAlignment, MainAxisSize, MultilineText, MultilineTextOptions, Node, PaddingBox, PhysicalTextAlign, Place, RenderFeedback, RendererOptions, ReplaceListItemAnimationOptions, ShrinkWrap, Text, TextAlign, TextEllipsisPosition, TextJustifyMode, TextJustifyOptions, TextOptions, TextOverflowMode, TextOverflowWrapMode, TextStyleOptions, TextWhiteSpaceMode, TextWordBreakMode, TimelineRenderer, VirtualizedRenderer, Wrapper, memoRenderItem, memoRenderItemBy };
|
|
680
707
|
//# sourceMappingURL=index.d.mts.map
|