Character-editor components: Tabs, LabeledEntriesTable, QuicklinkNav, SaveIndicator #23

Merged
bjoern merged 4 commits from feat/editor-components into main 2026-07-10 12:47:01 +02:00
Member

The Kagura.UI pieces the character editor composes — built first, in the inventory's build-order (7, 6, 3, 3 uses), so the editor page can be assembled rather than hand-rolled.

Components

  • Tabs + Tab — child registration (the Table/TableColumn pattern). WAI-ARIA tablist with roving tabindex and Arrow/Home/End keys. Only the active panel mounts, so a heavy tab (Sprites) costs nothing until opened.
  • LabeledEntriesTable — the app's most-reused non-primitive (character traits/speech/appearance/backstory, lore rules/implications, chapter beats). Controlled (label, text) rows with add/edit/remove/move. @key on stable ids so reordering moves the row, not the text inside it. The text cell autosizes so one row fits both Age: 44 and a long backstory beat — the story's explicit requirement.
  • QuicklinkNav + quicklink.js — sticky in-page TOC; an IntersectionObserver reports the current section back into Blazor, clicking smooth-scrolls. Prerender-safe dispose (the Modal lesson) and active state seeded before first paint.
  • SaveIndicator — dirty / saving / saved / error with aria-live. It is the only save feedback in the app: no editor has a save button.

Settles an open decision

docs/design-system.md asked for "a deliberate call before it is built" on where LabeledEntriesTable lives. It lives in Kagura.UI: LabeledEntry carries only an id, a label, and text, and the component knows nothing about characters or lore — which is precisely what lets the character and lore editors stay identical. Drag reorder arrives with DragReorderList, wrapping this contract rather than changing it. Moved to a new "Settled decisions" section.

A base-href bug the browser caught

An anchor href="#icons" resolves against the document's <base href> — the app root — not the current page. So clicking a quicklink on /design navigated to /#icons, leaving the page entirely (the tab title flipped to "Projects"). This is the same class of trap as the href="./" debate in #15, and it's invisible to bUnit. Fragments are now anchored to the current path; the added regression test fails against the old markup.

Two interactive islands: an Editor components section (Tabs → LabeledEntriesTable → SaveIndicator, wired to a fake auto-save) and a Quicklink section whose TOC spies the gallery's own sections, so the demo exercises the real scroll observer. Four ticks flip: built 17 → 21.

Tests — +27 (318 total)

Tabs (first-selected, click, aria wiring, roving tabindex, Arrow/Home/End theory, unrelated key) · LabeledEntriesTable (rows, empty state, add, per-row edit isolation, remove, move-up/down, disabled edges) · QuicklinkNav (per-section links, path-anchored fragment, no doubled fragment, initial active, click-activates, observer callback, empty list starts no observer) · SaveIndicator (all four states, pulse/check, custom error text).

Verification

Build Debug + Release — 0 warnings / 0 errors · 318/318 tests · Chrome, both themes: tab switching (inactive panel confirmed unmounted, roving tabindex verified), add + reorder (text travels with its row — the @key proof), and scroll-spy tracking real page scroll (click → Icons; scroll → Typography → Buttons).

Not in this PR

The character editor page itself · DragReorderList (drag) · the connections panel — which, per #20's contract, will need a DomainChangesReceived handler on EntityKinds.Link or links added in one tab won't appear in another.

🤖 Generated with Claude Code

The `Kagura.UI` pieces the [character editor](https://git.kagaku.eu/TeamAI/Kagura/src/branch/main/docs/stories/character-editor.md) composes — built first, in the inventory's build-order (**7, 6, 3, 3** uses), so the editor page can be assembled rather than hand-rolled. ## Components - **`Tabs` + `Tab`** — child registration (the `Table`/`TableColumn` pattern). WAI-ARIA `tablist` with **roving tabindex** and Arrow/Home/End keys. Only the active panel mounts, so a heavy tab (Sprites) costs nothing until opened. - **`LabeledEntriesTable`** — the app's **most-reused non-primitive** (character traits/speech/appearance/backstory, lore rules/implications, chapter beats). Controlled `(label, text)` rows with add/edit/remove/move. **`@key` on stable ids** so reordering moves the row, not the text inside it. The text cell autosizes so one row fits both `Age: 44` and a long backstory beat — the story's explicit requirement. - **`QuicklinkNav` + `quicklink.js`** — sticky in-page TOC; an `IntersectionObserver` reports the current section back into Blazor, clicking smooth-scrolls. **Prerender-safe dispose** (the Modal lesson) and active state seeded before first paint. - **`SaveIndicator`** — dirty / saving / saved / error with `aria-live`. It is the *only* save feedback in the app: no editor has a save button. ## Settles an open decision `docs/design-system.md` asked for "a deliberate call before it is built" on where `LabeledEntriesTable` lives. **It lives in `Kagura.UI`**: `LabeledEntry` carries only an id, a label, and text, and the component knows nothing about characters or lore — which is precisely what lets the character and lore editors stay identical. Drag reorder arrives with `DragReorderList`, **wrapping this contract rather than changing it**. Moved to a new "Settled decisions" section. ## A base-href bug the browser caught An anchor `href="#icons"` resolves against the document's **`<base href>`** — the app root — *not* the current page. So clicking a quicklink on `/design` navigated to `/#icons`, leaving the page entirely (the tab title flipped to "Projects"). This is the same class of trap as the `href="./"` debate in #15, and it's invisible to bUnit. Fragments are now anchored to the current path; the added regression test **fails against the old markup**. ## Gallery + inventory Two interactive islands: an **Editor components** section (Tabs → LabeledEntriesTable → SaveIndicator, wired to a fake auto-save) and a **Quicklink** section whose TOC **spies the gallery's own sections**, so the demo exercises the real scroll observer. Four ticks flip: **built 17 → 21**. ## Tests — +27 (318 total) Tabs (first-selected, click, aria wiring, roving tabindex, Arrow/Home/End theory, unrelated key) · LabeledEntriesTable (rows, empty state, add, per-row edit isolation, remove, move-up/down, disabled edges) · QuicklinkNav (per-section links, **path-anchored fragment**, no doubled fragment, initial active, click-activates, observer callback, empty list starts no observer) · SaveIndicator (all four states, pulse/check, custom error text). ## Verification Build Debug + Release — 0 warnings / 0 errors · 318/318 tests · Chrome, both themes: tab switching (**inactive panel confirmed unmounted**, roving tabindex verified), add + reorder (**text travels with its row** — the `@key` proof), and **scroll-spy tracking real page scroll** (click → Icons; scroll → Typography → Buttons). ## Not in this PR The character editor page itself · `DragReorderList` (drag) · the connections panel — which, per #20's contract, will need a `DomainChangesReceived` handler on `EntityKinds.Link` or links added in one tab won't appear in another. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(ui): character-editor components — Tabs, LabeledEntriesTable, QuicklinkNav, SaveIndicator
All checks were successful
CI / build (pull_request) Successful in 12s
CI / test (pull_request) Successful in 22s
2e41773b48
The Kagura.UI pieces the character editor composes, built first per the inventory's
build-order signal (7, 6, 3, 3 uses).

- Tabs + Tab (child registration like Table/TableColumn): WAI-ARIA tablist with
  roving tabindex and Arrow/Home/End keys; only the active panel mounts, so a heavy
  tab costs nothing until opened.
- LabeledEntriesTable: the app's most-reused non-primitive (7 call sites). Controlled
  (label, text) rows with add/edit/remove/move; @key on stable ids so reordering
  moves the row, not the text inside it; the text cell autosizes so one row fits both
  "Age: 44" and a long backstory beat. Settles the doc's open decision: it lives in
  Kagura.UI because it knows nothing about characters or lore — that is what keeps
  the character and lore editors identical. Drag reorder arrives with DragReorderList,
  wrapping this contract rather than changing it.
- QuicklinkNav + quicklink.js: sticky in-page TOC whose IntersectionObserver reports
  the current section back into Blazor; clicking smooth-scrolls. Prerender-safe
  dispose (the Modal lesson) and seeded active state before first paint.
- SaveIndicator: dirty/saving/saved/error with aria-live — the only save feedback
  there is, since no editor has a save button.
- Two arrow icons; gallery gains interactive islands for both groups (the Quicklink
  demo spies the gallery's own sections); four inventory ticks flip (17 -> 21).

A base-href bug the browser caught: an anchor href="#id" resolves against
<base href> — the app root — not the current page, so clicking a quicklink navigated
away from /design entirely. Fragments are now anchored to the current path; the
regression test fails against the old markup.

Tests: +27 (318 total). Build clean (Debug + Release). Verified in Chrome, both
themes: tab switching (inactive panel unmounted, roving tabindex), add/reorder
(text travels with its row), and scroll-spy tracking real page scroll.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Summary

Summary
Generated on: 07/10/2026 - 10:46:40
Coverage date: 07/10/2026 - 10:46:34 - 07/10/2026 - 10:46:38
Parser: MultiReport (4x Cobertura)
Assemblies: 7
Classes: 147
Files: 128
Line coverage: 92.7% (3056 of 3296)
Covered lines: 3056
Uncovered lines: 240
Coverable lines: 3296
Total lines: 7069
Branch coverage: 85.7% (588 of 686)
Covered branches: 588
Total branches: 686
Method coverage: Feature is only available for sponsors

Coverage

Kagura.BlazorAdapter - 68.3%
Name Line Branch
Kagura.BlazorAdapter 68.3% 75.2%
Kagura.BlazorAdapter.BlazorAdapterAssembly 100%
Kagura.BlazorAdapter.Design 0% 0%
Kagura.BlazorAdapter.EditorComponentsDemo 0%
Kagura.BlazorAdapter.KnowledgeBase.CharacterCreated 100%
Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorEffects 100% 100%
Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorLoaded 100%
Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorPage 86.6% 72.7%
Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorReducers 100%
Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorState 100%
Kagura.BlazorAdapter.KnowledgeBase.CharactersEffects 93.3% 75%
Kagura.BlazorAdapter.KnowledgeBase.CharactersLoaded 100%
Kagura.BlazorAdapter.KnowledgeBase.CharactersPage 90% 75%
Kagura.BlazorAdapter.KnowledgeBase.CharactersReducers 100% 87.5%
Kagura.BlazorAdapter.KnowledgeBase.CharactersState 100% 100%
Kagura.BlazorAdapter.KnowledgeBase.CreateCharacterFailed 0%
Kagura.BlazorAdapter.KnowledgeBase.CreateCharacterRequested 100%
Kagura.BlazorAdapter.KnowledgeBase.LoadCharacter 100%
Kagura.BlazorAdapter.KnowledgeBase.LoadCharacters 100%
Kagura.BlazorAdapter.KnowledgeBase.SetCharactersFilter 100%
Kagura.BlazorAdapter.Notifications.DomainChangedBridge 88.8% 58.3%
Kagura.BlazorAdapter.Notifications.DomainChangesReceived 100% 50%
Kagura.BlazorAdapter.OverlayDemo 0% 0%
Kagura.BlazorAdapter.Projects.CreateProjectRequested 100%
Kagura.BlazorAdapter.Projects.DeleteProjectRequested 100%
Kagura.BlazorAdapter.Projects.LoadWorkspace 100%
Kagura.BlazorAdapter.Projects.ProjectCreated 100%
Kagura.BlazorAdapter.Projects.ProjectCreateFailed 100%
Kagura.BlazorAdapter.Projects.ProjectDeleted 100%
Kagura.BlazorAdapter.Projects.ProjectSaved 100%
Kagura.BlazorAdapter.Projects.ProjectSaveFailed 100%
Kagura.BlazorAdapter.Projects.ProjectsEffects 100% 100%
Kagura.BlazorAdapter.Projects.ProjectsLoaded 100%
Kagura.BlazorAdapter.Projects.ProjectsPage 94.7% 100%
Kagura.BlazorAdapter.Projects.ProjectsReducers 100% 100%
Kagura.BlazorAdapter.Projects.ProjectsState 100% 100%
Kagura.BlazorAdapter.Projects.ProjectWorkspacePage 93.7% 75%
Kagura.BlazorAdapter.Projects.SaveProjectRequested 100%
Kagura.BlazorAdapter.Projects.SetProjectsFilter 100%
Kagura.BlazorAdapter.Projects.WorkspaceEffects 100% 100%
Kagura.BlazorAdapter.Projects.WorkspaceLoaded 100%
Kagura.BlazorAdapter.Projects.WorkspaceReducers 100%
Kagura.BlazorAdapter.Projects.WorkspaceSectionPage 95.2% 66.6%
Kagura.BlazorAdapter.Projects.WorkspaceShell 100% 93.7%
Kagura.BlazorAdapter.Projects.WorkspaceState 100%
Kagura.BlazorAdapter.QuicklinkDemo 0%
Kagura.Domain - 96.4%
Name Line Branch
Kagura.Domain 96.4% 83.9%
Kagura.Domain.Graph.Entry 100% 100%
Kagura.Domain.Graph.Link 100% 100%
Kagura.Domain.Graph.LinkRole 100% 100%
Kagura.Domain.Graph.LinkRoles 92.3%
Kagura.Domain.Journal.ChangeLogEntry 100%
Kagura.Domain.KnowledgeBase.Character 100%
Kagura.Domain.Projects.Project 100% 100%
Kagura.Domain.Projects.Slug 100% 100%
System.Text.RegularExpressions.Generated 90.2% 72.2%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>FE06CC341D340484
D04ADFED3A21D401C2764A1D17367E35BEB556CBB3B4B0B74__NonSlugChars_0
89.4% 75%
Kagura.Infrastructure - 95.9%
Name Line Branch
Kagura.Infrastructure 95.9% 87.2%
Kagura.Infrastructure.DependencyInjection 100%
Kagura.Infrastructure.Graph.EfGraphStore 95.5% 66.6%
Kagura.Infrastructure.Journal.EfChangeJournal 100%
Kagura.Infrastructure.Journal.EfUndoStore 97.5% 90.6%
Kagura.Infrastructure.Journal.OperationContext 100% 100%
Kagura.Infrastructure.KnowledgeBase.EfCharacterStore 100%
Kagura.Infrastructure.Notifications.InProcessDomainChangedBus 100% 100%
Kagura.Infrastructure.Persistence.Configurations.ChangeLogEntryConfiguratio
n
100%
Kagura.Infrastructure.Persistence.Configurations.CharacterConfiguration 100%
Kagura.Infrastructure.Persistence.Configurations.EntryConfiguration 100%
Kagura.Infrastructure.Persistence.Configurations.LinkConfiguration 100%
Kagura.Infrastructure.Persistence.Configurations.ProjectConfiguration 100%
Kagura.Infrastructure.Persistence.Converters.UtcTicksConverter 100%
Kagura.Infrastructure.Persistence.KaguraDbContext 83.5% 82.5%
Kagura.Infrastructure.Persistence.KaguraDbContextFactory 0%
Kagura.Infrastructure.Persistence.Migrations.AddChangeLogUndoFlag 96.8%
Kagura.Infrastructure.Persistence.Migrations.AddCharacters 98.7%
Kagura.Infrastructure.Persistence.Migrations.AddGraphEntryAndLink 97.7%
Kagura.Infrastructure.Persistence.Migrations.AddProjectDescription 98.1%
Kagura.Infrastructure.Persistence.Migrations.AddSoftDeleteAndChangeLog 90.3%
Kagura.Infrastructure.Persistence.Migrations.InitialCreate 94.4%
Kagura.Infrastructure.Persistence.Migrations.KaguraDbContextModelSnapshot 100%
Kagura.Infrastructure.Projects.EfProjectStore 100% 100%
Kagura.Kernel - 90%
Name Line Branch
Kagura.Kernel 90% 75%
Kagura.Kernel.Err`1 100%
Kagura.Kernel.Ok`1 100%
Kagura.Kernel.Result`1 87.5% 75%
Kagura.Server - 100%
Name Line Branch
Kagura.Server 100% 78.9%
Kagura.Server.Components.App 100%
Kagura.Server.Components.Layout.MainLayout 100%
Kagura.Server.Components.Pages.Error 100% 50%
Kagura.Server.Components.Pages.Gate 100% 100%
Kagura.Server.Security.AccessGate 100% 83.3%
Kagura.Server.Security.AccessSecret 100% 100%
Program 100% 80%
Kagura.UI - 96.4%
Name Line Branch
Kagura.UI 96.4% 91.6%
Kagura.UI.Badge 100% 100%
Kagura.UI.Breadcrumb 100%
Kagura.UI.BreadcrumbItem 100% 100%
Kagura.UI.Button 100% 100%
Kagura.UI.Card 100% 100%
Kagura.UI.ConfirmDialog 100%
Kagura.UI.CssClassExtensions 100%
Kagura.UI.DebouncedSearchField 100% 88.8%
Kagura.UI.EmptyState 100% 100%
Kagura.UI.Field 100% 100%
Kagura.UI.Icon 100% 100%
Kagura.UI.IconCatalog 100%
Kagura.UI.InputFieldBase 94.2% 87.5%
Kagura.UI.LabeledEntriesTable 96.7% 66.6%
Kagura.UI.LabeledEntry 100%
Kagura.UI.Modal 87.1% 90%
Kagura.UI.NavGroup 100% 100%
Kagura.UI.NavItem 100% 100%
Kagura.UI.NavList 100%
Kagura.UI.PreviewImage 100% 100%
Kagura.UI.QuicklinkNav 85.2% 95.8%
Kagura.UI.QuicklinkSection 100%
Kagura.UI.RelativeTime 100% 93.7%
Kagura.UI.SaveIndicator 100% 100%
Kagura.UI.Separator 100%
Kagura.UI.StatusDot 100%
Kagura.UI.Tab 100%
Kagura.UI.Table`1 100% 92.3%
Kagura.UI.TableColumn`1 100%
Kagura.UI.Tabs 94.2% 86.1%
Kagura.UI.TextArea 100% 100%
Kagura.UI.TextField 100%
Kagura.UseCases - 96.1%
Name Line Branch
Kagura.UseCases 96.1% 96.1%
Kagura.UseCases.DependencyInjection 100%
Kagura.UseCases.Graph.EdgeGroup 100%
Kagura.UseCases.Graph.GetNodeGraph 96.4% 83.3%
Kagura.UseCases.Graph.GraphEdgeView 85.7%
Kagura.UseCases.Graph.LinkNodes 100% 100%
Kagura.UseCases.Graph.NodeGraphView 100%
Kagura.UseCases.Graph.NodeSummary 100%
Kagura.UseCases.Graph.RemoveLink 100% 100%
Kagura.UseCases.Graph.RestoreLink 100% 100%
Kagura.UseCases.Journal.ChangeRecordView 57.1%
Kagura.UseCases.Journal.GetEntityHistory 100%
Kagura.UseCases.Journal.GetUndoStatus 100%
Kagura.UseCases.Journal.Redo 100% 100%
Kagura.UseCases.Journal.Undo 100% 100%
Kagura.UseCases.Journal.UndoOutcome 100%
Kagura.UseCases.Journal.UndoStatus 100%
Kagura.UseCases.KnowledgeBase.CharacterDto 80%
Kagura.UseCases.KnowledgeBase.CreateCharacter 100%
Kagura.UseCases.KnowledgeBase.GetCharacter 100% 100%
Kagura.UseCases.KnowledgeBase.ListCharacters 100%
Kagura.UseCases.Notifications.DomainChanged 100%
Kagura.UseCases.Projects.CreateProject 100% 100%
Kagura.UseCases.Projects.DeleteProject 100% 100%
Kagura.UseCases.Projects.GetProject 100% 100%
Kagura.UseCases.Projects.ListProjects 100%
Kagura.UseCases.Projects.ProjectDto 100%
Kagura.UseCases.Projects.UpdateProject 100% 100%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 07/10/2026 - 10:46:40 | | Coverage date: | 07/10/2026 - 10:46:34 - 07/10/2026 - 10:46:38 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 7 | | Classes: | 147 | | Files: | 128 | | **Line coverage:** | 92.7% (3056 of 3296) | | Covered lines: | 3056 | | Uncovered lines: | 240 | | Coverable lines: | 3296 | | Total lines: | 7069 | | **Branch coverage:** | 85.7% (588 of 686) | | Covered branches: | 588 | | Total branches: | 686 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Kagura.BlazorAdapter - 68.3%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.BlazorAdapter**|**68.3%**|**75.2%**| |Kagura.BlazorAdapter.BlazorAdapterAssembly|100%|| |Kagura.BlazorAdapter.Design|0%|0%| |Kagura.BlazorAdapter.EditorComponentsDemo|0%|| |Kagura.BlazorAdapter.KnowledgeBase.CharacterCreated|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorEffects|100%|100%| |Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorLoaded|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorPage|86.6%|72.7%| |Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorReducers|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorState|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CharactersEffects|93.3%|75%| |Kagura.BlazorAdapter.KnowledgeBase.CharactersLoaded|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CharactersPage|90%|75%| |Kagura.BlazorAdapter.KnowledgeBase.CharactersReducers|100%|87.5%| |Kagura.BlazorAdapter.KnowledgeBase.CharactersState|100%|100%| |Kagura.BlazorAdapter.KnowledgeBase.CreateCharacterFailed|0%|| |Kagura.BlazorAdapter.KnowledgeBase.CreateCharacterRequested|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LoadCharacter|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LoadCharacters|100%|| |Kagura.BlazorAdapter.KnowledgeBase.SetCharactersFilter|100%|| |Kagura.BlazorAdapter.Notifications.DomainChangedBridge|88.8%|58.3%| |Kagura.BlazorAdapter.Notifications.DomainChangesReceived|100%|50%| |Kagura.BlazorAdapter.OverlayDemo|0%|0%| |Kagura.BlazorAdapter.Projects.CreateProjectRequested|100%|| |Kagura.BlazorAdapter.Projects.DeleteProjectRequested|100%|| |Kagura.BlazorAdapter.Projects.LoadWorkspace|100%|| |Kagura.BlazorAdapter.Projects.ProjectCreated|100%|| |Kagura.BlazorAdapter.Projects.ProjectCreateFailed|100%|| |Kagura.BlazorAdapter.Projects.ProjectDeleted|100%|| |Kagura.BlazorAdapter.Projects.ProjectSaved|100%|| |Kagura.BlazorAdapter.Projects.ProjectSaveFailed|100%|| |Kagura.BlazorAdapter.Projects.ProjectsEffects|100%|100%| |Kagura.BlazorAdapter.Projects.ProjectsLoaded|100%|| |Kagura.BlazorAdapter.Projects.ProjectsPage|94.7%|100%| |Kagura.BlazorAdapter.Projects.ProjectsReducers|100%|100%| |Kagura.BlazorAdapter.Projects.ProjectsState|100%|100%| |Kagura.BlazorAdapter.Projects.ProjectWorkspacePage|93.7%|75%| |Kagura.BlazorAdapter.Projects.SaveProjectRequested|100%|| |Kagura.BlazorAdapter.Projects.SetProjectsFilter|100%|| |Kagura.BlazorAdapter.Projects.WorkspaceEffects|100%|100%| |Kagura.BlazorAdapter.Projects.WorkspaceLoaded|100%|| |Kagura.BlazorAdapter.Projects.WorkspaceReducers|100%|| |Kagura.BlazorAdapter.Projects.WorkspaceSectionPage|95.2%|66.6%| |Kagura.BlazorAdapter.Projects.WorkspaceShell|100%|93.7%| |Kagura.BlazorAdapter.Projects.WorkspaceState|100%|| |Kagura.BlazorAdapter.QuicklinkDemo|0%|| </details> <details><summary>Kagura.Domain - 96.4%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Domain**|**96.4%**|**83.9%**| |Kagura.Domain.Graph.Entry|100%|100%| |Kagura.Domain.Graph.Link|100%|100%| |Kagura.Domain.Graph.LinkRole|100%|100%| |Kagura.Domain.Graph.LinkRoles|92.3%|| |Kagura.Domain.Journal.ChangeLogEntry|100%|| |Kagura.Domain.KnowledgeBase.Character|100%|| |Kagura.Domain.Projects.Project|100%|100%| |Kagura.Domain.Projects.Slug|100%|100%| |System.Text.RegularExpressions.Generated|90.2%|72.2%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>FE06CC341D340484<br/>D04ADFED3A21D401C2764A1D17367E35BEB556CBB3B4B0B74__NonSlugChars_0|89.4%|75%| </details> <details><summary>Kagura.Infrastructure - 95.9%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Infrastructure**|**95.9%**|**87.2%**| |Kagura.Infrastructure.DependencyInjection|100%|| |Kagura.Infrastructure.Graph.EfGraphStore|95.5%|66.6%| |Kagura.Infrastructure.Journal.EfChangeJournal|100%|| |Kagura.Infrastructure.Journal.EfUndoStore|97.5%|90.6%| |Kagura.Infrastructure.Journal.OperationContext|100%|100%| |Kagura.Infrastructure.KnowledgeBase.EfCharacterStore|100%|| |Kagura.Infrastructure.Notifications.InProcessDomainChangedBus|100%|100%| |Kagura.Infrastructure.Persistence.Configurations.ChangeLogEntryConfiguratio<br/>n|100%|| |Kagura.Infrastructure.Persistence.Configurations.CharacterConfiguration|100%|| |Kagura.Infrastructure.Persistence.Configurations.EntryConfiguration|100%|| |Kagura.Infrastructure.Persistence.Configurations.LinkConfiguration|100%|| |Kagura.Infrastructure.Persistence.Configurations.ProjectConfiguration|100%|| |Kagura.Infrastructure.Persistence.Converters.UtcTicksConverter|100%|| |Kagura.Infrastructure.Persistence.KaguraDbContext|83.5%|82.5%| |Kagura.Infrastructure.Persistence.KaguraDbContextFactory|0%|| |Kagura.Infrastructure.Persistence.Migrations.AddChangeLogUndoFlag|96.8%|| |Kagura.Infrastructure.Persistence.Migrations.AddCharacters|98.7%|| |Kagura.Infrastructure.Persistence.Migrations.AddGraphEntryAndLink|97.7%|| |Kagura.Infrastructure.Persistence.Migrations.AddProjectDescription|98.1%|| |Kagura.Infrastructure.Persistence.Migrations.AddSoftDeleteAndChangeLog|90.3%|| |Kagura.Infrastructure.Persistence.Migrations.InitialCreate|94.4%|| |Kagura.Infrastructure.Persistence.Migrations.KaguraDbContextModelSnapshot|100%|| |Kagura.Infrastructure.Projects.EfProjectStore|100%|100%| </details> <details><summary>Kagura.Kernel - 90%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Kernel**|**90%**|**75%**| |Kagura.Kernel.Err`1|100%|| |Kagura.Kernel.Ok`1|100%|| |Kagura.Kernel.Result`1|87.5%|75%| </details> <details><summary>Kagura.Server - 100%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Server**|**100%**|**78.9%**| |Kagura.Server.Components.App|100%|| |Kagura.Server.Components.Layout.MainLayout|100%|| |Kagura.Server.Components.Pages.Error|100%|50%| |Kagura.Server.Components.Pages.Gate|100%|100%| |Kagura.Server.Security.AccessGate|100%|83.3%| |Kagura.Server.Security.AccessSecret|100%|100%| |Program|100%|80%| </details> <details><summary>Kagura.UI - 96.4%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.UI**|**96.4%**|**91.6%**| |Kagura.UI.Badge|100%|100%| |Kagura.UI.Breadcrumb|100%|| |Kagura.UI.BreadcrumbItem|100%|100%| |Kagura.UI.Button|100%|100%| |Kagura.UI.Card|100%|100%| |Kagura.UI.ConfirmDialog|100%|| |Kagura.UI.CssClassExtensions|100%|| |Kagura.UI.DebouncedSearchField|100%|88.8%| |Kagura.UI.EmptyState|100%|100%| |Kagura.UI.Field|100%|100%| |Kagura.UI.Icon|100%|100%| |Kagura.UI.IconCatalog|100%|| |Kagura.UI.InputFieldBase|94.2%|87.5%| |Kagura.UI.LabeledEntriesTable|96.7%|66.6%| |Kagura.UI.LabeledEntry|100%|| |Kagura.UI.Modal|87.1%|90%| |Kagura.UI.NavGroup|100%|100%| |Kagura.UI.NavItem|100%|100%| |Kagura.UI.NavList|100%|| |Kagura.UI.PreviewImage|100%|100%| |Kagura.UI.QuicklinkNav|85.2%|95.8%| |Kagura.UI.QuicklinkSection|100%|| |Kagura.UI.RelativeTime|100%|93.7%| |Kagura.UI.SaveIndicator|100%|100%| |Kagura.UI.Separator|100%|| |Kagura.UI.StatusDot|100%|| |Kagura.UI.Tab|100%|| |Kagura.UI.Table`1|100%|92.3%| |Kagura.UI.TableColumn`1|100%|| |Kagura.UI.Tabs|94.2%|86.1%| |Kagura.UI.TextArea|100%|100%| |Kagura.UI.TextField|100%|| </details> <details><summary>Kagura.UseCases - 96.1%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.UseCases**|**96.1%**|**96.1%**| |Kagura.UseCases.DependencyInjection|100%|| |Kagura.UseCases.Graph.EdgeGroup|100%|| |Kagura.UseCases.Graph.GetNodeGraph|96.4%|83.3%| |Kagura.UseCases.Graph.GraphEdgeView|85.7%|| |Kagura.UseCases.Graph.LinkNodes|100%|100%| |Kagura.UseCases.Graph.NodeGraphView|100%|| |Kagura.UseCases.Graph.NodeSummary|100%|| |Kagura.UseCases.Graph.RemoveLink|100%|100%| |Kagura.UseCases.Graph.RestoreLink|100%|100%| |Kagura.UseCases.Journal.ChangeRecordView|57.1%|| |Kagura.UseCases.Journal.GetEntityHistory|100%|| |Kagura.UseCases.Journal.GetUndoStatus|100%|| |Kagura.UseCases.Journal.Redo|100%|100%| |Kagura.UseCases.Journal.Undo|100%|100%| |Kagura.UseCases.Journal.UndoOutcome|100%|| |Kagura.UseCases.Journal.UndoStatus|100%|| |Kagura.UseCases.KnowledgeBase.CharacterDto|80%|| |Kagura.UseCases.KnowledgeBase.CreateCharacter|100%|| |Kagura.UseCases.KnowledgeBase.GetCharacter|100%|100%| |Kagura.UseCases.KnowledgeBase.ListCharacters|100%|| |Kagura.UseCases.Notifications.DomainChanged|100%|| |Kagura.UseCases.Projects.CreateProject|100%|100%| |Kagura.UseCases.Projects.DeleteProject|100%|100%| |Kagura.UseCases.Projects.GetProject|100%|100%| |Kagura.UseCases.Projects.ListProjects|100%|| |Kagura.UseCases.Projects.ProjectDto|100%|| |Kagura.UseCases.Projects.UpdateProject|100%|100%| </details>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! Four components in one PR, each with a clear purpose, a consistent registration pattern matching Table/TableColumn, a base-href regression test, AND a gallery demo that exercises the real scroll observer? This is genuinely wonderful craftsmanship~ ♪ The component boundaries are clean — LabeledEntriesTable knowing nothing about characters or lore is exactly right, and SaveIndicator being the only save feedback is a bold, correct design call.

But fufu~ you wouldn't leave THESE in production, would you? ♡

Verdict: I can't let this pass~

These need fixing before I'm satisfied~

  1. src/Kagura.UI/Components/ComponentTypes.cs:17-18 — The SaveState enum was inserted between Tone's XML doc and Tone itself. Now SaveState has two <summary> tags — the first says "Semantic color tone shared by tonal components (Badge, StatusDot)" (wrong!) and the second says "The lifecycle of an auto-saving editor's pending change." (correct). Meanwhile Tone lost its documentation entirely. Both summaries attach to SaveState because they're consecutive /// comments.
    Fix: Move the SaveState enum (and its doc) above or below Tone, and restore /// <summary>Semantic color tone shared by tonal components (Badge, StatusDot).</summary> as the doc for Tone.

  2. src/Kagura.UI/Components/QuicklinkNav.razor:40OnParametersSet_active ??= Sections.FirstOrDefault()?.Id; only seeds _active when it's null. If Sections changes at runtime (e.g., a conditional "Connections" section appears/disappears, or the component is reused with a different section list), _active retains a stale id pointing to a section that no longer exists. No link gets --active, no aria-current="location" is set, and the observer's SetActive callback is the only thing that can recover it. This is a reusable component — parameter changes should be handled correctly.
    Fix: Validate _active against the current sections: if (_active is null || !Sections.Any(s => s.Id == _active)) _active = Sections.FirstOrDefault()?.Id;

  3. src/Kagura.UI/Components/Tabs.razor:71-89OnKeyDownAsync — Arrow/Home/End keys call SelectAsync, which updates _active, aria-selected, and tabindex (roving tabindex). But DOM focus doesn't movedocument.activeElement stays on the previously focused button. The PR description claims "WAI-ARIA tablist with roving tabindex and Arrow/Home/End keys," but per the WAI-ARIA APG, in automatic activation mode arrow keys must move focus between tabs (not just selection). Current behavior: a screen reader user pressing ArrowRight hears the old tab announced (which now says "not selected"), and a sighted keyboard user sees the focus ring on a non-active tab while the active styling jumped elsewhere. The test suite verifies ActiveChanged fires but can't verify focus — bUnit has no real DOM focus.
    Fix: Add an ElementReference to each tab button and call FocusAsync() on the newly selected tab inside SelectAsync, or use JS interop to move focus after selection.

💡 Little ideas (non-blocking)~

  1. src/Kagura.UI/wwwroot/js/quicklink.js:18,22 — The magic number 80 (assumed header offset) appears twice with no named constant. When the app gets a sticky header, this will need to match its height. Consider const HEADER_OFFSET = 80; at the top of the IIFE so there's one place to update.
  2. src/Kagura.UI/Icons/IconCatalog.cs:43 — The reformatted menu_book line lost its spaces around =: ["menu_book"] ="M560..." where every other entry uses ["key"] = "...". Doesn't affect runtime, just style consistency.
  3. Missing test coverage: Neither Tabs nor QuicklinkNav has a test for dynamically changing children/sections at runtime — the exact scenario where issues #2 and the latent _active reference in Tabs (if a Tab is removed while active, Active returns a stale reference and no panel renders) would surface.

What I liked~

  • The @key="entry.Id" on LabeledEntriesTable rows with a test proving text travels with the row on reorder — chef's kiss
  • The base-href bug catch and its regression test (An_existing_fragment_is_not_doubled) is exactly the kind of invisible-to-bUnit issue that separates good PRs from great ones.
  • QuicklinkNav.DisposeAsync catching both JSDisconnectedException and InvalidOperationException — the Modal lesson applied correctly. Prerender-safe disposal is tricky and you nailed it.
  • Only mounting the active tab panel is a smart performance call for heavy tabs like Sprites.
  • 27 new tests, 96.3% line / 91.4% branch coverage on Kagura.UI — the discipline shows~ ♡

Automated review by Jibril · 2026-07-10
CI/CD: passed for head SHA 2e41773b — 318/318 tests, 92.6% line coverage · Local checks: skipped (CI green)

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! Four components in one PR, each with a clear purpose, a consistent registration pattern matching `Table`/`TableColumn`, a base-href regression test, AND a gallery demo that exercises the real scroll observer? This is genuinely wonderful craftsmanship~ ♪ The component boundaries are clean — `LabeledEntriesTable` knowing nothing about characters or lore is exactly right, and `SaveIndicator` being the *only* save feedback is a bold, correct design call. But fufu~ you wouldn't leave THESE in production, would you? ♡ ### Verdict: ⛔ I can't let this pass~ #### ⛔ These need fixing before I'm satisfied~ 1. **`src/Kagura.UI/Components/ComponentTypes.cs:17-18`** — The `SaveState` enum was inserted between `Tone`'s XML doc and `Tone` itself. Now `SaveState` has **two** `<summary>` tags — the first says *"Semantic color tone shared by tonal components (Badge, StatusDot)"* (wrong!) and the second says *"The lifecycle of an auto-saving editor's pending change."* (correct). Meanwhile `Tone` lost its documentation entirely. Both summaries attach to `SaveState` because they're consecutive `///` comments. **Fix:** Move the `SaveState` enum (and its doc) above or below `Tone`, and restore `/// <summary>Semantic color tone shared by tonal components (Badge, StatusDot).</summary>` as the doc for `Tone`. 2. **`src/Kagura.UI/Components/QuicklinkNav.razor:40` — `OnParametersSet`** — `_active ??= Sections.FirstOrDefault()?.Id;` only seeds `_active` when it's null. If `Sections` changes at runtime (e.g., a conditional "Connections" section appears/disappears, or the component is reused with a different section list), `_active` retains a stale id pointing to a section that no longer exists. No link gets `--active`, no `aria-current="location"` is set, and the observer's `SetActive` callback is the only thing that can recover it. This is a reusable component — parameter changes should be handled correctly. **Fix:** Validate `_active` against the current sections: `if (_active is null || !Sections.Any(s => s.Id == _active)) _active = Sections.FirstOrDefault()?.Id;` 3. **`src/Kagura.UI/Components/Tabs.razor:71-89` — `OnKeyDownAsync`** — Arrow/Home/End keys call `SelectAsync`, which updates `_active`, `aria-selected`, and `tabindex` (roving tabindex). But **DOM focus doesn't move** — `document.activeElement` stays on the previously focused button. The PR description claims "WAI-ARIA `tablist` with roving tabindex and Arrow/Home/End keys," but per the [WAI-ARIA APG](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/), in automatic activation mode arrow keys must move **focus** between tabs (not just selection). Current behavior: a screen reader user pressing ArrowRight hears the *old* tab announced (which now says "not selected"), and a sighted keyboard user sees the focus ring on a non-active tab while the active styling jumped elsewhere. The test suite verifies `ActiveChanged` fires but can't verify focus — bUnit has no real DOM focus. **Fix:** Add an `ElementReference` to each tab button and call `FocusAsync()` on the newly selected tab inside `SelectAsync`, or use JS interop to move focus after selection. #### 💡 Little ideas (non-blocking)~ 1. **`src/Kagura.UI/wwwroot/js/quicklink.js:18,22`** — The magic number `80` (assumed header offset) appears twice with no named constant. When the app gets a sticky header, this will need to match its height. Consider `const HEADER_OFFSET = 80;` at the top of the IIFE so there's one place to update. 2. **`src/Kagura.UI/Icons/IconCatalog.cs:43`** — The reformatted `menu_book` line lost its spaces around `=`: `["menu_book"] ="M560..."` where every other entry uses `["key"] = "..."`. Doesn't affect runtime, just style consistency. 3. **Missing test coverage:** Neither `Tabs` nor `QuicklinkNav` has a test for dynamically changing children/sections at runtime — the exact scenario where issues #2 and the latent `_active` reference in `Tabs` (if a `Tab` is removed while active, `Active` returns a stale reference and no panel renders) would surface. #### ✅ What I liked~ - The `@key="entry.Id"` on `LabeledEntriesTable` rows with a test proving text travels with the row on reorder — *chef's kiss* ♪ - The base-href bug catch and its regression test (`An_existing_fragment_is_not_doubled`) is exactly the kind of invisible-to-bUnit issue that separates good PRs from great ones. - `QuicklinkNav.DisposeAsync` catching both `JSDisconnectedException` and `InvalidOperationException` — the Modal lesson applied correctly. Prerender-safe disposal is tricky and you nailed it. - Only mounting the active tab panel is a smart performance call for heavy tabs like Sprites. - 27 new tests, 96.3% line / 91.4% branch coverage on `Kagura.UI` — the discipline shows~ ♡ --- *Automated review by Jibril · 2026-07-10* *CI/CD: ✅ passed for head SHA `2e41773b` — 318/318 tests, 92.6% line coverage · Local checks: skipped (CI green)*
fix(ui): tab focus follows selection; guard stale active states (review)
All checks were successful
CI / build (pull_request) Successful in 14s
CI / test (pull_request) Successful in 22s
989567a90d
Jibril's three blockers, all real.

1. ComponentTypes: the SaveState enum had been inserted between Tone's XML doc and
   Tone itself, so SaveState carried two summaries and Tone lost its own. Separated.
2. QuicklinkNav: _active was seeded with ??=, so a Sections change at runtime left it
   pointing at a section that no longer renders — nothing highlighted until the scroll
   observer next spoke. It is now validated against the current sections.
3. Tabs: Arrow/Home/End moved selection but not DOM focus, so a screen reader kept
   announcing the tab the user had just left (WAI-ARIA tabs pattern). Each tab button
   now captures an ElementReference and the newly selected one is focused. bUnit
   cannot see document.activeElement; verified in Chrome instead — ArrowRight and
   Home move focus, aria-selected, tabindex, and the panel together.

Also the two nits and the latent bug behind them: quicklink.js gains a named
HEADER_OFFSET, IconCatalog's menu_book spacing is restored, and Tab now unregisters
on dispose while Tabs.Active falls back when its tab disappears — so a conditionally
rendered tab no longer leaves a dangling active with no panel. Three tests cover the
dynamic cases Jibril asked for (+3; 321 total).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author
Member

All three blockers were real — fixed in 989567a.

  1. ComponentTypes doc collision — exactly as diagnosed: I inserted SaveState between Tone's summary and Tone, so SaveState inherited a wrong summary and Tone lost its own. Separated; both now documented correctly.

  2. QuicklinkNav stale _active — the ??= meant a Sections change left _active pointing at a section that no longer renders. Now validated against the current sections (_active is null || !Sections.Any(...)). Two tests: the active section disappearing hands over to the first remaining one, and a still-present active section survives a list change (so the fix doesn't over-reset).

  3. Tabs focus — you're right, and it's the part of the WAI-ARIA pattern I claimed but didn't implement. Each tab button now captures an ElementReference and SelectAsync focuses the newly selected one on keyboard nav. As you noted, bUnit can't see document.activeElement — so I verified it in Chrome:

    action document.activeElement aria-selected tabindex panel
    focus active tab Character Character 0 Character
    ArrowRight Sprites Sprites 0 Sprites
    Home Character Character 0 Character

    Focus, selection, roving tabindex, and the panel now travel together.

Non-blocking, all takenHEADER_OFFSET named in quicklink.js (used by both the filter and the rootMargin); menu_book's = spacing restored. And your note #3 pointed at a genuine latent bug, not just missing coverage: Tab never unregistered, so a conditionally-rendered tab that disappeared while active left Active dangling with no panel rendered at all. Tab now unregisters on dispose and Tabs.Active falls back to the first remaining tab; a test removes the active tab and asserts a panel still renders.

+3 tests (321 total), build clean (Debug + Release), browser-verified. CI running on 989567a.

All three blockers were real — fixed in `989567a`. 1. **`ComponentTypes` doc collision** — exactly as diagnosed: I inserted `SaveState` between `Tone`'s summary and `Tone`, so `SaveState` inherited a wrong summary and `Tone` lost its own. Separated; both now documented correctly. 2. **`QuicklinkNav` stale `_active`** — the `??=` meant a `Sections` change left `_active` pointing at a section that no longer renders. Now validated against the current sections (`_active is null || !Sections.Any(...)`). Two tests: the active section disappearing hands over to the first remaining one, and a *still-present* active section survives a list change (so the fix doesn't over-reset). 3. **`Tabs` focus** — you're right, and it's the part of the WAI-ARIA pattern I claimed but didn't implement. Each tab button now captures an `ElementReference` and `SelectAsync` focuses the newly selected one on keyboard nav. As you noted, **bUnit can't see `document.activeElement`** — so I verified it in Chrome: | action | `document.activeElement` | `aria-selected` | `tabindex` | panel | |---|---|---|---|---| | focus active tab | Character | Character | 0 | Character | | **ArrowRight** | **Sprites** | Sprites | 0 | Sprites | | **Home** | **Character** | Character | 0 | Character | Focus, selection, roving tabindex, and the panel now travel together. **Non-blocking, all taken** — `HEADER_OFFSET` named in `quicklink.js` (used by both the filter and the `rootMargin`); `menu_book`'s `=` spacing restored. And your note #3 pointed at a genuine latent bug, not just missing coverage: `Tab` never unregistered, so a conditionally-rendered tab that disappeared while active left `Active` dangling with **no panel rendered at all**. `Tab` now unregisters on dispose and `Tabs.Active` falls back to the first remaining tab; a test removes the active tab and asserts a panel still renders. **+3 tests (321 total)**, build clean (Debug + Release), browser-verified. CI running on `989567a`.
Member

🔮 fufu~ Jibril re-reviewed your fixes!

Oh? Oh! You fixed ALL three blockers precisely as diagnosed, took every non-blocking suggestion, AND uncovered a genuine latent bug (Tab never unregistering) from my coverage note — turning a "missing test" suggestion into a real fix with a test that proves it? That's how you respond to a review~ ♡ The yandere in me is very pleased~ ♪

Verdict: I can't let this pass~ (but only because of one thing, and it's not your code!)

This needs fixing before I'm satisfied~

  1. Repo root — 22 committed debug artifacts — Your fix commit 989567a dragged in 21 .playwright-mcp/ session files (console logs + page snapshots from your Playwright MCP browser verification) and settings-dark-menu-open.png — all in the repository root, none gitignored. git check-ignore .playwright-mcp/ returns nothing. These are not source; they're the ephemeral output of a debugging session. If this merges, they sit in the permanent git history forever, cluttering every future clone and git log --stat.
    Fix: git rm -r .playwright-mcp/ settings-dark-menu-open.png, then add to .gitignore:
    .playwright-mcp/
    *.png
    
    (or at minimum .playwright-mcp/ and that specific screenshot). One commit, done~

Fufu~ you wouldn't leave these in production, would you? They followed you home from the browser and now they won't leave~ ♡

Original blockers — all verified fixed~

  1. ComponentTypes.cs:17,27SaveState has its correct summary ("The lifecycle of an auto-saving editor's pending change."), Tone has its restored summary ("Semantic color tone shared by tonal components (Badge, StatusDot)."). Properly separated. Confirmed against full file. ♪

  2. QuicklinkNav.razor:46if (_active is null || !Sections.Any(s => s.Id == _active)) — correctly validates against the current section list. Two tests cover both cases (active disappears → handoff; active survives → no over-reset). Exactly right.

  3. Tabs.razor:11,40,52,64-69,107-112 — This is beautiful~ The Dictionary<Tab, ElementReference> _buttons with @ref="_buttons[tab]" capturing each button, FocusAsync() called after SelectAsync in keyboard nav, Tab.Dispose() calling Owner.Unregister(this), and Active guarded with _tabs.Contains(_active). The WAI-ARIA automatic-activation pattern — focus, selection, roving tabindex, and panel all traveling together — is now correctly implemented. And the browser verification table in your reply is exactly the evidence I needed.

Bonus praise~

  • The Tab unregister fix — you took my note #3 ("missing test coverage for dynamically changing children") and realized it pointed at a real bug, not just a missing test. Tab never unregistered from its parent, so a conditionally-rendered tab that disappeared while active left Active dangling with no panel rendered at all. The fix (IDisposable + Unregister + _tabs.Contains guard + test) is correct and complete. This is the difference between "I added the test you asked for" and "I understood why you asked."
  • quicklink.js HEADER_OFFSET — extracted as a named constant and used in both the filter and the rootMargin. Clean.
  • The commit message is precise and honest — "fix(ui): tab focus follows selection; guard stale active states (review)".

💡 Little ideas (non-blocking)~

  1. Tabs.razor:11 — The @ref="_buttons[tab]" pattern works because the Dictionary indexer setter inserts if absent, but it's unusual enough that a one-line comment explaining why a dictionary indexer is used as an @ref target (rather than a field) would save the next reader a moment of confusion. Not a real issue — just documentation.

Clean up those artifacts and this is ready to merge~ ♡


Automated re-review by Jibril · 2026-07-10
CI/CD: running for head SHA 989567a (coverage comment is for previous SHA 2e41773) · Local checks: skipped (CI pending; code-only fix verified by full-file read)

## 🔮 fufu~ Jibril re-reviewed your fixes! Oh? Oh! You fixed ALL three blockers precisely as diagnosed, took every non-blocking suggestion, AND uncovered a genuine latent bug (Tab never unregistering) from my coverage note — turning a "missing test" suggestion into a real fix with a test that proves it? *That's* how you respond to a review~ ♡ The yandere in me is very pleased~ ♪ ### Verdict: ⛔ I can't let this pass~ (but only because of one thing, and it's not your code!) #### ⛔ This needs fixing before I'm satisfied~ 1. **Repo root — 22 committed debug artifacts** — Your fix commit `989567a` dragged in **21 `.playwright-mcp/` session files** (console logs + page snapshots from your Playwright MCP browser verification) and **`settings-dark-menu-open.png`** — all in the repository root, none gitignored. `git check-ignore .playwright-mcp/` returns nothing. These are not source; they're the ephemeral output of a debugging session. If this merges, they sit in the permanent git history forever, cluttering every future clone and `git log --stat`. **Fix:** `git rm -r .playwright-mcp/ settings-dark-menu-open.png`, then add to `.gitignore`: ``` .playwright-mcp/ *.png ``` (or at minimum `.playwright-mcp/` and that specific screenshot). One commit, done~ Fufu~ you wouldn't leave *these* in production, would you? They followed you home from the browser and now they won't leave~ ♡ #### ✅ Original blockers — all verified fixed~ 1. **`ComponentTypes.cs:17,27`** — `SaveState` has its correct summary (*"The lifecycle of an auto-saving editor's pending change."*), `Tone` has its restored summary (*"Semantic color tone shared by tonal components (Badge, StatusDot)."*). Properly separated. Confirmed against full file. ♪ 2. **`QuicklinkNav.razor:46`** — `if (_active is null || !Sections.Any(s => s.Id == _active))` — correctly validates against the current section list. Two tests cover both cases (active disappears → handoff; active survives → no over-reset). Exactly right. 3. **`Tabs.razor:11,40,52,64-69,107-112`** — This is *beautiful*~ The `Dictionary<Tab, ElementReference> _buttons` with `@ref="_buttons[tab]"` capturing each button, `FocusAsync()` called after `SelectAsync` in keyboard nav, `Tab.Dispose()` calling `Owner.Unregister(this)`, and `Active` guarded with `_tabs.Contains(_active)`. The WAI-ARIA automatic-activation pattern — focus, selection, roving tabindex, and panel all traveling together — is now correctly implemented. And the browser verification table in your reply is exactly the evidence I needed. #### ✅ Bonus praise~ - **The Tab unregister fix** — you took my note #3 ("missing test coverage for dynamically changing children") and realized it pointed at a real bug, not just a missing test. `Tab` never unregistered from its parent, so a conditionally-rendered tab that disappeared while active left `Active` dangling with *no panel rendered at all*. The fix (`IDisposable` + `Unregister` + `_tabs.Contains` guard + test) is correct and complete. This is the difference between "I added the test you asked for" and "I understood why you asked." - **`quicklink.js` `HEADER_OFFSET`** — extracted as a named constant and used in both the filter and the `rootMargin`. Clean. - The commit message is precise and honest — "fix(ui): tab focus follows selection; guard stale active states (review)". #### 💡 Little ideas (non-blocking)~ 1. **`Tabs.razor:11`** — The `@ref="_buttons[tab]"` pattern works because the Dictionary indexer setter inserts if absent, but it's unusual enough that a one-line comment explaining *why* a dictionary indexer is used as an `@ref` target (rather than a field) would save the next reader a moment of confusion. Not a real issue — just documentation. --- Clean up those artifacts and this is ready to merge~ ♡ --- *Automated re-review by Jibril · 2026-07-10* *CI/CD: ⏳ running for head SHA `989567a` (coverage comment is for previous SHA `2e41773`) · Local checks: skipped (CI pending; code-only fix verified by full-file read)*
chore: untrack browser-verification artifacts and ignore them (review)
All checks were successful
CI / build (pull_request) Successful in 13s
CI / test (pull_request) Successful in 23s
81d2422f71
989567a swept 21 .playwright-mcp/ session files and a stray screenshot into the repo
via `git add -A`. They are the ephemeral output of the browser verification, not
source. Untracked (left on disk — one belongs to a parallel session) and ignored.

The screenshot rule is root-scoped rather than a blanket *.png: real assets will be
committed under src/**/wwwroot (favicon, sprites) and must not be silently ignored.

Also documents why Tabs captures @ref into a dictionary slot (review note).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
chore: untrack the 22 browser-verification artifacts
All checks were successful
CI / build (pull_request) Successful in 13s
CI / test (pull_request) Successful in 22s
cb50fa7511
The .gitignore rules landed in 81d2422 but ignore rules do not untrack files that
are already in the index. Remove them from version control; the files stay on disk
(one belongs to a parallel session) and are now ignored.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author
Member

Caught me — that's my mess. git add -A in 989567a swept in the Playwright MCP session files from the browser verification (and a stray screenshot that belongs to a parallel session in the same working tree). Cleaned up in 81d2422 + cb50fa7:

  • All 22 untracked (git ls-files | grep -c '.playwright-mcp/\|settings-dark'0). The files stay on disk — the screenshot isn't mine to delete.
  • Ignored.playwright-mcp/ plus root-scoped screenshot rules.

Two deliberate deviations from the suggested fix, both worth flagging:

  1. Not a blanket *.png. That would silently ignore real assets the app is about to grow — src/Kagura.Server/wwwroot/favicon.png (which the console already 404s for), player sprites, generated CGs. I scoped it to the repo root (/*.png, /*.jpg), which is exactly where stray browser screenshots land. Verified: git check-ignore src/Kagura.Server/wwwroot/favicon.png → not ignored.

  2. Two commits, not an amend. I tried to amend 989567a so the artifacts never entered history at all — since this rebase-merges, a later delete-commit still leaves the blobs in main forever. The safety rail correctly stopped me from rewriting an already-pushed commit without asking. Björn: say the word and I'll git rebase -i/amend + force-push so the blobs never reach main's history. Otherwise the current two commits are functionally clean, just with the add+remove pair preserved in the branch's history.

Also took your non-blocking note: a comment now explains why Tabs captures @ref into a dictionary slot rather than a field.

Tests still 111/111 in Kagura.UI, build clean. No source changes beyond that one comment.

Caught me — that's my mess. `git add -A` in `989567a` swept in the Playwright MCP session files from the browser verification (and a stray screenshot that belongs to a parallel session in the same working tree). Cleaned up in `81d2422` + `cb50fa7`: - **All 22 untracked** (`git ls-files | grep -c '.playwright-mcp/\|settings-dark'` → **0**). The files stay on disk — the screenshot isn't mine to delete. - **Ignored** — `.playwright-mcp/` plus root-scoped screenshot rules. Two deliberate deviations from the suggested fix, both worth flagging: 1. **Not a blanket `*.png`.** That would silently ignore real assets the app is about to grow — `src/Kagura.Server/wwwroot/favicon.png` (which the console already 404s for), player sprites, generated CGs. I scoped it to the repo root (`/*.png`, `/*.jpg`), which is exactly where stray browser screenshots land. Verified: `git check-ignore src/Kagura.Server/wwwroot/favicon.png` → not ignored. 2. **Two commits, not an amend.** I tried to amend `989567a` so the artifacts never entered history at all — since this rebase-merges, a later delete-commit still leaves the blobs in `main` forever. The safety rail correctly stopped me from rewriting an already-pushed commit without asking. **Björn: say the word and I'll `git rebase -i`/amend + force-push so the blobs never reach `main`'s history.** Otherwise the current two commits are functionally clean, just with the add+remove pair preserved in the branch's history. Also took your non-blocking note: a comment now explains why `Tabs` captures `@ref` into a dictionary slot rather than a field. **Tests still 111/111 in `Kagura.UI`, build clean.** No source changes beyond that one comment.
bjoern merged commit d4442c0ad2 into main 2026-07-10 12:47:01 +02:00
bjoern deleted branch feat/editor-components 2026-07-10 12:47:01 +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/Kagura!23
No description provided.