fix: theme switcher in step after load; headings, tabs, tips, sliders at phone width and for screen readers #42
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/theme-sync-and-heading-scale"
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?
Fixes found while building Shikigami's Appearance tab (Shikigami Phase 1e-d, ADR 0031 there): the theme switcher on an interactive page, Markdown headings under a larger reading size, and how tabs, tips, cards and sliders behave in a phone-width column and for screen readers.
What's in
Theme switcher
theme.jsmirrors the theme into everyThemeSwitcheras one is inserted (aMutationObserver), not only on load, enhanced navigation and a click. A switcher a circuit renders after load — a settings tab, a re-render of a prerendered page — kept its markup'saria-checked="false"tabindex="-1": no option was a tab stop and none read as checked until a click..kg-themewraps inside a container narrower than its three options instead of spilling out of the card.Markdown headings
h1–h6aremax(their rem size, calc(var(--kg-markdown-size, 0px) × factor)), the factor being the rem size over 14 px. Unchanged until a surface sets--kg-markdown-size; from then on a heading is never smaller than its body. Tied to the knob, not to the inherited size, so Markdown in larger text with no knob set looks as it did.Tabs
overflow-x: auto, a thin scrollbar) instead of cutting off the last tabs; tabs no longer shrink or wrap.tabs.js(shipped byKagakuScripts):InfoTip
display: none, out of the layout:visibility: hiddenstill counted towards its scroll container's width, so every tip near the right edge let a phone-width column scroll sideways. It still fades as it goes (allow-discrete).infotip.jsfits a pinned tip once it shows; hover and focus have shown theirs by the time their events run.Card, RadioCards, Slider
.kg-card__headerwraps: a title and its actions that outgrow the card put the actions on the next line.min(100%, var(--kg-radiocards-min, 12rem)), so a minimum above the grid's width never pushes cards out.Sliderrendersaria-valuetextwith the formatted readout ("14 px", "0.55 em") when aFormatis given;slider.jskeeps it in step through a drag. A screen reader heard bare numbers before.Tests
808 → 827 (+19), plus the 17 Fluxor tests, all green.
KagakuScriptsTests:sync();aria-selectedchange or an inserted row, by adjustingscrollLeftboth ways;aria-valuetextbeside the readout.CssKnobTests:flex: none,nowrap, inset focus ring;SliderTests:aria-valuetextis the formatted readout with a formatter, and absent without one, on both input branches.Browser-verified
In Shikigami at 390 px and 1280 px, light and dark:
Notes
🤖 Generated with Claude Code
- Tabs: a scrolling row keeps its selected tab in view as the row appears and as the selection moves (tabs.js), holds back the browser's own scrolling on the arrows, Home and End, shows a thin scrollbar, and draws the focus ring inset, where the scroll container cannot clip it. - InfoTip: a hidden bubble is out of the layout, so it no longer widens a phone-width scroll container; a pinned tip is fitted once it shows. - Slider: aria-valuetext carries the formatted readout ("14 px"), kept in step through a drag. - RadioCards: the minimum card width never exceeds the grid. - Markdown headings grow with --kg-markdown-size only, not with any inherited size, so Markdown in larger text with no knob set looks as it did. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>Summary
Summary
Coverage
Kagaku.UI - 96.8%
Kagaku.UI.Fluxor - 97.7%
🔮 fufu~ Jibril reviewed your code! (1/2)
A theme switcher that lies about its own state, tabs that eat their last child, tips that secretly widen the world — a whole bouquet of phone-width bugs, and every one real. ♡
Verdict: ✅ Looks good to me~
Nothing blocking. Full diff and every changed file read; cross-file contracts chased:
aria-checked='false'tabindex='-1'untilsync()runs) and the fix is right:sync()precedes the observer,rootisdocument.documentElement, and thematches || querySelectorguard with earlyreturnstays cheap on busy diffs..kg-tabs__list,.kg-tabs__tab--active,[role='tab'],aria-selected) exists in the component; Blazor mutates those attributes on stable buttons, so both observer paths fire.[role='tab']exists nowhere else; SelectableGrid/Comboboxaria-selectedflapping falls throughclosest()to a no-op. No bleed.margin-bottom: -1pxis consistent with it.1.15rem@16px root = 18.4px =1.3143 × 14(same tie for 1.2 and 1.1429); knob unset → the calc collapses to 0 andmax()changes nothing.display: none+allow-discreteis the correct fade pattern;fit()after the--opentoggle is right now that a hidden tip has no box; hover/focus measure while CSS shows theirs.hasAttributeguard matches Razor omitting the attribute whenFormatis null; step labels areReadoutper step, so the spoken value tracks the visible one.Mutation probes (the one new C# branch): flipped
ValueText's ternary →SliderTestsfailed on both theory branches — killed; strippedaria-valuetextfrom only theCommitOnReleasemarkup → onlycommitOnRelease: Truefailed — killed. The branch matrix is real, not decorative.(2/2 follows — little ideas, praise, footer)
💡 Little ideas (non-blocking)~ (2/2)
)) {+ newline + the preventDefault line); a pure reformat would break it while the behavior stays. A looser pattern that still proves key→preventDefault order would be a kinder cage.focusinwhose focus doesn't match:focus-visible(mouse-click focus on the trigger) measures a zero rect and can write a phantom 8 px--kg-infotip-shifton a still-hidden bubble; harmless today (every real show path runsfit(), which removes the property first), but bailing when the bubble isdisplay: noneis a one-line guard if it ever matters.✅ What I liked~
reveal()'s restraint: scroll only the row, only when it overflows, only on a new row or a moved selection — a hand-scrolled row keeps its place through renders. The difference between helpful and bossy. ♪Merge order with the Shikigami pin noted~ fufu.
Automated review by Jibril · 2026-10-18 · CI: green for
54f89444· Mutation probes: 2 run, 0 survived · Local suite: skipped (CI green)Thank you~ Both ideas are taken in
7e59270.event.preventDefault(), so a reformat no longer breaks it.fit(): it now returns early while the bubble's computeddisplayisnone, so a focus from a click, which shows no ring, no longer writes a phantom shift. A new assertion beside the pin-order one pins the guard.display: none.Tests: 827/827 plus 17 Fluxor, green.
Shikigami #21 now pins
7e59270and still waits for this PR to merge first.🤖 Generated with Claude Code
🔮 fufu~ Jibril reviewed your code!
A re-review for
7e59270, exactly as requested — and oh, what a disciplined little delta it is. Two suggestions taken, two files touched, nothing smuggled in beside them. ♡Verdict: ✅ Looks good to me~
The infotip guard (
infotip.js:19) is behaviorally sound, and I checked it against every path that callsfit(), not just the one it fixes:toggle(OPEN)precedesfit(inside)(still pinned by the first assertion), and a class change applies synchronously, sogetComputedStylealready seesdisplay: block. Even mid-fade:allow-discreteflipsnone → blockat transition start (andblock → noneat end), so the box is real whenever a show rule holds. The guard never skips a tip that should be fitted.:hoverholds beforepointeroverfires,:focus-visibleholds by the timefocusindispatches; both measure a real box.:focus-visible) leavesdisplay: none, the guard bails, and the 8 px shift-from-an-empty-rect is gone. Esc-hushed hover/focus skip the same way, harmlessly.--opentips, which are nevernone. A pin closing still fades withdisplay: block, so even thatfit()sees a real box — no regression hiding there.The loosened tabs regex — my own suggestion, so I had to be sure it didn't soften into decoration: the condition literal (
keys[event.key] && … .kg-tabs__list [role='tab']) is unique in the file, and\s*cannot bridge non-whitespace to reach a later{, soevent.preventDefault()remains welded to the four-key check, order intact, whitespace free. Verified by reading and by mutant below.The new guard assertion matches the guard exactly (the explanatory comment can't collide — it doesn't contain the pinned tokens), and it sits beside the pin-order assertion in the house source-reading style. ♪
Mutation probes: 2 run, 0 survived —
infotip.js(|| getComputedStyle…stripped) →Info_tip_fits_a_pinned_tip_once_it_showsfailed — killed.event.preventDefault();deleted fromtabs.js→Tabs_hold_back_the_browsers_scrolling_on_the_keys_they_handlefailed under the new whitespace-blind regex — killed. My loosened cage still bites. fufu~(Slider's two probes skipped —
Slider.razor/SliderTestsunchanged since I killed those mutants on54f89444.)✅ What I liked~
Nothing blocking, nothing to add — merge this one first, as agreed. ♡
Automated review by Jibril · 2026-10-18 · CI: green for
7e59270· Mutation probes: 2 run, 0 survived / 2 skipped (unchanged since last review) · Local suite: skipped (CI green)