chat-layout 1.1.2 → 1.2.0-1

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
@@ -7,13 +7,14 @@ The current v2-style APIs are:
7
7
  - `Flex`: row/column layout
8
8
  - `FlexItem`: explicit `grow` / `shrink` / `alignSelf`
9
9
  - `Place`: place a single child at `start` / `center` / `end`
10
+ - `ShrinkWrap`: search the narrowest width that keeps the current height stable
10
11
  - `MultilineText`: text layout with logical `align` or physical `physicalAlign`
11
12
  - `ChatRenderer` + `ListState`: virtualized chat rendering
12
13
  - `memoRenderItem` / `memoRenderItemBy`: item render memoization
13
14
 
14
15
  ## Quick example
15
16
 
16
- Use `Flex` to build structure, `FlexItem` to control resize behavior, and `Place` to align the final bubble:
17
+ Use `Flex` to build structure, `FlexItem` to control resize behavior, `ShrinkWrap` to keep the bubble as narrow as possible without adding lines, and `Place` to align the final bubble:
17
18
 
18
19
  ```ts
19
20
  const bubble = new RoundedBox(
@@ -26,17 +27,27 @@ const bubble = new RoundedBox(
26
27
  { top: 6, bottom: 6, left: 10, right: 10, radii: 8, fill: "#ccc" },
27
28
  );
28
29
 
30
+ const body = new ShrinkWrap(
31
+ new Flex(
32
+ [senderLine, bubble],
33
+ { direction: "column", gap: 4, alignItems: item.sender === "A" ? "end" : "start" },
34
+ ),
35
+ );
36
+
29
37
  const row = new Flex(
30
38
  [
31
39
  avatar,
32
- new FlexItem(bubble, { grow: 1, shrink: 1 }),
40
+ new FlexItem(
41
+ new Place(body, {
42
+ align: item.sender === "A" ? "end" : "start",
43
+ }),
44
+ { grow: 1, shrink: 1 },
45
+ ),
33
46
  ],
34
47
  { direction: "row", gap: 4, reverse: item.sender === "A" },
35
48
  );
36
49
 
37
- return new Place(row, {
38
- align: item.sender === "A" ? "end" : "start",
39
- });
50
+ return row;
40
51
  ```
41
52
 
42
53
  See [example/chat.ts](./example/chat.ts) for a full chat example.
@@ -47,9 +58,10 @@ See [example/chat.ts](./example/chat.ts) for a full chat example.
47
58
  - `maxWidth` / `maxHeight` limit measurement, but do not automatically make children fill the cross axis.
48
59
  - Use `alignItems: "stretch"` or `alignSelf: "stretch"` when a child should fill the computed cross size.
49
60
  - `Place` is the simplest way to align a single bubble left, center, or right.
61
+ - `ShrinkWrap` is useful when a bubble sits inside a growable slot but should still collapse to the narrowest width that preserves its current line count.
50
62
  - `MultilineText.align` uses logical values: `start`, `center`, `end`.
51
63
  - `MultilineText.physicalAlign` uses physical values: `left`, `center`, `right`.
52
- - `Text` and `MultilineText` default to `whiteSpace: "normal"`, matching pretext and CSS-style collapsible whitespace.
64
+ - `Text` and `MultilineText` default to `whiteSpace: "normal"`, using the library's canvas-first collapsible whitespace behavior.
53
65
  - Use `whiteSpace: "pre-wrap"` when blank lines, hard breaks, or edge spaces must stay visible.
54
66
  - `Text` and `MultilineText` default to `overflowWrap: "break-word"`, which preserves compatibility-first min-content sizing for shrink layouts.
55
67
  - Use `overflowWrap: "anywhere"` when long unspaced strings should contribute grapheme-level breakpoints to min-content sizing.
@@ -103,6 +115,7 @@ Notes:
103
115
  - Shrink only applies when there is a finite main-axis constraint and total content size overflows it.
104
116
  - Overflow is redistributed by `shrink * basis`; today `basis` is internal-only and always `"auto"`.
105
117
  - Custom nodes can implement `measureMinContent()` for better shrink results.
118
+ - `ShrinkWrap` complements flex shrink: it keeps probing narrower `maxWidth` values until the child would become taller, then uses the last safe width as the final layout.
106
119
  - Known limitation: column shrink with `MultilineText` does not clip drawing by itself.
107
120
 
108
121
  ## Migration notes
@@ -139,3 +152,5 @@ Build the chat example:
139
152
  ```bash
140
153
  bun run example
141
154
  ```
155
+
156
+ 文本性能观测基线见 `docs/text-performance.md`。
package/example/chat.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  MultilineText,
8
8
  PaddingBox,
9
9
  Place,
10
+ ShrinkWrap,
10
11
  Text,
11
12
  Wrapper,
12
13
  memoRenderItem,
@@ -357,7 +358,11 @@ const renderItem = memoRenderItem((item: ChatItem): Node<C> => {
357
358
  alignItems: item.sender === "A" ? "end" : "start",
358
359
  });
359
360
 
360
- const alignedBody = new Place<C>(body, {
361
+ const shrinkWrappedBody = new ShrinkWrap<C>(body, {
362
+ preferredMinWidth: 160,
363
+ });
364
+
365
+ const alignedBody = new Place<C>(shrinkWrappedBody, {
361
366
  align: item.sender === "A" ? "end" : "start",
362
367
  });
363
368
 
package/index.d.mts CHANGED
@@ -53,7 +53,7 @@ type TextWhiteSpaceMode = "normal" | "pre-wrap";
53
53
  */
54
54
  type TextOverflowMode = "clip" | "ellipsis";
55
55
  /**
56
- * Word breaking mode used by pretext during segmentation and line breaking.
56
+ * Word breaking mode used by the internal canvas text layout engine.
57
57
  */
58
58
  type TextWordBreakMode = "normal" | "keep-all";
59
59
  /**
@@ -74,9 +74,9 @@ interface TextStyleOptions<C extends CanvasRenderingContext2D> {
74
74
  font: string;
75
75
  /** Color or resolver used when drawing the text. */
76
76
  color: DynValue<C, string>;
77
- /** Default: normal; matches pretext and CSS-style collapsible whitespace behavior. */
77
+ /** Default: normal; uses canvas-first CSS-style collapsible whitespace behavior. */
78
78
  whiteSpace?: TextWhiteSpaceMode;
79
- /** Default: normal; use keep-all to match pretext's CJK-friendly line breaking mode. */
79
+ /** Default: normal; use keep-all for CJK-friendly line breaking. */
80
80
  wordBreak?: TextWordBreakMode;
81
81
  /** Default: break-word; use anywhere when min-content should honor grapheme break opportunities. */
82
82
  overflowWrap?: TextOverflowWrapMode;
@@ -91,9 +91,9 @@ interface InlineSpan<C extends CanvasRenderingContext2D> {
91
91
  font?: string;
92
92
  /** Color override for this fragment. Falls back to the node-level color. */
93
93
  color?: DynValue<C, string>;
94
- /** Optional break hint forwarded to pretext rich-inline layout. */
94
+ /** Optional break hint for atomic inline spans. */
95
95
  break?: "normal" | "never";
96
- /** Optional extra occupied width forwarded to pretext rich-inline layout. */
96
+ /** Optional extra occupied width appended after the span's rendered text. */
97
97
  extraWidth?: number;
98
98
  }
99
99
  /**
@@ -364,6 +364,24 @@ declare class Place<C extends CanvasRenderingContext2D> extends Wrapper<C> {
364
364
  hittest(ctx: Context<C>, test: HitTest): boolean;
365
365
  }
366
366
  //#endregion
367
+ //#region src/nodes/shrinkwrap.d.ts
368
+ interface ShrinkWrapOptions {
369
+ tolerance?: number;
370
+ preferredMinWidth?: number;
371
+ }
372
+ /**
373
+ * Shrinks a single child to the narrowest width that does not increase its reference height.
374
+ */
375
+ declare class ShrinkWrap<C extends CanvasRenderingContext2D> extends Wrapper<C> {
376
+ #private;
377
+ readonly options: ShrinkWrapOptions;
378
+ constructor(inner: Node<C>, options?: ShrinkWrapOptions);
379
+ measure(ctx: Context<C>): Box;
380
+ measureMinContent(ctx: Context<C>): Box;
381
+ draw(ctx: Context<C>, x: number, y: number): boolean;
382
+ hittest(ctx: Context<C>, test: HitTest): boolean;
383
+ }
384
+ //#endregion
367
385
  //#region src/nodes/text.d.ts
368
386
  /**
369
387
  * Draws wrapped text using the configured line height and alignment.
@@ -658,5 +676,5 @@ declare class TimelineRenderer<C extends CanvasRenderingContext2D, T extends {}>
658
676
  hittest(test: HitTest): boolean;
659
677
  }
660
678
  //#endregion
661
- 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, Text, TextAlign, TextEllipsisPosition, TextOptions, TextOverflowMode, TextOverflowWrapMode, TextStyleOptions, TextWhiteSpaceMode, TextWordBreakMode, TimelineRenderer, VirtualizedRenderer, Wrapper, memoRenderItem, memoRenderItemBy };
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 };
662
680
  //# sourceMappingURL=index.d.mts.map