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

@zappie/foundations

v0.9.1

Published

UI-less React primitives for Zappie storefronts: behaviour, state and accessibility without styles.

Readme

@zappie/foundations

Unstyled React parts for storefronts. They handle the behaviour, state and accessibility of commerce UI: overlays, item options, quantities, the cart and its totals, fulfilment, addresses, schedules, search, payment methods, one-time codes, images, money and opening hours. You bring the styles.

npm install @zappie/foundations @zappie/sdk react react-dom

React 18.2 or 19 is a peer dependency.

Use it

The parts read Zappie data through @zappie/sdk, so wrap them in its ZappieProvider. Each part renders a plain element you can style, or takes a render function:

import type { Branch } from "@zappie/sdk";
import { OpenStatus } from "@zappie/foundations";

export function Hours({ branch }: { branch: Branch }) {
  return (
    <OpenStatus branch={branch} className="hours">
      {(status) => status.label}
    </OpenStatus>
  );
}

The label reads like "Open until 10pm", "Opens 8am tomorrow" or "Closed", and it updates when the branch opens or closes. Parts expose their state as data-* attributes, such as data-state="open", so you can style each state in CSS.

Want ready-made, styled components instead? Copy them into your project from the kit with npx zappie add <name>.

Entry points

| Import | For | | ------------------------------- | --------------------------------------------------------------- | | @zappie/foundations | Every part | | @zappie/foundations/next | Keeping overlays in step with Back and Forward in a Next.js app | | @zappie/foundations/rich-text | Sanitising and parsing merchant rich text before you render it |

Docs

Status

Foundations is at 0.x. Its API can change in a minor release while it settles. Each change is listed in CHANGELOG.md.

License

MIT

Deferred overlays

Keep a bag, branch finder or search dialog off the first load without React.lazy. React holds a suspended boundary's first reveal for about 300 ms, so a lazy overlay opens late. deferred() loads the code on demand and renders on the frame it arrives. It never suspends.

import { deferred, useIntentPreload } from "@zappie/foundations";

const BagSheet = deferred(() => import("./bag-sheet"));
// Or a named export: deferred(() => import("./finder").then((m) => m.Finder))

function Header() {
  const [open, setOpen] = useState(false);
  const intent = useIntentPreload(BagSheet);
  return (
    <>
      <button {...intent} onClick={() => setOpen(true)}>
        Bag
      </button>
      <BagSheet.Component open={open} onClose={() => setOpen(false)} />
    </>
  );
}
  • deferred(load) takes a function that imports a module whose default export is the component, or that resolves to the component. Create it once at module scope.
  • Component renders nothing until open is first true and the code is in. When open turns true it loads the code and renders as soon as it arrives. After that first open the component stays mounted and receives every prop, open included, so it can animate out and return focus when it closes.
  • Pass fallback to hold an inline part's place while its code loads, for example a placeholder label. It shows only while open is true and the code is not in yet, and it never suspends.
  • Pass failed to show something when the code fails to load, such as a message with a button that calls retry. It shows only while open is true. Without it nothing shows, so a shopper who taps and sees nothing never knows to tap again. A later preload(), such as the one the next tap on the trigger starts, also retries, and the component renders when it succeeds.
  • preload() starts the load and returns its promise. Calls share one load. A failed load is forgotten, so the next preload() or the next open tries again.
  • preloadWhenIdle({ timeout? }) loads the code when the browser is idle and returns a function that cancels it. Use it only for small chunks. It does nothing on the server.
  • useIntentPreload(target) returns { onPointerEnter, onPointerDown, onFocus } for the trigger, so the code is usually in by the click.

Importing the helper never touches window. On the server Component renders nothing, and it works under StrictMode.

The first overlay you open on a page paints on the tap. Its backdrop and panel, with the content, show in the engine's markup straight away, and the overlay's focus handling, scroll lock and dismissal take over on the next frame. Until then the panel is hidden from assistive technology and takes no keys, presses or scrolls, apart from Escape and an outside press, which close it. Style the panel and [data-zappie-backdrop] as usual: both frames match them.

Item form focus

When an add fails validation, ItemForm.Root focuses the first invalid group with preventScroll, so the page doesn't jump. Pass onInvalid(groups) to scroll it into view yourself. groups lists the invalid groups in display order, each with its groupId, its group (undefined for a group the template renders from its own model), its element and its issues.

<ItemForm.Root
  item={item}
  onInvalid={(groups) =>
    groups[0]?.element?.scrollIntoView({ behavior: "smooth", block: "center" })
  }
>

Branch map

BranchMap accepts SDK Branch[] data, selectedBranchId, onSelect(branch), browserKey, and optional shopperLocation: { lat, lng }. Use maxZoom to limit the zoom after fitting, pinImageUrl to show an image in each branch pin, mapTypeControl to show the map and satellite switch, and pinLabel to show the branch name above the selected pin ("selected") or every pin ("all"). It also accepts standard div attributes such as className and style.

import { BranchMap } from "@zappie/foundations";

<BranchMap
  branches={branches}
  selectedBranchId={selectedBranchId}
  onSelect={(branch) => setSelectedBranchId(branch.id)}
  browserKey={integrations.googleMaps?.browserKey}
  shopperLocation={shopperLocation}
  pinImageUrl={store.brand.logoUrl}
  pinLabel="selected"
  mapTypeControl
  onLoadError={(error) => report(error)}
  renderLoadError={({ retry }) => (
    <div role="status">
      <p>Map didn't load</p>
      <button type="button" onClick={retry}>
        Try again
      </button>
    </div>
  )}
  className="branch-map"
/>;

Set a height on .branch-map. With a pinImageUrl, such as the business logo, each branch pin is the logo in a white ring: 32 px, or 40 px for the focused branch. The focused branch is the selected one or, once a shopperLocation is known, the nearest. Without a pinImageUrl, pins are dots themed with --branch-map-pin, --branch-map-pin-selected, --branch-map-pin-border and --branch-map-pin-size. Pins expose data-selected, data-focused and aria-pressed. Theme pin labels with --branch-map-label-background, --branch-map-label-color and --branch-map-label-border. Labels are marked data-branch-map-pin-label and hidden from assistive technology, because the pin already carries the name. pinLabel="selected" labels the focused pin.

With a shopperLocation, the map shows a house marker labelled “You” and draws a line from it to the focused branch. Nearby branches group into numbered clusters. Clicking a cluster zooms into it, and selecting a branch inside a cluster zooms in until its pin shows. Colour the clusters, the house marker, the line and the label borders with --map-accent, and the cluster count and house icon with --map-accent-foreground. The map reads these when it updates, because Maps draws clusters and lines without CSS.

The map fits branches with valid coordinates, including across the date line. Coordinates of (0, 0) count as missing. With a shopperLocation, it fits the shopper and the focused branch with 88 px of padding, and fits again when the focused branch changes. Without one, it fits every branch with 64 px of padding and a new selection pans to that branch. A single point is centred at zoom 14. compact frames a small map: the route gets 72 px of padding above, 52 px at the sides and 36 px below, and a single point is centred at zoom 13. The map fits again only when what it frames or maxZoom changes. Branches without coordinates remain selectable with “Map location unavailable”. maxZoom defaults to 15 with a shopper location and 14 without one. mapTypeControl defaults to false.

The component renders nothing without a key. It loads its map runtime and the Maps JavaScript API after mount, sharing one script across mounted maps. Use one browser key per page. A loading failure calls onLoadError(error). With renderLoadError({ error, retry }) you render the message, and retry loads the map again. Without it the map shows “Map location unavailable”. The optional location button pans to coordinates supplied by your app. Your app requests geolocation permission and supplies those coordinates.

Delivery map

DeliveryMap shows where an order goes: the branch logo marker, the shopper's house marker and the line between them, fitted to all of it. Use it in a cart or checkout in place of a static map image. It is a preview, so it has no controls and ignores gestures. Pass route, such as a decoded driving route, to draw that path instead of a straight line.

import { DeliveryMap } from "@zappie/foundations";

<DeliveryMap
  branch={branch}
  shopper={deliveryAddress.coordinates}
  browserKey={integrations.googleMaps?.browserKey}
  pinImageUrl={store.brand.logoUrl}
  renderLoadError={({ retry }) => (
    <button type="button" onClick={retry}>
      Try again
    </button>
  )}
  className="delivery-map"
/>;

Set a height on .delivery-map. The default storefront uses 180 px. The branch marker is a 36 px logo, the house marker uses --map-accent and the line uses --delivery-map-route at 60% opacity. Without a pinImageUrl the branch marker is the themed dot. Like BranchMap, it renders nothing without a key, loads Maps after mount, calls onLoadError(error) on a failure and shows “Map location unavailable” when either point has no valid coordinates. It fits again when the points, the route or its size change.