Skip to content

Hover previews for node references #34

Closed

Description

@malibio

Implement interactive decorations for node references within TextNode content, including hover previews, visual indicators, and contextual actions built on precise character positioning.

Dependencies

Related Issues

Self-Contained Scope

  • Complete reference parsing and decoration system for multiple reference types
  • Interactive hover preview components with contextual information
  • Visual decoration overlays with precise positioning integration
  • Mock services for node/person/task lookups during independent development
  • Comprehensive testing with various reference types and interaction scenarios
  • Documentation and usage examples for reference syntax and behaviors

Implementation Approach

  1. Reference Detection: Parser for [[nodes]], @people, #tasks, external links
  2. Decoration Overlay: Positioned elements over textarea content using mock element coordinates
  3. Preview Components: Interactive cards showing contextual information for each reference type
  4. Mock Services: Independent data layer for reference lookups and preview content
  5. Testing Strategy: Interaction tests + visual positioning tests + performance benchmarks
  6. Demo/Documentation: Working examples of all reference types with preview functionality

Requirements

  • Reference Types: Support [[Node Title]], @person-name, #task-name, https://links
  • Visual Indicators: Distinct styling and icons for each reference type
  • Hover Previews: Rich preview cards with contextual information and quick actions
  • Precise Positioning: Decorations accurately positioned over reference text
  • Interactive Actions: Click behaviors for navigation, editing, and copying references
  • Performance: Decoration rendering < 100ms for smooth user experience

Technical Implementation Notes

Step 1: Reference Parser

// ReferenceParser.js
export const REFERENCE_PATTERNS = {
  nodeReference: /\[\[([^\]]+)\]\]/g,
  personReference: /@(\w+(-\w+)*|\[([^\]]+)\])/g,
  taskReference: /#(\w+(-\w+)*|\[([^\]]+)\])/g,
  externalLink: /(https?:\/\/[^\s]+)/g,
  markdownLink: /\[([^\]]+)\]\(([^)]+)\)/g
};

export function parseReferences(content) {
  const references = [];
  
  for (const [type, pattern] of Object.entries(REFERENCE_PATTERNS)) {
    let match;
    const regex = new RegExp(pattern);
    
    while ((match = regex.exec(content)) \!== null) {
      references.push({
        type,
        text: match[0],
        content: match[1] || match[0],
        startIndex: match.index,
        endIndex: match.index + match[0].length
      });
    }
  }
  
  return references.sort((a, b) => a.startIndex - b.startIndex);
}

Step 2: Decoration Overlay System

<\!-- DecorationOverlay.svelte -->
<script>
  import { parseReferences } from './ReferenceParser.js';
  import NodePreview from './previews/NodePreview.svelte';
  import PersonPreview from './previews/PersonPreview.svelte';
  import TaskPreview from './previews/TaskPreview.svelte';
  
  export let content = '';
  export let mockElement;
  export let textareaElement;
  
  let hoveredReference = null;
  let previewPosition = null;
  let hoverTimeout;
  
  $: references = parseReferences(content);
  $: decorations = references.map(ref => ({
    ...ref,
    position: getCharacterPosition(mockElement, ref.startIndex),
    endPosition: getCharacterPosition(mockElement, ref.endIndex - 1)
  }));
  
  function handleReferenceHover(reference, event) {
    clearTimeout(hoverTimeout);
    hoverTimeout = setTimeout(() => {
      hoveredReference = reference;
      previewPosition = { x: event.clientX, y: event.clientY };
    }, 500); // 0.5s delay like Logseq
  }
  
  function handleMouseLeave() {
    clearTimeout(hoverTimeout);
    hoveredReference = null;
    previewPosition = null;
  }
</script>

<div class="decoration-overlay">
  {#each decorations as decoration}
    <div 
      class="reference-decoration reference-{decoration.type}"
      style="
        left: {decoration.position.left}px;
        top: {decoration.position.top}px;
        width: {decoration.endPosition.left - decoration.position.left}px;
        height: {decoration.position.height}px;
      "
      on:mouseenter={e => handleReferenceHover(decoration, e)}
      on:mouseleave={handleMouseLeave}
      on:click={e => handleReferenceClick(decoration, e)}
    >
      <\!-- Visual decoration overlay -->
    </div>
  {/each}
  
  {#if hoveredReference && previewPosition}
    <div class="reference-preview" style="left: {previewPosition.x}px; top: {previewPosition.y}px;">
      {#if hoveredReference.type === 'nodeReference'}
        <NodePreview nodeTitle={hoveredReference.content} />
      {:else if hoveredReference.type === 'personReference'}
        <PersonPreview personId={hoveredReference.content} />
      {:else if hoveredReference.type === 'taskReference'}
        <TaskPreview taskId={hoveredReference.content} />
      {/if}
    </div>
  {/if}
</div>

Step 3: Preview Components with Mock Data

<\!-- previews/NodePreview.svelte -->
<script>
  import { mockNodeService } from '$lib/services/mockNodeService';
  
  export let nodeTitle;
  let nodeData = null;
  let loading = true;
  
  onMount(async () => {
    try {
      nodeData = await mockNodeService.findNodeByTitle(nodeTitle);
      loading = false;
    } catch (error) {
      loading = false;
    }
  });
  
  function handleOpenNode() {
    // Mock navigation - will integrate with real routing later
    console.log('Navigate to node:', nodeTitle);
  }
  
  function handleCopyReference() {
    navigator.clipboard.writeText(`[[${nodeTitle}]]`);
  }
</script>

<div class="node-preview-card">
  {#if loading}
    <div class="preview-loading">Loading...</div>
  {:else if nodeData}
    <div class="preview-header">
      <h4>{nodeData.title}</h4>
      <span class="node-type">{nodeData.type}</span>
    </div>
    <div class="preview-content">
      {nodeData.content.substring(0, 150)}{nodeData.content.length > 150 ? '...' : ''}
    </div>
    <div class="preview-actions">
      <button on:click={handleOpenNode}>Open</button>
      <button on:click={handleCopyReference}>Copy Ref</button>
    </div>
  {:else}
    <div class="preview-error">Node not found: {nodeTitle}</div>
  {/if}
</div>

File Modifications Required:

  • Create: ReferenceParser.js for syntax detection
  • Create: DecorationOverlay.svelte main component
  • Create: Preview components (NodePreview.svelte, PersonPreview.svelte, etc.)
  • Create: mockNodeService.js for independent development
  • Modify: BaseNode.svelte to include decoration overlay
  • Add: CSS styles for decorations and preview cards

Mock Services Pattern:

// mockNodeService.js - Enable independent development
export const mockNodeService = {
  async findNodeByTitle(title) {
    // Simulate network delay
    await new Promise(resolve => setTimeout(resolve, 100));
    
    // Mock node data
    return {
      title,
      type: 'text',
      content: `This is mock content for ${title}. It provides enough detail to show in preview cards during development.`,
      createdAt: new Date(),
      updatedAt: new Date()
    };
  }
};

Acceptance Criteria

  • All reference types properly detected ([[nodes]], @people, #tasks, links)
  • Visual decorations positioned accurately over reference text
  • Hover previews appear after 0.5s delay with relevant information
  • Click actions work correctly (open, copy, edit)
  • Visual indicators distinct for each reference type
  • No interference with textarea editing or cursor positioning
  • Performance impact minimal (< 100ms for decoration rendering)
  • Preview cards show useful contextual information
  • Mobile-friendly touch interactions
  • Accessibility support (keyboard navigation, screen readers)

Resources & References

  • Logseq Block References: Inspiration from hover preview research
  • Mock Element System: Issue Mock Element Positioning System for Precise Cursor Handling #33 positioning foundation
  • Component Patterns: Existing BaseNode and TextNode architecture
  • Design System: Current CSS tokens and styling patterns in /src/lib/design/
  • Service Patterns: Existing mock service implementations

Definition of Done

  • Reference parsing system implemented and tested
  • Decoration overlay component created with precise positioning
  • All preview components implemented with mock data integration
  • Visual styling follows design system patterns
  • All acceptance criteria met and verified
  • Unit tests >90% coverage for parsing and positioning logic
  • Interaction tests for hover and click behaviors
  • Performance benchmarks meet requirements (< 100ms)
  • Code follows project standards (ESLint, Prettier, TypeScript)
  • Documentation includes reference syntax guide and examples
  • Ready for integration with real node/person/task services

Activity

  1. changed the title [-]Rich Node Reference Decorations with Interactive Previews[/-] [+]Hover previews for node references[/+] on Aug 3, 2026
  2. malibio commented on Aug 3, 2026

    @malibio
    CollaboratorAuthor

    Retitled and re-scoped: most of this issue is built, and the remaining gap is hover previews specifically.

    Already built (the spec predates all of it):

    • Reference components — packages/desktop-app/src/lib/components/references/base-node-reference.svelte, registered at lib/plugins/core-plugins.ts:18
    • Reference styling incl. deleted-node states — packages/desktop-app/src/lib/styles/noderef.css:163-239
    • nodeReferenceSelected event plumbed through every node type from design/components/base-node.svelte:395-661
    • Decoration service — lib/services/base-node-decoration.ts
    • Autocomplete on @ and [[ — lib/components/autocomplete-modal.svelte:86,183-212, components/ui/node-autocomplete/node-autocomplete.svelte
    • Backlinks panel — design/components/backlinks-panel.svelte:70

    Still missing: hover previews. No preview component exists; the only hover treatment on references is CSS color/::after on deleted refs (noderef.css:177,216).

    Note the reference model differs from what this spec assumed: references are stored as markdown links ([text](nodespace://id), built at design/components/textarea-controller.svelte.ts:999), not as raw decorated tokens over a textarea. So a preview attaches to a rendered link in design/components/view-mode-renderer.svelte, and the elaborate decoration-overlay-with-mock-element-positioning approach in the original body is unnecessary — drop it.

    Also obsolete in the original spec: the #task hashtag reference type (only @, /, and [[ are triggers — lib/services/content-processor.ts:60), and the mock-service scaffolding, since the real data layer exists.

    The remaining work is: a hover preview card showing the target node title and a content snippet, with a delay, positioned against the link, dismissed on mouse-out — plus a keyboard-accessible equivalent.

  3. self-assigned this
    on Aug 11, 2026
  4. mstomar125 commented on Aug 11, 2026

    @mstomar125
    Contributor

    Resolved in #2024 (merged to main).

    Implemented the remaining scoped work — hover/focus preview cards for node references, showing the target node's title and a content snippet:

    • One document-level trigger in app-shell covers both reference render paths (the markdown-link form and the [[id]] wikilink form), since both resolve to the same a.ns-noderef anchor — mirroring the existing delegated click handler.
    • Shows after a short delay on hover or keyboard focus; dismissed on mouse-out, blur, Escape, or scroll. Cache-first resolution via SharedNodeStore.
    • Keyboard/screen-reader parity via aria-describedby linking the active reference to the card. Deleted/broken references are skipped. Card is pointer-events:none and viewport-clamped (flips above near the bottom edge).
    • Node-id parsing extracted to extractNodeIdFromHref and shared with the click handler.

    As noted in the re-scope, the original 2025 decoration-overlay / mock-service / #task machinery was intentionally dropped — the real model is rendered markdown links and the data layer already exists.

    A two-pass adversarial review caught a real positioning bug (card measured at loading size, could clip near the viewport bottom) which was fixed before merge. Full suite green (195 files / 4500 tests); lint + svelte-check clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions