snapshotBones reads li, td, th, p and h1–h6 as atomic bones whatever they hold, so a list of cards paints as one solid rectangle per card and nothing inside it draws. config.leafTags only adds to the set, so a project cannot take a tag off it.
dist/extract.js, the isLeaf line:
const isLeaf = children.length === 0 || isMedia || isFormEl || leafTags.has(tag);
Reproduction
<ul>
<li>
<article class="card">
<h3>Upper A</h3>
<p>2 sections · 5 exercises · ≈42 min</p>
<footer><button>Edit</button><button>Start</button></footer>
</article>
</li>
</ul>
Expected: a card surface with a heading, a line of copy and two controls. Actual: one bone the size of the card. The <li> is on the leaf list, so the walk stops there and the article, the h3, the p and the two buttons never emit.
Our /routines page is exactly this and its capture is two grey slabs. Our routine builder is worse — its structure rail became a <ul> of sections, so a fresh capture replaces a rail of headings, pills and rows with a single 167px block.
The fix
A tag on the leaf list is a leaf while everything it holds is inline; a leaf tag that wraps a block is a container and is walked into like any other. <p>Hello <strong>world</strong></p> stays one bone, because strong computes to display: inline. <li><div class="card">…</div></li> recurses, because the div does not.
// A leaf tag is one bone only while everything it holds is inline. A tag on
// the list that wraps a block — an `li` around a card, a `td` around a panel —
// is a container, and is walked into like any other.
const holdsOnlyInline = children.every(child => {
const display = getComputedStyle(child).display;
return display === 'inline' || display === 'contents';
});
const isLeaf = children.length === 0 || isMedia || isFormEl || (leafTags.has(tag) && holdsOnlyInline);
children is already the visible-child list computed above it, and every runs only for a tag on the leaf list that has children, so the extra getComputedStyle calls are bounded by that case.
The leafTags doc comment in dist/types.d.ts says "always captured as a single atomic bone, regardless of children", which this narrows — worth rewording to say the tag is atomic while what it holds is inline.
We run this as a pnpm patch on boneyard-js@1.9.0 today. Happy to send it as a pull request if you would rather have one.
Version: boneyard-js@1.9.0, captured through the CLI (boneyard-js build) against a Next.js 16 app.
snapshotBonesreadsli,td,th,pandh1–h6as atomic bones whatever they hold, so a list of cards paints as one solid rectangle per card and nothing inside it draws.config.leafTagsonly adds to the set, so a project cannot take a tag off it.dist/extract.js, theisLeafline:Reproduction
Expected: a card surface with a heading, a line of copy and two controls. Actual: one bone the size of the card. The
<li>is on the leaf list, so the walk stops there and thearticle, theh3, thepand the two buttons never emit.Our
/routinespage is exactly this and its capture is two grey slabs. Our routine builder is worse — its structure rail became a<ul>of sections, so a fresh capture replaces a rail of headings, pills and rows with a single 167px block.The fix
A tag on the leaf list is a leaf while everything it holds is inline; a leaf tag that wraps a block is a container and is walked into like any other.
<p>Hello <strong>world</strong></p>stays one bone, becausestrongcomputes todisplay: inline.<li><div class="card">…</div></li>recurses, because thedivdoes not.childrenis already the visible-child list computed above it, andeveryruns only for a tag on the leaf list that has children, so the extragetComputedStylecalls are bounded by that case.The
leafTagsdoc comment indist/types.d.tssays "always captured as a single atomic bone, regardless of children", which this narrows — worth rewording to say the tag is atomic while what it holds is inline.We run this as a
pnpmpatch onboneyard-js@1.9.0today. Happy to send it as a pull request if you would rather have one.Version:
boneyard-js@1.9.0, captured through the CLI (boneyard-js build) against a Next.js 16 app.