# 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 `` 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 `` 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