Skip to content

Commit 84aef1d

Browse files
authored
Withhold an option's box-shadow ease until the page is presented (#122)
## Problem `tests/test_render_gate.py::test_a_reader_arrives_at_what_they_left_rather_than_watching_it_arrive` is one of the thirteen failures in [the run this was dispatched for](https://github.com/max-sixty/leaf/actions/runs/33295489155), and the one #121 leaves out. It is intermittent and it predates the scroller commit: measured at [d226ee2](d226ee2a), 26feec8's parent, it failed 3 runs in 18 at `-n0`; at `main` `1dd6976` it failed 5 in 17. The extra motion is `CSSTransition box-shadow 120ms` on the page's `lf-option`s, and it is not replay writing `[chosen]` — the keyframes are `boxShadow: "none"` → `"rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px"`. Both channels are transparent, so the transition eases from nothing to nothing and no pixel differs at either end. It fires at ~105–155ms, before `data-lf-upgraded` and long before `data-lf-presented`, on whichever arrangement's load happens to hit it. The cause is the stylesheet's own arrival. `theme.css` is render-blocking but not style-blocking, so on a load where a style recalc falls inside its fetch (measured at 86–135ms for the composed 230KB sheet) the parser has already styled the options with box-shadow's initial `none`. The sheet then lands and states `box-shadow: var(--lf-ring), var(--lf-lift)` in the same breath as the `transition: box-shadow .12s ease` two rules below it, and the value change plays. Whether a recalc falls in that window is what makes the failure intermittent, and it is why the arrangement named in the message differs every time. ## Solution Withhold the transition until the page says it is presented. `theme.css`'s arrival guard already says "arriving is not a gesture" for `body` and for `.lf-ui`, and its own comment names this as the trade it leaves to the suite — "`transition` does not inherit, so what an unreached descendant can still play is a transition of its own declaring; that is the trade, and the check that finds one is the suite's". This is the finding, answered in the package that declares the transition rather than by widening the layer's guard into the universal selector that comment measured and rejected. Spelled as that guard is — `html:has(script[type="module"][src="/leaf.js"]) body:not([data-lf-presented])` — so a page with no runtime behind it, which never stamps and never held its options back, keeps the ease it always had. A copy has no script link and is out by the same clause. Nothing visible changes: the suppressed transition ran between two transparent shadows, and it ran while `main` was still `visibility: hidden` behind the presentation gate. ## Testing The named test, 15 consecutive runs at `-n0`: all green, against 3-in-18 at the commit before the scroller moved and 5-in-17 at `main`. `uv run pytest tests` — 721 passed, 6 skipped. `test_render_options.py`, `test_render_gate.py`, `test_render_widgets.py`, `test_render_pages.py`, `test_render_margin.py` with `--run-nightly` — 237 passed, and the only failures are the seven gutter findings #121 fixes, unchanged in shape. `pre-commit run --files skills/leaf/packages/default/theme.css` passes. <details><summary>How the diagnosis was taken</summary> A `transitionrun` listener installed through `page.add_init_script` before each arrival, recording `performance.now()`, the readiness stamps, the computed `--lf-ring`/`--lf-lift`, and `getAnimations()[0].effect.getKeyframes()`: ``` {'t': 117, 'tag': 'lf-option', 'id': 'wait-day', 'prop': 'box-shadow', 'presented': False, 'upgraded': False, 'shadow': 'rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px', 'ring': '0 0 #0000', 'lift': '0 0 #0000', 'joined': '1', 'settled': False, 'chosen': False, 'ready': 'interactive', 'sheets': 1, 'theme': '16-86', 'nav': '4/0'} ``` `theme` is theme.css's `startTime`-`responseEnd`, `nav` the navigation's `responseEnd`/`domContentLoadedEventStart`: the HTML was complete at 4ms and the sheet landed at 86ms, with the transition starting at 117ms. On the runs where the test passes no such entry exists at all. Hoisting the two shadow channels onto `lf-option` itself, so the `:has(> lf-option > strong)` card rule could not be what flips the value mid-parse, was tried first and changed nothing — the keyframes still started at `none`, which is what pointed at the sheet rather than at a selector inside it. </details> --- Independent of #121 and based on `main`, not on its branch: a separate cause in a separate layer, revertable on its own. Together the two take the failed run to green. Automated fix for [failed run](https://github.com/max-sixty/leaf/actions/runs/33295489155) --------- Co-authored-by: leaf-agent <318509791+leaf-agent@users.noreply.github.com>
1 parent d2dd825 commit 84aef1d

1 file changed

Lines changed: 24 additions & 0 deletions

File tree

skills/leaf/packages/default/theme.css

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,30 @@ lf-compare:has(> lf-variant > :not(a, abbr, area, audio, b, bdi, bdo, br, button
357357
--lf-lift: 0 3px 10px rgba(0, 0, 0, .14); }
358358
}
359359
}
360+
/* Arriving is not a gesture, which theme.css says for body and for the runtime's own
361+
* chrome and cannot say for a page element: `transition` does not inherit, so what an
362+
* unreached descendant still plays is a transition of its own declaring. That comment
363+
* names the trade and leaves the finding to the suite; this is the finding, and the
364+
* option's own rule is where it is answered. The suite carries less of that trade than
365+
* the sentence sounds: the returning-reader gate holds a return against a first visit
366+
* and reports only what the return adds, so it names a transition that plays on some
367+
* arrivals and not others and says nothing about one that plays on every arrival alike.
368+
* This was the first kind. A package declaring a transition on an element of its own
369+
* gets no warning of the second and has to withhold it the way this rule does.
370+
*
371+
* Nothing about the reader moves here. The stylesheet is render-blocking but not
372+
* style-blocking, so the parser styles the options before it lands and box-shadow holds
373+
* its initial `none`; the sheet then arrives and states `var(--lf-ring), var(--lf-lift)`
374+
* — two transparent channels, no pixel different — while the line above has already
375+
* granted the ease. Chrome reports a 120ms box-shadow transition on every option of a
376+
* page nobody has touched, and whether it reports one at all turns on whether a recalc
377+
* fell inside the sheet's own load, which is why the returning-reader gate found it on
378+
* some loads and not others. Withheld until the page says it is presented, so the ease
379+
* belongs to the pointer that earns it. Spelled as the layer's guard is — the runtime's
380+
* own script link, the presentation stamp — so a page with no runtime behind it, which
381+
* never stamps and never held its options back, keeps the ease it always had. */
382+
html:has(script[type="module"][src="/leaf.js"])
383+
body:not([data-lf-presented]) lf-option { transition: none; }
360384
}
361385
/* The mark the upgrade injects: the keyboard path and the state in one element. It is
362386
* silent until there is a pick to state, so its box is not the same in both states — the

0 commit comments

Comments
 (0)