Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions skills/leaf/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,12 @@ Layout follows the same ownership rule. CSS owns the document shell: `body` is
the `lf-shell` container, `main` composes margin claims, and container queries
choose their postures from the room actually left by panels and trays.
`syncLayout` measures only chrome whose placement or reservation depends on
rendered chrome, and writes only chrome boxes. A `ResizeObserver` callback must
not resize the box it observes, directly or through a class or attribute that
changes that box.
rendered chrome, and writes only chrome boxes. It hears the shell's inline size
without deriving a posture from it: `layoutSizes` watches `document.body`'s
content-box width so a float placed in the margin is re-placed while a panel's
eased margin is still narrowing the page, and reads nothing else off that box. A
`ResizeObserver` callback must not resize the box it observes, directly or
through a class or attribute that changes that box.

## Startup and presentation

Expand Down Expand Up @@ -1245,8 +1248,22 @@ status-like item may yield its own width so controls to its right remain fixed.

`syncLayout` derives only floating chrome placement and reservations from current
chrome boxes. CSS owns the document shell: `body` is the named `lf-shell` inline-size
container, `main` composes its left and right claims, and container queries grant or
withdraw margin postures. No JavaScript measures that shell or mirrors a cramped state.
container, `main` composes its left and right claims, and queries grant or withdraw
margin postures. JavaScript may hear that shell's inline size as a signal to re-run
`syncLayout` — `layoutSizes` watches `document.body`'s content-box width, because a
panel's eased margin goes on narrowing the box a float stands in after `setPanel` has
returned and one synchronous pass at the press reads only the wide box — but it derives
no posture from it and mirrors no cramped state.

Which question a floor asks belongs to the posture it grants. A floor asking how much
room is left beside a panel or tray is a container query on `lf-shell`: 1152 and 1416
for the sidebar and sidenote strips, 1208 and 1472 for the thread's beside posture. The
living margin's 900 asks the window instead, because it is half of a pair —
`@media screen and (max-width: 899px)` stops drawing the margin at all, and a marker's
presence is not something a container can be asked about without the answer depending on
the strip the marker is asking for. Both halves of such a pair ask the same medium, or a
panel narrowing `body` under the floor withdraws the strip while the window keeps the
markers on screen with nothing reserved for them.

The browser's root is the document scrollport. `pageScroller` is the shared answer for
reading position and paging; native fragments, history restoration, wheel/touch input,
Expand Down
41 changes: 39 additions & 2 deletions skills/leaf/assets/runtime/chrome-layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,16 @@ export function createChromeLayout({
document.body.toggleAttribute("data-lf-panel", open);
toggleBtn.setAttribute("aria-expanded", String(open));
if (open) {
renderPanel();
// The layer before what goes in it. The panel is a dialog, and a dialog nobody has
// shown yet is display:none, so anything rendered into it measures zero — and
// renderPanel is where the anchor pass runs for the threads it draws. A mark hangs
// on the boxes its element shows through (shownParts), so a widget an agent sent in
// a reply resolved to an element with no box, took no mark, and left the thread
// still open in the panel pointing at nothing on either side. Focus is asked for in
// the same call because its target is threadsBox, which the render fills rather
// than replaces.
syncPanelLayer({ focus: true });
renderPanel();
syncGeneral(); // a restored draft has to reach the Send button's disabled state
} else if (panel.open) panel.close();
syncLayout();
Expand Down Expand Up @@ -250,7 +258,36 @@ export function createChromeLayout({
syncLayout();
});
};
const layoutSizes = new ResizeObserver(scheduleLayout);
// Body's own inline size is the first of them, because the strip the page yields to the
// panel is an eased margin: setPanel writes the attribute and returns, and the box the
// floats are placed in goes on narrowing for another fifth of a second. One synchronous
// syncLayout at the press reads the wide box, so a composer standing in a wide window's
// margin kept a place the narrowed page no longer has — an absolute child past body's
// client box, which is sideways-scrollable overflow, with the box standing on the panel
// that displaced it. Watched rather than heard through `transitionend` because an
// interrupted slide still reports its last frame, and a strip a margin resident claims
// moves this width with no transition to end.
//
// Filtered to the content box's width: body's block size is the document's content
// height, so hearing every body resize would feed ordinary page growth back into a
// writer that reserves flow content. The width still hears both an outer strip and a
// padding rail. Writes land in the following animation frame, outside ResizeObserver
// delivery, so a reservation changing another watched chrome box cannot create an
// undelivered-notification loop.
let bodyContentWidth = 0;
const layoutSizes = new ResizeObserver((entries) => {
let shellChanged = false;
for (const { contentRect, target } of entries) {
if (target !== document.body) {
shellChanged = true;
continue;
}
if (contentRect.width !== bodyContentWidth) shellChanged = true;
bodyContentWidth = contentRect.width;
}
if (shellChanged) scheduleLayout();
});
layoutSizes.observe(document.body);
Comment thread
leaf-agent marked this conversation as resolved.
layoutSizes.observe(panelFoot);
layoutSizes.observe(keylineEl);
// The composer grows under typing (field-sizing), and a box placed above its passage
Expand Down
12 changes: 11 additions & 1 deletion skills/leaf/assets/runtime/living-margin.js
Original file line number Diff line number Diff line change
Expand Up @@ -1093,7 +1093,17 @@ export function createLivingMargin(dependencies) {
if (!preview.matches(":popover-open") && !previewShowing) {
previewShowing = true;
try {
preview.showPopover({ source: button });
// Shown without naming the marker as its source, though the marker is what opened
// it. An invoker relationship puts the card in the sequential focus order directly
// after the control that invoked it, and this card is not invoked: it opens on the
// marker merely taking focus, so a reader walking the margin was made to Tab
// through a close button and every action of a preview they had not asked for —
// three stops between one suggestion's controls and the next, with the card's
// contents changing under them as they went. The card stays reachable where it
// stands in the layer, which is where it was reachable before. Its position is
// the anchor-name written above rather than the implicit anchor a source would
// give, so the placement asks nothing of this.
preview.showPopover();
} catch (error) {
// Chromium also refuses a second popover operation in the same rendering turn,
// even when it belongs to another surface. Keep the requested marker current and
Expand Down
13 changes: 10 additions & 3 deletions skills/leaf/assets/theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -1405,8 +1405,16 @@ lf-suggestion:not([data-lf-state]) > :is(lf-old, lf-new) > .lf-quiet {
font: 600 10px/14px var(--sans); text-align: center; }
.lf-margin-target { outline: var(--here-ring); --lf-here-ring: margin-target;
outline-offset: calc(-1 * var(--here-ring-w)); }
@media screen {
@container lf-shell (min-width: 900px) {
/* The one floor that asks the window rather than the page's own box, because it is the
* half of a pair: below it the rule further down stops drawing the living margin at all,
* and that rule is a media query because a marker's presence is not a thing a container
* can be asked about without the answer depending on the strip the marker is asking for.
* Two readings of one 900 is the page showing markers it has reserved no room for — the
* panel narrowing body under the floor took the strip away while the window kept the
* markers, and every row docked beside an empty rail. The floors below stay container
* queries: those ask how much room is left beside the panel, which is the question the
* runtime's old cramped veto asked, and the answer moves with the panel by design. */
@media screen and (min-width: 900px) {
body:has(:where(.lf-living-margin:not([hidden]) .lf-margin-marker:not([hidden]))) > main {
/* The 30px marker hangs 22px off the column and its complete host keeps the
shared focus-ring room at the outer edge. Claim all three, or opening a side
Expand All @@ -1421,7 +1429,6 @@ lf-suggestion:not([data-lf-state]) > :is(lf-old, lf-new) > .lf-quiet {
main:has(.lf-margin-item[data-lf-claims-rail]) {
--claim-rail: var(--rail, 54px);
}
}
}
@media screen {
/* 1208px leaves the document's 640px evidence floor beside the 520px thread
Expand Down