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

@scaleflex/annotator

v0.1.5

Published

Framework-agnostic image & video annotation Web Component for Scaleflex VXP

Readme

@scaleflex/annotator

A framework-agnostic Web Component (Lit 3) for image and video annotation — pin comments, shape markup, threaded discussions, and a frame-accurate video timeline. Ships as a standard <sfx-annotator> custom element with a first-class React wrapper.

Part of the Scaleflex widget family alongside @scaleflex/uploader and @scaleflex/asset-picker.

Features

  • Pin comments — click to drop a pin and open an anchored comment composer
  • Shape markup — rectangle, ellipse, arrow, line, freehand (Konva canvas)
  • Threaded comments — replies, @mentions, emoji, resolve/reopen, filter & sort
  • Video — timeline with comment markers, frame stepping, playback speed, timestamped annotations that show/hide as the video plays
  • Zoom & pan — wheel-to-zoom at cursor, space-drag / pinch pan, fit-to-screen, double-click toggle, fullscreen
  • Accessible — DOM pins with ARIA + arrow-key navigation, focus-trapped popups, aria-live announcements, prefers-reduced-motion aware
  • Themeable--sfx-an-* CSS custom properties (OkLCH)
  • i18n — i18next-based, every string has an inline English default

Install

npm install @scaleflex/annotator

Usage — Vanilla JS

import '@scaleflex/annotator/define'; // registers <sfx-annotator>

const el = document.querySelector('sfx-annotator');
el.config = {
  media: { type: 'image', src: 'https://example.com/image.jpg' },
  currentUser: { id: 'u1', name: 'Ada' },
  getUsers: (q) => myUsers.filter((u) => u.name.includes(q)), // @mention source
};

// Seed existing annotations
el.setAnnotations({ pins, shapes, comments });

// Listen to events
el.addEventListener('sfx-comment-added', (e) => console.log(e.detail.comment));
el.addEventListener('sfx-pin-clicked', (e) => console.log(e.detail.pin));

Usage — React

import { Annotator, type AnnotatorRef } from '@scaleflex/annotator/react';
import { useRef } from 'react';

function Review() {
  const ref = useRef<AnnotatorRef>(null);
  return (
    <Annotator
      ref={ref}
      config={{ media: { type: 'video', src: '/clip.mp4', fps: 30 } }}
      onCommentAdded={(c) => save(c)}
      onPinClicked={(pin, comment) => focus(comment)}
      style={{ height: 600 }}
    />
  );
}

Imperative methods on the ref: setAnnotations, getPins/getShapes/getComments, setTool, resolveComment/reopenComment/deleteComment, play/pause/seekTo, zoomTo/fitToScreen, toggleFullscreen.

Composable API

The monolithic <sfx-annotator> renders the toolbar, canvas, and comments panel together. When you need those pieces in different places — e.g. a drawing overlay on your own media viewer plus a comments panel in a separate tab — mount them independently and wire them to a single shared AnnotatorSession.

These composable elements each take a .session:

| Element | React | Purpose | |---|---|---| | <sfx-annotation-overlay> | <AnnotationOverlay> | Drawing surface (pins + shapes). Overlays your own media in overlay mode. | | <sfx-annotation-toolbar> | <Toolbar> | Drawing tools only (no comment input). Pair with the on-canvas draft popover + a comments panel. | | <sfx-composer> | <Composer> | Toolbar + mention-aware comment input (all-in-one). | | <sfx-comments> | <Comments> | Threaded comments panel (filter/sort/resolve). |

Toolbar vs Composer — use <Toolbar> when comments are captured elsewhere (the popover that opens after drawing/placing a pin, plus the panel's input), so you don't get a redundant comment box under the media. Use <Composer> for a single self-contained "draw + comment" field (the classic AnnotatingField).

Overlay mode (attach to your own media element)

Unlike the monolith (which renders media.src itself), the overlay can align its annotation layers over an external <img>/<video>/<canvas> you own — the host keeps control of the media and its zoom/pan.

import {
  createAnnotatorSession,
  AnnotatorProvider,
  AnnotationOverlay,
  Composer,
  Comments,
  useAnnotatorEvents,
} from '@scaleflex/annotator/react';
import { useRef, useState } from 'react';

function Review() {
  const [session] = useState(() =>
    createAnnotatorSession({
      media: { type: 'image', src: '/image.jpg' },
      currentUser: { id: 'u1', name: 'Ada' },
      getUsers: (q) => searchUsers(q),
    }),
  );
  const mediaRef = useRef<HTMLImageElement>(null);

  // One place to persist every change, wherever the pieces are mounted.
  useAnnotatorEvents(
    {
      onCommentAdded: (c) => api.create(c),
      onCommentResolved: (c) => api.resolve(c.id),
      onCommentLiked: (c, liked) => api.like(c.id, liked),
    },
    session,
  );

  return (
    <AnnotatorProvider session={session}>
      <div style={{ position: 'relative' }}>
        <img ref={mediaRef} src="/image.jpg" />
        {/* overlay aligns over your <img>; you own zoom/pan */}
        <AnnotationOverlay mediaRef={mediaRef} style={{ position: 'absolute', inset: 0 }} />
      </div>
      <Composer />
      {/* mount the panel anywhere — a tab, a drawer, a sibling route */}
      <Comments />
    </AnnotatorProvider>
  );
}

Each component also accepts an explicit session prop (takes precedence over the provider), so pieces in far-apart React subtrees can share one session without a common ancestor. The vanilla elements work the same way:

import '@scaleflex/annotator/define';
import { createAnnotatorSession } from '@scaleflex/annotator';

const session = createAnnotatorSession({ media: { type: 'image', src: '/i.jpg' } });
document.querySelector('sfx-annotation-overlay').session = session;
document.querySelector('sfx-annotation-overlay').externalMedia = myImgEl; // overlay mode
document.querySelector('sfx-comments').session = session;
session.on('sfx-comment-added', (e) => api.create(e.detail.comment));

See demo/composable.html for a full working example.

Content Security Policy (CSP)

The widget is designed to run under a strict CSP with no 'unsafe-inline' and no 'unsafe-eval':

  • No inline style attributes. All dynamic positioning (pins, popovers, tooltips, the canvas transform, timeline markers, colour swatches) is applied through the CSSOM (element.style.setProperty) via an internal directive, which CSP's style-src-attr does not intercept.
  • Component styles use Lit's constructable/adopted stylesheets, which are CSP-exempt (no <style> injection in modern browsers).
  • Konva renders to <canvas> and only touches element styles via the CSSOM — no eval/new Function and no injected <style>.

A policy like this is sufficient:

Content-Security-Policy: default-src 'self'; img-src 'self' data: blob:; style-src 'self';

Older browsers without adoptedStyleSheets support fall back to Lit injecting <style> elements. If you must support those under a strict policy, set window.litNonce = '<your-nonce>' before the widget loads so Lit adds the nonce to those elements.

Usage — CDN

<script src="https://cdn.example.com/sfx-annotator.min.js"></script>
<sfx-annotator id="a"></sfx-annotator>
<script>
  document.getElementById('a').config = {
    media: { type: 'image', src: '/image.jpg' },
  };
</script>

Keyboard shortcuts

| Keys | Action | |------|--------| | 17 | Select tool (select, pin, rectangle, ellipse, arrow, line, freehand) | | Ctrl/⌘ + Z / Ctrl/⌘ + Y | Undo / redo | | + / - | Zoom in / out | | F | Fit to screen | | Space (hold) + drag | Pan | | Esc | Cancel current drawing | | Delete / Backspace | Delete the active comment | | Tab / arrow keys | Move focus between pins |

Configuration

interface AnnotatorConfig {
  media: { type: 'image' | 'video'; src: string; poster?: string; fps?: number };
  currentUser?: User;
  tools?: { enabled?: ToolType[]; colors?: string[]; strokeWidths?: number[] };
  getUsers?: (query: string) => User[] | Promise<User[]>;
  showComments?: boolean; // default true
  showToolbar?: boolean;  // default true
  locale?: string;        // default 'en'
  readOnly?: boolean;
}

Events

sfx-comment-added, sfx-comment-resolved, sfx-comment-reopened, sfx-comment-deleted, sfx-comment-edited, sfx-comment-liked, sfx-comment-subscribed, sfx-pin-clicked, sfx-shape-added, sfx-tool-changed, sfx-annotation-changed. All bubble and are composed.

Theming

Override any --sfx-an-* custom property on the host:

sfx-annotator {
  --sfx-an-primary: oklch(0.6 0.2 250);
  --sfx-an-pin-bg: #ff5a5f;
  --sfx-an-panel-width: 360px;
}

Migrating from react-filerobot-media-annotator

The legacy library exposed three separate React components (Canvas, AnnotatingField, Comments) driven by callbacks. The composable API above is the modern equivalent — map the legacy pieces to a shared session and translate the data at the boundary (the widget stays generic; the mapping lives in your app):

| Legacy | New | |---|---| | <Canvas mediaRef> | <AnnotationOverlay mediaRef> (overlay mode) | | <AnnotatingField onSubmit getUsers> | <Composer> + session.on('sfx-comment-added') | | <Comments getComments createReply updateComment deleteComment> | <Comments> + session.setAnnotations(...) + useAnnotatorEvents({...}) |

Comment shape (userauthor, meta→top-level fields):

| Legacy | New | |---|---| | user.photo_uri | author.avatarUrl | | meta.resolved: boolean | status: 'open' \| 'resolved' | | meta.likers / meta.subscribers | likes / subscribers | | meta.displayTimeStamp | timestamp |

Shape geometry — legacy shapes were pixel coordinates plus canvasDimensions; the new model is normalized 0–1. Convert once when seeding: divide each coordinate by its canvas dimension, and rename types circle → ellipse, pen → freehand. Events emit normalized shapes.

Translations — instead of a flat translations object, call addTranslations (or set config.locale) with the widget's i18n keys.

Development

npm run dev        # demo dev server
npm run build      # library build → dist/ (ESM + CJS + d.ts)
npm run build:cdn  # single IIFE bundle → dist-cdn/
npm run typecheck
npm test

License

SEE LICENSE IN LICENSE