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

afto-ui

v1.2.6

Published

Shared UI components for AFTO e-commerce storefronts

Readme

afto-ui

Shared React UI component library for AFTO e-commerce storefronts.

Published on npm as afto-ui. Drop it into any AFTO-powered React app and get production-ready, fully-themed components with zero setup.


Table of Contents

  1. What is this library?
  2. How it works (architecture)
  3. Installation & peer dependencies
  4. Available components
  5. How to use components in your app
  6. Creating a new component
  7. Building the library
  8. Publishing to npm
  9. Project file structure explained
  10. Theming / design tokens
  11. Triggering DealsPopup manually
  12. Common mistakes for beginners

1. What is this library?

afto-ui is an npm component library — a collection of reusable React components that you install once and use across multiple projects.

Think of it like a LEGO kit:

  • This repo contains the moulds (component source code).
  • When you npm run build, it creates the plastic pieces (compiled JS in /dist).
  • Other apps install the package from npm and just use the pieces.

Current components (v1.1.0):

| Component | Purpose | |---|---| | DealsPopup | Smart popup showing coupons, loyalty rewards, and points balance | | StoreSelectionModal | Modal for picking a store location before shopping |


2. How it works (architecture)

afto-ui/
│
├── src/                       ← Source code (what you edit)
│   ├── index.js               ← Entry point — re-exports everything
│   ├── DealsPopup.jsx         ← Deals/coupons popup component
│   ├── StoreSelectionModal.jsx← Store picker modal component
│   └── assets.js              ← Embedded assets (base64 coin icon, etc.)
│
├── dist/                      ← Built output (what npm ships)
│   ├── afto-ui.es.js          ← ES Module format (modern bundlers)
│   └── afto-ui.umd.js         ← UMD format (legacy / CDN)
│
├── vite.config.js             ← Build configuration (library mode)
└── package.json               ← npm metadata, peer deps, scripts

Build pipeline

src/index.js
     │
     ▼
  Vite (library mode)
     │
     ├── afto-ui.es.js   ← imported by Next.js / Vite apps
     └── afto-ui.umd.js  ← fallback for older setups

Vite bundles all .jsx files together but excludes peer dependencies (react, react-dom, lucide-react). This keeps the package small — the consumer app provides those.


3. Installation & peer dependencies

npm install afto-ui

You also need these peer dependencies in your app (they are NOT bundled):

npm install react react-dom lucide-react

Why peer dependencies? If the library bundled React inside itself, your app would end up with two copies of React — this causes bugs. Peer deps let both the library and the app share the same single React instance.


4. Available components

DealsPopup

A smart promotional popup that appears automatically when a shopper scrolls down 20% of the page. It shows:

  • Coupons tab — coupon codes with discount labels; eligible ones can be applied in one tap
  • Rewards tab — loyalty reward offers redeemable with points
  • Points banner — displays the user's available loyalty points
  • Login nudge — prompts guest users to sign in to unlock rewards

Auto-show behaviour:

  • Waits until user scrolls ≥ 20% of the page
  • Then waits an additional 20 seconds (POPUP_DELAY_MS)
  • Only shows once every 12 hours (COOLDOWN_HOURS) — stored in localStorage
  • Never shows on /cart, /checkout, /payment, /order, /account

Props:

| Prop | Type | Required | Description | |---|---|---|---| | colors | object | yes | UI color tokens (see Theming) | | theme | object | yes | Brand color config { brand: { DEFAULT: "#hex" } } | | isLoggedIn | boolean | yes | Whether the current user is authenticated | | pathname | string | yes | Current URL path (from router) | | openCart | () => void | no | Called when user clicks "Shop & Apply Deals" | | openAuthModal | () => void | no | Called when guest user clicks login nudge | | getCoupons | () => Promise | no | Fetches coupons; resolves to { data: { coupons: [] } } | | getOffers | () => Promise | no | Fetches loyalty rewards; resolves to { data: { offers: [] } } | | getLoyaltyDashboard | () => Promise | no | Fetches points; resolves to { data: { availablePoints: number } } | | applyOffer | (orderId, offerId) => Promise | no | Applies a loyalty offer to a cart | | addToCart | (productId, qty) => Promise | no | Adds a product to cart; resolves to { success, orderId } |

Coupon object shape:

{
  id: "coupon_123",
  code: "SAVE20",
  name: "Save 20% on your order",
  discountType: "percentage",   // "percentage" | "flat" | "buy_x_get_y" | "free_delivery" | "free_gift"
  discountValue: 20,
  minOrderValue: 30,
  maxDiscountAmount: 15,
  previewDiscount: 8.50,        // how much the user saves right now
  eligible: true,               // false = locked/greyed out
  eligibleReason: null,         // "min_order_value_not_met" | "no_eligible_items" | etc.
}

Offer (reward) object shape:

{
  id: "offer_456",
  offerName: "Free Chocolate Cake",
  pointsCost: 500,
  discountPercent: 100,         // 100 = free item
  dollarValue: 12.00,
  targetId: "product_789",      // product to add to cart
  imageUrl: "https://...",
  canAfford: true,              // false = not enough points
}

StoreSelectionModal

A fullscreen/modal overlay for picking a store before shopping. On mobile it slides up from the bottom; on desktop it centres as a modal.

Props:

| Prop | Type | Required | Description | |---|---|---|---| | isOpen | boolean | yes | Whether the modal is visible | | stores | array | yes | List of store objects | | isLoading | boolean | no | Shows a spinner instead of the list | | onSelectStore | (id: string) => void | yes | Called when user picks a store | | themeConfig | object | no | Theme overrides (see Theming) |

Store object shape:

{
  id: "store_001",
  name: "Downtown Branch",
  logo: "https://...",          // optional — falls back to initials avatar
  address: "123 Main St",       // optional
  timezone: "America/New_York", // optional
}

5. How to use components in your app

Next.js (App Router) example

// app/layout.jsx  or any client component
"use client";

import { useState } from "react";
import { usePathname } from "next/navigation";
import { DealsPopup } from "afto-ui";
import { api } from "@/lib/api"; // your own API helper

export default function RootLayout({ children }) {
  const pathname = usePathname();

  return (
    <html>
      <body>
        {children}

        <DealsPopup
          colors={{
            bgPrimary:     "#ffffff",
            bgSecondary:   "#f8fafb",
            border:        "#e5e7eb",
            textPrimary:   "#0f1419",
            textSecondary: "#6b7280",
          }}
          theme={{ brand: { DEFAULT: "#6366f1" } }}
          isLoggedIn={true}
          pathname={pathname}
          openCart={() => router.push("/cart")}
          openAuthModal={() => setShowLogin(true)}
          getCoupons={() => api.get("/coupons")}
          getOffers={() => api.get("/loyalty/offers")}
          getLoyaltyDashboard={() => api.get("/loyalty/dashboard")}
          applyOffer={(orderId, offerId) =>
            api.post(`/orders/${orderId}/apply-offer`, { offerId })
          }
          addToCart={(productId, qty) =>
            api.post("/cart/add", { productId, qty })
          }
        />
      </body>
    </html>
  );
}

Store Selection example

"use client";

import { useState, useEffect } from "react";
import { StoreSelectionModal } from "afto-ui";

export default function App() {
  const [stores, setStores]     = useState([]);
  const [loading, setLoading]   = useState(true);
  const [storeId, setStoreId]   = useState(null);

  useEffect(() => {
    fetch("/api/stores")
      .then(r => r.json())
      .then(data => { setStores(data); setLoading(false); });
  }, []);

  return (
    <>
      {/* rest of your app */}

      <StoreSelectionModal
        isOpen={!storeId}
        stores={stores}
        isLoading={loading}
        onSelectStore={(id) => {
          setStoreId(id);
          localStorage.setItem("selectedStore", id);
        }}
        themeConfig={{
          brand: { DEFAULT: "#059669" },
          bgPrimary: "#ffffff",
          textPrimary: "#0f172a",
          textSecondary: "#6b7280",
          border: "#e5e7eb",
        }}
      />
    </>
  );
}

6. Creating a new component

Follow these exact steps to add a new component to the library.

Step 1 — Create the component file

src/MyNewComponent.jsx
// src/MyNewComponent.jsx
"use client";  // add this if the component uses hooks or browser APIs

export function MyNewComponent({ title, theme, colors }) {
  const brand = theme?.brand?.DEFAULT ?? "#6366f1";

  return (
    <div style={{ backgroundColor: colors?.bgPrimary, padding: 16 }}>
      <h2 style={{ color: colors?.textPrimary }}>{title}</h2>
      <button style={{ backgroundColor: brand, color: "#fff" }}>
        Click me
      </button>
    </div>
  );
}

Rules:

  • Always use named exports (export function) — not default exports.
  • Accept theme and colors props for theming consistency.
  • Use inline styles or CSS-in-JS — do not import external CSS files (they won't bundle correctly).
  • Use lucide-react for icons (it's a peer dependency, so it's already available).
  • Do not import npm packages that are not already peer dependencies — they would inflate the bundle.

Step 2 — Export it from index.js

// src/index.js
export { DealsPopup }         from "./DealsPopup.jsx";
export { StoreSelectionModal } from "./StoreSelectionModal.jsx";
export { MyNewComponent }      from "./MyNewComponent.jsx";   // ← add this line

Step 3 — Build

npm run build

The new component is now in dist/afto-ui.es.js and ready to publish.


7. Building the library

# One-time build
npm run build

# Watch mode (rebuild on every file save — useful during development)
npm run dev

Build output goes to dist/. The package.json "files" field ensures only dist/ is shipped to npm — source files and node_modules are excluded.

How Vite builds in library mode

vite.config.js uses Vite's library mode:

build: {
  lib: {
    entry: "src/index.js",  // start here
    name: "AftoUI",          // global variable name for UMD
    fileName: (format) => `afto-ui.${format}.js`,
  },
  rollupOptions: {
    external: ["react", "react-dom", "react/jsx-runtime", "lucide-react"],
    // ↑ These are NOT bundled — consumers provide them
  },
}

Result:

  • dist/afto-ui.es.js — tree-shakeable ES module (for Next.js, Vite apps)
  • dist/afto-ui.umd.js — self-contained UMD (for scripts, CDN)

8. Publishing to npm

First-time setup

  1. Create an account at npmjs.com
  2. Login in terminal:
    npm login
  3. The .npmrc file stores your auth token for CI/CD:
    //registry.npmjs.org/:_authToken=YOUR_TOKEN_HERE

    Keep this token secret — never commit it to git.

Publishing a new version

# 1. Build latest code
npm run build

# 2. Bump the version (choose one):
npm version patch   # 1.1.0 → 1.1.1  (bug fix)
npm version minor   # 1.1.0 → 1.2.0  (new feature)
npm version major   # 1.1.0 → 2.0.0  (breaking change)

# 3. Publish
npm publish

Semver version guide

| Change type | Example | Command | |---|---|---| | Bug fix, no new features | Fixed coupon copy button | npm version patch | | New component added | Added BannerAlert | npm version minor | | Removed/renamed a prop | Renamed themethemeConfig | npm version major |

After publishing

Consuming apps update with:

npm install afto-ui@latest
# or a specific version:
npm install [email protected]

9. Project file structure explained

afto-ui/
│
├── src/
│   ├── index.js               — Public API of the library.
│   │                            Only things exported here are usable by consumers.
│   │
│   ├── DealsPopup.jsx         — Deals / coupons / loyalty popup.
│   │                            ~980 lines. Contains CouponCard, RewardCard,
│   │                            EmptyState, SectionLabel as internal sub-components.
│   │
│   ├── StoreSelectionModal.jsx— Store picker modal.
│   │                            Contains StoreAvatar as internal sub-component.
│   │
│   └── assets.js              — Static assets embedded as base64 data URIs.
│                                Currently exports LOYALTY_COIN_URI (coin PNG image).
│
├── dist/                      — Auto-generated by `npm run build`. Do not edit manually.
│   ├── afto-ui.es.js          — ES module output
│   └── afto-ui.umd.js         — UMD output
│
├── vite.config.js             — Tells Vite to build in "library mode" instead of app mode.
│
├── package.json               — Package metadata.
│   • name: "afto-ui"          — The npm package name
│   • version: "1.1.0"         — Current published version
│   • main: dist/umd           — Entry for CommonJS (require())
│   • module: dist/es           — Entry for ES modules (import)
│   • files: ["dist"]          — Only dist/ is uploaded to npm
│
├── .npmrc                     — Stores npm auth token for publishing.
│                                The token line is commented out for safety.
│
└── .npmignore                 — Files excluded from the npm package.
                                 (src/, node_modules/, vite.config.js stay local)

10. Theming / design tokens

Both components accept theme props so they adapt to any brand colour.

colors object (used by DealsPopup)

{
  bgPrimary:     "#ffffff",  // card/modal background
  bgSecondary:   "#f8fafb",  // subtle inner backgrounds, skeletons
  border:        "#e5e7eb",  // dividers, card borders
  textPrimary:   "#0f1419",  // headings, main text
  textSecondary: "#6b7280",  // labels, subtitles, metadata
}

theme object (used by DealsPopup)

{
  brand: {
    DEFAULT: "#6366f1",  // primary brand colour — buttons, tabs, highlights
  }
}

themeConfig object (used by StoreSelectionModal)

Combines both into a single prop:

{
  brand: { DEFAULT: "#059669" },
  bgPrimary:     "#ffffff",
  bgSecondary:   "#f8fafb",
  textPrimary:   "#0f172a",
  textSecondary: "#6b7280",
  border:        "#e5e7eb",
}

11. Triggering DealsPopup manually

The popup auto-shows based on scroll + timer, but you can force it open from anywhere in your app by dispatching a custom window event:

window.dispatchEvent(new Event("openDealsPopup"));

This is useful for:

  • A "See My Deals" button in the header
  • A floating deals badge
  • Triggering after a user logs in

The component resets the hasShown guard when this event fires, so it will always open on demand.


12. Common mistakes for beginners

Mistake 1 — Forgetting to build before publishing

The src/ files are not what npm ships. You must run npm run build first to regenerate dist/. If you skip this, the old compiled code gets published.

Mistake 2 — Adding regular dependencies instead of peer dependencies

If you need to add a new library (e.g., dayjs), add it as a peer dependency in package.json and also list it in rollupOptions.external in vite.config.js. If you add it as a regular dependency, it gets bundled into the output and can conflict with the consumer's own copy.

// package.json
"peerDependencies": {
  "react": ">=18.0.0",
  "react-dom": ">=18.0.0",
  "lucide-react": ">=0.300.0",
  "dayjs": ">=1.0.0"   // ← add here
}
// vite.config.js
external: ["react", "react-dom", "react/jsx-runtime", "lucide-react", "dayjs"]

Mistake 3 — Using default exports

Always use named exports. Default exports make tree-shaking less effective and create import inconsistencies.

// ✅ correct
export function MyComponent() { ... }

// ❌ wrong
export default function MyComponent() { ... }

Mistake 4 — Importing CSS files

// ❌ this will break
import "./MyComponent.css";

// ✅ use inline styles or CSS-in-JS instead
const styles = { color: "red" };

Mistake 5 — Not bumping the version before publishing

npm will reject a publish if the version in package.json is already taken. Always run npm version patch/minor/major before npm publish.

Mistake 6 — The "use client" directive

Both components have "use client" at the top. This is a Next.js App Router directive that tells Next.js the component runs in the browser (not during server-side rendering). It does nothing in non-Next.js projects — safe to leave in.


Quick reference

# Install (in consumer app)
npm install afto-ui

# Start watch build (in this repo)
npm run dev

# Production build (in this repo)
npm run build

# Publish to npm
npm version patch && npm publish
// Import in consumer app
import { DealsPopup, StoreSelectionModal } from "afto-ui";

Afto-Component