From c98bf45d831a8c645ddcd30291edac75b6d2248f Mon Sep 17 00:00:00 2001 From: Ronald Roy Date: Wed, 21 Jan 2026 06:13:56 -0800 Subject: [PATCH 1/2] feat: Add snapshot hover preview in Pair Writing toolbar Shows truncated snapshot content (500 chars or 15 lines) when hovering over the Snapshot button, giving users a quick view of what they captured without needing to compare. Co-Authored-By: Claude Opus 4.5 --- frontend/src/components/PairWritingMode.css | 40 +++++++++ frontend/src/components/PairWritingMode.tsx | 1 + .../src/components/PairWritingToolbar.tsx | 89 ++++++++++++++----- .../__tests__/PairWritingToolbar.test.tsx | 83 +++++++++++++++++ 4 files changed, 191 insertions(+), 22 deletions(-) diff --git a/frontend/src/components/PairWritingMode.css b/frontend/src/components/PairWritingMode.css index 70964e33..46e8d0d1 100644 --- a/frontend/src/components/PairWritingMode.css +++ b/frontend/src/components/PairWritingMode.css @@ -33,6 +33,8 @@ backdrop-filter: blur(var(--glass-blur)); min-height: 48px; gap: var(--spacing-md); + position: relative; + z-index: 11; /* Above content panes and (+) for snapshot preview popover. */ } @supports not (backdrop-filter: blur(10px)) { @@ -124,6 +126,44 @@ background: var(--color-accent-primary-a15); } +/* Snapshot button wrapper for hover preview positioning */ +.pair-writing-toolbar__snapshot-wrapper { + position: relative; +} + +/* Snapshot preview popover */ +.pair-writing-toolbar__snapshot-preview { + position: absolute; + top: 100%; + right: 0; + margin-top: var(--spacing-xs); + min-width: 300px; + max-width: 500px; + max-height: 300px; + overflow-y: auto; + background: var(--color-surface); + border: 1px solid var(--glass-border); + border-radius: var(--radius-md); + box-shadow: var(--shadow-lg); + z-index: 100; + padding: var(--spacing-sm); +} + +.pair-writing-toolbar__snapshot-preview-content { + margin: 0; + font-family: var(--font-mono); + font-size: var(--text-xs); + line-height: 1.5; + color: var(--color-text); + white-space: pre-wrap; + word-break: break-word; +} + +.pair-writing-toolbar__snapshot-preview-ellipsis { + color: var(--color-text-secondary); + font-style: italic; +} + /* Save button */ .pair-writing-toolbar__btn--save { background: var(--gradient-primary); diff --git a/frontend/src/components/PairWritingMode.tsx b/frontend/src/components/PairWritingMode.tsx index f02f6f52..c953f995 100644 --- a/frontend/src/components/PairWritingMode.tsx +++ b/frontend/src/components/PairWritingMode.tsx @@ -227,6 +227,7 @@ export function PairWritingMode({ onSave={handleSave} onExit={handleExitClick} filePath={filePath} + snapshotContent={state.snapshot ?? undefined} /> {/* Split-screen content (REQ-F-11, TD-6) */} diff --git a/frontend/src/components/PairWritingToolbar.tsx b/frontend/src/components/PairWritingToolbar.tsx index 81710b11..6f5a49ae 100644 --- a/frontend/src/components/PairWritingToolbar.tsx +++ b/frontend/src/components/PairWritingToolbar.tsx @@ -7,7 +7,7 @@ * @see .sdd/specs/memory-loop/2026-01-20-pair-writing-mode.md REQ-F-14, REQ-F-23, REQ-F-29, REQ-F-30 */ -import React from "react"; +import React, { useState } from "react"; import "./PairWritingMode.css"; export interface PairWritingToolbarProps { @@ -25,6 +25,8 @@ export interface PairWritingToolbarProps { onExit: () => void; /** Current file path being edited (displayed in toolbar) */ filePath?: string; + /** The actual snapshot text content (for hover preview) */ + snapshotContent?: string; } /** @@ -37,6 +39,29 @@ export interface PairWritingToolbarProps { * * Note: Exit confirmation dialog is handled by the parent PairWritingMode component. */ +/** + * Truncates snapshot content for preview display. + * Limits to ~500 characters or ~15 lines, whichever is shorter. + */ +function truncateForPreview(content: string): { text: string; isTruncated: boolean } { + const MAX_CHARS = 500; + const MAX_LINES = 15; + + const lines = content.split("\n"); + let result = ""; + let lineCount = 0; + + for (const line of lines) { + if (lineCount >= MAX_LINES || result.length + line.length > MAX_CHARS) { + return { text: result.trimEnd(), isTruncated: true }; + } + result += (lineCount > 0 ? "\n" : "") + line; + lineCount++; + } + + return { text: result, isTruncated: false }; +} + export function PairWritingToolbar({ hasUnsavedChanges, hasSnapshot, @@ -45,7 +70,9 @@ export function PairWritingToolbar({ onSave, onExit, filePath, + snapshotContent, }: PairWritingToolbarProps): React.ReactNode { + const [isHoveringSnapshot, setIsHoveringSnapshot] = useState(false); return (
{/* Left section: file info */} @@ -64,29 +91,47 @@ export function PairWritingToolbar({ {/* Right section: action buttons */}
- {/* Snapshot button (REQ-F-23) */} - + + Snapshot + + {/* Snapshot preview popover */} + {hasSnapshot && isHoveringSnapshot && snapshotContent && (() => { + const { text, isTruncated } = truncateForPreview(snapshotContent); + return ( +
+
+                  {text}
+                  {isTruncated && ...}
+                
+
+ ); + })()} +
{/* Save button (REQ-F-29) */}