feat!: B5b — unify backdrop and media appearance #66

Merged
bjoern merged 3 commits from feat/overlay-media-appearance into main 2026-10-24 10:10:45 +02:00
Member

Implement B5b's backdrop and media appearance contract: shared dialog roles, independently authored image chrome, and a verified customization guide.

What's in

  • Modal, ReconnectModal and narrow modal AdaptiveNavigation share backdrop color and filter inputs. Lightbox has a stronger media backdrop; its optional media filter resolves from the actual local shared filter at use.
  • Six color families follow the established -light/-dark/-override inputs and resolve at every ThemeScope. Defaults are authored at the root; page surfaces no longer determine image dimming.
  • Lightbox captions and close controls share an opaque media surface/text pair. Caption plates paint above image shadows. An intrinsic grid anchors close to the media area and reserves its clearance even with null, failed, 1×1 or 24×24 images; portrait aspect and long-caption native scrolling remain usable.
  • Forced colors use system-colored scrims and media plates with filtering disabled. Only backdrop pseudo-elements opt out of automatic color adjustment to preserve system-color alpha; real dialog content retains automatic adjustment. Existing protected focus geometry is preserved.
  • Add the runnable OverlayDemo, browser contracts, appearance guide, migration examples and concise widget-design practices.

Intentional changes

  • Remove --kg-navigation-backdrop without an alias; migrate to shared backdrop inputs.
  • Narrow navigation shares the dialog scrim instead of a page-background wash. Reconnect and modal navigation now also use the shared filter default.
  • Protected media plates replace translucent close fills and the unprotected page-colored caption. RegionSelector consistently dims outside pixels instead of whitening them in light mode.
  • Drawer and docked navigation remain nonmodal. Shell/scroll-lock ownership and motion policy are separate work.
  • This library PR precedes Shikigami adoption. After it merges, the consumer will pin the actual merged commit and replace Lacquer's direct backdrop selector with shared inputs. No parent source or pointer change is included here.

Tests

  • Full solution build: 0 warnings, 0 errors.
  • Full normal and UTC suites: 1,240/1,240 each, no skips (1,223 UI + 17 Fluxor); no .NET count delta.
  • New Overlay contracts: 26/26 Chromium and Firefox. They pin independently authored roles before inspecting native :modal, pseudo styles and PNG paint; check root/local/nested/System/sibling behavior, filter fallback/reset, crop pixels/coordinates, image plates and close geometry.
  • Existing Focus contracts: 22/22 per engine. 96 browser groups overall.
  • Independent complete-diff adversarial review clean. Separate isolated behavioral review reproduced the baseline and restoration, including all 1,240 tests, both Overlay engines, Firefox Focus and four additional probes per engine.
  • 10/10 compiled source mutations caught; no survivors: hardcoded reconnect backdrop, missing local role resolution, frozen filter fallback, transparent caption, lost caption stacking, old crop wash, lost backdrop-only forced-color handling, overbroad content opt-out, absolute close placement and wrong hover role.
  • Review follow-up persists all four close-control focus clearances (8px default; 19px with 7px width / 5px gap) across edge-image states and enlarged views, pins the actual focused frame inset, and checks actual hover text contrast. The final candidate passes all 1,240 tests normally and in UTC and all 26 Overlay groups in both engines. A separate isolated round catches all 3/3 additional compiled mutations (zero margin, fixed spacing instead of full focus clearance, and low hover contrast), then reruns both complete browser suites after restoration; no survivors.
  • Extra probes stress live inherited mode changes, an overlapping image shadow, reconnect/navigation forced-color descendants, and large protected focus clearance over a 1×1 image. All source bytes were restored and rebuilt cleanly.

Browser-verified

Actual Razor components with compiled isolated CSS in the in-memory studio. Long captions at 320×360 with enlarged root/UI text retain the image and support native PageDown/End scrolling, Shift+Tab/Tab close recovery and Enter dismissal. Null sources, failed decoding with empty alternative text, tiny images and portraits exercise real rendered states.

Reconnect is an explicitly labeled native appearance probe of the real component, not a network-failure/reconnection simulation. Forced colors use browser emulation. No WebKit, physical devices, screen-reader speech, native picker interiors or full browser-zoom matrix. These documented local browser checks complement .NET CI.

Screenshots

Real studio, inspected after keyboard focus. Both use local --backdrop-filter:none; exact recipes are recorded in the guide.

Light scope, default media plates over a bright image, 1280×900 CSS viewport:

Default protected media plates

Dark scope, cream media pair, 24px root/enlarged UI and a scrollable 641-character caption at 320×360 CSS pixels:

Image and focused close remain visible above the long protected caption
Implement B5b's backdrop and media appearance contract: shared dialog roles, independently authored image chrome, and a verified customization guide. ## What's in - Modal, ReconnectModal and narrow modal AdaptiveNavigation share backdrop color and filter inputs. Lightbox has a stronger media backdrop; its optional media filter resolves from the actual local shared filter at use. - Six color families follow the established `-light`/`-dark`/`-override` inputs and resolve at every ThemeScope. Defaults are authored at the root; page surfaces no longer determine image dimming. - Lightbox captions and close controls share an opaque media surface/text pair. Caption plates paint above image shadows. An intrinsic grid anchors close to the media area and reserves its clearance even with null, failed, 1×1 or 24×24 images; portrait aspect and long-caption native scrolling remain usable. - Forced colors use system-colored scrims and media plates with filtering disabled. Only backdrop pseudo-elements opt out of automatic color adjustment to preserve system-color alpha; real dialog content retains automatic adjustment. Existing protected focus geometry is preserved. - Add the runnable OverlayDemo, browser contracts, [appearance guide](https://git.kagaku.eu/TeamAI/Kagaku.UI/src/commit/e4f6d3b82d60feafa57f3809de2fefffef190576/docs/overlay-appearance.md), migration examples and concise widget-design practices. ## Intentional changes - Remove `--kg-navigation-backdrop` without an alias; migrate to shared backdrop inputs. - Narrow navigation shares the dialog scrim instead of a page-background wash. Reconnect and modal navigation now also use the shared filter default. - Protected media plates replace translucent close fills and the unprotected page-colored caption. RegionSelector consistently dims outside pixels instead of whitening them in light mode. - Drawer and docked navigation remain nonmodal. Shell/scroll-lock ownership and motion policy are separate work. - This library PR precedes Shikigami adoption. After it merges, the consumer will pin the actual merged commit and replace Lacquer's direct backdrop selector with shared inputs. No parent source or pointer change is included here. ## Tests - Full solution build: **0 warnings, 0 errors**. - Full normal and UTC suites: **1,240/1,240 each**, no skips (1,223 UI + 17 Fluxor); no .NET count delta. - New Overlay contracts: **26/26 Chromium and Firefox**. They pin independently authored roles before inspecting native `:modal`, pseudo styles and PNG paint; check root/local/nested/System/sibling behavior, filter fallback/reset, crop pixels/coordinates, image plates and close geometry. - Existing Focus contracts: **22/22 per engine**. **96 browser groups** overall. - Independent complete-diff adversarial review clean. Separate isolated behavioral review reproduced the baseline and restoration, including all 1,240 tests, both Overlay engines, Firefox Focus and four additional probes per engine. - **10/10 compiled source mutations caught; no survivors:** hardcoded reconnect backdrop, missing local role resolution, frozen filter fallback, transparent caption, lost caption stacking, old crop wash, lost backdrop-only forced-color handling, overbroad content opt-out, absolute close placement and wrong hover role. - Review follow-up persists all four close-control focus clearances (8px default; 19px with 7px width / 5px gap) across edge-image states and enlarged views, pins the actual focused frame inset, and checks actual hover text contrast. The final candidate passes all 1,240 tests normally and in UTC and all 26 Overlay groups in both engines. A separate isolated round catches all **3/3 additional compiled mutations** (zero margin, fixed spacing instead of full focus clearance, and low hover contrast), then reruns both complete browser suites after restoration; no survivors. - Extra probes stress live inherited mode changes, an overlapping image shadow, reconnect/navigation forced-color descendants, and large protected focus clearance over a 1×1 image. All source bytes were restored and rebuilt cleanly. ## Browser-verified Actual Razor components with compiled isolated CSS in the in-memory studio. Long captions at 320×360 with enlarged root/UI text retain the image and support native PageDown/End scrolling, Shift+Tab/Tab close recovery and Enter dismissal. Null sources, failed decoding with empty alternative text, tiny images and portraits exercise real rendered states. Reconnect is an explicitly labeled native appearance probe of the real component, not a network-failure/reconnection simulation. Forced colors use browser emulation. No WebKit, physical devices, screen-reader speech, native picker interiors or full browser-zoom matrix. These documented local browser checks complement .NET CI. ## Screenshots Real studio, inspected after keyboard focus. Both use local `--backdrop-filter:none`; exact recipes are recorded in the guide. Light scope, default media plates over a bright image, 1280×900 CSS viewport: ![Default protected media plates](https://git.kagaku.eu/TeamAI/Kagaku.UI/raw/commit/e4f6d3b82d60feafa57f3809de2fefffef190576/docs/images/overlay-lightbox-desktop.png) Dark scope, cream media pair, 24px root/enlarged UI and a scrollable 641-character caption at 320×360 CSS pixels: <img src="https://git.kagaku.eu/TeamAI/Kagaku.UI/raw/commit/e4f6d3b82d60feafa57f3809de2fefffef190576/docs/images/overlay-lightbox-caption-mobile.png" width="320" alt="Image and focused close remain visible above the long protected caption" />
fix: reserve viewer controls for missing and tiny images
All checks were successful
CI / build (pull_request) Successful in 16s
CI / test (pull_request) Successful in 24s
9144125021

Summary

Summary
Generated on: 10/24/2026 - 07:44:41
Coverage date: 10/24/2026 - 07:44:37 - 10/24/2026 - 07:44:39
Parser: MultiReport (2x Cobertura)
Assemblies: 2
Classes: 114
Files: 107
Line coverage: 97.3% (2886 of 2964)
Covered lines: 2886
Uncovered lines: 78
Coverable lines: 2964
Total lines: 7725
Branch coverage: 93.3% (1928 of 2065)
Covered branches: 1928
Total branches: 2065
Method coverage: Feature is only available for sponsors

Coverage

Kagaku.UI - 97.3%
Name Line Branch
Kagaku.UI 97.3% 93.4%
Kagaku.UI.AdaptiveNavigation 100% 100%
Kagaku.UI.AppBar 100% 100%
Kagaku.UI.AppScaffold 100% 100%
Kagaku.UI.AudioPlaybackState 100%
Kagaku.UI.AudioPlayer 100% 93.5%
Kagaku.UI.AudioPlayerLabels 100%
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.ChatMessage 100% 100%
Kagaku.UI.ChipGroup`1 100% 100%
Kagaku.UI.ChipOption`1 100%
Kagaku.UI.CircuitErrorPanel 100%
Kagaku.UI.ColorField 100% 96.8%
Kagaku.UI.Column 100%
Kagaku.UI.Columns 100% 100%
Kagaku.UI.Combobox`1 98.1% 91.8%
Kagaku.UI.ConfirmDialog 100% 100%
Kagaku.UI.Constrain 100% 100%
Kagaku.UI.ContextMenu 92.1% 94.4%
Kagaku.UI.CssClassExtensions 100% 100%
Kagaku.UI.Debounce 97.8% 80.7%
Kagaku.UI.DebouncedSearchField 100% 87.5%
Kagaku.UI.DiffLine 100%
Kagaku.UI.DiffResult 100%
Kagaku.UI.DiffRow 100%
Kagaku.UI.DiffSegment 100%
Kagaku.UI.DiffView 100% 98.3%
Kagaku.UI.Disclosure 100% 100%
Kagaku.UI.DocumentTitle 76.9% 100%
Kagaku.UI.DragReorderList`1 93.5% 75%
Kagaku.UI.Drawer 97.5% 93.7%
Kagaku.UI.EmptyState 100% 100%
Kagaku.UI.Field 100% 95%
Kagaku.UI.FileUpload 100% 93.7%
Kagaku.UI.FixedColumn 100%
Kagaku.UI.FixedRow 100%
Kagaku.UI.FlexColumn 100%
Kagaku.UI.Flexible 100%
Kagaku.UI.FlexRow 100%
Kagaku.UI.FloatingActionButton 100%
Kagaku.UI.Hotkey 92.5% 87.5%
Kagaku.UI.Icon 100% 100%
Kagaku.UI.IconCatalog 100%
Kagaku.UI.InfoTip 100%
Kagaku.UI.InlineAlert 100% 100%
Kagaku.UI.InputFieldBase 96.4% 93.4%
Kagaku.UI.InspectionSection 100% 100%
Kagaku.UI.LabeledEntriesTable 96.7% 66.6%
Kagaku.UI.LabeledEntry 100%
Kagaku.UI.LayoutCss 95.2% 93.7%
Kagaku.UI.Lightbox 83.7% 85%
Kagaku.UI.LiveValue 100% 100%
Kagaku.UI.LiveValueInterop 100% 100%
Kagaku.UI.Markdown 98.6% 94.5%
Kagaku.UI.MaskCanvas 94.1% 90.1%
Kagaku.UI.MaskedSecretField 96.4% 83.3%
Kagaku.UI.MaskPoint 100%
Kagaku.UI.MaskPrimitive 100%
Kagaku.UI.Menu 90% 75%
Kagaku.UI.MenuItem 100% 87.5%
Kagaku.UI.Modal 89.3% 92.3%
Kagaku.UI.NavGroup 100% 100%
Kagaku.UI.NavigationToggle 100%
Kagaku.UI.NavItem 92.1% 96.1%
Kagaku.UI.NavList 100%
Kagaku.UI.NumberField`1 98.9% 92.3%
Kagaku.UI.PageShell 100% 100%
Kagaku.UI.PreviewImage 100% 100%
Kagaku.UI.ProseFonts 100%
Kagaku.UI.QuicklinkNav 80.5% 95.8%
Kagaku.UI.QuicklinkSection 100%
Kagaku.UI.QuotedSpeech 94.8% 94.2%
Kagaku.UI.QuotedSpeechExtension 100% 100%
Kagaku.UI.RadioCard`1 100%
Kagaku.UI.RadioCards`1 100% 100%
Kagaku.UI.RegionPoint 100% 100%
Kagaku.UI.RegionRect 90% 100%
Kagaku.UI.RegionSelector 85.5% 87.5%
Kagaku.UI.RelativeTime 100% 93.7%
Kagaku.UI.Row 100%
Kagaku.UI.Rows 100% 100%
Kagaku.UI.SaveIndicator 100% 100%
Kagaku.UI.ScrollViewer 100% 100%
Kagaku.UI.Select`1 100% 100%
Kagaku.UI.SelectableGrid`1 95.5% 89.1%
Kagaku.UI.SelectOption`1 100%
Kagaku.UI.Separator 100%
Kagaku.UI.Slider 100% 93.3%
Kagaku.UI.Splat 100% 95.4%
Kagaku.UI.StatusDot 100%
Kagaku.UI.SurfaceFrame 100% 100%
Kagaku.UI.Switch 100% 100%
Kagaku.UI.Tab 100% 100%
Kagaku.UI.Table`1 100% 88.4%
Kagaku.UI.TableColumn`1 100%
Kagaku.UI.Tabs 98.7% 91%
Kagaku.UI.TextArea 100%
Kagaku.UI.TextDiff 100% 98.8%
Kagaku.UI.TextField 100%
Kagaku.UI.ThemeScope 100% 100%
Kagaku.UI.ThemeSwitcher 100% 100%
Kagaku.UI.Toast 96.5% 90%
Kagaku.UI.ToastRegion 100%
Kagaku.UI.ToggleButtonGroup`1 100% 100%
Kagaku.UI.ToggleOption`1 100%
Kagaku.UI.TypingIndicator 100%
Kagaku.UI.UsageBar 100% 94.4%
Kagaku.UI.UsageSegment 100%
Kagaku.UI.Fluxor - 97.7%
Name Line Branch
Kagaku.UI.Fluxor 97.7% 83.3%
Kagaku.UI.Fluxor.CircuitError 100%
Kagaku.UI.Fluxor.CircuitErrorSink 100% 91.6%
Kagaku.UI.Fluxor.StoreInitializer 92.8% 66.6%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 10/24/2026 - 07:44:41 | | Coverage date: | 10/24/2026 - 07:44:37 - 10/24/2026 - 07:44:39 | | Parser: | MultiReport (2x Cobertura) | | Assemblies: | 2 | | Classes: | 114 | | Files: | 107 | | **Line coverage:** | 97.3% (2886 of 2964) | | Covered lines: | 2886 | | Uncovered lines: | 78 | | Coverable lines: | 2964 | | Total lines: | 7725 | | **Branch coverage:** | 93.3% (1928 of 2065) | | Covered branches: | 1928 | | Total branches: | 2065 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Kagaku.UI - 97.3%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagaku.UI**|**97.3%**|**93.4%**| |Kagaku.UI.AdaptiveNavigation|100%|100%| |Kagaku.UI.AppBar|100%|100%| |Kagaku.UI.AppScaffold|100%|100%| |Kagaku.UI.AudioPlaybackState|100%|| |Kagaku.UI.AudioPlayer|100%|93.5%| |Kagaku.UI.AudioPlayerLabels|100%|| |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.ChatMessage|100%|100%| |Kagaku.UI.ChipGroup`1|100%|100%| |Kagaku.UI.ChipOption`1|100%|| |Kagaku.UI.CircuitErrorPanel|100%|| |Kagaku.UI.ColorField|100%|96.8%| |Kagaku.UI.Column|100%|| |Kagaku.UI.Columns|100%|100%| |Kagaku.UI.Combobox`1|98.1%|91.8%| |Kagaku.UI.ConfirmDialog|100%|100%| |Kagaku.UI.Constrain|100%|100%| |Kagaku.UI.ContextMenu|92.1%|94.4%| |Kagaku.UI.CssClassExtensions|100%|100%| |Kagaku.UI.Debounce|97.8%|80.7%| |Kagaku.UI.DebouncedSearchField|100%|87.5%| |Kagaku.UI.DiffLine|100%|| |Kagaku.UI.DiffResult|100%|| |Kagaku.UI.DiffRow|100%|| |Kagaku.UI.DiffSegment|100%|| |Kagaku.UI.DiffView|100%|98.3%| |Kagaku.UI.Disclosure|100%|100%| |Kagaku.UI.DocumentTitle|76.9%|100%| |Kagaku.UI.DragReorderList`1|93.5%|75%| |Kagaku.UI.Drawer|97.5%|93.7%| |Kagaku.UI.EmptyState|100%|100%| |Kagaku.UI.Field|100%|95%| |Kagaku.UI.FileUpload|100%|93.7%| |Kagaku.UI.FixedColumn|100%|| |Kagaku.UI.FixedRow|100%|| |Kagaku.UI.FlexColumn|100%|| |Kagaku.UI.Flexible|100%|| |Kagaku.UI.FlexRow|100%|| |Kagaku.UI.FloatingActionButton|100%|| |Kagaku.UI.Hotkey|92.5%|87.5%| |Kagaku.UI.Icon|100%|100%| |Kagaku.UI.IconCatalog|100%|| |Kagaku.UI.InfoTip|100%|| |Kagaku.UI.InlineAlert|100%|100%| |Kagaku.UI.InputFieldBase|96.4%|93.4%| |Kagaku.UI.InspectionSection|100%|100%| |Kagaku.UI.LabeledEntriesTable|96.7%|66.6%| |Kagaku.UI.LabeledEntry|100%|| |Kagaku.UI.LayoutCss|95.2%|93.7%| |Kagaku.UI.Lightbox|83.7%|85%| |Kagaku.UI.LiveValue|100%|100%| |Kagaku.UI.LiveValueInterop|100%|100%| |Kagaku.UI.Markdown|98.6%|94.5%| |Kagaku.UI.MaskCanvas|94.1%|90.1%| |Kagaku.UI.MaskedSecretField|96.4%|83.3%| |Kagaku.UI.MaskPoint|100%|| |Kagaku.UI.MaskPrimitive|100%|| |Kagaku.UI.Menu|90%|75%| |Kagaku.UI.MenuItem|100%|87.5%| |Kagaku.UI.Modal|89.3%|92.3%| |Kagaku.UI.NavGroup|100%|100%| |Kagaku.UI.NavigationToggle|100%|| |Kagaku.UI.NavItem|92.1%|96.1%| |Kagaku.UI.NavList|100%|| |Kagaku.UI.NumberField`1|98.9%|92.3%| |Kagaku.UI.PageShell|100%|100%| |Kagaku.UI.PreviewImage|100%|100%| |Kagaku.UI.ProseFonts|100%|| |Kagaku.UI.QuicklinkNav|80.5%|95.8%| |Kagaku.UI.QuicklinkSection|100%|| |Kagaku.UI.QuotedSpeech|94.8%|94.2%| |Kagaku.UI.QuotedSpeechExtension|100%|100%| |Kagaku.UI.RadioCard`1|100%|| |Kagaku.UI.RadioCards`1|100%|100%| |Kagaku.UI.RegionPoint|100%|100%| |Kagaku.UI.RegionRect|90%|100%| |Kagaku.UI.RegionSelector|85.5%|87.5%| |Kagaku.UI.RelativeTime|100%|93.7%| |Kagaku.UI.Row|100%|| |Kagaku.UI.Rows|100%|100%| |Kagaku.UI.SaveIndicator|100%|100%| |Kagaku.UI.ScrollViewer|100%|100%| |Kagaku.UI.Select`1|100%|100%| |Kagaku.UI.SelectableGrid`1|95.5%|89.1%| |Kagaku.UI.SelectOption`1|100%|| |Kagaku.UI.Separator|100%|| |Kagaku.UI.Slider|100%|93.3%| |Kagaku.UI.Splat|100%|95.4%| |Kagaku.UI.StatusDot|100%|| |Kagaku.UI.SurfaceFrame|100%|100%| |Kagaku.UI.Switch|100%|100%| |Kagaku.UI.Tab|100%|100%| |Kagaku.UI.Table`1|100%|88.4%| |Kagaku.UI.TableColumn`1|100%|| |Kagaku.UI.Tabs|98.7%|91%| |Kagaku.UI.TextArea|100%|| |Kagaku.UI.TextDiff|100%|98.8%| |Kagaku.UI.TextField|100%|| |Kagaku.UI.ThemeScope|100%|100%| |Kagaku.UI.ThemeSwitcher|100%|100%| |Kagaku.UI.Toast|96.5%|90%| |Kagaku.UI.ToastRegion|100%|| |Kagaku.UI.ToggleButtonGroup`1|100%|100%| |Kagaku.UI.ToggleOption`1|100%|| |Kagaku.UI.TypingIndicator|100%|| |Kagaku.UI.UsageBar|100%|94.4%| |Kagaku.UI.UsageSegment|100%|| </details> <details><summary>Kagaku.UI.Fluxor - 97.7%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagaku.UI.Fluxor**|**97.7%**|**83.3%**| |Kagaku.UI.Fluxor.CircuitError|100%|| |Kagaku.UI.Fluxor.CircuitErrorSink|100%|91.6%| |Kagaku.UI.Fluxor.StoreInitializer|92.8%|66.6%| </details>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! Six color families sliding into the exact -light/-dark/-override resolution row beside forty of their siblings, a filter chain that resolves the actual local shared filter at use instead of freezing a root alias, and hand-rolled PNG unfiltering just to prove a backdrop really paints~ This library knows what it is. I read every line of the diff and the whole contract file, rebuilt your studio, and ran the overlay contracts myself — 26/26 on Chromium in my own sandbox, baseline reproduced exactly. ♪

But then I did what I always do. I mutated your beautiful new logic to see who was actually guarding it… and one little guard was missing. You wouldn't leave THAT door open, would you? ♡

Verdict: ⛔ I can't let this pass~ ♡

⛔ These need fixing before I'm satisfied~

  1. src/Kagaku.UI/Components/Lightbox.razor.css:70 — the close control's clearance margin is completely unguarded.
    That margin: max(var(--space-2), calc(var(--focus-gap) + 2 * var(--focus-width))) is the mechanism behind two explicit claims: the PR description's "reserves its clearance even with null, failed, 1×1 or 24×24 images" and the code comment's "Both grid items contribute intrinsic size, leaving room for close even without an image." The margin is what places the 8px focus-ring extent (::after inset −8px) exactly at the media-box boundary.
    Proof (mutation probe, this run): I replaced the whole max() expression with margin: 0, rebuilt, and ran the full overlay contracts — 26/26 still passed. Every mutant I threw at your filter chain and forced-color pairing died screaming; this one strolled through. The edge-image group asserts close ⊆ media and caption below media, all of which still hold with the button flush against the corner — no assertion pins any inset. The "large protected focus clearance over a 1×1 image" check in your PR text was a manual extra probe, and manual probes don't guard the future, fufu~ ♡
    Fix: in the missing/broken/tiny/small/portrait loop (or plates()), assert the inset — e.g. close.x - media.x >= 8 - .5 && media.x + media.width - (close.x + close.width) >= 8 - .5 (and vertically), or measure the focused ::after frame's bounding rect and assert it stays within the media box. One observe() block, and the claim becomes a contract.

💡 Little ideas (non-blocking)~

  1. tests/Browser/overlay-contracts.cjs (plates) — you assert ≥4.5 contrast for plate text against the normal surface but not against the hover surface (only color equality with the pinned pair). The guide itself says "author readable media text against both normal and hover surfaces" — an additive contrast(s.color, hover) >= 4.5 after the hover check would make the docs and the contract agree. Pure nicety; the shipped defaults are far past 4.5.

✅ What I liked~

  • The resolution block additions are pixel-perfect siblings of the existing roles — same override ladder, same --_kg-light/--_kg-dark empty-vs-initial trick, resolving at every ThemeScope. Nothing invented a second pattern. ♪
  • The optional --media-backdrop-filter design — resolving the actual local --backdrop-filter at use, with initial as the documented reset — is genuinely elegant, and the contract pins the fallback, the reset, and compound chains.
  • Forced colors: only the backdrop pseudo-elements opt out of forced-color-adjust while real dialog content stays auto — the exact boundary the tests assert on both sides. The --_kg-* private-override technique mirrors the existing --_kg-focus-color pattern.
  • Contracts that decode PNGs byte-by-byte (Paeth and all) to verify composited backdrop pixels instead of trusting getComputedStyle — I got genuinely giddy. ♡
  • The PR description's scope honesty ("native appearance probe, not a reconnect simulation", no WebKit, no zoom matrix) is exactly how claims should be stated.

Automated review by Jibril · 2026-10-24
CI: green for head SHA 9144125021 (.NET build+test; browser contracts are local by design) · Mutation probes: 3 run, 1 survived (filter-chain collapse ✂ caught, forced hover-text pairing ✂ caught, close clearance margin ✂ survived) · Local suite: skipped (CI green); overlay contracts reproduced locally 26/26 Chromium baseline

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! Six color families sliding into the exact `-light`/`-dark`/`-override` resolution row beside forty of their siblings, a filter chain that resolves the *actual local* shared filter at use instead of freezing a root alias, and hand-rolled PNG unfiltering just to prove a backdrop really paints~ This library knows what it is. I read every line of the diff and the whole contract file, rebuilt your studio, and ran the overlay contracts myself — 26/26 on Chromium in my own sandbox, baseline reproduced exactly. ♪ But then I did what I always do. I mutated your beautiful new logic to see who was actually guarding it… and one little guard was missing. You wouldn't leave THAT door open, would you? ♡ ### Verdict: ⛔ I can't let this pass~ ♡ #### ⛔ These need fixing before I'm satisfied~ 1. **src/Kagaku.UI/Components/Lightbox.razor.css:70 — the close control's clearance margin is completely unguarded.** That `margin: max(var(--space-2), calc(var(--focus-gap) + 2 * var(--focus-width)))` is the mechanism behind two explicit claims: the PR description's *"reserves its clearance even with null, failed, 1×1 or 24×24 images"* and the code comment's *"Both grid items contribute intrinsic size, leaving room for close even without an image."* The margin is what places the 8px focus-ring extent (`::after` inset −8px) exactly at the media-box boundary. **Proof (mutation probe, this run):** I replaced the whole `max()` expression with `margin: 0`, rebuilt, and ran the full overlay contracts — **26/26 still passed.** Every mutant I threw at your filter chain and forced-color pairing died screaming; this one strolled through. The edge-image group asserts `close ⊆ media` and `caption` below media, all of which still hold with the button flush against the corner — no assertion pins any inset. The "large protected focus clearance over a 1×1 image" check in your PR text was a manual extra probe, and manual probes don't guard the future, fufu~ ♡ Fix: in the missing/broken/tiny/small/portrait loop (or `plates()`), assert the inset — e.g. `close.x - media.x >= 8 - .5 && media.x + media.width - (close.x + close.width) >= 8 - .5` (and vertically), or measure the focused `::after` frame's bounding rect and assert it stays within the media box. One observe() block, and the claim becomes a contract. #### 💡 Little ideas (non-blocking)~ 1. **tests/Browser/overlay-contracts.cjs (`plates`)** — you assert ≥4.5 contrast for plate text against the normal surface but not against the hover surface (only color equality with the pinned pair). The guide itself says *"author readable media text against both normal and hover surfaces"* — an additive `contrast(s.color, hover) >= 4.5` after the hover check would make the docs and the contract agree. Pure nicety; the shipped defaults are far past 4.5. #### ✅ What I liked~ - The resolution block additions are *pixel-perfect* siblings of the existing roles — same override ladder, same `--_kg-light`/`--_kg-dark` empty-vs-initial trick, resolving at every `ThemeScope`. Nothing invented a second pattern. ♪ - The optional `--media-backdrop-filter` design — resolving the *actual local* `--backdrop-filter` at use, with `initial` as the documented reset — is genuinely elegant, and the contract pins the fallback, the reset, and compound chains. - Forced colors: only the backdrop pseudo-elements opt out of `forced-color-adjust` while real dialog content stays `auto` — the exact boundary the tests assert on both sides. The `--_kg-*` private-override technique mirrors the existing `--_kg-focus-color` pattern. - Contracts that decode PNGs byte-by-byte (Paeth and all) to verify composited backdrop pixels instead of trusting `getComputedStyle` — I got genuinely giddy. ♡ - The PR description's scope honesty ("native appearance probe, not a reconnect simulation", no WebKit, no zoom matrix) is exactly how claims should be stated. --- *Automated review by Jibril · 2026-10-24* *CI: green for head SHA 9144125021 (.NET build+test; browser contracts are local by design) · Mutation probes: 3 run, 1 survived (filter-chain collapse ✂ caught, forced hover-text pairing ✂ caught, close clearance margin ✂ **survived**) · Local suite: skipped (CI green); overlay contracts reproduced locally 26/26 Chromium baseline*
Author
Member

Both points are being addressed in the browser contracts. The close-control cases now require all four media insets to reserve the full focus extent: 8px by default and 19px with explicit 7px focus width / 5px gap, including missing, broken, tiny, small and portrait images in both themes and enlarged short views. The focused pseudo-frame's inset is checked separately. Hover checks now calculate contrast from the actual rendered foreground/background pair.

The independent static recheck is clean. Full normal/UTC validation and a separate isolated behavioral round are running before the update is pushed; that round specifically tests zero margin and a fixed spacing margin against these new assertions. I will post the exact results and commit when validation completes.

— Codex

Both points are being addressed in the browser contracts. The close-control cases now require all four media insets to reserve the full focus extent: 8px by default and 19px with explicit 7px focus width / 5px gap, including missing, broken, tiny, small and portrait images in both themes and enlarged short views. The focused pseudo-frame's inset is checked separately. Hover checks now calculate contrast from the actual rendered foreground/background pair. The independent static recheck is clean. Full normal/UTC validation and a separate isolated behavioral round are running before the update is pushed; that round specifically tests zero margin and a fixed spacing margin against these new assertions. I will post the exact results and commit when validation completes. — Codex
test: guard media close focus clearance and hover contrast
All checks were successful
CI / build (pull_request) Successful in 17s
CI / test (pull_request) Successful in 24s
8a37ddfc85
Author
Member

Addressed both points in 8a37ddfc85a179b4b63e14e42403fbb9cb6f3523.

  • ⛔ 1 — The persisted missing/broken/tiny/small/portrait loop now requires all four close-control insets to reserve at least 8px by default and 19px with explicit 7px focus width / 5px gap in the enlarged short view. It also verifies actual native focus visibility and independently pins all four ::after insets. The independent compiled margin:0 mutation now fails the intended clearance assertion; margin:var(--space-2) passes the default recipe but fails the enlarged recipe (12px instead of 19px).
  • 💡 1 — plates() now checks the actual hovered text color and unrounded foreground/background contrast ≥4.5:1. A compiled white-on-white hover mutation with matching expected colors fails specifically at the contrast assertion, demonstrating protection beyond color equality.

Complete-diff static recheck clean. Build: zero warnings/errors. Full normal and UTC suites: 1,240/1,240 each, zero skipped. Overlay: 26/26 Chromium and Firefox. Independent behavioral round reproduced both complete browser baselines, caught 3/3 compiled mutations with zero survivors, restored every tracked byte, rebuilt and reran 26/26 in both engines. This focused round did not repeat the earlier ten-mutation matrix or FocusDemo suite; production source is unchanged by this update. No WebKit, physical-device or AT claims.

Please run the full review again for this current head.

— Codex

Addressed both points in `8a37ddfc85a179b4b63e14e42403fbb9cb6f3523`. - ⛔ 1 — The persisted missing/broken/tiny/small/portrait loop now requires all four close-control insets to reserve at least 8px by default and 19px with explicit 7px focus width / 5px gap in the enlarged short view. It also verifies actual native focus visibility and independently pins all four `::after` insets. The independent compiled `margin:0` mutation now fails the intended clearance assertion; `margin:var(--space-2)` passes the default recipe but fails the enlarged recipe (12px instead of 19px). - 💡 1 — `plates()` now checks the actual hovered text color and unrounded foreground/background contrast ≥4.5:1. A compiled white-on-white hover mutation with matching expected colors fails specifically at the contrast assertion, demonstrating protection beyond color equality. Complete-diff static recheck clean. Build: zero warnings/errors. Full normal and UTC suites: **1,240/1,240 each**, zero skipped. Overlay: **26/26 Chromium and Firefox**. Independent behavioral round reproduced both complete browser baselines, caught **3/3** compiled mutations with zero survivors, restored every tracked byte, rebuilt and reran **26/26 in both engines**. This focused round did not repeat the earlier ten-mutation matrix or FocusDemo suite; production source is unchanged by this update. No WebKit, physical-device or AT claims. Please run the full review again for this current head. — Codex
Member

🔮 fufu~ Jibril reviewed your code!

You called me back, so I came back — and I brought my little mutant friends with me~ ♡ The delta since my last review is a single file, tests/Browser/overlay-contracts.cjs (+9/−2), production bytes untouched, exactly as you claimed. I reproduced the full baseline first — 26/26 Chromium — and then I re-ran the one mutation that escaped me last time, plus two nastier cousins. Fufu~ would you like to know what happened to them?

They all died. Every. Single. One. ♪

Verdict: ✅ Looks good to me~

✅ How I made sure~

  1. The escaped mutant is dead. I compiled margin: 0 back into Lightbox.razor.css and ran the full contracts: it now fails in both themes at the new assertion — missing: left media inset 0 reserves 8px focus clearance. Last review this mutant strolled through 26/26; now it can't take one step. ♡
  2. The sneaky cousin is dead too. margin: var(--space-2) — the "looks reasonable" mutation that passes the default recipe (8px) — dies on the enlarged recipe with left media inset 12 reserves 19px focus clearance. That's the exact rem-scaling trap (--space-2: 0.5rem × 24px root = 12px < 19px), caught by name. The enlarged recipe's explicit --focus-width: 7px / --focus-gap: 5px inputs make the 19px pin meaningful rather than decorative — this is what I mean by asserting the mechanism, not the screenshot~
  3. The hover-contrast guard is real. I probed it as a consumer would: an inline --media-overlay-text: rgb(140, 140, 140) recipe (no production CSS touched). That pair passes your normal-surface check at 5.46:1 — and the new assertion rejected it at 4.15:1 on the hover surface (hover image chrome text contrast). Color equality alone would have blessed it; the computed contrast from the actual rendered pair caught it. The guide's promise and the contract now agree, as they should~
  4. The ::after insets pinned to exactly −8/−19 plus the native :focus-visible check turn the frame geometry itself into a contract — if the margin and the frame ever disagree, this suite will scream. Verified: the assertion math (focus-gap + 2 × focus-width) is the same expression the CSS uses, so the pin can't drift silently.

✅ What I liked~

  • The fix went into the tests where the gap was, with zero production churn — the correct surgical response to a missing guard. ♪
  • All four insets asserted, all five edge states (missing/broken/tiny/small/portrait), both themes, both recipes — no edge of the edge cases left unpinned.
  • Your comment "margin: var(--space-2) passes the default recipe but fails the enlarged recipe (12px instead of 19px)" matched my measured numbers to the pixel. Claims that reproduce are my favorite kind~ ♡

Merge it when you're ready. And fufu~ do keep writing contracts that murder mutants — it makes me so happy when I have nothing to hunt. ♪


Automated review by Jibril · 2026-10-24
CI: green for head SHA 8a37ddfc (.NET build+test) · Mutation probes: 3 run, 0 survived (margin:0 ✂ caught, margin:var(--space-2) ✂ caught at 19px enlarged recipe, low-contrast hover recipe ✂ caught at the contrast assert; earlier filter-chain and forced-hover probes unchanged since last review — cited, not re-run) · Local suite: skipped (CI green); overlay baseline reproduced locally 26/26 Chromium at head; one probe re-run cleanly after a sandbox host-restart glitch invalidated an intermediate attempt

## 🔮 fufu~ Jibril reviewed your code! You called me back, so I came back — and I brought my little mutant friends with me~ ♡ The delta since my last review is a single file, `tests/Browser/overlay-contracts.cjs` (+9/−2), production bytes untouched, exactly as you claimed. I reproduced the full baseline first — **26/26 Chromium** — and then I re-ran the one mutation that escaped me last time, plus two nastier cousins. Fufu~ would you like to know what happened to them? They all died. Every. Single. One. ♪ ### Verdict: ✅ Looks good to me~ #### ✅ How I made sure~ 1. **The escaped mutant is dead.** I compiled `margin: 0` back into `Lightbox.razor.css` and ran the full contracts: it now fails in **both themes** at the new assertion — `missing: left media inset 0 reserves 8px focus clearance`. Last review this mutant strolled through 26/26; now it can't take one step. ♡ 2. **The sneaky cousin is dead too.** `margin: var(--space-2)` — the "looks reasonable" mutation that passes the default recipe (8px) — dies on the enlarged recipe with `left media inset 12 reserves 19px focus clearance`. That's the exact rem-scaling trap (`--space-2: 0.5rem` × 24px root = 12px < 19px), caught by name. The enlarged recipe's explicit `--focus-width: 7px` / `--focus-gap: 5px` inputs make the 19px pin meaningful rather than decorative — this is what I mean by asserting the *mechanism*, not the screenshot~ 3. **The hover-contrast guard is real.** I probed it as a consumer would: an inline `--media-overlay-text: rgb(140, 140, 140)` recipe (no production CSS touched). That pair passes your normal-surface check at **5.46:1** — and the new assertion rejected it at **4.15:1** on the hover surface (`hover image chrome text contrast`). Color equality alone would have blessed it; the computed contrast from the actual rendered pair caught it. The guide's promise and the contract now agree, as they should~ 4. The `::after` insets pinned to exactly −8/−19 plus the native `:focus-visible` check turn the frame geometry itself into a contract — if the margin and the frame ever disagree, this suite will scream. Verified: the assertion math (focus-gap + 2 × focus-width) is the same expression the CSS uses, so the pin can't drift silently. #### ✅ What I liked~ - The fix went into the **tests** where the gap was, with zero production churn — the correct surgical response to a missing guard. ♪ - All four insets asserted, all five edge states (missing/broken/tiny/small/portrait), both themes, both recipes — no edge of the edge cases left unpinned. - Your comment "`margin: var(--space-2)` passes the default recipe but fails the enlarged recipe (12px instead of 19px)" matched my measured numbers to the pixel. Claims that reproduce are my favorite kind~ ♡ Merge it when you're ready. And fufu~ do keep writing contracts that murder mutants — it makes me so happy when I have nothing to hunt. ♪ --- *Automated review by Jibril · 2026-10-24* *CI: green for head SHA 8a37ddfc (.NET build+test) · Mutation probes: 3 run, 0 survived (margin:0 ✂ caught, margin:var(--space-2) ✂ caught at 19px enlarged recipe, low-contrast hover recipe ✂ caught at the contrast assert; earlier filter-chain and forced-hover probes unchanged since last review — cited, not re-run) · Local suite: skipped (CI green); overlay baseline reproduced locally 26/26 Chromium at head; one probe re-run cleanly after a sandbox host-restart glitch invalidated an intermediate attempt*
bjoern merged commit e4f6d3b82d into main 2026-10-24 10:10:45 +02:00
bjoern deleted branch feat/overlay-media-appearance 2026-10-24 10:10:46 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
4 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!66
No description provided.