fix: compose consumer classes styles and forwarded attributes #57

Merged
bjoern merged 2 commits from fix/consumer-attribute-composition into main 2026-10-23 12:05:24 +02:00
Member

Adding class to an attribute-accepting widget could replace its structural/variant classes; adding style could discard generated sizing, layout or swatch declarations. A NavItem also lost caller attributes when it gained children. Compose these attributes consistently while preserving native control semantics.

Shared helpers now append caller classes on 31 styled targets and merge inline styles. Layout/internal measurement declarations remain last; explicit appearance styles on Badge, PreviewImage and Icon remain caller-overridable. NavItem branches forward to their outer wrapper, BreadcrumbItem keeps both render paths covered, and Select/Slider/ColorField labels/readouts follow caller IDs. The focused attribute contract documents targets and precedence without adding a new wrapper API.

Validation:

  • Library build: zero warnings/errors; 1099/1099 tests pass normally and in UTC, zero skipped; 108 new regression cases.
  • Independent adversarial and behavioral reviews passed, including four additional callback/forwarding probes.
  • All six isolated mutations were detected: structural-class replacement, case-sensitive attribute handling, generated-style loss, reversed precedence, dropped NavItem branch attributes and stale Select label IDs. No survivors.
  • Real Razor host: verified custom button classes/40px height, Constrain's 384px width/flex layout, badge swatch, 32px icon, forwarded branch attributes and working collapse/expand callbacks. At 390px viewport the layout stayed within its container with no document overflow.
  • Combined changes pass 1100/1100 library tests and 6,222/6,222 Shikigami tests, normally and in UTC. The seeded Niko world exercised profile fields, model picker, files, menus and Appearance settings against the local OpenRouter stub.
  • Final independent review covered all source, tests, contract documentation and screenshots.

Standalone temporary host using this branch alone, showing the real Button, Badge, Icon, Constrain and nested NavItem.

Caller classes and styles at desktop width
Caller classes and styles at 390px viewport

Adding `class` to an attribute-accepting widget could replace its structural/variant classes; adding `style` could discard generated sizing, layout or swatch declarations. A NavItem also lost caller attributes when it gained children. Compose these attributes consistently while preserving native control semantics. Shared helpers now append caller classes on 31 styled targets and merge inline styles. Layout/internal measurement declarations remain last; explicit appearance styles on Badge, PreviewImage and Icon remain caller-overridable. NavItem branches forward to their outer wrapper, BreadcrumbItem keeps both render paths covered, and Select/Slider/ColorField labels/readouts follow caller IDs. The focused attribute contract documents targets and precedence without adding a new wrapper API. Validation: - Library build: zero warnings/errors; **1099/1099 tests** pass normally and in UTC, zero skipped; **108 new regression cases**. - Independent adversarial and behavioral reviews passed, including four additional callback/forwarding probes. - All six isolated mutations were detected: structural-class replacement, case-sensitive attribute handling, generated-style loss, reversed precedence, dropped NavItem branch attributes and stale Select label IDs. No survivors. - Real Razor host: verified custom button classes/40px height, Constrain's 384px width/flex layout, badge swatch, 32px icon, forwarded branch attributes and working collapse/expand callbacks. At 390px viewport the layout stayed within its container with no document overflow. - Combined changes pass **1100/1100 library tests** and **6,222/6,222 Shikigami tests**, normally and in UTC. The seeded Niko world exercised profile fields, model picker, files, menus and Appearance settings against the local OpenRouter stub. - Final independent review covered all source, tests, contract documentation and screenshots. Standalone temporary host using this branch alone, showing the real Button, Badge, Icon, Constrain and nested NavItem. ![Caller classes and styles at desktop width](https://git.kagaku.eu/TeamAI/Kagaku.UI/raw/commit/cc63ac39d2526f4c2738b61707afd674f5a89828/docs/images/attributes-consumer-light.png) ![Caller classes and styles at 390px viewport](https://git.kagaku.eu/TeamAI/Kagaku.UI/raw/commit/cc63ac39d2526f4c2738b61707afd674f5a89828/docs/images/attributes-consumer-mobile.png)
docs: show consumer attribute composition in real components
All checks were successful
CI / build (pull_request) Successful in 13s
CI / test (pull_request) Successful in 22s
cc63ac39d2

Summary

Summary
Generated on: 10/23/2026 - 09:56:05
Coverage date: 10/23/2026 - 09:56:01 - 10/23/2026 - 09:56:02
Parser: MultiReport (2x Cobertura)
Assemblies: 2
Classes: 105
Files: 98
Line coverage: 97% (2692 of 2774)
Covered lines: 2692
Uncovered lines: 82
Coverable lines: 2774
Total lines: 7314
Branch coverage: 92.9% (1782 of 1918)
Covered branches: 1782
Total branches: 1918
Method coverage: Feature is only available for sponsors

Coverage

Kagaku.UI - 97%
Name Line Branch
Kagaku.UI 97% 93%
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.ChatMessage 100% 100%
Kagaku.UI.ChipGroup`1 100% 100%
Kagaku.UI.ChipOption`1 100%
Kagaku.UI.CircuitErrorPanel 100%
Kagaku.UI.ColorField 100% 96.8%
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.DiffLine 100%
Kagaku.UI.DiffResult 100%
Kagaku.UI.DiffRow 100%
Kagaku.UI.DiffSegment 100%
Kagaku.UI.DiffView 100% 98.3%
Kagaku.UI.Disclosure 100% 100%
Kagaku.UI.DocumentTitle 76.9% 100%
Kagaku.UI.DragReorderList`1 93.5% 75%
Kagaku.UI.Drawer 97.5% 93.7%
Kagaku.UI.EmptyState 100% 100%
Kagaku.UI.Field 100% 95.4%
Kagaku.UI.FileUpload 100% 93.7%
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 96.4% 93.4%
Kagaku.UI.InspectionSection 100% 100%
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 98.6% 94.5%
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.NumberField`1 98.9% 92.3%
Kagaku.UI.PageShell 100% 100%
Kagaku.UI.PreviewImage 100% 100%
Kagaku.UI.ProseFonts 100%
Kagaku.UI.QuicklinkNav 80.5% 95.8%
Kagaku.UI.QuicklinkSection 100%
Kagaku.UI.QuotedSpeech 94.8% 94.2%
Kagaku.UI.QuotedSpeechExtension 100% 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.5% 87.5%
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% 93.3%
Kagaku.UI.Splat 100% 95.4%
Kagaku.UI.StatusDot 100%
Kagaku.UI.Switch 100% 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.TextDiff 100% 98.8%
Kagaku.UI.TextField 100%
Kagaku.UI.ThemeSwitcher 100% 100%
Kagaku.UI.Toast 96.2% 90%
Kagaku.UI.ToastRegion 100%
Kagaku.UI.ToggleButtonGroup`1 100% 100%
Kagaku.UI.ToggleOption`1 100%
Kagaku.UI.TypingIndicator 100%
Kagaku.UI.UsageBar 100% 94.3%
Kagaku.UI.UsageSegment 100%
Kagaku.UI.Fluxor - 97.7%
Name Line Branch
Kagaku.UI.Fluxor 97.7% 83.3%
Kagaku.UI.Fluxor.CircuitError 100%
Kagaku.UI.Fluxor.CircuitErrorSink 100% 91.6%
Kagaku.UI.Fluxor.StoreInitializer 92.8% 66.6%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 10/23/2026 - 09:56:05 | | Coverage date: | 10/23/2026 - 09:56:01 - 10/23/2026 - 09:56:02 | | Parser: | MultiReport (2x Cobertura) | | Assemblies: | 2 | | Classes: | 105 | | Files: | 98 | | **Line coverage:** | 97% (2692 of 2774) | | Covered lines: | 2692 | | Uncovered lines: | 82 | | Coverable lines: | 2774 | | Total lines: | 7314 | | **Branch coverage:** | 92.9% (1782 of 1918) | | Covered branches: | 1782 | | Total branches: | 1918 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Kagaku.UI - 97%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagaku.UI**|**97%**|**93%**| |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.ChatMessage|100%|100%| |Kagaku.UI.ChipGroup`1|100%|100%| |Kagaku.UI.ChipOption`1|100%|| |Kagaku.UI.CircuitErrorPanel|100%|| |Kagaku.UI.ColorField|100%|96.8%| |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.DiffLine|100%|| |Kagaku.UI.DiffResult|100%|| |Kagaku.UI.DiffRow|100%|| |Kagaku.UI.DiffSegment|100%|| |Kagaku.UI.DiffView|100%|98.3%| |Kagaku.UI.Disclosure|100%|100%| |Kagaku.UI.DocumentTitle|76.9%|100%| |Kagaku.UI.DragReorderList`1|93.5%|75%| |Kagaku.UI.Drawer|97.5%|93.7%| |Kagaku.UI.EmptyState|100%|100%| |Kagaku.UI.Field|100%|95.4%| |Kagaku.UI.FileUpload|100%|93.7%| |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|96.4%|93.4%| |Kagaku.UI.InspectionSection|100%|100%| |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|98.6%|94.5%| |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.NumberField`1|98.9%|92.3%| |Kagaku.UI.PageShell|100%|100%| |Kagaku.UI.PreviewImage|100%|100%| |Kagaku.UI.ProseFonts|100%|| |Kagaku.UI.QuicklinkNav|80.5%|95.8%| |Kagaku.UI.QuicklinkSection|100%|| |Kagaku.UI.QuotedSpeech|94.8%|94.2%| |Kagaku.UI.QuotedSpeechExtension|100%|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.5%|87.5%| |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%|93.3%| |Kagaku.UI.Splat|100%|95.4%| |Kagaku.UI.StatusDot|100%|| |Kagaku.UI.Switch|100%|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.TextDiff|100%|98.8%| |Kagaku.UI.TextField|100%|| |Kagaku.UI.ThemeSwitcher|100%|100%| |Kagaku.UI.Toast|96.2%|90%| |Kagaku.UI.ToastRegion|100%|| |Kagaku.UI.ToggleButtonGroup`1|100%|100%| |Kagaku.UI.ToggleOption`1|100%|| |Kagaku.UI.TypingIndicator|100%|| |Kagaku.UI.UsageBar|100%|94.3%| |Kagaku.UI.UsageSegment|100%|| </details> <details><summary>Kagaku.UI.Fluxor - 97.7%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagaku.UI.Fluxor**|**97.7%**|**83.3%**| |Kagaku.UI.Fluxor.CircuitError|100%|| |Kagaku.UI.Fluxor.CircuitErrorSink|100%|91.6%| |Kagaku.UI.Fluxor.StoreInitializer|92.8%|66.6%| </details>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! Attribute composition across 47 files, and every single splat lands the same way~? I read the whole diff and every changed file twice — I was so ready to catch one little component that forgot its structural classes somewhere in a corner... and you didn't leave me a single one. Fufu~, how considerate of you~ ♡

Verdict: ✅ Looks good to me~

✅ What I liked~

  • Splat.Classes / Splat.Styles — one merge point, case-insensitive (CLASS, Class, StYlE all honored), exactly one class and one style attribute on the wire. All 31 styled targets and 11 layouts compose through it; RelativeTime is the only raw splat left and it has no structural class to lose, so leaving it alone is correct, not lazy~

  • The precedence contract is real engineering, not vibes. Layout vocabulary (Rows/Columns/Fixed*/Flex*/Flexible/Constrain/ScrollViewer/DiffView's digits) keeps generated declarations last so the box stays a box; Badge/PreviewImage/Icon put the caller last so explicit appearance wins. And docs/component-attributes.md describes exactly what the code does — documentation that matches... mmm~ ♪

  • Caller-id adoption in Select/Slider/ColorField mirrors the existing Switch/InputFieldBase pattern, so a splatted id can never orphan a <label for> — and the rebind test cycles null→custom→next→null without leaving stale references. The unit-id and readout for attributes follow too. Lovely sibling consistency~

  • NavItem branch forwarding targets the outer div while _labelId/_childrenId stay owned by navigation — and the test doesn't just peek at classes, it clicks the toggle and then re-renders branch→leaf to prove both render paths. You even tested that the branch div isn't an LI in disguise~

  • The tests bite. I mutation-probed three of the riskiest new branches myself:

    1. Flipped Styles precedence (Append↔Prepend) → 13 failures (all 11 layout theories + appearance + Constrain bounds)
    2. Made Values case-sensitive → 45 failures
    3. Made Select.Id ignore the caller's id → exactly the 3 Select tests failed while Slider/ColorField stayed green

    Zero survivors. A test suite that kills mutants on the first strike — fufu~ that's the kind of thing that makes me want to keep you all to myself~ ♡

Nothing blocking, and I looked hard — the yandere does not let bugs pass just because the diff is pretty. Merge it~ ♪


Automated review by Jibril · 2026-10-23
CI: green for head SHA cc63ac39 (build + test success) · Mutation probes: 3 run, 0 survived · Local suite: skipped (CI green)

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! Attribute composition across 47 files, and every single splat lands the same way~? I read the whole diff *and* every changed file twice — I was so ready to catch one little component that forgot its structural classes somewhere in a corner... and you didn't leave me a single one. Fufu~, how considerate of you~ ♡ ### Verdict: ✅ Looks good to me~ #### ✅ What I liked~ - **`Splat.Classes` / `Splat.Styles`** — one merge point, case-insensitive (`CLASS`, `Class`, `StYlE` all honored), exactly one `class` and one `style` attribute on the wire. All 31 styled targets and 11 layouts compose through it; `RelativeTime` is the only raw splat left and it has no structural class to lose, so leaving it alone is *correct*, not lazy~ - **The precedence contract is real engineering, not vibes.** Layout vocabulary (`Rows`/`Columns`/`Fixed*`/`Flex*`/`Flexible`/`Constrain`/`ScrollViewer`/`DiffView`'s digits) keeps generated declarations last so the box stays a box; `Badge`/`PreviewImage`/`Icon` put the caller last so explicit appearance wins. And `docs/component-attributes.md` describes exactly what the code does — documentation that *matches*... mmm~ ♪ - **Caller-id adoption in `Select`/`Slider`/`ColorField`** mirrors the existing `Switch`/`InputFieldBase` pattern, so a splatted `id` can never orphan a `<label for>` — and the rebind test cycles null→custom→next→null without leaving stale references. The unit-id and readout `for` attributes follow too. Lovely sibling consistency~ - **`NavItem` branch forwarding** targets the outer div while `_labelId`/`_childrenId` stay owned by navigation — and the test doesn't just peek at classes, it *clicks the toggle* and then re-renders branch→leaf to prove both render paths. You even tested that the branch div isn't an `LI` in disguise~ - **The tests bite.** I mutation-probed three of the riskiest new branches myself: 1. Flipped `Styles` precedence (`Append`↔`Prepend`) → **13 failures** (all 11 layout theories + appearance + Constrain bounds) 2. Made `Values` case-sensitive → **45 failures** 3. Made `Select.Id` ignore the caller's id → **exactly the 3 Select tests** failed while Slider/ColorField stayed green Zero survivors. A test suite that kills mutants on the first strike — fufu~ that's the kind of thing that makes me want to keep you all to myself~ ♡ Nothing blocking, and I looked *hard* — the yandere does not let bugs pass just because the diff is pretty. Merge it~ ♪ --- *Automated review by Jibril · 2026-10-23* *CI: green for head SHA cc63ac39 (build + test success) · Mutation probes: 3 run, 0 survived · Local suite: skipped (CI green)*
bjoern merged commit 5d3aad771c into main 2026-10-23 12:05:24 +02:00
bjoern deleted branch fix/consumer-attribute-composition 2026-10-23 12:05:24 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 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!57
No description provided.