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:

  1. 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 in main.js) and no RAC is on the start-up path.
  2. 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 loads CoreSpikeHelpMenu.js (its own chunk, 367 B gz) with core MenuPopover, 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 needs MenuPopover in core/Menu.js (the menu without its trigger; the caller sets aria-haspopup and aria-expanded): a first version that swapped a placeholder trigger for a full Menu lost 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.
  3. 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.
  4. npm run build:remotes -- --only gitlab: check-remote-bundle.mjs OK (25 modules: 6 own, 8 consume-shared including the six @nistor-io/ui/core/*, 9 npm, all lucide's gitlab icon); no react-aria, react-stately, @react-aria or @internationalized string 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 through REACT_APP_DEV_REMOTES): remote.Tooltip === shell Tooltip is 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:

  • DetailSections is a tab strip over detail panels, not a key/value list; it draws its strip with SegmentedControl, itself a wrapper over core SegmentedButton.
  • TruncatedCell (one importer, the rules table's name cell) is an ellipsis with the native title; core DataTable has no truncation API to map it onto, and adding one is new core surface.
  • pages/Lineage/erd/ErdTableNode.js is an ERD table with its columns, not an asset node: it keeps its markup and takes LineageNode's surface (radius 8, no rail, no shadow, the centre node drawn as selected).
  • The wiki's CodeBlock sits on the always-dark --code-hl-* surface, and those highlight tokens exist for the dark surface only; core CodeBlock is 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.

Edit this page on GitLab

↑ ↓ to moveEnter to openEsc to close