zero-memory
Platform

UI stack

The component system, theming, and localization runtime behind the zero-memory dashboard.

The zero-memory dashboard is built on a single, uniform UI stack: shadcn/ui components living in a dedicated workspace package, styled with Tailwind CSS v4, themed through CSS variables, and localized by a minimal in-house i18n runtime. Every visible surface — the memory feed, memory cards, entities, scopes, the review and incubator queues, insights, settings, and the auth pages — is composed from this one component system.

Why a single stack

A dashboard assembled from ad-hoc utility classes drifts fast: duplicated button and input styles, one-off badge components, hardcoded strings. In zero-memory, if shadcn is the component system, it is the component system everywhere, and localization is part of the foundation rather than a retrofit.

Two properties follow. Pages cannot invent their own primitives, so visual and behavioral drift has no room to start. And the components are reusable across surfaces: the documentation site you are reading is a second application on the same package, with the same stylesheet, the same PostCSS config, the same primitives. (The server's OAuth sign-in page is the deliberate exception: a single self-contained HTML page with inline styles, because it must render behind a strict content-security policy with no assets of any kind.)

What the stack is made of

The component package

All UI lives in the workspace package @workspace/ui (packages/ui), not inside the app.

Primitives are flat: CLI-generated shadcn components (button, dialog, select, …) sit directly in src/components/. Compositions are grouped by feature instead. Generic mechanisms (empty states, confirm dialogs, badge lists, search forms, dropzones, the theme switcher) live in components/common/, and domain compositions get one folder per surface: dashboard/, memory/, entity/, scope/, review/, rules/, reflections/, portability/, settings/, and auth/. A new surface adds a folder; it does not add a second component system.

The headless primitive layer is @base-ui/react, the successor library from the ex-Radix team, with the same visual preset shadcn styles are generated against. Primitives are always added through the shadcn CLI, and hand-written Radix imports do not appear in the package. Charts are a primitive too: the insights charts and sparklines are built on a chart primitive wrapping recharts, themed from the same CSS variables as everything else. Icons come from lucide-react; class composition uses class-variance-authority, tailwind-merge, and clsx.

Styling and theming

Tailwind v4 runs CSS-first. The package's globals.css imports Tailwind, the animation layer, and the shadcn base styles, declares a dark custom variant, and defines the theme as CSS variables via @theme inline. An application adopts the whole system with two lines: importing @workspace/ui/globals.css in its root layout and re-exporting the package's PostCSS config.

Theme switching is driven by next-themes; the switcher is a package component while the provider stays in the app. Colors are referenced only through theme CSS variables — never raw palette classes.

The package boundary

The boundary of @workspace/ui is strict in both directions. Application code never renders a bare <button>, <input>, or <table>; pages are assembled exclusively from package components. In the other direction the transfer is maximal: anything UI-pure belongs to the package, including heavy, product-specific components like the memory card or feed filters.

Infrastructure enters by inversion. Network calls, database clients, and server actions never leak into the package. Components declare callback interfaces (onSubmit, onSearch, loadMore, …) and the application injects implementations. What remains in the app is wiring: data fetching, actions, routing, and auth.

Localization

Instead of a heavyweight i18n framework, the stack uses a small purpose-built runtime in @workspace/i18n-catalogs. Flat JSON catalogs map key → template with {token} interpolation, one file per locale, and each locale is lazy-loaded through a statically resolvable dynamic import(), so the client only ever downloads the locale it needs. A manifest of domains × locales is the single source of truth, and a validation step in the lint gate checks that every locale of every domain carries the full key set. A partially translated catalog cannot land.

There is more than one consumer. Besides the dashboard catalog, the outgoing authentication mail has its own domain of the same shape. It is loaded with plain static imports rather than the chunked loader, because mail is rendered on the server and never reaches a browser bundle, and it declares a narrower locale list than the product's: those emails are rendered to fixed templates that are chosen without knowing the recipient's language, so they ship in English. Adding a language there is adding a locale to the manifest and a catalog file, not re-laying-out the emails.

Missing keys are visible. The translator t(key, vars) returns the key itself when a translation is absent, so a broken string shows on screen instead of silent emptiness. There is no provider and no context: a server component loads the catalog and passes a plain messages object as a prop, and the client component memoizes a translator from it. @workspace/ui itself is i18n-agnostic, since package components receive already-translated strings as props and never depend on the catalogs.

Supported product locales are en (default) and es. The locale is resolved per request from the Accept-Language header — an exact match first, then the base language, then the default — and there is no locale segment in URLs.

Why custom i18n, and why a package

Why a custom i18n runtime? At dashboard scale, dozens of keys rather than thousands, plural rules and ICU syntax and localized routing are pure overhead. The runtime is on the order of a hundred lines, works natively with React Server Components, and if plurals or ICU ever become necessary, the runtime can be swapped while the catalogs stay unchanged.

Why a package instead of an app folder? Multiple UI surfaces share the components, and a hard package boundary is what makes the "no raw primitives" rule enforceable rather than aspirational.

On this page