Skip to content

fix(webview): key the concealed fence gap offset on the gap border, not the language tag - #396

Merged
mtskf merged 4 commits into
mainfrom
fix/copy-button-top-inset-bare-fence
Sep 5, 2026
Merged

fix(webview): key the concealed fence gap offset on the gap border, not the language tag#396
mtskf merged 4 commits into
mainfrom
fix/copy-button-top-inset-bare-fence

Conversation

@mtskf

@mtskf mtskf commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Summary

A concealed fenced code block with no language tag drew its copy button 4px above the panel's painted top edge, hanging over the rounded corner; its language-tagged twin sat 4px inside. The correction that fixes this already existed — it was just gated on the wrong thing.

Why it happened

The panel fill is background-clip: padding-box, so a visible open edge carrying blockEdgeGapCorner's transparent --quoll-block-gap-y top border paints its surface that far below its border box.

  • Revealed: the copy button's anchor is that bordered line, so top: 0.3em resolves against the paint.
  • Concealed: the anchor is the zero-height .quoll-fenced-code-fence-hidden row, which sits at the border box's top and has no border of its own — the same top resolves against a datum 8px too high, netting −4px.

The rule adding that 8px back was scoped to :has(+ .cm-line.quoll-fenced-code-has-language.quoll-fenced-code-outer-open), and its comment declared bare blocks deliberately untouched. But the language tag never asked for the offset — the gap border does, and a bare block's panel carries the identical one. The gate had frozen an attribute that merely correlated while the header bar (a tagged-only feature) was being built.

Changes

  • concealedGapAnchor keys the offset on the gap border, not the language tag. This also fixes a second miss on the same axis: block-style.ts suppresses the fenced -outer-open inside a blockquote and lets quoll-blockquote-outer-open supply the same border, so blockquote-nested blocks were uncorrected too — tagged ones included.
  • Both sources are spelled as a selector list, which cannot double-count: the two classes are mutually exclusive on a fenced open edge, and two matching branches still apply one declaration. (Contrast the horizontal axis, where :not(.quoll-blockquote) guards a genuine two-path double-count.)
  • fencedHeaderBarThemeSpec gains an explicit Record<string, Record<string, string>> annotation — the two computed template-literal keys otherwise make TS infer a union that no longer satisfies StyleSpec's index signature.

Related

  • TODO: 🐛 [MEDIUM] 言語タグなし fenced code のコピーボタンがパネルの塗り上端から 4px はみ出す(concealed 状態のみ)

Test Plan

Real-pixel regression pins in test/webview-browser/fenced-header-bar.browser.test.ts measure the copy button's top against the panel's painted top edge (the open-edge line's rect top plus its borderTopWidth), pairing each button with its own panel. happy-dom cannot see any of this — no layout, and it drops calc()/var().

  • bare / tagged × concealed / revealed, top-level — all land +4px inside
  • blockquote-nested bare and tagged, concealed — the second gap-border source
  • list-nested bare, concealed
  • bodyless bare and header-only tagged — already correct, must not gain a second offset
  • directly-adjacent pair — the second block yields its -outer-open, so it needs no offset and gets none
  • Non-vacuity: reverting only theme.ts turns 5 of these red (and 2 spec-contract pins in cm-fenced-code-language-picker.test.ts), while the already-correct states stay green
  • pnpm compile, pnpm lint, pnpm test:unit (5212), pnpm test:browser (74), pnpm build, pnpm package (audit clean), force-install + reload

…ot the language tag

A concealed fenced block with no language tag drew its copy button 4px ABOVE
the panel's painted top edge, hanging over the rounded corner. Its tagged twin
sat 4px inside.

The panel fill is `background-clip: padding-box`, so a visible open edge that
carries blockEdgeGapCorner's transparent --quoll-block-gap-y top border paints
its surface that far below its border box. In the revealed state the copy
button's anchor IS that bordered line, so `top: 0.3em` resolves against the
paint. In the concealed state the anchor is the zero-height
`.quoll-fenced-code-fence-hidden` row, which sits at the border box's top and
has no border of its own — the same `top` resolves against a datum 8px too
high. The rule that adds the gap back existed, but it was gated on the NEXT
line being a `-has-language` header, and its comment declared bare blocks
deliberately untouched.

The language tag was never what asked for the offset — the gap border is, and
a bare block's panel carries exactly the same one. The gate had simply frozen
an attribute that happened to correlate while the header bar (a tagged-only
feature) was being built.

Widening it also fixes a second miss on the same axis: block-style suppresses
the fenced `-outer-open` inside a blockquote and lets `quoll-blockquote-outer-open`
supply the identical border instead, so blockquote-nested blocks were
uncorrected too — tagged ones included. `concealedGapAnchor` now spells both
sources as a selector LIST, which cannot double-count: the two classes are
mutually exclusive on a fenced open edge, and two matching branches still apply
one declaration.

Measured in the real-browser harness across bare/tagged x concealed/revealed x
top-level/blockquote/list/bodyless, plus a directly-adjacent pair (whose second
block correctly needs no offset because it yields its `-outer-open`). Every
state now lands 4px inside the paint; the already-correct ones did not move.
The intro comment above fencedHeaderBarThemeSpec's "Conceal/reveal
geometry" paragraph said "two :has(+ …) / descendant rules" re-add the
alignment offsets, but the spec actually has three: two independent
`:has(+ …)` selector lists from concealedGapAnchor (one per control —
copy button, label) for the vertical gap-y offset, plus one descendant
rule for the label's horizontal column inset. The undercount predates
this PR; this PR only touched the paragraph while generalizing
concealedGapAnchor to take a control argument, so resolve the
pre-existing ambiguity while the comment is already being edited.

Comment-only change, no selectors or behaviour touched.
…r pins

paintedPanelTop's `lines[i] as HTMLElement` restated a type the array
already has, and the cases array declared an `expected?: number` field
no case ever set and no assertion ever read — the band comes from the
module-level EXPECTED_INSET / TOLERANCE. Both are cosmetic; no assertion,
statement, or selector changed.
insets[0] is already typed number (copyInsets returns number[], and
noUncheckedIndexedAccess is not enabled in any tsconfig), so the
'as number' cast at line 208 was a no-op. Cycle 1 refactor commit
b46e98e removed the same-shape cast at line 146 but missed this one.
Consistency cleanup, not a correctness fix.
@mtskf
mtskf merged commit ad56e35 into main Sep 5, 2026
2 checks passed
@mtskf
mtskf deleted the fix/copy-button-top-inset-bare-fence branch September 5, 2026 02:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant