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