@ayodev/commenting-widget
v0.2.1
Published
Embeddable commenting widget for annotating page elements
Downloads
462
Maintainers
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 installStep 2: Build the Package
npm run buildStep 3: Link Locally (for development)
npm linkStep 4: Use in Your Microsite
In your microsite project:
cd ../ayo-microsite-frontend
npm link @ayo/commenting-widgetOr 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:
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²)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)
- Selector path (e.g.,
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
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 modetoggleIndicators()- Toggle comment indicators visibilitygetComments()- 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
- Check
showIndicatorsistruein options - Verify comments exist for the current route
- Check browser console for resolution errors
- 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
