Loyalty Patternsv1.00

Installation

There is no package. Distribution is this repository: take the folders you need, and they are yours to edit.

That is a deliberate choice with a real cost: you do not get updates by bumping a version. What you get instead is a component you can change without fighting a maintainer's abstraction. For UI this brand-specific, that is the right trade.

Requirements

React 18 or later. That is the only runtime dependency. Components ship plain CSS files, so no build plugin, CSS-in-JS runtime or Tailwind installation is required.

Copy the files

bash
git clone https://github.com/mrcl-st/loyalty-patterns

Take two things:

  1. registry/tokens.css — the token defaults. Once, for the project.
  2. registry/<component>/ — the folder for each component you want.

A component folder plus tokens.css is a working install, and that is the whole repository: components, their engines, their styles and their documentation. There is nothing to install and nothing to run — no dependencies to add, no dev server, no build step of its own. The repository is a source you copy from, not an app you clone and start.

Which is why the docs and the playground are here on this site rather than in the repository. What you see running on these pages is a separate application; what you take away is the folder.

Wire it up

Import the tokens once, near your global styles:

tsx
import "./tokens.css";

Then import the component's own stylesheet and the component:

tsx
import "@/components/scratch-card/styles.css";
import { ScratchCard } from "@/components/scratch-card/scratch-card";

Both stylesheets are plain CSS. If your bundler handles a CSS import, and Next.js, Vite, CRA and Remix all do, you are finished.

The one rule that matters

Components never decide outcomes. Pass a result your server already decided:

tsx
<ScratchCard
  getOutcome={() => fetch("/api/draw", { method: "POST" }).then((r) => r.json())}
  renderOutcome={(o) => (o.kind === "win" ? <Prize o={o} /> : <NoWin />)}
  labels={{
    hint: "Scratch to reveal",
    revealButton: "Reveal without scratching",
    announce: (o) => (o.kind === "win" ? "You won 500 points." : "No win this time."),
    resolving: "Checking your result",
    error: "We couldn't check your result.",
    retry: "Try again",
  }}
/>

getOutcome is called once, when the member starts interacting, so the network round-trip hides inside the gesture. If the result is already known — a restored session, a prior play — pass outcome instead.

All copy arrives through labels, including the screen-reader announcement. There are no hardcoded strings to find and translate later.

Updating

Copy the folder again and re-apply your edits, or diff against the repository. Because you own the files, an update is a merge you control rather than a version bump that changes behaviour you did not ask about.

Component folders are self-contained on purpose, so a diff never spans the project.