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 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 randomText(words: number): string {
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