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 +22 -18
- package/example/chat.ts +44 -11
- package/example/test.ts +4 -2
- package/index.d.mts +44 -22
- package/index.mjs +517 -290
- package/index.mjs.map +1 -1
- package/package.json +4 -1
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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,
|
|
123
|
-
justifyLastLine: false,
|
|
124
|
-
justifyGapThreshold: 2.0,
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
|
|
238
|
-
if (index < 0) {
|
|
245
|
+
if (!list.items.includes(this.item)) {
|
|
239
246
|
return true;
|
|
240
247
|
}
|
|
241
|
-
const nextItem =
|
|
248
|
+
const nextItem =
|
|
249
|
+
this.item.kind === "revoked"
|
|
250
|
+
? this.item.original
|
|
251
|
+
: revokeMessage(this.item);
|
|
242
252
|
currentHover = nextItem;
|
|
243
|
-
list.
|
|
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: () =>
|
|
270
|
-
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
51
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
658
|
+
render(feedback?: RenderFeedback): boolean;
|
|
640
659
|
/** Hit-tests the current visible window. */
|
|
641
|
-
|
|
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
|
|
655
|
-
protected _renderVisibleWindow(window: VisibleWindow<VirtualizedResolvedItem
|
|
656
|
-
protected
|
|
657
|
-
|
|
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,
|
|
662
|
-
value: VirtualizedResolvedItem
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|