| ... | ... | @@ -73,7 +73,9 @@ import { |
| 73 | 73 | resolveTimelineIsAtEnd, |
| 74 | 74 | resolveTimelineMinimapHasPersistentGutter, |
| 75 | 75 | resolveTimelineMinimapHeightStyle, |
| 76 | resolveTimelineMinimapHitStripWidth, |
| 76 | 77 | resolveTimelineMinimapIndexFromPointer, |
| 78 | resolveTimelineMinimapInteractiveWidth, |
| 77 | 79 | resolveTimelineMinimapTopPercent, |
| 78 | 80 | type StableMessagesTimelineRowsState, |
| 79 | 81 | type MessagesTimelineRow, |
| ... | ... | @@ -324,6 +326,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({ |
| 324 | 326 | null, |
| 325 | 327 | ); |
| 326 | 328 | const [minimapHasPersistentGutter, setMinimapHasPersistentGutter] = useState(false); |
| 329 | const [minimapHitStripWidth, setMinimapHitStripWidth] = useState(0); |
| 327 | 330 | const handleAnchorReady = useCallback( |
| 328 | 331 | (info: { anchorIndex: number | undefined }) => { |
| 329 | 332 | if (anchorMessageId !== null && info.anchorIndex !== undefined) { |
| ... | ... | @@ -395,6 +398,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({ |
| 395 | 398 | setMinimapHasPersistentGutter((current) => |
| 396 | 399 | current === nextHasPersistentGutter ? current : nextHasPersistentGutter, |
| 397 | 400 | ); |
| 401 | setMinimapHitStripWidth(resolveTimelineMinimapHitStripWidth(viewportWidth)); |
| 398 | 402 | }; |
| 399 | 403 | |
| 400 | 404 | const frame = requestAnimationFrame(measure); |
| ... | ... | @@ -511,6 +515,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({ |
| 511 | 515 | items={minimapItems} |
| 512 | 516 | bottomInset={contentInsetEndAdjustment} |
| 513 | 517 | hasPersistentGutter={minimapHasPersistentGutter} |
| 518 | hitStripWidth={minimapHitStripWidth} |
| 514 | 519 | stripMap={minimapStripMap} |
| 515 | 520 | onSelect={(item) => { |
| 516 | 521 | onManualNavigation(); |
| ... | ... | @@ -605,15 +610,21 @@ function resolveTimelineRowHeight(state: TimelinePositionState, rowIndex: number |
| 605 | 610 | return typeof height === "number" && Number.isFinite(height) ? height : null; |
| 606 | 611 | } |
| 607 | 612 | |
| 613 | function timelineMinimapEventTargetsPreview(target: EventTarget): boolean { |
| 614 | return target instanceof Element && target.closest("[data-minimap-preview]") !== null; |
| 615 | } |
| 616 | |
| 608 | 617 | function TimelineMinimap({ |
| 609 | 618 | bottomInset, |
| 610 | 619 | hasPersistentGutter, |
| 620 | hitStripWidth, |
| 611 | 621 | items, |
| 612 | 622 | stripMap, |
| 613 | 623 | onSelect, |
| 614 | 624 | }: { |
| 615 | 625 | bottomInset: number; |
| 616 | 626 | hasPersistentGutter: boolean; |
| 627 | hitStripWidth: number; |
| 617 | 628 | items: ReadonlyArray<TimelineMinimapItem>; |
| 618 | 629 | stripMap: Map<string, HTMLSpanElement>; |
| 619 | 630 | onSelect: (item: TimelineMinimapItem) => void; |
| ... | ... | @@ -676,7 +687,7 @@ function TimelineMinimap({ |
| 676 | 687 | return ( |
| 677 | 688 | <div |
| 678 | 689 | className={cn( |
| 679 | | "group/minimap pointer-events-auto absolute top-0 left-0 z-40 hidden w-18 [@media(pointer:fine)]:block", |
| 690 | "group/minimap pointer-events-none absolute top-0 left-0 z-40 hidden w-18 [@media(pointer:fine)]:block", |
| 680 | 691 | hasPersistentGutter |
| 681 | 692 | ? "opacity-100" |
| 682 | 693 | : "opacity-0 transition-opacity duration-150 hover:opacity-100 focus-within:opacity-100", |
| ... | ... | @@ -688,9 +699,17 @@ function TimelineMinimap({ |
| 688 | 699 | <div className="relative h-full w-full select-none"> |
| 689 | 700 | <button |
| 690 | 701 | aria-label={`Jump to message: ${activeItem?.userText ?? "User message"}`} |
| 691 | | className="pointer-events-auto absolute top-1/2 left-3 w-10 -translate-y-1/2 cursor-pointer bg-transparent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/70" |
| 702 | className={cn( |
| 703 | "absolute top-1/2 left-3 -translate-y-1/2 cursor-pointer bg-transparent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/70", |
| 704 | // The strip is width-capped to the side gutter so it never overlays |
| 705 | // the centered content column; with no usable gutter it goes inert. |
| 706 | hitStripWidth > 0 ? "pointer-events-auto" : "pointer-events-none", |
| 707 | )} |
| 692 | 708 | onBlur={() => setActiveIndex(null)} |
| 693 | 709 | onClick={(event) => { |
| 710 | if (timelineMinimapEventTargetsPreview(event.target)) { |
| 711 | return; |
| 712 | } |
| 694 | 713 | const nextIndex = resolveActiveIndexFromPointer(event); |
| 695 | 714 | const nextItem = nextIndex === null ? null : (items[nextIndex] ?? null); |
| 696 | 715 | if (nextItem) { |
| ... | ... | @@ -722,9 +741,15 @@ function TimelineMinimap({ |
| 722 | 741 | onMouseLeave={() => setActiveIndex(null)} |
| 723 | 742 | onMouseMove={updateActiveIndexFromPointer} |
| 724 | 743 | onMouseDown={(event) => { |
| 744 | if (timelineMinimapEventTargetsPreview(event.target)) { |
| 745 | return; |
| 746 | } |
| 725 | 747 | event.preventDefault(); |
| 726 | 748 | }} |
| 727 | | style={{ height: resolveTimelineMinimapHeightStyle(items.length) }} |
| 749 | style={{ |
| 750 | height: resolveTimelineMinimapHeightStyle(items.length), |
| 751 | width: resolveTimelineMinimapInteractiveWidth(hitStripWidth, activeItem !== null), |
| 752 | }} |
| 728 | 753 | type="button" |
| 729 | 754 | > |
| 730 | 755 | <div className="absolute top-0 left-3 h-full w-px bg-border/15" /> |
| ... | ... | @@ -761,27 +786,31 @@ function TimelineMinimap({ |
| 761 | 786 | })} |
| 762 | 787 | {activeItem ? ( |
| 763 | 788 | <span |
| 764 | | className="pointer-events-none absolute left-8 w-80 rounded-xl border border-border/70 bg-popover/95 p-3 text-left text-popover-foreground shadow-xl shadow-black/25 backdrop-blur" |
| 789 | className="pointer-events-auto absolute left-8 w-80 cursor-text select-text" |
| 790 | data-minimap-preview |
| 791 | onMouseMove={(event) => event.stopPropagation()} |
| 765 | 792 | style={{ |
| 766 | 793 | top: `${activeTopPercent}%`, |
| 767 | 794 | transform: `translateY(${activeTooltipTranslate})`, |
| 768 | 795 | }} |
| 769 | 796 | > |
| 770 | | <span className="block max-w-full overflow-hidden text-ellipsis whitespace-nowrap text-sm font-medium leading-5"> |
| 771 | | {activeItem.userText ?? "User message"} |
| 772 | | </span> |
| 773 | | {activeItem.assistantText ? ( |
| 774 | | <span |
| 775 | | className="mt-1 max-h-[3.75rem] overflow-hidden text-muted-foreground text-sm leading-5" |
| 776 | | style={{ |
| 777 | | display: "-webkit-box", |
| 778 | | WebkitBoxOrient: "vertical", |
| 779 | | WebkitLineClamp: 3, |
| 780 | | }} |
| 781 | | > |
| 782 | | {activeItem.assistantText} |
| 797 | <span className="block rounded-xl border border-border/70 bg-popover/95 p-3 text-left text-popover-foreground shadow-xl shadow-black/25 backdrop-blur"> |
| 798 | <span className="block max-w-full overflow-hidden text-ellipsis whitespace-nowrap text-sm font-medium leading-5"> |
| 799 | {activeItem.userText ?? "User message"} |
| 783 | 800 | </span> |
| 784 | | ) : null} |
| 801 | {activeItem.assistantText ? ( |
| 802 | <span |
| 803 | className="mt-1 max-h-[3.75rem] overflow-hidden text-muted-foreground text-sm leading-5" |
| 804 | style={{ |
| 805 | display: "-webkit-box", |
| 806 | WebkitBoxOrient: "vertical", |
| 807 | WebkitLineClamp: 3, |
| 808 | }} |
| 809 | > |
| 810 | {activeItem.assistantText} |
| 811 | </span> |
| 812 | ) : null} |
| 813 | </span> |
| 785 | 814 | </span> |
| 786 | 815 | ) : null} |
| 787 | 816 | </button> |