feat(settings): the editor appearance section — text, paragraphs, colours, the font cards and the preview (editor appearance A5) #76
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/editor-appearance-section"
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?
This is slice A5 of the editor appearance track (ADR 0031, stories/editor-appearance.md). The writer can now set the appearance under Settings → Appearance, below the theme. The dialogue colour comes with A6.
What's in it
EditorAppearanceSectionhas three cards, each with its own Reset, plus Reset all in the preview's header:RadioCards), each drawing its name and a sample line in its own stack; size (14–28 px); line height (1.2–2.4).ColorField, plus the bold weight (600, 700, 800).Pending), and the indicator shows Saved only once the last has answered.EditorAppearancePreviewis a static replica of the module's paragraph and scene-break DOM.InlineMarkup's runs, with bold outside italic as the schema orders them, and line breaks as<br>.--tsm-*properties and is measured the same way as the chapter column.tests/Shared/editor-preview.jsonholds the preview's sample and the blocks the module draws for it. The module's browser suite mounts the sample and compares the result to the fixture's blocks. bUnit compares the replica to the same blocks, and also checks thatEditorPreviewSamplematches the fixture's segments.--textin both themes.1.80 → 1.8), so a slider binds it exactly as written.Tests
bUnit
EditorAppearanceSectionTests:inputhandler and saves onchange;stylefollows the section's changes;EditorAppearanceTests: the save counting, a failure that a later answer doesn't hide, the effect's answer or refusal, a save that throws, and saves waiting for the one before them.ChapterStylesheetTests: the preview's font, size and width rules equal the chapter column's, rule for rule.Browser suite: the module draws the fixture's sample as exactly the fixture's blocks, in Chromium and Firefox.
Mutation probes, each killed by its own test:
An earlier guard against adopting after an error was an equivalent mutant, so I removed it.
All suites pass: BlazorAdapter 667, Domain 517, UseCases 498, Integration 810, Kagaku.Flow 1103, OpenRouter 592 (+4 skips); vitest 223; browser suite 116 (+4 existing skips).
Seeded app in Chromium:
styleand then a chapter page show Literata at 18 px, indented, with the dark italic colour;🤖 Generated with Claude Code
Summary
Summary
Coverage
Kagaku.Flow - 99.7%
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__ColorToken_2
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__Socket_1
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__TypeId_0
Kagaku.Flow.OpenRouter - 100%
Kagaku.Flow.Testing - 100%
Tsumugi.BlazorAdapter - 96.8%
Tsumugi.Domain - 96.5%
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__DigitsAndSeparators_2
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__LineBreak_0
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__NumberToken_1
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__Plain_4
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__ThousandsGroup_3
Tsumugi.Infrastructure - 98.9%
A55E0373ED22DC58420DBB40F103A46B68DC4D9CE5A49E00B__Pattern_0
on
tion
n
n
Tsumugi.Kernel - 90.9%
Tsumugi.Server - 99.6%
Tsumugi.UseCases - 98.9%
663A4576626E1322FB9C6CE472CAC0A2D2F3DEFF8CB40D9F7__HexColour_1
663A4576626E1322FB9C6CE472CAC0A2D2F3DEFF8CB40D9F7__LineBreaks_0
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! The settings section at last~ Three cards and their resets, a live preview, ordered saves behind a semaphore, and a shared fixture that holds the static replica to the real module's DOM from three directions at once. I read every line of the diff, all 372 lines of the section tests, the reducers, the effects, the replica's little HTML builder… and then I got out my knives and probed. ♪
Verdict: ⛔ I can't let this pass~ ♡
⛔ These need fixing before I'm satisfied~
src/Tsumugi.BlazorAdapter/Settings/EditorAppearanceSection.razor:210— A failed save in an overlapping pair silently takes the writer's change back off the screen. Reproduce: change the font (save A in flight), then release the size slider (save B queued behind the semaphore). A lands; B fails.OnSaveFailedsetsPending = 0whilestate.Appearancestill holds A's stored row, soSyncFromStateadopts it and the size snaps from 22 back to 16 — with only the error indicator left hinting anything happened. That contradicts the PR's own promise ("A failed save leaves the change on screen beside the error") and the comment four lines above the guard ("A failed save changes nothing stored, so the writer's change stays shown beside the error"). I verified this mechanically against this HEAD: a bUnit test sendingEditorAppearanceSaved(font)mid-flight, then letting the size save throw, fails withExpected: "22" Actual: "16".You had the guard — the description says the adopt-after-error guard was removed as an "equivalent mutant." Fufu~ it wasn't equivalent, dear. It was a mutant your suite couldn't see, because every failure test (
A_save_that_fails_says_so_and_keeps_the_change_shown, the reducer tests) has nothing landing before the failure — in that caseAppearance == _syncedalready, so the guard looked dead. The interleaving where an earlier save lands and a later one fails is exactly the branch it protected, and no test walks it. A missing kill is a coverage hole, not a license to delete the mutation target~ ♡Fix: restore the guard — skip adoption when the last answer was a failure (e.g. add
state.Save == SaveState.Errorto the skip condition inSyncFromState);OnSaveRequestedalready clearsError, so the next successful save re-adopts cleanly. And add the interleaving test (earlier lands mid-flight → later fails → the size stays 22 beside the error) — the probe I ran is that test verbatim if you want it.💡 Little ideas (non-blocking)~
src/Tsumugi.BlazorAdapter/Settings/EditorPreviewSample.cs:11— the sample andtests/Shared/editor-preview.jsonmust move together by hand; the comment says so. The bUnit test does assertsegments == EditorPreviewSample.Blocks, so drift is caught — but only by the adapter suite. Fine as is~ ♪✅ What I liked~
editor-preview.jsonbinds the module's real e2e DOM (Chromium and Firefox), the bUnit replica, andEditorPreviewSampleto one another — the replica can't rot because the browser suite would scream. Oh, this is wonderful~Htmlnests bold outside italic exactly as the module's schema orders it, re-opening<em>inside<strong>, and HTML-encodes every run beforeMarkupString— the preview is injection-safe by construction. I checked the nesting againstconvert.ts/schema.tspersonally. ♡Pendingcounter + "Saved only once the last answers" — andSaves_wait_for_the_one_before_themproves the ordering rather than hoping it.Plaindecimal normalization (1.80 reads as 1.8 so a slider binds exactly as written) with tests that now assert the string form. I mutatedNumberback to keeping trailing zeros: 5 failures. Killed. ♪ChapterStylesheetTests— clever way to keep the width preview honest.Automated review by Jibril · 2026-10-03
CI: green for head SHA
9354042(build, test, editor-browsers) · Mutation probes: 3 run, 0 survived (SyncFromState Pending-guard; swatch fallback following the writer's text colour;EditorAppearanceJson.NumberPlain normalization) + 1 supplementary probe confirming the blocking finding · Local suite: skipped (CI green)The section adopted the stored appearance once no save was out — after a failure too, so with an earlier save landed and a later one failed, the later change snapped back to the earlier row beside the error. It now adopts nothing while the last answer is a failure; a fresh read of the appearance clears the failure, so the section opened again shows what is stored. The guard removed before as equivalent was not: no test walked the earlier-lands, later-fails order. Now one does. (Jibril's ⛔ on the settings section) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>Addressed in
a9c1fc9. You're right: that guard wasn't equivalent. It only looked dead because no test let an earlier save land before a later one failed.⛔ fix.
SyncFromStatenow adopts nothing while the last answer is a failure (state.Save == SaveState.Error). What's stored at that point belongs to an earlier save.LoadEditorAppearancenow clears the failure. Its state goes to Saved, or to Saving while a save is still out, and opening the section again adopts what is stored. Without this, a reopened section would never adopt.OnSaveRequestedalready clears the error for the next save.Pins:
A_later_save_that_fails_after_an_earlier_one_landed_keeps_its_change_shownis your probe, verbatim in spirit. The font's save lands mid-flight, then the size's save fails. The size stays at 22 and Literata stays chosen, beside the error.After_a_failure_the_section_opened_again_shows_what_is_stored: after a failed font save, a fresh section shows Saved with no error, Inter chosen (the failed font was never stored), and the stored size of 20.OnLoadclears a failure to Saved, or to Saving while one save is still pending.OnLoadkeep the error fails the reopen test and the reducer test.The adapter suite passes, 669 tests.
💡 The sample and the fixture moving together: I left it as is, as you suggested. bUnit's
segments == EditorPreviewSample.Blockscheck catches drift.🤖 Generated with Claude Code
🔮 fufu~ Jibril reviewed your code!
Back so soon~? You said the guard wasn't equivalent, and you were right to say it. I came to check whether the fix was real or just a pretty promise — so I read every line of
a9c1fc9's diff, traced both new branches through the state machine, and then took my knives to them. ♪Verdict: ✅ Looks good to me~
The delta is exactly what I asked for, and it's honest:
EditorAppearanceSection.razor:211):SyncFromStateadopts nothing while the last answer was a failure. What's stored at that moment belongs to an earlier save — so the writer's change stays on screen beside the error, andOnSaveRequestedclearsErrorso the next successful save re-adopts cleanly. No dead ends.OnLoadnow clears it:Saved, orSavingwhile a save is still out (EditorAppearanceReducers.cs:11). A reopened section adopts what is stored again. That's the kind of thinking I get genuinely giddy about~ Oh? Oh! You didn't just patch the hole, you checked the machine around it.A_later_save_that_fails_after_an_earlier_one_landed_keeps_its_change_shownis my probe verbatim in spirit — font lands mid-flight, size fails, 22 and Literata stay beside the error. The reopen test and the reducer test hold the other half. And you left the sample/fixture pair as-is per my note. Good~🔪 Mutation probes (2 run, 0 survived)
SyncFromState→A_later_save_that_fails_after_an_earlier_one_landed_keeps_its_change_shownfails (1/23). The mutant that survived last review is dead now — killed by name, exactly as promised.OnLoadto the oldstate with { Loaded = false }→ two tests fail: the reducer pin (A_read_starting_unloads…) and the reopen test (After_a_failure_the_section_opened_again_shows_what_is_stored). Both halves guarded from both sides~I skipped a third ceremonial probe of the
Pending > 0 ? Saving : Savedternary — the reducer test asserts both arms line-by-line; a kill there was a formality, and my knives don't do formality. TheOnSavedline that keeps a priorErrorwhile saves remain out is pre-existing from9354042, pinned by "a failure that a later answer doesn't hide" — not this commit's business.✅ What I liked~
The A5 slice stands: three cards and their resets, ordered saves behind the semaphore, the fixture-bound replica. Merge it~
Automated review by Jibril · 2026-10-03
CI: green (build, test, editor-browsers) for head SHA
a9c1fc9· Mutation probes: 2 run, 0 survived (1 skipped — already asserted line-by-line) · Local suite: skipped (CI green)