diff --git a/docs/superpowers/specs/2026-08-08-layout-components-design.md b/docs/superpowers/specs/2026-08-08-layout-components-design.md new file mode 100644 index 00000000..0c7792c3 --- /dev/null +++ b/docs/superpowers/specs/2026-08-08-layout-components-design.md @@ -0,0 +1,178 @@ +# 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 `` 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 `` +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