Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | 533x 533x 533x 323x 266x 533x 533x 533x 79x 454x 169x 6x 6x 285x | /**
* Wrapper component for events in the EventStream that provides:
* - Hover action row (Copy + Select) in normal mode
* - Checkbox + full-row click target in selection mode
*
* Presentational component decoupled from useGrackle(). Performs clipboard
* side effects via navigator.clipboard when the user clicks Copy.
*/
import {
useState,
useCallback,
useEffect,
useRef,
type JSX,
type ReactNode,
type MouseEvent,
} from "react";
import { Clipboard, Check, CheckSquare } from "lucide-react";
import { ICON_SM } from "../../utils/iconSize.js";
import styles from "./EventHoverRow.module.scss";
/** Props for the EventHoverRow component. */
export interface EventHoverRowProps {
/** Text to copy when the hover Copy button is clicked. */
copyText: string;
/** Whether this event has copyable content (shows hover actions). */
isContentBearing: boolean;
/** Whether multi-select mode is active. */
isSelecting: boolean;
/** Whether this event is currently selected in multi-select mode. */
isSelected: boolean;
/** Accessible label for the selection checkbox (e.g. "Select message from assistant at 2:34 PM"). */
checkboxLabel?: string;
/** Called when the Select button in the hover row is clicked (enters selection mode). */
onSelect: () => void;
/** Called when the row is clicked in selection mode. Receives the shiftKey state. */
onToggle: (shiftKey: boolean) => void;
/** Called after a successful single-event copy from the hover row. */
onCopied?: () => void;
/** The event content to wrap. */
children: ReactNode;
}
/**
* Wraps an event in the EventStream with hover actions (normal mode) or
* selection affordances (multi-select mode).
*/
export function EventHoverRow({
copyText,
isContentBearing,
isSelecting,
isSelected,
checkboxLabel,
onSelect,
onToggle,
onCopied,
children,
}: EventHoverRowProps): JSX.Element {
const [copied, setCopied] = useState(false);
const copiedTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
// Clear the "copied" feedback timer on unmount
useEffect(() => {
return () => {
Iif (copiedTimerRef.current !== undefined) {
clearTimeout(copiedTimerRef.current);
}
};
}, []);
const handleCopy = useCallback(async () => {
try {
await navigator.clipboard.writeText(copyText);
setCopied(true);
onCopied?.();
Iif (copiedTimerRef.current !== undefined) {
clearTimeout(copiedTimerRef.current);
}
copiedTimerRef.current = setTimeout(() => {
setCopied(false);
}, 2000);
} catch {
// Clipboard write failed silently
}
}, [copyText, onCopied]);
const handleRowClick = useCallback(
(e: MouseEvent<HTMLDivElement>) => {
Iif (!isSelecting) {
return;
}
// Don't intercept if the user clicked inside an interactive element
const target = e.target as HTMLElement;
Iif (target.closest("a, button, input, textarea, select, [role=button]")) {
return;
}
e.preventDefault();
onToggle(e.shiftKey);
},
[isSelecting, onToggle],
);
// Non-content-bearing events: render plain, no interactivity
if (!isContentBearing) {
return <div className={styles.row}>{children}</div>;
}
// Selection mode: checkbox + clickable row
if (isSelecting) {
return (
<div
className={`${styles.row} ${styles.selectingRow} ${isSelected ? styles.selected : ""}`}
onClick={handleRowClick}
data-testid="event-selectable-row"
>
<div className={styles.checkboxArea}>
<input
type="checkbox"
checked={isSelected}
onChange={(e) => {
onToggle((e.nativeEvent as globalThis.MouseEvent).shiftKey);
}}
onClick={(e) => {
e.stopPropagation();
}}
className={styles.checkbox}
aria-label={checkboxLabel ?? "Select this event"}
data-testid="event-select-checkbox"
/>
</div>
<div className={styles.contentArea}>{children}</div>
</div>
);
}
// Normal mode: hover action row
return (
<div className={styles.row} data-testid="event-hover-row">
<div className={styles.hoverActions} data-testid="event-hover-actions">
<button
type="button"
className={styles.hoverButton}
onClick={() => {
handleCopy().catch(() => {});
}}
aria-label="Copy event content"
data-testid="event-hover-copy"
>
{copied ? (
<Check size={ICON_SM} aria-hidden="true" />
) : (
<Clipboard size={ICON_SM} aria-hidden="true" />
)}
</button>
<button
type="button"
className={styles.hoverButton}
onClick={onSelect}
aria-label="Select this event"
data-testid="event-hover-select"
>
<CheckSquare size={ICON_SM} aria-hidden="true" />
</button>
</div>
{children}
</div>
);
}
|