9.0 KiB
Layout and page-structure components — design
Date: 2026-08-08 Status: approved, not yet implemented
Problem
Two problems, in different proportions to the navigation group.
Three components advertise behaviour they do not have.
LayoutSplitter is the worst component in the library. It declares
resizeStart, resize and resizeEnd, so a caller reasonably connects up
@resize and receives nothing, forever, with no error. It has no pointer
handling at all, and its props are columns, gap and maxWidth, copied
wholesale from a grid scaffold. CustomScrollbar is the same shape: a
scroll output, no behaviour, and the same wrong grid props.
Kbd is nine lines, but that is not the same failure — a <kbd> element
genuinely does not need more. It is thin, not dishonest.
Most of the group is built on Tailwind utilities.
Seven of the ten use utility classes and class: conditionals rather than
wrn-* classes and a local style block. They are theme-aware, because the
utilities carry arbitrary values bound to tokens
(bg-[var(--wrn-color-surface-soft)]), but they depend on Tailwind being
present and produce a class explosion. SectionHeader has 38 such lines.
A correction that shaped this survey: when Tabs was rewritten it was described as "the only component in the library using Tailwind utility classes". That was wrong. 20 of 108 do, and seven of those are in this group.
Scope
The ten layout components plus the four page and section primitives, because the goal is composing whole pages and the section-level pieces live in other categories:
| Layout | Container, Columns, Grid, LayoutSplitter, Typography, Image, Link, Divider, Kbd, CustomScrollbar |
| Page and section | Section, SectionHeader, PageHeader, PublicPageShell |
PageHeader is already on the target convention — 696 lines, its own style
block, zero Tailwind. It is the model the rest migrate toward, and it needs an
audit rather than a rewrite.
Decisions
Styling
Every component in scope moves to wrn-* classes with a local style {}
block and data-* attributes for variants, matching PageHeader, DataTable,
Toaster and the navigation group.
Variants become data attributes rather than class: conditionals, so
variant="soft" renders data-variant="soft" and the style block selects on
it. That replaces roughly a dozen class: lines per component with one
attribute and a handful of rules.
Tailwind stays available to applications. This is about the shipped library not requiring it.
LayoutSplitter
Real resizing, driven from the reactive runtime by a declarative attribute:
data-wrn-splitter="horizontal|vertical"on the containerdata-wrn-splitter-handleon the divider- the runtime writes the resolved size onto the container as a CSS custom property, so the component styles from it
Pointer events fire far too often to route through client functions, and drag
state written inside a pointermove callback is dropped — the same constraint
that put roving focus and scrollspy in the runtime.
Keyboard support is part of the contract, not an extra: arrow keys move the
divider by a step, Home and End jump to the bounds, and the handle carries
role="separator" with aria-valuenow, aria-valuemin and aria-valuemax.
Sizes are clamped to a minSize per pane so a pane cannot be dragged to
nothing and become unrecoverable.
CustomScrollbar
Scrollbar styling is CSS: scrollbar-width and scrollbar-color for the
standard property, with ::-webkit-scrollbar rules for browsers that still
need them. No runtime, no scroll output — the existing scroll output is
removed rather than left unimplemented, since a caller can listen for a plain
scroll event on the element.
Its grid props (columns, gap, maxWidth) are replaced with ones that mean
something: axis, thickness, thumb, track, maxHeight.
Kbd
Left as it is, beyond the wrn-* migration. It is thin because a <kbd>
is thin.
Runtime budget
The splitter is roughly 2k minified against 67.7k of an 80k budget. It fits. The budget now measures minified output, so comments cost nothing.
Components
| Component | Work |
|---|---|
| LayoutSplitter | Build. Runtime-driven pointer resize, keyboard steps, role="separator" with value attributes, per-pane minSize, real outputs |
| CustomScrollbar | Build. Themed scrollbar styling, meaningful props, the fake scroll output removed |
| Container | Migrate. maxWidth, padding, centered as data attributes |
| Columns | Migrate. columns, gap, responsive collapse in a media query rather than md: prefixes |
| Grid | Migrate. columns, gap, minItemWidth for an auto-fit track |
| Typography | Migrate the remaining Tailwind lines into its existing style block |
| Image | Migrate. ratio, fit, rounded, loading |
| Link | Migrate. variant, underline, external-link affordance |
| Divider | Migrate. orientation, variant, optional label |
| Kbd | Migrate only |
| Section | Migrate. spacing, variant, color as data attributes |
| SectionHeader | Migrate. Its 38 Tailwind lines collapse into a style block |
| PublicPageShell | Migrate. minHeight, overflow, background, headerOffset |
| PageHeader | Audit only — already on the convention |
Component count stays at 108; every file already exists.
Data flow
Props in, outputs out, no global state. The splitter is the only component with runtime state, and that state lives in the DOM as a custom property rather than in component state.
Error handling
Numeric props (columns, minSize, thickness) clamp rather than throw,
because they arrive as HTML attributes. A splitter with one child renders that
child full width instead of erroring.
Testing
- Per-component entries in
packages/ui/test/ui.test.ts - Splitter drag and keyboard covered in
packages/csr/test/reactive.test.ts, using markers rather than measured size: the test DOM reports every element as zero-sized - Showcase profiles for each, so every component gets live demos
- Regenerate the component reference before the showcase — the generator cannot see new props otherwise
- Each phase ends with
bun run check:productiongreen
Phases
- Scaffolds — splitter runtime and component, CustomScrollbar
- Layout migration — Container, Columns, Grid, Divider, Image, Link, Typography, Kbd
- Page and section — Section, SectionHeader, PublicPageShell, PageHeader
audit, plus a page in
examples/basic-appcomposing the whole set
Risks
- The migration rewrites working markup. Visual regressions are the main danger, and the browser pane cannot take screenshots, so verification is computed styles and the UI visual contract rather than eyes on pixels.
- Removing
class:conditionals changes the rendered class list, so any application selecting on those utility classes breaks. This needs a migration entry alongside the Tabs and Sidebar changes already outstanding.
Codebase constraints to respect
- No apostrophes in
.wrncomments /* */only inside style blocks;//silently eats the next CSS rule- Block comments are not allowed inside
props {}— line comments only data-showtakes a bare expression, not{...}- One root element per component
- No deferred state writes in client functions
- Object props are passed through state, never inline braces in an attribute
- Package components need explicit imports
- Stop the dev server before running tests; it wipes
.wrnexus - Restart the dev server after changing a package component