Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Architecture

`@corbits/react-ui` is an installed npm component library: the code lives in the
consumer's `node_modules` and is picked up by a version bump. Three properties shape
the layout.
consumer's `node_modules` and is picked up by a version bump. Why it exists is
in [PRODUCT.md](./PRODUCT.md); toolchain, peers and publish layout are in
[IMPLEMENTATION.md](./IMPLEMENTATION.md). Three properties shape the layout.

**The public surface is a semver commitment.** Every subpath in `exports` is something
we have promised not to break casually, so the map is *generated* from a triage list in
Expand Down Expand Up @@ -52,7 +53,10 @@ Components in this package **never fetch**, and never import a server-side packa
component that fetches has chosen the consumer's data layer for them; a component that
imports a server package drags a database tree into a browser bundle.

Data arrives one of two ways: as props, or through a `DataPort`.
Data arrives one of two ways: as props, or through a `DataPort`. That is the
README contract — components take data through a `DataPort` (or none). "None"
is a props-only surface; `DataPortProvider` is required only for components
that call `useDataPort`.

```ts
type DataPort = {
Expand Down Expand Up @@ -153,9 +157,13 @@ Tailwind resolves `@source` relative to the file containing it, so it means `src
build the standalone sheet and `dist/` when a consumer imports it out of `node_modules`.
Since there is only one file, the tokens and keyframes cannot drift between the two.

Dark mode is opt-in through a `dark` class on an ancestor. `ThemeProvider`
owns applying that class (and `data-theme` presets + `color-scheme`) and
persisting the choice; the host mounts the provider and keys storage per user.
Dark mode is a `dark` class on an ancestor. The stylesheet reads it
(`@custom-variant dark (&:is(.dark *))`); it does not manage it. A host can
set the class itself (`<html className="dark">`). `ThemeProvider` is an
optional helper that applies that class (and `data-theme` presets plus
`color-scheme`) and persists the choice; the host mounts it and keys storage
per user. `useTheme` / `ThemeToggle` require the provider; every other
component does not.

### Contrast is gated

Expand Down
86 changes: 86 additions & 0 deletions IMPLEMENTATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Implementation

Concrete toolchain, peers, formats and publish layout. Design of the
`DataPort` seam and the theme layer lives in [ARCHITECTURE.md](./ARCHITECTURE.md).
Why the library exists lives in [PRODUCT.md](./PRODUCT.md).

## Runtime

- **Node.js** `>=24` (`engines` in `package.json`).
- Package name `@corbits/react-ui`, ESM (`"type": "module"`), license
`LGPL-2.1-only`.
- Required peers: `react` and `react-dom` (`^18.2.0 || ^19.0.0`),
`lucide-react` (`^0.545.0 || ^1.0.0`), `sonner` (`^2.0.7`),
`@radix-ui/react-dialog` (`^1.1.15`), `@radix-ui/react-dropdown-menu`
(`^2.1.16`), `@radix-ui/react-slot` (`^1.2.3`),
`@radix-ui/react-tooltip` (`^1.2.8`).
- Optional peer: `@tanstack/react-query` (`^5.90.2`), needed only for
`createTanstackDataPort()`. Declared in `peerDependenciesMeta`.
- Direct dependencies: `class-variance-authority`, `clsx`, `tailwind-merge`.
- `sideEffects` is CSS-only (`**/*.css`). `files` publishes `dist/` only.

## Toolchain

Development uses **bun**. `bun run build` is four steps and a gate, joined
by `&&`:

| Step | Tool | Output |
| --- | --- | --- |
| `generate` | `scripts/generate-exports.mjs` | `src/index.ts` and the `exports` map in `package.json` |
| `build:js` | SWC (`@swc/cli`, `@swc/core`) | `dist/**/*.js`, one per source file |
| `build:types` | `tsc -p tsconfig.build.json` | `dist/**/*.d.ts` |
| `build:css` | Tailwind v4 CLI | `dist/styles.css`, plus `dist/theme.css` copied from source |
| `contrast-test` | `scripts/contrast-test.mjs` | the gate — reads `dist/styles.css` |

Source imports are relative and carry `.js` extensions. There is no path
alias. `prepack` runs the whole build so a failing gate cannot be packed.
`prepare` (`scripts/prepare.mjs`) builds `dist/` on a git install; it is a
no-op when `dist/` is already present.

Other scripts: `bun run typecheck`, `bun run lint` (eslint),
`bun run dep-guard` (`scripts/dep-guard.mjs`), `bun test`, `bun run stories`
/ `stories:build` ([Ladle](https://ladle.dev) over `stories/`).

## CSS artifacts

Two published sheets, both generated from `src/theme.css`:

- `@corbits/react-ui/styles.css` → `dist/styles.css`. Tailwind compiles
`src/styles.css` (`@import "tailwindcss"` then `./theme.css`). The host
needs no Tailwind.
- `@corbits/react-ui/theme.css` → `dist/theme.css`, a byte copy of
`src/theme.css`. For a host already on Tailwind v4.

Dark mode is the CSS custom variant `@custom-variant dark (&:is(.dark *));`.
The sheet reads a `dark` class on an ancestor; it does not set it. Named
presets are `data-theme` values (`default` / `warm` / `cool`). Brand faces
are `--font-sans: "Red Hat Display", …` and `--font-mono: "Space Mono", …`;
no font files ship.

`ThemeProvider` (`src/ui/theme-provider.tsx`) is optional host wiring. It
writes `.dark`, `data-theme`, and `color-scheme` on a root (default
`document.documentElement`) and persists `{ mode, preset }` JSON under
`localStorage` key `corbits-theme` (override with `storageKey`). Default
mode is `"system"`. `ThemeToggle` / `useTheme` require the provider; the
rest of the library does not.

## DataPort adapter

`src/lib/data-port.ts` is the seam type plus `DataPortProvider` /
`useDataPort`. `src/lib/tanstack-data-port.ts` is the one implementation:
`useInfiniteQuery` from `@tanstack/react-query`, `initialPageParam: 0`,
`getNextPageParam` from `page.nextOffset`. That file is the only public
module on `BARREL_EXCLUDED` in `scripts/generate-exports.mjs` — import it by
subpath `@corbits/react-ui/lib/tanstack-data-port`. The host must already
have a TanStack `QueryClientProvider`.

`dep-guard` fails if anything reachable from `src/index.ts` imports an
optional peer, if any file other than the adapter imports
`@tanstack/react-query`, or if any file imports `@workbench/*`.

## Publish

Registry install is the documented path (`npm` / `pnpm` / `yarn` /
`bun add @corbits/react-ui`). `publishConfig.access` is `public`. A git
install still works via `prepare`, and under bun that requires
`trustedDependencies: ["@corbits/react-ui"]`.
55 changes: 55 additions & 0 deletions PRODUCT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Product

`@corbits/react-ui` is the Corbits component library for React. It exists so a
host can ship agent and workflow surfaces that already look finished — chat,
runs, schedules, artifacts, analytics, collections — without taking a data
layer, a backend, or a docs site from this package.

The GitHub description is the product claim: **ship agentic UIs that feel
finished.** The code lives in the consumer's `node_modules` and is picked up
by a version bump.

## Who it is for

Teams building an agent or workflow product in React. The host already has an
app, a router, and a source of data. This package is the visual and
interaction layer, not the product.

It is not a design-system starter, not a CLI, and not a template app.

## What a host can do

- Import one module at a time (`@corbits/react-ui/ui/button`) or from the
root barrel, and bundle only what they used.
- Render collection surfaces against *their* data. Components never fetch.
Data arrives as props, or through a `DataPort` the host provides. Pieces
that take no collection need no port at all.
- Style with a single prebuilt sheet (`@corbits/react-ui/styles.css`) and no
Tailwind, or import `@corbits/react-ui/theme.css` into an existing Tailwind
v4 build.
- Choose dark mode by putting a `dark` class on an ancestor. The stylesheet
reads that class; the host owns switching. Mounting `ThemeProvider` is
optional — persistence and named presets, not a requirement to go dark.
- Mark their own React Server Components boundary. This package ships no
`"use client"` directives.

## What it is not

There is no CLI, no component generator, and no docs site. There is no
server, no database, and no default backend. `@tanstack/react-query` is
optional and only required if the host uses `createTanstackDataPort()`.

The prebuilt stylesheet is not inert: it restyles the consuming page
(Tailwind preflight plus a base layer). Import it at the root of an app that
is willing to hand over those defaults, or use `theme.css` instead.

Brand faces (Red Hat Display, Space Mono) are named by the theme and not
bundled. The stack falls through to system fonts unless the host loads them.

## License

LGPL-2.1-only. See [LICENSE](./LICENSE).

The public surface is a semver commitment: every subpath in `exports` is
something we have promised not to break casually. Until 1.0 a minor bump may
still contain a breaking change; those are called out in [CHANGELOG.md](./CHANGELOG.md).
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Import the prebuilt stylesheet once at the app root (no Tailwind build required)
import "@corbits/react-ui/styles.css";
```

That sheet includes Tailwind preflight and a base layer — it restyles the page. If you already use Tailwind v4, import `@corbits/react-ui/theme.css` instead and let your own build generate utilities. Dark mode is a `dark` class on an ancestor; the library reads it and does not manage it.
That sheet includes Tailwind preflight and a base layer — it restyles the page. If you already use Tailwind v4, import `@corbits/react-ui/theme.css` instead and let your own build generate utilities. Dark mode is a `dark` class on an ancestor; the stylesheet reads it and does not manage it. Mount `ThemeProvider` only if you want the library to persist the choice and apply named presets.

```tsx
import { Button } from "@corbits/react-ui/ui/button";
Expand Down Expand Up @@ -66,7 +66,7 @@ This package ships no `"use client"` directives. In a React Server Components ap

Components take data through a `DataPort` (or none). The root entry is re-exports only and the package is side-effect free aside from the CSS files, so either import style tree-shakes to what you used. Tokens and keyframes live in `theme.css`; `styles.css` is the prebuilt sheet for hosts that do not run Tailwind.

See [ARCHITECTURE.md](./ARCHITECTURE.md) for the DataPort seam and the theme layer.
See [PRODUCT.md](./PRODUCT.md) for why this library exists, [ARCHITECTURE.md](./ARCHITECTURE.md) for the DataPort seam and the theme layer, and [IMPLEMENTATION.md](./IMPLEMENTATION.md) for toolchain and peers.

## Development

Expand Down