chat-layout 1.2.0-2 → 1.2.0-4

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
@@ -28,10 +28,11 @@ const bubble = new RoundedBox(
28
28
  );
29
29
 
30
30
  const body = new ShrinkWrap(
31
- new Flex(
32
- [senderLine, bubble],
33
- { direction: "column", gap: 4, alignItems: item.sender === "A" ? "end" : "start" },
34
- ),
31
+ new Flex([senderLine, bubble], {
32
+ direction: "column",
33
+ gap: 4,
34
+ alignItems: item.sender === "A" ? "end" : "start",
35
+ }),
35
36
  );
36
37
 
37
38
  const row = new Flex(
@@ -74,17 +75,20 @@ See [example/chat.ts](./example/chat.ts) for a full chat example.
74
75
  Single-line `Text` can ellipsize at the start, end, or middle when a finite width constraint is present:
75
76
 
76
77
  ```ts
77
- const title = new Text([
78
- { text: "Extremely long " },
79
- { text: "thread title", font: "700 16px system-ui", color: "#0f766e" },
80
- { text: " that should not blow out the row" },
81
- ], {
82
- lineHeight: 20,
83
- font: "16px system-ui",
84
- color: "#111",
85
- overflow: "ellipsis",
86
- ellipsisPosition: "middle",
87
- });
78
+ const title = new Text(
79
+ [
80
+ { text: "Extremely long " },
81
+ { text: "thread title", font: "700 16px system-ui", color: "#0f766e" },
82
+ { text: " that should not blow out the row" },
83
+ ],
84
+ {
85
+ lineHeight: 20,
86
+ font: "16px system-ui",
87
+ color: "#111",
88
+ overflow: "ellipsis",
89
+ ellipsisPosition: "middle",
90
+ },
91
+ );
88
92
  ```
89
93
 
90
94
  Multi-line `MultilineText` can cap the visible line count and convert the last visible line to an end ellipsis:
@@ -119,9 +123,9 @@ const justified = new MultilineText(paragraph, {
119
123
  font: "16px system-ui",
120
124
  color: "#111",
121
125
  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
126
+ justify: true, // or "inter-word" | "inter-character"
127
+ justifyLastLine: false, // default: last line uses normal alignment
128
+ justifyGapThreshold: 2.0, // max gap ratio before fallback
125
129
  });
126
130
  ```
127
131
 
package/example/chat.ts CHANGED
@@ -195,7 +195,11 @@ const richTextMessage: InlineSpan<C>[] = [
195
195
  { text: "、" },
196
196
  { text: "粗体", font: "700 16px system-ui", color: "#b91c1c" },
197
197
  { text: ",以及 " },
198
- { text: "inline code", font: "15px ui-monospace, SFMono-Regular, Consolas, monospace", color: "#7c3aed" },
198
+ {
199
+ text: "inline code",
200
+ font: "15px ui-monospace, SFMono-Regular, Consolas, monospace",
201
+ color: "#7c3aed",
202
+ },
199
203
  { text: " 这样的片段混排。" },
200
204
  ];
201
205
 
@@ -205,7 +209,11 @@ const richReplyPreview: InlineSpan<C>[] = [
205
209
  { text: ",比如 " },
206
210
  { text: "关键词高亮", color: "#2563eb" },
207
211
  { text: " 和 " },
208
- { text: "code()", font: "12px ui-monospace, SFMono-Regular, Consolas, monospace", color: "#7c3aed" },
212
+ {
213
+ text: "code()",
214
+ font: "12px ui-monospace, SFMono-Regular, Consolas, monospace",
215
+ color: "#7c3aed",
216
+ },
209
217
  {
210
218
  text: ",超长内容仍然会按原来的两行省略规则收起,不需要额外处理。",
211
219
  },
@@ -234,13 +242,22 @@ class ItemDetector extends Wrapper<C> {
234
242
  hittest(_ctx: Context<C>, test: HitTest): boolean {
235
243
  currentHover = this.item;
236
244
  if (test.type === "click") {
237
- const index = list.items.indexOf(this.item);
238
- if (index < 0) {
245
+ if (!list.items.includes(this.item)) {
239
246
  return true;
240
247
  }
241
- const nextItem = this.item.kind === "revoked" ? this.item.original : revokeMessage(this.item);
248
+ const nextItem =
249
+ this.item.kind === "revoked"
250
+ ? this.item.original
251
+ : revokeMessage(this.item);
242
252
  currentHover = nextItem;
243
- list.replace(index, nextItem, {
253
+ list.update(this.item, nextItem, {
254
+ duration: REPLACE_ANIMATION_DURATION,
255
+ });
256
+ } else if (test.type === "auxclick") {
257
+ if (!list.items.includes(this.item)) {
258
+ return true;
259
+ }
260
+ list.delete(this.item, {
244
261
  duration: REPLACE_ANIMATION_DURATION,
245
262
  });
246
263
  }
@@ -266,8 +283,10 @@ const renderItem = memoRenderItem((item: ChatItem): Node<C> => {
266
283
  left: 12,
267
284
  right: 12,
268
285
  radii: 999,
269
- fill: () => (currentHover?.id === item.id ? "#d9d9d9" : "#ececec"),
270
- stroke: () => (currentHover?.id === item.id ? "#bcbcbc" : "#d3d3d3"),
286
+ fill: () =>
287
+ currentHover?.id === item.id ? "#d9d9d9" : "#ececec",
288
+ stroke: () =>
289
+ currentHover?.id === item.id ? "#bcbcbc" : "#d3d3d3",
271
290
  },
272
291
  ),
273
292
  {
@@ -543,6 +562,19 @@ canvas.addEventListener("click", (e) => {
543
562
  }
544
563
  });
545
564
 
565
+ canvas.addEventListener("contextmenu", (e) => {
566
+ e.preventDefault();
567
+ const { top, left } = canvas.getBoundingClientRect();
568
+ const result = renderer.hittest({
569
+ x: e.clientX - left,
570
+ y: e.clientY - top,
571
+ type: "auxclick",
572
+ });
573
+ if (!result) {
574
+ currentHover = undefined;
575
+ }
576
+ });
577
+
546
578
  function randomEnglishText(words: number): string {
547
579
  const out: string[] = [];
548
580
  for (let i = 0; i < words; i += 1) {
@@ -632,12 +664,13 @@ button("jump latest (no anim)", () => {
632
664
  });
633
665
 
634
666
  button("revoke first", () => {
635
- const item = list.items.find((entry): entry is MessageItem => entry.kind === "message");
667
+ const item = list.items.find(
668
+ (entry): entry is MessageItem => entry.kind === "message",
669
+ );
636
670
  if (item == null) {
637
671
  return;
638
672
  }
639
- const index = list.items.indexOf(item);
640
- list.replace(index, revokeMessage(item), {
673
+ list.update(item, revokeMessage(item), {
641
674
  duration: REPLACE_ANIMATION_DURATION,
642
675
  });
643
676
  });
package/example/test.ts CHANGED
@@ -47,8 +47,10 @@ class RoundedBox extends PaddingBox<C> {
47
47
  // Reuse the current layout constraints so the background matches wrapped text.
48
48
  const { width, height } = ctx.measureNode(this, ctx.constraints);
49
49
  ctx.with((g) => {
50
- const fill = this.fill == null ? undefined : ctx.resolveDynValue(this.fill);
51
- const stroke = this.stroke == null ? undefined : ctx.resolveDynValue(this.stroke);
50
+ const fill =
51
+ this.fill == null ? undefined : ctx.resolveDynValue(this.fill);
52
+ const stroke =
53
+ this.stroke == null ? undefined : ctx.resolveDynValue(this.stroke);
52
54
  g.beginPath();
53
55
  g.roundRect(x, y, width, height, this.radii);
54
56
  if (fill != null) {
package/index.d.mts CHANGED
@@ -509,7 +509,11 @@ declare class DebugRenderer<C extends CanvasRenderingContext2D> extends BaseRend
509
509
  /**
510
510
  * Mutable list state shared with virtualized renderers.
511
511
  */
512
- interface ReplaceListItemAnimationOptions {
512
+ interface UpdateListItemAnimationOptions {
513
+ /** Animation duration in milliseconds. */
514
+ duration?: number;
515
+ }
516
+ interface DeleteListItemAnimationOptions {
513
517
  /** Animation duration in milliseconds. */
514
518
  duration?: number;
515
519
  }
@@ -536,9 +540,17 @@ declare class ListState<T extends {}> {
536
540
  /** Appends an array of items. */
537
541
  pushAll(items: T[]): void;
538
542
  /**
539
- * Replaces an existing item by index.
543
+ * Updates an existing item by object identity.
544
+ */
545
+ update(targetItem: T, nextItem: T, animation?: UpdateListItemAnimationOptions): void;
546
+ /**
547
+ * Starts deleting an existing item by object identity.
540
548
  */
541
- replace(index: number, item: T, animation?: ReplaceListItemAnimationOptions): void;
549
+ delete(item: T, animation?: DeleteListItemAnimationOptions): void;
550
+ /**
551
+ * Finalizes a pending delete by removing the item from the list.
552
+ */
553
+ finalizeDelete(item: T): void;
542
554
  /**
543
555
  * Sets the current anchor item and pixel offset.
544
556
  */
@@ -570,6 +582,13 @@ declare function memoRenderItemBy<C extends CanvasRenderingContext2D, T, K>(keyO
570
582
  resetKey: (key: K) => boolean;
571
583
  };
572
584
  //#endregion
585
+ //#region src/renderer/virtualized/base-types.d.ts
586
+ /** Per-item draw/hittest callbacks produced by the resolver. */
587
+ type VirtualizedResolvedItem = {
588
+ draw: (y: number) => boolean;
589
+ hittest: (test: HitTest, y: number) => boolean;
590
+ };
591
+ //#endregion
573
592
  //#region src/renderer/virtualized/solver.d.ts
574
593
  interface VisibleListState {
575
594
  position?: number;
@@ -589,6 +608,10 @@ interface VisibleWindow<T> {
589
608
  drawList: VisibleWindowEntry<T>[];
590
609
  shift: number;
591
610
  }
611
+ interface VisibleWindowResult<T> {
612
+ normalizedState: NormalizedListState;
613
+ window: VisibleWindow<T>;
614
+ }
592
615
  //#endregion
593
616
  //#region src/renderer/virtualized/base.d.ts
594
617
  /**
@@ -604,10 +627,6 @@ interface JumpToOptions {
604
627
  /** Called after the jump completes or finishes animating. */
605
628
  onComplete?: () => void;
606
629
  }
607
- type VirtualizedResolvedItem<C extends CanvasRenderingContext2D> = {
608
- draw: (y: number) => boolean;
609
- hittest: (test: HitTest, y: number) => boolean;
610
- };
611
630
  /**
612
631
  * Shared base class for virtualized list renderers.
613
632
  */
@@ -636,9 +655,9 @@ declare abstract class VirtualizedRenderer<C extends CanvasRenderingContext2D, T
636
655
  /** Replaces the current item collection. */
637
656
  set items(value: T[]);
638
657
  /** Renders the current visible window. */
639
- abstract render(feedback?: RenderFeedback): boolean;
658
+ render(feedback?: RenderFeedback): boolean;
640
659
  /** Hit-tests the current visible window. */
641
- abstract hittest(test: {
660
+ hittest(test: {
642
661
  x: number;
643
662
  y: number;
644
663
  type: "click" | "auxclick" | "hover";
@@ -651,19 +670,26 @@ declare abstract class VirtualizedRenderer<C extends CanvasRenderingContext2D, T
651
670
  jumpTo(index: number, options?: JumpToOptions): void;
652
671
  protected _resetRenderFeedback(feedback?: RenderFeedback): void;
653
672
  protected _accumulateRenderFeedback(feedback: RenderFeedback, idx: number, top: number, height: number): void;
654
- protected _renderDrawList(list: VisibleWindow<VirtualizedResolvedItem<C>>["drawList"], shift: number, feedback?: RenderFeedback): boolean;
655
- protected _renderVisibleWindow(window: VisibleWindow<VirtualizedResolvedItem<C>>, feedback?: RenderFeedback): boolean;
656
- protected _hittestVisibleWindow(window: VisibleWindow<VirtualizedResolvedItem<C>>, test: HitTest): boolean;
657
- protected _prepareRender(): boolean;
673
+ protected _renderDrawList(list: VisibleWindow<VirtualizedResolvedItem>["drawList"], shift: number, feedback?: RenderFeedback): boolean;
674
+ protected _renderVisibleWindow(window: VisibleWindow<VirtualizedResolvedItem>, feedback?: RenderFeedback): boolean;
675
+ protected _readVisibleRange(top: number, height: number): {
676
+ top: number;
677
+ bottom: number;
678
+ } | undefined;
679
+ protected _pruneReplacementAnimations(_window: VisibleWindow<unknown>): boolean;
680
+ protected _hittestVisibleWindow(window: VisibleWindow<VirtualizedResolvedItem>, test: HitTest): boolean;
681
+ protected _captureVisibleItemSnapshot(window: VisibleWindow<unknown>): void;
682
+ protected _prepareRender(now: number): boolean;
658
683
  protected _finishRender(requestRedraw: boolean): boolean;
659
684
  protected _clampItemIndex(index: number): number;
660
685
  protected _getItemHeight(index: number): number;
661
- protected _resolveItem(item: T, index: number, now: number): {
662
- value: VirtualizedResolvedItem<C>;
686
+ protected _resolveItem(item: T, _index: number, now: number): {
687
+ value: VirtualizedResolvedItem;
663
688
  height: number;
664
689
  };
665
690
  protected _getAnchorAtOffset(index: number, offset: number): number;
666
691
  protected abstract _normalizeListState(state: VisibleListState): NormalizedListState;
692
+ protected abstract _resolveVisibleWindow(now: number): VisibleWindowResult<VirtualizedResolvedItem>;
667
693
  protected abstract _readAnchor(state: NormalizedListState): number;
668
694
  protected abstract _applyAnchor(anchor: number): void;
669
695
  protected abstract _getDefaultJumpBlock(): NonNullable<JumpToOptions["block"]>;
@@ -676,15 +702,13 @@ declare abstract class VirtualizedRenderer<C extends CanvasRenderingContext2D, T
676
702
  * Virtualized renderer anchored to the bottom, suitable for chat-style UIs.
677
703
  */
678
704
  declare class ChatRenderer<C extends CanvasRenderingContext2D, T extends {}> extends VirtualizedRenderer<C, T> {
679
- #private;
705
+ protected _resolveVisibleWindow(now: number): VisibleWindowResult<VirtualizedResolvedItem>;
680
706
  protected _getDefaultJumpBlock(): NonNullable<JumpToOptions["block"]>;
681
707
  protected _normalizeListState(state: VisibleListState): NormalizedListState;
682
708
  protected _readAnchor(state: NormalizedListState): number;
683
709
  protected _applyAnchor(anchor: number): void;
684
710
  protected _getTargetAnchor(index: number, block: NonNullable<JumpToOptions["block"]>): number;
685
711
  protected _getAnimatedLayerOffset(slotHeight: number, nodeHeight: number): number;
686
- render(feedback?: RenderFeedback): boolean;
687
- hittest(test: HitTest): boolean;
688
712
  }
689
713
  //#endregion
690
714
  //#region src/renderer/virtualized/timeline.d.ts
@@ -692,16 +716,14 @@ declare class ChatRenderer<C extends CanvasRenderingContext2D, T extends {}> ext
692
716
  * Virtualized renderer anchored to the top, suitable for timeline-style UIs.
693
717
  */
694
718
  declare class TimelineRenderer<C extends CanvasRenderingContext2D, T extends {}> extends VirtualizedRenderer<C, T> {
695
- #private;
719
+ protected _resolveVisibleWindow(now: number): VisibleWindowResult<VirtualizedResolvedItem>;
696
720
  protected _getDefaultJumpBlock(): NonNullable<JumpToOptions["block"]>;
697
721
  protected _normalizeListState(state: VisibleListState): NormalizedListState;
698
722
  protected _readAnchor(state: NormalizedListState): number;
699
723
  protected _applyAnchor(anchor: number): void;
700
724
  protected _getTargetAnchor(index: number, block: NonNullable<JumpToOptions["block"]>): number;
701
725
  protected _getAnimatedLayerOffset(_slotHeight: number, _nodeHeight: number): number;
702
- render(feedback?: RenderFeedback): boolean;
703
- hittest(test: HitTest): boolean;
704
726
  }
705
727
  //#endregion
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 };
728
+ export { Axis, BaseRenderer, Box, ChatRenderer, ChildLayoutResult, Context, CrossAxisAlignment, DebugRenderer, DeleteListItemAnimationOptions, DynValue, Fixed, Flex, FlexContainerOptions, FlexItem, FlexItemOptions, FlexLayoutResult, Group, HitTest, InlineSpan, JumpToOptions, LayoutConstraints, LayoutRect, ListState, MainAxisAlignment, MainAxisSize, MultilineText, MultilineTextOptions, Node, PaddingBox, PhysicalTextAlign, Place, RenderFeedback, RendererOptions, ShrinkWrap, Text, TextAlign, TextEllipsisPosition, TextJustifyMode, TextJustifyOptions, TextOptions, TextOverflowMode, TextOverflowWrapMode, TextStyleOptions, TextWhiteSpaceMode, TextWordBreakMode, TimelineRenderer, UpdateListItemAnimationOptions, VirtualizedRenderer, Wrapper, memoRenderItem, memoRenderItemBy };
707
729
  //# sourceMappingURL=index.d.mts.map