Skip to content
KaufmannCommerce UI
Foundations · v0.1⌘ K

Kaufmann Commerce UI / Foundation

Astro architecture

Server-first components, stable TypeScript projections, and an optional WebSocket adapter—without inventing a backend contract that does not exist yet.

One foundation, three small layers

@kaufmann/ui owns the canonical tokens and primitives. Commerce compositions build Product Card, Stock Signal, and Cart Panel from them. Transport adapters translate future WebSocket messages into small projection events.

This keeps the API stable: a statically rendered Product Card needs no client runtime. A cart can use the same types and load a script only when live behaviour is actually required.

The commerce documentation is therefore an additional catalogue of the same Kaufmann UI—not a theme, fork, or second primitive system.

WaterlineThe UI may display events and bridge them optimistically. It may not turn a received payload into inventory, price, or checkout truth.
Astro
---
import { ProductCard } from "@kaufmann/ui";
import "@kaufmann/ui/styles.css";
---

<ProductCard product={offerProjection} />

The event seam is typed

Islands stay loosely coupled by talking over DOM custom events. Every name, payload and helper lives in one module — @kaufmann/ui/events — and no kaufmann:* string literal is written anywhere else. The module is framework-free, so a zero-JS seam script can import it without pulling in a framework.

Several events are deliberately two-way: an island both dispatches its own event and listens for it. That is what lets the storefront's zero-results reset move the collection pill and the search input, not merely re-filter the grid.

EventDirectionDetailDispatched byHeard by
kaufmann:collection-changeTwo-way{ collectionId }CollectionFilter, zero-results resetpage filter script, CollectionFilter
kaufmann:search-queryTwo-way{ query }StoreHeader input, zero-results resetpage filter script, StoreHeader
kaufmann:cart-openOne-wayStoreHeader cart buttonuseStorefrontCart
kaufmann:cart-updatedOne-way{ count, subtotal, currency, open }useStorefrontCartStoreHeader (badge + aria-expanded)
kaufmann:shipping-destination-changeElement-scoped{ destination }ShippingSelectorcomposing page
kaufmann:shipping-destination-syncElement-scoped{ countryCode }composing pageShippingSelector
Seams, not authorityAn event is a message between two pieces of one page. Nothing that arrives on this seam is proof that stock was held, money moved, or an Order was accepted.
Astro
// Framework-free subpath: safe in a zero-JS seam script.
import { KAUFMANN_EVENTS, dispatchKaufmann, onKaufmann } from "@kaufmann/ui/events";

dispatchKaufmann(KAUFMANN_EVENTS.collectionChange, { collectionId: "edition" });

// Returns its own unsubscribe, so an effect body is a single line.
useEffect(() => onKaufmann(KAUFMANN_EVENTS.cartUpdated, (detail) => {
  setCount(detail.count);
  setOpen(detail.open);
}), []);

// The shipping pair is element-scoped, not page-wide.
onKaufmann(KAUFMANN_EVENTS.shippingDestinationChange, apply, { target: root });

class or className

One rule, no exceptions: Astro components take class — matching the HTML attribute Astro templates already use — and React components take className, matching JSX. So OfferCard, ProductCard, EditorialHero, EditorialCard, PricingStages, StockSignal and CartPanel take class; Button, Select, StoreHeader, CollectionFilter, ShippingSelector, Reader and the cart surfaces take className.

Every surface appends the incoming value to its own k- class rather than replacing it, so passing a class extends the component and never strips its styling.