chat-layout 1.2.0-4 → 1.2.0-5

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
@@ -9,7 +9,7 @@ The current v2-style APIs are:
9
9
  - `Place`: place a single child at `start` / `center` / `end`
10
10
  - `ShrinkWrap`: search the narrowest width that keeps the current height stable
11
11
  - `MultilineText`: text layout with logical `align` or physical `physicalAlign`
12
- - `ChatRenderer` + `ListState`: virtualized chat rendering
12
+ - `ListRenderer` + `ListState`: virtualized chat or timeline rendering
13
13
  - `memoRenderItem` / `memoRenderItemBy`: item render memoization
14
14
 
15
15
  ## Quick example
@@ -53,6 +53,29 @@ return row;
53
53
 
54
54
  See [example/chat.ts](./example/chat.ts) for a full chat example.
55
55
 
56
+ ## List insert animation
57
+
58
+ `pushAll()` and `unshiftAll()` can opt into short-list insertion animations. They only animate when the previous rendered frame still had spare space below the last item; otherwise they fall back to the normal hard cut:
59
+
60
+ ```ts
61
+ list.pushAll([nextMessage], {
62
+ distance: 24, // duration defaults to 220ms when animation options are present
63
+ });
64
+
65
+ list.unshiftAll([olderMessage], {
66
+ duration: 220,
67
+ });
68
+ ```
69
+
70
+ To make chat-style inserts automatically follow the latest visible edge, pass `followIfAtBoundary: true`. When the viewport was already pinned to that edge, the insert behaves like a conditional `jumpTo()` instead of combining with the enter animation:
71
+
72
+ ```ts
73
+ list.pushAll([nextMessage], {
74
+ followIfAtBoundary: true,
75
+ duration: 220,
76
+ });
77
+ ```
78
+
56
79
  ## Layout notes
57
80
 
58
81
  - `Flex` handles the main axis only. It shrink-wraps on the cross axis unless you opt into stretch behavior.
package/example/chat.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import {
2
- ChatRenderer,
3
2
  Flex,
4
3
  FlexItem,
5
4
  Fixed,
5
+ ListRenderer,
6
6
  ListState,
7
7
  MultilineText,
8
8
  PaddingBox,
@@ -221,6 +221,7 @@ const richReplyPreview: InlineSpan<C>[] = [
221
221
 
222
222
  let currentHover: ChatItem | undefined;
223
223
  const REPLACE_ANIMATION_DURATION = 320;
224
+ const INSERT_ANIMATION_DURATION = 220;
224
225
 
225
226
  function revokeMessage(item: MessageItem): RevokedItem {
226
227
  return {
@@ -498,7 +499,9 @@ const list = new ListState<ChatItem>([
498
499
  },
499
500
  { id: 9, kind: "message", sender: "B", content: randomText(5) },
500
501
  ]);
501
- const renderer = new ChatRenderer(ctx, {
502
+ const renderer = new ListRenderer(ctx, {
503
+ anchorMode: "bottom",
504
+ underflowAlign: "top",
502
505
  renderItem,
503
506
  list,
504
507
  });
@@ -630,21 +633,37 @@ function randomText(words: number): string {
630
633
  }
631
634
 
632
635
  button("unshift", () => {
633
- list.unshift({
634
- id: nextMessageId++,
635
- kind: "message",
636
- sender: Math.random() < 0.5 ? "A" : "B",
637
- content: randomText(10 + Math.floor(200 * Math.random())),
638
- });
636
+ list.unshiftAll(
637
+ [
638
+ {
639
+ id: nextMessageId++,
640
+ kind: "message",
641
+ sender: Math.random() < 0.5 ? "A" : "B",
642
+ content: randomText(10 + Math.floor(200 * Math.random())),
643
+ },
644
+ ],
645
+ {
646
+ duration: INSERT_ANIMATION_DURATION,
647
+ followIfAtBoundary: true,
648
+ },
649
+ );
639
650
  });
640
651
 
641
652
  button("push", () => {
642
- list.push({
643
- id: nextMessageId++,
644
- kind: "message",
645
- sender: Math.random() < 0.5 ? "A" : "B",
646
- content: randomText(10 + Math.floor(200 * Math.random())),
647
- });
653
+ list.pushAll(
654
+ [
655
+ {
656
+ id: nextMessageId++,
657
+ kind: "message",
658
+ sender: Math.random() < 0.5 ? "A" : "B",
659
+ content: randomText(10 + Math.floor(200 * Math.random())),
660
+ },
661
+ ],
662
+ {
663
+ distance: 24,
664
+ followIfAtBoundary: true,
665
+ },
666
+ );
648
667
  });
649
668
 
650
669
  button("jump middle", () => {
package/index.d.mts CHANGED
@@ -517,6 +517,16 @@ interface DeleteListItemAnimationOptions {
517
517
  /** Animation duration in milliseconds. */
518
518
  duration?: number;
519
519
  }
520
+ interface InsertListItemsAnimationOptions {
521
+ /** Animation duration in milliseconds. */
522
+ duration?: number;
523
+ /** Enter offset in pixels measured from the final resting position. */
524
+ distance?: number;
525
+ /** Auto-follow the insertion edge when the viewport was already pinned there. */
526
+ followIfAtBoundary?: boolean;
527
+ }
528
+ type PushListItemsAnimationOptions = InsertListItemsAnimationOptions;
529
+ type UnshiftListItemsAnimationOptions = InsertListItemsAnimationOptions;
520
530
  declare class ListState<T extends {}> {
521
531
  #private;
522
532
  /** Pixel offset from the anchored item edge. */
@@ -534,11 +544,11 @@ declare class ListState<T extends {}> {
534
544
  /** Prepends one or more items. */
535
545
  unshift(...items: T[]): void;
536
546
  /** Prepends an array of items. */
537
- unshiftAll(items: T[]): void;
547
+ unshiftAll(items: T[], animation?: UnshiftListItemsAnimationOptions): void;
538
548
  /** Appends one or more items. */
539
549
  push(...items: T[]): void;
540
550
  /** Appends an array of items. */
541
- pushAll(items: T[]): void;
551
+ pushAll(items: T[], animation?: PushListItemsAnimationOptions): void;
542
552
  /**
543
553
  * Updates an existing item by object identity.
544
554
  */
@@ -590,6 +600,16 @@ type VirtualizedResolvedItem = {
590
600
  };
591
601
  //#endregion
592
602
  //#region src/renderer/virtualized/solver.d.ts
603
+ type ListAnchorMode = "top" | "bottom";
604
+ type ListUnderflowAlign = "top" | "bottom";
605
+ interface ListLayoutOptions {
606
+ anchorMode?: ListAnchorMode;
607
+ underflowAlign?: ListUnderflowAlign;
608
+ }
609
+ interface ResolvedListLayoutOptions {
610
+ anchorMode: ListAnchorMode;
611
+ underflowAlign: ListUnderflowAlign;
612
+ }
593
613
  interface VisibleListState {
594
614
  position?: number;
595
615
  offset: number;
@@ -610,6 +630,7 @@ interface VisibleWindow<T> {
610
630
  }
611
631
  interface VisibleWindowResult<T> {
612
632
  normalizedState: NormalizedListState;
633
+ resolutionPath: number[];
613
634
  window: VisibleWindow<T>;
614
635
  }
615
636
  //#endregion
@@ -637,7 +658,7 @@ declare abstract class VirtualizedRenderer<C extends CanvasRenderingContext2D, T
637
658
  #private;
638
659
  static readonly MIN_JUMP_DURATION = 160;
639
660
  static readonly MAX_JUMP_DURATION = 420;
640
- static readonly JUMP_DURATION_PER_ITEM = 28;
661
+ static readonly JUMP_DURATION_PER_PIXEL = 0.7;
641
662
  constructor(graphics: C, options: {
642
663
  renderItem: (item: T) => Node<C>;
643
664
  list: ListState<T>;
@@ -663,6 +684,7 @@ declare abstract class VirtualizedRenderer<C extends CanvasRenderingContext2D, T
663
684
  type: "click" | "auxclick" | "hover";
664
685
  }): boolean;
665
686
  protected _readListState(): VisibleListState;
687
+ protected _resolveVisibleWindow(now: number): VisibleWindowResult<VirtualizedResolvedItem>;
666
688
  protected _commitListState(state: NormalizedListState): void;
667
689
  /**
668
690
  * Scrolls the viewport to the requested item index.
@@ -671,59 +693,53 @@ declare abstract class VirtualizedRenderer<C extends CanvasRenderingContext2D, T
671
693
  protected _resetRenderFeedback(feedback?: RenderFeedback): void;
672
694
  protected _accumulateRenderFeedback(feedback: RenderFeedback, idx: number, top: number, height: number): void;
673
695
  protected _renderDrawList(list: VisibleWindow<VirtualizedResolvedItem>["drawList"], shift: number, feedback?: RenderFeedback): boolean;
674
- protected _renderVisibleWindow(window: VisibleWindow<VirtualizedResolvedItem>, feedback?: RenderFeedback): boolean;
696
+ protected _renderVisibleWindow(window: VisibleWindow<VirtualizedResolvedItem>, feedback?: RenderFeedback, extraShift?: number): boolean;
675
697
  protected _readVisibleRange(top: number, height: number): {
676
698
  top: number;
677
699
  bottom: number;
678
700
  } | undefined;
679
- protected _pruneReplacementAnimations(_window: VisibleWindow<unknown>): boolean;
680
- protected _hittestVisibleWindow(window: VisibleWindow<VirtualizedResolvedItem>, test: HitTest): boolean;
681
- protected _captureVisibleItemSnapshot(window: VisibleWindow<unknown>): void;
701
+ protected _pruneTransitionAnimations(_window: VisibleWindow<unknown>, now: number): boolean;
702
+ protected _hittestVisibleWindow(window: VisibleWindow<VirtualizedResolvedItem>, test: HitTest, extraShift?: number): boolean;
703
+ protected _captureVisibleItemSnapshot(solution: VisibleWindowResult<unknown>, extraShift?: number): void;
682
704
  protected _prepareRender(now: number): boolean;
683
705
  protected _finishRender(requestRedraw: boolean): boolean;
684
706
  protected _clampItemIndex(index: number): number;
685
707
  protected _getItemHeight(index: number): number;
708
+ protected _getItemHeightAt(index: number, now: number): number;
709
+ protected _readAnchorAt(now: number): number | undefined;
710
+ protected _restoreAnchor(anchor: number): void;
686
711
  protected _resolveItem(item: T, _index: number, now: number): {
687
712
  value: VirtualizedResolvedItem;
688
713
  height: number;
689
714
  };
690
- protected _getAnchorAtOffset(index: number, offset: number): number;
691
715
  protected abstract _normalizeListState(state: VisibleListState): NormalizedListState;
692
- protected abstract _resolveVisibleWindow(now: number): VisibleWindowResult<VirtualizedResolvedItem>;
693
- protected abstract _readAnchor(state: NormalizedListState): number;
716
+ protected abstract _getLayoutOptions(): ResolvedListLayoutOptions;
717
+ protected abstract _resolveVisibleWindowForState(state: VisibleListState, now: number): VisibleWindowResult<VirtualizedResolvedItem>;
718
+ protected abstract _readAnchor(state: NormalizedListState, readItemHeight: (index: number) => number): number;
694
719
  protected abstract _applyAnchor(anchor: number): void;
695
720
  protected abstract _getDefaultJumpBlock(): NonNullable<JumpToOptions["block"]>;
696
721
  protected abstract _getTargetAnchor(index: number, block: NonNullable<JumpToOptions["block"]>): number;
697
- protected abstract _getAnimatedLayerOffset(slotHeight: number, nodeHeight: number): number;
698
722
  }
699
723
  //#endregion
700
- //#region src/renderer/virtualized/chat.d.ts
701
- /**
702
- * Virtualized renderer anchored to the bottom, suitable for chat-style UIs.
703
- */
704
- declare class ChatRenderer<C extends CanvasRenderingContext2D, T extends {}> extends VirtualizedRenderer<C, T> {
705
- protected _resolveVisibleWindow(now: number): VisibleWindowResult<VirtualizedResolvedItem>;
706
- protected _getDefaultJumpBlock(): NonNullable<JumpToOptions["block"]>;
707
- protected _normalizeListState(state: VisibleListState): NormalizedListState;
708
- protected _readAnchor(state: NormalizedListState): number;
709
- protected _applyAnchor(anchor: number): void;
710
- protected _getTargetAnchor(index: number, block: NonNullable<JumpToOptions["block"]>): number;
711
- protected _getAnimatedLayerOffset(slotHeight: number, nodeHeight: number): number;
724
+ //#region src/renderer/virtualized/list.d.ts
725
+ interface ListRendererOptions<C extends CanvasRenderingContext2D, T extends {}> extends ListLayoutOptions {
726
+ renderItem: (item: T) => Node<C>;
727
+ list: ListState<T>;
712
728
  }
713
- //#endregion
714
- //#region src/renderer/virtualized/timeline.d.ts
715
729
  /**
716
- * Virtualized renderer anchored to the top, suitable for timeline-style UIs.
730
+ * Virtualized list renderer with configurable anchor semantics.
717
731
  */
718
- declare class TimelineRenderer<C extends CanvasRenderingContext2D, T extends {}> extends VirtualizedRenderer<C, T> {
719
- protected _resolveVisibleWindow(now: number): VisibleWindowResult<VirtualizedResolvedItem>;
732
+ declare class ListRenderer<C extends CanvasRenderingContext2D, T extends {}> extends VirtualizedRenderer<C, T> {
733
+ #private;
734
+ constructor(graphics: C, options: ListRendererOptions<C, T>);
735
+ protected _getLayoutOptions(): ResolvedListLayoutOptions;
736
+ protected _resolveVisibleWindowForState(state: VisibleListState, now: number): VisibleWindowResult<VirtualizedResolvedItem>;
720
737
  protected _getDefaultJumpBlock(): NonNullable<JumpToOptions["block"]>;
721
738
  protected _normalizeListState(state: VisibleListState): NormalizedListState;
722
- protected _readAnchor(state: NormalizedListState): number;
739
+ protected _readAnchor(state: NormalizedListState, readItemHeight: (index: number) => number): number;
723
740
  protected _applyAnchor(anchor: number): void;
724
741
  protected _getTargetAnchor(index: number, block: NonNullable<JumpToOptions["block"]>): number;
725
- protected _getAnimatedLayerOffset(_slotHeight: number, _nodeHeight: number): number;
726
742
  }
727
743
  //#endregion
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 };
744
+ export { Axis, BaseRenderer, Box, ChildLayoutResult, Context, CrossAxisAlignment, DebugRenderer, DeleteListItemAnimationOptions, DynValue, Fixed, Flex, FlexContainerOptions, FlexItem, FlexItemOptions, FlexLayoutResult, Group, HitTest, InlineSpan, InsertListItemsAnimationOptions, JumpToOptions, LayoutConstraints, LayoutRect, ListAnchorMode, ListLayoutOptions, ListRenderer, ListRendererOptions, ListState, ListUnderflowAlign, MainAxisAlignment, MainAxisSize, MultilineText, MultilineTextOptions, Node, PaddingBox, PhysicalTextAlign, Place, PushListItemsAnimationOptions, RenderFeedback, RendererOptions, ShrinkWrap, Text, TextAlign, TextEllipsisPosition, TextJustifyMode, TextJustifyOptions, TextOptions, TextOverflowMode, TextOverflowWrapMode, TextStyleOptions, TextWhiteSpaceMode, TextWordBreakMode, UnshiftListItemsAnimationOptions, UpdateListItemAnimationOptions, VirtualizedRenderer, Wrapper, memoRenderItem, memoRenderItemBy };
729
745
  //# sourceMappingURL=index.d.mts.map