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

@retalia/pos-components

v0.0.7

Published

Presentational Adaptive POS Angular components. State in, typed intents out — no service calls.

Readme

@retalia/pos-components

Presentational Adaptive POS Angular components. State in, typed intents out — no service calls.

Optional HTTP clients live behind a separate entry point: @retalia/pos-components/services. Components never import that entry. The host calls a service, then passes display state into the UI.

Requires Angular 21 (@angular/core and @angular/common ^21.2.0).

Install

npm i @retalia/pos-components

Services

Import from @retalia/pos-components/services, never from a pos-* component.

import {
  ClerkAuthService,
  POS_API_CONFIG,
  PosCommandsClient,
  posApiInterceptor,
} from '@retalia/pos-components/services';

| | | |---|---| | posApiInterceptor | Reads portalApiToken + contextShopId cookies and sets Authorization / Shop-Context. Provide via provideHttpClient(withInterceptors([posApiInterceptor])). | | ClerkAuthService | signIn(clerkNumber, password) — GET /api/v1.0/clerks/{number}, password check in the service (stub until ID-647). Call this from the host on login.submit. | | PosCommandsClient | addItem / changeQuantity / clearBasket / tenderCash / newSale — each returns a PosStateEnvelope. |

In local Adaptive POS, paste a Portal Bearer into the portalApiToken cookie and the shop id into contextShopId. Do not send clerk credentials to POST /token.

Styles

Add the package stylesheet once in the host app (e.g. angular.json styles):

node_modules/@retalia/pos-components/styles/styles.css

Theme via CSS custom properties (--pos-*) defined in that file. Override tokens in the host to rebrand; values not exposed as tokens are not overridable without a library change.

Use <pos-tokens> (Storybook: Theme / Tokens, or an internal theme lab page) to see every token in action. Click a sample or token to edit values as live overrides — this does not change the package defaults in styles.css. Export a complete :root CSS file for the consuming POS, and import that file back into the lab later.

Tokens gallery

import { Component } from '@angular/core';
import { PosTokens } from '@retalia/pos-components';

@Component({
  selector: 'app-theme-lab',
  imports: [PosTokens],
  template: `<pos-tokens />`,
})
export class ThemeLab {}

Host the exported file after the package stylesheet so --pos-* overrides win:

node_modules/@retalia/pos-components/styles/styles.css
src/styles/pos-theme.css

| | | |---|---| | Selector | pos-tokens | | Purpose | Documentation / theme lab — not for production POS screens | | Export | Complete :root { --pos-*: … } CSS for the host POS | | Import | Previously exported theme CSS, applied as lab overrides only |

Login

Keypad login UI — presentational only. It does not call auth services. Layout around it (e.g. split with an image) belongs in the host app.

The host envelope holds login/session state and passes it in. If the host uses a package AuthService, it calls that on login.submit, then updates [session] / [errorMessage]. The component never injects the service.

import { Component, signal } from '@angular/core';
import {
  PosLogin,
  PosLoginIntent,
  PosLoginSessionState,
} from '@retalia/pos-components';

@Component({
  selector: 'app-root',
  imports: [PosLogin],
  template: `
    <pos-login
      [session]="session()"
      [errorMessage]="errorMessage()"
      (action)="onLoginAction($event)"
    />
  `,
})
export class App {
  readonly session = signal<PosLoginSessionState>({ status: 'signedOut' });
  readonly errorMessage = signal('');

  onLoginAction(intent: PosLoginIntent): void {
    if (intent.type !== 'login.submit') {
      return;
    }
    // Host (or package ClerkAuthService) calls the API, then:
    // this.session.set({ status: 'busy' });
    // this.session.set({ status: 'signedIn', displayName: clerk.name });
    // this.errorMessage.set('Invalid clerk or password');
  }
}

| | | |---|---| | Selector | pos-login | | Input session | Display-only login/session state: signedOut | busy | signedIn, optional displayName | | Input errorMessage | Optional error text | | Output action | Typed intents: login.key, login.next, login.submit, login.back, … |

Basket

Presentational sale basket — renders basket.lines[] and totals exactly as the host envelope provides them. It does not price, tax, or discount. Quantity, void, and clear only emit intents; the host (or server) updates the envelope.

import { Component, signal } from '@angular/core';
import {
  PosBasket,
  PosBasketIntent,
  PosBasketLine,
  PosBasketTotals,
} from '@retalia/pos-components';

@Component({
  selector: 'app-sale',
  imports: [PosBasket],
  template: `
    <pos-basket
      [lines]="lines()"
      [totals]="totals()"
      (action)="onBasketAction($event)"
    />
  `,
})
export class Sale {
  readonly lines = signal<readonly PosBasketLine[]>([]);
  readonly totals = signal<PosBasketTotals>({
    gross: 0,
    net: 0,
    vat: 0,
    discount: 0,
    due: 0,
    currency: 'EUR',
  });

  onBasketAction(intent: PosBasketIntent): void {
    // Host envelope binding maps:
    //   basket.changeQuantity → change-quantity
    //   basket.voidLine       → void-line
    //   basket.clear          → clear-basket
  }
}

| | | |---|---| | Selector | pos-basket | | Input lines | Envelope basket.lines[] as given (lineId, sku, name, qty, unitPrice, lineTotal, vat, struckThrough) | | Input totals | Envelope totals as given (gross, net, vat, discount, due, currency) | | Input disabled | Optional lock while the host is busy | | Output action | Typed intents: basket.changeQuantity, basket.voidLine, basket.clear |

Item entry

Presentational scan-or-type field for the sale screen. It captures an item reference (barcode scan or typed SKU) and emits an item-entry intent. It does not look up, validate, or price the item — the host envelope maps the intent to item-entry, and the server handles catalog lookup.

import { Component, signal } from '@angular/core';
import { PosItemEntry, PosItemEntryIntent } from '@retalia/pos-components';

@Component({
  selector: 'app-sale',
  imports: [PosItemEntry],
  template: `
    <pos-item-entry
      [disabled]="busy()"
      [errorMessage]="errorMessage()"
      (action)="onItemEntryAction($event)"
    />
  `,
})
export class Sale {
  readonly busy = signal(false);
  readonly errorMessage = signal('');

  onItemEntryAction(intent: PosItemEntryIntent): void {
    // Host envelope binding maps:
    //   itemEntry.submit → item-entry  (payload: reference)
  }
}

| | | |---|---| | Selector | pos-item-entry | | Input disabled | Optional lock while the host is busy (e.g. AddItem in flight) | | Input errorMessage | Optional host-provided error text (lookup failures belong to the host) | | Input autofocus | Focus the field on render so a scanner can type immediately (default true) | | Output action | Typed intent: itemEntry.submit with reference |

Tender

Presentational cash tender — the cashier enters the amount tendered; the host envelope sends that intent and passes back changeDue. The component never calculates change or takes payment. Card / electronic tender is out of scope.

import { Component, signal } from '@angular/core';
import { PosTender, PosTenderIntent } from '@retalia/pos-components';

@Component({
  selector: 'app-tender',
  imports: [PosTender],
  template: `
    <pos-tender
      [amountDue]="amountDue()"
      [changeDue]="changeDue()"
      [currency]="currency()"
      [disabled]="busy()"
      [errorMessage]="errorMessage()"
      (action)="onTenderAction($event)"
    />
  `,
})
export class Tender {
  readonly amountDue = signal(12);
  readonly changeDue = signal<number | null>(null);
  readonly currency = signal('EUR');
  readonly busy = signal(false);
  readonly errorMessage = signal('');

  onTenderAction(intent: PosTenderIntent): void {
    if (intent.type !== 'tender.cash') {
      return;
    }
    // Host envelope binding maps tender.cash → tender intent,
    // then sets changeDue from the envelope response.
    // this.busy.set(true);
    // this.changeDue.set(envelope.changeDue);
  }
}

| | | |---|---| | Selector | pos-tender | | Input amountDue | Envelope amount due as given — display only | | Input changeDue | Envelope change due as given (null until the host responds). Shown as received, never calculated | | Input currency | ISO currency code used only for display formatting | | Input disabled | Optional lock while the host is busy | | Input errorMessage | Optional error text | | Output action | Typed intent: tender.cash with the cashier-entered amount |

Receipt

Presentational completed-sale receipt — renders basket.lines[], totals, payments[], and document exactly as the host envelope provides them. It does not format, tax, or calculate change. Printing is out of scope.

import { Component, signal } from '@angular/core';
import {
  PosReceipt,
  PosReceiptIntent,
  PosReceiptLine,
  PosReceiptPayment,
  PosReceiptTotals,
  PosReceiptDocument,
} from '@retalia/pos-components';

@Component({
  selector: 'app-receipt',
  imports: [PosReceipt],
  template: `
    <pos-receipt
      [lines]="lines()"
      [totals]="totals()"
      [payments]="payments()"
      [document]="document()"
      (action)="onReceiptAction($event)"
    />
  `,
})
export class Receipt {
  readonly lines = signal<readonly PosReceiptLine[]>([]);
  readonly totals = signal<PosReceiptTotals>({
    gross: 0,
    net: 0,
    vat: 0,
    discount: 0,
    due: 0,
    currency: 'EUR',
  });
  readonly payments = signal<readonly PosReceiptPayment[]>([]);
  readonly document = signal<PosReceiptDocument>({
    number: '',
    receiptReady: false,
  });

  onReceiptAction(intent: PosReceiptIntent): void {
    if (intent.type !== 'receipt.newSale') {
      return;
    }
    // Host envelope binding maps receipt.newSale → newSale.
  }
}

| | | |---|---| | Selector | pos-receipt | | Input lines | Envelope basket.lines[] as given (lineId, sku, name, qty, unitPrice, lineTotal, vat, struckThrough) | | Input totals | Envelope totals as given (gross, net, vat, discount, due, currency) | | Input payments | Envelope payments[] as given (tender, amount, tendered, change, state) | | Input document | Envelope document as given (number, receiptReady) | | Input disabled | Optional lock while the host is busy | | Output action | Typed intent: receipt.newSale |