mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 02:44:50 +01:00
fix(ui): align a delivery notice to its own comment's body edge
Two consecutive comments can each carry a delivery obligation. A full-thread -width banner under a right-aligned human bubble left ownership ambiguous, so the notice now tracks the message body edge on desktop while keeping the full thread width on mobile, and the thread's hash handling recognizes the feedback-delivery anchor so an attention deep link scrolls it into view. Browser-verified at 1440x900 and 390x844, light and dark: - no horizontal scroll at either width (scrollWidth == clientWidth) - mobile actions 308x44, desktop inline 36px - deep link focuses the banner heading inside the banner at both widths - spinner animation resolves to none under prefers-reduced-motion - contrast AA: light 4.70-4.74, dark 7.66-9.68 (baseline comment timestamp 4.74) - retry button focus-visible ring matches the app-wide 3px ring Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
b930ecf415
commit
6d210a995c
2 files changed
+35
-6
No files matched your search
@@ -43,10 +43,26 @@ export function isIssueCommentFeedbackDelivery(
|
||||
}
|
||||
|
||||
/** Anchor id for deep-linking the attention queue straight at the banner. */
|
||||
export const FEEDBACK_DELIVERY_ANCHOR_PREFIX = "feedback-delivery-";
|
||||
|
||||
export function feedbackDeliveryAnchorId(commentId: string) {
|
||||
return `feedback-delivery-${commentId}`;
|
||||
return `${FEEDBACK_DELIVERY_ANCHOR_PREFIX}${commentId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delay before claiming focus for a deep-linked banner. The thread's own
|
||||
* hash handling makes up to three scroll attempts over ~250ms; focusing inside
|
||||
* that window loses the focus to the thread viewport.
|
||||
*/
|
||||
const DEEP_LINK_FOCUS_SETTLE_MS = 350;
|
||||
|
||||
/**
|
||||
* Desktop constrains the notice to the same edge as the message body it belongs
|
||||
* to, so ownership is unambiguous when consecutive comments each carry one.
|
||||
* Mobile keeps the full thread width inside the thread's own padding.
|
||||
*/
|
||||
const FEEDBACK_DELIVERY_NOTICE_WIDTH = "w-full sm:max-w-(--pct-85)";
|
||||
|
||||
function shortRunId(runId: string) {
|
||||
return runId.slice(0, 8);
|
||||
}
|
||||
@@ -235,13 +251,18 @@ export function FeedbackDeliveryNotice({
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
if (window.location.hash !== `#${anchorId}`) return;
|
||||
headingRef.current?.focus();
|
||||
const timer = setTimeout(() => headingRef.current?.focus(), DEEP_LINK_FOCUS_SETTLE_MS);
|
||||
return () => clearTimeout(timer);
|
||||
}, [anchorId]);
|
||||
|
||||
if (delivery.state === "recovered_after_attention") {
|
||||
const deliveredAt = delivery.deliveredAt ? deliveredTimeLabel(delivery.deliveredAt) : null;
|
||||
return (
|
||||
<div id={anchorId} data-testid="feedback-delivery-recovered" className={className}>
|
||||
<div
|
||||
id={anchorId}
|
||||
data-testid="feedback-delivery-recovered"
|
||||
className={cn(FEEDBACK_DELIVERY_NOTICE_WIDTH, className)}
|
||||
>
|
||||
<SystemNotice
|
||||
tone="success"
|
||||
label="Feedback delivered"
|
||||
@@ -270,7 +291,11 @@ export function FeedbackDeliveryNotice({
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div id={anchorId} data-testid="feedback-delivery-exhausted" className={className}>
|
||||
<div
|
||||
id={anchorId}
|
||||
data-testid="feedback-delivery-exhausted"
|
||||
className={cn(FEEDBACK_DELIVERY_NOTICE_WIDTH, className)}
|
||||
>
|
||||
{/* Announced once on transition, not on every rerender of the banner. */}
|
||||
<span role="alert" className="sr-only">
|
||||
Feedback wasn't delivered. Paperclip stopped retrying.
|
||||
|
||||
@@ -186,6 +186,7 @@ import {
|
||||
type RecoveryResolveOutcome,
|
||||
} from "./IssueRecoveryActionCard";
|
||||
import {
|
||||
FEEDBACK_DELIVERY_ANCHOR_PREFIX,
|
||||
FeedbackDeliveryFooterStatus,
|
||||
FeedbackDeliveryNotice,
|
||||
isIssueCommentFeedbackDelivery,
|
||||
@@ -1682,7 +1683,7 @@ function IssueChatUserMessage({
|
||||
</div>
|
||||
{feedbackDelivery ? (
|
||||
<FeedbackDeliveryNotice
|
||||
className="mt-2"
|
||||
className={cn("mt-2", isCurrentUser && "sm:ml-auto")}
|
||||
delivery={feedbackDelivery}
|
||||
onRetry={onRetryFeedbackDelivery}
|
||||
retryPending={retryingFeedbackDeliveryCommentId === feedbackDelivery.sourceCommentId}
|
||||
@@ -4742,7 +4743,10 @@ export function IssueChatThread({
|
||||
const isThreadHash = hash.startsWith("#comment-")
|
||||
|| hash.startsWith("#activity-")
|
||||
|| hash.startsWith("#run-")
|
||||
|| hash.startsWith("#interaction-");
|
||||
|| hash.startsWith("#interaction-")
|
||||
// The attention queue deep-links an exhausted feedback delivery straight
|
||||
// at its banner, which lives inside the source comment's anchored block.
|
||||
|| hash.startsWith(`#${FEEDBACK_DELIVERY_ANCHOR_PREFIX}`);
|
||||
if (messages.length === 0) return;
|
||||
if (!isThreadHash) {
|
||||
if (!didInitialHashScrollDecisionRef.current) {
|
||||
|
||||
Reference in new issue
Block a user