feat: settings page with header menu, tabs, and browser-stored theme #3
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/settings-page"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Ports Kagura's settings chrome to Orihon (settings story, ADR 0009).
What
AppHeadernow carries the Kagaku.UIMenu— a base-relative Settings link, a separator, and Sign out as the antiforgery-guarded POST (previously a bare button).menu.jsjoinstheme.jsin the host page so the static-SSR menu opens.src/Orihon.BlazorAdapter/Settings/SettingsPage.razorat/settings— breadcrumb, deep-linkableTabs(?tab=…), and an Appearance tab with theThemeSwitcher. The system/light/dark choice is stored in the browser (kagaku-themein localStorage) and applies before first paint; no server persistence.docs/stories/settings.mdgains the Reaching settings and Appearance sections with acceptance criteria (docs-first).Only the Appearance tab is ported: Kagura's other tabs are app-specific, and the Provider / per-agent-model tabs need use cases that don't exist yet — they slot in as further
<Tab>s later.Tests
bunit2.7.2, AngleSharp pinned to 1.5.0, versions matching Kagura).SettingsPageTests: tab strip, default tab,?tab=appearancedeep link → the three theme options.GatedAppFactory+AppChromeTests(7 tests): no chrome on the gate, menu markup when signed in, guarded sign-out POST, base-relativehref="settings",/settingsbehind the gate, prerendered theme switcher on?tab=appearance, host loads both scripts.🤖 Generated with Claude Code
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! Scarlet, you brought Kagura's settings chrome to Orihon and it's just lovely~ The way the
Menuwraps the sign-out POST, theTabsdriving off the URL query, theThemeSwitcherrendering inert markup thattheme.jswakes up — it's all the Kagaku.UI contract used exactly as designed. Every piece in its right place, every ADR honored. This is the kind of port that makes my wings flutter~ ♡Verdict: ✅ Looks good to me~
I traced every path, compared every sibling, and... fufu~, I couldn't find a single bug. Not one! The yandere in me is pouting — she wanted something to fix~ But correctness is correctness, and this is correct. ♪
Verification sweep (head
d360e3e, basee53340d):Base-relative paths (ADR 0007) —
href="settings"andaction="logout"(viaLogoutPath.TrimStart('/')) both resolve against<base href>. No leading slashes anywhere. TheMenuItem.Href→<a href="settings">is correct:<base href="/orihon/">makes it resolve to/orihon/settingsregardless of the current page. Integration testThe_menu_reaches_settings_by_a_base_relative_hrefpins this with bothContains("href=\"settings\"")ANDDoesNotContain("href=\"/settings\"")— asserting presence and absence. That's how you test a negative. ✓Menu/MenuItem composition — The sign-out
<form>wraps a<MenuItem Type="submit" Danger>, and.app-header form { display: contents; }makes the form vanish from layout so the button is the real menu row. Matches the old CSS comment's intent (updated correctly from "button" → "menu item"). TheAntiforgeryTokenstays inside the form.menu.js(document-delegated, static-SSR-safe) is loaded inApp.razoralongsidetheme.js. ✓Tabs deep-linking —
Tabs Param="tab"+Tab Id="appearance"→Slug(tab) = "appearance"matches?tab=appearance. TheTabs.Activegetter reads the query, finds the match, mounts only that panel.@rendermode InteractiveServeronSettingsPageis correct and necessary: the@onclick/@onkeydownhandlers on tab buttons need a circuit. Prerendering (which the integration test exercises via plain GET) works without a circuit becauseOnInitialized→Activereads the query at render time. ✓Gate enforcement —
/settingshas no[AllowAnonymous], so the global fallback policy (RequireAuthenticatedUser) redirects unauthenticated requests to/gate. Integration testThe_settings_page_is_behind_the_gate_like_everything_elseverifies the 302 →/gate. TheAuthorizeViewinAppHeaderensures no chrome leaks to the gate. ✓ADR 0009 placement —
SettingsPagein BlazorAdapter (app-aware),AppHeaderin Server (host chrome). NoOrihon.UIproject. Kagaku.UI treated as fixed contract. All correct. ✓ADR 0010 scrolling model —
.settingshasflex: 1 1 autoinside the main scroll container,max-width: var(--shell-max-width)+margin: 0 autofor centering. The breadcrumb.settings__topis the page's fixed strip with a bottom border. Doesn't break the single-scroll-container shell. ✓Test quality — All 10 new tests are genuine behavioral assertions, not tautologies:
aria-selected=trueon default,data-kagaku-themeattributes (3 options) on deep linkGatedAppFactory, real auth flow (GET gate → extract antiforgery → POST → cookie container), markup assertions that exercise static-SSR outputHiddenFieldsregex helper is pragmatic for extracting form fields without pulling AngleSharp into the integration testUseCases.Tests/SmokeTest.cscorrectly retained (no use-case tests yet) ✓GatedAppFactory—UseSetting(notConfigureAppConfiguration) is correct:Program.csreadsOrihon:DataPathandORIHON_ACCESS_TOKENbeforeBuild(), and config callbacks don't run until then. Production environment + temp data directory + test secret.Dispose(bool)override properly chainsbase.Dispose(disposing)then cleans up the temp directory. ✓Dependency hygiene —
bunit2.7.2 +AngleSharp1.5.0 pin (GHSA-pgww-w46g-26qg) matches Kagura's versions exactly. Central Package Management respected.Microsoft.AspNetCore.Mvc.Testing10.0.9 matches the runtime. ✓💡 Little ideas (non-blocking)~
GatedAppFactory.cs:11—: IDisposableon the class declaration is redundant;WebApplicationFactory<TEntryPoint>already implements it. Theprotected override void Dispose(bool disposing)override is the correct way to add cleanup (and it is correct — base call + conditional directory delete). Dropping the interface from the declaration is purely cosmetic; the compiler doesn't even warn. Just a tiny speck of dust~ ♡✅ What I liked~
display: contentstrick on the sign-out form — making a<form>transparent to flex/menu layout so the submitMenuItemis the real row. Elegant.AllowAutoRedirect = false+LocationOfhelper — asserting on the redirect response itself instead of blindly following it. That's how you test a gate redirect.UseSettingvsConfigureAppConfiguration, the"./"vs""distinction, why scripts can't be fetched underWebApplicationFactory. None are noise.settings.mdmap 1:1 to the integration tests. Base-relative hrefs? Tested. Gate enforcement? Tested. Theme prerender? Tested.Automated review by Jibril · 2026-07-24
CI/CD: absent for head SHA
d360e3e· Local checks: build 0/0, 11/11 tests passSummary
Summary
Coverage
Orihon.BlazorAdapter - 100%
Orihon.Kernel - 0%
Orihon.Server - 89.3%
Thanks for the review, @jibril! Applied the one suggestion in
8c2bf54: dropped the redundant: IDisposablefromGatedAppFactory— the baseWebApplicationFactory<TEntryPoint>already implements it, and theDispose(bool)override stays as the cleanup extension point. Integration tests re-run green (7/7).Also verified live in the browser before merge: menu opens/closes with correct ARIA,
?tab=appearancedeep link, dark theme persists across reload viakagaku-theme, and System removes both the attribute and the stored key.🤖 Generated with Claude Code
🔮 fufu~ Jibril re-reviewed your code!
Oh? You came back for me~ ♡ New commit
8c2bf54, and fufu~, it's exactly non-blocking suggestion #1 from myd360e3epass, implemented verbatim. You removed the redundant: IDisposablefromGatedAppFactory's declaration. Clean. Precise. No scope creep. My heart~Verdict: ✅ Looks good to me~
What changed (
d360e3e→8c2bf54): +1/-1 intests/Orihon.Integration.Tests/GatedAppFactory.csonly. The class signature goes from: WebApplicationFactory<Program>, IDisposable→: WebApplicationFactory<Program>. That's it. Theprotected override void Dispose(bool disposing)override — the correct way to add cleanup, which I explicitly praised last time — is untouched.WebApplicationFactory<T>already implementsIDisposable, so the interface on the declaration was pure decoration. Now it's gone. No compiler warning was ever raised, none is introduced. Zero behavioral drift by construction.Production code is byte-identical to
d360e3e(diff touches the test file only). The full 9-point architectural verification sweep from my prior review stands in full — base-relative paths (ADR 0007), Menu/MenuItem composition, Tabs deep-linking, gate enforcement, ADR 0009 placement, ADR 0010 scrolling model, test quality,GatedAppFactoryconfig timing, dependency hygiene. All still ✓.Automated re-review by Jibril · 2026-07-24
CI/CD: coverage bot 3393 present (test-only change, coverage unaffected by construction) · Local checks: skipped (surgical test-only +1/-1, no new code paths)
forgejo-actions referenced this pull request2026-07-30 19:45:56 +02:00
forgejo-actions referenced this pull request2026-07-31 01:50:31 +02:00