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

@kerusiweb/angular

v2.0.0

Published

An Angular seat-map component that renders Kerusi Seat Map & Availability Format documents as interactive, accessible inline SVG.

Readme

@kerusiweb/angular

An Angular seat map that renders Kerusi documents as interactive, accessible inline SVG.

npm install @kerusiweb/core @kerusiweb/angular

Requires Angular 22+. Standalone, signal-based, zoneless-friendly.

This package is the Angular binding only — the components and the signal-backed KerusiStateStore. The document types, the conformance validator, the render model and its geometry live in its peer @kerusiweb/core, which is framework-free; import them from there.


<kerusi-seatmap>

import { KerusiSeatmapComponent } from '@kerusiweb/angular';

@Component({ imports: [KerusiSeatmapComponent], /* ... */ })
<kerusi-seatmap
  [map]="map"
  [state]="state"
  [(selection)]="picked"
  [showLegend]="true"
  (seatDisallowed)="explain($event)"
/>

Inputs

Documents

| Input | Type | Default | | | --------- | --------------- | ---------- | --------------------------------------------------------------------------- | | map | KerusiMap | required | The static venue layout. | | state | KerusiState | — | Live availability. Merged by Seat.id; an absent seat is available (§5.1). | | session | KerusiSession | — | The optional map↔event join (§5.3). Validated against the map and state. |

Selection

| Input | Type | Default | | | -------------------- | ------------------------------- | --------------- | ------------------------------------------------------------------------------- | | selection | readonly string[] | [] | Selected seat ids. Two-way — [(selection)] — or read-only with [selection]. | | selectableStatuses | SeatRenderStatus[] | ['available'] | Which statuses a seat may be picked in. | | companionMode | 'auto' \| 'independent' | 'auto' | auto selects a seat's whole companion closure together (§4.6). | | maxSelection | number | — | Cap on selected seats. Counts a companion closure as its full size. | | seatSelectable | (seat: RenderSeat) => boolean | — | A final say, applied after the status test. | | interactive | boolean | true | false renders read-only. |

Appearance

| Input | Type | Default | | | --------------------------------- | --------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------- | | colors | KerusiSeatmapColors | theme defaults | Per-status fills, element tones, focus ring, backdrop. See Theming for the CSS custom properties. | | typeColors | boolean | true | Let an available seat take its SeatType.color (§4.7). | | seatSize | number | 28 | Grid cell edge, in viewBox units. | | seatGap | number | 6 | Gap between grid cells. | | freeformBasis | number | 1000 | Freeform viewBox width; height follows the aspect ratio. | | unitScale | number | 1 | CSS pixels per viewBox unit. Caps each section at its natural size so a narrow section and a wide one draw seats the same size. | | showSectionLabels | boolean | true | | | showLegend / showLegendPrices | boolean | false / true | |

Sections

| Input | Type | | | ------------------ | -------------------------------------- | ------------------------------------------------------------------- | | sectionIds | readonly string[] | Render only these, in this order. Default: all, by Section.index. | | sectionOverrides | Record<string, SectionRenderOptions> | Per-section hidden, aspectRatio, seatSize, label. |

Localization, validation, lifecycle

| Input | Type | Default | | | ------------------ | ------------------------------- | ---------------------------- | ---------------------------------------------------------------------- | | locale | string | KerusiMap.locale ?? 'en' | BCP-47. Resolves Section.label / SeatType.label locale maps. | | rtl | boolean \| 'auto' | 'auto' | Mirrors layout and arrow direction. auto derives it from the locale. | | ariaStrings | SeatAriaStrings | English | Every announced phrase, for translation. | | validate | 'collect' \| 'throw' \| 'off' | 'collect' | collect reports through validationIssues and renders anyway. | | expireHolds | boolean | false | Revert a lapsed holdExpires to available on a ticker. | | expiryIntervalMs | number | 1000 | |

Outputs

| Output | Payload | | | ----------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------ | | selectionChange | readonly string[] | From the selection model. | | seatSelect / seatDeselect | SeatInteraction | The RenderSeat, the resulting selection, and every seat that changed (companions included). | | seatDisallowed | SeatDisallowed | reason is booked, held, blocked, not-selectable, max-selection or companion-unavailable. | | seatFocus | RenderSeat | | | validationIssues | readonly KerusiViolation[] | Emitted whenever the documents change. |

The seat glyph

Colour means seat type; shape means status. A seat's fill is always its SeatType.color, or the theme's availableBg when the type has none — it does not change when the seat is held or booked. Overloading fill with both type and status hid the type colour on exactly the seats a busy map has most of; see the doc comment on seatFill for the full argument. selected is the one deliberate exception: it still owns the seat's colour outright, so your own picks are never ambiguous, and a purple fill on its own would in any case fail WCAG 1.4.1 (Use of Colour) for anyone who cannot distinguish it — hence the shape cues below.

The seat body itself carries orientation: square-ish at the front, tapered at the back, so which way a seat faces is legible from its outline alone. Seat.rotation turns the whole group, and the taper turns with it — no separate frame is drawn on top. This is what makes a fanned lecture theatre or a stadium stand read at a glance. The seat number counter-rotates and stays upright regardless.

Status is read from two marks layered over the body, never from its colour:

| State | Fill | Wash | Occupant figure | | --------- | ---------------------------------------------- | ---------- | ---------------------------------------------------- | | Available | type colour | — | — | | Selected | selectedBg frame + selectedFg core | — | solid, tinted selectedBg | | Held | type colour | heldBg | hollow, stroked heldFg — a hold is provisional | | Booked | type colour | bookedBg | solid, tinted bookedFg — settled, someone else's | | Blocked | blockedBg | — | — (withheld by the venue; no one is there) |

Solid vs. hollow is the real distinction — settled vs. still in progress — and every figure leans with the seat rather than staying upright, so it doubles as an orientation cue too.

Selection is the only state drawn from two tones rather than one, and that is deliberate. selectedBg frames the seat; selectedFg fills a core inside that frame and is what the number and the figure are read against. Because the treatment always holds a light tone and a dark one at once, one of the two separates from the page whichever way you have themed it — so the library never has to detect a colour scheme, and there is no prefers-color-scheme default to fight with your own. An earlier revision had this inverted, a near-white rim around a mid-purple middle, which put the low-contrast tone on the outside boundary: a selected seat dissolved into a light page and sank into a dark one.

Both tokens therefore carry real visual weight — override them as a pair. Swapping the two gives the inverse treatment, a light frame around a dark core, which works just as well.

A theme that overrides heldFg/bookedFg owns the contrast of that mark against whatever SeatType.color the document supplies — the library cannot know the pairing in advance, since the figure tint is fixed while the type colour underneath it is not. The shipped defaults are light marks over a darkened wash, which reads across the tier colours in the demo fixtures.

The geometry is exported, if you want to draw a matching seat elsewhere: seatBodyPath, seatSelectedFrame, seatOccupantPath, seatOccupantStroke. (seatRingStroke is the old name for seatSelectedFrame and still resolves to it.) A seat is always square, which makes seatBodyPath's inset an exact scaled copy about the centre — that is how the selected core is derived, and why marks drawn to the core's box keep every clearance they had against the body.

Theming

Colors resolve through three tiers, highest first:

  1. A --kerusi-* custom property in your stylesheet.
  2. The [colors] input — a partial KerusiSeatmapColors, merged over the defaults.
  3. The library default.

The library never writes a --kerusi-* property onto its own host, which is what keeps that order true: every fill is emitted as var(--kerusi-selected-bg, <the resolved input value>). Use whichever tier fits — the input for values known at build time, CSS for anything that has to respond to a media query or a theme class.

kerusi-seatmap {
  --kerusi-selected-bg: #8b6ad6;
  --kerusi-available-bg: #2f6b45;
}

@media (prefers-color-scheme: dark) {
  kerusi-seatmap {
    --kerusi-element-bg: #232a36;
  }
}

Every key of KerusiSeatmapColors has a property, kebab-cased:

| Input key | Custom property | | ------------------------------------- | ----------------------------------------------------------- | | availableBg / availableFg | --kerusi-available-bg / --kerusi-available-fg | | selectedBg / selectedFg | --kerusi-selected-bg / --kerusi-selected-fg | | heldBg / heldFg | --kerusi-held-bg / --kerusi-held-fg | | bookedBg / bookedFg | --kerusi-booked-bg / --kerusi-booked-fg | | blockedBg / blockedFg | --kerusi-blocked-bg / --kerusi-blocked-fg | | elementBg / elementFg | --kerusi-element-bg / --kerusi-element-fg | | elementAccentBg / elementAccentFg | --kerusi-element-accent-bg / --kerusi-element-accent-fg | | elementMutedBg / elementMutedFg | --kerusi-element-muted-bg / --kerusi-element-muted-fg | | focusRing | --kerusi-focus-ring | | backdrop | --kerusi-backdrop |

A SeatType.color from the document is deliberately not themable — it is the map's own value under §4.7. Turn it off wholesale with [typeColors]="false" if you want the theme to own every fill.

Overriding selectedBg does not cost you the selected cue: the core and the figure are shape, not color. Do set selectedFg alongside it, though — it is the core's fill, so it is most of a selected seat's area rather than just a label tint, and the two are read against each other. heldBg/bookedBg are the wash drawn over a taken seat's type colour, not the seat's own fill — see The seat glyph for the full state table.

If you need the pre-1.1 flat selected seat back, the core is its own node:

kerusi-seatmap .kerusi-seat__core {
  display: none;
}
/* Without the core, these would be selectedBg on selectedBg. */
kerusi-seatmap .kerusi-seat--selected .kerusi-seat__label,
kerusi-seatmap .kerusi-seat--selected .kerusi-seat__occupant,
kerusi-seatmap .kerusi-seat--selected .kerusi-seat__wheelchair {
  fill: var(--kerusi-selected-fg, #f3ecff);
  stroke: none;
}

For anything the palette does not cover, each seat group carries class hooks:

| Class | | | --------------------------------------------------------------- | ------------------------------------------ | | .kerusi-seat | Every seat group. | | .kerusi-seat--available / --held / --booked / --blocked | Its SeatStatus. | | .kerusi-seat--selected | Currently picked. | | .kerusi-seat--unselectable | Fails the status test or seatSelectable. | | .kerusi-seat--wheelchair | accessibility.wheelchairAccessible. | | .kerusi-seat--companion | Has companions (§4.6). |

Inside a seat: .kerusi-seat__box, __wash, __core, __occupant (__occupant--selected / --held / --booked), __label, __wheelchair.

Keyboard

Tab moves between sections; each section keeps its own tab stop. Within a section, arrow keys follow the §4.3.1 col order — so an aisle, which is a column no seat occupies, is stepped across rather than into.

| Key | | | ------------------------ | ---------------------------------- | | ← → | Previous / next seat in the row | | ↑ ↓ | Nearest column in the adjacent row | | Home / End | First / last seat of the row | | Ctrl+Home / Ctrl+End | First / last seat of the section | | PageUp / PageDown | First / last seat of the section | | Enter / Space | Toggle | | Escape | Clear the selection |

Each seat is a role="button" announcing its position, type, price, status, every Seat.accessibility property and its attributes. A polite live region reports the running selection and any disallowed reason.


<kerusi-legend>

Rendered inline by [showLegend]="true", or placed anywhere yourself:

<kerusi-legend [legend]="model.legend" [locale]="'ms'" [showPrices]="true" />

It resolves swatches through the same path as the seat fills, so the two cannot drift apart. Every availability swatch — not just Selected — draws the seat glyph itself (wash, ring, occupant, as it applies) rather than a flat colour, because shape is the cue the map uses for status.


Working with the format directly

All of this lives in @kerusiweb/core and is pure — no Angular import — so it can run in a test, a build step or on a server.

import {
  buildRenderModel, // KerusiMap + KerusiState -> the resolved render model
  checkKerusiMap, // every violation, no throw
  validateKerusiMap, // throws on the first error
  validateDocumentSet, // map / session / state joins
  resolveSeatPrice, // the §4.9 precedence order
  resolveLocalizedText, // string | Record<string, string> -> string
  formatMoney, // minor units -> "RM 45.00", "¥1,200", "KD 12.500"
  summarizeSelection, // seats, total, unpriced count
  computeSectionLayout, // placed geometry for one section
  buildNavigationGraph, // per-seat keyboard neighbours
} from '@kerusiweb/core';

Validation

checkKerusiMap returns every violation in document order without throwing; validateKerusiMap throws a KerusiValidationError carrying the first error and the full list. Each violation has a stable rule slug, a severity (error blocks conformance, warning is advisory) and a document path.

for (const v of checkKerusiMap(map)) {
  console.warn(`${v.severity} ${v.rule} at ${v.path}: ${v.message}`);
}

Live availability

import { KerusiStateStore } from '@kerusiweb/angular';

const store = new KerusiStateStore(initialState);
socket.onmessage = (e) => {
  const result = store.apply(JSON.parse(e.data));
  if (result.outcome === 'gap') refetch(); // store.needsRefetch() is now true
};
const stop = store.startExpiryTicker();

Deltas that are stale, duplicate or scoped to another session are discarded.

Gap detection needs a sequence. §5.2 requires updatedAt to be strictly increasing but not contiguous, so it cannot by itself distinguish "a delta was lost" from "nothing happened for a while". The store detects gaps when the transport supplies a monotonic sequence — metadata.seq by default, or your own sequenceOf reader. A gapped delta is still applied, so the map degrades rather than freezes while you re-fetch.


MIT © Arsyad "Shad Ahm" Ahmad