Skip to content
Merged
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
17 changes: 6 additions & 11 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,16 @@ 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
`scripts/generate-exports.mjs` rather than hand-written — keeping a module internal is
the cheap direction. `lib/chart-geometry` is internal: it is rendering machinery whose
**The public surface is a semver commitment.** Everything `src/index.ts` re-exports, and
every entry in `exports`, is something we have promised not to break casually. Both are
hand-written; a module not listed is internal, and keeping a module internal is the cheap
direction. Only modules that import an optional peer get their own subpath. `lib/chart-geometry` is internal: it is rendering machinery whose
names appear in no public prop type. `lib/chart-palette` is public — a consumer painting
its own data marks needs the same ramp, in the same order, under the same rules.

**Tree-shaking is a requirement.** Output is per-file ESM — one `.js` and one `.d.ts`
per source file, no bundling — and `sideEffects` is declared CSS-only. The root entry is
re-exports only, so importing from the root and importing by subpath both bundle just
what you used. They are not guaranteed byte-for-byte identical — a bundler may order or
name things differently depending on the entry — but neither drags in a component you did
not reference.
re-exports only, so importing from the root bundles just what you used.

**Abstraction has to earn its place.** No config indirection, no speculative extension
points. There is no CLI, no component generator and no docs site, and that is deliberate.
Expand All @@ -28,7 +25,6 @@ points. There is no CLI, no component generator and no docs site, and that is de

| Step | Tool | Output |
| --- | --- | --- |
| `generate` | `scripts/generate-exports.mjs` | `src/index.ts` and the `exports` map in `package.json` |
| `build:js` | SWC | `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 |
Expand Down Expand Up @@ -188,8 +184,7 @@ and numbers out, checkable without rendering anything).
| `src/blocks/` | Multi-file compositions (`login`). |
| `src/theme.css` | Tokens, keyframes, base layer. The only CSS source. |
| `src/styles.css` | Two `@import`s. The entry Tailwind compiles into `dist/styles.css`. |
| `src/index.ts` | The root barrel. **Generated** — do not edit. |
| `scripts/generate-exports.mjs` | Writes `src/index.ts` and the `exports` map. Holds the internal-module triage list. |
| `src/index.ts` | The root barrel. Hand-written: add a public module here. |
| `scripts/contrast-test.mjs` | The theme gate. Reads `dist/styles.css`. |
| `scripts/dep-guard.mjs` | The forbidden-import gates. |
| `dist/` | Build output. Generated, not committed; the only thing `files` publishes. |
Expand Down
9 changes: 7 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ All notable changes to `@corbits/react-ui` are documented here. The format follo
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

Until 1.0, a minor bump may contain a breaking change; breaking changes are always called
out under their own heading. Every subpath in the package's `exports` map is part of that
contract — modules that are not exported are internal and may change in any release.
out under their own heading. Every entry in the package's `exports` map is part of that
contract, and so is everything the root re-exports — modules that are not exported are internal and may change in any release.

## [Unreleased]

Expand Down Expand Up @@ -73,6 +73,11 @@ contract — modules that are not exported are internal and may change in any re
`StepListItem` and `StepDisplayStatus` stay public from `ui/step-list`;
`GateShellModel` and `GateKind` from `ui/gate-block`. For `cn`, compose `clsx` and
`tailwind-merge` directly: `twMerge(clsx(...inputs))`.
- **Per-module subpaths are removed; import from the root.** The `exports` map is now
`.`, the six optional-peer modules below, `styles.css`, `theme.css` and
`package.json`. `@corbits/react-ui/ui/button` and every other per-module subpath no
longer resolve: `import { Button } from "@corbits/react-ui"`. The root barrel is
hand-written and `scripts/generate-exports.mjs` is gone.
- **Four peers are optional and their modules are subpath-only.** `@radix-ui/react-dialog`,
`@radix-ui/react-dropdown-menu`, `@radix-ui/react-tooltip` and `sonner` move to
`peerDependenciesMeta`. `ui/dialog`, `ui/command-palette`, `ui/mic-permission-dialog`,
Expand Down
27 changes: 12 additions & 15 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ once that is clear.

## How it works

Components never fetch; data arrives as props. The root entry is generated re-exports
Components never fetch; data arrives as props. The root entry is hand-written re-exports
only, and the package is side-effect free apart from its CSS, so either import style
tree-shakes. Tokens and keyframes live in `theme.css`; `styles.css` is the prebuilt sheet
for hosts that do not run Tailwind. [PRODUCT.md](./PRODUCT.md) covers why the library
Expand All @@ -26,7 +26,7 @@ consumer's data layer, not in a component — data arrives as props.
## Gates

```sh
bun run build # generate → SWC → tsc → Tailwind → the contrast gate
bun run build # SWC → tsc → Tailwind → the contrast gate
bun run typecheck # tsc --noEmit
bun run lint # eslint .
bun run dep-guard # forbidden-import checks over src/
Expand Down Expand Up @@ -65,7 +65,7 @@ secret is a guard nobody can comply with.
in the *consumer's* install, where they cannot fix it.
2. **Nothing reachable from the root barrel imports an optional peer.** Re-exporting a
module that statically imports an optional peer makes that peer mandatory for every
root import, so such a module must be kept out of the barrel (`BARREL_EXCLUDED`).
root import, so such a module must be kept out of the barrel and given its own `exports` entry.

The same rule, not machine-checked, applies to server-side packages: a component must
never import one. They carry database drivers, and pulling one into a browser bundle is a
Expand All @@ -78,13 +78,12 @@ around it.

1. Write the file under `src/{ui,lib,hooks,blocks}/`. Imports of sibling modules are
**relative and carry a `.js` extension** (`../lib/utils.js`) — there is no path alias.
2. Do not edit `src/index.ts` or the `exports` map in `package.json`. Both are generated;
`bun run build` picks the new file up.
3. If the module is machinery rather than public API, add it to `INTERNAL` in
`scripts/generate-exports.mjs`. Keeping something internal is cheap; taking a subpath
back after it ships is not. If it is public but statically imports an *optional* peer,
add it to `BARREL_EXCLUDED` in the same file instead — it keeps its subpath but must
stay out of the root barrel. `dep-guard` fails if you forget.
2. If it is public API, add `export * from "./<dir>/<name>.js";` to `src/index.ts`.
Machinery stays unlisted: keeping something internal is cheap; taking an export back
after it ships is not.
3. If it is public but statically imports an *optional* peer, leave it out of
`src/index.ts` and add a `./<dir>/<name>` entry to `exports` in `package.json` instead.
`dep-guard` fails if it reaches the barrel.
4. If a `ui/` or `blocks/` module calls a hook, creates a context, defines an inline
event handler, or imports an optional peer, start it with `"use client"`;
`@radix-ui/react-slot` alone does not count. Leave stateless components and
Expand Down Expand Up @@ -133,11 +132,9 @@ every published component, running against `src/theme.css` so what you see is wh
consumer gets. `bun run stories:build` produces a static build of the same canvas for
anyone who wants to check it without running the dev server.

Stories live under `stories/`, not next to their component in `src/`. `src/` is walked to
generate the `exports` map (see [ARCHITECTURE.md](./ARCHITECTURE.md)); a colocated
`*.stories.tsx` would either leak into that map or need its own entry in `INTERNAL` for
every single story file. Keeping them in a separate top-level directory keeps the public
surface exactly the set of files that ship, with nothing to remember to exclude.
Stories live under `stories/`, not next to their component in `src/`. SWC and `tsc`
compile all of `src/`, so a colocated `*.stories.tsx` would ship in `dist/`. Keeping them
in a separate top-level directory keeps `dist/` exactly the set of files that matter.

The theme toggle in Ladle's own top bar switches the real `.dark` class from
`src/theme.css` — not a canvas-only colour swap — so a component's dark-mode tokens,
Expand Down
3 changes: 1 addition & 2 deletions IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Why the library exists lives in [PRODUCT.md](./PRODUCT.md).
- Required peers: `react` and `react-dom` (`^18.2.0 || ^19.0.0`),
`lucide-react` (`^0.545.0 || ^1.0.0`), `@radix-ui/react-slot` (`^1.2.3`).
- Optional peers (`peerDependenciesMeta`), each imported only by subpath-only
modules kept out of the root barrel (`BARREL_EXCLUDED`):
modules kept out of the root barrel, each with its own `exports` entry:
`@radix-ui/react-dialog` (`^1.1.15`; `ui/dialog`, `ui/command-palette`,
`ui/mic-permission-dialog`), `@radix-ui/react-dropdown-menu` (`^2.1.16`;
`ui/menu`), `@radix-ui/react-tooltip` (`^1.2.8`; `ui/tooltip`), `sonner`
Expand All @@ -27,7 +27,6 @@ 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 |
Expand Down
5 changes: 2 additions & 3 deletions PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ 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.
- Import from the root (`@corbits/react-ui`) and bundle only what they used.
- Render collection surfaces against *their* data. Components never fetch.
Data arrives as props. Pieces that take no collection need no extra
host wiring.
Expand Down Expand Up @@ -51,6 +50,6 @@ bundled. The stack falls through to system fonts unless the host loads them.

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

The public surface is a semver commitment: every subpath in `exports` is
The public surface is a semver commitment: everything the root re-exports, and every entry 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).
Loading
Loading