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

@cool-ai/beach-a2ui-renderer-lit

v0.5.5

Published

A2UI v0.9 wire-type re-exports plus Beach-specific glue (id-order tracker, host-fit conventions). Authoring and rendering live in @a2ui/web_core / @a2ui/lit.

Readme

@cool-ai/beach-a2ui-renderer-lit

A2UI v0.9 wire-type re-exports plus Beach-specific glue (id-order tracker, host-fit conventions). Authoring and rendering live in the A2UI ecosystem itself; this package is a thin bridge.

Home: cool-ai.org · Documentation: cool-ai.org/docs

Install

npm install @cool-ai/beach-a2ui-renderer-lit @a2ui/web_core

@a2ui/web_core is a peer dependency. Consumers using only the bridge or host-fit subpaths (no wire-shape types) need not install it.

What this package contains

The package now ships only Beach-specific glue and re-exports from @a2ui/web_core/v0_9. Earlier versions shipped a parallel typed-component layer + DOM/HTML renderers; that layer has been retired. This is the consequence of the "no parallel walker" architectural commitment — A2UI's catalogue model is the canonical authoring surface, @a2ui/lit is the canonical browser renderer, and Beach contributes only what is genuinely Beach-specific.

| Subpath | What it ships | |---|---| | @cool-ai/beach-a2ui-renderer-lit | Wire-shape type re-exports (SurfaceCommand, CreateSurfaceCommand, …), SurfaceInteractionEvent, and the Beach SurfaceComponent type schema | | @cool-ai/beach-a2ui-renderer-lit/bridge | createIdOrderTracker — Beach-specific glue for paged-update patterns (updateDataModel against indexed paths) | | @cool-ai/beach-a2ui-renderer-lit/host-fit | parseHostConfig + breakpoint constants — Beach's --beach-host-config viewport conventions for catalogue components | | @cool-ai/beach-a2ui-renderer-lit/render-server | renderToHtml and renderToText — server-side rendering of A2UI surfaces. Runs @a2ui/lit under jsdom and adapts the rendered DOM tree per channel. |

Authoring A2UI surfaces

Author components against A2UI v0.9's open catalogue shape. Either use object literals directly:

import type { SurfaceCommand } from '@cool-ai/beach-a2ui-renderer-lit';

const surface: SurfaceCommand[] = [
  { version: 'v0.9', createSurface: { surfaceId: 'flight-results', catalogId: 'basic' } },
  { version: 'v0.9', updateComponents: { surfaceId: 'flight-results', components: [
    { component: 'Heading', text: 'Flights to Paris', level: 2 },
    { component: 'List', items: { path: '/flights' }, template: {
      component: 'Card',
      children: [
        { component: 'Text', text: { path: 'flightNumber' }, variant: 'bold' },
        { component: 'Text', text: { path: 'pricePerPerson' } },
      ],
    } },
  ] } },
];

Or compose against catalogue-specific helpers from @cool-ai/beach-a2ui-basics, which extends @a2ui/web_core's basic catalogue with Beach-shaped primitives (Pill, etc.) registered via the catalogue mechanism.

Rendering

Browser

Browser DOM rendering is delegated entirely to @a2ui/lit:

import { A2UIClient } from '@a2ui/lit';

const client = new A2UIClient(document.getElementById('workspace')!);
client.handle(surface);

Server (email, WhatsApp, SMS, voice)

Server-side rendering ships in @cool-ai/beach-a2ui-renderer-lit/render-server. Two pairs of renderers cover Beach's outbound delivery surfaces — a Lit-based pair for the live-runtime path and a static pair for batched non-browser channels:

Lit-based (browser-shaped, async, jsdom + Lit):

  • renderToHtml(commands, options?) — email-client-compatible HTML. Runs @a2ui/lit under jsdom; serialises the rendered DOM tree (including shadow roots); applies juice to inline styles. options.inlineCss = false skips the juicer pass.
  • renderToText(commands, options?) — plain text via DOM-tree walker with semantic HTML rules (block elements newline-separate, <a> becomes text (url), <button> becomes [ text ], headings prefix line breaks, list items bullet). For WhatsApp / SMS / voice composers.

Static (Lit-free, synchronous, no jsdom):

  • renderToStaticHTML(commands, options?) — same input, different output strategy: synchronous walk of the SurfaceCommand stream, table-based markup, every style inline, no custom-element tags. The channel-safety contract is codified in assertEmailSafe(html) (also exported); every Beach test runs its output through it.
  • renderToStaticText(commands, options?) — sibling to the above for plain text.

Use the static pair when the consumer never reaches a browser: outbound email, WhatsApp summary, archived render, MIME plainText alternative. Use the Lit pair when the same surface that will reach a browser also needs a server render and you want byte-equal output across both paths.

import { renderToHtml, renderToText } from '@cool-ai/beach-a2ui-renderer-lit/render-server';

const html = await renderToHtml(commands);   // email body
const text = await renderToText(commands);   // WhatsApp message body

The architectural commitment for the Lit-based pair: Beach does not walk the SurfaceCommand stream itself. @a2ui/lit walks it (Google's reference renderer); Beach adapts the resulting DOM tree. New A2UI components — Card v2, novel layouts, future primitives — get text + HTML output for free because they compose existing HTML semantics.

The static pair walks the stream directly because the channels it serves (email, WhatsApp, archive) cannot accept custom-element tags. Per-component emission rules live in render-server/static/emit-html.ts and emit-text.ts; data-bindings ({ path }) resolve at compose time against the accumulated updateDataModel state via RFC-6901 JSON-Pointer; function-call bindings throw A2uiStaticRenderFunctionCallError (the live runtime owns function-call evaluation, not the static walker). Consumer-catalogue components plug in via the extensions option: each extension receives resolved props (data-bindings already evaluated) plus the pre-rendered children string, so it wraps the inner content in domain-specific markup without re-implementing basic-catalogue emission.

import { renderToStaticHTML, assertEmailSafe } from '@cool-ai/beach-a2ui-renderer-lit/render-server';

const html = renderToStaticHTML(commands, {
  extensions: {
    DestinationCard: (props, renderedChildren) =>
      `<table><tr><td><strong>${props.destinationName}</strong>${renderedChildren}</td></tr></table>`,
  },
});

assertEmailSafe(html); // throws A2uiEmailSafetyError if anything would break in Gmail / Outlook

Custom catalogues

Both renderers accept an additional catalogs option for surfaces that use consumer catalogues alongside the default basic catalogue:

import { renderToHtml, renderToText } from '@cool-ai/beach-a2ui-renderer-lit/render-server';
import type { Catalog } from '@cool-ai/beach-a2ui-renderer-lit/render-server';
import { taCatalogue } from '@my-org/ta-catalogue';

const html = await renderToHtml(commands, { catalogs: [taCatalogue] });
const text = await renderToText(commands, { catalogs: [taCatalogue] });

The same Catalog type also re-exports from @cool-ai/beach-a2ui-renderer-lit for catalogue authoring. Two constraints:

  1. @a2ui/lit flavour, not @a2ui/web_core flavour. Catalogues passed here must register their custom elements via @customElement at import time — the same shape A2uiBridge({ catalogues }) accepts on the browser. A data-only @a2ui/web_core catalogue without Lit elements will not render.
  2. SSR-compatible component implementations. Component classes get loaded inside the jsdom render bootstrap, so they cannot rely on browser-only APIs (window.fetch, click handlers firing during render, network calls in connected lifecycle hooks). The render pass is one-shot serialisation; any work that assumes a live browser does not apply.

basicCatalog is always registered regardless of the catalogs value; the list is additive. Passing catalogs: [], undefined, or omitting the field are equivalent.

The renderer awaits every Lit element in the surface subtree, not just the outer <a2ui-surface> host. Consumer-catalogue elements with deferred internal renders are settled before the rendered tree is serialised. If a custom element fails to settle within a bounded retry budget the renderer throws rather than emitting an empty wrapper; that loud failure is intentional — the alternative (silent empty output) hides real rendering bugs.

Merging catalogues

A2UI v0.9 binds each surface to exactly one catalogue. A surface that uses Card (basic) wrapping Show (basics) wrapping a custom domain component needs all three resolvable from one catalogue. mergeCatalogues() from @cool-ai/beach-a2ui-renderer-lit (also re-exported from @cool-ai/beach-a2ui-renderer-lit/render-server and @cool-ai/beach-a2ui-renderer-lit/catalogues) builds that single catalogue:

import { basicCatalog } from '@a2ui/lit/v0_9';
import { beachBasicsCatalog } from '@cool-ai/beach-a2ui-basics';
import { mergeCatalogues } from '@cool-ai/beach-a2ui-renderer-lit';

const taCatalogue = mergeCatalogues(
  'https://travel-assistant.cool-digital.co.uk/a2ui/v0.9/catalogue.json',
  [basicCatalog, beachBasicsCatalog, taOwnComponents],
);

// Surfaces created with `catalogId: taCatalogue.id` resolve every
// component from any source catalogue under one Map lookup.
const html = await renderToHtml(commands, { catalogs: [taCatalogue] });

Collision rules:

  • Component-name collisions: default onCollision: 'error' throws A2uiCatalogueMergeError naming both source catalogue ids. Use 'first-wins' to make basic-catalogue entries authoritative; use 'last-wins' for consumer-side overrides of basic entries.
  • functions map: only one source catalogue may carry a non-empty functions map; the helper throws if two do. Function merging is intentionally not automatic — consumers should build a single source-of-truth catalogue and pass that explicitly.
  • themeSchema: only one theme schema may be present; identity-equal references are accepted (same theme passed through multiple catalogues). Different schemas throw.

Each merged entry preserves its tagName and other extra fields verbatim from the source — the helper is a structural combine, not a re-shape. Merged catalogues can themselves be merged again (the operation is associative for non-conflicting inputs).

Wire into @cool-ai/beach-format's A2uiContentRenderer:

import { A2uiContentRenderer } from '@cool-ai/beach-format';
import { renderToHtml, renderToText } from '@cool-ai/beach-a2ui-renderer-lit/render-server';

const a2uiRenderer = new A2uiContentRenderer({
  html: (commands) => renderToHtml(commands as SurfaceCommand[]),
  text: (commands) => renderToText(commands as SurfaceCommand[]),
});

The render-server subpath has runtime deps on jsdom and juice and a peer-dep on @a2ui/lit. Consumers using only bridge / host-fit (no server rendering) do not pull these in.

Defensive instrumentation — validateCatalogues and renderer error types

renderToHtml and renderToText invoke validateCatalogues inline before processing commands. The validator catches the two catalogue-shape failures that previously surfaced as silent <a2ui-surface></a2ui-surface> output:

  1. A @a2ui/web_core data-only catalogue (no tagName on entries) was passed where a @a2ui/lit-flavoured catalogue is expected — throws A2uiRendererCatalogueShapeError with reason: 'missing-tagName'.
  2. A Lit catalogue module was imported before bootstrapServerRender() provisioned jsdom's customElements registry, so the @customElement decorator's customElements.define() call no-op'd — throws A2uiRendererCatalogueShapeError with reason: 'unregistered-tagName'.

After the render pass, an empty-render guard catches the remaining three failure modes (unknown component name, consumer-catalogue element's render() returned nothing, top-level component id ≠ "root") by throwing A2uiRendererEmptyRenderError with the affected surfaceIds.

You can call validateCatalogues directly from your own boot path — after a dynamic catalogue import — to surface failures at module load rather than at first render:

import { bootstrapServerRender, validateCatalogues } from '@cool-ai/beach-a2ui-renderer-lit/render-server';

await bootstrapServerRender();
const { taCatalogue } = await import('@your-org/ta-a2ui-catalogue');
validateCatalogues([taCatalogue]);  // throws now, not at first render

Exported error types (A2uiRendererError base, A2uiRendererCatalogueShapeError, A2uiRendererEmptyRenderError) carry catalogueId / componentName / reason / surfaceIds / commandCount fields so consumers can route on specific failure modes if needed.

bridge — id-order tracker

Some progressive-update patterns receive a list of IDs early (a discover-grid populated with destinations) and per-ID detail later (hotel results for one destination). To target the later updates with a JSON-Pointer updateDataModel path, the integration layer needs the index of each ID within the original ordered list. createIdOrderTracker is the small piece of Beach-specific glue that records and looks up that mapping.

import { createIdOrderTracker } from '@cool-ai/beach-a2ui-renderer-lit/bridge';

const tracker = createIdOrderTracker();
tracker.set('discover-grid', ['Paris', 'Rome', 'Tokyo', 'Lima']);

const idx = tracker.indexOf('discover-grid', 'Rome');  // 1
// updateDataModel path:  `/destinations/${idx}/hotels`

Tiny, no external dependencies, scope-it-however-the-consumer-wants.

host-fit — viewport conventions

Catalogue components that adapt to their host (chat panel vs. full-page surface vs. modal) need a uniform way to discover the host's intended sizing. parseHostConfig reads the --beach-host-config CSS custom property — a JSON blob describing breakpoints, container types, and other host hints — and returns a typed config consumers can read inside their components.

import { parseHostConfig, BREAKPOINT_NARROW } from '@cool-ai/beach-a2ui-renderer-lit/host-fit';

const config = parseHostConfig(myComponent);
if (config.maxWidth !== undefined && config.maxWidth < BREAKPOINT_NARROW) {
  // narrow-host layout
}

This convention exists so catalogue components can use @container queries against the surface's allocated inline size, not the viewport. The renderer no longer applies container-type: inline-size automatically — that lived in the retired Beach renderer; consumers who use @a2ui/lit apply the same CSS in their own stylesheet (.a2ui-surface { container-type: inline-size; }).

Coexisting with bespoke part renderers

A consumer with stateful or proprietary UI (an itinerary builder with slot-bound state, a multi-centre timeline) runs their own renderer alongside @a2ui/lit. The pattern is to dispatch envelope parts by partType:

envelopeStream.on('part', (part) => {
  switch (part.partType) {
    case 'a2ui-surface':
      a2uiClient.handle(part.data);         // @a2ui/lit
      break;
    case 'ta.itinerary-slot-state':
      itineraryRenderer.apply(part.data);   // consumer-owned
      break;
  }
});

a2ui-surface and consumer-defined types are siblings in the envelope. Both reach the browser via the same SSE stream, dispatched by partType. Beach's scope here is the dispatch + the bridge helpers above; the actual rendering is Google's reference renderer plus consumer-owned bespoke renderers.

SurfaceComponent type schema

SurfaceComponent is Beach's typed authoring vocabulary for surface components. It is a discriminated union where the type field identifies the component kind. Handlers use it to produce typed a2ui-surface parts; a builder layer (forthcoming) translates SurfaceComponent[] to the A2UI v0.9 wire shape (SurfaceCommand[]) consumed by @a2ui/lit.

Note the naming convention: SurfaceComponent uses type: as the discriminant ({ type: 'Text', text: 'hello' }). The A2UI v0.9 wire shape uses component: ({ component: 'Text', text: 'hello' }). These two layers are distinct — SurfaceComponent is the Beach authoring surface; SurfaceCommand is the wire format sent to the browser.

import type { SurfaceComponent } from '@cool-ai/beach-a2ui-renderer-lit';

const components: SurfaceComponent[] = [
  { type: 'Heading', text: 'Flights to Paris', level: 2 },
  { type: 'List', items: { path: 'flights' }, template: {
    type: 'Card',
    children: [
      { type: 'Text', text: { path: 'flightNumber' }, variant: 'bold' },
      { type: 'Text', text: { $: 'formatNumber', value: { path: 'price' }, style: 'currency', currency: 'GBP' } },
      { type: 'Button', label: 'Book', action: { type: 'submit', payload: { flightId: 'id' } } },
    ],
  } },
];

Bindable<T> — literal or data-model binding

Any field typed as Bindable<T> accepts either a literal value ("hello", 99) or a PathRef ({ path: 'user.name' }) that is resolved at render time from the surface's data model.

Format expressions

FormatExpression produces a display string at render time:

{ $: 'formatNumber', value: { path: 'price' }, style: 'currency', currency: 'GBP' }
{ $: 'formatDate',   value: { path: 'departureDate' }, format: 'short' }
{ $: 'formatString', template: 'Hello, {name}!', values: { name: { path: 'user.name' } } }

Full export list

SurfaceComponent and all constituent interfaces (TextComponent, HeadingComponent, BadgeComponent, ImageComponent, IconComponent, RowComponent, ColumnComponent, CardComponent, ListComponent, DividerComponent, ButtonComponent, TextInputComponent, CheckBoxComponent, ChoicePickerComponent, TabsComponent, ComparisonTableComponent, TimelineComponent, PriceBreakdownComponent, RatingComponent, GalleryComponent) plus supporting types (PathRef, Bindable, FormatExpression, ButtonAction, TabItem, ComparisonTableColumn, PriceLine, PriceLineVariant, GalleryItem) are all exported from the package root.

What this package no longer contains

Beach's parallel A2UI renderer layer has been retired. Specifically removed:

  • Typed component constructors (text, heading, card, button, …). Author component object literals directly or use @cool-ai/beach-a2ui-basics's catalogue helpers.
  • The vanilla-DOM A2UIRenderer and the original renderHtml server-side renderer. Browser rendering uses @a2ui/lit; server-side rendering ships in the @cool-ai/beach-a2ui-renderer-lit/render-server subpath.
  • The ./builder, ./renderer, and ./html subpath exports.

Not in this package

  • The envelope builder that includes the surface part (@cool-ai/beach-protocol).
  • Channel delivery of surface messages (@cool-ai/beach-transport).
  • Browser rendering (@a2ui/lit, Google's reference).
  • Catalogue extensions (@cool-ai/beach-a2ui-basics, plus consumer-defined catalogues).

Consumers

Any agent with a UI. Pure API-serving agents skip this package.

Related