feat(components): InfoTip, and every field's Info behind one beside its label #31

Merged
bjoern merged 2 commits from feat/infotip into main 2026-10-03 16:39:07 +02:00
Member

Long explanatory text under a field or legend pushes a layout around (Tsumugi's Colours card: the dialogue hint took a third grid column). InfoTip keeps it behind the catalog's existing info glyph instead, and fields get it through a new Info parameter, so apps explain themselves one way.

What

  • InfoTip: Label (the button's accessible name), ChildContent (the tip), Align (MenuAlign.Start by default, or End).
    • Hover or keyboard focus (:focus-visible) shows it; the pointer can travel onto the tip.
    • A click pins it; a second click or a click elsewhere unpins it.
    • Esc dismisses it and hushes a hovered or focused tip until the pointer or focus leaves (WCAG 1.4.13). The Esc is consumed only when a tip was showing, so a surrounding modal stays open.
    • A tip that would cross the viewport's edge is nudged back inside (8px margin), including a pinned tip when the window resizes.
    • The trigger is a 24×24px pointer target (WCAG 2.5.8) without growing the row.
    • The tip stays in the DOM as role="tooltip" and describes its button (aria-describedby).
  • Info on every field: TextField, TextArea, Select, Slider, ColorField, RadioCards, Combobox, FileUpload, MaskedSecretField.
    • Field renders it as an InfoTip in a .kg-field__head beside the label, never inside it, since the label's text names the input.
    • Hint and Error stay under the input for what must be seen at once.
    • Without Info, the markup is unchanged.
  • Card.TitleContent is shown right after the title in a .kg-card__heading group, apart from the header's actions. Without it, the header is unchanged.
  • infotip.js is delegated at the document like menu.js, so it works in static SSR. It's in KagakuScripts.
  • README: states the Info/Hint split.

Review round 0 (Jibril)

  • 24px target: added (min-width/height: 24px, margin: -3px so the row height stays put).
  • Pinned tip after a resize: refit on resize.

Verified

  • bunit: 559 + 15 pass. New: InfoTipTests, the Card TitleContent test, and FieldInfoTests, which runs a theory over all nine field components (the tip beside the label and out of it, no message under the input, unchanged without Info).
  • Mutation probe: dropping the passthrough from Slider fails its theory case.
  • Browser pass in Tsumugi (Chromium; Settings Defaults/Appearance/Backups, story General, chapter, category editor):
    • Every trigger measures 24×24.
    • Hover, pin, Esc and keyboard focus behave.
    • A pinned tip refits to 8px inside a 420px window after a resize.

🤖 Generated with Claude Code

Long explanatory text under a field or legend pushes a layout around (Tsumugi's Colours card: the dialogue hint took a third grid column). `InfoTip` keeps it behind the catalog's existing `info` glyph instead, and fields get it through a new `Info` parameter, so apps explain themselves one way. ## What - **`InfoTip`**: `Label` (the button's accessible name), `ChildContent` (the tip), `Align` (`MenuAlign.Start` by default, or `End`). - Hover or keyboard focus (`:focus-visible`) shows it; the pointer can travel onto the tip. - A click pins it; a second click or a click elsewhere unpins it. - Esc dismisses it and hushes a hovered or focused tip until the pointer or focus leaves (WCAG 1.4.13). The Esc is consumed only when a tip was showing, so a surrounding modal stays open. - A tip that would cross the viewport's edge is nudged back inside (8px margin), including a pinned tip when the window resizes. - The trigger is a 24×24px pointer target (WCAG 2.5.8) without growing the row. - The tip stays in the DOM as `role="tooltip"` and describes its button (`aria-describedby`). - **`Info` on every field**: TextField, TextArea, Select, Slider, ColorField, RadioCards, Combobox, FileUpload, MaskedSecretField. - `Field` renders it as an `InfoTip` in a `.kg-field__head` beside the label, never inside it, since the label's text names the input. - `Hint` and `Error` stay under the input for what must be seen at once. - Without `Info`, the markup is unchanged. - **`Card.TitleContent`** is shown right after the title in a `.kg-card__heading` group, apart from the header's actions. Without it, the header is unchanged. - **`infotip.js`** is delegated at the document like `menu.js`, so it works in static SSR. It's in `KagakuScripts`. - **README**: states the `Info`/`Hint` split. ## Review round 0 (Jibril) - 24px target: added (`min-width/height: 24px`, `margin: -3px` so the row height stays put). - Pinned tip after a resize: refit on `resize`. ## Verified - bunit: 559 + 15 pass. New: `InfoTipTests`, the Card `TitleContent` test, and `FieldInfoTests`, which runs a theory over all nine field components (the tip beside the label and out of it, no message under the input, unchanged without `Info`). - Mutation probe: dropping the passthrough from `Slider` fails its theory case. - Browser pass in Tsumugi (Chromium; Settings Defaults/Appearance/Backups, story General, chapter, category editor): - Every trigger measures 24×24. - Hover, pin, Esc and keyboard focus behave. - A pinned tip refits to 8px inside a 420px window after a resize. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(components): InfoTip — a circled "i" that shows its explanation on hover, focus or a click
All checks were successful
CI / build (pull_request) Successful in 12s
CI / test (pull_request) Successful in 20s
9f085691c3
Long explanatory text under a field or legend pushes a layout around; InfoTip keeps it behind the
catalog's info glyph instead. Hover or keyboard focus shows the tip, a click pins it, Esc dismisses
it (hushing a hovered one until the pointer or focus leaves — WCAG 1.4.13), and a tip that would
cross the viewport's edge is nudged back inside. The tip describes its button for screen readers.
infotip.js is delegated at the document like menu.js, so it works in static SSR.

Card takes a TitleContent fragment, shown right after the title and apart from the header's
actions — the place for a card's own InfoTip.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Summary

Summary
Generated on: 10/03/2026 - 14:31:26
Coverage date: 10/03/2026 - 14:31:23 - 10/03/2026 - 14:31:24
Parser: MultiReport (2x Cobertura)
Assemblies: 2
Classes: 86
Files: 84
Line coverage: 96.1% (1804 of 1877)
Covered lines: 1804
Uncovered lines: 73
Coverable lines: 1877
Total lines: 5293
Branch coverage: 90.8% (970 of 1068)
Covered branches: 970
Total branches: 1068
Method coverage: Feature is only available for sponsors

Coverage

Kagaku.UI - 96%
Name Line Branch
Kagaku.UI 96% 90.9%
Kagaku.UI.Badge 100% 100%
Kagaku.UI.Breadcrumb 100%
Kagaku.UI.BreadcrumbItem 100% 100%
Kagaku.UI.Button 100% 100%
Kagaku.UI.Card 100% 100%
Kagaku.UI.CircuitErrorPanel 100%
Kagaku.UI.ColorField 100% 96.4%
Kagaku.UI.Column 100%
Kagaku.UI.Columns 100% 100%
Kagaku.UI.Combobox`1 98.1% 91.8%
Kagaku.UI.ConfirmDialog 100% 100%
Kagaku.UI.Constrain 100% 100%
Kagaku.UI.ContextMenu 92.1% 94.4%
Kagaku.UI.CssClassExtensions 100%
Kagaku.UI.Debounce 97.8% 80.7%
Kagaku.UI.DebouncedSearchField 100% 87.5%
Kagaku.UI.Disclosure 100% 100%
Kagaku.UI.DocumentTitle 76.9% 100%
Kagaku.UI.DragReorderList`1 93.5% 75%
Kagaku.UI.EmptyState 100% 100%
Kagaku.UI.Field 100% 100%
Kagaku.UI.FileUpload 100% 91.6%
Kagaku.UI.FixedColumn 100%
Kagaku.UI.FixedRow 100%
Kagaku.UI.FlexColumn 100%
Kagaku.UI.Flexible 100%
Kagaku.UI.FlexRow 100%
Kagaku.UI.FloatingActionButton 100%
Kagaku.UI.Hotkey 92.5% 87.5%
Kagaku.UI.Icon 100% 100%
Kagaku.UI.IconCatalog 100%
Kagaku.UI.InfoTip 100%
Kagaku.UI.InlineAlert 83.3% 75%
Kagaku.UI.InputFieldBase 95.3% 87.5%
Kagaku.UI.LabeledEntriesTable 96.7% 66.6%
Kagaku.UI.LabeledEntry 100%
Kagaku.UI.LayoutCss 95.2% 93.7%
Kagaku.UI.Lightbox 83.7% 85%
Kagaku.UI.LiveValue 100% 100%
Kagaku.UI.LiveValueInterop 100% 100%
Kagaku.UI.Markdown 100% 83.3%
Kagaku.UI.MaskCanvas 94.1% 90.1%
Kagaku.UI.MaskedSecretField 96.4% 83.3%
Kagaku.UI.MaskPoint 100%
Kagaku.UI.MaskPrimitive 100%
Kagaku.UI.Menu 90% 75%
Kagaku.UI.MenuItem 100% 87.5%
Kagaku.UI.Modal 88.8% 90.9%
Kagaku.UI.NavGroup 100% 100%
Kagaku.UI.NavItem 92.1% 96.1%
Kagaku.UI.NavList 100%
Kagaku.UI.PageShell 100% 100%
Kagaku.UI.PreviewImage 100% 100%
Kagaku.UI.QuicklinkNav 80.5% 95.8%
Kagaku.UI.QuicklinkSection 100%
Kagaku.UI.RadioCard`1 100%
Kagaku.UI.RadioCards`1 100% 100%
Kagaku.UI.RegionPoint 100% 100%
Kagaku.UI.RegionRect 90% 100%
Kagaku.UI.RegionSelector 85% 86.9%
Kagaku.UI.RelativeTime 100% 93.7%
Kagaku.UI.Row 100%
Kagaku.UI.Rows 100% 100%
Kagaku.UI.SaveIndicator 100% 100%
Kagaku.UI.ScrollViewer 100% 100%
Kagaku.UI.Select`1 100% 100%
Kagaku.UI.SelectableGrid`1 95.5% 89.1%
Kagaku.UI.SelectOption`1 100%
Kagaku.UI.Separator 100%
Kagaku.UI.Slider 100% 91.6%
Kagaku.UI.StatusDot 100%
Kagaku.UI.Tab 100% 100%
Kagaku.UI.Table`1 100% 88.4%
Kagaku.UI.TableColumn`1 100%
Kagaku.UI.Tabs 95.9% 84.4%
Kagaku.UI.TextArea 100%
Kagaku.UI.TextField 100%
Kagaku.UI.ThemeSwitcher 100% 100%
Kagaku.UI.ToggleButtonGroup`1 100% 100%
Kagaku.UI.ToggleOption`1 100%
Kagaku.UI.TypingIndicator 100%
Kagaku.UI.UsageBar 100% 100%
Kagaku.UI.UsageSegment 100%
Kagaku.UI.Fluxor - 97.6%
Name Line Branch
Kagaku.UI.Fluxor 97.6% 85%
Kagaku.UI.Fluxor.CircuitError 100%
Kagaku.UI.Fluxor.CircuitErrorSink 100% 92.8%
Kagaku.UI.Fluxor.StoreInitializer 92.8% 66.6%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 10/03/2026 - 14:31:26 | | Coverage date: | 10/03/2026 - 14:31:23 - 10/03/2026 - 14:31:24 | | Parser: | MultiReport (2x Cobertura) | | Assemblies: | 2 | | Classes: | 86 | | Files: | 84 | | **Line coverage:** | 96.1% (1804 of 1877) | | Covered lines: | 1804 | | Uncovered lines: | 73 | | Coverable lines: | 1877 | | Total lines: | 5293 | | **Branch coverage:** | 90.8% (970 of 1068) | | Covered branches: | 970 | | Total branches: | 1068 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Kagaku.UI - 96%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagaku.UI**|**96%**|**90.9%**| |Kagaku.UI.Badge|100%|100%| |Kagaku.UI.Breadcrumb|100%|| |Kagaku.UI.BreadcrumbItem|100%|100%| |Kagaku.UI.Button|100%|100%| |Kagaku.UI.Card|100%|100%| |Kagaku.UI.CircuitErrorPanel|100%|| |Kagaku.UI.ColorField|100%|96.4%| |Kagaku.UI.Column|100%|| |Kagaku.UI.Columns|100%|100%| |Kagaku.UI.Combobox`1|98.1%|91.8%| |Kagaku.UI.ConfirmDialog|100%|100%| |Kagaku.UI.Constrain|100%|100%| |Kagaku.UI.ContextMenu|92.1%|94.4%| |Kagaku.UI.CssClassExtensions|100%|| |Kagaku.UI.Debounce|97.8%|80.7%| |Kagaku.UI.DebouncedSearchField|100%|87.5%| |Kagaku.UI.Disclosure|100%|100%| |Kagaku.UI.DocumentTitle|76.9%|100%| |Kagaku.UI.DragReorderList`1|93.5%|75%| |Kagaku.UI.EmptyState|100%|100%| |Kagaku.UI.Field|100%|100%| |Kagaku.UI.FileUpload|100%|91.6%| |Kagaku.UI.FixedColumn|100%|| |Kagaku.UI.FixedRow|100%|| |Kagaku.UI.FlexColumn|100%|| |Kagaku.UI.Flexible|100%|| |Kagaku.UI.FlexRow|100%|| |Kagaku.UI.FloatingActionButton|100%|| |Kagaku.UI.Hotkey|92.5%|87.5%| |Kagaku.UI.Icon|100%|100%| |Kagaku.UI.IconCatalog|100%|| |Kagaku.UI.InfoTip|100%|| |Kagaku.UI.InlineAlert|83.3%|75%| |Kagaku.UI.InputFieldBase|95.3%|87.5%| |Kagaku.UI.LabeledEntriesTable|96.7%|66.6%| |Kagaku.UI.LabeledEntry|100%|| |Kagaku.UI.LayoutCss|95.2%|93.7%| |Kagaku.UI.Lightbox|83.7%|85%| |Kagaku.UI.LiveValue|100%|100%| |Kagaku.UI.LiveValueInterop|100%|100%| |Kagaku.UI.Markdown|100%|83.3%| |Kagaku.UI.MaskCanvas|94.1%|90.1%| |Kagaku.UI.MaskedSecretField|96.4%|83.3%| |Kagaku.UI.MaskPoint|100%|| |Kagaku.UI.MaskPrimitive|100%|| |Kagaku.UI.Menu|90%|75%| |Kagaku.UI.MenuItem|100%|87.5%| |Kagaku.UI.Modal|88.8%|90.9%| |Kagaku.UI.NavGroup|100%|100%| |Kagaku.UI.NavItem|92.1%|96.1%| |Kagaku.UI.NavList|100%|| |Kagaku.UI.PageShell|100%|100%| |Kagaku.UI.PreviewImage|100%|100%| |Kagaku.UI.QuicklinkNav|80.5%|95.8%| |Kagaku.UI.QuicklinkSection|100%|| |Kagaku.UI.RadioCard`1|100%|| |Kagaku.UI.RadioCards`1|100%|100%| |Kagaku.UI.RegionPoint|100%|100%| |Kagaku.UI.RegionRect|90%|100%| |Kagaku.UI.RegionSelector|85%|86.9%| |Kagaku.UI.RelativeTime|100%|93.7%| |Kagaku.UI.Row|100%|| |Kagaku.UI.Rows|100%|100%| |Kagaku.UI.SaveIndicator|100%|100%| |Kagaku.UI.ScrollViewer|100%|100%| |Kagaku.UI.Select`1|100%|100%| |Kagaku.UI.SelectableGrid`1|95.5%|89.1%| |Kagaku.UI.SelectOption`1|100%|| |Kagaku.UI.Separator|100%|| |Kagaku.UI.Slider|100%|91.6%| |Kagaku.UI.StatusDot|100%|| |Kagaku.UI.Tab|100%|100%| |Kagaku.UI.Table`1|100%|88.4%| |Kagaku.UI.TableColumn`1|100%|| |Kagaku.UI.Tabs|95.9%|84.4%| |Kagaku.UI.TextArea|100%|| |Kagaku.UI.TextField|100%|| |Kagaku.UI.ThemeSwitcher|100%|100%| |Kagaku.UI.ToggleButtonGroup`1|100%|100%| |Kagaku.UI.ToggleOption`1|100%|| |Kagaku.UI.TypingIndicator|100%|| |Kagaku.UI.UsageBar|100%|100%| |Kagaku.UI.UsageSegment|100%|| </details> <details><summary>Kagaku.UI.Fluxor - 97.6%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagaku.UI.Fluxor**|**97.6%**|**85%**| |Kagaku.UI.Fluxor.CircuitError|100%|| |Kagaku.UI.Fluxor.CircuitErrorSink|100%|92.8%| |Kagaku.UI.Fluxor.StoreInitializer|92.8%|66.6%| </details>
Member

🔮 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 of infotip.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~

  • The Esc logic is genuinely careful. preventDefault() fires only when a tip was actually showing, and since your Modal is a native <dialog> (Esc-close is the keydown's default action, per overlay.js), a tip dismissed inside a modal leaves the modal open. I checked the interaction against Modal.razor and overlay.js myself — the claim in the PR body holds. Most tooltip implementations get this wrong; you didn't. ♪
  • CSS and JS mirror each other exactly. The three ways CSS shows a bubble (: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/focusout with relatedTarget containment) 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 on pointerover and focusin before the click pin means Enter-activated tips are positioned too.
  • Contract details siblings sometimes fumble: type="button" (won't submit a surrounding form), aria-describedby → persistent role="tooltip", Label as the accessible name, the "kg-tip-" + Guid…[..8] id pattern that twelve sibling components already use, MenuAlign reuse, and Label = "" matching the ContextMenu/DocumentTitle house style.
  • Card.TitleContent degrades exactly: without it the markup is byte-identical to before, and KagakuScriptsTests self-syncs the script registry against wwwroot/js, so infotip.js can't silently drift.

Mutation probes (I couldn't resist — a reviewer who trusts is a reviewer who gets betrayed~):

  1. Deleted aria-describedby="@_tipId" from the trigger → The_tip_is_in_the_dom_and_describes_its_button failed. Killed. ♡
  2. Neutralized the Title && TitleContent branch in Card.razor (forced the old shape) → Card_title_content_sits_beside_the_title_not_among_the_actions failed. Killed.

The hover/pin/Esc/fit logic in infotip.js has no JS test harness in this repo (no package.json, nothing for menu.js either), 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)~

  1. InfoTip.razor.css (trigger) — 1.125em icon + 2px padding is a ~22×22px hit target, a hair under WCAG 2.5.8's 24×24px minimum for pointer targets. A tiny min-width/height: 1.5rem would clear it without changing the visual size much.
  2. infotip.js (fit) — a pinned tip keeps its --kg-infotip-shift through 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)

## 🔮 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 of `infotip.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~ - **The Esc logic is genuinely careful.** `preventDefault()` fires only when a tip was actually showing, and since your `Modal` is a native `<dialog>` (Esc-close is the keydown's *default action*, per `overlay.js`), a tip dismissed inside a modal leaves the modal open. I checked the interaction against `Modal.razor` and `overlay.js` myself — the claim in the PR body holds. Most tooltip implementations get this wrong; you didn't. ♪ - **CSS and JS mirror each other exactly.** The three ways CSS shows a bubble (`: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`/`focusout` with `relatedTarget` containment) 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 on `pointerover` *and* `focusin` before the click pin means Enter-activated tips are positioned too. - **Contract details siblings sometimes fumble**: `type="button"` (won't submit a surrounding form), `aria-describedby` → persistent `role="tooltip"`, `Label` as the accessible name, the `"kg-tip-" + Guid…[..8]` id pattern that twelve sibling components already use, `MenuAlign` reuse, and `Label = ""` matching the `ContextMenu`/`DocumentTitle` house style. - **`Card.TitleContent` degrades exactly**: without it the markup is byte-identical to before, and `KagakuScriptsTests` self-syncs the script registry against `wwwroot/js`, so `infotip.js` can't silently drift. **Mutation probes** (I couldn't resist — a reviewer who trusts is a reviewer who gets betrayed~): 1. Deleted `aria-describedby="@_tipId"` from the trigger → `The_tip_is_in_the_dom_and_describes_its_button` **failed**. Killed. ♡ 2. Neutralized the `Title && TitleContent` branch in `Card.razor` (forced the old shape) → `Card_title_content_sits_beside_the_title_not_among_the_actions` **failed**. Killed. The hover/pin/Esc/fit logic in `infotip.js` has no JS test harness in this repo (no `package.json`, nothing for `menu.js` either), 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)~ 1. **`InfoTip.razor.css` (trigger)** — `1.125em` icon + `2px` padding is a ~22×22px hit target, a hair under WCAG 2.5.8's 24×24px minimum for pointer targets. A tiny `min-width/height: 1.5rem` would clear it without changing the visual size much. 2. **`infotip.js` (`fit`)** — a pinned tip keeps its `--kg-infotip-shift` through 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): every field takes an Info — its explanation behind an InfoTip beside the label
All checks were successful
CI / build (pull_request) Successful in 13s
CI / test (pull_request) Successful in 21s
20011dc782
Field renders Info as an InfoTip beside the label — never inside it, whose text names the input —
and every field component (TextField, TextArea, Select, Slider, ColorField, RadioCards, Combobox,
FileUpload, MaskedSecretField) passes it through. Hint and Error stay under the input for what must
be seen at once. Without Info, the field renders as before.

Review round 0: the trigger is a 24 px pointer target (WCAG 2.5.8) without growing the row, and a
pinned tip refits when the window resizes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
scarlet changed title from feat(components): InfoTip — a circled "i" that shows its explanation on hover, focus or a click to feat(components): InfoTip, and every field's Info behind one beside its label 2026-10-03 16:24:42 +02:00
Member

🔮 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: 24px with a margin: -3px so the row doesn't grow — clever, the negative margin eats the row's own spacing), and a pinned tip now refits on resize. Then you went and made every field explain itself through one Info parameter — nine components, one Field, one place the tip lives. That's not a feature, that's a doctrine~ ♡

Verdict: ✅ Looks good to me~

✅ What I liked~

  • The nine-way passthrough is complete. I grepped every <Field consumer in the library: all nine (TextField, TextArea, Select, Slider, ColorField, RadioCards, Combobox, FileUpload, MaskedSecretField) pass Info="@Info", InputFieldBase covers 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. ♪
  • The tip sits beside the label, never in it. A label's accessible text names its input; a button inside it would corrupt that name. Putting InfoTip in a .kg-field__head sibling keeps aria-describedby on the button where it belongs. And Without_info_the_label_keeps_its_old_place proves the old markup is byte-shape identical — no surprise <div> for every existing field in every app.
  • FieldInfoTests is a real theory, not a vibe — it runs both cases over all nine components and asserts the negative space too (.kg-field__msg stays empty, no tip without Info, no head without Info).
  • The resize refit is scoped exactly right: only --open tips refit, because a hover tip refits itself on the next pointerover anyway. Restrained~

Mutation probes (a reviewer who trusts is a reviewer who gets betrayed~):

  1. Dropped the && Info is not null guard in Field.razor (head always renders) → Without_info_the_label_keeps_its_old_place failed on all nine fields. Killed. ♡
  2. Removed the InfoTip from the head → testhost was OOM-killed by sandbox load twice (retry with -m:1 too), so I couldn't watch it die — but the theory does head.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 && TitleContent neutralized) touch unchanged code — both killed then, cited now.

💡 Little ideas (non-blocking)~

  1. infotip.js (resize) — a hovered (unpinned) tip keeps its --kg-infotip-shift if 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)

## 🔮 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: 24px` with a `margin: -3px` so the row doesn't grow — clever, the negative margin eats the row's own spacing), and a pinned tip now refits on `resize`. Then you went and made **every** field explain itself through one `Info` parameter — nine components, one `Field`, one place the tip lives. That's not a feature, that's a *doctrine*~ ♡ ### Verdict: ✅ Looks good to me~ #### ✅ What I liked~ - **The nine-way passthrough is complete.** I grepped every `<Field ` consumer in the library: all nine (TextField, TextArea, Select, Slider, ColorField, RadioCards, Combobox, FileUpload, MaskedSecretField) pass `Info="@Info"`, `InputFieldBase` covers 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. ♪ - **The tip sits beside the label, never in it.** A label's accessible text names its input; a button inside it would corrupt that name. Putting `InfoTip` in a `.kg-field__head` sibling keeps `aria-describedby` on the button where it belongs. And `Without_info_the_label_keeps_its_old_place` proves the old markup is byte-shape identical — no surprise `<div>` for every existing field in every app. - **`FieldInfoTests` is a real theory, not a vibe** — it runs both cases over all nine components and asserts the negative space too (`.kg-field__msg` stays empty, no tip without `Info`, no head without `Info`). - **The resize refit is scoped exactly right**: only `--open` tips refit, because a hover tip refits itself on the next `pointerover` anyway. Restrained~ **Mutation probes** (a reviewer who trusts is a reviewer who gets betrayed~): 1. Dropped the `&& Info is not null` guard in `Field.razor` (head always renders) → `Without_info_the_label_keeps_its_old_place` **failed on all nine fields**. Killed. ♡ 2. Removed the `InfoTip` from the head → testhost was OOM-killed by sandbox load twice (retry with `-m:1` too), so I couldn't watch it die — but the theory does `head.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 && TitleContent` neutralized) touch unchanged code — both killed then, cited now. #### 💡 Little ideas (non-blocking)~ 1. **`infotip.js` (resize)** — a *hovered* (unpinned) tip keeps its `--kg-infotip-shift` if 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)*
bjoern merged commit cd433ea5cf into main 2026-10-03 16:39:07 +02:00
bjoern deleted branch feat/infotip 2026-10-03 16:39:08 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
4 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/Kagaku.UI!31
No description provided.