Files
WRNexusJS/docs/superpowers/specs/2026-08-08-layout-components-design.md
T
Clintchiz 2c960fc1dc
Quality / quality (ubuntu-latest) (push) Failing after 9m49s
Quality / quality (windows-latest) (push) Canceled after 0s
refactor: migrate legacy wire namespace to wrn
2026-08-12 18:51:15 +05:30

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 container
  • data-wrn-splitter-handle on 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:production green

Phases

  1. Scaffolds — splitter runtime and component, CustomScrollbar
  2. Layout migration — Container, Columns, Grid, Divider, Image, Link, Typography, Kbd
  3. Page and section — Section, SectionHeader, PublicPageShell, PageHeader audit, plus a page in examples/basic-app composing 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 .wrn comments
  • /* */ only inside style blocks; // silently eats the next CSS rule
  • Block comments are not allowed inside props {} — line comments only
  • data-show takes 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