npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ayodev/commenting-widget

v0.2.1

Published

Embeddable commenting widget for annotating page elements

Downloads

462

Readme

Ayo Commenting Widget

An embeddable commenting widget that allows users to annotate any div element on a webpage. Built specifically for React/Next.js applications with robust element identification that survives re-renders and route changes.

Features

  • 🎯 Smart element selection - Normalizes clicks to stable semantic containers
  • 🔍 Robust element resolution - Multi-fingerprint matching (selector + heading + text + position)
  • 💬 Floating bubble to view all comments
  • 💭 Reply to comments - Threaded conversation support
  • ✏️ Edit comments - Modify your existing comments inline
  • Mark as done - Toggle comments completed/in-progress
  • 📌 Visual indicators on commented elements (optional, toggleable)
  • 🖱️ Hover to view comments - Hover over indicators to see comments in a tooltip
  • ⌨️ Keyboard shortcuts - Cmd+Shift+C (Mac) or Ctrl+Shift+C (Windows) to toggle selection mode
  • 💬 Inline comment input - Add and edit comments directly in the side panel
  • 👤 @ mentions - Tag people in comments with @ symbol and autocomplete
  • �💾 Local storage persistence
  • 👤 User identification - Prompts for name on first comment, stored in session
  • 🔄 Next.js optimized - MutationObserver + route change detection for automatic rehydration
  • ⚠️ Unresolved indicators - Shows when elements can't be found (e.g., after major layout changes)
  • 🎨 Customizable styling
  • 🔄 Content resizing when panel opens

Installation

Step 1: Install Dependencies

First, navigate to the package directory and install dependencies:

cd ayo-commenting-widget
npm install

Step 2: Build the Package

npm run build

Step 3: Link Locally (for development)

npm link

Step 4: Use in Your Microsite

In your microsite project:

cd ../ayo-microsite-frontend
npm link @ayo/commenting-widget

Or add to package.json:

{
  "dependencies": {
    "@ayo/commenting-widget": "file:../ayo-commenting-widget"
  }
}

Usage

Basic Setup

import { CommentingWidget } from '@ayo/commenting-widget';

// Initialize the widget
const widget = new CommentingWidget({
  apiEndpoint: '/api/comments', // If provided, pulls comments by hostname
  enableSelection: true,
  showIndicators: true, // Set to false to hide comment bubbles on elements
});
### Quick API Mode (Hostname)

Provide an `apiEndpoint` (e.g., `/api/v1/website-comments`) and the widget will fetch comments for the current hostname on startup:

Request (GET): `/api/v1/website-comments?host=<hostname>`

Response (JSON):

```json
[
  {
    "id": "comment-123",
    "elementId": "main > section.hero > div.container",
    "elementSelector": "main > section.hero > div.container",
    "text": "Fix spacing here",
    "author": "Matt",
    "timestamp": 1767104523952,
    "signature": {
      "routeKey": "/",
      "selectorPath": "main > section.hero > div.container",
      "tag": "div",
      "heading": "Hero",
      "snippet": "Welcome to the site",
      "rectHint": { "x": 100, "y": 200, "w": 800, "h": 300 }
    },
    "resolved": true,
    "done": false
  }
]

No auth required; results are cached locally for offline usage.


### In React/Next.js (Recommended)

Create a wrapper component for proper lifecycle management:

```jsx
// components/CommentingWidget.tsx
'use client';

import { useEffect } from 'react';

export default function CommentingWidgetLoader() {
  useEffect(() => {
    // Only load in development/staging
    if (process.env.NODE_ENV !== 'production') {
      import('@ayo/commenting-widget').then(({ CommentingWidget }) => {
        const widget = new CommentingWidget({
          showIndicators: true,
          enableSelection: true,
        });

        return () => widget.destroy();
      });
    }
  }, []);

  return null;
}

Then add to your root layout:

// app/layout.tsx
import CommentingWidget from '@/components/CommentingWidget';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <CommentingWidget />
      </body>
    </html>
  );
}

How It Works

Element Identification

The widget uses a sophisticated signature-based approach to identify elements without requiring custom data attributes:

  1. Anchor Normalization: When you click an element, it's normalized to a stable semantic container (<article>, <section>, <li>, etc.) or a sizeable <div> (>12,000px²)

  2. Composite Signatures: Each comment stores multiple fingerprints:

    • Selector path (e.g., main > section.hero > div.container)
    • Tag name
    • Heading text (nearest h1-h6 or aria-label)
    • Text snippet (first 60 chars of text content)
    • Bounding rectangle hint
    • Route key (pathname)
  3. Confidence-Based Resolution: When re-finding elements:

    • First tries the selector path directly
    • Falls back to finding candidates and scoring similarity
    • Only matches if confidence ≥ 0.75
    • Shows ⚠️ badge if element can't be reliably found
  4. Automatic Rehydration: Listens for:

    • DOM mutations (via MutationObserver)
    • Route changes (Next.js navigation)
    • Scroll/resize events (for repositioning)

Selection Granularity

The widget now prefers selecting child components when they look substantive (block display, stable classes, meaningful text, or adequate size). If a clicked element is too small or purely structural, the selector gently bubbles up to the nearest meaningful container.

  • Prefer child: Visible blocks, components with stable classes, or elements with text
  • Bubble up: Tiny or inline-only elements without meaningful content
  • Tip: Aim your cursor at the specific subcomponent you want; the overlay will preview what will be selected.

Storage

Comments are stored in localStorage by default, keyed as ayo-comments. Each comment includes:

{
  id: string;
  userName: string;
  timestamp: number;
  text: string;
  signature: ElementSignature; // Multi-fingerprint for robust resolution
  resolved: boolean; // False if element can't be found
  replies?: Comment[];
}

User names are stored separately in sessionStorage as ayo-user-name.

API

CommentingWidget(options)

Options:

  • apiEndpoint?: string - Backend endpoint for persisting comments (optional)
  • enableSelection?: boolean - Enable element selection mode (default: true)
  • position?: 'right' | 'left' - Bubble position (default: 'right')
  • showIndicators?: boolean - Show visual indicators on commented elements (default: true)

Methods:

  • destroy() - Clean up and remove the widget (removes event listeners, MutationObserver, DOM elements)
  • toggleSelection() - Toggle element selection mode
  • toggleIndicators() - Toggle comment indicators visibility
  • getComments() - Get all comments from storage

Marking Comments as Done

Each comment can be marked as done to indicate completion. In the panel, use the "Mark Done" / "Undo Done" button on a comment. Done comments show a ✓ badge, are dimmed, and have strike-through text.

Programmatic updates via storage:

// Toggle done state
await commentStorage.updateComment(commentId, { done: true });

Troubleshooting

Comments showing ⚠️ Unresolved badge

This happens when the element signature can't be matched with sufficient confidence (< 0.75). Common causes:

  • Major layout restructuring (changed DOM hierarchy)
  • Element removed from the page
  • Significant text content changes
  • Route mismatch (comment was on different page)

Solutions:

  • Navigate to the correct page where the comment was made
  • Check if the element still exists
  • Delete and re-create the comment on the new structure

Indicators not appearing

  1. Check showIndicators is true in options
  2. Verify comments exist for the current route
  3. Check browser console for resolution errors
  4. Ensure MutationObserver is running (widget not destroyed)

Performance issues with many comments

The widget uses debouncing (80ms for mutations, 200ms for scroll/resize) to prevent excessive rehydration. If you have 100+ comments:

  • Consider filtering by route more aggressively
  • Adjust confidence threshold lower (0.70) for faster matching
  • Implement backend pagination instead of storing all in localStorage

License

MIT