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

@washi-ui/core

v1.0.2

Published

Framework-agnostic HTML commenting engine

Downloads

108

Readme

@washi-ui/core

Framework-agnostic HTML commenting engine. Mount an annotation layer on top of any iframe, handle pin-based comments, and delegate storage to any backend via the adapter pattern.

Installation

npm install @washi-ui/core

Quick Start

import { Washi } from '@washi-ui/core';

// 1. Implement a storage adapter
const adapter = {
  async save(comment) {
    await fetch('/api/comments', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(comment),
    });
  },
  async load() {
    return fetch('/api/comments').then(r => r.json());
  },
  async update(id, updates) {
    await fetch(`/api/comments/${id}`, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(updates),
    });
  },
  async delete(id) {
    await fetch(`/api/comments/${id}`, { method: 'DELETE' });
  },
};

// 2. Create and mount
const washi = new Washi(adapter);
await washi.mount(document.querySelector('iframe'));

// 3. Handle events
washi.on('pin:placed', async ({ x, y }) => {
  // User clicked the overlay in annotate mode
  // Show your comment input, then call addComment
  const text = prompt('Comment:');
  if (text) await washi.addComment({ x, y, text });
});

washi.on('comment:clicked', ({ comment }) => {
  console.log('Pin clicked:', comment.text);
});

// 4. Enable annotate mode
washi.setMode('annotate');

API Reference

new Washi(adapter)

Creates a Washi instance. The adapter handles all storage — it is not called until mount() is called.

const washi = new Washi(adapter);

washi.mount(iframe, options?)

Mounts the annotation layer onto an iframe. Async — loads existing comments from the adapter and waits for iframe content dimensions to stabilise before rendering pins.

await washi.mount(document.querySelector('iframe'));

// With options
await washi.mount(iframe, {
  readOnly: true,             // Disable annotate mode
  disableBuiltinDialog: true, // Suppress built-in click popover
});

MountOptions

| Option | Type | Description | |--------|------|-------------| | readOnly | boolean | Prevents switching to annotate mode | | disableBuiltinDialog | boolean | Suppresses the built-in pin popover on click. Use when rendering your own popover via comment:clicked. |

Throws if the iframe is already mounted (call unmount() first) or not attached to the DOM.


washi.unmount()

Removes the overlay, cleans up all event listeners, and clears internal state. Safe to call multiple times.

washi.unmount();
// Can now mount to a different iframe
await washi.mount(otherIframe);

washi.setMode(mode)

Switches between interaction modes. Emits mode:changed.

| Mode | Behaviour | |------|-----------| | 'view' | Overlay has no pointer events; existing pins are clickable | | 'annotate' | Overlay captures clicks and emits pin:placed with { x, y } coordinates |

washi.setMode('annotate');
washi.setMode('view');

Throws if readOnly was set in mount options and mode is 'annotate'.


washi.addComment(input)

Adds a comment and renders its pin on the overlay. The library generates id and createdAt — you only need to provide x, y, and text.

const comment = await washi.addComment({
  x: 42.5,           // 0–100, percentage of content width
  y: 18.0,           // 0–100, percentage of content height
  text: 'This heading needs more contrast',
  color: '#ef4444',  // optional, defaults to palette
});

console.log(comment.id);        // auto-generated UUID
console.log(comment.createdAt); // auto-generated timestamp

Emits comment:created with the full Comment on success.


washi.updateComment(id, updates)

Updates a comment with partial data. Re-renders the pin if x, y, color, or resolved changes.

await washi.updateComment('abc', { resolved: true });
await washi.updateComment('abc', { x: 60, y: 30 });
await washi.updateComment('abc', { color: '#10b981' });

Emits comment:updated with { id, updates }.


washi.deleteComment(id)

Deletes a comment and removes its pin. Re-renders remaining pins to update their numbered badges.

await washi.deleteComment('abc');

Emits comment:deleted with { id }.


washi.getComments()

Returns a snapshot of all current comments as shallow copies.

const all = washi.getComments();
const unresolved = washi.getComments().filter(c => !c.resolved);

washi.on(event, handler)

Subscribes to an event. Returns an unsubscribe function.

const off = washi.on('comment:created', (comment) => {
  console.log('New comment:', comment.id);
});

// Unsubscribe later
off();

Events

| Event | Payload type | When | |-------|-------------|------| | pin:placed | PinPlacedEvent | User clicked the overlay in annotate mode | | comment:created | Comment | addComment() succeeded and comment was persisted | | comment:updated | CommentUpdatedEvent | updateComment() succeeded | | comment:deleted | CommentDeletedEvent | deleteComment() succeeded | | comment:clicked | CommentClickedEvent | User clicked an existing pin | | mode:changed | ModeChangedEvent | setMode() was called | | error | ErrorEvent | An internal load/save error occurred |


Types

WashiAdapter

interface WashiAdapter {
  save(comment: Comment): Promise<void>;
  load(): Promise<Comment[]>;
  update(id: string, updates: Partial<Comment>): Promise<void>;
  delete(id: string): Promise<void>;
}

Comment

interface Comment {
  id: string;         // UUID, generated by the library
  x: number;          // 0–100, percentage of content width
  y: number;          // 0–100, percentage of content height
  text: string;
  color?: string;
  resolved?: boolean;
  createdAt: number;  // Unix ms, generated by the library
}

NewComment

Input type for addComment(). Omits id and createdAt, which are generated automatically.

interface NewComment {
  x: number;
  y: number;
  text: string;
  color?: string;
}

MountOptions

interface MountOptions {
  readOnly?: boolean;
  disableBuiltinDialog?: boolean;
}

Event payloads

interface PinPlacedEvent     { x: number; y: number; }
interface CommentUpdatedEvent { id: string; updates: Partial<Comment>; }
interface CommentDeletedEvent { id: string; }
interface CommentClickedEvent { comment: Comment; }
interface ModeChangedEvent   { mode: WashiMode; previousMode: WashiMode; }
interface ErrorEvent         { type: string; message: string; error?: Error; }

License

MIT