diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 72a9063d..ea738b24 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -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.
@@ -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 |
@@ -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. |
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8bd47e80..1054611c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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]
@@ -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`,
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c93d02fb..3824314c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -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
@@ -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/
@@ -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
@@ -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 "./
/.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 `.//` 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
@@ -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,
diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md
index e1de2127..9a344c1b 100644
--- a/IMPLEMENTATION.md
+++ b/IMPLEMENTATION.md
@@ -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`
@@ -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 |
diff --git a/PRODUCT.md b/PRODUCT.md
index 40682bcb..e0b88882 100644
--- a/PRODUCT.md
+++ b/PRODUCT.md
@@ -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.
@@ -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).
diff --git a/README.md b/README.md
index 6b66c673..930c6c0d 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ React components for agent products: chat transcripts with reasoning and tool ca
1. **Props in, markup out.** No component fetches, subscribes or holds a client. Your data layer stays yours, and every component renders the same in stories, tests and production.
2. **Agent-shaped building blocks.** `ChatThread` renders a message as ordered parts (text, reasoning, tool calls), and `ApprovalCard`, `GateBlock` and `StepList` cover a run that waits on a person.
-3. **Pay for what you import.** Every component has its own subpath, the package is side-effect free apart from its CSS, and heavy peers (Radix dialog, dropdown-menu, tooltip, `sonner`) are optional.
+3. **Pay for what you import.** One root entry, the package is side-effect free apart from its CSS, and heavy peers (Radix dialog, dropdown-menu, tooltip, `sonner`) are optional.
4. **Server Components ready.** Stateful modules ship `"use client"`, so a React Server Components app imports them directly. Outside RSC the directive is a no-op.
## Install
@@ -17,7 +17,7 @@ bun add @corbits/react-ui react react-dom lucide-react @radix-ui/react-slot
Requires Node 24 or later. Tested on React 19; the peer range also accepts React 18.2+.
-Optional peers are needed only by the subpaths that import them. Those modules are not in the root barrel, so import them by subpath:
+Optional peers are needed only by the modules that import them. Those modules are not in the root entry, so import them by subpath:
| Peer | Subpaths |
| ------------------------------- | ------------------------------------------------------------- |
@@ -82,7 +82,7 @@ This renders the user message, a collapsed reasoning block, a finished tool call
## Usage
-Import from the root or from a subpath; both tree-shake to what you use:
+Import from the root; it tree-shakes to what you use. Only the optional-peer modules have their own subpath:
```tsx
import { Button } from "@corbits/react-ui";
@@ -115,135 +115,135 @@ The theme names Red Hat Display and Space Mono but does not bundle them. Load th
### Components
-Subpaths are relative to `@corbits/react-ui/`. Rows marked "optional peer" are subpath-only; see [Install](#install).
+Everything imports from `@corbits/react-ui`, except rows marked "optional peer", which import from the subpath shown; see [Install](#install).
#### Chat
-| Component | Subpath | What it is |
-| --------------------- | -------------------------- | ----------------------------------------------------------- |
-| `ChatThread` | `ui/chat-thread` | Scrolling transcript of user and agent messages |
-| `AgentTurn` | `ui/agent-turn` | One agent reply: reasoning, tool calls, answer, in order |
-| `MessageBubble` | `ui/message-bubble` | One user or system message |
-| `ChatInput` | `ui/chat-input` | Auto-growing message input with a send/stop button |
-| `ChatComposer` | `ui/chat-composer` | `ChatInput` with suggestion chips for an empty conversation |
-| `ChatPanel` | `ui/chat-panel` | Docked chat frame with header and footer |
-| `PartsRenderer` | `ui/parts-renderer` | Renders a list of message parts |
-| `ReasoningBlock` | `ui/reasoning-block` | Collapsible thinking text |
-| `ToolBlock` | `ui/tool-block` | A tool call's lifecycle: running, done, failed |
-| `ToolNarrative` | `ui/tool-narrative` | What the agent did, as one expandable line |
-| `ToolPicker` | `ui/tool-picker` | Browse, search and select tools |
-| `QuickReplyChips` | `ui/quick-reply-chips` | Tappable suggested replies |
-| `TypingIndicator` | `ui/typing-indicator` | Agent-is-typing dots |
-| `ThinkingIndicator` | `ui/thinking-indicator` | Animated Corbits mark with a status label |
-| `ThinkingMark` | `ui/thinking-mark` | Decorative animated Corbits mark |
-| `ThinkingLabel` | `ui/thinking-label` | Rotating-verb status label |
-| `ShimmerText` | `ui/shimmer-text` | Shimmer sweep over text |
-| `MicButton` | `ui/mic-button` | Permission-aware microphone toggle |
-| `VoiceWaveform` | `ui/voice-waveform` | Live audio level bars |
-| `DictationStatusLine` | `ui/dictation-status-line` | Composer dictation status row |
-| `MicPermissionDialog` | `ui/mic-permission-dialog` | Microphone permission prompt (optional peer) |
+| Component | What it is |
+| --------------------- | ------------------------------------------------------------------------ |
+| `ChatThread` | Scrolling transcript of user and agent messages |
+| `AgentTurn` | One agent reply: reasoning, tool calls, answer, in order |
+| `MessageBubble` | One user or system message |
+| `ChatInput` | Auto-growing message input with a send/stop button |
+| `ChatComposer` | `ChatInput` with suggestion chips for an empty conversation |
+| `ChatPanel` | Docked chat frame with header and footer |
+| `PartsRenderer` | Renders a list of message parts |
+| `ReasoningBlock` | Collapsible thinking text |
+| `ToolBlock` | A tool call's lifecycle: running, done, failed |
+| `ToolNarrative` | What the agent did, as one expandable line |
+| `ToolPicker` | Browse, search and select tools |
+| `QuickReplyChips` | Tappable suggested replies |
+| `TypingIndicator` | Agent-is-typing dots |
+| `ThinkingIndicator` | Animated Corbits mark with a status label |
+| `ThinkingMark` | Decorative animated Corbits mark |
+| `ThinkingLabel` | Rotating-verb status label |
+| `ShimmerText` | Shimmer sweep over text |
+| `MicButton` | Permission-aware microphone toggle |
+| `VoiceWaveform` | Live audio level bars |
+| `DictationStatusLine` | Composer dictation status row |
+| `MicPermissionDialog` | Microphone permission prompt (optional peer, `ui/mic-permission-dialog`) |
#### Runs
-| Component | Subpath | What it is |
-| ------------------- | ----------------------- | ------------------------------------------------- |
-| `LiveRunBanner` | `ui/live-run-banner` | Status of a run in progress |
-| `LiveStatusLine` | `ui/live-status-line` | The single "what's happening now" line |
-| `ApprovalCard` | `ui/approval-card` | Approve or reject a pending action |
-| `GateBlock` | `ui/gate-block` | Chrome for a run parked on a human decision |
-| `StepList` | `ui/step-list` | Vertical list of run steps and their status |
-| `HorizontalStepper` | `ui/horizontal-stepper` | Horizontal step progress |
-| `ProgressChecklist` | `ui/progress-checklist` | Steps of a long operation and how far along it is |
-| `TraceWaterfall` | `ui/trace-waterfall` | Run-trace timeline, one row per span |
-| `IntakeForm` | `ui/intake-form` | Inputs a workflow asks for before it runs |
-| `RunNowButton` | `ui/run-now-button` | Run something immediately |
-| `ConfirmButton` | `ui/confirm-button` | Button that asks for a second click |
-| `NowCards` | `ui/now-cards` | Attention band of items needing action |
-| `NotificationsBell` | `ui/notifications-bell` | Bell with count and a panel slot |
+| Component | What it is |
+| ------------------- | ------------------------------------------------- |
+| `LiveRunBanner` | Status of a run in progress |
+| `LiveStatusLine` | The single "what's happening now" line |
+| `ApprovalCard` | Approve or reject a pending action |
+| `GateBlock` | Chrome for a run parked on a human decision |
+| `StepList` | Vertical list of run steps and their status |
+| `HorizontalStepper` | Horizontal step progress |
+| `ProgressChecklist` | Steps of a long operation and how far along it is |
+| `TraceWaterfall` | Run-trace timeline, one row per span |
+| `IntakeForm` | Inputs a workflow asks for before it runs |
+| `RunNowButton` | Run something immediately |
+| `ConfirmButton` | Button that asks for a second click |
+| `NowCards` | Attention band of items needing action |
+| `NotificationsBell` | Bell with count and a panel slot |
#### Artifacts
-| Component | Subpath | What it is |
-| ---------------- | -------------------- | --------------------------------------------------- |
-| `ArtifactBody` | `ui/artifact-body` | One artifact, drawn the way its kind implies |
-| `ArtifactNotice` | `ui/artifact-notice` | "Nothing to draw here" message for artifact viewers |
-| `BlockCard` | `ui/block-card` | Generative-UI block frame |
-| `ResearchBody` | `ui/research-body` | Research brief and report |
-| `CompareBody` | `ui/compare-body` | Side-by-side comparison |
-| `EmbedBody` | `ui/embed-body` | Sandboxed third-party embed |
-| `CsvTable` | `ui/csv-table` | Capped preview of CSV text |
-| `QuoteCard` | `ui/quote-card` | A rotating quote |
-| `ProfileCard` | `ui/profile-card` | Identity, status, actions and channels |
+| Component | What it is |
+| ---------------- | --------------------------------------------------- |
+| `ArtifactBody` | One artifact, drawn the way its kind implies |
+| `ArtifactNotice` | "Nothing to draw here" message for artifact viewers |
+| `BlockCard` | Generative-UI block frame |
+| `ResearchBody` | Research brief and report |
+| `CompareBody` | Side-by-side comparison |
+| `EmbedBody` | Sandboxed third-party embed |
+| `CsvTable` | Capped preview of CSV text |
+| `QuoteCard` | A rotating quote |
+| `ProfileCard` | Identity, status, actions and channels |
#### Charts
-| Component | Subpath | What it is |
-| ----------------- | ---------------------- | --------------------------------------------------- |
-| `ChartFrame` | `ui/chart-frame` | Caption, legend, plot, and the same data as a table |
-| `TimeSeriesChart` | `ui/time-series-chart` | Values over time |
-| `BarChart` | `ui/bar-chart` | Vertical bars |
-| `CategoryBars` | `ui/category-bars` | Dense horizontal category ranking |
-| `Sparkline` | `ui/sparkline` | Inline trend line |
-| `TokenMosaic` | `ui/token-mosaic` | Shares of a whole as one stacked strip |
-| `StatGrid` | `ui/stat-grid` | Grid of headline numbers |
-| `AnimatedNumber` | `ui/animated-number` | Number that counts to its new value |
+| Component | What it is |
+| ----------------- | --------------------------------------------------- |
+| `ChartFrame` | Caption, legend, plot, and the same data as a table |
+| `TimeSeriesChart` | Values over time |
+| `BarChart` | Vertical bars |
+| `CategoryBars` | Dense horizontal category ranking |
+| `Sparkline` | Inline trend line |
+| `TokenMosaic` | Shares of a whole as one stacked strip |
+| `StatGrid` | Grid of headline numbers |
+| `AnimatedNumber` | Number that counts to its new value |
#### Layout
-| Component | Subpath | What it is |
-| ------------------- | -------------------------- | -------------------------------------------- |
-| `PageShell` | `ui/page-shell` | Page margins and scroll ownership |
-| `PagePanel` | `ui/page-panel` | Bordered page-content frame |
-| `TopBar` | `ui/top-bar` | Title, breadcrumbs and actions row |
-| `Sidebar` | `ui/sidebar` | Collapsible navigation sidebar |
-| `SidebarPanel` | `ui/sidebar-panel` | Sidebar with pins, body and footer |
-| `SidebarItemRow` | `ui/sidebar-item-row` | One sidebar row |
-| `ListDetail` | `ui/list-detail` | Index on the left, open item on the right |
-| `InspectorShell` | `ui/inspector-shell` | Side inspector rail |
-| `Section` | `ui/section` | Titled block of content |
-| `SettingsPanel` | `ui/settings-panel` | Settings frame with optional save |
-| `LibraryPageHeader` | `ui/library-page-header` | Title, count and controls for a library page |
-| `Card` | `ui/card` | Bordered content card |
-| `EmptyState` | `ui/empty-state` | Title, description, icon and action |
-| `RichEmptyState` | `ui/rich-empty-state` | Empty state with suggested next-step actions |
-| `BootScreen` | `ui/boot-screen` | Screen shown before the app loads |
-| `DitherCanvas` | `ui/dither-canvas` | Animated dithered background |
-| `CorbitsMark` | `ui/corbits-mark` | Corbits logo |
-| `AuthLayout` | `blocks/login/auth-layout` | Sign-in page layout |
-| `LoginForm` | `blocks/login/login-form` | Sign-in and sign-up form, with optional SSO |
+| Component | What it is |
+| ------------------- | -------------------------------------------- |
+| `PageShell` | Page margins and scroll ownership |
+| `PagePanel` | Bordered page-content frame |
+| `TopBar` | Title, breadcrumbs and actions row |
+| `Sidebar` | Collapsible navigation sidebar |
+| `SidebarPanel` | Sidebar with pins, body and footer |
+| `SidebarItemRow` | One sidebar row |
+| `ListDetail` | Index on the left, open item on the right |
+| `InspectorShell` | Side inspector rail |
+| `Section` | Titled block of content |
+| `SettingsPanel` | Settings frame with optional save |
+| `LibraryPageHeader` | Title, count and controls for a library page |
+| `Card` | Bordered content card |
+| `EmptyState` | Title, description, icon and action |
+| `RichEmptyState` | Empty state with suggested next-step actions |
+| `BootScreen` | Screen shown before the app loads |
+| `DitherCanvas` | Animated dithered background |
+| `CorbitsMark` | Corbits logo |
+| `AuthLayout` | Sign-in page layout |
+| `LoginForm` | Sign-in and sign-up form, with optional SSO |
#### Controls
-| Component | Subpath | What it is |
-| ------------------- | ----------------------- | --------------------------------------------------- |
-| `Button` | `ui/button` | Button with variants and sizes |
-| `Input` | `ui/input` | Text input |
-| `Textarea` | `ui/textarea` | Multi-line input |
-| `Select` | `ui/select` | Native select |
-| `Checkbox` | `ui/checkbox` | Checkbox |
-| `SelectionCheckbox` | `ui/selection-checkbox` | Row-selection checkbox |
-| `Switch` | `ui/switch` | On/off toggle |
-| `FileInput` | `ui/file-input` | File picker |
-| `Tabs` | `ui/tabs` | Tab strip |
-| `ViewToggle` | `ui/view-toggle` | Grid or rows segmented control |
-| `FilterBar` | `ui/filter-bar` | Row of filters with an active-filter summary |
-| `FilterChip` | `ui/filter-chip` | One toggleable filter |
-| `KindCardGrid` | `ui/kind-card-grid` | Selectable card grid |
-| `BulkActionBar` | `ui/bulk-action-bar` | Actions for a multi-row selection |
-| `Table` | `ui/table` | Table primitives |
-| `SortableTable` | `ui/sortable-table` | Table with sortable columns |
-| `Badge` | `ui/badge` | Status label |
-| `StatusDot` | `ui/status-dot` | Colored status dot |
-| `Avatar` | `ui/avatar` | Initials or image avatar |
-| `ProviderMark` | `ui/provider-mark` | Which service an action goes through |
-| `Skeleton` | `ui/skeleton` | Loading placeholder |
-| `Dialog` | `ui/dialog` | Modal dialog (optional peer) |
-| `CommandPalette` | `ui/command-palette` | Global search overlay (optional peer) |
-| `Menu` | `ui/menu` | Dropdown menu (optional peer) |
-| `InfoTooltip` | `ui/tooltip` | Hover info tooltip (optional peer) |
-| `Toaster` | `ui/toast` | Toast host; call `toast()` anywhere (optional peer) |
-| `ThemeProvider` | `ui/theme-provider` | Persists light, dark or system mode and presets |
-| `ThemeToggle` | `ui/theme-toggle` | Light, dark or system switch |
+| Component | What it is |
+| ------------------- | --------------------------------------------------------------- |
+| `Button` | Button with variants and sizes |
+| `Input` | Text input |
+| `Textarea` | Multi-line input |
+| `Select` | Native select |
+| `Checkbox` | Checkbox |
+| `SelectionCheckbox` | Row-selection checkbox |
+| `Switch` | On/off toggle |
+| `FileInput` | File picker |
+| `Tabs` | Tab strip |
+| `ViewToggle` | Grid or rows segmented control |
+| `FilterBar` | Row of filters with an active-filter summary |
+| `FilterChip` | One toggleable filter |
+| `KindCardGrid` | Selectable card grid |
+| `BulkActionBar` | Actions for a multi-row selection |
+| `Table` | Table primitives |
+| `SortableTable` | Table with sortable columns |
+| `Badge` | Status label |
+| `StatusDot` | Colored status dot |
+| `Avatar` | Initials or image avatar |
+| `ProviderMark` | Which service an action goes through |
+| `Skeleton` | Loading placeholder |
+| `Dialog` | Modal dialog (optional peer, `ui/dialog`) |
+| `CommandPalette` | Global search overlay (optional peer, `ui/command-palette`) |
+| `Menu` | Dropdown menu (optional peer, `ui/menu`) |
+| `InfoTooltip` | Hover info tooltip (optional peer, `ui/tooltip`) |
+| `Toaster` | Toast host; call `toast()` anywhere (optional peer, `ui/toast`) |
+| `ThemeProvider` | Persists light, dark or system mode and presets |
+| `ThemeToggle` | Light, dark or system switch |
## Using with Interchange
@@ -256,6 +256,7 @@ The host app owns the connection to the hub; components only take props.
## Upgrading from 0.1
- Install from npm. Git installs (`github:corbitsdev/react-ui#`) no longer build `dist/`.
+- Per-module subpaths are gone: import from `@corbits/react-ui` (`import { Button } from "@corbits/react-ui"`).
- `ui/dialog`, `ui/command-palette`, `ui/mic-permission-dialog`, `ui/menu`, `ui/tooltip` and `ui/toast` are subpath-only, and their peers are optional: `import { Toaster, toast } from "@corbits/react-ui/ui/toast"`. Install the peer for each subpath you keep.
- `lib/csv`, `lib/url`, `lib/utils`, `lib/workflow-registry` and `ui/chat-dock-timing` are internal. Replace `cn` with `twMerge(clsx(...))`.
- Stateful components ship `"use client"`. Client wrappers you wrote for them still work, and can go.
diff --git a/e2e/exports.test.ts b/e2e/exports.test.ts
new file mode 100644
index 00000000..33c5fb99
--- /dev/null
+++ b/e2e/exports.test.ts
@@ -0,0 +1,40 @@
+import { describe, expect, test } from "bun:test";
+import { existsSync, readFileSync } from "node:fs";
+import { join } from "node:path";
+
+// Runs against the build: `bun run build` first.
+const ROOT = join(import.meta.dir, "..");
+const PKG = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"));
+const OPTIONAL_PEERS = Object.keys(PKG.peerDependenciesMeta);
+
+const targets = (entry: string | Record) =>
+ typeof entry === "string" ? [entry] : Object.values(entry);
+
+describe.skipIf(!existsSync(join(ROOT, "dist/index.js")))("exports", () => {
+ test("every entry resolves to a file", () => {
+ const missing = Object.values(PKG.exports)
+ .flatMap((entry) => targets(entry as string | Record))
+ .filter((path) => !existsSync(join(ROOT, path)));
+ expect(missing).toEqual([]);
+ });
+
+ test("the root entry loads with no optional peer installed", () => {
+ const script = `
+ import { registerHooks } from "node:module";
+ const peers = ${JSON.stringify(OPTIONAL_PEERS)};
+ registerHooks({
+ resolve(specifier, context, next) {
+ if (peers.includes(specifier)) throw new Error("loaded optional peer " + specifier);
+ return next(specifier, context);
+ },
+ });
+ const root = await import("${PKG.name}");
+ if (!root.Button) throw new Error("Button missing from the root entry");
+ `;
+ const run = Bun.spawnSync(["node", "--input-type=module", "-e", script], {
+ cwd: ROOT,
+ });
+ expect(run.stderr.toString()).toBe("");
+ expect(run.exitCode).toBe(0);
+ });
+});
diff --git a/package.json b/package.json
index 8bd7f0b6..c6671a76 100644
--- a/package.json
+++ b/package.json
@@ -28,9 +28,8 @@
"access": "public"
},
"scripts": {
- "build": "bun run clean && bun run generate && bun run build:js && bun run build:types && bun run build:css && bun run contrast-test && bun run dep-guard",
+ "build": "bun run clean && bun run build:js && bun run build:types && bun run build:css && bun run contrast-test && bun run dep-guard",
"clean": "rm -rf dist",
- "generate": "node scripts/generate-exports.mjs",
"build:js": "swc src -d dist --strip-leading-paths --extensions .ts,.tsx --ignore \"**/*.test.ts\" --ignore \"**/*.test.tsx\"",
"build:types": "tsc -p tsconfig.build.json",
"build:css": "tailwindcss -i src/styles.css -o dist/styles.css && cp src/theme.css dist/theme.css",
@@ -102,476 +101,32 @@
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
- "./styles.css": "./dist/styles.css",
- "./theme.css": "./dist/theme.css",
- "./package.json": "./package.json",
- "./blocks/login/auth-layout": {
- "types": "./dist/blocks/login/auth-layout.d.ts",
- "default": "./dist/blocks/login/auth-layout.js"
- },
- "./blocks/login/login-form": {
- "types": "./dist/blocks/login/login-form.d.ts",
- "default": "./dist/blocks/login/login-form.js"
- },
- "./hooks/use-command-palette-navigation": {
- "types": "./dist/hooks/use-command-palette-navigation.d.ts",
- "default": "./dist/hooks/use-command-palette-navigation.js"
- },
- "./hooks/use-controllable-state": {
- "types": "./dist/hooks/use-controllable-state.d.ts",
- "default": "./dist/hooks/use-controllable-state.js"
- },
- "./hooks/use-delayed-autofocus": {
- "types": "./dist/hooks/use-delayed-autofocus.d.ts",
- "default": "./dist/hooks/use-delayed-autofocus.js"
- },
- "./hooks/use-dismissable-popover": {
- "types": "./dist/hooks/use-dismissable-popover.d.ts",
- "default": "./dist/hooks/use-dismissable-popover.js"
- },
- "./hooks/use-list-selection": {
- "types": "./dist/hooks/use-list-selection.d.ts",
- "default": "./dist/hooks/use-list-selection.js"
- },
- "./hooks/use-prefers-reduced-motion": {
- "types": "./dist/hooks/use-prefers-reduced-motion.d.ts",
- "default": "./dist/hooks/use-prefers-reduced-motion.js"
- },
- "./hooks/use-scroll-current-into-view": {
- "types": "./dist/hooks/use-scroll-current-into-view.d.ts",
- "default": "./dist/hooks/use-scroll-current-into-view.js"
- },
- "./lib/artifact": {
- "types": "./dist/lib/artifact.d.ts",
- "default": "./dist/lib/artifact.js"
- },
- "./lib/chart-palette": {
- "types": "./dist/lib/chart-palette.d.ts",
- "default": "./dist/lib/chart-palette.js"
- },
- "./lib/chat-message": {
- "types": "./dist/lib/chat-message.d.ts",
- "default": "./dist/lib/chat-message.js"
- },
- "./lib/chat-parts": {
- "types": "./dist/lib/chat-parts.d.ts",
- "default": "./dist/lib/chat-parts.js"
- },
- "./lib/metrics": {
- "types": "./dist/lib/metrics.d.ts",
- "default": "./dist/lib/metrics.js"
- },
- "./lib/now-item": {
- "types": "./dist/lib/now-item.d.ts",
- "default": "./dist/lib/now-item.js"
- },
- "./lib/relative-time": {
- "types": "./dist/lib/relative-time.d.ts",
- "default": "./dist/lib/relative-time.js"
- },
- "./lib/theme": {
- "types": "./dist/lib/theme.d.ts",
- "default": "./dist/lib/theme.js"
- },
- "./lib/workflow-display-flow": {
- "types": "./dist/lib/workflow-display-flow.d.ts",
- "default": "./dist/lib/workflow-display-flow.js"
- },
- "./lib/workflow-run": {
- "types": "./dist/lib/workflow-run.d.ts",
- "default": "./dist/lib/workflow-run.js"
- },
- "./lib/workflow-run-error": {
- "types": "./dist/lib/workflow-run-error.d.ts",
- "default": "./dist/lib/workflow-run-error.js"
- },
- "./lib/workflow-run-progress": {
- "types": "./dist/lib/workflow-run-progress.d.ts",
- "default": "./dist/lib/workflow-run-progress.js"
- },
- "./ui/agent-turn": {
- "types": "./dist/ui/agent-turn.d.ts",
- "default": "./dist/ui/agent-turn.js"
- },
- "./ui/animated-number": {
- "types": "./dist/ui/animated-number.d.ts",
- "default": "./dist/ui/animated-number.js"
- },
- "./ui/approval-card": {
- "types": "./dist/ui/approval-card.d.ts",
- "default": "./dist/ui/approval-card.js"
- },
- "./ui/artifact-body": {
- "types": "./dist/ui/artifact-body.d.ts",
- "default": "./dist/ui/artifact-body.js"
- },
- "./ui/artifact-notice": {
- "types": "./dist/ui/artifact-notice.d.ts",
- "default": "./dist/ui/artifact-notice.js"
- },
- "./ui/avatar": {
- "types": "./dist/ui/avatar.d.ts",
- "default": "./dist/ui/avatar.js"
- },
- "./ui/badge": {
- "types": "./dist/ui/badge.d.ts",
- "default": "./dist/ui/badge.js"
- },
- "./ui/bar-chart": {
- "types": "./dist/ui/bar-chart.d.ts",
- "default": "./dist/ui/bar-chart.js"
- },
- "./ui/block-card": {
- "types": "./dist/ui/block-card.d.ts",
- "default": "./dist/ui/block-card.js"
- },
- "./ui/boot-screen": {
- "types": "./dist/ui/boot-screen.d.ts",
- "default": "./dist/ui/boot-screen.js"
- },
- "./ui/bulk-action-bar": {
- "types": "./dist/ui/bulk-action-bar.d.ts",
- "default": "./dist/ui/bulk-action-bar.js"
- },
- "./ui/button": {
- "types": "./dist/ui/button.d.ts",
- "default": "./dist/ui/button.js"
- },
- "./ui/card": {
- "types": "./dist/ui/card.d.ts",
- "default": "./dist/ui/card.js"
- },
- "./ui/category-bars": {
- "types": "./dist/ui/category-bars.d.ts",
- "default": "./dist/ui/category-bars.js"
- },
- "./ui/chart-frame": {
- "types": "./dist/ui/chart-frame.d.ts",
- "default": "./dist/ui/chart-frame.js"
- },
- "./ui/chat-composer": {
- "types": "./dist/ui/chat-composer.d.ts",
- "default": "./dist/ui/chat-composer.js"
- },
- "./ui/chat-input": {
- "types": "./dist/ui/chat-input.d.ts",
- "default": "./dist/ui/chat-input.js"
- },
- "./ui/chat-panel": {
- "types": "./dist/ui/chat-panel.d.ts",
- "default": "./dist/ui/chat-panel.js"
- },
- "./ui/chat-thread": {
- "types": "./dist/ui/chat-thread.d.ts",
- "default": "./dist/ui/chat-thread.js"
- },
- "./ui/checkbox": {
- "types": "./dist/ui/checkbox.d.ts",
- "default": "./dist/ui/checkbox.js"
- },
"./ui/command-palette": {
"types": "./dist/ui/command-palette.d.ts",
"default": "./dist/ui/command-palette.js"
},
- "./ui/compare-body": {
- "types": "./dist/ui/compare-body.d.ts",
- "default": "./dist/ui/compare-body.js"
- },
- "./ui/confirm-button": {
- "types": "./dist/ui/confirm-button.d.ts",
- "default": "./dist/ui/confirm-button.js"
- },
- "./ui/corbits-mark": {
- "types": "./dist/ui/corbits-mark.d.ts",
- "default": "./dist/ui/corbits-mark.js"
- },
- "./ui/csv-table": {
- "types": "./dist/ui/csv-table.d.ts",
- "default": "./dist/ui/csv-table.js"
- },
"./ui/dialog": {
"types": "./dist/ui/dialog.d.ts",
"default": "./dist/ui/dialog.js"
},
- "./ui/dictation-status-line": {
- "types": "./dist/ui/dictation-status-line.d.ts",
- "default": "./dist/ui/dictation-status-line.js"
- },
- "./ui/dither-canvas": {
- "types": "./dist/ui/dither-canvas.d.ts",
- "default": "./dist/ui/dither-canvas.js"
- },
- "./ui/embed-body": {
- "types": "./dist/ui/embed-body.d.ts",
- "default": "./dist/ui/embed-body.js"
- },
- "./ui/empty-state": {
- "types": "./dist/ui/empty-state.d.ts",
- "default": "./dist/ui/empty-state.js"
- },
- "./ui/file-input": {
- "types": "./dist/ui/file-input.d.ts",
- "default": "./dist/ui/file-input.js"
- },
- "./ui/filter-bar": {
- "types": "./dist/ui/filter-bar.d.ts",
- "default": "./dist/ui/filter-bar.js"
- },
- "./ui/filter-chip": {
- "types": "./dist/ui/filter-chip.d.ts",
- "default": "./dist/ui/filter-chip.js"
- },
- "./ui/gate-block": {
- "types": "./dist/ui/gate-block.d.ts",
- "default": "./dist/ui/gate-block.js"
- },
- "./ui/horizontal-stepper": {
- "types": "./dist/ui/horizontal-stepper.d.ts",
- "default": "./dist/ui/horizontal-stepper.js"
- },
- "./ui/input": {
- "types": "./dist/ui/input.d.ts",
- "default": "./dist/ui/input.js"
- },
- "./ui/inspector-shell": {
- "types": "./dist/ui/inspector-shell.d.ts",
- "default": "./dist/ui/inspector-shell.js"
- },
- "./ui/intake-form": {
- "types": "./dist/ui/intake-form.d.ts",
- "default": "./dist/ui/intake-form.js"
- },
- "./ui/kind-card-grid": {
- "types": "./dist/ui/kind-card-grid.d.ts",
- "default": "./dist/ui/kind-card-grid.js"
- },
- "./ui/library-page-header": {
- "types": "./dist/ui/library-page-header.d.ts",
- "default": "./dist/ui/library-page-header.js"
- },
- "./ui/list-detail": {
- "types": "./dist/ui/list-detail.d.ts",
- "default": "./dist/ui/list-detail.js"
- },
- "./ui/live-run-banner": {
- "types": "./dist/ui/live-run-banner.d.ts",
- "default": "./dist/ui/live-run-banner.js"
- },
- "./ui/live-status-line": {
- "types": "./dist/ui/live-status-line.d.ts",
- "default": "./dist/ui/live-status-line.js"
- },
"./ui/menu": {
"types": "./dist/ui/menu.d.ts",
"default": "./dist/ui/menu.js"
},
- "./ui/message-bubble": {
- "types": "./dist/ui/message-bubble.d.ts",
- "default": "./dist/ui/message-bubble.js"
- },
- "./ui/mic-button": {
- "types": "./dist/ui/mic-button.d.ts",
- "default": "./dist/ui/mic-button.js"
- },
"./ui/mic-permission-dialog": {
"types": "./dist/ui/mic-permission-dialog.d.ts",
"default": "./dist/ui/mic-permission-dialog.js"
},
- "./ui/notifications-bell": {
- "types": "./dist/ui/notifications-bell.d.ts",
- "default": "./dist/ui/notifications-bell.js"
- },
- "./ui/now-cards": {
- "types": "./dist/ui/now-cards.d.ts",
- "default": "./dist/ui/now-cards.js"
- },
- "./ui/page-panel": {
- "types": "./dist/ui/page-panel.d.ts",
- "default": "./dist/ui/page-panel.js"
- },
- "./ui/page-shell": {
- "types": "./dist/ui/page-shell.d.ts",
- "default": "./dist/ui/page-shell.js"
- },
- "./ui/parts-renderer": {
- "types": "./dist/ui/parts-renderer.d.ts",
- "default": "./dist/ui/parts-renderer.js"
- },
- "./ui/profile-card": {
- "types": "./dist/ui/profile-card.d.ts",
- "default": "./dist/ui/profile-card.js"
- },
- "./ui/progress-checklist": {
- "types": "./dist/ui/progress-checklist.d.ts",
- "default": "./dist/ui/progress-checklist.js"
- },
- "./ui/provider-mark": {
- "types": "./dist/ui/provider-mark.d.ts",
- "default": "./dist/ui/provider-mark.js"
- },
- "./ui/quick-reply-chips": {
- "types": "./dist/ui/quick-reply-chips.d.ts",
- "default": "./dist/ui/quick-reply-chips.js"
- },
- "./ui/quote-card": {
- "types": "./dist/ui/quote-card.d.ts",
- "default": "./dist/ui/quote-card.js"
- },
- "./ui/reasoning-block": {
- "types": "./dist/ui/reasoning-block.d.ts",
- "default": "./dist/ui/reasoning-block.js"
- },
- "./ui/research-body": {
- "types": "./dist/ui/research-body.d.ts",
- "default": "./dist/ui/research-body.js"
- },
- "./ui/rich-empty-state": {
- "types": "./dist/ui/rich-empty-state.d.ts",
- "default": "./dist/ui/rich-empty-state.js"
- },
- "./ui/run-now-button": {
- "types": "./dist/ui/run-now-button.d.ts",
- "default": "./dist/ui/run-now-button.js"
- },
- "./ui/section": {
- "types": "./dist/ui/section.d.ts",
- "default": "./dist/ui/section.js"
- },
- "./ui/select": {
- "types": "./dist/ui/select.d.ts",
- "default": "./dist/ui/select.js"
- },
- "./ui/selection-checkbox": {
- "types": "./dist/ui/selection-checkbox.d.ts",
- "default": "./dist/ui/selection-checkbox.js"
- },
- "./ui/settings-panel": {
- "types": "./dist/ui/settings-panel.d.ts",
- "default": "./dist/ui/settings-panel.js"
- },
- "./ui/shimmer-text": {
- "types": "./dist/ui/shimmer-text.d.ts",
- "default": "./dist/ui/shimmer-text.js"
- },
- "./ui/sidebar": {
- "types": "./dist/ui/sidebar.d.ts",
- "default": "./dist/ui/sidebar.js"
- },
- "./ui/sidebar-item-row": {
- "types": "./dist/ui/sidebar-item-row.d.ts",
- "default": "./dist/ui/sidebar-item-row.js"
- },
- "./ui/sidebar-panel": {
- "types": "./dist/ui/sidebar-panel.d.ts",
- "default": "./dist/ui/sidebar-panel.js"
- },
- "./ui/skeleton": {
- "types": "./dist/ui/skeleton.d.ts",
- "default": "./dist/ui/skeleton.js"
- },
- "./ui/sortable-table": {
- "types": "./dist/ui/sortable-table.d.ts",
- "default": "./dist/ui/sortable-table.js"
- },
- "./ui/sparkline": {
- "types": "./dist/ui/sparkline.d.ts",
- "default": "./dist/ui/sparkline.js"
- },
- "./ui/stat-grid": {
- "types": "./dist/ui/stat-grid.d.ts",
- "default": "./dist/ui/stat-grid.js"
- },
- "./ui/status-dot": {
- "types": "./dist/ui/status-dot.d.ts",
- "default": "./dist/ui/status-dot.js"
- },
- "./ui/step-list": {
- "types": "./dist/ui/step-list.d.ts",
- "default": "./dist/ui/step-list.js"
- },
- "./ui/switch": {
- "types": "./dist/ui/switch.d.ts",
- "default": "./dist/ui/switch.js"
- },
- "./ui/table": {
- "types": "./dist/ui/table.d.ts",
- "default": "./dist/ui/table.js"
- },
- "./ui/tabs": {
- "types": "./dist/ui/tabs.d.ts",
- "default": "./dist/ui/tabs.js"
- },
- "./ui/textarea": {
- "types": "./dist/ui/textarea.d.ts",
- "default": "./dist/ui/textarea.js"
- },
- "./ui/theme-provider": {
- "types": "./dist/ui/theme-provider.d.ts",
- "default": "./dist/ui/theme-provider.js"
- },
- "./ui/theme-toggle": {
- "types": "./dist/ui/theme-toggle.d.ts",
- "default": "./dist/ui/theme-toggle.js"
- },
- "./ui/thinking-indicator": {
- "types": "./dist/ui/thinking-indicator.d.ts",
- "default": "./dist/ui/thinking-indicator.js"
- },
- "./ui/thinking-label": {
- "types": "./dist/ui/thinking-label.d.ts",
- "default": "./dist/ui/thinking-label.js"
- },
- "./ui/thinking-mark": {
- "types": "./dist/ui/thinking-mark.d.ts",
- "default": "./dist/ui/thinking-mark.js"
- },
- "./ui/time-series-chart": {
- "types": "./dist/ui/time-series-chart.d.ts",
- "default": "./dist/ui/time-series-chart.js"
- },
"./ui/toast": {
"types": "./dist/ui/toast.d.ts",
"default": "./dist/ui/toast.js"
},
- "./ui/token-mosaic": {
- "types": "./dist/ui/token-mosaic.d.ts",
- "default": "./dist/ui/token-mosaic.js"
- },
- "./ui/tool-block": {
- "types": "./dist/ui/tool-block.d.ts",
- "default": "./dist/ui/tool-block.js"
- },
- "./ui/tool-narrative": {
- "types": "./dist/ui/tool-narrative.d.ts",
- "default": "./dist/ui/tool-narrative.js"
- },
- "./ui/tool-picker": {
- "types": "./dist/ui/tool-picker.d.ts",
- "default": "./dist/ui/tool-picker.js"
- },
"./ui/tooltip": {
"types": "./dist/ui/tooltip.d.ts",
"default": "./dist/ui/tooltip.js"
},
- "./ui/top-bar": {
- "types": "./dist/ui/top-bar.d.ts",
- "default": "./dist/ui/top-bar.js"
- },
- "./ui/trace-waterfall": {
- "types": "./dist/ui/trace-waterfall.d.ts",
- "default": "./dist/ui/trace-waterfall.js"
- },
- "./ui/typing-indicator": {
- "types": "./dist/ui/typing-indicator.d.ts",
- "default": "./dist/ui/typing-indicator.js"
- },
- "./ui/view-toggle": {
- "types": "./dist/ui/view-toggle.d.ts",
- "default": "./dist/ui/view-toggle.js"
- },
- "./ui/voice-waveform": {
- "types": "./dist/ui/voice-waveform.d.ts",
- "default": "./dist/ui/voice-waveform.js"
- }
+ "./styles.css": "./dist/styles.css",
+ "./theme.css": "./dist/theme.css",
+ "./package.json": "./package.json"
}
}
diff --git a/scripts/dep-guard.mjs b/scripts/dep-guard.mjs
index 9cc8f14c..910a3d2f 100644
--- a/scripts/dep-guard.mjs
+++ b/scripts/dep-guard.mjs
@@ -75,7 +75,7 @@ function reachableFrom(entry) {
const barrel = join(ROOT, "index.ts");
if (!sources.includes(barrel)) {
- console.error("dep-guard: src/index.ts is missing — run `bun run generate` first");
+ console.error("dep-guard: src/index.ts is missing — the root barrel is hand-written");
process.exit(1);
}
@@ -85,7 +85,7 @@ for (const path of reachableFrom(barrel)) {
if (source.includes(`"${peer}"`)) {
violations.push(
`${id(path)}: imports the optional peer ${peer} and is reachable from the root barrel — ` +
- `add it to BARREL_EXCLUDED in scripts/generate-exports.mjs`,
+ `drop it from src/index.ts and give it its own "exports" entry`,
);
}
}
diff --git a/scripts/generate-exports.mjs b/scripts/generate-exports.mjs
deleted file mode 100644
index b3e1e284..00000000
--- a/scripts/generate-exports.mjs
+++ /dev/null
@@ -1,103 +0,0 @@
-// Derives the package's public surface from the source tree, then writes both
-// the root barrel and the `exports` map in package.json.
-//
-// Generated rather than hand-written because there are ~100 entries and a
-// hand-maintained map drifts from the tree the first time a file is added.
-//
-// `INTERNAL` is the triage list: modules that exist to serve the components and
-// whose names appear in no public prop type. Anything not listed is a semver
-// commitment, so adding to this list is the cheap direction and removing from
-// it is the expensive one.
-//
-// `BARREL_EXCLUDED` is a different thing and must not be confused with it: these
-// modules are fully public and keep their subpath, they are just kept *out of
-// the root barrel*. A module that statically imports an optional peer
-// dependency cannot be re-exported from `index.ts`, because the barrel is one
-// module — importing anything from the root would load it and crash for every
-// consumer who did not install that peer. `dep-guard` enforces the property
-// independently.
-import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
-import { join, relative } from "node:path";
-
-const ROOT = new URL("..", import.meta.url).pathname;
-const SRC = join(ROOT, "src");
-
-const INTERNAL = new Set([
- "lib/chart-geometry",
- "lib/csv",
- "lib/url",
- "lib/utils",
- "lib/workflow-registry",
- "ui/chat-dock-timing",
-]);
-const BARREL_EXCLUDED = new Set([
- "ui/command-palette",
- "ui/dialog",
- "ui/menu",
- "ui/mic-permission-dialog",
- "ui/toast",
- "ui/tooltip",
-]);
-
-const walk = (dir) =>
- readdirSync(dir).flatMap((entry) => {
- const path = join(dir, entry);
- return statSync(path).isDirectory() ? walk(path) : [path];
- });
-
-const modules = walk(SRC)
- .filter((path) => /\.tsx?$/.test(path))
- // Tests are co-located under src/ and must never enter the public surface —
- // re-exporting them from the barrel pulls bun:test into every consumer build.
- .filter((path) => !/\.test\.tsx?$/.test(path))
- .map((path) => relative(SRC, path).replace(/\.tsx?$/, ""))
- .filter(
- (id) =>
- id !== "index" &&
- !INTERNAL.has(id) &&
- // Test modules never join the published surface — shipping them through
- // the barrel makes every consumer load them, and bun test discovers the
- // dist *.test.js files as runnable suites.
- !/\.test$/.test(id) &&
- !/(^|\/)test\//.test(id),
- )
- .sort();
-
-const exportsMap = {
- ".": { types: "./dist/index.d.ts", default: "./dist/index.js" },
- "./styles.css": "./dist/styles.css",
- "./theme.css": "./dist/theme.css",
- "./package.json": "./package.json",
-};
-for (const id of modules) {
- exportsMap[`./${id}`] = { types: `./dist/${id}.d.ts`, default: `./dist/${id}.js` };
-}
-
-const pkgPath = join(ROOT, "package.json");
-const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
-pkg.exports = exportsMap;
-writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
-
-const barrelModules = modules.filter((id) => !BARREL_EXCLUDED.has(id));
-
-const barrel = [
- "// Generated by scripts/generate-exports.mjs. Do not edit.",
- "//",
- "// Re-exports only, and the package is sideEffects-free, so importing from",
- "// the root still tree-shakes down to what you used.",
- "//",
- "// Modules that statically import an optional peer dependency are absent here",
- "// on purpose and must be imported by their subpath. The barrel is a single",
- "// module: anything it re-exports is loaded by any root import, so one such",
- "// re-export would make the optional peer mandatory for everyone.",
- ...[...BARREL_EXCLUDED].sort().map((id) => `// @corbits/react-ui/${id}`),
- "",
- ...barrelModules.map((id) => `export * from "./${id}.js";`),
- "",
-].join("\n");
-writeFileSync(join(SRC, "index.ts"), barrel);
-
-console.log(
- `generate-exports: ${modules.length} public modules ` +
- `(${INTERNAL.size} internal, ${BARREL_EXCLUDED.size} subpath-only)`,
-);
diff --git a/src/index.ts b/src/index.ts
index 98ff32a2..990fc983 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,12 +1,8 @@
-// Generated by scripts/generate-exports.mjs. Do not edit.
+// The public surface. Anything not re-exported here is internal.
//
-// Re-exports only, and the package is sideEffects-free, so importing from
-// the root still tree-shakes down to what you used.
-//
-// Modules that statically import an optional peer dependency are absent here
-// on purpose and must be imported by their subpath. The barrel is a single
-// module: anything it re-exports is loaded by any root import, so one such
-// re-export would make the optional peer mandatory for everyone.
+// Modules that statically import an optional peer are not re-exported: the
+// barrel is one module, so any root import would load that peer. They have
+// their own entries in the `exports` map instead:
// @corbits/react-ui/ui/command-palette
// @corbits/react-ui/ui/dialog
// @corbits/react-ui/ui/menu