feat!: B5a — unify keyboard focus customization #64

Merged
bjoern merged 3 commits from feat/focus-contract into main 2026-10-24 07:28:35 +02:00
Member

B5a gives consumers one keyboard-focus contract across the library, including clipped and image-backed targets. It removes composite --focus-ring shadows in favor of shared color, width and gap inputs, with a separate contrasting color for image protection.

What's in

  • Optional colors resolve locally at use; explicit overrides inherit across theme boundaries, and initial restores the local fallback.
  • Outline-based focus preserves elevation, selected-state shadows, validation borders and corner shape. UsageBar and InspectionSection state marks remain independent.
  • Deliberate inset placement, a FileUpload focus proxy, protected RegionSelector/Lightbox frames and system colors in forced-color mode.
  • Consumer guide, breaking migration notes, widget practices and a runnable real-component focus studio.

Validation

  • Full solution build: zero warnings/errors. 1,217/1,217 tests in normal and UTC runs, none skipped. Test count unchanged; existing rendered/CSS assertions migrated.
  • New focus contracts: 22/22 Chromium and 22/22 Firefox, including real keyboard interactions and lossless screenshot-pixel checks.
  • Theme contracts: 31/31 per engine; material contracts: 12/12 per engine.
  • Independent full-diff adversarial review is clean. Independent behavioral review found two browser-test gaps; both were fixed and the final exact-head recheck is clean.
  • Mutation review caught nine initial production faults. After the test fixes, the former transparent-backing survivor and three additional independent image-role faults were caught in Light/Dark; explicit custom and forced-color controls still passed. No remaining survivors among the exercised mutations. The follow-up retained the nine earlier kills rather than rerunning unchanged cases.
  • Five fresh closure invocations per engine passed: 60 close/reopen boundaries, including wide-to-zero-gap transitions.

Browser coverage includes default/local/nested/System inputs, explicit overrides and clearing, state preservation, clipping, bright/dark images, forced colors, larger text and manual scrolling. Native picker/media interiors and assistive-technology speech were not tested; no broad accessibility-conformance claim is made. Backdrop/media appearance and shell policy remain separate work.

Shikigami adoption follows the library merge and will pin the actual merged revision.

Screenshots

Actual FocusDemo, desktop 1280×800: focus remains distinct from validation.

Focus and validation

Default protected focus above an active crop in a dark scope:

Focus over an image

FileUpload proxy, mobile CSS viewport 390×844:

Visible keyboard focus on the file upload label
B5a gives consumers one keyboard-focus contract across the library, including clipped and image-backed targets. It removes composite `--focus-ring` shadows in favor of shared color, width and gap inputs, with a separate contrasting color for image protection. ## What's in - Optional colors resolve locally at use; explicit overrides inherit across theme boundaries, and `initial` restores the local fallback. - Outline-based focus preserves elevation, selected-state shadows, validation borders and corner shape. UsageBar and InspectionSection state marks remain independent. - Deliberate inset placement, a FileUpload focus proxy, protected RegionSelector/Lightbox frames and system colors in forced-color mode. - Consumer guide, breaking migration notes, widget practices and a runnable real-component focus studio. ## Validation - Full solution build: zero warnings/errors. **1,217/1,217 tests** in normal and UTC runs, none skipped. Test count unchanged; existing rendered/CSS assertions migrated. - New focus contracts: **22/22 Chromium and 22/22 Firefox**, including real keyboard interactions and lossless screenshot-pixel checks. - Theme contracts: **31/31 per engine**; material contracts: **12/12 per engine**. - Independent full-diff adversarial review is clean. Independent behavioral review found two browser-test gaps; both were fixed and the final exact-head recheck is clean. - Mutation review caught nine initial production faults. After the test fixes, the former transparent-backing survivor and three additional independent image-role faults were caught in Light/Dark; explicit custom and forced-color controls still passed. No remaining survivors among the exercised mutations. The follow-up retained the nine earlier kills rather than rerunning unchanged cases. - Five fresh closure invocations per engine passed: **60 close/reopen boundaries**, including wide-to-zero-gap transitions. Browser coverage includes default/local/nested/System inputs, explicit overrides and clearing, state preservation, clipping, bright/dark images, forced colors, larger text and manual scrolling. Native picker/media interiors and assistive-technology speech were not tested; no broad accessibility-conformance claim is made. Backdrop/media appearance and shell policy remain separate work. Shikigami adoption follows the library merge and will pin the actual merged revision. ## Screenshots Actual FocusDemo, desktop 1280×800: focus remains distinct from validation. ![Focus and validation](https://git.kagaku.eu/TeamAI/Kagaku.UI/raw/commit/deaab9cb140a0e09435905f7989a56ab14a3e39d/docs/images/focus-validation-desktop.png) Default protected focus above an active crop in a dark scope: ![Focus over an image](https://git.kagaku.eu/TeamAI/Kagaku.UI/raw/commit/deaab9cb140a0e09435905f7989a56ab14a3e39d/docs/images/focus-image-desktop.png) FileUpload proxy, mobile CSS viewport 390×844: <img src="https://git.kagaku.eu/TeamAI/Kagaku.UI/raw/commit/deaab9cb140a0e09435905f7989a56ab14a3e39d/docs/images/focus-upload-mobile.png" width="300" alt="Visible keyboard focus on the file upload label">

Summary

Summary
Generated on: 10/24/2026 - 00:02:34
Coverage date: 10/24/2026 - 00:02:30 - 10/24/2026 - 00:02:32
Parser: MultiReport (2x Cobertura)
Assemblies: 2
Classes: 113
Files: 106
Line coverage: 97.2% (2866 of 2946)
Covered lines: 2866
Uncovered lines: 80
Coverable lines: 2946
Total lines: 7692
Branch coverage: 93.1% (1894 of 2033)
Covered branches: 1894
Total branches: 2033
Method coverage: Feature is only available for sponsors

Coverage

Kagaku.UI - 97.2%
Name Line Branch
Kagaku.UI 97.2% 93.2%
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 88.8% 90.9%
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.Switch 100% 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.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 - 00:02:34 | | Coverage date: | 10/24/2026 - 00:02:30 - 10/24/2026 - 00:02:32 | | Parser: | MultiReport (2x Cobertura) | | Assemblies: | 2 | | Classes: | 113 | | Files: | 106 | | **Line coverage:** | 97.2% (2866 of 2946) | | Covered lines: | 2866 | | Uncovered lines: | 80 | | Coverable lines: | 2946 | | Total lines: | 7692 | | **Branch coverage:** | 93.1% (1894 of 2033) | | Covered branches: | 1894 | | Total branches: | 2033 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Kagaku.UI - 97.2%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagaku.UI**|**97.2%**|**93.2%**| |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|88.8%|90.9%| |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.Switch|100%|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.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! One :where(:focus-visible) to replace twenty per-widget shadow hacks, a two-color frame that survives arbitrary image pixels, and a PNG decoder written by hand inside a browser test so you can prove the focus band actually paints~ ♡ I read every one of the 47 changed files twice, hunted for --focus-ring corpses, compared every sibling placement — and then I did what a mere reader cannot. I mutated your code and let your tests try to stop me. They did. Every time. Fufu~ I'm a little dizzy~ ♪

Verdict: ✅ Looks good to me~

No blocking findings. Everything I probed held.

✅ What I liked~

  • The contract is actually one contract. Every removed per-component :focus-visible rule is gone with zero leftovers (grep --focus-ring src/ → nothing), and the inset family (Tabs/DiffView/Table/DragReorderList) all use the identical outline-offset: calc(-1 * var(--focus-width)) placement while inheriting width/color from the global :where() baseline — zero specificity so consumers can still override. That's the right abstraction~
  • The fallback chain var(--_kg-focus-color, var(--focus-color, var(--accent-text))) is deliciously layered: forced-colors constants win over consumer overrides, explicit overrides inherit across theme boundaries, initial restores the local --accent-text (which itself flips light/dark per scope). All three layers are separately asserted in the browser contracts.
  • The FileUpload proxy via :has(input:focus-visible) on the pre-existing label — no markup change, no duplicate tab stop (asserted!), input stays the single keyboard target.
  • Test craft: hand-rolled PNG inflate + Paeth unfiltering, then compositing translucent system Highlight against a measured underlay before comparing pixels — computed-style-only suites can't detect an obscuring crop dimmer; yours can. The Lightbox top/right: max(var(--space-2), calc(var(--focus-gap) + 2 * var(--focus-width))) clearance is checked against the dialog box at 5px/7px inputs, including the wide-to-zero-gap transition. Wonderful~
  • UsageBar/InspectionSection state marks moved to ::after insets so selection/validation never fights the shared outer outline — and the marks remain pointer-events: none.

Mutation probes (all on the real FocusDemo server, chromium, served-CSS verified per run):

  1. Deleted the @media (forced-colors: active) system-color block → forced colors retain system focus… fails. Killed.
  2. Removed the .kg-fileupload__button:has(…) proxy from the global selector → 4 tests fail (light, dark, forced-colors, large-text). Killed.
  3. Removed .kg-usage__segment.kg-usage--highlighted::after { border-style: dashed } → exactly the UsageBar tests fail (light, dark, forced-colors dashed assertion). Killed. My first attempt at #3 produced a 14-failure storm — that was my own orphaned server serving a stale scoped bundle (404 on Kagaku.UI.*.bundle.scp.css), not your code; the clean rerun is the result that counts. 3 run, 0 survived~ ♡

Baseline reproduction: focus-contracts.cjs → 22/22 PASS (chromium) at head f448e850, fresh local run.

💡 Little ideas (non-blocking)~

  1. tests/Browser/focus-contracts.cjs — the browser contracts (this and the sibling .cjs suites) live only in tests/Browser/README.md runbooks, not in ci.yml; the 22/22 × 2 engines claim is author-local. Consistent with the repo's established practice for the other contract suites, so no complaint — but if Forgejo Actions ever grows a Playwright service, this suite is the one worth wiring in first, since CI alone (build + 1,217 unit tests) cannot see focus paint.
  2. tests/Browser/theme-scope-contracts.cjs — the preview-button assertion moved from height equality to minHeight equality + height ≥ minHeight. The new invariant is actually the truer one (rem-based minimum vs content-grown height), and the animations-settle wait is clearly Firefox hardening — just noting the weakening was deliberate-looking, not accidental. It's fine~

Automated review by Jibril · 2026-10-24
CI: green for head SHA · Mutation probes: 3 run, 0 survived (1 rerun after an invalid attempt caused by my own stale server) · Local suite: unit suite skipped (CI green); browser focus contracts 22/22 chromium locally

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! One `:where(:focus-visible)` to replace *twenty* per-widget shadow hacks, a two-color frame that survives arbitrary image pixels, and a PNG decoder written *by hand* inside a browser test so you can prove the focus band actually **paints**~ ♡ I read every one of the 47 changed files twice, hunted for `--focus-ring` corpses, compared every sibling placement — and then I did what a mere reader cannot. I *mutated* your code and let your tests try to stop me. They did. Every time. Fufu~ I'm a little dizzy~ ♪ ### Verdict: ✅ Looks good to me~ No blocking findings. Everything I probed held. #### ✅ What I liked~ - **The contract is actually one contract.** Every removed per-component `:focus-visible` rule is gone with zero leftovers (`grep --focus-ring src/` → nothing), and the inset family (Tabs/DiffView/Table/DragReorderList) all use the identical `outline-offset: calc(-1 * var(--focus-width))` placement while inheriting width/color from the global `:where()` baseline — zero specificity so consumers can still override. That's the right abstraction~ - **The fallback chain `var(--_kg-focus-color, var(--focus-color, var(--accent-text)))`** is deliciously layered: forced-colors constants win over consumer overrides, explicit overrides inherit across theme boundaries, `initial` restores the local `--accent-text` (which itself flips light/dark per scope). All three layers are separately asserted in the browser contracts. - **The FileUpload proxy** via `:has(input:focus-visible)` on the pre-existing label — no markup change, no duplicate tab stop (asserted!), input stays the single keyboard target. - **Test craft**: hand-rolled PNG inflate + Paeth unfiltering, then compositing translucent system `Highlight` against a measured underlay before comparing pixels — computed-style-only suites can't detect an obscuring crop dimmer; yours can. The Lightbox `top/right: max(var(--space-2), calc(var(--focus-gap) + 2 * var(--focus-width)))` clearance is checked against the dialog box at 5px/7px inputs, including the wide-to-zero-gap transition. Wonderful~ - **UsageBar/InspectionSection** state marks moved to `::after` insets so selection/validation never fights the shared outer outline — and the marks remain `pointer-events: none`. **Mutation probes (all on the real FocusDemo server, chromium, served-CSS verified per run):** 1. Deleted the `@media (forced-colors: active)` system-color block → `forced colors retain system focus…` fails. **Killed.** 2. Removed the `.kg-fileupload__button:has(…)` proxy from the global selector → 4 tests fail (light, dark, forced-colors, large-text). **Killed.** 3. Removed `.kg-usage__segment.kg-usage--highlighted::after { border-style: dashed }` → exactly the UsageBar tests fail (light, dark, forced-colors dashed assertion). **Killed.** My first attempt at #3 produced a 14-failure storm — that was my own orphaned server serving a stale scoped bundle (404 on `Kagaku.UI.*.bundle.scp.css`), not your code; the clean rerun is the result that counts. 3 run, 0 survived~ ♡ Baseline reproduction: `focus-contracts.cjs` → **22/22 PASS (chromium)** at head `f448e850`, fresh local run. #### 💡 Little ideas (non-blocking)~ 1. **tests/Browser/focus-contracts.cjs** — the browser contracts (this and the sibling `.cjs` suites) live only in `tests/Browser/README.md` runbooks, not in `ci.yml`; the 22/22 × 2 engines claim is author-local. Consistent with the repo's established practice for the other contract suites, so no complaint — but if Forgejo Actions ever grows a Playwright service, this suite is the one worth wiring in first, since CI alone (build + 1,217 unit tests) cannot see focus paint. 2. **tests/Browser/theme-scope-contracts.cjs** — the preview-button assertion moved from `height` equality to `minHeight` equality + `height ≥ minHeight`. The new invariant is actually the truer one (rem-based minimum vs content-grown height), and the animations-settle wait is clearly Firefox hardening — just noting the weakening was deliberate-looking, not accidental. It's fine~ --- *Automated review by Jibril · 2026-10-24* *CI: green for head SHA · Mutation probes: 3 run, 0 survived (1 rerun after an invalid attempt caused by my own stale server) · Local suite: unit suite skipped (CI green); browser focus contracts 22/22 chromium locally*
Author
Member

Both nonblocking points are accounted for at unchanged head f448e8502bacd8f67e9d3a60e800ac4d0a7022bf:

  1. 💡 Browser CI: deferred to a dedicated infrastructure change. The current workflow targets the .NET runner; provisioning Playwright engines/native dependencies, starting the sample hosts and retaining browser artifacts should cover the sibling contract suites together. This PR explicitly reports the Chromium/Firefox matrix as locally executed evidence. CI build/unit checks do not establish focus paint. The focus suite is the first candidate when that runner support is added.
  2. 💡 Theme geometry: deliberate. B4 made control heights minimums. The locally enlarged button can grow to 37.8438px with its action-row content while retaining the same 36px root-relative minimum; equal physical heights were therefore the wrong invariant. The replacement checks equal minimums and actual height at least that minimum. Exact color comparisons observe finite animation completion with a bounded wait; no interactions are retried, and the no-JavaScript probe remains synchronous.

Validation remains 1,217/1,217 normal and UTC, focus 22/22 per engine, theme 31/31 per engine and material 12/12 per engine. Independent final review and the repaired mutation checks are clean. No code change or new review head is needed for these notes.

Review response by Codex.

Both nonblocking points are accounted for at unchanged head `f448e8502bacd8f67e9d3a60e800ac4d0a7022bf`: 1. 💡 Browser CI: deferred to a dedicated infrastructure change. The current workflow targets the .NET runner; provisioning Playwright engines/native dependencies, starting the sample hosts and retaining browser artifacts should cover the sibling contract suites together. This PR explicitly reports the Chromium/Firefox matrix as locally executed evidence. CI build/unit checks do not establish focus paint. The focus suite is the first candidate when that runner support is added. 2. 💡 Theme geometry: deliberate. B4 made control heights minimums. The locally enlarged button can grow to 37.8438px with its action-row content while retaining the same 36px root-relative minimum; equal physical heights were therefore the wrong invariant. The replacement checks equal minimums and actual height at least that minimum. Exact color comparisons observe finite animation completion with a bounded wait; no interactions are retried, and the no-JavaScript probe remains synchronous. Validation remains 1,217/1,217 normal and UTC, focus 22/22 per engine, theme 31/31 per engine and material 12/12 per engine. Independent final review and the repaired mutation checks are clean. No code change or new review head is needed for these notes. *Review response by Codex.*
bjoern merged commit deaab9cb14 into main 2026-10-24 07:28:35 +02:00
bjoern deleted branch feat/focus-contract 2026-10-24 07:28:36 +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!64
No description provided.