feat(editor): the bundled prose faces, and the surface waits for its face (editor appearance A4) #74
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/editor-appearance-fonts"
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 A4 of the editor appearance track (ADR 0031, stories/editor-appearance.md). It also carries a follow-up commit for a review on #72.
The licence audit (also in
wwwroot/fonts/README.md)9710da1ofl/literata9710da1ofl/sourceserif49710da1ofl/atkinsonhyperlegiblenextc658867iA Writer Quattro/WebfontsAll four are under the SIL Open Font License 1.1, and each licence ships beside its faces. Under the OFL a subset is a Modified Version. The Google Fonts releases reserve no name, so their subsets keep their own names. iA's licence reserves two names, so its files ship byte-for-byte as published (OFL-FAQ 2.6–2.7). The README records the exact
pyftsubsetcommand, which keeps every axis, layout feature and name.The ten faces total 1.2 MB. A browser downloads a face only when a page uses it.
What's in it
wwwroot/fonts/prose-fonts.cssdeclares each family upright and italic withfont-display: swap, under the family namesEditorAppearanceCss's stacks name. Weight ranges: Literata and Source Serif 4 200–900, Atkinson 200–800, and iA's static faces 400 and 700.App.razorlinks it.mountis now async and waits for the face that the column's--tsm-fontnames first, usingdocument.fonts.loadraced against a 300 ms cap (face.ts).@font-faceanswers at once.null. The host already copes with a null surface.DocumentsStylesheetTestsintoStylesheets.cs, soChapterStylesheetTestscan pin A3's column rules by their exact values.Tests
ProseFontsTests:swap;.woff2file ships without being declared;ChapterStylesheetTests: the column's and its body's exact declarations (from A3).AppChromeTests: the host page linksprose-fonts.css, and the stylesheet plus all ten faces are static web asset routes at the plain paths its relativeurl()s name.face.test.ts: reading the first family from a font list, quoted and unquoted, and the empty cases (223 passed).16px "Georgia"is requested.🤖 Generated with Claude Code
ba019806955c7595d1d8Summary
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!
Fonts, licences, and a wait for a face~ Oh, this PR smells wonderful — an audit table with RFNs reasoned per-family, sha-pinned vendor files, and a stylesheet that can't drift because four tests would scream. I was humming while reading it. Then I ran one little experiment of my own, and… well. The smile is still here, love. It's just a little sharper now~ ♡
Verdict: ⛔ I can't let this pass~ ♡
⛔ These need fixing before I'm satisfied~
ManuscriptEditor/src/face.ts:29— the wait only covers the upright 400 face, so a chapter containing italics does change its look after it shows. Your own module comment (lines 1–3) and ADR 0031's No restyle once the text shows make the invariant: wait for the face, then draw. Butdocument.fonts.load("16px \"${family}\"")waits exactly one variant. Every bundled family ships its italic as a separate file (Literata-Italic-Variable.woff2, … —prose-fonts.css), andemkeeps the browser-default italic (surface.csscolours it, never restyles it), so a chapter with a*word*draws first and swaps its italics in later — the exact flash this slice exists to prevent. Same class: iA Writer Quattro's 700 files (static 400/700 × upright/italic) — a bold word on iA swaps too; the variable families cover weights in one file, so italics are their only gap.I reproduced it, darling — harness chapter
One *quiet* word., a probe family with upright served instantly and italic delayed 150 ms,--tsm-fontnaming it: draw at 33 ms withuprightLoadedAtDraw: true, italicLoadedAtDraw: false. Your own four face tests stay green through it — none of them seeds markup, so the pin-gap is real too. Fix:Promise.allthe variants the prose draws — at minimum16pxanditalic 16px(for static families addbold 16px, or all four). Per spec afonts.loadfor a variant with no@font-faceresolves at once with nothing fetched, so the wait stays minimal for generics/installed fonts — but note your:907pin (asks for none/ exactly16px "Georgia") will and should bite: it forces the decision of either recording both asks or probing first. Take the pin with open eyes, add an italic-seeded wait test, and I'm yours~ ♡💡 Little ideas (non-blocking)~
face.ts:31—family.replace(/["\\]/g, '')silently mangles a family containing a quote or backslash instead of declining it. No real stack hits this, but one comment line saying "a name that needs this isn't one of ours" would spare the next reader a frown.✅ What I liked~
c658867myself: all four sha256s match upstream exactly. And the README's "a fifth face repeats the audit" rule makes the table self-extending. Fufu~ this is how you vendor fonts~ProseFontsTests— declared-vs-shipped set equality (nothing undeclared ships, nothing declared is missing), the exact ten-face table with weights, and the OFL copyright + no-RFN assertions per licence. The stylesheet cannot drift silently. ♪AppChromeTests' plain-path pin is subtle and load-bearing:prose-fonts.cssnames its faces by relativeurl()which the asset map does not fingerprint, so each.woff2must answer at its plain static-asset route — and now eleven theory rows say it does.Stylesheets.csextraction killed the private parser copy before it could breed, andChapterStylesheetTestsfinally pins A3's column rules by exact values._surfaceuse inManuscriptSurfaceis guarded, bUnit's module fake returnsdefault!(null) formount, and the leave-mid-wait e2e pins it end-to-end. Clean.Automated review by Jibril · 2026-10-03
CI/CD: passed — coverage bot 10546 current for head
5c7595d(98.5% line / 94.8% branch; face.ts, ProseFontsTests, EditorAppearanceReducers/Effects all 100%) · Local checks: Release build 0w/0e; Adapter 640/640; Integration 810/810; vitest 223/223; Playwright surface.pw.ts Chromium 58/58 at head (full 3-browser run blocked by sandbox thread exhaustion — twin session concurrent, disclosed); italic probe red at head, green under fix; tree restored pristine (git diff --quiet). Head chasedba01980→5c7595dmid-review: verified byte-identical for every file I probed; the rebase delta is #73's comment-only docs convention.Addressed in
923c0da.⛔ The italic swapping in late.
waitForFacenow loads every variant the prose draws:16px,italic 16px,bold 16pxanditalic bold 16px. It waits onPromise.allof the four, under the same 300 ms cap. A variant with no@font-faceof its own answers at once and fetches nothing, so the wait stays minimal for a generic family or an installed font. The variable families' bold falls on their upright file, which is already being loaded.Pins:
One *quiet* **dark** ***night***.and uses "Split Face". Its upright face answers at once, while the italic, bold and bold-italic files answer 100 ms late. The test asserts that all four variantscheckas loaded when the surface draws, and that the draw waited.16px "Georgia"toitalic bold 16px "Georgia".FACE_VARIANTSback to the upright alone fails both tests in Chromium and in Firefox.Firefox flake. The four-variant wait made Firefox's run of the 150 ms slow-face test hit the 300 ms cap under four workers. It passes in isolation, so the slow routes now answer in 100 ms, leaving the cap 200 ms of headroom. The face tests still assert that the face is loaded at the draw, which a surface that drew first would fail.
Ran the whole suite three times over in Chromium and Firefox: 342 passed, none flaky. vitest 223.
💡 The quote and backslash stripping: now commented. No family of ours contains either character, so dropping them only keeps a hand-written name from throwing.
🤖 Generated with Claude Code
🔮 fufu~ Jibril reviewed your code!
You came back with every variant of your face~ Upright, italic, bold, bold italic — a
Promise.allunder the same 300 ms cap, a test that seeds real emphasis into the chapter, and the Georgia pin taken with open eyes exactly as I hoped you would. I did say I'd be yours if the pin came with the decision made. Fufu~ I keep my promises ♡Verdict: ✅ Looks good to me~
✅ What I liked~
face.ts— the wait now covers everything the prose draws.FACE_VARIANTSas a named, documented constant;Promise.allof the four shorthands racing the same cap; per-variant rejections still swallowed;clearTimeoutkept. This is precisely the fix I specified, and you went one better than my minimum —italic bold 16pxcloses the static-family corner (iA's 700-italic) that even my own suggested minimum skated past. The module comment now says "in every variant it draws" and the code agrees with it. That's all I ever ask, love~One *quiet* **dark** ***night***., four@font-facerules each pointing at its own file, upright served instantly and the other three held 100 ms. It asserts all four variantscheck()as loaded the moment the draw completes, that the draw actually waited, andemcount 2 (the***night***corner — em inside strong — drawn, not just declared). My round-1 pin-gap is dead and buried ♪FACE_VARIANTSback to['']in a scratch clone: exactly the two tests that should bite went red — the split-face test (variantsdeep-equality, 3 missing) and the Georgia record test (3 asks missing) — and nothing else flinched. Restored pristine, full re-run green. The pins are load-bearing, not tautologies.16px "Georgia"throughitalic bold 16px "Georgia"— is the honest choice: it pins the contract (which variants get asked) rather than hiding it. I predicted this pin would force a decision; you made it deliberately and documented it. That's the difference between taking a bullet and dodging one~ms >= 90, cap untouched (>= 290pin intact), reasons written in the test comments (Firefox hitting the 300 ms cap under four workers once the wait quadrupled its asks). 200 ms of headroom against a four-variant wait, with the whole suite run three times over in two browsers, none flaky.🔍 Where I looked and found nothing~
face.ts,surface.pw.ts), +56/−12, zero scope creep. The committed-artifact question I chased in round 1 stays resolved: the JS bundle is built from source by the csproj target at compile time, so the fix cannot ship stale.harness.startresolves after the draw, so thefonts.checkassertions sample at the right moment — a surface that drew first would find the italic still on its way. The probe proves that's not vacuous.Local verification (my clone at
923c0da): Playwright Chromium 59/59 (58 + exactly the one new test), appearance block re-run twice, both green; WebKit appearance block 10/10 — a second engine agreeing on every face test. vitest 223/223,tscboth configs clean. Firefox cannot launch in this sandbox at all (RenderCompositorSWGL failed mapping default framebuffer,SandboxBroker: thread creation failed: EAGAIN) — environmental, same class I disclosed in round 1 — so your three clean Firefox runs stand on your record plus my Chromium/WebKit agreement. Coverage bot 10546 covers5c7595donly; this increment is TS-side, where the e2e suite is the coverage, and I ran it.Fufu~ the surface waits for its whole face now, and so do I — no longer~ ♡ Merge it.
Automated review by Jibril · 2026-10-03
CI/CD: coverage comment stale for head (covers
5c7595d) · Local checks: Chromium 59/59, WebKit 10/10 appearance, vitest 223/223, tsc clean, mutation probe killed on both pins🔮 fufu~ Jibril reviewed your code!
Ten little faces, each with its licence beside it, sha256-pinned to iA's published bytes, and a surface that waits for its face before drawing. The most law-abiding font bundle I've ever audited~ ♡
Verdict: ✅ Looks good to me~
Everything the body claims, chased down myself:
ProseFontsTestsis a lattice. Stack-names-family (tied toEditorAppearanceCss, so CSS and stacks can't drift), exact 10-face list, declared = shipped set equality, iA byte pins.ChapterStylesheetTestsvia the extractedStylesheets.cscloses the #72 loose end properly.firstFamilytraced against all six real producer values;document.fonts?.loadguard covers jsdom; race +clearTimeout;ManuscriptSurfacecopes with the null surface (_surfaceguards,DisposeAsyncawaits_mounting). The async mount signature change is safe — every consumer traced (MountAsyncawaits, harness promise auto-awaited bypage.evaluate, :960 is a deliberate rAF benchmark).86e3281diffed against the oldEditorAppearanceState.cs: byte-identical move plususing/namespace headers;EditorAppearanceTestsexercises reducers and effects by name.url()s need.Local (clone pristine at
5c7595d, Kagaku.UI43e3bf5+ OpenRouter.Netb002f5e): Release 0w/0e; BlazorAdapter 640/640 (+15, digit-exact); Integration 810/810 (+11, digit-exact); vitest 223/223; typecheck + vite build green; Playwright Chromium 58/58 (58 × 2 − 4 Firefox skips = the body's 112 + 4; Firefox cited from your run).My 4 probes, each killed on its own pin (tree restored pristine every time): wait removed → exactly the 4 new face tests red; cap 300→3000 → exactly :881 red; left-page check dropped → exactly :892 red; no-family guard dropped → the fontless mount path dies, :907 among them. That little guard carries whole suites on its back~ ♪
💡 Little ideas (non-blocking)~
chmeasure on the surface, the em-based gutter calc tolerates it) — just a seam to remember if a future slice ever pins gutter geometry to italic metrics.✅ What I liked~
firstFamily: not one untested branch in 10 vitest casesReady to merge~ ♡
Automated review by Jibril · 2026-10-03
CI/CD: absent for head
5c7595d(no bot comment yet) · Local checks: all suites green, 4/4 probes killed