| ... | @@ -73,7 +73,9 @@ import { | ... | @@ -73,7 +73,9 @@ import { |
| 73 | resolveTimelineIsAtEnd, | 73 | resolveTimelineIsAtEnd, |
| 74 | resolveTimelineMinimapHasPersistentGutter, | 74 | resolveTimelineMinimapHasPersistentGutter, |
| 75 | resolveTimelineMinimapHeightStyle, | 75 | resolveTimelineMinimapHeightStyle, |
| | 76 | resolveTimelineMinimapHitStripWidth, |
| 76 | resolveTimelineMinimapIndexFromPointer, | 77 | resolveTimelineMinimapIndexFromPointer, |
| | 78 | resolveTimelineMinimapInteractiveWidth, |
| 77 | resolveTimelineMinimapTopPercent, | 79 | resolveTimelineMinimapTopPercent, |
| 78 | type StableMessagesTimelineRowsState, | 80 | type StableMessagesTimelineRowsState, |
| 79 | type MessagesTimelineRow, | 81 | type MessagesTimelineRow, |
| ... | @@ -324,6 +326,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({ | ... | @@ -324,6 +326,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({ |
| 324 | null, | 326 | null, |
| 325 | ); | 327 | ); |
| 326 | const [minimapHasPersistentGutter, setMinimapHasPersistentGutter] = useState(false); | 328 | const [minimapHasPersistentGutter, setMinimapHasPersistentGutter] = useState(false); |
| | 329 | const [minimapHitStripWidth, setMinimapHitStripWidth] = useState(0); |
| 327 | const handleAnchorReady = useCallback( | 330 | const handleAnchorReady = useCallback( |
| 328 | (info: { anchorIndex: number | undefined }) => { | 331 | (info: { anchorIndex: number | undefined }) => { |
| 329 | if (anchorMessageId !== null && info.anchorIndex !== undefined) { | 332 | if (anchorMessageId !== null && info.anchorIndex !== undefined) { |
| ... | @@ -395,6 +398,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({ | ... | @@ -395,6 +398,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({ |
| 395 | setMinimapHasPersistentGutter((current) => | 398 | setMinimapHasPersistentGutter((current) => |
| 396 | current === nextHasPersistentGutter ? current : nextHasPersistentGutter, | 399 | current === nextHasPersistentGutter ? current : nextHasPersistentGutter, |
| 397 | ); | 400 | ); |
| | 401 | setMinimapHitStripWidth(resolveTimelineMinimapHitStripWidth(viewportWidth)); |
| 398 | }; | 402 | }; |
| 399 | | 403 | |
| 400 | const frame = requestAnimationFrame(measure); | 404 | const frame = requestAnimationFrame(measure); |
| ... | @@ -511,6 +515,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({ | ... | @@ -511,6 +515,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({ |
| 511 | items={minimapItems} | 515 | items={minimapItems} |
| 512 | bottomInset={contentInsetEndAdjustment} | 516 | bottomInset={contentInsetEndAdjustment} |
| 513 | hasPersistentGutter={minimapHasPersistentGutter} | 517 | hasPersistentGutter={minimapHasPersistentGutter} |
| | 518 | hitStripWidth={minimapHitStripWidth} |
| 514 | stripMap={minimapStripMap} | 519 | stripMap={minimapStripMap} |
| 515 | onSelect={(item) => { | 520 | onSelect={(item) => { |
| 516 | onManualNavigation(); | 521 | onManualNavigation(); |
| ... | @@ -605,15 +610,21 @@ function resolveTimelineRowHeight(state: TimelinePositionState, rowIndex: number | ... | @@ -605,15 +610,21 @@ function resolveTimelineRowHeight(state: TimelinePositionState, rowIndex: number |
| 605 | return typeof height === "number" && Number.isFinite(height) ? height : null; | 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 | function TimelineMinimap({ | 617 | function TimelineMinimap({ |
| 609 | bottomInset, | 618 | bottomInset, |
| 610 | hasPersistentGutter, | 619 | hasPersistentGutter, |
| | 620 | hitStripWidth, |
| 611 | items, | 621 | items, |
| 612 | stripMap, | 622 | stripMap, |
| 613 | onSelect, | 623 | onSelect, |
| 614 | }: { | 624 | }: { |
| 615 | bottomInset: number; | 625 | bottomInset: number; |
| 616 | hasPersistentGutter: boolean; | 626 | hasPersistentGutter: boolean; |
| | 627 | hitStripWidth: number; |
| 617 | items: ReadonlyArray<TimelineMinimapItem>; | 628 | items: ReadonlyArray<TimelineMinimapItem>; |
| 618 | stripMap: Map<string, HTMLSpanElement>; | 629 | stripMap: Map<string, HTMLSpanElement>; |
| 619 | onSelect: (item: TimelineMinimapItem) => void; | 630 | onSelect: (item: TimelineMinimapItem) => void; |
| ... | @@ -676,7 +687,7 @@ function TimelineMinimap({ | ... | @@ -676,7 +687,7 @@ function TimelineMinimap({ |
| 676 | return ( | 687 | return ( |
| 677 | <div | 688 | <div |
| 678 | className={cn( | 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 | hasPersistentGutter | 691 | hasPersistentGutter |
| 681 | ? "opacity-100" | 692 | ? "opacity-100" |
| 682 | : "opacity-0 transition-opacity duration-150 hover:opacity-100 focus-within:opacity-100", | 693 | : "opacity-0 transition-opacity duration-150 hover:opacity-100 focus-within:opacity-100", |
| ... | @@ -688,9 +699,17 @@ function TimelineMinimap({ | ... | @@ -688,9 +699,17 @@ function TimelineMinimap({ |
| 688 | <div className="relative h-full w-full select-none"> | 699 | <div className="relative h-full w-full select-none"> |
| 689 | <button | 700 | <button |
| 690 | aria-label={`Jump to message: ${activeItem?.userText ?? "User message"}`} | 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 | onBlur={() => setActiveIndex(null)} | 708 | onBlur={() => setActiveIndex(null)} |
| 693 | onClick={(event) => { | 709 | onClick={(event) => { |
| | 710 | if (timelineMinimapEventTargetsPreview(event.target)) { |
| | 711 | return; |
| | 712 | } |
| 694 | const nextIndex = resolveActiveIndexFromPointer(event); | 713 | const nextIndex = resolveActiveIndexFromPointer(event); |
| 695 | const nextItem = nextIndex === null ? null : (items[nextIndex] ?? null); | 714 | const nextItem = nextIndex === null ? null : (items[nextIndex] ?? null); |
| 696 | if (nextItem) { | 715 | if (nextItem) { |
| ... | @@ -722,9 +741,15 @@ function TimelineMinimap({ | ... | @@ -722,9 +741,15 @@ function TimelineMinimap({ |
| 722 | onMouseLeave={() => setActiveIndex(null)} | 741 | onMouseLeave={() => setActiveIndex(null)} |
| 723 | onMouseMove={updateActiveIndexFromPointer} | 742 | onMouseMove={updateActiveIndexFromPointer} |
| 724 | onMouseDown={(event) => { | 743 | onMouseDown={(event) => { |
| | 744 | if (timelineMinimapEventTargetsPreview(event.target)) { |
| | 745 | return; |
| | 746 | } |
| 725 | event.preventDefault(); | 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 | type="button" | 753 | type="button" |
| 729 | > | 754 | > |
| 730 | <div className="absolute top-0 left-3 h-full w-px bg-border/15" /> | 755 | <div className="absolute top-0 left-3 h-full w-px bg-border/15" /> |
| ... | @@ -761,27 +786,31 @@ function TimelineMinimap({ | ... | @@ -761,27 +786,31 @@ function TimelineMinimap({ |
| 761 | })} | 786 | })} |
| 762 | {activeItem ? ( | 787 | {activeItem ? ( |
| 763 | <span | 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 | style={{ | 792 | style={{ |
| 766 | top: `${activeTopPercent}%`, | 793 | top: `${activeTopPercent}%`, |
| 767 | transform: `translateY(${activeTooltipTranslate})`, | 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"> | 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"> |
| 771 | {activeItem.userText ?? "User message"} | 798 | <span className="block max-w-full overflow-hidden text-ellipsis whitespace-nowrap text-sm font-medium leading-5"> |
| 772 | </span> | 799 | {activeItem.userText ?? "User message"} |
| 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} | | |
| 783 | </span> | 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 | </span> | 814 | </span> |
| 786 | ) : null} | 815 | ) : null} |
| 787 | </button> | 816 | </button> |