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

@xeonr/trellis-support-widget

v0.5.0

Published

The Xeonr lightbulb: a button and modal, in a shadow root, that sends feedback or a support request into a product's Trellis customer project and shows the person their own tickets and replies.

Readme

@xeonr/trellis-support-widget

The lightbulb every Xeonr product shows: a button and a modal where a person sends feedback or a support request into the product's Trellis customer project, attaches files or an annotated capture of the page, and reads and answers the team's replies under My tickets. It renders in a shadow root, so it looks the same on styled-components, Mantine/emotion and Tailwind hosts.

Token client (XMB, upl.im, …)

import { SupportProvider, SupportButton, SupportLauncher, useSupport } from "@xeonr/trellis-support-widget";

<SupportProvider config={{
  product: "xmb",
  version: import.meta.env.VITE_BUILD_SHA,
  trellisUrl: "https://trellis.prod.wtf",
  token: () => client.accessToken(),        // the person's own bearer, with trellis:customer
  reauth: () => signIn(),                   // a session without the scope signs in again
  context: () => ({ mediaId }),             // app.context.mediaId, shown to the person
}}>
  <Nav><SupportButton /></Nav>              // or <SupportLauncher /> in a corner
</SupportProvider>

const { open, unseen, copy } = useSupport();
open({ topic: "problem" });                 // a "Report a problem" link
open({ tab: "tickets" });

Support or feedback

A product that takes only feedback (XMB, PrintHub) passes mode: "feedback", and nothing the widget shows says "support":

| | mode: "support" (default) | mode: "feedback" | |---|---|---| | lightbulb, tooltip, ⌘K | Contact support | Send feedback | | modal title | Contact support | Share your feedback | | tabs | New request · Your requests | New feedback · Your feedback | | once sent | We read every one. Replies show up here under Your requests. | Thanks — we read every piece of feedback. Replies show up under Your feedback. |

copy overrides single strings over the preset, e.g. copy: { title: "Talk to the XMB team" }. Every string is in SupportCopy (button and launcher labels, tooltip, title, the ⌘K entry, tab names, topic heading, body label and placeholder, submit, sending and success text, the empty list, reply placeholders, the sign-in-again line); the presets are exported as SUPPORT_COPY and FEEDBACK_COPY, and useSupport().copy is the resolved set, for a host's own menu entry.

The unseen badge

The lightbulb (SupportButton, SupportLauncher) shows how many of the person's tickets have news they haven't opened: a public reply from the team or a change of state. The count is Trellis's (GetMyUnseenCount), so it is the same on every device; opening a ticket under My tickets marks it seen. It shows 1–9, then "9+", nothing at 0, and the button's name says "(N updates)". The widget refreshes it on mount, on window focus and when the tab becomes visible, every five minutes while visible, and after a ticket is opened. A background count never calls reauth: signed out, there is just no badge. In My tickets, unseen rows are bold and the tab shows the count. useSupport().unseen is the number.

Console

ConsoleShell support={{ product, version }} draws it in the top bar and ⌘K. Without the shell, <ConsoleSupport config={{ product }}>; calls go same-origin to bff's /api/support/ proxy, with no token in the browser.

Config

| | | |---|---| | product | the product slug intake is resolved for | | version | app.version | | trellisUrl + token / baseUrl | where calls go (default baseUrl: "/api/support") | | reauth() | called when a call the person made fails unauthenticated or for want of the scope | | context(), route(), redactUrl(url) | app.context.<name>, page.route, and a scrub for page.url | | telemetry() | telemetry.session / telemetry.errors, e.g. over @lntrn/web (not imported) | | screen | "dom" (default), "display" or "both": DOM snapshot, and/or "Screen or window…" (a getDisplayMedia frame) | | theme | "auto" (default: follows the host page, see Theme), "light", "dark", or () => "light" \| "dark" \| undefined | | brand | { primary, onPrimary?, vars? }: the product's colour over the violet, for a host without the kit (see Brand) | | mode, copy | "support" (default) or "feedback" wording, and single strings over it (SupportCopy) | | topic, myTickets, zIndex, fetch | preselected topic, the second tab and the badge (default on), stacking, a fetch to use | | title | deprecated: copy.title |

The form is the project's: topics and fields come from GetIntake. The widget fills the well-known keys a project asks for (page.url, page.title, page.route, app.product, app.version, app.context.*, client.viewport, client.ua, client.locale, client.timezone, client.theme, telemetry.session, telemetry.errors) and shows them under "Also sending".

Mark anything that must never be in a page capture with data-feedback-redact: it is covered before the snapshot is taken, in every capture mode, so no preview ever shows it. The widget's own hosts are never in a capture.

Theme

The modal and the lightbulb follow the host's theme, not the system's, so a dark app gets a dark modal whatever the OS says. "auto" reads, in order:

  1. data-theme="light|dark", data-mantine-color-scheme, or a dark / light (theme-dark / theme-light) class on <html>, then on <body>;
  2. the computed color-scheme of <html>, then <body>, when it names one scheme (light dark and normal name none);
  3. prefers-color-scheme, only when the page declares nothing.

A MutationObserver on <html> and <body> and a matchMedia listener apply changes live, the modal open or not. A function is asked first, on every change the widget sees and every render, and undefined falls back to the page; use it when the host keeps its theme somewhere the page doesn't show. "light" / "dark" fix it and beat everything. In a console, ConsoleShell passes the kit's live theme, so its own toggle drives the widget.

Brand

The modal wears the host's brand, not only its light or dark: the accent, focus ring, tab underline, chips, primary button and the gradient stripe.

On a kit host (every console) nothing is configured. The widget's brand tokens are var(--x-<name>, <kit default>), and custom properties inherit into its shadow root, so a console whose brand.ts repointed --x-brand (a green Functions, an amber Dev) gets that family in the modal, live. While the widget's theme is the page's, it takes the page's surfaces, text and lines too (data-follow on the hosts), so the modal matches the console's own panels; a modal forced to the other scheme keeps its own.

Elsewhere, pass the product's primary and the widget derives the rest with the kit's own brandRamp(), the same ramp a console of that colour gets:

<SupportProvider config={{ product: "printhub", brand: { primary: "#3b8cff" }, … }}>

onPrimary (text and icons on the fill) defaults to white or near-black, whichever reads on the gradient's palest stop; vars sets any --xf-* property last. This replaces overriding raw --xf-* properties on the hosts, which still works.

Never brand-coloured: the lightbulb's yellow, and the status tones (danger, success, warning), which are the kit's everywhere.

The lightbulb's colour

The lightbulb is lit-bulb yellow in both themes, from the kit's --x-idea token (here --xf-idea; #ffd43b on dark, a gold stroke with a yellow glass fill on light). Hover and the open state change the square behind it, not the bulb. A host repaints it on the widget's host elements:

xeonr-support-button, xeonr-support-launcher {
  --xf-bulb: #fff;                 /* the glyph */
  --xf-bulb-glow: transparent;     /* the glass fill */
}

Any other --xf-* property set on the hosts (xeonr-support, xeonr-support-button, xeonr-support-launcher) still overrides the token sheet, as before.

Capture and annotate

"Capture this page" is a split button. The main half takes the visible area (the viewport); the caret offers Region, Visible area, Full page (the whole scrollable document) and, with screen: "both", Screen or window…. Region steps the modal aside and dims the live page under a crosshair: drag out a rectangle (its size shows; corners resize it, dragging inside moves it), then Enter or Capture. Esc puts the modal back unchanged. From the keyboard, the first arrow key drops a rectangle in the middle; arrows move it, Shift+arrows resize it, Alt steps by 1px. Captures are at devicePixelRatio (capped at 2), in page coordinates, so a scrolled page captures what was selected.

The capture then opens in the annotator: Select (click to select, drag to move, corners and arrow ends resize, Delete removes, arrow keys nudge), Crop (drag, then Apply), Arrow, Box (an outline), Marker (a translucent freehand pen, simplified so it isn't a point per mousemove), Text (click, or Enter, to place a label and type it inline; double-click to edit; drag to move) and Black out, in four pen colours from the token sheet. One undo/redo history covers all of it, crops included (⌘Z, ⇧⌘Z, Ctrl+Y). Everything is burned into the PNG at the capture's scale, and a mark outside the crop is clipped with it.

@xeonr/trellis-support-widget/core is the same without React: the CustomerService client, auto-fill and validation, capture and the annotator's model.

0.5

  • The modal follows the host's brand: on a kit host, its --x-brand family (and, in the page's own scheme, its surfaces) by inheritance; elsewhere, the new brand: { primary, onPrimary?, vars? } option, derived with the kit's brandRamp(). SupportBrand is exported, and /core has brandVars. The lightbulb stays yellow (TRL-61).

0.4

  • The lightbulb is yellow (--xf-idea, from the kit's new --x-idea) in both themes, on SupportButton, SupportLauncher and the console top bar; --xf-bulb and --xf-bulb-glow override it (TRL-61).
  • theme: "auto" follows the host page (data-theme or a class on <html>/<body>, then color-scheme) before the system, live, and the hosts always carry the resolved data-theme. theme also takes a function. ThemeSetting is exported.

0.3

  • Capture modes chosen before capturing (Region, Visible area, Full page, Screen or window…), the region picker, and the annotator's Select, Crop, Arrow, Marker and Text tools, pen colours and undo/redo (TRL-55). The menu, picker and annotator strings are in SupportCopy.
  • Highlight is now a rectangle outline (it was a translucent fill).
  • /core: captureDom({ area }), regionToPixels, and the annotator's model (Mark, arrowHead, simplifyPath, layoutText, hitTest, …); renderAnnotated takes { crop, unit }. The 0.2 Annotation boxes are still valid marks.

0.2

  • mode and copy (TRL-54); the support preset's wording is now "Contact support", with tabs "New request" / "Your requests".
  • The unseen badge, from Trellis (TRL-53). The localStorage last-seen tracking is gone, with its /core exports (readSeen, markSeen, isUnread, unreadCount, seenKey). useSupport().unread is now a deprecated alias of unseen.
  • TicketSummary.unseen and ListMyTicketsResponse.unseenCount are new, and CustomerClient has getMyUnseenCount.