feat(editor): the stored editor appearance, applied to the chapter's column (editor appearance A3) #72
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/editor-appearance-stored"
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 A3 of the editor appearance track (ADR 0031, stories/editor-appearance.md). The appearance is now stored, read, and applied to the chapter page. The Settings section to edit it is A5. Until then, nothing is stored, so nothing changes.
What's in it
editor.appearance(SettingKeys) is oneAppSettingrow holding a versioned JSON object with only the values the writer set, for example{"v":1,"size":18,"italic":{"dark":"#9c9ca6"}}. The stored spellings never change (EditorAppearanceJson).GetEditorAppearance) is lenient, value by value. A number is clamped to its range and rounded to its step. A missing value, one of the wrong type, an unknown choice, or a colour that is not#rrggbbreads as unset. A newer version reads the values this code knows.SaveEditorAppearance) is strict. It refuses a value off its range or step, an unknown choice, or a malformed colour, and writes nothing. Saving with nothing set removes the row.EditorAppearanceCssbuilds the column's--tsm-*declarations from typed values only. It writes one declaration per value that is set and none otherwise, so the stylesheet's fallbacks (today's look) hold:rem(18 →1.125rem).--tsm-gap: 0plus the indent, 1.5 em by default.--tsm-measureinch.light-dark(). An unset side isvar(--text)for the text colour andcurrentColorfor italic, bold and dialogue.EditorAppearanceState(Fluxor): the chapter page dispatchesLoadEditorAppearanceevery time it opens and shows the column only once that read has landed. The prose never changes its look after it appears, and a change made in Settings or in another tab shows on the next chapter page. A read that throws lands today's look first, so the chapter still opens, then goes on to the circuit's error containment..chapter-prosecarries the prose's font and size so that a width counts the prose's characters: Nch, plus the gutter column, its gap and the text padding.inline-size: 100%keeps it from shrinking, which a flex item with auto margins otherwise would..chapter-prose__bodysets the app's own font back for everything inside: the header, the selection menu and the instruction dialog. The editor surface sets the prose font on itself.Tests
EditorAppearanceJsonTests(71):18.5 → 19,0.55 → 0.6);EditorAppearanceTests:ChapterPageTests:.chapter-prose__body, both elements carry the scope attribute their rules match through, and there is nostylewhile nothing is stored.style, and the header keeps 14 px Inter.0then30px;🤖 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 my~ The whole A3 slice in one clean cut — a versioned row, a lenient reader, a strict saver, and a formatter that speaks only in typed values. I read every line hunting for the seam where something a writer typed could slither into a stylesheet... and found nothing. How thrilling~ ♡
Verdict: ✅ Looks good to me~
What I verified myself, not just read:
GetEditorAppearance/SaveEditorAppearancemirrorGetLlmDefaults/SaveLlmDefaults: constructor-injectedIAppSettingsStore,TimeProvideron the save,Result<T>refusals with field-shaped messages,Enum.IsDefinedguards for hand-cast enums, DI in the same block. The Fluxor slice followsContextPreviewState/DefaultsSettingsStateconventions, and the effect routes throughEffectCalls.CallAsyncwith a settle action — a throwing read lands today's look and the chapter still opens. Exactly the ADR's "no restyle once the text shows."EditorAppearanceCsswrites has a consumer insurface.cssof the right kind: unitless--tsm-leading(my #71 calc note honored~), rem size,chmeasure,light-dark()pairs withvar(--text)/currentColorunset sides. The column'smax-inline-size: calc(Nch + 1.5em + 3 × var(--space-2))matches.tsm-paragraph's grid exactly — gutter 1.5em, one gap, two paddings. Every em accounted for.b6252c5: Release build 0w/0e; all six suites digit-exact vs your table — Domain 517, UseCases 498, BlazorAdapter 625, Integration 799, Kagaku.Flow 1103, OpenRouter 592 (+4 skips). (One first-runDocumentsStoreFailureTestsWaitForAssertion timeout under cold-build load — 19/19 green in isolation and in the full re-run; the known flake, untouched by this PR.)git diff --quiet): rem divisor 16→12 (6 red), step rounding AwayFromZero→ToEven (exactly the18.5→19and0.55→0.6rows),var(--text)→currentColorunset side (1 red),OffRangestep check dropped (exactly the 3 off-step refusal rows), theLoadedgate removed (exactly the gate test), weight tie flipped to heavier (both 650/750 rows),Normalizedropped from Save (1 red), indent-default fallback dropped (1 red). Your tests bite, fufu~ ♡💡 Little ideas (non-blocking)~
OnInitializedreading afresh per page instance is airtight today (the route reuses the instance only chapter→chapter, and a Settings visit destroys it), but that reasoning lives in this review, not the code. A sentence in the comment beside it would spare A5 — or any future route-parameterized variant — from rediscovering whyOnParametersSetwas not the home.SizeDefault,LeadingDefault,GapDefault,BoldWeightDefaultwait for A5's pickers, exactly like #65'sActiveGraphIdwaited for 10b. Fine by me; when A5 lands, pointing those constants at their consumers would make the "promised, not dead" state grep-able.✅ What I liked~
light-dark(var(--text), #c8c8d0)for an unset side is the correct semantics — the unset side follows the theme, not a frozen hex. The e2ecolor-schemetests pin it both ways.indentin a hand-edited row costs the writer one value, never the editor. Thev-skip keeps newer rows forward-compatible. Quietly excellent.1.000000000000000000000000000000000mnormalize-divisor dropping trailing zeros in both JSON and CSS, tested at1.80 → 1.8and0.250 → 0.25— the kind of small knife I approve of~ ♪ReadGate: toolbar visible, zero mounts, exactstyleafter release — the "prose never changes its look after it appears" criterion pinned mechanically, not cosmetically.Automated review by Jibril · 2026-10-03
CI/CD: passed for head
b6252c5(coverage bot rev #1 — every changed class 100% line; Css/Json/Save 100% branch) · Local checks: build 0w/0e, 6 suites digit-exact, 8/8 mutation probes killed🔮 fufu~ Jibril reviewed your code!
Oh? Oh! The whole A3 slice in one clean cut — a versioned lenient-read/strict-save setting, a typed-values-only formatter, and a chapter page that refuses to show the prose until it knows how it should look~ That last one is exactly the ADR 0031 "no restyle once the text shows" promise, and you tested the gate itself. I got genuinely giddy reading
EditorAppearanceJson— clamping, step-rounding, tie-to-lighter weights,1e999reading as unset… ♡Verdict: ⛔ I can't let this pass~ ♡
One thing. Just one. But you know how I am about consistency~
⛔ These need fixing before I'm satisfied~
src/Tsumugi.BlazorAdapter/Settings/EditorAppearanceState.cs:31-49— the reducers and the effect live inside the state file. Every other Fluxor feature in the adapter — all seven of them — splits this:DefaultsSettingsState.cs+DefaultsSettingsReducers.cs+DefaultsSettingsEffects.cs, same forProviderSettings*,BackupsSettings*,StoryGraphs*,Workspace*,ManuscriptEffects,HistoryEffects. Yours is the only file insrc/Tsumugi.BlazorAdapterwith aReducerMethodorEffectMethodoutside a*Reducers.cs/*Effects.cs(I grepped — it is literally the only one). State + actions together is fine, mind you —StoryGraphsState.cskeeps its four action records in the state file — but reducers and effects belong in their own files like the three siblings in your own folder. A tiny feature is not an exemption; the smallest sibling feature still splits. Fufu~ you wouldn't leave THIS one inconsistency in the codebase, would you? ♡Fix: split into
EditorAppearanceState.cs(state + the two action records),EditorAppearanceReducers.cs,EditorAppearanceEffects.cs— pure file moves, zero behavioral change, matching theDefaultsSettings*trio.💡 Little ideas (non-blocking)~
EditorAppearanceJson.cs:186(Number) — theTryGetValue<decimal>-returns-false arm (a JSON number too big fordecimal, e.g.{"size":1e999}) is the one read branch no test exercises; the wrong-kind theory rows all fail theJsonValueKindcheck first. I probe-verified it behaves correctly — reads as unset, lenient as promised — so this is a pin, not a bug: two overflow rows (1e999, a 40-digit integer) inA_value_of_the_wrong_kind_reads_as_unsetwould close it.ChapterPage.razor.css:18— the3 * var(--space-2)term of the measure calc is pinned by no automated test (bunit can't see CSS geometry; the module'ssurface.pw.tsdoesn't cover the wrapper). Your Chromium numbers in the body are the only pin — arithmetically consistent (55ch + 1.5em gutter + grid gap + two text paddings, matching.tsm-paragraph's grid exactly). House practice has no adapter-level e2e, so this is consistent, not a deviation — just noting where the contract's only witness lives.✅ What I liked~
EditorAppearanceCssbuilds only from typed values — fonts by id lookup, numbers invariant, colours already validated#rrggbb. The injection surface is zero.--tsm-leadingstays unitless (pinned by your1.80 → 1.8test) — my #71 note's gutter calc survives contact with real storage. Thank you for honoring the contract~calc(measure + 1.5em + 3 × space-2)= the gutter column + the grid gap + both text paddings from.tsm-paragraph— I traced every term againstsurface.css.b-*present on both elements) pin that the scoped CSS actually matches, not just that the elements exist. Sneaky and wonderful~Loadedgate dropped → the held-back-read test red on its own pin. Your tests bite exactly where they claim to.--tsm-dialoguestored but not yet consumed is correct — A6's decoration plugin reads it; storing the vocabulary now means no row-format change later.One file split and this is a beautiful slice. Make it consistent with its siblings and I'll be all smiles~ ♡
Automated review by Jibril · 2026-10-03
CI/CD: absent for head SHA
b6252c5(PR minutes old, no coverage bot yet) · Local checks: full build 0w/0e; all 6 suites pass digit-exact vs body table; 4 mutation probes + 1 overflow scratch probe, tree verified pristine after🔮 fufu~ Jibril reviewed your code!
Back so soon~? A single commit on little cat feet: three comment lines above
OnInitialized, closing my round-1 💡 #1. But fufu~ documentation deserves the same skepticism as code — a comment that lies is worse than no comment at all. So I checked every claim against the machine ♡Verdict: ✅ Looks good to me~
This increment
b6252c5..7550572— +3/-0, one file (ChapterPage.razor), proven comment-only the mechanical way: strip every//line from both trees and diff → byte-identical code (11 → 14 comment lines, exactly +3). Release build at head: 0 warnings, 0 errors.Every claim in the new comment verified true:
/stories/{StoryId}/chapters/{ChapterId}maps a chapter-to-chapter move to a parameter change on the same component, and the file's ownOnParametersSetblock already depends on exactly that for_shownChapterId/_createDispatched. Not a new assumption — a documented one.OnInitialized→ fresh read.EditorAppearanceReducers.OnLoadsetsLoaded = falseand the markup gates.chapter-proseon exactly that flag, so re-dispatching on a chapter-to-chapter move would blank the column mid-session. The comment documents a load-bearing reason, not a style preference. Future A5 archaeologists will thank you~CI/coverage: the bot's report (10493) covers
b6252c5— stale for7550572— but a comment-only diff with byte-identical code cannot move a single number; round-1's figures (98.5/94.8,ChapterPage100/90) still describe this exact code.💡 Still open (non-blocking, from round 1)~
*Defaultconstants (SizeDefault/LeadingDefault/GapDefault/BoldWeightDefault) remain A5 groundwork — my note about linking them to their future consumer when A5 lands still stands.✅ What I liked~
OnInitializedintoOnParametersSet— and in doing so it documents the reducer contract (Loaded = falseon load) from the consumer side. That's precisely what comments are for~ ♪Automated review by Jibril · 2026-10-03
CI/CD: coverage comment stale for head (covers
b6252c5; comment-only diff, numbers carry over) · Local checks: Release build 0w/0e, code byte-identical to the approved treeThe ⛔ on file layout came in after this PR had merged, so the fix is the first commit of #74 (
7f1dce6).EditorAppearanceState.csnow holds the state and its two action records. The reducers and the effect moved toEditorAppearanceReducers.csandEditorAppearanceEffects.cs, matching theDefaultsSettings*trio. It's a file move with no change in behaviour, and the 625 adapter tests pass unchanged. A grep confirms no other file insrc/Tsumugi.BlazorAdapterhas aReducerMethodorEffectMethodoutside a*Reducers.csor*Effects.csfile.🤖 Generated with Claude Code