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

@rendara/report-viewer

v1.0.0

Published

Embeddable Angular report viewer for Rendara Reports (renderer + toolbar + public API).

Readme

@rendara/report-viewer

The embeddable Angular Report Viewer: hand it a validated Template JSON and a Data JSON and it renders the final, paginated report inside any Angular host app. It bundles the shared engine + renderer, so what was designed is exactly what renders.

Quick start

Install the package (Angular is a peer dependency you already have — see Compatibility):

pnpm add @rendara/report-viewer     # or: npm i / yarn add @rendara/report-viewer

ReportViewer is a standalone component: import it, then hand it a template (a validated RendaraTemplate object or a raw JSON string) and a data JSON. Both typically come from your backend at runtime — fetching and binding them is the developer's only job:

import { Component } from '@angular/core';
import { ReportViewer, type RenderedEvent, type ViewerError } from '@rendara/report-viewer';

@Component({
  selector: 'app-invoice',
  imports: [ReportViewer],
  template: `
    <rdr-report-viewer
      [template]="template"
      [data]="data"
      (rendered)="onRendered($event)"
      (error)="onError($event)"
      style="display:block; height:100dvh"
    />
  `,
})
export class InvoiceComponent {
  /** Fetch both from your backend; `template` may be a JSON string or a parsed object. */
  protected readonly template: string = TEMPLATE_JSON;
  protected readonly data: unknown = DATA_JSON;

  protected onRendered(e: RenderedEvent): void {
    console.log(`rendered ${e.pageCount} page(s)`);
  }

  protected onError(e: ViewerError): void {
    console.error(e.message); // e.g. "Template failed validation: missing 'page.size'"
  }
}

That's the whole integration: the viewer validates → binds → paginates → renders the report and gives it a print / export-PDF / zoom / find toolbar. Everything below is optional configuration ([config], [theme], more outputs).

  • Full input/output reference — generated API docs (TypeDoc). Build them with pnpm docs:build (→ dist/docs/report-viewer/index.html); this landing page is the docs home, and every input, output and config type is documented from source.
  • Live examples of every state — Storybook: npx nx storybook report-viewer (Default, Themed, Paginated, Zoom, toolbar variants, Export, Watermark, Search, Empty / No-data / Error, …).
  • Version historyCHANGELOG.md (versioned by Changesets).

Build & packaging (E9-S1)

The package is built to Angular Package Format (APF) with engine + renderer + schema bundled in, so a host installs one package and gets everything:

nx run report-viewer:pack   # build schema→engine→renderer→viewer, inline, npm-pack verify

ng-packagr can't compile a sibling lib's source into a package, so the build is two stages: ng-packagr emits an APF package per lib (cross-lib refs left external), then tools/bundle-viewer.mjs inlines those @rendara/* FESM + .d.ts into the viewer and strips them from dependencies. The published tarball depends only on Angular (peer) + jsonata/ajv/tslib. Note: nx build report-viewer alone is not publishable (its FESM still imports @rendara/*); use bundle/pack. See ADR 0013.

A clean-room smoke test (nx run report-viewer:clean-room) then installs the packed tarball into a fresh Angular app outside the monorepo, AOT-builds it and renders a report in a headless browser — proof the package is consumable as a real npm i, and a release gate. See ADR 0019 and releases.md.

Compatibility & version tolerance (E9-S2)

The package is built to fit a host app's existing Angular install:

  • Wide Angular peers. @angular/core and @angular/cdk are declared as peerDependencies over >=20.0.0 — tested against Angular 20 (min) – 22 (max). Angular is a peer (never bundled), so it isn't duplicated into your app. @angular/common is not required.
  • Tree-shakeable. The package is "sideEffects": false and ships a single FESM2022, so a bundler drops it entirely when you don't reference it and you pay only for what you import. (Per-feature dead-code elimination within the bundle happens in your Angular app build via the Ivy optimizer.) A CI gate (tools/verify-viewer-treeshake.mjs) proves the package has no eager side effects.
  • Single entry point. Everything is exported from @rendara/report-viewer; there are no secondary entry points. For framework-agnostic schema validation without Angular, use the separate @rendara/report-schema package.
  • SSR-safe. All browser APIs are guarded — file download, window.print() and the default PDF exporter no-op (or return bytes) without a DOM — so the component imports and renders under server-side rendering without throwing.

See ADR 0014.

Usage

import { ReportViewer } from '@rendara/report-viewer';

@Component({
  selector: 'host-app',
  imports: [ReportViewer],
  template: `
    <rdr-report-viewer
      [template]="template"
      [data]="data"
      [config]="{ locale: 'en-US', initialZoom: 'fit-width', pageMode: 'continuous' }"
      [theme]="{ '--rdr-accent': '#4F46E5' }"
      (rendered)="onRendered($event)"
      (error)="onError($event)"
    />
  `,
})
export class HostApp {
  /* template: RendaraTemplate | string, data: arbitrary JSON */
}

Public API (brief §8)

| Input | Type | Notes | | ---------- | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | template | RendaraTemplate \| string \| null | A validated template object or a raw JSON string. null paints nothing. | | data | unknown | Arbitrary host JSON bound into the template. | | config | ViewerConfig | locale, initialZoom, toolbar, watermark, pageMode, thumbnails, pdfExporter, exportFilename, pdfMetadata. | | theme | ViewerTheme | --rdr-* CSS custom-property overrides for the host. |

| Output | Payload | Fired when | | ------------ | -------------------- | ------------------------------------------------ | | rendered | { pageCount } | a template+data render completes. | | pageChange | { current, total } | the visible page changes (E7-S3). | | error | ViewerError | a validation/binding/render failure is surfaced. |

Theming & style isolation (E9-S5)

The viewer is designed to drop into your app: it re-themes from CSS custom properties, it never leaks its styles into your page, and your page's styles can't break the rendered report. See ADR 0017 (and, for the renderer's shadow-DOM surface, ADR 0009).

Theming — the [theme] input

Every non-data-driven colour/measure is a --rdr-* CSS custom property with a safe default. Override any of them for a single viewer through the [theme] input (applied as inline styles on the host element, so nothing else on your page is touched):

import { ReportViewer, type ViewerTheme } from '@rendara/report-viewer';

// A dark, brand-accented chrome:
const theme: ViewerTheme = {
  '--rdr-viewer-backdrop': '#0f172a',
  '--rdr-viewer-surface': '#111827',
  '--rdr-viewer-panel': '#1f2937',
  '--rdr-viewer-hairline': '#334155',
  '--rdr-viewer-text': '#e5e7eb',
  '--rdr-viewer-text-secondary': '#94a3b8',
  '--rdr-viewer-accent': '#818cf8',
};
<rdr-report-viewer [template]="template" [data]="data" [theme]="theme" />

You can also set the same tokens from your own CSS on the <rdr-report-viewer> element (or any ancestor — custom properties inherit), which is handy for a site-wide theme:

rdr-report-viewer {
  --rdr-viewer-accent: #0ea5e9;
}

There are two token families. Chrome tokens style the viewer's frame (toolbar, rail, backdrop, states); content tokens style the rendered report inside it (page, tables, watermark, search marks). All have defaults, so you override only what you need.

Chrome tokens (--rdr-viewer-*)

| Token | Default | Themes | | ----------------------------- | --------- | ----------------------------------------- | | --rdr-viewer-backdrop | #f3f4f6 | The grey area behind the pages | | --rdr-viewer-panel | #f9fafb | Thumbnail rail / find-bar surface | | --rdr-viewer-surface | #ffffff | Toolbar, status bar, buttons, inputs | | --rdr-viewer-hairline | #e5e7eb | Dividers and borders | | --rdr-viewer-input-border | #d1d5db | Button / input outlines | | --rdr-viewer-text | #111827 | Primary chrome text | | --rdr-viewer-text-secondary | #5f6672 | Secondary / muted chrome text | | --rdr-viewer-accent | #4f46e5 | Focus rings, active thumbnail, Find state | | --rdr-viewer-accent-subtle | #eef2ff | Hover / active tints | | --rdr-viewer-danger | #dc2626 | Error-state icon + border | | --rdr-viewer-danger-subtle | #fef2f2 | Error-state icon fill |

Content tokens (--rdr-*, from the shared renderer)

| Token | Default | Themes | | ------------------------- | -------------------------------- | ---------------------------------------------------- | | --rdr-page-background | #ffffff | Page sheet fill (a template background still wins) | | --rdr-page-shadow | soft slate drop shadow | Drop shadow under each page sheet | | --rdr-page-gap | 24px | Gap between stacked pages (continuous) | | --rdr-printable-guide | rgba(79, 70, 229, 0.25) | The (non-printing) printable-area outline | | --rdr-font-family | 'Inter', system-ui, sans-serif | Base report font (runs set their own inline) | | --rdr-text-color | #111827 | Base report text colour | | --rdr-table-header-fill | #F1F5F9 | Table header-row fill | | --rdr-table-group-fill | #EEF2FF | Table group-header band fill | | --rdr-table-detail-rule | #E2E8F0 | Rule under each detail row | | --rdr-table-band-rule | #CBD5E1 | Rule under header / around group footers | | --rdr-table-total-rule | #334155 | Strongest rule (header bottom / grand total top) | | --rdr-watermark-color | #9CA3AF | Default watermark text colour | | --rdr-search-highlight | #FDE68A | Fill behind a search match (E8-S6) | | --rdr-search-active | #FBBF24 | Fill behind the active search match |

The --rdr-viewer-* and --rdr-* names are deliberately distinct from the designer's ui-kit tokens (--rdr-color-*, --rdr-font-size-*), so theming a host page's own design system never collides with the viewer.

Style isolation — the guarantees

The viewer uses Angular's default ViewEncapsulation.Emulated and gives two guarantees:

  • It won't leak out. All chrome CSS is class-scoped by Angular's emulated encapsulation (.rdr-viewer-* rules only match the viewer's own elements), so a <p class="rdr-viewer-title"> elsewhere on your page is never restyled by the viewer, and no report style escapes into your app.
  • Your page won't (casually) break the report. The report is painted with inline styles plus a CSS reset that pins every inheritable text property (colour, font, line-height, letter-spacing…) to a host-independent baseline. So a host body { font: … } / * { color: … } cascade cannot bleed into the rendered report — inline styles and the reset outrank plain element/class rules.

The one thing emulated encapsulation can't defend against is a host rule that uses !important on a selector that happens to match. If your app injects such global CSS, use the shadow-DOM opt-in below for the report content.

Shadow-DOM opt-in (hard boundary)

For a hard boundary that blocks every host selector — including !important — render the report through the shared renderer's Shadow-DOM surface, <rdr-report-surface> (ViewEncapsulation.ShadowDom, ADR 0009). A real shadow root blocks incoming selectors, the same reset neutralises the inheritable properties that do cross a shadow boundary, and — because nothing escapes the shadow root — the report also can't leak back out. Theming works identically: inherited --rdr-* custom properties cross the boundary, so overriding a token on (or above) the surface still re-themes it.

The <rdr-report-viewer> chrome itself stays emulated by design, for two reasons: Angular places an emulated child component's styles in document.head, where they do not cross a shadow boundary (a whole-viewer shadow root would render the toolbar/rail unstyled), and the Export/Watermark dialogs use CDK overlays that attach to document.body, outside any shadow root. So the shadow-DOM opt-in is content-only: use <rdr-report-surface> when you need the rendered report walled off from hostile host CSS, and the emulated <rdr-report-viewer> for the full toolbar experience.

Render pipeline (E7-S2)

On any change to template/data/config the viewer runs a single, shared validate → bind → paginate → render pipeline:

  1. Validate — a JSON string is parsed; any input is migrated to the current schema and validated. Older templates are carried forward automatically.
  2. Bind — bound elements and data tables are resolved through the sandboxed JSONata + Intl engine (no eval / new Function).
  3. Paginate — the bound document is laid out into pages by the shared engine.
  4. Render — the paginated document is painted by the shared renderer.

The pipeline is total: a failure surfaces through (error) (and the viewer paints nothing) rather than throwing. It is the same engine path the designer preview uses, so the viewer and the designer agree pixel-for-pixel.

Export PDF (E8-S3)

The toolbar's Export PDF action opens a dialog (filename · pages · include watermark) and produces a PDF through a swappable PdfExporter:

  • Default (client-side): defaultPdfExporter renders a selectable-text, vector PDF entirely in the browser via the shared renderer — no server round-trip, no heavy dependency, no rasterisation — and downloads it. It covers text, vector shapes, table grids and a text watermark; it does not paint images and approximates typography (base-14 Helvetica). For pixel-perfect output use Print or a server-side exporter. See ADR 0012.
  • Custom / server-side: pass your own config.pdfExporter to, e.g., POST the PdfExportRequest to a Puppeteer/Playwright route for pixel-perfect or batch output.
// Swap in a server-side exporter (the documented optional path):
const config: ViewerConfig = {
  exportFilename: 'invoice.pdf',
  pdfMetadata: { title: 'Invoice INV-2042', author: 'Acme Corp' },
  pdfExporter: {
    async export(req) {
      const res = await fetch('/api/pdf', { method: 'POST', body: serialize(req) });
      const blob = await res.blob();
      download(blob, req.filename);
      return { pageCount: req.document.pageCount, filename: req.filename, blob };
    },
  },
};

In-report search (E8-S6)

The toolbar's Find in report (magnifier) action opens a compact Find bar. Typing a query highlights every matching run of text — text elements, data-table cells and group labels — across all pages, shows an N / total match count, and lets you step through hits with the prev/next buttons (or Enter / Shift+Enter); the active match is scrolled into view. Matching is case-insensitive.

Search is a screen-only aid: highlights never appear in Print or PDF export output. Hide the control with config.toolbar.search: false. The highlight colours are themeable via the renderer's --rdr-search-highlight and --rdr-search-active CSS custom properties.

Optional thumbnail rail (E8-S7)

The left thumbnail rail — one mini render per page, the current page outlined, click to jump — is optional. The toolbar's Toggle page thumbnails action shows or hides it at runtime (a hidden rail is removed from the DOM, freeing the width for the report). Set the rail's initial visibility with config.thumbnails: false (default true), and hide the toggle button itself with config.toolbar.thumbnails: false.

Localization & RTL (E10-S2)

The viewer is locale-aware end to end. All formatting (currency, number, percent, date) uses the effective locale — config.locale if you set one, otherwise the template's metadata.locale — through the Intl-based engine, so the same report renders $1,234.50 in en-US, 1.234,50 € in de-DE, and Arabic-Indic digits in ar-EG.

Right-to-left rendering is derived from that same locale — there is no direction field on the template (the schema is a versioned contract). When the effective locale is RTL (Arabic, Hebrew, Persian, Urdu, …) the report reads right-to-left: the page carries dir="rtl", un-aligned text right-aligns, and data-table columns mirror across the table width. LTR reports are unchanged.

<!-- RTL because config.locale is Arabic (overrides the template locale). -->
<rendara-report-viewer [template]="tpl" [data]="data" [config]="{ locale: 'ar-EG' }" />

Leave config.locale unset to inherit the template's metadata.locale; a Latin/LTR locale keeps the report left-to-right.

Security & Content-Security-Policy (E10-S4)

The viewer treats every string in your data and template as untrusted and neutralises it:

  • No code execution. Template expressions run only through the sandboxed JSONata engine — there is no eval / new Function anywhere (enforced by a source-scan test). So the viewer runs under a strict script-src without 'unsafe-eval'.
  • Output is escaped. All text is emitted via Angular interpolation (and an escaping serializer for PDF/snapshot output); there is no [innerHTML]. A <script> / <img onerror> in the data or template renders as inert text.
  • Image URLs are allow-listed. Static and data-bound image sources pass through sanitizeImageUrl: http(s), data:image, and relative URLs are allowed; javascript: / vbscript: / file: / data:text/html are dropped.
  • Styles are isolated (emulated encapsulation, with the Shadow-DOM opt-in above).

CSP for your host app. The viewer positions each element with an inline style attribute and Angular injects component <style> elements, so allow style-src (either 'unsafe-inline' or an Angular ngCspNonce); list your image origins under img-src. A good starting policy:

Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self' 'unsafe-inline';
  img-src 'self' https: data:;
  object-src 'none'

See the repo's SECURITY.md for the full threat model, CSP rationale, dependency-audit policy, and how to report a vulnerability, and ADR 0023 for the decisions.

Versioning & upgrades (E10-S7)

The package follows semver; every release's notes are in this package's CHANGELOG.md (generated by Changesets). Integration and upgrade guidance — including what majors may change and how stored templates stay loadable across releases via migrate() — is in the repo's integration & upgrade guide; the Template JSON contract's own rules are the schema versioning & migration policy.