Stop snapshotting the page; own the wheel gesture; one-row phone nav

Selection markers, tab panels, brand changes and theme changes no longer use
a view transition. The page snapshot could not blur what sits behind frosted
glass, so every frosted surface went clear for the length of the change.
nv.transition now measures the marker before and after, and animates the
marker itself (FLIP); arriving content animates on its own; colours fade on
their own surfaces. Verified in Firefox: no view transition is ever started.

Time picker: wheels take every pointer type (mouse, pen, touch) with
touch-action: none, so a phone, a browser's phone mode and a desktop behave
alike. The rubber-band is capped at about 40% of a row (a whole row made the
neighbour look selected). Each wheel has its own frame slot, so one wheel can
no longer swallow another's last scroll event. Switching 24H / AM / PM
happens when the wheel settles, and the hours animate into the new layout.

Carousel arrows are frosted glass, as anything floating over content should be.
At phone width the top bar keeps to one row and its link list scrolls.

GUIDELINES.md records the new cross-engine rules.

check.py, build.py and build.py --check pass.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-10-28 10:09:14 +01:00
commit 2e956cec42
11 changed files with 273 additions and 112 deletions

View file

@ -186,22 +186,24 @@ could have watched it reach.
| Change | How it stays continuous |
| ------ | ----------------------- |
| Selection moves between items | The marker itself travels. It is a pseudo-element with a shared `view-transition-name`, so the browser interpolates its old box into its new one. |
| Switching tabs | Panels cross-fade inside one box and slide with the direction of travel — forward one way, back the other. |
| Selection moves between items | The marker itself travels. It is a pseudo-element; `niveus.js` measures it before and after the change and animates the new marker from the old box to its own. |
| Switching tabs | The incoming panel slides in from the side the user is moving towards, and the marker travels with it. |
| Content expanding | `interpolate-size` lets `block-size: auto` animate, so content grows from the row that revealed it. Nothing is measured. |
| Menus and dialogs | A menu is anchored to its trigger and scales out of its edge; a dialog expands away from the control that opened it. |
| Theme change | The whole surface cross-fades. Surfaces also transition their own colors, so it glides even with no script. |
| Theme or brand change | Surfaces transition their own colors, so it glides even with no script. Nothing snapshots the page, so frosted glass stays live while it happens. |
| Hover, press, focus, select | All have a duration. Marks grow from their centre, the switch thumb travels its track. |
Most of this is plain CSS. The three cases CSS cannot do alone — a selection
moving *between two elements*, one panel replacing another, and the whole page
changing appearance — need a view transition, which needs one line of script:
Most of this is plain CSS. The two cases CSS cannot do alone — a selection
moving *between two elements*, and one panel replacing another — need one line
of script. It deliberately does **not** use a view transition: a page-wide
snapshot cannot blur what sits behind frosted glass, so every frosted surface
went clear for the length of the change.
```js
nv.transition(() => { // or document.startViewTransition directly
nv.transition(() => {
previous.removeAttribute("aria-current");
next.setAttribute("aria-current", "page");
});
}, { scope: group }); // the group whose marker should travel
```
`niveus.js` is optional and dependency-free. Without it every component still
@ -214,27 +216,13 @@ place once as it scrolls into view, and `.nv-reveal-group` brings its children
in one after another, `--nv-stagger` apart. Only the script marks anything as
waiting, so without it — and under reduced motion — nothing is ever hidden.
### One rule worth knowing
### Nothing to name, nothing to keep unique
A `view-transition-name` must be unique among all rendered elements, or the
browser skips the transition entirely. Each selection group therefore needs its
own marker name:
```html
<nav class="nv-nav" style="--nv-selection-name: nv-sel-sidebar"> … </nav>
<nav class="nv-nav-tabs" style="--nv-selection-name: nv-sel-tabs"> … </nav>
```
Tab strips default to `nv-tabs-selection` and everything else to
`nv-selection`, which is enough for one of each per page.
### Motion under a transition
Transition layers always paint above the page, so a travelling marker cannot
stay behind the labels it passes the way it does at rest. Rather than let an
opaque marker wipe them, Niveus thins it while it is in flight: it stays
visible the whole way — it can still be followed — but reads as having lifted
off the surface to travel.
Because the marker is animated directly, several selection groups can change at
once and none of them needs a name. (`--nv-selection-name` only matters to
application code that starts its own view transition; if you do, remember that
a `view-transition-name` must be unique among all rendered elements, and that
the snapshot flattens frosted glass for as long as it runs.)
`prefers-reduced-motion` collapses every duration at the token level and skips
view transitions entirely. The change still happens; it just arrives.
@ -490,7 +478,7 @@ to the state change simply happening rather than travelling:
| Feature | Used for | Without it |
| ------- | -------- | ---------- |
| View transitions | travelling selection, tab panels, theme cross-fade | the change applies instantly |
| Web Animations (`pseudoElement`) | travelling selection marker, tab panels | the change applies instantly |
| `interpolate-size` | disclosure height | content appears at full height (`.nv-collapse` still animates) |
| Anchor positioning | menus tied to their trigger | the menu centres itself |
| `popover` | menus, light dismiss | needs the application's own toggle |