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

@andresh11/pay-preview

v0.1.0

Published

Framework-agnostic, tree-shakeable payment UI preview components (Card, Nequi, PSE, Bancolombia). One .lite.tsx source, multi-target (React, Vue, Svelte, Solid, RNative).

Downloads

75

Readme

@andresh11/pay-preview

Componentes de UI agnósticos y tree-shakeables para previsualización de pagos — tarjeta, Nequi, PSE y Bancolombia. Un solo source .lite.tsx, multi-target (React, Vue, Svelte, Solid, React Native). Sin dependencias de runtime.

Framework-agnostic, tree-shakeable payment UI preview components — card, Nequi, PSE, Bancolombia. One source .lite.tsx, multi-target (React, Vue, Svelte, Solid, React Native). Zero runtime dependencies.

License: MIT Tree-shakeable Multi-framework TypeScript strict npm version


📑 Tabla de contenidos / Table of contents


Español

¿Qué es?

Una librería UI de previsualización de pagos para Colombia. No procesa pagos, no llama a APIs, no almacena datos: solo renderiza componentes de UI que muestran cómo se verá la información de pago (tarjeta, Nequi, PSE, Bancolombia) antes de que el usuario confirme.

Está pensada para stayspot, kiddoFamily, kiddoAdmin y cualquier otro frontend que necesite mostrar vistas previas ricas de medios de pago.

Características

  • 🎨 Multi-framework: una sola fuente compila a 5 targets idiomáticos (React, Vue, Svelte, Solid, React Native).
  • 🌳 Tree-shakeable real: importás solo lo que usás. CreditCard solo en React ≈ 11 KB / 4 KB gz.
  • 🚫 Cero dependencias runtime: tu bundle no se infla con nada que no uses. Solo react/react-dom como peer si importás de /react.
  • 🎯 Strategy pattern: validación pura (Luhn, BIN, longitud) extensible para nuevos métodos.
  • 🔒 TypeScript estricto: tipos públicos exportados en cada subpath.
  • 🎨 CSS con variables: personalizá colores, gradientes, glow sin tocar la librería.
  • 📦 Componentes 100% agnósticos: mismo .lite.tsx → 5 frameworks.
  • 🎯 Auto-flip 3D: la tarjeta rota al reverso cuando recibe foco el CVC.
  • 🎯 Detección automática de marca: Visa, Mastercard, Amex, genérica.

Galería visual

Todas las previews en estado funcional con sus datos por defecto:

Tarjeta de crédito/débito — CreditCard

| Marca | Preview | |---|---| | Visa | Visa card preview | | Mastercard | Mastercard card preview | | Amex | Amex card preview | | Genérica | Generic card preview | | Reverso (auto-flip con CVC) | Card back preview |

Billetera y transferencias — Nequi / PSE / Bancolombia

| Método | Preview | |---|---| | NequiPreview | Nequi preview | | PSEPreview | PSE preview | | BancolombiaTransferPreview | Bancolombia preview |

💡 Las previews son reactivas — actualizá los campos del formulario y la previsualización cambia en vivo (esto es lo que muestra el demo).


Instalación

npm install @andresh11/pay-preview react react-dom

react y react-dom son peer dependencies (no se incluyen en el bundle).

Si usás Vue, Svelte o Solid, no necesitás React. Cada target tiene su propio peer (o ninguno, según el framework).


Uso rápido

React (TypeScript)

import { CreditCard } from '@andresh11/pay-preview/react'
import '@andresh11/pay-preview/styles/credit-card.css'

export const PaymentScreen = () => (
  <CreditCard
    number="4111 1111 1111 1111"
    cardHolder="Andres Hernandez"
    expMonth="12"
    expYear="28"
    cvc="123"
  />
)

Vue 3

<script setup>
import { CreditCard } from '@andresh11/pay-preview/vue'
import '@andresh11/pay-preview/styles/credit-card.css'
</script>

<template>
  <CreditCard
    number="4111 1111 1111 1111"
    cardHolder="Andres H"
    expMonth="12"
    expYear="28"
    cvc="123"
  />
</template>

Svelte

<script>
  import { CreditCard } from '@andresh11/pay-preview/svelte'
  import '@andresh11/pay-preview/styles/credit-card.css'
</script>

<CreditCard
  number="4111 1111 1111 1111"
  cardHolder="Andres H"
  expMonth="12"
  expYear="28"
  cvc="123"
/>

Solid

import { CreditCard } from '@andresh11/pay-preview/solid'
import '@andresh11/pay-preview/styles/credit-card.css'

export const PaymentScreen = () => (
  <CreditCard
    number="4111 1111 1111 1111"
    cardHolder="Andres H"
    expMonth="12"
    expYear="28"
    cvc="123"
  />
)

Vue, Svelte y Solid requieren bundler con los plugins oficiales para procesar los SFC. Vite los trae por defecto. Si usás webpack o Rollup necesitás el plugin correspondiente.


Componentes disponibles

| Componente | Descripción | Tipo de preview | |---|---|---| | CreditCard | Tarjeta Visa/Mastercard/Amex/genérica con flip 3D | Tarjeta física | | PaymentCatalog | Lista radio accesible de métodos | Selector | | NequiPreview | Número de celular Nequi | Billetera digital | | PSEPreview | Banco + datos del pagador | Débito ACH | | BancolombiaTransferPreview | Cuenta + tipo + titular | Transferencia |

Ver las screenshots en la galería visual para ver cada uno en acción.

Tipos de marcas (CardType)

type CardType = 'visa' | 'mastercard' | 'amex' | 'generic'

Si no se pasa brand, CreditCard lo autodetecta por el BIN del número.


Referencia de API

CreditCard

interface ICreditCardProps {
  number: string            // número de tarjeta (con o sin separadores)
  cardHolder: string        // nombre del titular tal como aparece en la tarjeta
  expMonth: string          // mes (1-12) o "01"-"12"
  expYear: string           // año ("28" o "2028")
  cvc: string               // código de seguridad
  isFlipped?: boolean       // forzar reverso (default: heurística por cvc)
  brand?: CardType          // forzar marca (default: autodetección)
  genericBrandLabel?: string  // texto cuando brand === 'generic' (default: "STAYSPOT")
}
Auto-flip

El reverso de la tarjeta (donde aparece el CVC) se muestra automáticamente cuando se tipea el CVC. Si querés forzar el flip manualmente:

const [showBack, setShowBack] = useState(false)
<CreditCard ... cvc="123" isFlipped={showBack} />

Si pasás isFlipped, esa gana sobre la heurística.

Reverso de la tarjeta con CVC

Marca automática
// autodetección (Visa si empieza en 4, Mastercard 51-55, Amex 34/37, resto genérica)
<CreditCard number="4111..." ... />

// forzar marca (útil para BINs desconocidos)
<CreditCard brand="generic" number="..." genericBrandLabel="Mi Tienda" ... />

NequiPreview

interface INequiPreviewProps {
  phone: string             // celular Nequi (10 dígitos, solo dígitos)
  genericBrandLabel?: string
}

Muestra el número con formato 300 123 4567 y un glow animado corporativo.

PSEPreview

interface IPSEPreviewProps {
  bankName?: string         // nombre del banco
  userFullName?: string     // nombre del pagador
  userLegalId?: string      // cédula
  userLegalIdType?: string  // tipo (CC, CE, etc.)
  email?: string            // email de confirmación
  genericBrandLabel?: string
}

BancolombiaTransferPreview

type AccountType = 'SAVINGS' | 'CHECKING' | 'AHORROS' | 'CORRIENTE'

interface IBancolombiaTransferPreviewProps {
  accountNumber?: string
  accountType?: AccountType  // default: 'AHORROS'
  holderName?: string
  holderLegalId?: string
  genericBrandLabel?: string
}

PaymentCatalog

type PaymentMethodCode = 'CARD' | 'NEQUI' | 'PSE' | 'BANCOLOMBIA_TRANSFER'

interface IPaymentMethodOption {
  code: PaymentMethodCode
  label: string            // ej: "Tarjeta de crédito"
  description: string      // ej: "Visa, Mastercard, Amex"
  available: boolean       // si está habilitado (gris si false)
}

interface IPaymentCatalogProps {
  methods: IPaymentMethodOption[]
  selected?: PaymentMethodCode
  onSelect?: (code: PaymentMethodCode) => void
  title?: string           // título opcional arriba del listado
}
const methods: IPaymentMethodOption[] = [
  { code: 'CARD', label: 'Tarjeta', description: 'Visa, MC, Amex', available: true },
  { code: 'NEQUI', label: 'Nequi', description: 'Desde tu celular', available: true },
  { code: 'PSE', label: 'PSE', description: 'Débito bancario', available: false },
]

<PaymentCatalog
  methods={methods}
  selected={selected}
  onSelect={setSelected}
  title="Métodos de pago"
/>

Strategy pattern — validación pura

Si necesitás validar campos antes de mandar al backend, la librería trae un validador de tarjeta listo y extensible para otros métodos:

import {
  CardPaymentStrategy,
  validateAndNormalizeCard,
} from '@andresh11/pay-preview/strategies'

const [error, normalized] = validateAndNormalizeCard({
  number: '4111111111111111',
  cardHolder: 'Andres Hernandez',
  expMonth: '12',
  expYear: '28',
  cvc: '123',
})

if (error) {
  console.error(error) // mensaje en español
} else {
  console.log(normalized)
  // {
  //   method: 'CARD',
  //   number: '4111111111111111',
  //   numberFormatted: '4111 1111 1111 1111',
  //   cardHolder: 'Andres Hernandez',
  //   expMonth: '12',
  //   expYear: '28',
  //   cvc: '123',
  //   brand: 'visa',
  // }
}

Detectores standalone (sin Strategy)

import { detectCardBrand, isValidLuhn, hasValidLength } from '@andresh11/pay-preview/core'

detectCardBrand('4111111111111111')           // 'visa'
detectCardBrand('5105105105105100')           // 'mastercard'
detectCardBrand('371449635398431')            // 'amex'
detectCardBrand('6011111111111117')           // 'generic'

isValidLuhn('4111111111111111')               // true
hasValidLength('4111111111111111', 'visa')    // true

Extender con tu propio método

import type {
  IPaymentMethodStrategy,
  IPaymentMethodMetadata,
  TPaymentMethodCode,
  TValidationResult,
} from '@andresh11/pay-preview'
import { PaymentStrategiesRegistry } from '@andresh11/pay-preview/strategies'

class MyCustomStrategy implements IPaymentMethodStrategy<TMyInput, TMyOutput> {
  readonly code: TPaymentMethodCode = 'DAVIPLATA'  // string literal

  validate(raw: TMyInput): TValidationResult {
    return raw.phone.length === 10 ? null : 'Celular inválido'
  }

  normalize(raw: TMyInput): TMyOutput {
    return { method: 'DAVIPLATA', phone: raw.phone }
  }

  getMetadata(): IPaymentMethodMetadata {
    return {
      code: 'DAVIPLATA',
      label: 'Daviplata',
      description: 'Billetera digital',
      enabled: true,
    }
  }
}

const registry = new PaymentStrategiesRegistry()
registry.register(new MyCustomStrategy())

Tree-shaking

| Importación | Tamaño min | Tamaño gz | |---|---|---| | Solo CreditCard (/react) | ~11 KB | ~4 KB | | CreditCard + PaymentCatalog + Nequi (/react) | ~14 KB | ~5 KB | | Root completo (@andresh11/pay-preview) — solo strategies/core | ~3 KB | ~1.5 KB |

El bundler (Vite, esbuild, Rollup) descarta lo que no importaste, incluyendo el CSS si no lo importás.


Personalización con CSS variables

Todos los estilos usan prefijo --pp- (pay-preview). Override en tu CSS global o en un selector contenedor:

:root {
  /* ── CreditCard ── */
  --pp-card-width: 360px;
  --pp-card-height: 210px;
  --pp-card-radius: 16px;
  --pp-card-shadow: 0 8px 24px rgba(15, 23, 42, 0.18);
  --pp-card-flip-duration: 600ms;

  /* ── Marcas ── */
  --pp-brand-visa: linear-gradient(135deg, #ff6b6b, #4ecdc4);
  --pp-brand-mastercard: linear-gradient(135deg, #e96443, #904e95);
  --pp-brand-amex: linear-gradient(135deg, #006fcf, #00b7ff);
  --pp-brand-generic: linear-gradient(135deg, #1c1d22, #5b86e5);

  /* ── Nequi glow ── */
  --pp-nq-glow-1: #2000a0;
  --pp-nq-glow-2: #b900c5;
  --pp-nq-text: #ffffff;

  /* ── Bank method (PSE + Bancolombia) ── */
  --pp-bm-card-bg: #ffffff;
  --pp-bm-card-fg: #0f172a;
  --pp-bm-accent: #4f46e5;
}

Las variables usan fallback en el componente, así que podés definir solo las que necesitás.


Subpath exports

| Path | Qué exporta | Peer deps necesarias | |---|---|---| | @andresh11/pay-preview | Strategies + detectores + tipos | ninguna | | @andresh11/pay-preview/react | Componentes React | react, react-dom | | @andresh11/pay-preview/vue | Componentes Vue | vue (en tu bundler) | | @andresh11/pay-preview/svelte | Componentes Svelte | svelte (en tu bundler) | | @andresh11/pay-preview/solid | Componentes Solid | solid-js | | @andresh11/pay-preview/react-native | Componentes React Native | react, react-native | | @andresh11/pay-preview/core | Detectores standalone | ninguna | | @andresh11/pay-preview/strategies | Strategies + validador de tarjeta | ninguna | | @andresh11/pay-preview/styles/credit-card.css | CSS de CreditCard | ninguna | | @andresh11/pay-preview/styles/nequi.css | CSS de NequiPreview | ninguna | | @andresh11/pay-preview/styles/bank-method.css | CSS de PSE + Bancolombia | ninguna | | @andresh11/pay-preview/styles/catalog.css | CSS de PaymentCatalog | ninguna |


Stack técnico

  • Mitosis 0.14 — una sola fuente .lite.tsx → 5 frameworks.
  • TypeScript estrictostrict: true, noUncheckedIndexedAccess: true.
  • ESLint 8 + eslint-plugin-perfectionist — 0 warnings.
  • Sin runtime dependencies — solo react/react-dom como peer.
  • Cero polyfills — solo APIs nativas que ya están en navegadores modernos.

Desarrollo

npm install
npm run typecheck               # tsc --noEmit
npm run lint                    # eslint --max-warnings 0
npm run build                   # mitosis → tsc → css → copy
npm run dev:demo                # demo con HMR (http://localhost:5173)
npm run build:demo              # bundle de demo (verificación final)
npm run validate:tree-shaking   # verifica que bundle < 30 KB

Estructura

src/
├── core/
│   ├── detectors/              ← detectCardBrand, isValidLuhn, hasValidLength
│   └── formatters/             ← formatCardNumber, formatExpMonth, etc.
├── mitosis/                    ← fuentes .lite.tsx + CSS (generan 5 targets)
│   ├── CreditCard.lite.tsx
│   ├── NequiPreview.lite.tsx
│   ├── PSEPreview.lite.tsx
│   ├── BancolombiaTransferPreview.lite.tsx
│   ├── PaymentCatalog.lite.tsx
│   └── styles/                 ← credit-card, nequi, bank-method, catalog
├── strategies/
│   ├── types/                  ← IPaymentMethodStrategy, PaymentStrategiesRegistry
│   └── card/                   ← CardPaymentStrategy + validateAndNormalizeCard
└── types/                      ← tipos públicos puros

Flujo de build (multi-target)

src/mitosis/*.lite.tsx
        │
        ├── build-mitosis.mjs ──→ 5 targets × N componentes
        │                         (react, vue, svelte, solid, reactNative)
        │
        ├── build-ts.mjs ────────→ dist/{index,core,strategies,types}/*
        │
        ├── build-css.mjs ───────→ dist/styles/*.css (minificado)
        │
        └── copy-mitosis.mjs ────→ dist/{target}/* (bundle por target
                                                 + index.d.ts autogenerado)

copy-mitosis.mjs también genera un index.d.ts por target extrayendo los export interface y export type de cada .lite.tsx, así TypeScript puede tipar las props de los componentes sin tener que procesar JSX de Mitosis.


Notas de compatibilidad

Mitosis 0.14 + Windows

La build de Mitosis en Windows tiene 5 workarounds documentados:

  1. --force rompe el path parser → no usarlo; limpiar src/components/ antes.
  2. --out-dir ignora la extensión correcta con múltiples archivos: invocar mitosis archivo por archivo y capturar stdout.
  3. mitosis.config.json con targets[] NO funciona en Windows.
  4. --state useState (no default useLocalProxy) para emitir código React idiomático sin import { useLocalProxy } from '@builder.io/mitosis'.
  5. "use client" se emite automáticamente al usar useStore() en React: post-procesar y eliminar antes de exponer.

ESLint 8.57

NO soporta ignoreTypeLikeDeclarations ni ignoreFunctionDeclarations (del plugin perfectionist). Se usa ESLint 8.57 sin esas reglas.



English

What is it?

A payment UI preview library for Colombia. It does NOT process payments, does NOT call APIs, does NOT store data: it only renders UI components that show how a payment method will look (card, Nequi, PSE, Bancolombia) before the user confirms.

It's designed for stayspot, kiddoFamily, kiddoAdmin, and any other frontend that needs rich previews of payment methods.

Features

  • 🎨 Multi-framework: one source compiles to 5 idiomatic targets (React, Vue, Svelte, Solid, React Native).
  • 🌳 Real tree-shakeable: import only what you use. CreditCard alone in React ≈ 11 KB / 4 KB gz.
  • 🚫 Zero runtime dependencies: your bundle doesn't grow with anything you don't use. Only react/react-dom as peer if you import from /react.
  • 🎯 Strategy pattern: pure validation (Luhn, BIN, length) extensible for new methods.
  • 🔒 Strict TypeScript: public types exported in each subpath.
  • 🎨 CSS variables: customize colors, gradients, glow without touching the library.
  • 📦 100% agnostic components: same .lite.tsx → 5 frameworks.
  • 🎯 3D auto-flip: the card rotates to its back when CVC receives focus.
  • 🎯 Auto brand detection: Visa, Mastercard, Amex, generic.

Visual gallery

All previews in functional state with default data:

Credit/debit card — CreditCard

| Brand | Preview | |---|---| | Visa | Visa card preview | | Mastercard | Mastercard card preview | | Amex | Amex card preview | | Generic | Generic card preview | | Back (auto-flip with CVC) | Card back preview |

Wallets and transfers — Nequi / PSE / Bancolombia

| Method | Preview | |---|---| | NequiPreview | Nequi preview | | PSEPreview | PSE preview | | BancolombiaTransferPreview | Bancolombia preview |

💡 The previews are reactive — update the form fields and the preview changes live (this is what the demo shows).


Installation

npm install @andresh11/pay-preview react react-dom

react and react-dom are peer dependencies (not bundled).

If you use Vue, Svelte, or Solid, you don't need React. Each target has its own peer (or none, depending on the framework).


Quick start

React (TypeScript)

import { CreditCard } from '@andresh11/pay-preview/react'
import '@andresh11/pay-preview/styles/credit-card.css'

export const PaymentScreen = () => (
  <CreditCard
    number="4111 1111 1111 1111"
    cardHolder="Andres Hernandez"
    expMonth="12"
    expYear="28"
    cvc="123"
  />
)

Vue 3

<script setup>
import { CreditCard } from '@andresh11/pay-preview/vue'
import '@andresh11/pay-preview/styles/credit-card.css'
</script>

<template>
  <CreditCard
    number="4111 1111 1111 1111"
    cardHolder="Andres H"
    expMonth="12"
    expYear="28"
    cvc="123"
  />
</template>

Svelte

<script>
  import { CreditCard } from '@andresh11/pay-preview/svelte'
  import '@andresh11/pay-preview/styles/credit-card.css'
</script>

<CreditCard
  number="4111 1111 1111 1111"
  cardHolder="Andres H"
  expMonth="12"
  expYear="28"
  cvc="123"
/>

Solid

import { CreditCard } from '@andresh11/pay-preview/solid'
import '@andresh11/pay-preview/styles/credit-card.css'

export const PaymentScreen = () => (
  <CreditCard
    number="4111 1111 1111 1111"
    cardHolder="Andres H"
    expMonth="12"
    expYear="28"
    cvc="123"
  />
)

Vue, Svelte, and Solid require the bundler to process SFC files. Vite includes them by default. If you use webpack or Rollup you need the corresponding plugin.


Available components

| Component | Description | Preview type | |---|---|---| | CreditCard | Visa/Mastercard/Amex/generic card with 3D flip | Physical card | | PaymentCatalog | Accessible radio list of methods | Selector | | NequiPreview | Nequi phone number | Digital wallet | | PSEPreview | Bank + payer details | ACH debit | | BancolombiaTransferPreview | Account + type + holder | Transfer |

See the screenshots in the visual gallery to see each one in action.

Card brands (CardType)

type CardType = 'visa' | 'mastercard' | 'amex' | 'generic'

If brand is not passed, CreditCard auto-detects by the BIN of the number.


API reference

CreditCard

interface ICreditCardProps {
  number: string            // card number (with or without separators)
  cardHolder: string        // cardholder name as shown on the card
  expMonth: string          // month (1-12) or "01"-"12"
  expYear: string           // year ("28" or "2028")
  cvc: string               // security code
  isFlipped?: boolean       // force back (default: heuristic by cvc)
  brand?: CardType          // force brand (default: autodetection)
  genericBrandLabel?: string  // text when brand === 'generic' (default: "STAYSPOT")
}
Auto-flip

The back of the card (where CVC is shown) is automatically displayed when CVC is typed. If you want to force the flip manually:

const [showBack, setShowBack] = useState(false)
<CreditCard ... cvc="123" isFlipped={showBack} />

If you pass isFlipped, it wins over the heuristic.

Card back with CVC

Brand auto-detection
// autodetection (Visa if starts with 4, Mastercard 51-55, Amex 34/37, else generic)
<CreditCard number="4111..." ... />

// force brand (useful for unknown BINs)
<CreditCard brand="generic" number="..." genericBrandLabel="My Store" ... />

NequiPreview

interface INequiPreviewProps {
  phone: string             // Nequi cell number (10 digits, no separators)
  genericBrandLabel?: string
}

Shows the number formatted as 300 123 4567 with an animated corporate glow.

PSEPreview

interface IPSEPreviewProps {
  bankName?: string         // bank name
  userFullName?: string     // payer name
  userLegalId?: string      // ID number
  userLegalIdType?: string  // type (CC, CE, etc.)
  email?: string            // confirmation email
  genericBrandLabel?: string
}

BancolombiaTransferPreview

type AccountType = 'SAVINGS' | 'CHECKING' | 'AHORROS' | 'CORRIENTE'

interface IBancolombiaTransferPreviewProps {
  accountNumber?: string
  accountType?: AccountType  // default: 'AHORROS'
  holderName?: string
  holderLegalId?: string
  genericBrandLabel?: string
}

PaymentCatalog

type PaymentMethodCode = 'CARD' | 'NEQUI' | 'PSE' | 'BANCOLOMBIA_TRANSFER'

interface IPaymentMethodOption {
  code: PaymentMethodCode
  label: string            // e.g. "Credit card"
  description: string      // e.g. "Visa, Mastercard, Amex"
  available: boolean       // if enabled (greyed if false)
}

interface IPaymentCatalogProps {
  methods: IPaymentMethodOption[]
  selected?: PaymentMethodCode
  onSelect?: (code: PaymentMethodCode) => void
  title?: string           // optional title above the list
}
const methods: IPaymentMethodOption[] = [
  { code: 'CARD', label: 'Card', description: 'Visa, MC, Amex', available: true },
  { code: 'NEQUI', label: 'Nequi', description: 'From your phone', available: true },
  { code: 'PSE', label: 'PSE', description: 'Bank debit', available: false },
]

<PaymentCatalog
  methods={methods}
  selected={selected}
  onSelect={setSelected}
  title="Payment methods"
/>

Strategy pattern — pure validation

If you need to validate fields before sending to the backend, the library comes with a ready-to-use card validator and an extensible pattern for other methods:

import {
  CardPaymentStrategy,
  validateAndNormalizeCard,
} from '@andresh11/pay-preview/strategies'

const [error, normalized] = validateAndNormalizeCard({
  number: '4111111111111111',
  cardHolder: 'Andres Hernandez',
  expMonth: '12',
  expYear: '28',
  cvc: '123',
})

if (error) {
  console.error(error) // Spanish language error message
} else {
  console.log(normalized)
  // {
  //   method: 'CARD',
  //   number: '4111111111111111',
  //   numberFormatted: '4111 1111 1111 1111',
  //   cardHolder: 'Andres Hernandez',
  //   expMonth: '12',
  //   expYear: '28',
  //   cvc: '123',
  //   brand: 'visa',
  // }
}

Standalone detectors (without Strategy)

import { detectCardBrand, isValidLuhn, hasValidLength } from '@andresh11/pay-preview/core'

detectCardBrand('4111111111111111')           // 'visa'
detectCardBrand('5105105105105100')           // 'mastercard'
detectCardBrand('371449635398431')            // 'amex'
detectCardBrand('6011111111111117')           // 'generic'

isValidLuhn('4111111111111111')               // true
hasValidLength('4111111111111111', 'visa')    // true

Extending with your own method

import type {
  IPaymentMethodStrategy,
  IPaymentMethodMetadata,
  TPaymentMethodCode,
  TValidationResult,
} from '@andresh11/pay-preview'
import { PaymentStrategiesRegistry } from '@andresh11/pay-preview/strategies'

class MyCustomStrategy implements IPaymentMethodStrategy<TMyInput, TMyOutput> {
  readonly code: TPaymentMethodCode = 'DAVIPLATA'  // string literal

  validate(raw: TMyInput): TValidationResult {
    return raw.phone.length === 10 ? null : 'Invalid phone'
  }

  normalize(raw: TMyInput): TMyOutput {
    return { method: 'DAVIPLATA', phone: raw.phone }
  }

  getMetadata(): IPaymentMethodMetadata {
    return {
      code: 'DAVIPLATA',
      label: 'Daviplata',
      description: 'Digital wallet',
      enabled: true,
    }
  }
}

const registry = new PaymentStrategiesRegistry()
registry.register(new MyCustomStrategy())

Tree-shaking

| Import | Min size | Gz size | |---|---|---| | CreditCard only (/react) | ~11 KB | ~4 KB | | CreditCard + PaymentCatalog + Nequi (/react) | ~14 KB | ~5 KB | | Full root (@andresh11/pay-preview) — strategies/core only | ~3 KB | ~1.5 KB |

The bundler (Vite, esbuild, Rollup) discards what you don't import, including the CSS if you don't import it.


CSS variables customization

All styles use the --pp- prefix (pay-preview). Override in your global CSS or in a container selector:

:root {
  /* ── CreditCard ── */
  --pp-card-width: 360px;
  --pp-card-height: 210px;
  --pp-card-radius: 16px;
  --pp-card-shadow: 0 8px 24px rgba(15, 23, 42, 0.18);
  --pp-card-flip-duration: 600ms;

  /* ── Brands ── */
  --pp-brand-visa: linear-gradient(135deg, #ff6b6b, #4ecdc4);
  --pp-brand-mastercard: linear-gradient(135deg, #e96443, #904e95);
  --pp-brand-amex: linear-gradient(135deg, #006fcf, #00b7ff);
  --pp-brand-generic: linear-gradient(135deg, #1c1d22, #5b86e5);

  /* ── Nequi glow ── */
  --pp-nq-glow-1: #2000a0;
  --pp-nq-glow-2: #b900c5;
  --pp-nq-text: #ffffff;

  /* ── Bank method (PSE + Bancolombia) ── */
  --pp-bm-card-bg: #ffffff;
  --pp-bm-card-fg: #0f172a;
  --pp-bm-accent: #4f46e5;
}

Variables use fallback in the component, so you only need to define the ones you want.


Subpath exports

| Path | Exports | Peer deps | |---|---|---| | @andresh11/pay-preview | Strategies + detectors + types | none | | @andresh11/pay-preview/react | React components | react, react-dom | | @andresh11/pay-preview/vue | Vue components | vue (in your bundler) | | @andresh11/pay-preview/svelte | Svelte components | svelte (in your bundler) | | @andresh11/pay-preview/solid | Solid components | solid-js | | @andresh11/pay-preview/react-native | React Native components | react, react-native | | @andresh11/pay-preview/core | Standalone detectors | none | | @andresh11/pay-preview/strategies | Strategies + card validator | none | | @andresh11/pay-preview/styles/credit-card.css | CreditCard CSS | none | | @andresh11/pay-preview/styles/nequi.css | NequiPreview CSS | none | | @andresh11/pay-preview/styles/bank-method.css | PSE + Bancolombia CSS | none | | @andresh11/pay-preview/styles/catalog.css | PaymentCatalog CSS | none |


Tech stack

  • Mitosis 0.14 — one .lite.tsx source → 5 frameworks.
  • Strict TypeScriptstrict: true, noUncheckedIndexedAccess: true.
  • ESLint 8 + eslint-plugin-perfectionist — 0 warnings.
  • No runtime dependencies — only react/react-dom as peer.
  • Zero polyfills — only native APIs already in modern browsers.

Development

npm install
npm run typecheck               # tsc --noEmit
npm run lint                    # eslint --max-warnings 0
npm run build                   # mitosis → tsc → css → copy
npm run dev:demo                # demo with HMR (http://localhost:5173)
npm run build:demo              # demo bundle (final verification)
npm run validate:tree-shaking   # verifies bundle < 30 KB

Structure

src/
├── core/
│   ├── detectors/              ← detectCardBrand, isValidLuhn, hasValidLength
│   └── formatters/             ← formatCardNumber, formatExpMonth, etc.
├── mitosis/                    ← .lite.tsx sources + CSS (generates 5 targets)
│   ├── CreditCard.lite.tsx
│   ├── NequiPreview.lite.tsx
│   ├── PSEPreview.lite.tsx
│   ├── BancolombiaTransferPreview.lite.tsx
│   ├── PaymentCatalog.lite.tsx
│   └── styles/                 ← credit-card, nequi, bank-method, catalog
├── strategies/
│   ├── types/                  ← IPaymentMethodStrategy, PaymentStrategiesRegistry
│   └── card/                   ← CardPaymentStrategy + validateAndNormalizeCard
└── types/                      ← pure public types

Build flow (multi-target)

src/mitosis/*.lite.tsx
        │
        ├── build-mitosis.mjs ──→ 5 targets × N components
        │                         (react, vue, svelte, solid, reactNative)
        │
        ├── build-ts.mjs ────────→ dist/{index,core,strategies,types}/*
        │
        ├── build-css.mjs ───────→ dist/styles/*.css (minified)
        │
        └── copy-mitosis.mjs ────→ dist/{target}/* (bundle per target
                                                 + index.d.ts autogenerated)

copy-mitosis.mjs also generates an index.d.ts per target by extracting the export interface and export type declarations from each .lite.tsx, so TypeScript can type the props without having to parse Mitosis JSX.


Compatibility notes

Mitosis 0.14 + Windows

Mitosis build on Windows has 5 documented workarounds:

  1. --force breaks the path parser → don't use it; clean src/components/ first.
  2. --out-dir ignores the correct extension with multiple files: invoke mitosis file by file and capture stdout.
  3. mitosis.config.json with targets[] does NOT work on Windows.
  4. --state useState (not default useLocalProxy) to emit idiomatic React code without import { useLocalProxy } from '@builder.io/mitosis'.
  5. "use client" is auto-emitted when using useStore() in React: post-process and remove it before exposing.

ESLint 8.57

Does NOT support ignoreTypeLikeDeclarations or ignoreFunctionDeclarations (from the perfectionist plugin). We use ESLint 8.57 without those rules.


Licencia / License

MIT © Andres Hernandez — ver LICENSE.


🤝 Contribuir / Contributing

  1. Fork & branch.
  2. npm install && npm run dev:demo to see live changes.
  3. npm run typecheck && npm run lint before commit.
  4. Pull request describing which method or feature you add.

Important: if you're going to add a new payment method (Daviplata, Caja Social, BNPL, etc.), open an issue first to discuss the layout of the preview UI. The library keeps 5 previews and an extensible catalog, not 10 custom components.