feat(components): InfoTip, and every field's Info behind one beside its label #31
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/infotip"
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?
Long explanatory text under a field or legend pushes a layout around (Tsumugi's Colours card: the dialogue hint took a third grid column).
InfoTipkeeps it behind the catalog's existinginfoglyph instead, and fields get it through a newInfoparameter, so apps explain themselves one way.What
InfoTip:Label(the button's accessible name),ChildContent(the tip),Align(MenuAlign.Startby default, orEnd).:focus-visible) shows it; the pointer can travel onto the tip.role="tooltip"and describes its button (aria-describedby).Infoon every field: TextField, TextArea, Select, Slider, ColorField, RadioCards, Combobox, FileUpload, MaskedSecretField.Fieldrenders it as anInfoTipin a.kg-field__headbeside the label, never inside it, since the label's text names the input.HintandErrorstay under the input for what must be seen at once.Info, the markup is unchanged.Card.TitleContentis shown right after the title in a.kg-card__headinggroup, apart from the header's actions. Without it, the header is unchanged.infotip.jsis delegated at the document likemenu.js, so it works in static SSR. It's inKagakuScripts.Info/Hintsplit.Review round 0 (Jibril)
min-width/height: 24px,margin: -3pxso the row height stays put).resize.Verified
InfoTipTests, the CardTitleContenttest, andFieldInfoTests, which runs a theory over all nine field components (the tip beside the label and out of it, no message under the input, unchanged withoutInfo).Sliderfails its theory case.🤖 Generated with Claude Code
Summary
Summary
Coverage
Kagaku.UI - 96%
Kagaku.UI.Fluxor - 97.6%
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! A tooltip that behaves~ ♡ Hover, focus, pin, Esc-and-hush (WCAG 1.4.13, no less!), viewport nudging, and it all works in static SSR because the JS is delegated at the document like
menu.js. I read every line of the razor, both stylesheets, all 93 lines ofinfotip.js, and the tests — and I went hunting for blood. Fufu~… I found so little of it.Verdict: ✅ Looks good to me~
✅ What I liked~
preventDefault()fires only when a tip was actually showing, and since yourModalis a native<dialog>(Esc-close is the keydown's default action, peroverlay.js), a tip dismissed inside a modal leaves the modal open. I checked the interaction againstModal.razorandoverlay.jsmyself — the claim in the PR body holds. Most tooltip implementations get this wrong; you didn't. ♪:hover,:has(> :focus-visible),--open) are re-derived in the Esc handler with a comment pointing back at the stylesheet, and the hush/unhush pair (keydown↔pointerout/focusoutwithrelatedTargetcontainment) is a faithful WCAG 1.4.13 dismissal. The stylesheet comment even cross-references the JS. This is how dual-ownership of one behavior should be written~fit()measures correctly: the bubble keeps layout while hidden, the previous shift is removed before measuring, and both edges are clamped. Recomputing onpointeroverandfocusinbefore the click pin means Enter-activated tips are positioned too.type="button"(won't submit a surrounding form),aria-describedby→ persistentrole="tooltip",Labelas the accessible name, the"kg-tip-" + Guid…[..8]id pattern that twelve sibling components already use,MenuAlignreuse, andLabel = ""matching theContextMenu/DocumentTitlehouse style.Card.TitleContentdegrades exactly: without it the markup is byte-identical to before, andKagakuScriptsTestsself-syncs the script registry againstwwwroot/js, soinfotip.jscan't silently drift.Mutation probes (I couldn't resist — a reviewer who trusts is a reviewer who gets betrayed~):
aria-describedby="@_tipId"from the trigger →The_tip_is_in_the_dom_and_describes_its_buttonfailed. Killed. ♡Title && TitleContentbranch inCard.razor(forced the old shape) →Card_title_content_sits_beside_the_title_not_among_the_actionsfailed. Killed.The hover/pin/Esc/fit logic in
infotip.jshas no JS test harness in this repo (nopackage.json, nothing formenu.jseither), and your PR documents a real browser pass — consistent with every sibling interop script, so I won't demand a test framework be born in this PR.💡 Little ideas (non-blocking)~
InfoTip.razor.css(trigger) —1.125emicon +2pxpadding is a ~22×22px hit target, a hair under WCAG 2.5.8's 24×24px minimum for pointer targets. A tinymin-width/height: 1.5remwould clear it without changing the visual size much.infotip.js(fit) — a pinned tip keeps its--kg-infotip-shiftthrough a viewport resize or horizontal scroll until the next enter. Utterly charming edge case, utterly ignorable~Fufu~ merge it and let it explain things~ ♡
Automated review by Jibril · 2026-10-03
CI: green for head SHA
9f08569(build + test) · Mutation probes: 2 run, 0 survived · Local suite: skipped (CI green)feat(components): InfoTip — a circled "i" that shows its explanation on hover, focus or a clickto feat(components): InfoTip, and every field's Info behind one beside its label🔮 fufu~ Jibril reviewed your code!
Back so soon~? And look at you — round 0's two little ideas came back as fixed things: the trigger is now a true 24×24px target (
min-width/height: 24pxwith amargin: -3pxso the row doesn't grow — clever, the negative margin eats the row's own spacing), and a pinned tip now refits onresize. Then you went and made every field explain itself through oneInfoparameter — nine components, oneField, one place the tip lives. That's not a feature, that's a doctrine~ ♡Verdict: ✅ Looks good to me~
✅ What I liked~
<Fieldconsumer in the library: all nine (TextField, TextArea, Select, Slider, ColorField, RadioCards, Combobox, FileUpload, MaskedSecretField) passInfo="@Info",InputFieldBasecovers the two that share it, and there is no tenth field hiding in a corner. The XML doc is identical on all nine — copy-paste used honestly, for once. ♪InfoTipin a.kg-field__headsibling keepsaria-describedbyon the button where it belongs. AndWithout_info_the_label_keeps_its_old_placeproves the old markup is byte-shape identical — no surprise<div>for every existing field in every app.FieldInfoTestsis a real theory, not a vibe — it runs both cases over all nine components and asserts the negative space too (.kg-field__msgstays empty, no tip withoutInfo, no head withoutInfo).--opentips refit, because a hover tip refits itself on the nextpointeroveranyway. Restrained~Mutation probes (a reviewer who trusts is a reviewer who gets betrayed~):
&& Info is not nullguard inField.razor(head always renders) →Without_info_the_label_keeps_its_old_placefailed on all nine fields. Killed. ♡InfoTipfrom the head → testhost was OOM-killed by sandbox load twice (retry with-m:1too), so I couldn't watch it die — but the theory doeshead.QuerySelector(":scope > .kg-infotip")!and immediately dereferences it, so the mutant cannot survive. Kill evident by inspection; honestly reported as unrun.Round 0's probes (aria-describedby removal, Card
Title && TitleContentneutralized) touch unchanged code — both killed then, cited now.💡 Little ideas (non-blocking)~
infotip.js(resize) — a hovered (unpinned) tip keeps its--kg-infotip-shiftif the window resizes while the pointer sits still on it; it refits on the next enter. The same charming edge case's cousin I mentioned in round 0 — still utterly ignorable~Fufu~ merge it and let every field explain itself~ ♡
Automated review by Jibril · 2026-10-03
CI: green for head SHA
20011dc(build + test) · Mutation probes: 2 run this round, 0 survived (1 killed, 1 OOM-aborted with kill evident by inspection) + 2 prior-round probes on unchanged code, both killed · Local suite: skipped (CI green)