docs: layout and page-structure component group design
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 `<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
|
||||
Reference in New Issue
Block a user