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

@welcome-back/brand

v0.11.0

Published

WelcomeBack brand assets for React apps: the running-W loader and the POS design tokens.

Readme

@welcome-back/brand

WelcomeBack brand assets for React apps.

Today the package has one component: RunnerLoader, the isotipo W running left. Its legs swing on their hips and cross under the body while the body bobs. Use it for loading moments that mean "on the way to the next step".

  • React 18 or 19. Works in Next.js (App Router and Pages), Vite and plain React.
  • No CSS import, no Tailwind, no bundler setup: the animation ships inside the SVG.
  • Server-renderable: it shows before JavaScript loads. Works in React Server Components (it uses only useId).
  • ESM and CommonJS builds, so Jest suites load it with no transform setup.

Contents

Install

npm i @welcome-back/brand

The package is public on npm. No token or .npmrc is needed.

Quick start

import { RunnerLoader } from "@welcome-back/brand";

export function Loading() {
  return <RunnerLoader className="w-24" label="Loading" />;
}

That is all. Set a width, and the height follows the art.

API

Props

| Prop | Type | Default | What it does | |---|---|---|---| | className | string | — | Added after the built-in wb-runner class. Use it for size, color and the CSS variables below. | | label | string | — | Accessible name. With a label the SVG is role="img" and screen readers announce it. Without a label it is aria-hidden: use that when a parent element already names the loading state. |

The component renders one <svg>. It takes no other props.

CSS custom properties

You can set these in className (Tailwind arbitrary properties), in your own CSS class, or on any ancestor element. Values inherit, so one setting on a wrapper applies to all runners inside it.

| Property | Controls | Default | Notes | |---|---|---|---| | color | Ink of the W, legs and shoes | currentColor | Any Tailwind text-* class works. | | --wb-runner-pants | Fill inside the pants | Canvas | Set it to the color behind the runner. The near leg covers the far leg when they cross, so the pants must be opaque. | | --wb-runner-step | Duration of one step | 0.32s | A full stride is two steps (0.64 s). Lower is faster. |

Canvas is the page background color that the browser uses for the current color-scheme: white in light mode, near-black in dark mode. On any other surface (a card, a tinted panel, a brand color) set --wb-runner-pants.

Size guide

| Width | Use | |---|---| | 96–128 px | Full-screen or full-panel loading | | 48–64 px | Loading inside a card or a list area | | 32–40 px | Next to text ("Loading…") | | under 32 px | Not recommended: the legs become unreadable. Use a plain spinner. |

Examples

Tailwind: size, color and speed in one className

<RunnerLoader className="w-16 text-foreground" />

// Faster (0.2 s per step) and in the muted text color:
<RunnerLoader className="w-16 text-muted-foreground [--wb-runner-step:0.2s]" />

Plain CSS (no Tailwind)

/* loading.css */
.page-loader {
  width: 112px;
  color: #171717;
  --wb-runner-pants: #ffffff;
}
import "./loading.css";

<RunnerLoader className="page-loader" label="Loading" />;

Inline style on a wrapper

RunnerLoader has no style prop. The CSS variables inherit, so put inline styles on a parent element. TypeScript does not know custom properties, so cast the style object:

import type { CSSProperties } from "react";

const runnerTheme = {
  color: "#171717",
  "--wb-runner-pants": "#F6F4E9",
  "--wb-runner-step": "0.28s",
} as CSSProperties;

<div style={runnerTheme}>
  <RunnerLoader className="w-24" label="Loading" />
</div>;

Match a shadcn / Tailwind 3 theme token

shadcn stores colors as bare HSL values (--background: 0 0% 100%), so wrap them in hsl():

<div className="bg-background text-foreground [--wb-runner-pants:hsl(var(--background))]">
  <RunnerLoader className="w-24" />
</div>

In Tailwind 4 the tokens are full colors, so drop the hsl(): [--wb-runner-pants:var(--background)].

On a card or a colored surface

The pants must match the surface directly behind the runner, not the page:

// On a card
<div className="rounded-xl bg-card p-8 [--wb-runner-pants:hsl(var(--card))]">
  <RunnerLoader className="w-16" />
</div>

// On a brand-orange disc, with a cream runner
<div className="grid size-32 place-items-center rounded-full bg-[#FF6E26] text-[#FFFAF0] [--wb-runner-pants:#FF6E26]">
  <RunnerLoader className="w-20" label="Loading" />
</div>

Dark mode

With the defaults, no extra code is needed on the page background: Canvas follows color-scheme, and the ink follows your text color.

<main className="bg-background text-foreground">
  <RunnerLoader className="w-24" />
</main>

If your dark mode is a .dark class and not color-scheme, set the pants from the token, as in the shadcn example above.

Next to text

The label is on the wrapper, so the runner stays decorative:

<div role="status" className="flex items-center gap-3 text-sm text-muted-foreground">
  <RunnerLoader className="w-9" />
  <span>Loading your rewards…</span>
</div>

Next.js route loading (loading.tsx)

// app/dashboard/loading.tsx
import { RunnerLoader } from "@welcome-back/brand";

export default function Loading() {
  return (
    <div className="grid min-h-[60vh] place-items-center">
      <RunnerLoader className="w-24" label="Loading" />
    </div>
  );
}

This is a Server Component. It needs no "use client".

React Suspense fallback

<Suspense fallback={<RunnerLoader className="mx-auto w-20" label="Loading" />}>
  <CustomerDetails id={id} />
</Suspense>

Show only if the wait is long

On a fast request the runner can flash for a fraction of a stride. Wait before you show it:

"use client";
import { useEffect, useState } from "react";
import { RunnerLoader } from "@welcome-back/brand";

export function DelayedRunner({ delayMs = 200 }: { delayMs?: number }) {
  const [visible, setVisible] = useState(false);
  useEffect(() => {
    const timer = setTimeout(() => setVisible(true), delayMs);
    return () => clearTimeout(timer);
  }, [delayMs]);
  return visible ? <RunnerLoader className="w-16" label="Loading" /> : null;
}

Full-screen splash

A server-rendered overlay covers the page before JavaScript runs. Hide it after the app hydrates. This is a short version of staff-ui's AppSplash, which also holds the splash for two strides on the first open of each session.

"use client";
import { useEffect, useState } from "react";
import { RunnerLoader } from "@welcome-back/brand";

export function Splash() {
  const [leaving, setLeaving] = useState(false);
  const [gone, setGone] = useState(false);

  useEffect(() => {
    setLeaving(true);
    const timer = setTimeout(() => setGone(true), 300); // fade duration
    return () => clearTimeout(timer);
  }, []);

  if (gone) return null;
  return (
    <div
      role="status"
      aria-label="Loading"
      className={`fixed inset-0 z-50 grid place-items-center bg-background text-foreground
        [--wb-runner-pants:hsl(var(--background))] transition-opacity duration-300
        ${leaving ? "pointer-events-none opacity-0" : ""}`}
    >
      <RunnerLoader className="w-28" />
    </div>
  );
}

Vite / plain React

Same import, no config:

import { RunnerLoader } from "@welcome-back/brand";

function App() {
  const { data, isLoading } = useQuery(/* … */);
  if (isLoading) return <RunnerLoader className="loader" label="Loading" />;
  return <Dashboard data={data} />;
}

Jest and Testing Library

Jest loads the CommonJS build automatically. The SVG always has the class wb-runner, so you can find it in tests:

render(<Loading />);
expect(screen.getByRole("img", { name: "Loading" })).toHaveClass("wb-runner");

POS design tokens

The WelcomeBack POS look: the --wb-* CSS custom properties that pos-ui paints with. The POS is not themable (owner decision, 2026-09-28): every business gets this same look, so nothing overrides these per tenant. The dashboard's POS and KDS previews read the same values, so a preview cannot drift from the register.

The source is src/posTokens.ts. npm run build also writes dist/pos-tokens.css from it (scripts/write-pos-tokens-css.mjs), so the stylesheet and the TS export always agree. Change a value there, never in an app.

In a stylesheet (pos-ui), before any other rule:

@import '@welcome-back/brand/pos-tokens.css';

In React (the dashboard previews):

import { POS_TOKENS } from '@welcome-back/brand';

<div style={POS_TOKENS as React.CSSProperties}>…</div>;
POS_TOKENS['--wb-accent']; // '#26231f'

posTokensCss(selector) returns the same rule for another selector.

POS_WIDE_MIN_WIDTH (600) is the one layout switch: below it is phone / payment terminal, from it on is tablet. It is a number because media queries cannot read a CSS variable; pos-ui uses it as the wide screen in Tailwind.

Dark

POS_DARK_TOKENS holds a dark value for every colour token (Paper F4, direction A «Warm night», owner 2026-10-21); sizes, radius and type do not change. The generated pos-tokens.css also carries the rule .theme-dark { … }, so an app turns dark by putting POS_DARK_CLASS (theme-dark) on <body>. Dark is a per-device setting, never automatic. CATEGORY_PALETTE_DARK has the same 8 colours, slot for slot, and passes the same contrast test on the dark card.

On media

Square tiles show white text on a photo or on a category colour. --wb-on-media / --wb-on-media-muted (text), --wb-media-ink (badge fill), --wb-media-veil (sold-out wash) and --wb-media-scrim (the bottom gradient) are the same in light and dark. The scrim goes on the text block only, never the whole tile: a band behind the text (white text >= 4.7:1 even on a pure white photo) that fades out over the block's 28 px top padding.

Category colours

CATEGORY_PALETTE (src/categoryPalette.ts) is the 8-colour set of the POS (set F2, owner 2026-10-21). Each colour has a swatch, a tile tint, a photo slot and an ink for the initials; they are also in POS_TOKENS as --wb-cat-1 … --wb-cat-8 (+ -tint, -slot, -ink).

A category stores its slot number (1–8), never a hex. categorySlotForIndex(i) gives the automatic slot in tree order (1…8, 1…), so neighbours never share a colour; DEFAULT_CATEGORY_SLOT (8, Piedra) is the neutral one. The test checks the contrast rules for every colour. Design: Paper 01M5FB9SPXPPK51CYD7VT2GP6F, F2.

Product image frames

The frames a product photo is shown in: 1:1, 4:3, 3:2, 16:9. The dashboard cropper saves one square photo. The digital menu, the kiosk and the POS show it centered with object-cover in one of these frames, and the cropper draws a guide for each one. So what the owner crops is what each surface shows.

A surface that shows a product photo in a frame outside this list breaks that promise. Add the frame here, or use one that is already here.

import {
  productImageAspectRatio,
  productImageKeptArea,
} from '@welcome-back/brand';

<div style={{ aspectRatio: productImageAspectRatio('4:3') }}>…</div>;
productImageKeptArea('16:9'); // { width: 1, height: 0.5625 } of the square, centered

PRODUCT_IMAGE_FRAMES holds the raw { width, height } of each frame.

Accessibility

  • Give exactly one accessible name per loading state. Either put label on the runner, or put role="status" and aria-label on its wrapper and leave label out. Do not do both, or screen readers announce it twice.
  • Translate the label. Pass it from your i18n layer, for example label={t("loading")}.
  • Reduced motion is built in. With prefers-reduced-motion: reduce the runner holds one pose (legs apart, mid-stride), so the loading state is still visible.

Where to use it, and where not

| Use it | Do not use it | |---|---| | App open and splash screens | Inside buttons: at button size the legs cannot be read | | Moves to the next step (after a scan, before a customer page) | Pages where the customer sees a business's own brand first (enrollment, card download): our W would lead over their brand | | Route and Suspense fallbacks for a full page or panel | Every small wait: if it is everywhere, staff stop noticing it |

A skeleton is better when the layout that is coming is known: it shows where the content will appear.

Troubleshooting

| Symptom | Cause | Fix | |---|---|---| | The legs show a light or dark rectangle | --wb-runner-pants does not match the surface behind the runner | Set it to that surface color | | The runner is invisible | Ink is the same color as the background | Set color / a text-* class | | The runner does not move | The OS asks for reduced motion | Nothing to fix; this is intended | | The runner fills the whole container | No width is set | Give it a width (w-24, or width in CSS) | | A Tailwind arbitrary property has no effect | Tailwind does not scan the file | Check the content globs in the Tailwind config |

Develop

Repo layout

| Path | What it holds | |---|---| | src/index.ts | Public exports. Only what is exported here is part of the API. | | src/RunnerLoader.tsx | The component: SVG structure, clip paths, props | | src/runnerCss.ts | The animation: keyframes, timing, swing angles, reduced motion | | src/runnerArt.ts | Vector data: paths, pants fills, clip polygons | | src/*.test.tsx | Vitest tests (server render, no browser) | | scripts/mark-cjs.mjs | Marks dist/cjs as CommonJS after the build | | .github/workflows/ci.yml | Runs on every PR and on main | | .github/workflows/publish.yml | Runs on a v* tag and publishes to npm |

Commands

Node 22 or later.

npm ci             # install
npm run typecheck  # tsc, no output
npm test           # vitest
npm run build      # dist/esm (ESM + types) and dist/cjs (CommonJS)

CI runs all four, then loads both builds to prove that import and require work.

Try a change in an app before you publish

Pack the package and install the tarball in the app:

# in brand/
npm run build && npm pack          # writes welcome-back-brand-<version>.tgz

# in the app, for example staff-ui/
npm i ../brand/welcome-back-brand-<version>.tgz

Undo it in the app with npm i @welcome-back/brand@latest. Do not commit the tarball path in the app's package.json.

Prefer npm pack to npm link. A linked package can load a second copy of React, and then hooks fail with "Invalid hook call".

Common changes

| To change | Edit | Note | |---|---|---| | Speed | --wb-runner-step-default in src/runnerCss.ts | Apps can already override it per use with --wb-runner-step | | Stride length (swing angles) | @keyframes wb-runner-swing-front / -back in src/runnerCss.ts | Angles are measured from each leg drawing's rest pose. The legs cross at -36deg (front) and 39deg (back). The swings are not equal: they are tuned so both shoes reach the same spot forward and back. The crossing keyframes sit at 17% / 83% so each leg moves at an even speed; if you change an end angle, check that both still hold. | | Where the legs join the body | @keyframes wb-runner-hip-front / -back | Values are in the art's coordinates (the 1500×1500 PDF page). The front hip rises at the crossing so its shoe stays inside the viewBox. | | What is hidden behind the W | LEGS_CLIP / BODY_CLIP in src/runnerArt.ts | See Source art | | A new prop | src/RunnerLoader.tsx, plus a test and a row in the props table | | | A new asset (logo, icon, token) | A new file in src/, exported from src/index.ts, plus a test and a README section | |

After a visual change, check it in a real app with npm pack (above) in light mode, dark mode and with reduced motion on.

Publish a change

Every change reaches npm through a PR and a tag. Nobody publishes from a laptop, and there is no npm token: GitHub Actions publishes with npm trusted publishing.

1. Pick the version

The package follows semver. Apps install it with ^, so they get minor and patch releases on their next install.

| Change | Bump | Example | |---|---|---| | Docs, tests, or a fix with no visible change | patch | 0.1.2 → 0.1.3 | | A new prop, asset or CSS variable; a visual tweak that keeps the API | minor | 0.1.3 → 0.2.0 | | A removed or renamed prop, export or CSS variable | major | 0.9.0 → 1.0.0 |

While the version is 0.x, npm treats a minor bump as breaking for ^ ranges. Apps then need an explicit update to get it.

2. Open a PR with the change and the new version

git switch main && git pull
git switch -c <type>/<short-name>
# make the change
npm version <patch|minor|major> --no-git-tag-version   # edits package.json and package-lock.json
git commit -am "<type>: <what changed> (v<new version>)"
git push -u origin HEAD
gh pr create --base main

CI must pass before you merge. Squash-merge the PR.

3. Tag the merge commit

git switch main && git pull
git tag v<new version>          # must equal "version" in package.json
git push origin v<new version>

The tag starts the Publish workflow. It:

  1. checks that the tag equals version in package.json,
  2. runs typecheck, tests and build (prepublishOnly),
  3. publishes to npm through trusted publishing.

Watch it with gh run watch, or in the repo's Actions tab.

4. Check npm and update the apps

npm view @welcome-back/brand version     # the new version, after a few minutes

A new version can take up to about 3 minutes to show on the registry. Then, in each app:

npm i @welcome-back/brand@latest

If the publish fails

| Error in the workflow log | Cause | Fix | |---|---|---| | Tag must match package.json version step fails | The tag and version differ | Delete the tag (git push origin :v<x> and git tag -d v<x>), fix the version in a PR, tag again | | 403 … OIDC permission denied | The trusted publisher on npmjs.com does not allow direct publish, or its fields do not match | npmjs.com → package → Settings → Trusted Publisher: WelcomeBackLoyalty / brand / publish.yml, no environment, Allow npm publish ticked | | 403 … cannot publish over the previously published versions | That version is already on npm | Bump the version in a new PR and tag again. npm never allows a republish of a version. | | Typecheck or test fails | The tagged commit is broken | Fix it in a PR, bump the version, tag again |

To rerun a failed publish without a new tag: gh run rerun <run-id> --failed.

npm settings (for maintainers)

  • Org: welcome-back. Package access: public.
  • Trusted publisher: GitHub Actions, WelcomeBackLoyalty/brand, publish.yml, no environment, Allow npm publish ticked.
  • No provenance: npm cannot generate it for a private GitHub repo.

Source art

src/runnerArt.ts comes from page 1 of KitDeMarca/Isotipo/Ai PDF/Orig_Isotipo.pdf (converted with pdftocairo -svg, 1-decimal paths). The source draws both pants tops into the W and merges the ground line into the front shoe, so hand-traced clip polygons trim them. The comments in that file explain each one.