Skip to content

feat: Add relaxed interaction mode and fix citation indicators#145

Merged
bensonwong merged 10 commits intomainfrom
9595-add-option-to-be
Feb 2, 2026
Merged

feat: Add relaxed interaction mode and fix citation indicators#145
bensonwong merged 10 commits intomainfrom
9595-add-option-to-be

Conversation

@bensonwong
Copy link
Collaborator

@bensonwong bensonwong commented Feb 2, 2026

Summary

  • Add interactionMode prop to CitationComponent with two modes:
    • eager (default): Hover shows popover immediately, click opens image/expands details
    • relaxed: Hover only applies style effects (no popover), click opens popover first, then second click zooms image
  • Fix X icon for not-found states: Use XCircleIcon (circle with X) instead of text character, centered instead of subscript positioning
  • Fix URL citation click behavior: Clicking now always opens the URL in a new tab (external link icon on hover is a visual hint)
  • Fix document citation header: Always show anchor text next to status icon in miss/partial states (consistent with URL citations)

Changes

New interactionMode Prop

Use relaxed mode when citations are densely packed and hover popovers would be distracting:

<CitationComponent
  citation={citation}
  verification={verification}
  interactionMode="relaxed"
/>
Mode Hover Click (no popover) Click (popover open)
eager (default) Shows popover Zooms image Zooms image
relaxed Style effects only Opens popover Zooms image

Icon Fixes

  • MissIndicator now uses XCircleIcon SVG instead of text character
  • Error states in UrlCitationComponent use XCircleIcon with centered alignment
  • STATUS_ICONS uses symbol instead of "404" text for error states

URL Citation Behavior

  • Clicking anywhere on the URL citation opens the URL in a new tab
  • External link icon appears on hover as a visual hint
  • openUrlOnClick prop is now deprecated (clicking always opens URL)

Cursor Updates

  • In eager mode with image: cursor-zoom-in
  • In relaxed mode without popover: cursor-pointer
  • In relaxed mode with popover + image: cursor-zoom-in

Test plan

  • Verify interactionMode="eager" shows popover on hover
  • Verify interactionMode="relaxed" only shows style hover effects, click opens popover
  • Verify miss indicator shows red X in circle (not text character)
  • Verify URL citation click opens URL in new tab
  • Verify document citation header shows anchor text for miss/partial states

## Summary of Changes

### 1. Fixed X icon for URL not found indicator (UrlCitationComponent.tsx)
- Changed import from `CloseIcon` to `XCircleIcon` for error states
- Updated `renderStatusIndicator()` to use `XCircleIcon` with centered alignment (not subscript) for error states
- Changed `STATUS_ICONS` to use `⊗` symbol instead of "404" text for all error states

### 2. Fixed X icon for not found indicator (CitationComponent.tsx)
- Updated `MissIndicator` component to use `XCircleIcon` instead of a text character `✕`
- Changed positioning from subscript (`top-[0.15em]`) to centered (`items-center`)

### 3. Added `interactionMode` prop to CitationComponent
- Added new type `CitationInteractionMode` = `"eager" | "relaxed"` in types.ts with documentation
- Added `interactionMode` prop to `CitationComponentProps` with documentation
- Updated `handleMouseEnter` to not show popover on hover when in `relaxed` mode
- Updated `handleClick` to open popover on first click in `relaxed` mode (instead of zooming image)

### 4. Updated mouse pointer for relaxed mode
- In `eager` mode with image: `cursor-zoom-in` (click zooms)
- In `relaxed` mode without popover open: `cursor-pointer` (click opens popover)
- In `relaxed` mode with popover open + image: `cursor-zoom-in` (click zooms)

### 5. Exported new types
- Exported `CitationInteractionMode` from types.ts, CitationComponent.tsx, and react/index.ts
- Exported `XCircleIcon` from react/index.ts

### Interaction Behavior Summary:

| Mode | Hover | Click (no popover) | Click (popover open) |
|------|-------|-------------------|---------------------|
| `eager` (default) | Shows popover | Zooms image | Zooms image |
| `relaxed` | Style effects only | Opens popover | Zooms image |
## Summary of All Changes

### 1. Fixed X icon for "not found" indicator
- **CitationComponent.tsx**: Updated `MissIndicator` to use `XCircleIcon` (circle with X) instead of `✕` text character, and centered it instead of subscript positioning
- **UrlCitationComponent.tsx**: Updated error indicator to use `XCircleIcon` with centered alignment
- **UrlCitationComponent.tsx**: Changed `STATUS_ICONS` to use `⊗` symbol for all error states instead of "404"

### 2. Added `interactionMode` prop to CitationComponent
- Added new type `CitationInteractionMode` = `"eager" | "relaxed"`
- **`eager` (default)**: Hover shows popover, click opens image/expands details
- **`relaxed`**: Hover only applies style effects (no popover), click opens popover first, then second click zooms image
- Updated cursor to show `cursor-pointer` in relaxed mode when popover isn't open yet

### 3. Fixed UrlCitationComponent click behavior
- **Before**: Clicking didn't open the URL unless `openUrlOnClick` was true
- **After**: Clicking always opens the URL in a new tab (hover reveals external link icon as visual hint)
- Deprecated `openUrlOnClick` prop since clicking now always opens the URL
- Updated `showExternalLinkOnHover` to default to `true`
…the miss/partial-without-image state will always show the anchor text next to the icon, just like the URL citations do.

The change removes the conditional that was hiding the anchor text when a humanizing message existed. Now:
- The header shows: `[X icon] "anchor text"`
- Below it, the humanizing message provides additional context: "We couldn't find 'anchor text' in this document."

This provides a consistent experience between URL and document citations.
@bensonwong bensonwong changed the title add option to be less eager with citationcomponent interactions (vibe-kanban) feat: Add relaxed interaction mode and fix citation indicators Feb 2, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Feb 2, 2026

✅ Playwright Test Report

Status: Tests passed

📊 Download Report & Snapshots (see Artifacts section)

What's in the Visual Snapshots

The gallery includes visual snapshots for:

  • 🖥️ Desktop showcase (all variants × all states)
  • 📱 Mobile showcase (iPhone SE viewport)
  • 📟 Tablet showcase (iPad viewport)
  • 🔍 Popover states (verified, partial, not found)
  • 🔗 URL citation variants

Run ID: 21583341862

@claude
Copy link

claude bot commented Feb 2, 2026

Code Review - PR #145

Thank you for this PR! The implementation looks solid overall. Here are my findings:

✅ Positive Aspects

  1. Well-documented changes: The new interactionMode prop has excellent JSDoc documentation explaining the difference between 'eager' and 'relaxed' modes with clear examples.

  2. Consistent visual language: Using XCircleIcon (X in circle) for error states across both CitationComponent and UrlCitationComponent creates better visual consistency.

  3. Thoughtful UX design: The cursor changes (cursor-pointercursor-zoom-in) provide clear affordance about what will happen on click.

  4. Backwards compatible: The new prop defaults to "eager", maintaining existing behavior.

🐛 Potential Issues

1. Deprecated prop not handled (UrlCitationComponent.tsx:217)

The openUrlOnClick prop is marked as @deprecated but the code still destructures it with an underscore prefix. This should either:

  • Be completely removed from the interface, OR
  • Show a console warning when used to help developers migrate
// Current (line 208):
openUrlOnClick: _openUrlOnClick, // Deprecated, clicking always opens URL now

// Suggestion: Add deprecation warning
const openUrlOnClick = _openUrlOnClick;
if (openUrlOnClick !== undefined && process.env.NODE_ENV !== 'production') {
  console.warn('openUrlOnClick is deprecated. URL citations now always open on click.');
}

2. MissIndicator centering inconsistency

The PR description mentions centering the not-found indicator, and the code uses inline-flex items-center (line 733), but the other indicators (VerifiedIndicator, PartialIndicator, PendingIndicator) still use subscript positioning with top-[0.15em].

This creates visual inconsistency where the red X appears centered while green checks appear as subscripts. Consider:

  • Option A: Keep them all consistent (all subscript or all centered)
  • Option B: Document why not-found deserves different positioning

Current:

// Line 733 - MissIndicator: centered
className="inline-flex items-center ml-0.5 size-2.5..."

// Line 706 - PartialIndicator: subscript
className="inline-flex relative ml-0.5 top-[0.15em] size-2.5..."

3. Incomplete humanizing message logic change (CitationComponent.tsx:1407)

The comment was updated to say "Humanizing message provides additional context below the header" and the anchorText is no longer conditionally hidden when humanizingMessage exists. However, the comment removal suggests this was intentional to avoid redundancy.

Question: Does this cause the anchor text to appear twice when a humanizing message exists? Please verify this doesn't create duplicate text displays.

Before:

anchorText={humanizingMessage ? undefined : anchorText}
// Comment: "When humanizingMessage exists, skip anchorText in header to avoid redundancy"

After:

anchorText={anchorText}
// Comment: "Humanizing message provides additional context below the header"

🎯 Best Practices

Type Safety

Good use of the discriminated union type for CitationInteractionMode. Consider adding it to the main exports since it's part of the public API.

Accessibility

The XCircleIcon is properly marked with aria-hidden="true" since the parent component conveys status. Good practice!

🧪 Testing Concerns

Missing test coverage: No test files found for React components. Consider adding tests for:

  1. interactionMode="eager" behavior (default)
  2. interactionMode="relaxed" behavior
  3. Cursor class switching logic
  4. XCircleIcon rendering in error states

📊 Performance

No concerns - the new logic uses existing hooks properly and doesn't introduce unnecessary re-renders.

🔒 Security

No concerns identified.

📝 Documentation

The CLAUDE.md should be updated to document the new interactionMode prop with examples showing when to use "relaxed" vs "eager".

Suggested addition:

#### Interaction Modes

Control how eagerly citations respond to interactions:

```tsx
// Eager (default): hover shows popover, click opens image
<CitationComponent citation={citation} verification={verification} />

// Relaxed: hover only styles, click shows popover
<CitationComponent 
  citation={citation} 
  verification={verification} 
  interactionMode="relaxed" 
/>

Use relaxed mode when citations are densely packed and hover popovers would be distracting.


### 🎨 Minor Style Nits

Line 733: Consider extracting the repeated size/margin classes into a constant since all indicators share similar styling.

---

**Overall Assessment**: 🟢 Approve with minor suggestions

The core implementation is solid and achieves the stated goals. The main concerns are around consistency (centering vs subscript positioning) and clarifying the humanizing message behavior. These can be addressed in follow-up commits or marked as non-blocking.

Nice work on the thoughtful UX improvements!

The type was being exported twice from react/index.ts:
- Once from types.js (line 25)
- Once from CitationComponent.js (line 89)

This caused a TypeScript build error. Keeping only the export from types.js.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@claude
Copy link

claude bot commented Feb 2, 2026

Code Review

Summary

This PR introduces a new interactionMode prop with eager and relaxed modes, fixes icon consistency issues, and improves URL citation behavior. Overall, the changes are well-structured and align with the project's goals. Below are my findings organized by category.


✅ Strengths

  1. Well-documented API: The new CitationInteractionMode type has excellent inline documentation with clear examples and a comparison table.

  2. Backward compatibility: Default behavior is preserved (eager mode is default), so existing implementations won't break.

  3. Consistent icon usage: Switching from text characters () to XCircleIcon SVG improves visual consistency across the component library.

  4. Logical cursor management: The cursor class logic properly reflects the interaction state (pointer vs zoom-in) based on mode and popover state.

  5. Sensible URL behavior: Making URL citations always open links on click (with visual hints) is more intuitive than the previous openUrlOnClick prop.


🔍 Code Quality Issues

1. Potential Race Condition in Click Handler

// Line 1797-1800
if (isRelaxedMode && !isHovering) {
  setIsHovering(true);
  return;
}

Issue: In relaxed mode, the first click sets isHovering to true but immediately returns. The second click (when isHovering is true) proceeds to open the image. However, if a user clicks rapidly, there could be timing issues where isHovering state hasn't updated yet.

Recommendation: Consider using a ref to track click count or adding a small debounce to ensure state updates are processed.

2. Inconsistent Error Icon Usage

// UrlCitationComponent.tsx line 61-67
error_timeout: { icon: "⊗", label: "Timed out", className: "text-red-500 dark:text-red-400" },
error_not_found: { icon: "⊗", label: "Not found", className: "text-red-500 dark:text-red-400" },

Issue: The STATUS_ICONS object still uses the text character, but the render function below (line 380) uses XCircleIcon. This creates confusion - the icon field in the object is never used for error states.

Recommendation: Either:

  • Remove the unused icon field for error states, OR
  • Update to icon: "XCircle" to indicate it should use the component

3. Deprecated Prop Not Removed

// Line 208
openUrlOnClick: _openUrlOnClick, // Deprecated, clicking always opens URL now

Issue: The deprecated prop is accepted but ignored. This could lead to confusion for developers who update the package expecting the prop to still work.

Recommendation: Consider removing the prop in the next major version, or log a deprecation warning when it's used:

if (_openUrlOnClick !== undefined) {
  console.warn('openUrlOnClick prop is deprecated and will be removed in v2.0');
}

4. Missing Dependency Array Item

// Line 1812-1824
[
  behaviorConfig,
  eventHandlers,
  citation,
  citationKey,
  verification?.verificationImageBase64,
  isMiss,
  isMobile,
  isRelaxedMode,
  isHovering,  // ⚠️ This is state, so including it is correct
  getBehaviorContext,
  applyBehaviorActions,
],

Concern: Including isHovering in the dependency array means the callback is recreated whenever hover state changes. This is technically correct for the logic, but could lead to unnecessary re-renders. Consider if this is optimal.


🎯 Design & UX Considerations

5. Double-Click Requirement in Relaxed Mode

The relaxed mode requires users to click once to see the popover, then click again to zoom the image. For users accustomed to single-click interactions, this might feel unintuitive.

Suggestion: Consider adding a visual hint (e.g., "Click to zoom" text in the popover footer) when in relaxed mode to guide users.

6. Mobile vs Desktop Parity

The PR description states that relaxed mode mimics mobile behavior on desktop. However, there's a subtle difference:

  • Mobile: Uses touch events (wasPopoverOpenBeforeTap ref)
  • Relaxed desktop: Uses isHovering state

Question: Should these use the same mechanism for consistency? The different implementations could lead to subtle behavioral differences.


🔒 Security & Performance

7. No Security Concerns

The changes don't introduce any security vulnerabilities. The external link opening uses proper noopener,noreferrer attributes.

8. Performance Impact

The additional conditional checks in click/hover handlers are minimal and won't cause noticeable performance issues. The cursor class calculation is memoized through the props object.


🧪 Testing Recommendations

The PR includes a test plan checklist but no automated tests. Consider adding:

  1. Unit tests for interaction modes:

    describe('CitationComponent interactionMode', () => {
      it('shows popover on hover in eager mode', () => { ... })
      it('does not show popover on hover in relaxed mode', () => { ... })
      it('shows popover on first click in relaxed mode', () => { ... })
      it('zooms image on second click in relaxed mode', () => { ... })
    })
  2. Visual regression tests: Since icon rendering changed, screenshot tests would catch any visual regressions.

  3. Accessibility tests: Ensure keyboard navigation still works correctly with the new interaction modes.


📝 Documentation Gaps

9. CLAUDE.md Updates Needed

The CLAUDE.md file should be updated to document the new interactionMode prop with usage examples:

// For dense citation layouts
<CitationComponent
  citation={citation}
  verification={verification}
  interactionMode="relaxed"
/>

10. Migration Guide

Consider adding a migration note for the deprecated openUrlOnClick prop to help users update their code.


🎨 Minor Style Suggestions

11. Comment Clarity

// Line 1795-1796
// Relaxed mode: first click shows popover, second click opens image
// (similar to mobile behavior but for desktop)

Suggestion: Expand this comment to explain why relaxed mode exists:

// Relaxed mode: Reduces hover popover noise in dense citation layouts.
// First click shows popover, second click opens image (similar to mobile UX).

📊 Overall Assessment

Category Rating Notes
Code Quality 8/10 Well-structured, minor issues noted above
Documentation 7/10 Good inline docs, but missing examples in CLAUDE.md
Testing 5/10 Manual test plan provided, but no automated tests
Security 10/10 No concerns
Performance 9/10 Minimal overhead
UX Design 8/10 Thoughtful feature, could use better discoverability

✅ Recommended Actions Before Merge

  1. High Priority:

  2. Medium Priority:

  3. Future Work:

    • Add automated tests for interaction modes
    • Plan removal of deprecated props for v2.0
    • Add visual regression tests

🎉 Conclusion

This is a solid PR that adds a useful feature for handling dense citation layouts. The code is well-written and maintains backward compatibility. With the minor issues addressed, this will be a valuable addition to the library.

Recommendation: Approve with minor changes requested (issues #2 and #3).

Updated tests to reflect that clicking URL citations now opens
the URL by default. The openUrlOnClick prop is deprecated.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@claude
Copy link

claude bot commented Feb 2, 2026

Code Review for PR #145: Add relaxed interaction mode and fix citation indicators

Summary

This PR introduces a new interactionMode prop to control hover/click behavior, fixes icon rendering for not-found states, and improves URL citation click behavior. Overall, the changes are well-implemented and improve UX flexibility.

✅ Strengths

  1. Clear API Design: The interactionMode prop with "eager" and "relaxed" options is intuitive and well-documented
  2. Comprehensive Testing: Tests were updated to reflect the new URL click behavior
  3. Consistent Icon Usage: Replacing text characters with proper SVG icons (XCircleIcon) improves visual consistency
  4. Good Documentation: PR description clearly explains the behavior differences in table format
  5. Type Safety: Proper TypeScript types exported (CitationInteractionMode)

🔍 Code Quality Observations

Good Practices

  • Proper use of useCallback with correct dependencies (lines 1812-1824 in CitationComponent.tsx)
  • Accessibility maintained with aria-hidden on decorative icons
  • Clean separation of concerns between eager and relaxed modes

Minor Issues

  1. Deprecated Prop Handling (UrlCitationComponent.tsx:217)

    openUrlOnClick: _openUrlOnClick, // Deprecated, clicking always opens URL now
    • The deprecated prop is renamed to _openUrlOnClick (with underscore) but never used
    • Consider adding a console warning in development mode to help users migrate:
    if (process.env.NODE_ENV === 'development' && _openUrlOnClick !== undefined) {
      console.warn('DeepCitation: openUrlOnClick prop is deprecated. Clicking now always opens URLs.');
    }
  2. Cursor Class Logic (CitationComponent.tsx:2204-2210)

    • The cursor class logic is correct but could be more readable:
    const cursorClass = isRelaxedMode
      ? isHovering && hasImage
        ? "cursor-zoom-in"
        : "cursor-pointer"
      : hasImage
        ? "cursor-zoom-in"
        : "cursor-pointer";
    • Consider extracting to a helper function or adding a comment explaining the decision tree
  3. Duplicate Comment (CitationComponent.tsx:1407)

    // Humanizing message provides additional context below the header
    • This comment is helpful, but there's an orphaned comment on line 1400 that was partially updated
    • The old comment logic about "skip anchorText when humanizingMessage exists" should be completely removed

🐛 Potential Issues

  1. Icon Sizing Consistency (CitationComponent.tsx:732-733)

    className="inline-flex items-center ml-0.5 size-2.5 text-red-500 dark:text-red-400"
    • Using size-2.5 (10px) for the XCircleIcon wrapper, but the icon renders at 100% width/height
    • At this small size, the circle + X may be hard to see on some displays
    • Consider testing visibility at different zoom levels and screen densities
  2. State Management Race Condition (CitationComponent.tsx:1795-1800)

    if (isRelaxedMode && !isHovering) {
      setIsHovering(true);
      return;
    }
    • In relaxed mode, clicking when !isHovering sets hovering to true
    • However, if user clicks very quickly twice, the second click might fire before isHovering state updates
    • Consider using a ref to track click count for more reliable "first click vs second click" detection

🎯 Suggestions

  1. Add JSDoc for interactionMode in CitationComponentProps (already good in types.ts, but would be helpful at the prop level too)

  2. Consider Mobile Parity: The PR description mentions mobile behavior is the same for both modes. Should relaxed mode potentially have different mobile behavior? Current implementation seems reasonable, but worth documenting the design decision.

  3. Test Coverage: Consider adding tests for:

    • Relaxed mode hover not showing popover
    • Relaxed mode first click opening popover
    • Cursor class changes based on mode and state
    • Icon rendering for all error states

🔒 Security

  • ✅ No security concerns
  • Proper use of noopener,noreferrer in window.open() calls

⚡ Performance

  • ✅ No performance concerns
  • Proper use of memoization and callbacks

📋 Test Coverage

  • ✅ Tests updated to match new behavior
  • ⚠️ Consider adding explicit tests for interactionMode="relaxed" behavior

Overall Assessment

LGTM with minor suggestions. The code is well-written, properly typed, and the feature is useful. The minor issues mentioned above are not blockers and can be addressed in follow-up work if desired.

Recommended next steps:

  1. Add deprecation warning for openUrlOnClick prop
  2. Add test cases specifically for relaxed mode
  3. Verify icon visibility at 10px size on various displays

Great work on maintaining consistency across document and URL citations! 🎉

…components.md

- Document eager vs relaxed interaction modes for CitationComponent
- Add props reference for interactionMode
- Add notes about UrlCitationComponent click behavior and XCircleIcon usage

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@github-actions
Copy link
Contributor

github-actions bot commented Feb 2, 2026

📚 Documentation Preview

The documentation has been built successfully.

To preview locally:

  1. Download the github-pages artifact from this workflow run
  2. Extract and serve with any static file server:
    cd artifact && python -m http.server 8000
  3. Open http://localhost:8000/deepcitation-js/

Workflow run: 21583341831

@claude
Copy link

claude bot commented Feb 2, 2026

Code Review: PR #145 - Add Relaxed Interaction Mode and Fix Citation Indicators

Overview

This PR introduces a new interactionMode prop for controlling citation interaction behavior and fixes several UI inconsistencies with indicators. The changes are well-structured and align with the package's design principles.


✅ Strengths

  1. Clear API Design: The interactionMode prop with eager/relaxed values is intuitive and well-documented. The mode names clearly convey their behavior.

  2. Comprehensive Documentation: Excellent updates to CLAUDE.md, AGENTS.md, and docs/components.md with clear tables explaining behavior differences. The "Best For" column is particularly helpful.

  3. Consistent Status Indicators: Using XCircleIcon instead of text characters (, "404") across all error states creates a more professional and consistent UI. The switch from subscript to centered alignment makes the not-found state more prominent.

  4. Test Coverage: Updated tests in UrlCitationComponent.test.tsx reflect the new default behavior (clicking always opens URLs), maintaining test quality.

  5. Type Safety: Proper TypeScript types exported for CitationInteractionMode with clear JSDoc comments.

  6. Backward Compatibility: The openUrlOnClick prop is properly deprecated without breaking existing implementations.


🔍 Issues & Suggestions

1. Missing Test Coverage for New interactionMode Feature

The new interactionMode prop is not tested. This is a significant behavioral change that should have dedicated tests.

Recommendation: Add tests to verify:

  • Relaxed mode doesn't show popover on hover
  • Relaxed mode shows popover on first click
  • Relaxed mode opens image on second click (when popover is already open)
  • Cursor classes change correctly based on mode and state
  • Event handlers fire in the correct sequence for both modes

Suggested test location: Add to existing CitationComponentBehavior.test.tsx or create new file


2. Potential State Management Issue in Relaxed Mode

In CitationComponent.tsx line ~1796, the relaxed mode logic checks !isHovering before setting isHovering to true.

Issue: If the user hovers over the citation in relaxed mode (which doesn't set isHovering), then clicks, isHovering will be false. But if they hover away and click again while the cursor is still over the element, isHovering might still be true from the first click, causing unexpected behavior.

Recommendation: Consider tracking popover state separately from hover state in relaxed mode, or add a comment explaining the expected state flow.


3. Accessibility Concern: External Link Icon

The ExternalLinkButton has proper keyboard navigation, but the visual hint only appears on hover, not on keyboard focus for the external link icon itself.

Recommendation: Ensure screen reader users understand that clicking the citation will open a URL. Consider adding aria-label="Open {domain} in new tab" to the main button.


4. Documentation: Cursor Behavior Could Be Clearer

In CLAUDE.md:363-364, the cursor behavior explanation could be more precise.

Suggestion: Add that eager mode still requires an image to show zoom cursor, e.g.:

  • Eager mode: cursor-zoom-in when image is available, otherwise cursor-pointer

5. Type Safety: XCircleIcon Import

In UrlCitationComponent.tsx, XCircleIcon is imported from ./icons.js. Verify that this icon is properly exported from that file.

Recommendation: Verify the icon exists or add it if missing.


6. Potential Edge Case: Double-Click in Relaxed Mode

If a user double-clicks in relaxed mode:

  1. First click: open popover
  2. Second click (immediate): open image
  3. Both actions happen rapidly

Recommendation: Consider debouncing or adding a guard to prevent double-click race conditions, or add a test to verify expected behavior.


7. Comment Removal Could Use Context

In CitationComponent.tsx:1407, two comments were removed explaining why anchorText was conditionally shown. The new code always shows anchorText, but context for future maintainers is missing.

Recommendation: Add a brief comment explaining why anchorText is now always shown, or reference the PR/issue.


🎯 Recommendations Summary

Before merging:

  1. ✅ Add comprehensive tests for interactionMode behavior
  2. ⚠️ Review hover/click state management in relaxed mode for edge cases
  3. ⚠️ Verify XCircleIcon exists in icons.js

Nice to have:
4. 📝 Improve accessibility labels for URL citations
5. 📝 Clarify cursor behavior documentation
6. 🧪 Test double-click behavior in relaxed mode
7. 💬 Add context comment for removed conditional logic


📊 Overall Assessment

Code Quality: ⭐⭐⭐⭐ (4/5)
Documentation: ⭐⭐⭐⭐⭐ (5/5)
Test Coverage: ⭐⭐⭐ (3/5) - Missing tests for new feature
Breaking Changes: ✅ None (backward compatible)

This is a solid feature addition with excellent documentation. The main concern is the lack of test coverage for the new interactionMode prop. Once tests are added and the XCircleIcon import is verified, this will be ready to merge.

Great work on the consistent UX improvements! 🚀

bensonwong and others added 2 commits February 2, 2026 15:40
Tests cover:
- Eager mode: hover shows popover, click opens image, cursor classes
- Relaxed mode: hover only styles (no popover), first click shows popover,
  second click opens image, cursor class transitions
- Integration with behaviorConfig and eventHandlers
- Edge cases: no image available, cursor reset after closing overlay

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Changed popover detection from [data-popover] to [data-state="open"]
which is the correct Radix UI selector for open popover content.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@claude
Copy link

claude bot commented Feb 2, 2026

Code Review for PR #145

Summary

This PR adds a new interactionMode prop to CitationComponent with two modes (eager/relaxed), fixes citation indicators to use XCircleIcon, and updates URL citation click behavior. The changes are well-implemented with comprehensive test coverage and excellent documentation.


✅ Strengths

1. Excellent Design & UX Considerations

  • The interactionMode prop addresses a real UX problem: dense citations causing popover clutter
  • The two-mode approach (eager/relaxed) is intuitive and well-named
  • Cursor changes (cursor-pointercursor-zoom-in) provide clear visual feedback
  • Mobile and desktop behaviors are well-differentiated

2. Strong Test Coverage

  • 372 new lines of comprehensive tests in CitationComponentBehavior.test.tsx
  • Tests cover all interaction modes, edge cases, and integration with existing features
  • Tests verify cursor classes, hover behavior, click sequences, and behavioral config interactions

3. Excellent Documentation

  • Updated CLAUDE.md, AGENTS.md, and docs/components.md with clear examples
  • Comparison tables make it easy to understand the differences between modes
  • JSDoc comments are thorough and include usage recommendations

4. Consistent Visual Language

  • Switching from text to XCircleIcon provides better consistency
  • The circle-X icon is more recognizable and professional
  • Proper centering (not subscript) improves visual hierarchy

5. Backward Compatibility

  • The interactionMode prop defaults to "eager" (existing behavior)
  • The openUrlOnClick prop is deprecated gracefully
  • No breaking changes to existing API

🔍 Issues & Suggestions

1. Potential State Inconsistency in Relaxed Mode (Priority: High)

Location: CitationComponent.tsx:1802-1808

Issue: In relaxed mode, clicking opens the popover by setting isHovering=true, but if the user moves their mouse away, the handleMouseLeave timer (150ms) will close the popover. This means the user has a very short window to click again to zoom the image.

Suggestion: Add a state flag like isPopoverPinnedByClick to distinguish between hover-opened and click-opened popovers. This would prevent the popover from closing on mouse leave when it was opened via click.

2. Missing Accessibility Consideration (Priority: Medium)

Location: Keyboard navigation in relaxed mode

Issue: The interaction mode logic is only applied to mouse clicks. Keyboard users will trigger handleClick the same way, but there's no clear indication that the first keypress opens a popover vs. zooming.

Suggestion: Consider adding aria-expanded attributes to communicate popover state to screen reader users.

3. Deprecated Prop Handling (Priority: Medium)

Location: UrlCitationComponent.tsx:814

Issue: The deprecated openUrlOnClick prop is silently ignored. This could confuse users who explicitly set openUrlOnClick={false} expecting it to prevent URL opening.

Suggestion: Add a development warning when the prop is used.

4. Type Safety Enhancement (Priority: Low)

Location: types.ts:932-934

The CitationInteractionMode type could benefit from const assertion for better autocomplete and iteration in tests.

5. Performance Issue (Priority: Low)

Location: CitationComponent.tsx:2211-2217

The cursor class computation runs on every render. Consider memoizing with useMemo.


🛡️ Security Considerations

✅ No security concerns identified

  • URL opening uses window.open with noopener,noreferrer
  • No user input is directly rendered without sanitization
  • External links properly isolated

⚡ Performance Considerations

Minor cursor class calculation could be optimized, but overall performance impact is negligible.


🧪 Test Coverage

✅ Excellent test coverage with all major scenarios covered. Suggestion: Add one test case for issue #1 (popover closing on mouse leave after click-to-open).


📚 Documentation Quality

✅ Excellent with clear examples, comparison tables, and thorough JSDoc comments.


✅ Approval

This PR is well-designed, thoroughly tested, and excellently documented. The core functionality works as intended, and the identified issues are relatively minor refinements that could be addressed in follow-up PRs.

Recommendation: Approve with suggestions for follow-up improvements.

The new interactionMode feature will be valuable for users with dense citation layouts, and the visual consistency improvements enhance the overall component quality.

Great work! 🎉

Removed waitFor that was causing timeout in CI. When no image is
available, the test just verifies that clicking doesn't open any
dialog and cursor stays as pointer.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@claude
Copy link

claude bot commented Feb 2, 2026

PR Review: Add relaxed interaction mode and fix citation indicators

Overall, this is a well-executed PR with thoughtful UX improvements, comprehensive testing, and excellent documentation. The code quality is high, and the changes are backward compatible. Here's my detailed review:


✅ Strengths

1. Excellent Test Coverage

  • Added 393 lines of comprehensive tests in CitationComponentBehavior.test.tsx
  • Tests cover all interaction modes (eager, relaxed), edge cases, and integration with behaviorConfig
  • Good use of Radix UI's [data-state="open"] selector for verifying popover state
  • Tests verify cursor class changes dynamically

2. Backward Compatibility

  • interactionMode defaults to "eager" (existing behavior)
  • openUrlOnClick prop properly deprecated without breaking changes
  • All existing functionality preserved

3. Documentation

  • Comprehensive updates to CLAUDE.md, AGENTS.md, and docs/components.md
  • Clear comparison tables showing when to use each mode
  • Excellent inline JSDoc comments explaining the new prop

4. UX Improvements

  • Icon fix (XCircleIcon vs text character) is a clear visual improvement
  • Relaxed mode addresses a real UX problem (dense citation layouts)
  • Cursor feedback is intuitive and matches user expectations

🔍 Code Quality Observations

Strong Points

  • Clean separation of concerns in handleClick (src/react/CitationComponent.tsx:1802-1808)
  • Proper dependency array management in useCallback hooks
  • Good state management for tracking hover/popover state

Minor Concerns

1. Cursor Logic Complexity (src/react/CitationComponent.tsx:2169-2180)

The cursor class calculation uses nested ternary operators which can be hard to read:

const cursorClass = isRelaxedMode
  ? isHovering && hasImage
    ? "cursor-zoom-in"
    : "cursor-pointer"
  : hasImage
    ? "cursor-zoom-in"
    : "cursor-pointer";

Suggestion: Consider extracting to a helper function for clarity:

const getCursorClass = () => {
  if (isRelaxedMode) {
    return isHovering && hasImage ? "cursor-zoom-in" : "cursor-pointer";
  }
  return hasImage ? "cursor-zoom-in" : "cursor-pointer";
};
const cursorClass = getCursorClass();

2. Popover State Reset (src/react/CitationComponent.tsx:1359)

When closing the image overlay in relaxed mode, the comment says "popover closed when overlay closes", but I don't see explicit code resetting isHovering state when the overlay closes. This might cause the cursor to not reset properly.

Verification needed: Does the overlay close handler properly reset the popover state? The test at line 357-383 seems to expect cursor reset, but verify the implementation handles this.

3. Potential Race Condition (src/react/CitationComponent.tsx:1847-1852)

In handleMouseEnter, the relaxed mode check happens after cancelHoverCloseTimeout(). If there's a pending close timeout, canceling it and then NOT showing the popover could lead to unexpected state.

cancelHoverCloseTimeout();
// In relaxed mode, don't show popover on hover (only style hover effects)
if (!isRelaxedMode) {
  setIsHovering(true);
}

This is probably fine since you'd only have a pending timeout if the popover was already open, but worth considering edge cases.


🚨 Issues Found

1. Deprecated Prop Still Functional (src/react/UrlCitationComponent.tsx:835)

The openUrlOnClick prop is marked deprecated but still used in code logic (assigned to _openUrlOnClick). While ignoring it is correct, the variable name suggests it might have been used previously.

Recommendation: Remove the parameter entirely in the next major version, or add a console warning if it's provided:

if (_openUrlOnClick !== undefined) {
  console.warn('UrlCitationComponent: openUrlOnClick is deprecated. Clicking always opens the URL.');
}

2. Documentation Comment Mismatch (src/react/CitationComponent.tsx:1697)

The comment removed at line 1386 said:

"When humanizingMessage exists, skip anchorText in header to avoid redundancy"

But the new behavior always shows anchor text (line 1406). While this is intentional and improves consistency, the commit message at line 1698 says:

"Humanizing message provides additional context below the header"

Potential issue: For very long anchor texts, showing both the anchor text in the header AND in the humanizing message might create visual clutter. Consider truncating long anchor texts in the header when a humanizing message exists.

3. Missing Type Export Check (src/react/index.ts:918)

The duplicate export issue was fixed in commit ae7b8e3, but it's worth double-checking that CitationInteractionMode is only exported once and in the right place. The current exports look correct.


🔐 Security Considerations

✅ No security concerns identified. The changes are purely UI/UX focused and don't introduce any new attack vectors.


🎯 Performance Considerations

  • The cursor class calculation runs on every render but is lightweight
  • Test timeout values (150ms hover delay) are reasonable and match component constants
  • No unnecessary re-renders observed from the changes

📊 Test Coverage Analysis

Well-Covered:

  • ✅ Eager mode behavior (hover, click, cursor)
  • ✅ Relaxed mode behavior (hover, first click, second click, cursor transitions)
  • ✅ Integration with behaviorConfig and eventHandlers
  • ✅ Edge cases (no image, cursor reset after closing overlay)
  • ✅ Mobile/touch behavior unchanged

Potential Gaps:

  1. Missing test for rapid mode switching: What happens if interactionMode prop changes while popover is open?
  2. Accessibility: No tests for keyboard navigation in relaxed mode (Tab + Enter behavior)
  3. URL citation + relaxed mode: Tests focus on document citations with interactionMode, but UrlCitationComponent doesn't support it. Should it?

🎨 UX/Design Feedback

Icon Change (✕ → ⊗)

The change from a text character to XCircleIcon is excellent. The centered alignment makes the error state more prominent. One minor suggestion:

Consider: Adding a subtle animation (scale or fade-in) when the error indicator appears to draw attention to verification failures.

Relaxed Mode Cursor Feedback

The cursor transitions (pointerzoom-in) are intuitive. Great attention to detail!

External Link Icon Always Showing

Having the external link icon show on hover (even though click always opens URL) is good UX. It provides visual affordance without requiring a separate click target.


📝 Documentation Review

CLAUDE.md Changes (lines 29-64)

✅ Excellent comparison table and clear use cases
✅ Cursor behavior well-documented

AGENTS.md Changes (lines 9-16)

✅ Concise and helpful for AI agents
✅ Mentions centered vs subscript for icons (good detail)

docs/components.md Changes (lines 76-112)

✅ Thorough with usage examples
✅ Mobile behavior note is helpful

Suggestion: Add a visual example or screenshot showing the difference between eager and relaxed modes in the docs site.


🔧 Suggestions for Follow-up

  1. Extract cursor logic to helper function for better readability
  2. Add console.warn for deprecated openUrlOnClick usage
  3. Verify overlay close resets popover state in relaxed mode
  4. Consider animation for error indicator appearance
  5. Add test for interactionMode prop change mid-interaction
  6. Document whether interactionMode should apply to URL citations (design decision needed)

✅ Approval Recommendation

LGTM with minor suggestions. This PR is ready to merge. The issues identified are minor and can be addressed in follow-up PRs if desired. The core functionality is solid, well-tested, and properly documented.

Key Metrics

  • Lines Changed: +593/-58
  • Test Coverage: Comprehensive (393 new test lines)
  • Breaking Changes: None
  • Documentation: Excellent

Great work! 🎉

@claude
Copy link

claude bot commented Feb 2, 2026

PR Review: Add relaxed interaction mode and fix citation indicators

Overall, this is a well-implemented feature with excellent test coverage and documentation.

Strengths

  1. Excellent Documentation - CLAUDE.md, AGENTS.md, and docs/components.md all updated comprehensively
  2. Comprehensive Test Coverage - 394 lines of new tests covering edge cases
  3. Backward Compatibility - Default behavior unchanged, no breaking changes
  4. Type Safety - New CitationInteractionMode type exported properly
  5. Icon Consistency - XCircleIcon used consistently for error states

Code Quality Observations

Minor Issues

1. Potential State Race Condition (CitationComponent.tsx:1804-1807)
On rapid double-clicks in relaxed mode, the second click might not see updated isHovering state due to React batching. Impact is low but consider using a ref for immediate state access if needed.

2. Cursor Class Logic Readability (CitationComponent.tsx:2174-2186)
Nested ternary is functional but dense - could benefit from helper function extraction.

3. UrlCitationComponent Behavior Change (UrlCitationComponent.tsx:264-266)
Component now always opens URLs on click, even with eventHandlers.onClick. Might be unexpected for consumers wanting to intercept clicks.

Security, Performance, Test Coverage

✅ Security: No concerns - noopener,noreferrer correctly used
✅ Performance: No issues - useCallback dependencies correct
✅ Tests: Comprehensive coverage of all modes and edge cases

Final Assessment

Overall: Approved ✅

This is production-ready code. The identified issues are minor and can be addressed in follow-up PRs if needed.

Great work!

…nent

The openUrlOnClick prop was never used by users and has been completely
removed rather than kept for backwards compatibility. Clicking a URL
citation now always opens the URL in a new tab.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@claude
Copy link

claude bot commented Feb 2, 2026

PR Review: Add relaxed interaction mode and fix citation indicators

Summary

This PR introduces a relaxed interaction mode for citations and improves the visual consistency of error indicators. Overall, the implementation is solid and well-tested, with comprehensive test coverage and good documentation updates.

✅ Strengths

1. Excellent Test Coverage

  • 394 lines of new tests in CitationComponentBehavior.test.tsx
  • Covers all interaction modes thoroughly (eager, relaxed, with/without images)
  • Tests edge cases like cursor state changes, custom behavior overrides, and mobile interactions
  • Tests are clear and well-structured

2. Good Documentation

  • CLAUDE.md, AGENTS.md, and docs/components.md all updated consistently
  • Clear usage examples and comparison tables
  • Well-explained use cases for each mode

3. Clean API Design

  • The interactionMode prop is intuitive and self-documenting
  • Default behavior (eager) maintains backward compatibility
  • The two-mode approach is simple enough to understand but powerful enough to cover main use cases

4. Consistent Icon Updates

  • Replacing text characters (, 404) with proper SVG icons (XCircleIcon) improves visual consistency
  • The new XCircleIcon component follows the existing icon pattern perfectly
  • Centering instead of subscript positioning makes error states more prominent

🔍 Issues & Concerns

1. URL Citation Click Behavior Change (Breaking)

The PR changes UrlCitationComponent to always open URLs on click, removing the openUrlOnClick prop. This is a breaking change for any users who were relying on the default behavior of openUrlOnClick=false.

Location: src/react/UrlCitationComponent.tsx:257-267

// Before: Conditional based on openUrlOnClick prop
if (openUrlOnClick) {
  window.open(url, "_blank", "noopener,noreferrer");
}

// After: Always opens URL
window.open(url, "_blank", "noopener,noreferrer");

Impact:

  • Users who integrated URL citations expecting to handle clicks themselves will see their functionality break
  • The external link icon now always appears (was conditional before)

Recommendation:

  • This should be called out as a BREAKING CHANGE in the PR description or release notes
  • Consider deprecating the prop with a console warning first, then removing in a major version bump
  • Or keep the prop for backward compatibility with deprecation notice

2. Potential State Management Bug in Relaxed Mode

When the image overlay closes in relaxed mode, the popover state may not properly reset.

Location: src/react/CitationComponent.tsx:1796-1809

The logic assumes that clicking the overlay will close the popover, but I don't see explicit code to reset isHovering when the overlay closes. This could leave the component in a state where the cursor is cursor-zoom-in even though the popover isn't actually showing.

Test that might reveal this:

// The test at line 358-383 checks this but may not catch timing issues
it("resets to cursor-pointer after closing image overlay", () => {
  // ... opens image, then clicks overlay to close
  // Should verify isHovering state, not just cursor class
});

Recommendation:

  • Add a useEffect that resets isHovering when expandedImageSrc becomes null
  • Or ensure the overlay close handler explicitly calls setIsHovering(false)

3. Documentation Inconsistency

The documentation shows different cursor behaviors in different places:

In CLAUDE.md (lines 62-63):

- **Eager mode**: `cursor-zoom-in` (indicates click will zoom)
- **Relaxed mode**: `cursor-pointer` initially, then `cursor-zoom-in` when popover is open

In docs/components.md (line 110):

The cursor changes based on mode and state: `cursor-zoom-in` when clicking will zoom the image, `cursor-pointer` otherwise.

The second description is more general but loses the nuance about relaxed mode's state-dependent cursor.

Recommendation: Keep the detailed explanation from CLAUDE.md or unify both.

💡 Minor Suggestions

4. Status Icons Text Alternative

In UrlCitationComponent, the STATUS_ICONS object was changed to use for all error states instead of distinct icons like (timeout), (network). While this simplifies the visual language, it loses information.

Location: src/react/UrlCitationComponent.tsx:819-826

Trade-off: Consistency vs. Information Density

  • ✅ Pro: Cleaner, consistent error indicator
  • ❌ Con: User can't distinguish error types at a glance

Recommendation: This is acceptable if the error type is shown on hover/in the popover. Verify that errorMessage is displayed prominently in the tooltip.

5. Missing Type Export

The new CitationInteractionMode type is exported from src/react/types.ts and src/react/CitationComponent.tsx, but double-check it's properly exported from the main package entry point.

Location to verify: src/index.ts or package exports field

6. Test Timing Dependencies

The tests use hardcoded delays like HOVER_CLOSE_DELAY_MS + 50. This can lead to flaky tests in CI environments with variable performance.

Location: src/__tests__/CitationComponentBehavior.test.tsx:167

Recommendation: Consider using waitFor with a condition instead of fixed timeouts where possible, or use Jest's fake timers (jest.useFakeTimers()).

🔐 Security Review

✅ No security concerns identified. The changes are UI/interaction-focused with no new external inputs or XSS vectors.

🎨 Code Quality

  • Style: Consistent with existing codebase
  • Readability: Well-commented, especially the cursor logic sections
  • Maintainability: Good separation of concerns, state changes are localized

📊 Performance Considerations

✅ No performance concerns. The additional state checks (isRelaxedMode) are simple boolean checks with negligible overhead.

Final Recommendation

Approve with minor revisions:

  1. Critical: Document the breaking change for UrlCitationComponent (always opens URL now)
  2. Important: Verify popover state resets properly when image overlay closes in relaxed mode
  3. Nice to have: Unify documentation wording for cursor behavior

The feature itself is well-designed and thoroughly tested. With the breaking change properly communicated, this is ready to merge.


Reviewed by Claude Code 🤖

@bensonwong bensonwong merged commit d17d9bf into main Feb 2, 2026
6 checks passed
@bensonwong bensonwong deleted the 9595-add-option-to-be branch February 2, 2026 08:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant