Components
Nistor's own UI components on React Aria Components, the decisions behind them and where the catalogue lives.
@nistor-io/ui/core/*: Nistor's own components on React Aria Components (the charter's component-library decision), plan
ux-p2-components.md. This page records the decisions the plan asks for; the catalogue lives at
/wiki/design-system.
Foundation spike#
Foundation spike, 17 Oct 2026, branch ux/p2-spike from that day's origin/main. The six components and
MenuPopover reached main with the core package's build work; the harness (the ?spike=core page, the gitlab
remote's module, the top-bar variants and their jest tests) stays on that branch.
What was built. Button, IconButton, Tooltip, Menu, Tabs and Dialog in web/ui/src/core/, on
react-aria-components 1.21.1 (pinned exact; the latest 1.x at least two weeks old: 1.22.0 and 1.22.1
are from 8 and 9 Oct). Imported all six from one shell page (/wiki/design-system?spike=core) and from the
gitlab remote; the boot-path case imports Button, Tooltip and Menu from the top bar (AppHeader.js), as
P3 will. Every build is npm run build:image (plugin-free image build, then check-bundle-size.mjs).
What stays on main. The six components, MenuPopover (2b) and the pinned dependency. The test-only
code the measurements used (the ?spike=core section of the design-system page, the gitlab plugin's demo,
the top bar's Help button and the two jest files named below) was measured on the spike branch and then
removed from main (branch ux/p2-spike-cleanup), since the top bar's part cost every visitor 25.8 kB gz of
start-up for nothing they could see. It is in git history for P3 to copy.
Method for the RAC cost. Every byte of every emitted static/js/*.js is attributed to its original
source through the chunk's source map (what source-map-explorer does, with the locked
@jridgewell/trace-mapping). "RAC" is the package family it installs: react-aria-components,
react-aria, react-stately, @react-aria/*, @react-stately/*, @internationalized/*,
@swc/helpers, aria-hidden, client-only, clsx. RAC is not in one chunk: webpack splits it across
the per-file provider chunks of core/*, so the cost is summed over chunks, each source counted once.
The four numbers#
| # | Measure | Result | Threshold |
|---|---|---|---|
| 1 | RAC cost, six primitives | 60,324 B gz (58.9 KiB) as one gzip stream of the 222 unique RAC modules, 192,097 B raw; 64,533 B gz as served (10 chunks gzipped separately) | 60 kB gz (charter) |
| 2 | Start-up delta, Button + Tooltip + Menu on the boot path | +63,810 B gz (266,008 → 329,818 B, 37 → 46 files); 60,475 B of it is RAC | budget 264,946 B |
| 2b | Same, with the Menu loaded on the trigger's first press | +25,759 B gz (266,008 → 291,767 B, 37 → 43 files); 23,589 B of it is RAC | budget 264,946 B |
| 3 | Barrel cost, core/index.js vs per-file subpaths |
+3,310 B gz at start-up (333,128 vs 329,818 B); total raw JS −23,165 B | 0 to allow the barrel |
| 4 | One instance | pass: one RAC, shared warm-up, no RAC in the remote | sharing works |
Details:
- RAC by package (unique, raw): react-aria 144,248 B, react-aria-components 25,275 B, react-stately
19,837 B, @internationalized/string 1,986 B, clsx 751 B. No
@internationalized/date(no date component yet). 12 RAC files are emitted twice, because the shell provides each package module both by request and through the run-time registration (src/plugins/packageShares.js); a page loads one copy. With the page and the remote only (no boot-path import), start-up grows by 756 B (the register calls inmain.js) and no RAC is on the start-up path. - Measured against this run's own baseline build of
origin/main, which is already over its budget before any spike code: start-up 266,008 B vs 264,946 B (+1,062 B) and total raw JS 15,095,440 B vs 15,070,130 B (+25,310 B). With the boot-path import, start-up is 64,872 B over the budget, and total raw JS is 15,345,715 B (+250,275 B on the baseline, 275,585 B over its budget). 2b. The lazy variant (CoreSpikeHeader.js): the top bar imports Button and Tooltip eagerly and renders its trigger; the first press loadsCoreSpikeHelpMenu.js(its own chunk, 367 B gz) with coreMenuPopover, anchored to that same trigger. The 38,051 B gz the eager import had on the start-up path (329,818 − 291,767) move to the first press. Start-up is still 26,821 B over the budget. The pattern needsMenuPopoverincore/Menu.js(the menu without its trigger; the caller setsaria-haspopupandaria-expanded): a first version that swapped a placeholder trigger for a fullMenulost focus on Escape, because React Aria restored it to the unmounted placeholder. Jest (src/components/shell/__tests__/CoreSpikeHeader.test.js, 3 tests): a keyboard press loads and opens the menu, focus lands on the first item, Escape closes it and focus returns to the trigger, and later presses reopen it. - Through the barrel, the top bar's Button, Tooltip and Menu bring Tabs and Dialog onto the start-up path, since a shared module is provided whole. The barrel stays out of the shell and the plugins (a guard checks it) and serves the typed build only.
npm run build:remotes -- --only gitlab:check-remote-bundle.mjsOK (25 modules: 6 own, 8 consume-shared including the six@nistor-io/ui/core/*, 9 npm, all lucide's gitlab icon); noreact-aria,react-stately,@react-ariaor@internationalizedstring in the remote's emitted JS; the remote is 11,601 B. Jest (src/plugins/__tests__/coreSpikeOneInstance.test.js, 3 tests): the remote's Tooltip is the shell's module; after the shell tooltip opens, the remote's opens with no delay; a cold one waits 500 ms. Browser (dev server, gitlab loaded as a run-time remote throughREACT_APP_DEV_REMOTES):remote.Tooltip === shell Tooltipis true; shell tooltip opened 509 ms after the pointer entered, then the remote's 5 ms after; a cold remote tooltip 502 ms.
Decision#
React Aria (Rémy, 17 Oct 2026): D1 stands; the spike shows no sharing problem and the cost is at the
60 kB line. Before any waiver, the top bar's Menu is measured loaded lazily (2b). Start-up waiver (Rémy, 17 Oct
2026): a dated startupGzBytes waiver in web/shell/scripts/bundle-budgets.json _meta.waivers, sized
to the lazy-menu build: budget 264,946 → 295,943 B (291,767 B measured plus the margin the budget carried),
expires 31 Mar 2027. Burn-down owner: P3's top bar, which keeps React Aria beyond Button and Tooltip off
the start-up path and re-banks with --update. Total raw JS waiver (Rémy, 17 Oct 2026): totalRawJsBytes 15,070,130 → 15,331,378 B, sized to
main after the spike's test-only code left it (15,320,321 B measured plus the margin the budget carried),
expires 31 Mar 2027. The shell provides every package file, so React Aria's chunks are emitted even before
anything imports core/*, 12 of its files twice; origin/main was already 25,310 B over before the spike.
Burn-down owner: the core package's build work, which looks at emitting React Aria once and re-banks with --update.
Close-out: wrappers, ratchet and catalogue#
18 Oct 2026, branch ux/p2-closeout: the three component waves, then main merged in (the token cut-over and the
identity kit).
Wrappers. Every old name in the plan's inventory keeps its path and export and renders core, except four, each for a reason:
DetailSectionsis a tab strip over detail panels, not a key/value list; it draws its strip withSegmentedControl, itself a wrapper over coreSegmentedButton.TruncatedCell(one importer, the rules table's name cell) is an ellipsis with the native title; coreDataTablehas no truncation API to map it onto, and adding one is new core surface.pages/Lineage/erd/ErdTableNode.jsis an ERD table with its columns, not an asset node: it keeps its markup and takesLineageNode's surface (radius 8, no rail, no shadow, the centre node drawn as selected).- The wiki's
CodeBlocksits on the always-dark--code-hl-*surface, and those highlight tokens exist for the dark surface only; coreCodeBlockis on the theme's surface, so moving it needs light-theme highlight tokens first.
TabLoadingFallback stays the brand loader (brand/README.md, "The loader": the route loader after 400 ms), not a
core Skeleton.
Old stylesheet rules. Every class the wrappers stopped emitting was looked up in the shell, @nistor-io/ui, the
plugin SDK and the Enterprise plugins, on the branch and at its base: seven selectors had lost their element. Two whose
intent holds were re-keyed onto core's markup, the rest deleted. The canonical KPI-tile exclusion of
check-design-system-baseline.mjs moved from metric-card.css (two caller hooks left) to core/MetricTile.css;
badge.css keeps its exclusion while CountBadge, EnvironmentBadge, FreshnessBadge and HoldReasonBadge
compose .badge.
Ratchet (npm run lint:ds, re-banked by --update; every metric at or below main's baseline):
| Metric | Plan target | Main before | Banked |
|---|---|---|---|
| bespokeCardRules | below 1844 | 1426 | 1371 |
| badgeSelectors | below 1330 | 1156 | 1135 |
| kpiTileSelectors | below 388 | 303 | 295 |
| rawHexCssFiles | at most 34 | 18 | 17 |
| rawFontSizeDecls | at most 280 | 153 | 150 |
| legacyTokenUses | (no rise) | 5906 | 5626 |
Catalogue. /wiki/design-system is an index: the token sections by role name (foundations/), the 31 core
sections (sections/, each in a light and a dark frame, with its props and the charter's do and don't), then the
shell's own blocks that are not in core (shell/). Hover, focus and pressed are forced through core's data-*
attributes wherever the pointer cannot hold them, form fields and table rows included.
Bundle budgets. The final build is over the budgets main carried, and dated waivers sized to it were granted
(Rémy, 18 Oct 2026; web/shell/scripts/bundle-budgets.json, _meta.waivers, expiring 31 Mar 2027):
| Budget | Before | Measured | Banked |
|---|---|---|---|
| totalRawJsBytes | 15,331,378 B | 17,012,763 B | 17,025,042 B |
| startupGzBytes | 295,943 B | 301,083 B | 305,393 B |
| mainGzBytes | 19,360 B | 20,128 B | 24,815 B |
Measured as the shell image builds it (web/shell/Dockerfile: REACT_APP_BACKEND_MODE=gcp, no source maps), which
is what the image's own check-bundle-size.mjs step reads; npm run build:image leaves out 4 files and 72 KB raw.
By source map, the largest contributors to total raw JS are core/* (392 KB raw, of which 313 KB are the same modules
emitted again in other chunks), the React Aria family (569 KB raw) and the wrappers with the rest of
@nistor-io/ui (181 KB). At start-up they are React Aria (26.4 KB gz), core/* (16.1 KB) and the rest of
@nistor-io/ui (12.2 KB). The main chunk is webpack's federation runtime, whose share and consume tables list each
provided core file. The burn-down is one shared chunk for core/* and React Aria (or providing core once), and React
Aria beyond Button and Tooltip off the start-up path.