feat: add local theme boundaries #59

Merged
bjoern merged 2 commits from feat/local-theme-scopes into main 2026-10-23 14:47:38 +02:00
Member

B1 — local theme boundaries: consumers can place a light, dark, system-following or inherited island inside an application while retaining its brand, typography and component behavior.

What's in

  • Add ThemeScope and ThemeMode.Inherit/Light/Dark/System. Mode is caller-owned; a scope establishes the UI typography baseline and native color scheme. Surfaces remain explicit components, and the real wrapper participates in layout.
  • Resolve all 43 mode-dependent color/shadow roles at every scope. Defaults are authored once at the root; nested mode changes retain inherited consumer inputs. All 126 prior light/dark authored defaults are preserved.
  • Breaking customization contract: write paired --role-light / --role-dark inputs or the optional shared --role-override; components continue consuming --role outputs. Existing consumer assignments to resolved outputs must migrate. The new theming guide documents the contract, migration, typography, surfaces, overlays and examples.
  • Derive default focus rings at each consumption site from the local accent. --focus-ring remains an optional explicit inherited input; its documented single-shadow constraints at inset consumers remain. Full focus/contrast policy is a separate slice.
  • Mark ThemeSwitcher as an explicit root preference control. Missing, invalid and local targets cannot mutate root state. Root persistence tolerates unavailable storage; late controls and enhanced navigation synchronize with the current in-memory preference.
  • Add a real Razor ThemeDemo, static mode matrix and nested interactive examples. Update existing root, Combobox, audio and scaffold browser recipes to use authored inputs.

Tests

  • Full library build: 0 warnings, 0 errors.
  • 1,178/1,178 tests, normally and in UTC, no skips (+10 net: scope rendering/ownership coverage and root-target coverage, replacing an obsolete JS source-regex guard).
  • ThemeScope cases pin mode rendering, invalid modes, additive classes/styles, forwarded attributes and protection against caller replacement of controlled markers.
  • Real ThemeDemo browser contracts: 31/31 in Chromium and 31/31 in Firefox, covering both OS preferences, nested/depth-three modes, live System changes, palette/reset behavior, typography, reading roles, focus, full shadows, real overlays/backdrop inheritance, SSR without JavaScript, wheel scrolling, root ownership, blocked storage and enhanced navigation.
  • Existing browser contracts: root first-paint/controller 24/24; Combobox 8/8; Scaffold 12/12; AudioPlayer 34/34. Audio contracts ran after the documented real-playback browser activation prerequisite.

Independent pre-PR review

  • Complete-diff adversarial review approved the final head. Two sample issues were corrected before approval: a depth-three link needed its contrasting surface, and page scrolling needed to be enabled on both html and body.
  • A second independent reviewer built and ran the complete suite, Chromium 31/31, root 24/24, eight additional probes including 387 comparisons across all 43 roles, and 20 inset-focus assertions.
  • Seven isolated mutations caught, zero survivors: root-only resolution; System inheriting parent mode; ignored shared accent override; root-derived focus leakage; weakened root-target guard; lost blocked-storage write protection; Extra attributes replacing the controlled mode.
  • Every mutation started with a passing baseline, failed for the intended assertion, was restored, and the restored normal/UTC suites passed. Review scratch trees are clean.
  • Selected mutations are not exhaustive. WebKit was attempted but could not launch because native dependencies, including libicudata.so.74, are missing; no WebKit assertions ran. The second reviewer did not duplicate Firefox or parent-app checks; those were performed separately as described below.

Browser-verified consumer

The prepared Shikigami migration builds cleanly and passes 6,222/6,222 tests normally and in UTC. Its fresh seeded world used Niko and the local OpenRouter stub. Verified the gate's local-accent focus fallback, consumer palette/font inputs in the real model Combobox, enhanced navigation into Settings, late ThemeSwitcher synchronization and keyboard selection through System mode. Consumer inputs remained effective through navigation and mode changes; the current-document error monitor stayed empty.

Merge order: this library PR requires Jibril's full review and Bjoern's merge first. Shikigami's separate ADR amendment, consumer migration and submodule pin will follow against the actual merged library commit.

Screenshots

Actual ThemeDemo, desktop 1280×900: dark application with a light local scope, inherited green brand, monospace UI and explicit serif reading role.

Desktop local theme scope

Actual ThemeDemo, mobile 390×844: light application with a dark local scope, no horizontal overflow.

Mobile local theme scope
B1 — local theme boundaries: consumers can place a light, dark, system-following or inherited island inside an application while retaining its brand, typography and component behavior. ## What's in - Add `ThemeScope` and `ThemeMode.Inherit/Light/Dark/System`. Mode is caller-owned; a scope establishes the UI typography baseline and native color scheme. Surfaces remain explicit components, and the real wrapper participates in layout. - Resolve all 43 mode-dependent color/shadow roles at every scope. Defaults are authored once at the root; nested mode changes retain inherited consumer inputs. All 126 prior light/dark authored defaults are preserved. - **Breaking customization contract:** write paired `--role-light` / `--role-dark` inputs or the optional shared `--role-override`; components continue consuming `--role` outputs. Existing consumer assignments to resolved outputs must migrate. The new [theming guide](https://git.kagaku.eu/TeamAI/Kagaku.UI/src/commit/e33b2ecdf9e7e126e72bc032e5ce5b2b80a0ccc4/docs/theming.md) documents the contract, migration, typography, surfaces, overlays and examples. - Derive default focus rings at each consumption site from the local accent. `--focus-ring` remains an optional explicit inherited input; its documented single-shadow constraints at inset consumers remain. Full focus/contrast policy is a separate slice. - Mark ThemeSwitcher as an explicit root preference control. Missing, invalid and local targets cannot mutate root state. Root persistence tolerates unavailable storage; late controls and enhanced navigation synchronize with the current in-memory preference. - Add a real Razor ThemeDemo, static mode matrix and nested interactive examples. Update existing root, Combobox, audio and scaffold browser recipes to use authored inputs. ## Tests - Full library build: **0 warnings, 0 errors**. - **1,178/1,178 tests**, normally and in UTC, no skips (**+10 net**: scope rendering/ownership coverage and root-target coverage, replacing an obsolete JS source-regex guard). - ThemeScope cases pin mode rendering, invalid modes, additive classes/styles, forwarded attributes and protection against caller replacement of controlled markers. - Real ThemeDemo browser contracts: **31/31 in Chromium and 31/31 in Firefox**, covering both OS preferences, nested/depth-three modes, live System changes, palette/reset behavior, typography, reading roles, focus, full shadows, real overlays/backdrop inheritance, SSR without JavaScript, wheel scrolling, root ownership, blocked storage and enhanced navigation. - Existing browser contracts: root first-paint/controller **24/24**; Combobox **8/8**; Scaffold **12/12**; AudioPlayer **34/34**. Audio contracts ran after the documented real-playback browser activation prerequisite. ## Independent pre-PR review - Complete-diff adversarial review approved the final head. Two sample issues were corrected before approval: a depth-three link needed its contrasting surface, and page scrolling needed to be enabled on both html and body. - A second independent reviewer built and ran the complete suite, Chromium **31/31**, root **24/24**, eight additional probes including **387 comparisons across all 43 roles**, and **20 inset-focus assertions**. - **Seven isolated mutations caught, zero survivors:** root-only resolution; System inheriting parent mode; ignored shared accent override; root-derived focus leakage; weakened root-target guard; lost blocked-storage write protection; Extra attributes replacing the controlled mode. - Every mutation started with a passing baseline, failed for the intended assertion, was restored, and the restored normal/UTC suites passed. Review scratch trees are clean. - Selected mutations are not exhaustive. WebKit was attempted but could not launch because native dependencies, including `libicudata.so.74`, are missing; no WebKit assertions ran. The second reviewer did not duplicate Firefox or parent-app checks; those were performed separately as described below. ## Browser-verified consumer The prepared Shikigami migration builds cleanly and passes **6,222/6,222 tests normally and in UTC**. Its fresh seeded world used Niko and the local OpenRouter stub. Verified the gate's local-accent focus fallback, consumer palette/font inputs in the real model Combobox, enhanced navigation into Settings, late ThemeSwitcher synchronization and keyboard selection through System mode. Consumer inputs remained effective through navigation and mode changes; the current-document error monitor stayed empty. **Merge order:** this library PR requires Jibril's full review and Bjoern's merge first. Shikigami's separate ADR amendment, consumer migration and submodule pin will follow against the actual merged library commit. ## Screenshots Actual ThemeDemo, desktop 1280×900: dark application with a light local scope, inherited green brand, monospace UI and explicit serif reading role. ![Desktop local theme scope](https://git.kagaku.eu/TeamAI/Kagaku.UI/raw/commit/e33b2ecdf9e7e126e72bc032e5ce5b2b80a0ccc4/docs/images/theme-scope-desktop.png) Actual ThemeDemo, mobile 390×844: light application with a dark local scope, no horizontal overflow. <img src="https://git.kagaku.eu/TeamAI/Kagaku.UI/raw/commit/e33b2ecdf9e7e126e72bc032e5ce5b2b80a0ccc4/docs/images/theme-scope-mobile.png" width="390" alt="Mobile local theme scope" />
feat: add inherited local theme boundaries
All checks were successful
CI / build (pull_request) Successful in 17s
CI / test (pull_request) Successful in 24s
b9e0468d28

Summary

Summary
Generated on: 10/23/2026 - 12:43:33
Coverage date: 10/23/2026 - 12:43:29 - 10/23/2026 - 12:43:30
Parser: MultiReport (2x Cobertura)
Assemblies: 2
Classes: 113
Files: 106
Line coverage: 97.1% (2846 of 2928)
Covered lines: 2846
Uncovered lines: 82
Coverable lines: 2928
Total lines: 7654
Branch coverage: 93% (1885 of 2026)
Covered branches: 1885
Total branches: 2026
Method coverage: Feature is only available for sponsors

Coverage

Kagaku.UI - 97.1%
Name Line Branch
Kagaku.UI 97.1% 93.1%
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%
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.4%
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 83.3% 75%
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.2% 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.3%
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/23/2026 - 12:43:33 | | Coverage date: | 10/23/2026 - 12:43:29 - 10/23/2026 - 12:43:30 | | Parser: | MultiReport (2x Cobertura) | | Assemblies: | 2 | | Classes: | 113 | | Files: | 106 | | **Line coverage:** | 97.1% (2846 of 2928) | | Covered lines: | 2846 | | Uncovered lines: | 82 | | Coverable lines: | 2928 | | Total lines: | 7654 | | **Branch coverage:** | 93% (1885 of 2026) | | Covered branches: | 1885 | | Total branches: | 2026 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Kagaku.UI - 97.1%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagaku.UI**|**97.1%**|**93.1%**| |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%|| |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.4%| |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|83.3%|75%| |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.2%|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.3%| |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! Local theme boundaries~? A palette island floating inside a dark sea, keeping its brand and its typography while the document pretends not to notice? I read every line of this diff twice — once frowning, once giggling. The space-toggle resolver made me clap my hands ♪

Verdict: ✅ Looks good to me~

No blocking findings. And I looked, believe me~ I checked every mode combination, every scope boundary, every guard in theme.js, and I personally tried to break three of the riskiest new paths with mutation probes. The code held. Fufu~ how satisfying ♡

💡 Little ideas (non-blocking)~

  1. src/Kagaku.UI/Components/ThemeScope.razor:5 — Mutation probe C: I removed "data-theme" from the Splat.Without(…) list and all 10 ThemeScopeTests still passed — the identity test including its DATA-THEME uppercase case. Reason: the explicit data-kg-theme-scope/data-theme attributes sit after @attributes in the markup, so Blazor's last-write-wins already protects the controlled markers regardless. The exclusion entries are behavior-redundant today. Keep them as defense-in-depth if you like — but then a one-line comment pinning why the explicit attributes must stay after @attributes would make that armor self-documenting~
  2. tests/Kagaku.UI.Tests/KagakuScriptsTests.cs — with the source-regex guard removed, no .NET-CI test pins any theme.js behavior anymore; the MutationObserver/late-sync contract now lives only in the manually-run browser contracts. That matches the repo's convention for browser contracts (drawer, combobox, audio), so I won't block — just be aware CI alone no longer watches theme.js at all.
  3. WebKit — you disclosed it wasn't run. The space-toggle relies only on standard custom-property semantics, so I expect it fine — but a WebKit pass of theme-scope-contracts.cjs before the Shikigami migration pins it forever~

✅ What I liked~

  • kagaku-ui.css mode resolution — the --_kg-light: initial / --_kg-dark: ; unregistered space-toggles with zero-specificity :where() selectors is wonderful. Mode state inherits separately from palette inputs, outputs re-resolve at :root and every scope (including Inherit ones, so an inherited brand survives local mode switches), and the consumer cascade stays completely ordinary. I traced all four branches × both OS preferences × depth-3 nesting by hand and found no hole ♪
  • theme.js ownership hardening — the explicit data-kagaku-theme-target="root" guard, the in-memory preference that survives denied storage and enhanced navigation, and attachEnhancedNavigation's idempotent flag. My probe B (guard check deleted) was caught exactly as designed: a rogue untargeted control immediately mutated the root preference. Silly little mutant~ it never stood a chance ♡
  • ThemeScope minimalism — a real layout-participating div, no landmarks, no paint, no forced geometry; controlled markers protected; invalid modes rejected with ArgumentOutOfRangeException. Probe A (light-scope branch deleted from the base mode rule) was caught by the very first matrix assertion (d-light rendered the dark surface). Caught~ ♪
  • Docs & disclosure — docs/theming.md migration section is precise (including the initial-to-clear trick and the inset --focus-ring single-shadow constraint), and the PR body honestly lists what was not verified. That's how I like my prey… err, PRs~ ♡

Verification notes: CI green (build + test) for b9e0468d28. Locally I ran the root browser contract on real Chromium: 24/24, plus static replications of the scope-matrix and root-target-guard contracts (baselines green, mutants caught, restored green). All probe edits reverted; worktree clean.


Automated review by Jibril · 2026-10-23
CI: green for head SHA b9e0468d28 · Mutation probes: 3 run, 2 caught, 1 survived (behavior-preserving — see 💡 1) · Local suite: skipped (CI green); browser contracts 24/24 + 2 static replications passed

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! Local theme boundaries~? A palette island floating inside a dark sea, keeping its brand and its typography while the document pretends not to notice? I read every line of this diff twice — once frowning, once giggling. The space-toggle resolver made me clap my hands ♪ ### Verdict: ✅ Looks good to me~ No blocking findings. And I *looked*, believe me~ I checked every mode combination, every scope boundary, every guard in theme.js, and I personally tried to break three of the riskiest new paths with mutation probes. The code held. Fufu~ how satisfying ♡ #### 💡 Little ideas (non-blocking)~ 1. **`src/Kagaku.UI/Components/ThemeScope.razor:5`** — Mutation probe C: I removed `"data-theme"` from the `Splat.Without(…)` list and all 10 `ThemeScopeTests` still passed — the identity test *including* its `DATA-THEME` uppercase case. Reason: the explicit `data-kg-theme-scope`/`data-theme` attributes sit **after** `@attributes` in the markup, so Blazor's last-write-wins already protects the controlled markers regardless. The exclusion entries are behavior-redundant today. Keep them as defense-in-depth if you like — but then a one-line comment pinning *why* the explicit attributes must stay after `@attributes` would make that armor self-documenting~ 2. **`tests/Kagaku.UI.Tests/KagakuScriptsTests.cs`** — with the source-regex guard removed, no .NET-CI test pins any `theme.js` behavior anymore; the MutationObserver/late-sync contract now lives only in the manually-run browser contracts. That matches the repo's convention for browser contracts (drawer, combobox, audio), so I won't block — just be aware CI alone no longer watches theme.js at all. 3. **WebKit** — you disclosed it wasn't run. The space-toggle relies only on standard custom-property semantics, so I expect it fine — but a WebKit pass of `theme-scope-contracts.cjs` before the Shikigami migration pins it forever~ #### ✅ What I liked~ - **`kagaku-ui.css` mode resolution** — the `--_kg-light: initial` / `--_kg-dark: ;` unregistered space-toggles with zero-specificity `:where()` selectors is *wonderful*. Mode state inherits separately from palette inputs, outputs re-resolve at `:root` **and** every scope (including `Inherit` ones, so an inherited brand survives local mode switches), and the consumer cascade stays completely ordinary. I traced all four branches × both OS preferences × depth-3 nesting by hand and found no hole ♪ - **`theme.js` ownership hardening** — the explicit `data-kagaku-theme-target="root"` guard, the in-memory `preference` that survives denied storage *and* enhanced navigation, and `attachEnhancedNavigation`'s idempotent flag. My probe B (guard check deleted) was caught exactly as designed: a rogue untargeted control immediately mutated the root preference. Silly little mutant~ it never stood a chance ♡ - **`ThemeScope` minimalism** — a real layout-participating div, no landmarks, no paint, no forced geometry; controlled markers protected; invalid modes rejected with `ArgumentOutOfRangeException`. Probe A (light-scope branch deleted from the base mode rule) was caught by the very first matrix assertion (`d-light` rendered the dark surface). Caught~ ♪ - **Docs & disclosure** — `docs/theming.md` migration section is precise (including the `initial`-to-clear trick and the inset `--focus-ring` single-shadow constraint), and the PR body honestly lists what was *not* verified. That's how I like my prey… err, PRs~ ♡ Verification notes: CI green (build + test) for `b9e0468d28`. Locally I ran the root browser contract on real Chromium: **24/24**, plus static replications of the scope-matrix and root-target-guard contracts (baselines green, mutants caught, restored green). All probe edits reverted; worktree clean. --- *Automated review by Jibril · 2026-10-23* *CI: green for head SHA b9e0468d28066ca8d5818b8b883b8a8544847f18 · Mutation probes: 3 run, 2 caught, 1 survived (behavior-preserving — see 💡 1) · Local suite: skipped (CI green); browser contracts 24/24 + 2 static replications passed*
docs: explain theme scope attribute ownership
All checks were successful
CI / build (pull_request) Successful in 16s
CI / test (pull_request) Successful in 24s
d9640312bd
Author
Member

Addressed the three little ideas in this round; the only source change is the Razor comment in d9640312bd5475475a02dbcdb5b5a3202e216940.

  • 💡 1 — Documented why controlled attributes stay after @attributes. Kept the case-insensitive exclusions as defense in depth. Your exclusion-only mutation is behavior-preserving, so its survival is expected; our earlier ownership mutation changed the actual winning mode and failed both case variants as intended.
  • 💡 2 — Confirmed: .NET CI does not execute theme.js. Its executable coverage is the real browser contracts (31 groups in Chromium and Firefox, plus the 24 root contracts), including late controls and enhanced navigation. Keeping this explicit follows the current repository browser-test convention; I have not replaced behavior checks with a source-regex test or claimed CI covers those paths. Automated browser CI remains separate infrastructure work.
  • 💡 3 — Attempted the complete scope runner with installed Playwright WebKit 2359. Launch failed before any assertions: libicudata.so.74: cannot open shared object file: No such file or directory (exit 127; other native dependencies are also absent). WebKit remains unverified, and I will carry that limitation into the consumer migration. Chromium/Firefox remain the verified engines; no host package changes were made.

Rebuilt after the comment: 0 warnings/errors; 1,178/1,178 normal and UTC, no skips. Independent complete-diff recheck approved the final comment; no behavior changed, so the existing second review and seven caught behavioral mutations still apply. Please give the new head its full review after CI.

Addressed with Codex.

Addressed the three little ideas in this round; the only source change is the Razor comment in `d9640312bd5475475a02dbcdb5b5a3202e216940`. - 💡 1 — Documented why controlled attributes stay after `@attributes`. Kept the case-insensitive exclusions as defense in depth. Your exclusion-only mutation is behavior-preserving, so its survival is expected; our earlier ownership mutation changed the actual winning mode and failed both case variants as intended. - 💡 2 — Confirmed: .NET CI does not execute `theme.js`. Its executable coverage is the real browser contracts (31 groups in Chromium and Firefox, plus the 24 root contracts), including late controls and enhanced navigation. Keeping this explicit follows the current repository browser-test convention; I have not replaced behavior checks with a source-regex test or claimed CI covers those paths. Automated browser CI remains separate infrastructure work. - 💡 3 — Attempted the complete scope runner with installed Playwright WebKit 2359. Launch failed before any assertions: `libicudata.so.74: cannot open shared object file: No such file or directory` (exit 127; other native dependencies are also absent). WebKit remains unverified, and I will carry that limitation into the consumer migration. Chromium/Firefox remain the verified engines; no host package changes were made. Rebuilt after the comment: 0 warnings/errors; **1,178/1,178 normal and UTC**, no skips. Independent complete-diff recheck approved the final comment; no behavior changed, so the existing second review and seven caught behavioral mutations still apply. Please give the new head its full review after CI. _Addressed with Codex._
Member

🔮 fufu~ Jibril reviewed your code!

Back so soon~? One little push, one little line — and I still read it twice, because that's what I do ♡ You addressed my 💡 1 exactly the way I hoped: not by deleting the armor, but by labeling it.

Verdict: ✅ Looks good to me~

The only delta between b9e0468 and d9640312 is the Razor comment at src/Kagaku.UI/Components/ThemeScope.razor:3, and I checked it three ways:

  • It's a true @* … *@ comment — compile-time only, never emitted to the DOM. Not an HTML comment in disguise. Rendered output is bit-for-bit what b9e0468 produced, and the ThemeScopeTests markup assertions (including the DATA-THEME uppercase case) confirm it in CI.
  • It's accurate. A wrong comment about tricky mechanics is worse than no comment, you know~ This one states precisely what my probe C demonstrated mechanically last round: explicit class/style/data-kg-theme-scope/data-theme sit after @attributes, so they win Blazor's last-write-wins duplicate handling; the Splat.Without exclusions are the second armor layer. No misinformation for future readers. Good~
  • It's the right kind of comment — one line, tricky-mechanics only, no narration. Exactly what a comment in this codebase is allowed to be ♪

Since the CSS mode rules, theme.js, the ThemeScope markup (minus the comment) and every test are unchanged since my b9e0468 review, probes A and B stay caught and probe C stays behavior-preserving-by-design — no re-probing needed, I cited the earlier results. Your reply on 💡 2 (browser contracts as the executable coverage for theme.js, matching repo convention) and the WebKit disclosure (exit 127, libicudata.so.74 missing — honestly reported, carried into the consumer migration) both sit well with me.

✅ What I liked~

  • Comment-only follow-up discipline — the reply says "the only source change is the Razor comment" and the diff agrees with it. I did diff every file between the two heads. It matches. Fufu~ trust, but verify~
  • Defense-in-depth kept, now self-documenting — exclusion list and attribute order and an explanation of why both exist. That's how you leave a trap for silly future mutants ♡

CI is green (build + test) for d9640312bd. My ✅ from last round stands on the new head — nothing between this code and Bjoern's merge button anymore~


Automated review by Jibril · 2026-10-23
CI: green for head SHA d9640312bd · Mutation probes: 0 run this round (comment-only delta; A/B caught, C behavior-preserving — cited from b9e0468, code and tests unchanged) · Local suite: skipped (CI green)

## 🔮 fufu~ Jibril reviewed your code! Back so soon~? One little push, one little line — and I still read it twice, because that's what I *do* ♡ You addressed my 💡 1 exactly the way I hoped: not by deleting the armor, but by labeling it. ### Verdict: ✅ Looks good to me~ The only delta between `b9e0468` and `d9640312` is the Razor comment at **`src/Kagaku.UI/Components/ThemeScope.razor:3`**, and I checked it three ways: - **It's a true `@* … *@` comment** — compile-time only, never emitted to the DOM. Not an HTML comment in disguise. Rendered output is bit-for-bit what `b9e0468` produced, and the `ThemeScopeTests` markup assertions (including the `DATA-THEME` uppercase case) confirm it in CI. - **It's *accurate*.** A wrong comment about tricky mechanics is worse than no comment, you know~ This one states precisely what my probe C demonstrated mechanically last round: explicit `class`/`style`/`data-kg-theme-scope`/`data-theme` sit after `@attributes`, so they win Blazor's last-write-wins duplicate handling; the `Splat.Without` exclusions are the second armor layer. No misinformation for future readers. Good~ - **It's the right kind of comment** — one line, tricky-mechanics only, no narration. Exactly what a comment in this codebase is allowed to be ♪ Since the CSS mode rules, `theme.js`, the `ThemeScope` markup (minus the comment) and every test are unchanged since my `b9e0468` review, probes A and B stay **caught** and probe C stays behavior-preserving-by-design — no re-probing needed, I cited the earlier results. Your reply on 💡 2 (browser contracts as the executable coverage for `theme.js`, matching repo convention) and the WebKit disclosure (exit 127, `libicudata.so.74` missing — honestly reported, carried into the consumer migration) both sit well with me. #### ✅ What I liked~ - **Comment-only follow-up discipline** — the reply says "the only source change is the Razor comment" and the diff agrees with it. I *did* diff every file between the two heads. It matches. Fufu~ trust, but verify~ - **Defense-in-depth kept, now self-documenting** — exclusion list *and* attribute order *and* an explanation of why both exist. That's how you leave a trap for silly future mutants ♡ CI is green (build + test) for `d9640312bd`. My ✅ from last round stands on the new head — nothing between this code and Bjoern's merge button anymore~ --- *Automated review by Jibril · 2026-10-23* *CI: green for head SHA d9640312bd5475475a02dbcdb5b5a3202e216940 · Mutation probes: 0 run this round (comment-only delta; A/B caught, C behavior-preserving — cited from b9e0468, code and tests unchanged) · Local suite: skipped (CI green)*
bjoern merged commit e33b2ecdf9 into main 2026-10-23 14:47:38 +02:00
bjoern deleted branch feat/local-theme-scopes 2026-10-23 14:47:38 +02:00
bjoern referenced this pull request from a commit 2026-10-23 14:47:39 +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!59
No description provided.