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

notant

v0.9.1

Published

Embeddable website feedback and issue reporting. Screenshot capture, annotation, and automatic diagnostics for React/Ionic apps.

Readme

Notant

Embeddable website feedback and issue reporting for React/Ionic apps.

Users capture a region of the page, annotate it with pen/text/shapes, and file a report. Notant attaches 19 diagnostic fields automatically and posts everything to a backend you configure.

Status: verified end-to-end against a live backend. Published to npm.

Licence: PolyForm Noncommercial 1.0.0 — free for noncommercial use. Commercial use requires a licence from Loved Digital Consulting.


Install

npm install notant

Peer dependencies: react (18 or 19), react-dom, @ionic/react (8), ionicons (7 or 8).

Capture uses modern-screenshot — the one runtime dependency.

Use

import { Notant, createHttpTransport } from 'notant';
import 'notant/styles.css';

<Notant
  enabled={isAuthenticated}
  config={{
    transport: createHttpTransport({
      endpoint: 'https://<project>.supabase.co/functions/v1/ld-crm/public/issue-forms/<api-key>',
    }),
    siteId: 'hwt',
    appVersion: __APP_VERSION__,
    getUser: () => ({ email: user?.email, id: user?.id }),
    getContext: () => ({ scope: activeScope, companyId: viewCompanyId }),
  }}
/>;

Mount it once, near the root, inside <IonApp>.

Configuration

| Option | Type | Notes | | --------------- | ------------------------------- | ------------------------------------------------------------------------ | | transport | IssueTransport | Required. Where reports go. | | siteId | string | Identifies this site in the payload. | | appVersion | string | Surfaced in diagnostics. | | getUser | () => { email?, id? } | Called at submit time, so it always reflects the current session. | | getContext | () => Record<string, unknown> | Arbitrary host context merged into the report. | | categories | { value, label }[] | Defaults to bug / feature / question / general. | | position | NotantPosition | FAB placement — see below. Defaults to bottom-right, 20px in. | | appearance | NotantAppearance | FAB icons and colours, per button — see below. | | privacyNotice | string \| false | Notice shown beside the capture before sending. Defaults on — see below. | | shortcut | string \| false | Key that captures the viewport instantly. Defaults to mod+shift+s. |

Keyboard shortcut

Press Ctrl/Cmd+Shift+S and the viewport is captured as it stands — no area selector, no click — straight to the annotation form.

That exists because the FAB cannot photograph the thing you most often need a photograph of. A dropdown closes on blur, a popover closes on outside-click, an alert takes the click itself: reaching for the launcher destroys the state you were trying to report. A key press moves no focus, so nothing is dismissed.

shortcut: 'alt+shift+c'; // any combination
shortcut: false; // bind nothing

mod resolves per platform — Command on macOS, Control elsewhere. ctrl, meta/cmd, alt/option and shift are literal. Modifiers are matched exactly, so Ctrl+Alt+Shift+S will not trigger a Ctrl+Shift+S binding — that combination belongs to whoever bound it. A binding with no modifier at all ('f2', '/') is suppressed while the user is typing in a field; modifier combinations always fire. An unparseable string binds nothing and warns on the console.

The default is not collision-free — Cmd+Shift+S is Save As in Safari — which is why the handler calls preventDefault, and why this is configurable. Set your own if it clashes with a binding of yours.

One limitation is worth stating plainly: a native <select> cannot be captured. The OS draws that popup, not the page, so nothing in the DOM can see it. Ionic's own overlays — ion-modal, ion-popover, ion-alert, ion-action-sheet — capture fine.

Privacy notice

A capture takes whatever is on the page, which on an authenticated app routinely means someone else's personal data. The reporter is the only person in the loop who can see that and act on it, so a notice is shown beside the screenshot by default:

Everything visible in this screenshot is sent with your report. Check for personal or sensitive information before submitting.

privacyNotice: 'Tarkista henkilötiedot ennen lähettämistä.'; // reword
privacyNotice: false; // remove entirely

DEFAULT_PRIVACY_NOTICE is exported, so you can append rather than replace:

import { DEFAULT_PRIVACY_NOTICE } from 'notant';

privacyNotice: `${DEFAULT_PRIVACY_NOTICE} Screenshots are kept for 90 days.`;

Setting false submits captures with no warning at all. That's a deliberate choice, not a default.

Placement

Host apps often already have a FAB. Notant can sit on any corner or edge:

position: {
  vertical:   'top' | 'middle' | 'bottom',   // default 'bottom'
  horizontal: 'left' | 'center' | 'right',   // default 'right'
  offset:     20,                            // px or any CSS length
                                             // or { x, y } per axis
}

The action list opens away from the nearest edge, so it never expands off-screen. Offsets apply only to edges the FAB is anchored to — a centred axis is positioned by Ionic and an offset there would push it off-centre.

Appearance

The launcher sits on top of your product, so it should look like it belongs there. A chat bubble is the wrong affordance on an app where reports mean bugs.

import { bug, cameraReverse } from 'ionicons/icons';

appearance: {
  size: 'lg',                                                  // sm | md | lg
  trigger: { icon: bug, color: '#111827', iconColor: '#fbbf24' },
  capture: { icon: cameraReverse, color: 'danger' },
  blank:   { color: 'rebeccapurple', iconColor: '#fff' },
}

size applies to all three buttons and scales the icon with them — sm 40px, md 56px, lg 72px. Leaving it unset is not the same as 'md': unset emits no sizing at all, so a host that has restyled the button keeps its own.

Three buttons, each optional: trigger is the launcher, capture opens area selection, blank reports without a capture. Any field you leave out keeps its default.

color takes either a built-in Ionic palette name (primary, secondary, tertiary, success, warning, danger, light, medium, dark) or any CSS colour. The two vocabularies cannot collide — none of the nine names is a valid CSS colour — so there is no ambiguity and no second field to learn.

Passing an Ionic name routes it to the color prop, so your theme (including dark mode and contrast variants) still applies. A CSS colour goes to --background instead, because color would silently ignore it.

For a custom Ionic palette colour you defined yourself, pass the variable rather than the name — color: 'var(--ion-color-brand)'. A bare 'brand' would be treated as a CSS colour, and an invalid one.

Icons are ionicons exports, which you already have as a peer dependency. Anything IonIcon accepts works, including a data URI.

getUser and getContext are the seam that keeps Notant host-agnostic — there is no auth library, no API client, and no framework assumption baked in.

Colour picker

NotantColorPicker is exported on its own. It is a colour control that happens to be used by the annotation tools — nothing in it is specific to them.

import { NotantColorPicker, DEFAULT_SWATCHES } from 'notant';

<NotantColorPicker value={colour} onChange={setColour} label="Pen colour" />;

| Prop | Type | Notes | | ------------- | --------------------- | ----------------------------------------- | | value | string | Any hex form; compared after normalising. | | onChange | (v: string) => void | Always receives #rrggbb. | | swatches | readonly string[] | Defaults to DEFAULT_SWATCHES (12). | | label | string | Accessible name, e.g. "Pen colour". | | allowCustom | boolean | Set false to leave only the swatches. |

The default palette is picked for legibility on top of a screenshot, where the background is arbitrary — saturated mid-tones rather than pastels, and both neutral extremes, since white is the only thing that reads on a dark capture and black the only thing that reads on a light one.

Behind Custom is a picker we wrote — saturation/value square, hue slider, and HEX / RGB / HSL inputs — not <input type="color">. The native input opens the OS picker, which brings its own chrome and its own idea of a colour model into the middle of your product. Dragging and arrow keys both work; the hex field accepts fff, #FFF or #ffffff.

Saved colours

The star beside Custom saves the current colour; saved colours appear as a row under the palette. Six of them, most recent first — one row, because the popover has to keep fitting on a short window.

Persistence defaults to localStorage, namespaced by siteId so two Notant instances on one origin don't collide. A host that knows who the user is can keep them somewhere better:

favorites: {
  get: () => api.getUserColors(),        // may return a promise
  set: (colors) => api.saveUserColors(colors),
}

favorites: false removes the feature. The colour maths and list helpers (addFavorite, removeFavorite, sanitizeFavorites) are exported too, so a host implementing its own store gets the same capping and de-duplication.

The conversions are exported for reuse: normalizeHex, isLightColor, contrastInk, hexToHsv/hsvToHex, hexToRgb/rgbToHex and hexToHsl/hslToHex.

Transports

Notant makes no assumption about the backend. createHttpTransport posts multipart/form-data with each field as its own part, plus image as a file part.

interface IssueTransport {
  submit(payload: IssueSubmission, image?: Blob | null): Promise<IssueSubmissionResult>;
}

Field names title, details and category are load-bearing for ld-crm: its issue-form AUTO_DETECT map picks them up as subject, description and category. Renaming them fails silently — the request still returns 200, the fields just land nowhere.

What gets collected automatically

browser + version   OS + platform      language      timezone
viewport w/h/DPR    screen w/h/depth   orientation   colour scheme
network type        downlink  RTT      saveData      cookies enabled
device RAM          CPU cores          touch         referrer
PWA standalone

Plus the page URL. Refreshes on resize and orientation change.

Layout

src/
├─ core/          no Ionic; framework-agnostic contracts
│   ├─ clientEnv.ts     diagnostics
│   ├─ transport.ts     IssueTransport + createHttpTransport
│   └─ types.ts         IssueSubmission
├─ react/
│   ├─ Notant.tsx       mount point + workflow state
│   ├─ NotantFab.tsx    launcher
│   ├─ config.tsx       context: transport, getUser, getContext
│   ├─ screenshot/      selector, modal, annotation stage, controls
│   └─ experimental/    NOT exported — see below
└─ styles/

The core/ boundary is kept deliberately, so a non-React build stays a possible additive change rather than a rewrite.

Not shipped yet

src/react/experimental/ holds QuickFeedbackModal (thumbs rating) and ContactModal (contact support), carried over from the original HWT branch. Both are unwired stubs — they console.log and simulate an 800 ms delay instead of submitting. They are excluded from the build and the public exports.

Screen recording was dropped entirely: it referenced a feedbackService module that never existed.

Known gaps

  • No tests. The demo harness is the only verification path.
  • The widget wears stock Ionic chrome; it doesn't adopt a host's design tokens.

~~Screenshots do not arrive~~ — fixed in ld-crm#495 and verified in production (#1).

Development

npm install
npm run typecheck
npm run build      # → dist/notant.js, notant.cjs, notant.css, index.d.ts