119 lines
6.3 KiB
Markdown
119 lines
6.3 KiB
Markdown
# UI library audit — 2026-08-08
|
||
|
||
A measured pass over `@wrnexus/ui` after the navigation and layout groups were
|
||
rebuilt. Numbers come from the source and from a browser, not from estimates.
|
||
|
||
## Fixed in this pass
|
||
|
||
**Ten theme tokens were referenced and never defined.** An undefined custom
|
||
property does not warn — it resolves to nothing — so the styles that used them
|
||
silently did nothing:
|
||
|
||
| Token | Effect while undefined |
|
||
| ---------------------------- | --------------------------------------------------------- |
|
||
| `--wrn-color-focus` | Focus rings drew with no colour, in 14 components |
|
||
| `--wrn-color-surface-soft` | Every soft surface rendered transparent, in 27 components |
|
||
| `--wrn-color-on-danger` | Text on danger fills had no colour |
|
||
| `--wrn-color-surface-subtle` | Subtle panels rendered transparent |
|
||
| `--wrn-color-input-*` (6) | Input backgrounds, borders and placeholders unstyled |
|
||
| `*-hover`, `*-contrast` | Missing for info, success, warning and danger |
|
||
|
||
They are derived in the theme now, so they follow the palette and the accent.
|
||
Verified in a browser: 38 tokens referenced, **0 undefined** (was 10).
|
||
|
||
Two mistakes worth recording, because both nearly became wrong conclusions:
|
||
|
||
- A first version of the check read the theme _source_ and reported a dozen
|
||
false positives, because most tokens are generated per palette rather than
|
||
written as literals. The check now reads the **rendered** theme CSS.
|
||
- The semantic token spread sat _after_ the primary and secondary palette
|
||
entries, so it would have overridden them. It now sits before.
|
||
|
||
## Correction: outputs, and what was actually wrong
|
||
|
||
The first version of this audit called the LayoutSplitter output failure
|
||
"narrow and unexplained" and treated 32 dead outputs as 16 components needing a
|
||
rebuild. Both were wrong, and the real cause was found by binding a page
|
||
handler to a component in a browser and watching what arrived.
|
||
|
||
**Two distinct faults were hiding behind one symptom.**
|
||
|
||
_Emitting the wrong way._ An output only reaches a parent `@binding` when the
|
||
component calls `output.<name>()`. The runtime resolves parent handlers from a
|
||
registry that only `invokeComponentOutput` reads, so a component that builds
|
||
its own `CustomEvent` — even a bubbling one, dispatched on its own root —
|
||
emits into nothing. Eighteen components did exactly that, so their outputs were
|
||
declared, documented, apparently fired, and never delivered. Converting them
|
||
took ten dead outputs off the list and cost no new machinery.
|
||
|
||
_Case._ HTML lowercases attribute names, so a parent's `@sizeChange` registers
|
||
under `sizechange` while the component emits `sizeChange`. The lookup missed,
|
||
fell through to a DOM dispatch, and the binding was never invoked — silently,
|
||
with no error at either end. **All 17 camelCase outputs in the library were
|
||
undeliverable**, including `DataTable.pageChange` and `DataTable.rowClick`,
|
||
`Map.markerClick`, `ChatBubble.messageClick` and `LayoutSplitter.sizeChange`.
|
||
The runtime now falls back to a case-insensitive lookup, and a test in
|
||
`packages/csr` fails without that fallback.
|
||
|
||
The lesson worth keeping: "outputs work elsewhere" was an assumption, not a
|
||
measurement. The components that worked happened to use all-lowercase names and
|
||
`output.*`; that coincidence made a framework-wide bug look like one broken
|
||
component.
|
||
|
||
## Outstanding, with numbers
|
||
|
||
**22 outputs across 9 components have no emitter of any kind** (was 32 across
|
||
16; see the correction above). The component advertises an output, a caller
|
||
binds to it, and nothing ever fires. Native event names are excluded — the
|
||
runtime binds a DOM-listener fallback on component tags, so `click` and `input`
|
||
do arrive.
|
||
|
||
| Component | Dead outputs |
|
||
| ------------------- | ----------------------------------------- |
|
||
| FileUpload | upload, progress, success, cancel, remove |
|
||
| ToastNotifications | add, dismiss, clear, action |
|
||
| AdvancedDatePicker | open, close, clear |
|
||
| AdvancedRangeSlider | start, end |
|
||
| Chart | dataPointClick, legendToggle |
|
||
| Confetti | start, complete |
|
||
| TreeView | expand, collapse |
|
||
| Toast | dismiss |
|
||
| CopyMarkup | success |
|
||
|
||
A test pins this at 22 as a ceiling that only moves down.
|
||
|
||
**28 components are still scaffolds** — no style block, no functions and no
|
||
handlers. Nine of them also declare outputs nothing emits. (An earlier figure of
|
||
23 here used a looser rule; see §3.2 of the remediation plan.)
|
||
These are the same shape as the Table scaffold that was removed and the
|
||
LayoutSplitter scaffold that was rebuilt.
|
||
|
||
**66 of 108 components have no local style block**, so they depend on
|
||
`ui.css`. That matters for delivery: a migrated component ships its CSS only
|
||
when it renders, while `ui.css` is served whole to every page — 175,848 bytes,
|
||
**25,908 gzipped**. Migrating the remainder would shrink it toward nothing.
|
||
|
||
**10 components still use Tailwind utilities**: FeatureIconCard, HeroActions,
|
||
InputNumber, List, Map, Marquee, SearchBox, TextLink, Timeline, button.
|
||
|
||
## Not worth doing
|
||
|
||
**Splitting the runtime into chunks.** Measured rather than assumed:
|
||
`reactive.js` is 69,947 bytes minified, roughly 21 kB gzipped, served as its
|
||
own file with a one-year immutable cache. Component-specific controllers are
|
||
about 18% of it, so splitting them out saves 3–4 kB gzipped on a first visit
|
||
and nothing after that. That is a poor return for lazy-loading complexity.
|
||
|
||
Hydration is not a bottleneck either: 21 scopes across 4,325 elements rehydrate
|
||
in 1.5 ms, and the shared document observer cost 0.9 ms across three seconds of
|
||
the busiest activity in the library.
|
||
|
||
## Known framework defects
|
||
|
||
- **A component tag nested inside another component slot is dropped**, leaving
|
||
only its children.
|
||
- **Component props and slot content render once** and do not track page state,
|
||
so the controlled-component pattern does not work.
|
||
- **i18n text is not server-rendered.** Nav links ship as empty spans and are
|
||
filled by the client, so navigation is blank until the runtime loads.
|