diff --git a/skills/leaf/CLAUDE.md b/skills/leaf/CLAUDE.md
index 1fe630b657..7c48ea4dfd 100644
--- a/skills/leaf/CLAUDE.md
+++ b/skills/leaf/CLAUDE.md
@@ -1732,8 +1732,12 @@ cost one letter; only the tail branches when the viewport holds more targets tha
alphabet. Unlike `g` addresses, these hints are ephemeral and make no promise across a
scroll or revision. They are the whole route, so none may be dropped because its chip
collides. A target whose visible box is strictly smaller and fully enclosed by another
-target steps its chip right once per enclosing box. Equal boxes stay at the same depth;
-the collision pass separates their chips without inventing a hierarchy.
+target steps its chip right once per enclosing box. An ancestor and descendant with the
+same visible box name one target: the innermost remains, matching direct aim. Equal boxes
+outside one containment chain stay at the same depth, and the collision pass separates
+their chips without inventing a hierarchy. Membership is fixed for the length of a
+scroll and re-read once it settles, so a target arriving mid-scroll is named at rest
+rather than on the frame it appears.
Tab and Shift-Tab walk the visible target map and announce each item. Enter chooses the
last one announced. A viewport change that removes or renames that target clears the
diff --git a/skills/leaf/assets/runtime/composing/targets.js b/skills/leaf/assets/runtime/composing/targets.js
index b63c8bc072..888c5b0907 100644
--- a/skills/leaf/assets/runtime/composing/targets.js
+++ b/skills/leaf/assets/runtime/composing/targets.js
@@ -46,6 +46,7 @@ export function createTargetSelection({
let hintActive = -1;
let opener = null;
let searchReturnsToHints = false;
+ let scrolling = false;
const clips = () => new Map();
const covered = () => banner.getBoundingClientRect().bottom;
@@ -68,11 +69,13 @@ export function createTargetSelection({
const y = Math.max(covered(), Math.min(innerHeight - 1, box.top + 1));
return !inChrome(document.elementFromPoint(x, y));
};
- // Chromium retains geometry for descendants suppressed by a closed disclosure.
- // Require a part the browser paints before that stale box can enter or remain in the
- // hint map. shownParts keeps display: contents items eligible through visible children.
+ // Chromium retains geometry for descendants suppressed by a closed disclosure. Ask
+ // visibility before geometry so those descendants cost no box reads. A display: contents
+ // item has no box of its own and stays eligible through a visible child.
const targetShown = ({ element }) =>
- shownParts(element).some((part) => part.checkVisibility());
+ element.checkVisibility() ||
+ (getComputedStyle(element).display === "contents" &&
+ shownParts(element).some((part) => part.checkVisibility()));
function clippedRect(box, clip) {
if (!box || !clip) return null;
@@ -115,6 +118,12 @@ export function createTargetSelection({
return codes.slice(0, count);
}
+ const sameVisibleBox = (a, b) =>
+ Math.abs(a.left - b.left) < 0.5 &&
+ Math.abs(a.top - b.top) < 0.5 &&
+ Math.abs(a.right - b.right) < 0.5 &&
+ Math.abs(a.bottom - b.bottom) < 0.5;
+
function visibleTargets() {
const cache = clips();
const targets = aimTargets()
@@ -123,11 +132,24 @@ export function createTargetSelection({
.map((target) => ({ ...target, rect: shownRect(target.element, cache) }))
.filter(({ rect }) => exposed(rect))
.sort((a, b) => a.rect.top - b.rect.top || a.rect.left - b.rect.left);
- const codes = hintCodes(targets.length);
- return targets.map((target, index) => ({
+ // Direct aiming chooses the innermost stable item under the pointer. When an
+ // ancestor and descendant paint the same visible box, naming both would offer two
+ // keys for that one choice. Keep distinct nested extents and unrelated overlaps.
+ const unique = targets.filter(
+ (outer) =>
+ !targets.some(
+ (inner) =>
+ inner !== outer &&
+ outer.element !== inner.element &&
+ outer.element.contains(inner.element) &&
+ sameVisibleBox(outer.rect, inner.rect),
+ ),
+ );
+ const codes = hintCodes(unique.length);
+ return unique.map((target, index) => ({
...target,
code: codes[index],
- nesting: targets.filter(
+ nesting: unique.filter(
(outer) =>
outer !== target &&
outer.rect.left <= target.rect.left &&
@@ -151,6 +173,7 @@ export function createTargetSelection({
matches = [];
active = -1;
hintActive = -1;
+ scrolling = false;
selectionInput.value = "";
selectionSearch.hidden = true;
if (on && withHints) {
@@ -390,9 +413,9 @@ export function createTargetSelection({
if (selectionLayer.childElementCount) selectionLayer.replaceChildren();
return;
}
- const refreshed = !searching && !prefix;
+ const refreshed = !searching && !prefix && !scrolling;
+ const heard = hinted()[hintActive];
if (refreshed) {
- const heard = hinted()[hintActive];
candidates = visibleTargets();
const still = heard
? candidates.findIndex(
@@ -403,6 +426,7 @@ export function createTargetSelection({
hintActive = still;
}
const drawn = [];
+ const drawnTargets = new Set();
if (!searching) {
const cache = clips();
for (const target of candidates) {
@@ -415,6 +439,7 @@ export function createTargetSelection({
chip.style.top = `${Math.min(bottomCovered() - 10, Math.max(covered(), rect.top))}px`;
if (rect.clippedTop || rect.top < covered()) chip.classList.add("lf-in");
drawn.push(chip);
+ drawnTargets.add(target);
}
} else if (matches[active]) {
const owner = matchOwner(matches[active]);
@@ -431,6 +456,7 @@ export function createTargetSelection({
drawn.push(mark);
}
}
+ if (!refreshed && heard && !drawnTargets.has(heard)) hintActive = -1;
selectionLayer.replaceChildren(...drawn);
if (!searching) spreadHints(drawn);
}
@@ -440,11 +466,25 @@ export function createTargetSelection({
"scroll",
() => {
if (!open) return;
+ scrolling = true;
+ paintHere();
+ },
+ { capture: true, passive: true },
+ );
+ addEventListener(
+ "scrollend",
+ () => {
+ if (!open || !scrolling) return;
+ scrolling = false;
paintHere();
},
{ capture: true, passive: true },
);
- addEventListener("resize", () => open && paintHere());
+ addEventListener("resize", () => {
+ if (!open) return;
+ scrolling = false;
+ paintHere();
+ });
const PAGE_SEARCH = {
id: "page.search.open",
diff --git a/skills/leaf/packages/default/theme.css b/skills/leaf/packages/default/theme.css
index 579a4d634d..6d8a2b9b9f 100644
--- a/skills/leaf/packages/default/theme.css
+++ b/skills/leaf/packages/default/theme.css
@@ -1315,7 +1315,7 @@ lf-toc a:hover, lf-toc a:focus-visible { color: var(--accent); }
forced-color-adjust: none; background: Highlight;
}
}
-.lf-toc-target { display: block; width: 1px; height: 1px; margin-bottom: -1px; }
+.lf-toc-target { display: block; width: 1px; height: 0; }
/* lf-decision: a semantic reading and arrival boundary, with no presentation of its own.
* Block rather than the custom-element default inline so its box begins at the
diff --git a/skills/leaf/references/page-authoring.md b/skills/leaf/references/page-authoring.md
index 7a2835e6bb..7c09ee2356 100644
--- a/skills/leaf/references/page-authoring.md
+++ b/skills/leaf/references/page-authoring.md
@@ -85,7 +85,9 @@ page width.
## Stable anchors
-Give every section, major block, and widget item a stable, meaningful `id`.
+Give each section, major block, and widget item a stable, meaningful `id` at the
+tightest semantic boundary a reader can distinguish. Where a sole child fills a
+transparent wrapper, let the child carry the pair's one id.
Threads and reading position attach to those ids across versions. Keep an id
where its passage survives, and stay out of the `lf-` prefix: it is the runtime's
namespace for ids and for classes alike, and `data-lf-` is the same for
diff --git a/tests/test_render_semantic_selection.py b/tests/test_render_semantic_selection.py
index 4c79921f75..ee3b35ca01 100644
--- a/tests/test_render_semantic_selection.py
+++ b/tests/test_render_semantic_selection.py
@@ -280,6 +280,38 @@ def test_nested_item_hints_show_containment_without_covering_each_other(browser,
page.close()
+def test_identical_nested_item_hints_choose_the_innermost_target(browser, serve):
+ """A transparent wrapper and its only child can describe one visible box. The
+ chooser names that box once and agrees with direct aiming by selecting the child."""
+ html = leaf_page(
+ "identical nested targets",
+ '
A boxless visible target.