Publish CSS per area alongside the aggregate stylesheet - #309
Merged
Merged
Conversation
The package shipped exactly one stylesheet, `dist/esm/styles.css`, holding every component's rules. An application that mounts two dialogs and a data table downloaded PivotViewer, TimeMachine, Canvas and Chat as well. The single budget that guarded it — 205 KiB raw, 32 KiB gzip — was down to roughly 200 gzip bytes of headroom, the honest reductions were spent, and it had started deciding authoring questions it has no business deciding: whether a component may have its own stylesheet, and how many stylesheets a group of collection controls gets. Every JavaScript subpath now also publishes `<subpath>/styles`, beside one shared `@cratis/components/styles/base`. The aggregate is untouched in content and remains the recommended default; this is additive. The mapping is derived, not written down twice. A stylesheet's owning area is the first path segment of its `@import` in `Source/styles.css` — the same segment the exports map already uses for the JavaScript subpath — and a bare specifier names its area in a `@cratis-area` comment, which only Allotment needs. A subpath's sheet is then the union of the areas its built module graph actually reaches, walked with the same closure helper `verify-package-graph.mjs` uses, so `PivotViewer/styles` carries Filter's rules because PivotViewer renders a FilterPanel, and nobody has to remember that. The build fails when the package's stylesheet exports drift from what it emits. Shared CSS is the compiled Tailwind theme and prefixed utility output plus the cascade-layer order statement: generated from the whole package's JSX, and an order that must be established exactly once. That is `styles/base`, imported once and never duplicated into an area sheet. The `--cratis-*` seam and the baseline look already had their own entry points and are unchanged. Budgets follow the same shape. The aggregate keeps its reviewed raw, gzip and declaration-block ceilings with their measurement history; each per-area sheet gets its own gzip ceiling, because a 22 KiB PivotViewer stylesheet is fine for a consumer that mounts a pivot viewer and unacceptable for one that does not. A new area cannot ship without a measured number, and the emitted set has a combined ceiling so the repetition that makes each sheet self-contained cannot grow unreviewed. No component's CSS content changed. (#301)
…unmapped The 'migration contract' test in transform.test.js asserts that every subpath in Source/package.json's exports map is either a codemod rewrite target (a historical root-namespace symbol like Canvas or DataTables) or explicitly listed as intentionallyUnmapped, so any new subpath forces a conscious decision rather than silently falling through the migration contract. The new per-area */styles entries and ./styles/base are side-effect CSS imports, not symbol imports the codemod rewrites, so they belong in intentionallyUnmapped alongside the pre-existing aggregate ./styles export they extend. The Migrator has never handled style imports; consumers on older Components versions still import @cratis/components/styles, which keeps working unchanged, so no new Migrator rewrite behavior is required for this PR.
This was referenced Sep 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
@cratis/componentspublished exactly one stylesheet. An application that mounts two dialogs and a data table downloadedPivotViewer,TimeMachine,CanvasandChatas well — about 32 KiB gzip of CSS for surfaces it never renders. Every JavaScript subpath now also publishes its own stylesheet at<subpath>/styles, beside one shared@cratis/components/styles/base, so an application pays for what it imports.@cratis/components/stylesis unchanged and remains the recommended default; nothing has to migrate.Added
@cratis/components/Dialogs/styles,@cratis/components/PivotViewer/styles,@cratis/components/DataTables/stylesand the rest — so an application downloads only the CSS for the surfaces it mounts (One aggregated stylesheet makes every consumer pay for every surface #301)@cratis/components/styles/base, the shared internal utilities and cascade-layer order that every per-area entry point needs, imported once and never duplicated per surface (One aggregated stylesheet makes every consumer pay for every surface #301)Documentation/Styling/per-area-stylesheets.mddocumentation covering when to prefer them over the aggregate, the full list of entry points and what each one covers (One aggregated stylesheet makes every consumer pay for every surface #301)Changed
<subpath>/stylesis self-contained for that subpath —@cratis/components/PivotViewer/stylesincludesFilter's rules becausePivotViewerrenders aFilterPanel, and@cratis/components/DataPage/stylesincludes the vendored Allotment split-view rules — so there is never a second stylesheet to remember (One aggregated stylesheet makes every consumer pay for every surface #301)styles/baseestablishes cascade-layer precedence, every area sheet just reopenscratis-components, and the cascade contract is identical whichever entry points are used (One aggregated stylesheet makes every consumer pay for every surface #301)Documentation/ui-foundation.mdrecords the split instead of the criteria for a future one (One aggregated stylesheet makes every consumer pay for every surface #301)Indicative gzip transfer against roughly 32 KiB for the aggregate: about 2 KiB for the shared base, 6.4 KiB for
Dialogs, 6.1 KiB forDataTables, 6.7 KiB forPivotViewer, 4.3 KiB forTimeMachine, 2.7 KiB forFilter.No component's CSS content changed — this is packaging only, and every existing export, part name and token keeps working unchanged.