# 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 wires 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
`wire-*` classes and a local style block. They are theme-aware, because the
utilities carry arbitrary values bound to tokens
(`bg-[var(--wire-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 `wire-*` 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 `wire-*` 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
