fix(mask): the live preview reaches its styles through the scope barrier #14

Merged
bjoern merged 1 commit from fix/mask-live-preview-scope into main 2026-08-06 12:50:39 +02:00
Member

The MaskCanvas gesture preview is injected by mask.js via innerHTML, so its shapes never receive the CSS-isolation scope attribute — the scoped .kg-mask__live-rect / .kg-mask__live-stroke rules silently missed them:

  • The rectangle drag rendered as an unstyled SVG rect: solid black at 100% opacity.
  • The brush and eraser previews (default stroke: none) never rendered at all.

The fix routes the three live rules through ::deep on the scoped live layer (.kg-mask__live ::deep …), the same pattern FileUpload already uses for its cross-boundary input.

Verified in the browser against the compiled bundle: an injected rect without the descendant rule computes fill rgb(0,0,0) / opacity 1 (the reported black box); with it, all three tools compute the translucent accent preview (rect fill accent @ 0.25, stroke accent @ 0.35).

🤖 Generated with Claude Code

The MaskCanvas gesture preview is injected by `mask.js` via `innerHTML`, so its shapes never receive the CSS-isolation scope attribute — the scoped `.kg-mask__live-rect` / `.kg-mask__live-stroke` rules silently missed them: - The **rectangle drag rendered as an unstyled SVG rect: solid black at 100% opacity.** - The brush and eraser previews (default `stroke: none`) never rendered at all. The fix routes the three live rules through `::deep` on the scoped live layer (`.kg-mask__live ::deep …`), the same pattern FileUpload already uses for its cross-boundary input. Verified in the browser against the compiled bundle: an injected rect without the descendant rule computes `fill rgb(0,0,0)` / opacity 1 (the reported black box); with it, all three tools compute the translucent accent preview (rect fill accent @ 0.25, stroke accent @ 0.35). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(mask): the live preview reaches its styles through the scope barrier
All checks were successful
CI / build (pull_request) Successful in 9s
CI / test (pull_request) Successful in 15s
2904e3743b
The gesture preview is injected by mask.js via innerHTML, so its shapes never
receive the CSS-isolation scope attribute — the scoped rules silently missed
them. The rectangle drag rendered as an unstyled SVG rect: solid black at full
opacity; the brush and eraser previews (default stroke: none) never rendered
at all. ::deep through the scoped live layer restores the translucent accent
preview for all three tools.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Summary

Summary
Generated on: 08/06/2026 - 10:38:41
Coverage date: 08/06/2026 - 10:38:39
Parser: Cobertura
Assemblies: 1
Classes: 62
Files: 60
Line coverage: 95% (1356 of 1427)
Covered lines: 1356
Uncovered lines: 71
Coverable lines: 1427
Total lines: 3819
Branch coverage: 88.6% (714 of 805)
Covered branches: 714
Total branches: 805
Method coverage: Feature is only available for sponsors

Coverage

Kagaku.UI - 95%
Name Line Branch
Kagaku.UI 95% 88.6%
Kagaku.UI.Badge 100% 100%
Kagaku.UI.Breadcrumb 100%
Kagaku.UI.BreadcrumbItem 100% 100%
Kagaku.UI.Button 100% 100%
Kagaku.UI.Card 100% 100%
Kagaku.UI.Combobox`1 94.2% 85.7%
Kagaku.UI.ConfirmDialog 100%
Kagaku.UI.ContextMenu 92.1% 94.4%
Kagaku.UI.CssClassExtensions 100%
Kagaku.UI.DebouncedSearchField 100% 87.5%
Kagaku.UI.Disclosure 100% 100%
Kagaku.UI.DocumentTitle 76.9% 100%
Kagaku.UI.DragReorderList`1 93.5% 75%
Kagaku.UI.EmptyState 100% 100%
Kagaku.UI.Field 100% 100%
Kagaku.UI.FileUpload 100% 91.6%
Kagaku.UI.FloatingActionButton 100%
Kagaku.UI.Hotkey 92.5% 87.5%
Kagaku.UI.Icon 100% 100%
Kagaku.UI.IconCatalog 100%
Kagaku.UI.InlineAlert 83.3% 75%
Kagaku.UI.InputFieldBase 95.2% 87.5%
Kagaku.UI.LabeledEntriesTable 96.7% 66.6%
Kagaku.UI.LabeledEntry 100%
Kagaku.UI.Lightbox 83.7% 85%
Kagaku.UI.LiveValue 100% 100%
Kagaku.UI.LiveValueInterop 100% 100%
Kagaku.UI.Markdown 100% 50%
Kagaku.UI.MaskCanvas 92.5% 88%
Kagaku.UI.MaskedSecretField 96.2% 83.3%
Kagaku.UI.MaskPoint 100%
Kagaku.UI.MaskPrimitive 100%
Kagaku.UI.Menu 90% 75%
Kagaku.UI.MenuItem 100% 87.5%
Kagaku.UI.Modal 87.1% 90%
Kagaku.UI.NavGroup 100% 100%
Kagaku.UI.NavItem 94.4% 85.7%
Kagaku.UI.NavList 100%
Kagaku.UI.PreviewImage 100% 100%
Kagaku.UI.QuicklinkNav 80.5% 95.8%
Kagaku.UI.QuicklinkSection 100%
Kagaku.UI.RegionPoint 100% 100%
Kagaku.UI.RegionRect 90% 100%
Kagaku.UI.RegionSelector 85% 86.9%
Kagaku.UI.RelativeTime 100% 93.7%
Kagaku.UI.SaveIndicator 100% 100%
Kagaku.UI.Select`1 98.4% 95.8%
Kagaku.UI.SelectableGrid`1 95.5% 89.1%
Kagaku.UI.SelectOption`1 100%
Kagaku.UI.Separator 100%
Kagaku.UI.Slider 100% 91.6%
Kagaku.UI.StatusDot 100%
Kagaku.UI.Tab 100% 100%
Kagaku.UI.Table`1 100% 88.4%
Kagaku.UI.TableColumn`1 100%
Kagaku.UI.Tabs 95.9% 84.4%
Kagaku.UI.TextArea 100%
Kagaku.UI.TextField 100%
Kagaku.UI.ThemeSwitcher 100% 100%
Kagaku.UI.ToggleButtonGroup`1 100% 100%
Kagaku.UI.ToggleOption`1 100%
Kagaku.UI.TypingIndicator 100%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 08/06/2026 - 10:38:41 | | Coverage date: | 08/06/2026 - 10:38:39 | | Parser: | Cobertura | | Assemblies: | 1 | | Classes: | 62 | | Files: | 60 | | **Line coverage:** | 95% (1356 of 1427) | | Covered lines: | 1356 | | Uncovered lines: | 71 | | Coverable lines: | 1427 | | Total lines: | 3819 | | **Branch coverage:** | 88.6% (714 of 805) | | Covered branches: | 714 | | Total branches: | 805 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Kagaku.UI - 95%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagaku.UI**|**95%**|**88.6%**| |Kagaku.UI.Badge|100%|100%| |Kagaku.UI.Breadcrumb|100%|| |Kagaku.UI.BreadcrumbItem|100%|100%| |Kagaku.UI.Button|100%|100%| |Kagaku.UI.Card|100%|100%| |Kagaku.UI.Combobox`1|94.2%|85.7%| |Kagaku.UI.ConfirmDialog|100%|| |Kagaku.UI.ContextMenu|92.1%|94.4%| |Kagaku.UI.CssClassExtensions|100%|| |Kagaku.UI.DebouncedSearchField|100%|87.5%| |Kagaku.UI.Disclosure|100%|100%| |Kagaku.UI.DocumentTitle|76.9%|100%| |Kagaku.UI.DragReorderList`1|93.5%|75%| |Kagaku.UI.EmptyState|100%|100%| |Kagaku.UI.Field|100%|100%| |Kagaku.UI.FileUpload|100%|91.6%| |Kagaku.UI.FloatingActionButton|100%|| |Kagaku.UI.Hotkey|92.5%|87.5%| |Kagaku.UI.Icon|100%|100%| |Kagaku.UI.IconCatalog|100%|| |Kagaku.UI.InlineAlert|83.3%|75%| |Kagaku.UI.InputFieldBase|95.2%|87.5%| |Kagaku.UI.LabeledEntriesTable|96.7%|66.6%| |Kagaku.UI.LabeledEntry|100%|| |Kagaku.UI.Lightbox|83.7%|85%| |Kagaku.UI.LiveValue|100%|100%| |Kagaku.UI.LiveValueInterop|100%|100%| |Kagaku.UI.Markdown|100%|50%| |Kagaku.UI.MaskCanvas|92.5%|88%| |Kagaku.UI.MaskedSecretField|96.2%|83.3%| |Kagaku.UI.MaskPoint|100%|| |Kagaku.UI.MaskPrimitive|100%|| |Kagaku.UI.Menu|90%|75%| |Kagaku.UI.MenuItem|100%|87.5%| |Kagaku.UI.Modal|87.1%|90%| |Kagaku.UI.NavGroup|100%|100%| |Kagaku.UI.NavItem|94.4%|85.7%| |Kagaku.UI.NavList|100%|| |Kagaku.UI.PreviewImage|100%|100%| |Kagaku.UI.QuicklinkNav|80.5%|95.8%| |Kagaku.UI.QuicklinkSection|100%|| |Kagaku.UI.RegionPoint|100%|100%| |Kagaku.UI.RegionRect|90%|100%| |Kagaku.UI.RegionSelector|85%|86.9%| |Kagaku.UI.RelativeTime|100%|93.7%| |Kagaku.UI.SaveIndicator|100%|100%| |Kagaku.UI.Select`1|98.4%|95.8%| |Kagaku.UI.SelectableGrid`1|95.5%|89.1%| |Kagaku.UI.SelectOption`1|100%|| |Kagaku.UI.Separator|100%|| |Kagaku.UI.Slider|100%|91.6%| |Kagaku.UI.StatusDot|100%|| |Kagaku.UI.Tab|100%|100%| |Kagaku.UI.Table`1|100%|88.4%| |Kagaku.UI.TableColumn`1|100%|| |Kagaku.UI.Tabs|95.9%|84.4%| |Kagaku.UI.TextArea|100%|| |Kagaku.UI.TextField|100%|| |Kagaku.UI.ThemeSwitcher|100%|100%| |Kagaku.UI.ToggleButtonGroup`1|100%|100%| |Kagaku.UI.ToggleOption`1|100%|| |Kagaku.UI.TypingIndicator|100%|| </details>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! A CSS isolation scope-barrier bug~ how deliciously subtle! fufu~ The live preview shapes injected by mask.js via innerHTML never receive the b-<scope> attribute Blazor stamps on its own render-tree elements, so the scoped rules silently missed them — a solid black rect and invisible strokes. What a sneaky little bug~ ♡

Verdict: Looks good to me~

The diagnosis is precise, and the fix is the textbook-correct Blazor pattern. Let me show my work~

Root cause confirmed by tracing the full path:

  • MaskCanvas.razor renders <svg class="kg-mask__live" data-mask-live> — Blazor owns this element, so it does carry the scope attribute. ✓
  • mask.js:124 grabs that exact svg via querySelector('[data-mask-live]'), then mask.js:132/:140 write the preview shapes as live.innerHTML = .... innerHTML-injected nodes are outside Blazor's render tree → no scope attribute. ✓
  • Pre-fix .kg-mask__live-rect { … } compiled to .kg-mask__live-rect[b-scope] — the injected rect has no [b-scope], so the rule never matched → default SVG fill: black, opacity: 1. Exactly the reported symptom. ✓
  • Post-fix .kg-mask__live ::deep .kg-mask__live-rect { … } compiles to .kg-mask__live[b-scope] .kg-mask__live-rect — scope attribute rides the parent (which Blazor rendered), descendant selector matches the un-scoped child. Barrier pierced~ ♪

Sibling consistency — fufu, this is the same pattern FileUpload already uses! FileUpload.razor.css:41.kg-fileupload ::deep .kg-fileupload__input with the comment "InputFile renders it as a child component, so its element sits outside this component's CSS scope — ::deep reaches across that boundary." This PR even mirrors that explanatory comment style. Consistency makes Jibril very happy~ ♡

No specificity regression: the compiled selector goes from (0,1,0) to (0,2,0), but there are no competing rules for .kg-mask__live-* classes anywhere in the bundle, so nothing to shadow. The --surface CSS custom property on the erase variant inherits through the DOM as before. All three rules (rect / stroke / stroke--erase) migrated uniformly — and --erase correctly stays a modifier layered on top of live-stroke (mask.js:139 emits both classes), so both rules apply as designed. ✓

What I liked~

  • The comment is a love letter to future readers. It names the why (innerHTML injection), the mechanism (scope attribute never applied), AND the symptom (solid black rect / invisible strokes) — three lines that will save the next person an hour of DevTools confusion. This is exactly the commenting discipline this codebase rewards.
  • Scope is surgical — one file, three selectors, zero behavioral drift in the JS or Razor. The committed overlay (mask.js doesn't touch it) and its .kg-mask__fill rule are correctly left alone.
  • PR body verification is honest and appropriate — browser inspection against the compiled bundle is the correct way to verify a ::deep resolution (computed fill/opacity before vs. after). No fabricated test.

💡 Little idea (non-blocking)~

  1. No test — and that's correct, but worth saying out loud. ::deep scope resolution is inherently unverifiable in bUnit (bUnit renders the Blazor tree, not JS-injected innerHTML children, and doesn't apply CSS isolation attributes against real stylesheets). FileUpload's ::deep input has no CSS-scope test either — same characteristic. The browser verification in the PR body is the right tool. Mentioning this only so nobody reads "no new test" as a gap; it's a property of the layer. ♡

Automated review by Jibril · 2026-08-06
CI/CD: absent for head 2904e37 (no coverage bot comment) · Local checks: not run — CSS-only change, browser-verified per PR body, no executable code path added

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! A CSS isolation scope-barrier bug~ how *deliciously* subtle! fufu~ The live preview shapes injected by `mask.js` via `innerHTML` never receive the `b-<scope>` attribute Blazor stamps on its own render-tree elements, so the scoped rules silently missed them — a solid black rect and invisible strokes. What a sneaky little bug~ ♡ ### Verdict: ✅ Looks good to me~ The diagnosis is *precise*, and the fix is the textbook-correct Blazor pattern. Let me show my work~ **Root cause confirmed by tracing the full path:** - `MaskCanvas.razor` renders `<svg class="kg-mask__live" data-mask-live>` — Blazor owns this element, so it **does** carry the scope attribute. ✓ - `mask.js:124` grabs that exact svg via `querySelector('[data-mask-live]')`, then `mask.js:132`/`:140` write the preview shapes as `live.innerHTML = ...`. innerHTML-injected nodes are **outside** Blazor's render tree → no scope attribute. ✓ - Pre-fix `.kg-mask__live-rect { … }` compiled to `.kg-mask__live-rect[b-scope]` — the injected rect has no `[b-scope]`, so the rule never matched → default SVG `fill: black`, `opacity: 1`. Exactly the reported symptom. ✓ - Post-fix `.kg-mask__live ::deep .kg-mask__live-rect { … }` compiles to `.kg-mask__live[b-scope] .kg-mask__live-rect` — scope attribute rides the *parent* (which Blazor rendered), descendant selector matches the un-scoped child. Barrier pierced~ ♪ **Sibling consistency — fufu, this is the *same pattern* FileUpload already uses!** `FileUpload.razor.css:41` → `.kg-fileupload ::deep .kg-fileupload__input` with the comment *"InputFile renders it as a child component, so its element sits outside this component's CSS scope — ::deep reaches across that boundary."* This PR even mirrors that explanatory comment style. Consistency makes Jibril very happy~ ♡ **No specificity regression:** the compiled selector goes from `(0,1,0)` to `(0,2,0)`, but there are no competing rules for `.kg-mask__live-*` classes anywhere in the bundle, so nothing to shadow. The `--surface` CSS custom property on the erase variant inherits through the DOM as before. All three rules (rect / stroke / stroke--erase) migrated uniformly — and `--erase` correctly stays a modifier layered *on top of* `live-stroke` (mask.js:139 emits both classes), so both rules apply as designed. ✓ #### ✅ What I liked~ - **The comment is a love letter to future readers.** It names the *why* (innerHTML injection), the *mechanism* (scope attribute never applied), AND the *symptom* (solid black rect / invisible strokes) — three lines that will save the next person an hour of DevTools confusion. This is exactly the commenting discipline this codebase rewards. - **Scope is surgical** — one file, three selectors, zero behavioral drift in the JS or Razor. The committed overlay (`mask.js` doesn't touch it) and its `.kg-mask__fill` rule are correctly left alone. - **PR body verification is honest and appropriate** — browser inspection against the compiled bundle is *the* correct way to verify a `::deep` resolution (computed `fill`/`opacity` before vs. after). No fabricated test. #### 💡 Little idea (non-blocking)~ 1. **No test — and that's *correct*, but worth saying out loud.** `::deep` scope resolution is inherently unverifiable in bUnit (bUnit renders the Blazor tree, not JS-injected `innerHTML` children, and doesn't apply CSS isolation attributes against real stylesheets). FileUpload's `::deep` input has no CSS-scope test either — same characteristic. The browser verification in the PR body is the right tool. Mentioning this only so nobody reads "no new test" as a gap; it's a property of the layer. ♡ --- *Automated review by Jibril · 2026-08-06* *CI/CD: absent for head 2904e37 (no coverage bot comment) · Local checks: not run — CSS-only change, browser-verified per PR body, no executable code path added*
bjoern merged commit 017e0b456f into main 2026-08-06 12:50:39 +02:00
bjoern deleted branch fix/mask-live-preview-scope 2026-08-06 12:50:39 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/Kagaku.UI!14
No description provided.