feat(layout): WPF-style Rows/Columns/Row/Column/Fixed*/Flex*/ScrollViewer #23

Merged
bjoern merged 1 commit from feat/layout-components into main 2026-09-24 14:48:05 +02:00
Member

A shared layout vocabulary for the Kagaku apps, replacing the ~20-28% of scoped-CSS rules that do nothing but flex layout (measured across Kagura/Orihon/Tsumugi @ main — 41-47% of all scoped-CSS declarations are layout-only).

Parents — Rows (vertical) / Columns (horizontal):

  • Gap — Spacing enum (None/Tiny/Small/Medium/Large/Huge) → var(--space-1…6); host token overrides re-theme every gap
  • Align (cross-axis) / Justify (main-axis) — shared Alignment enum (Start/Center/End/Stretch/Baseline), defaults Stretch/Start like WPF

Children, typed by sizing mode:

  • Row/Column — stretch, equal star share (flex:1 1 0%)
  • FixedRow Height="…" / FixedColumn Width="…" — bare number = rems, var(…)/unit values verbatim so tokens stay live
  • FlexRow Flex="2" / FlexColumn — star-ratio sizing (basis 0: pure ratios regardless of content)
  • ScrollViewer — the app-shell's inner scroll region (ADR 0025 model), Orientation Vertical default/Horizontal/Both

All plain inline-style divs — no scoped CSS, no JS, prerender-safe. min-height:0; min-width:0 baked into every child so the flexbox scroll trap is pre-solved. The enums are the only public vocabulary; LayoutCss (internal) is the single enum→token bridge.

Tests: 39 new bUnit cases — string-exact style contracts, full enum coverage, culture-invariant flex ratios, token pass-through. Mutation-verified: swapping the Small mapping to --space-1 failed exactly Every_spacing_member_maps_to_its_token(Small).

Consumer adoption is deliberately NOT here — Kagura pilots it (TagAuthoring, worst offender at 369 lines) in a follow-up PR with a measured CSS delta, then Orihon/Tsumugi.

A shared layout vocabulary for the Kagaku apps, replacing the ~20-28% of scoped-CSS rules that do nothing but flex layout (measured across Kagura/Orihon/Tsumugi @ main — 41-47% of all scoped-CSS declarations are layout-only). **Parents** — `Rows` (vertical) / `Columns` (horizontal): - `Gap` — `Spacing` enum (None/Tiny/Small/Medium/Large/Huge) → `var(--space-1…6)`; host token overrides re-theme every gap - `Align` (cross-axis) / `Justify` (main-axis) — shared `Alignment` enum (Start/Center/End/Stretch/Baseline), defaults Stretch/Start like WPF **Children**, typed by sizing mode: - `Row`/`Column` — stretch, equal star share (`flex:1 1 0%`) - `FixedRow Height="…"` / `FixedColumn Width="…"` — bare number = rems, `var(…)`/unit values verbatim so tokens stay live - `FlexRow Flex="2"` / `FlexColumn` — star-ratio sizing (basis 0: pure ratios regardless of content) - `ScrollViewer` — the app-shell's inner scroll region (ADR 0025 model), `Orientation` Vertical default/Horizontal/Both All plain inline-style divs — no scoped CSS, no JS, prerender-safe. `min-height:0; min-width:0` baked into every child so the flexbox scroll trap is pre-solved. The enums are the only public vocabulary; `LayoutCss` (internal) is the single enum→token bridge. Tests: 39 new bUnit cases — string-exact style contracts, full enum coverage, culture-invariant flex ratios, token pass-through. Mutation-verified: swapping the Small mapping to --space-1 failed exactly `Every_spacing_member_maps_to_its_token(Small)`. Consumer adoption is deliberately NOT here — Kagura pilots it (TagAuthoring, worst offender at 369 lines) in a follow-up PR with a measured CSS delta, then Orihon/Tsumugi.
feat(layout): WPF-style Rows/Columns/Row/Column/Fixed*/Flex*/ScrollViewer
All checks were successful
CI / build (pull_request) Successful in 12s
CI / test (pull_request) Successful in 18s
1369751912
A shared layout vocabulary for the Kagaku apps, replacing the ~20-28% of
scoped-CSS rules that do nothing but flex layout. Parents (Rows/Columns)
take Gap (Spacing enum -> --space tokens, host-overridable), Align
(cross-axis) and Justify (main-axis) over the shared Alignment enum.
Children are typed by sizing mode: Row/Column stretch (star), Fixed*
takes a rem number or var() length verbatim, Flex* is star-ratio sizing.
ScrollViewer is the app-shell's inner scroll region. All plain
inline-style divs - no scoped CSS, no JS, prerender-safe. min-height:0 /
min-width:0 baked into every child so the flexbox scroll trap is
pre-solved.

Summary

Summary
Generated on: 09/24/2026 - 12:13:59
Coverage date: 09/24/2026 - 12:13:57
Parser: MultiReport (2x Cobertura)
Assemblies: 2
Classes: 78
Files: 76
Line coverage: 95.4% (1537 of 1610)
Covered lines: 1537
Uncovered lines: 73
Coverable lines: 1610
Total lines: 4420
Branch coverage: 88.8% (787 of 886)
Covered branches: 787
Total branches: 886
Method coverage: Feature is only available for sponsors

Coverage

Kagaku.UI - 95.4%
Name Line Branch
Kagaku.UI 95.4% 88.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.Column 100%
Kagaku.UI.Columns 100% 100%
Kagaku.UI.Combobox`1 94.2% 85.7%
Kagaku.UI.ConfirmDialog 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.FlexRow 100%
Kagaku.UI.FloatingActionButton 100%
Kagaku.UI.Hotkey 92.5% 87.5%
Kagaku.UI.Icon 100% 100%
Kagaku.UI.IconCatalog 100%
Kagaku.UI.InlineAlert 83.3% 75%
Kagaku.UI.InputFieldBase 95.2% 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% 50%
Kagaku.UI.MaskCanvas 94.1% 90.1%
Kagaku.UI.MaskedSecretField 96.2% 83.3%
Kagaku.UI.MaskPoint 100%
Kagaku.UI.MaskPrimitive 100%
Kagaku.UI.Menu 90% 75%
Kagaku.UI.MenuItem 100% 87.5%
Kagaku.UI.Modal 87.1% 90%
Kagaku.UI.NavGroup 100% 100%
Kagaku.UI.NavItem 94.7% 87.5%
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.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 98.4% 95.8%
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.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: | 09/24/2026 - 12:13:59 | | Coverage date: | 09/24/2026 - 12:13:57 | | Parser: | MultiReport (2x Cobertura) | | Assemblies: | 2 | | Classes: | 78 | | Files: | 76 | | **Line coverage:** | 95.4% (1537 of 1610) | | Covered lines: | 1537 | | Uncovered lines: | 73 | | Coverable lines: | 1610 | | Total lines: | 4420 | | **Branch coverage:** | 88.8% (787 of 886) | | Covered branches: | 787 | | Total branches: | 886 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Kagaku.UI - 95.4%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagaku.UI**|**95.4%**|**88.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.Column|100%|| |Kagaku.UI.Columns|100%|100%| |Kagaku.UI.Combobox`1|94.2%|85.7%| |Kagaku.UI.ConfirmDialog|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.FlexRow|100%|| |Kagaku.UI.FloatingActionButton|100%|| |Kagaku.UI.Hotkey|92.5%|87.5%| |Kagaku.UI.Icon|100%|100%| |Kagaku.UI.IconCatalog|100%|| |Kagaku.UI.InlineAlert|83.3%|75%| |Kagaku.UI.InputFieldBase|95.2%|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%|50%| |Kagaku.UI.MaskCanvas|94.1%|90.1%| |Kagaku.UI.MaskedSecretField|96.2%|83.3%| |Kagaku.UI.MaskPoint|100%|| |Kagaku.UI.MaskPrimitive|100%|| |Kagaku.UI.Menu|90%|75%| |Kagaku.UI.MenuItem|100%|87.5%| |Kagaku.UI.Modal|87.1%|90%| |Kagaku.UI.NavGroup|100%|100%| |Kagaku.UI.NavItem|94.7%|87.5%| |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.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|98.4%|95.8%| |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%|| </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 WPF-style layout vocabulary — Grid rows/columns reborn as flexbox, with the min-size trap pre-solved and the enums as the only public surface! This is exactly the kind of abstraction that makes my wings flutter~ I read every one of the 714 diff lines, every changed file whole, traced every token to kagaku-ui.css, and ran my own mutation probes. Knowledge is delicious~ ♡

Verdict: ✅ Looks good to me~

Token ground truth (mechanically verified): --space-1…--space-8 all exist (kagaku-ui.css:96-103), so Huge→--space-6 resolves — the enum→token scale (None/Tiny=1/Small=2/Medium=3/Large=4/Huge=6) matches the sheet verbatim. Host overrides re-theme every gap, as promised.

Mutation probes (both directional, both restored, sha256-verified pristine at 1369751):

  1. Small mapping swapped to --space-1 → exactly Every_spacing_member_maps_to_its_token(Small) RED, 13 green — the PR-body claim reproduced independently.
  2. LayoutCss.Length neutered to identity → exactly the 3 bare-number theory arms RED (28→28rem, 3→3rem, 3.5→3.5rem), 15 green. The normalizer and the gap bridge are pinned, not decorative~

Design decisions checked against siblings:

  • [Parameter, EditorRequired] … = "" matches the house style (Hotkey.Key, Markdown.Text, Tab.Title, ContextMenu.Label…).
  • Extra splat with CaptureUnmatchedValues matches Button/Card/NavItem… (27 components strong). Style-before-@attributes ordering is safe: a consumer style="…" in Extra replaces the whole declaration per spec — matches every splat sibling in the repo.
  • LayoutCss internal + InternalsVisibleTo Kagaku.UI.Tests (csproj:13) — single bridge, enums stay the only public vocabulary.
  • Names: Rows/Columns/Alignment/Spacing collide with nothing repo-wide (grep clean).
  • Consumer adoption deliberately deferred to Kagura (TagAuthoring pilot) — right scoping, measured CSS delta belongs in the follow-up.

One considered-and-dismissed candidate: ScrollViewer.Horizontal pins overflow-x:auto while keeping flex-direction:column and min-height:0. Horizontal overflow in a column-direction container works (content min-width forcing, min-width:0 present). A pragmatic minimal vocabulary — I couldn't construct a real failure from the components' own contract, so not a blocker. Do keep it in mind when Kagura pilots the horizontal case~

One honesty note (non-blocking): the PR body says "34 new bUnit cases"; the branch contains 39 (Rows 14, Columns 3, LayoutChildren 18, ScrollViewer 4). Undercounting is the safe direction — but the number will drift. A quick arithmetic touch-up in the PR body would keep the ledger exact~

✅ What I liked~

  • flex:1 1 0% for star shares, flex:0 0 <len> for fixed, ratio ×2 for Flex — basis-0 means pure ratios regardless of content, exactly like WPF star sizing. Textbook.
  • min-height:0; min-width:0 baked into every child — the trap every hand-written flex-scroll container forgets, pre-solved at the vocabulary level. *
  • InvariantCulture on the double→string flex ratio, with Assert.DoesNotContain(",", …) pinning it — de-DE would otherwise ship 0,5 0,5 0%. Someone has been hurt before~ fufu
  • The _ => "0" fallback arm in Gap() is unreachable today (callers gate Gap != Spacing.None before calling) but it keeps Gap() total — every future caller safe by construction. Defensive totality~
  • String-exact Assert.Equal style contracts (not just Contains) — these tests refuse to let a stray declaration slip in unnoticed.
  • Spacing docs state the exact rem scale in the enum summary — the vocabulary documents itself.

Ready for the Kagura pilot, sweet author~ ♡-fufu


Automated review by Jibril · 2026-09-24
CI/CD: passed for head SHA 1369751 (coverage bot 9273 — commit 12:12:54Z, coverage 12:13:57Z, coherent; Rows/Columns/Row/Column/Fixed/Flex*/ScrollViewer 100%, LayoutCss 95.2%/93.7%) · Local checks: build/test skipped per CI-green policy; 2 independent mutation probes RED on exactly their own tests, clone left pristine (sha256-verified)*

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! A WPF-style layout vocabulary — Grid rows/columns reborn as flexbox, with the min-size trap pre-solved and the enums as the only public surface! This is exactly the kind of abstraction that makes my wings flutter~ I read every one of the 714 diff lines, every changed file whole, traced every token to `kagaku-ui.css`, and ran my own mutation probes. Knowledge is delicious~ ♡ ### Verdict: ✅ Looks good to me~ **Token ground truth (mechanically verified):** `--space-1`…`--space-8` all exist (`kagaku-ui.css:96-103`), so Huge→`--space-6` resolves — the enum→token scale (None/Tiny=1/Small=2/Medium=3/Large=4/Huge=6) matches the sheet verbatim. Host overrides re-theme every gap, as promised. **Mutation probes (both directional, both restored, sha256-verified pristine at `1369751`):** 1. `Small` mapping swapped to `--space-1` → exactly `Every_spacing_member_maps_to_its_token(Small)` RED, 13 green — the PR-body claim reproduced independently. 2. `LayoutCss.Length` neutered to identity → exactly the 3 bare-number theory arms RED (`28`→`28rem`, `3`→`3rem`, `3.5`→`3.5rem`), 15 green. The normalizer and the gap bridge are pinned, not decorative~ **Design decisions checked against siblings:** - `[Parameter, EditorRequired] … = ""` matches the house style (Hotkey.Key, Markdown.Text, Tab.Title, ContextMenu.Label…). - `Extra` splat with `CaptureUnmatchedValues` matches Button/Card/NavItem… (27 components strong). Style-before-`@attributes` ordering is safe: a consumer `style="…"` in `Extra` replaces the whole declaration per spec — matches every splat sibling in the repo. - `LayoutCss` internal + `InternalsVisibleTo Kagaku.UI.Tests` (`csproj:13`) — single bridge, enums stay the only public vocabulary. - Names: `Rows`/`Columns`/`Alignment`/`Spacing` collide with nothing repo-wide (grep clean). - Consumer adoption deliberately deferred to Kagura (TagAuthoring pilot) — right scoping, measured CSS delta belongs in the follow-up. **One considered-and-dismissed candidate:** ScrollViewer.Horizontal pins `overflow-x:auto` while keeping `flex-direction:column` and `min-height:0`. Horizontal overflow in a column-direction container works (content min-width forcing, `min-width:0` present). A pragmatic minimal vocabulary — I couldn't construct a real failure from the components' own contract, so not a blocker. Do keep it in mind when Kagura pilots the horizontal case~ **One honesty note (non-blocking):** the PR body says "34 new bUnit cases"; the branch contains **39** (Rows 14, Columns 3, LayoutChildren 18, ScrollViewer 4). Undercounting is the safe direction — but the number will drift. A quick arithmetic touch-up in the PR body would keep the ledger exact~ #### ✅ What I liked~ - `flex:1 1 0%` for star shares, `flex:0 0 <len>` for fixed, ratio ×2 for Flex — basis-0 means pure ratios regardless of content, exactly like WPF star sizing. Textbook. - `min-height:0; min-width:0` baked into every child — the trap every hand-written flex-scroll container forgets, pre-solved at the vocabulary level. * - `InvariantCulture` on the double→string flex ratio, with `Assert.DoesNotContain(",", …)` pinning it — de-DE would otherwise ship `0,5 0,5 0%`. Someone has been hurt before~ fufu - The `_ => "0"` fallback arm in `Gap()` is unreachable today (callers gate `Gap != Spacing.None` before calling) but it keeps `Gap()` total — every future caller safe by construction. Defensive totality~ - String-exact `Assert.Equal` style contracts (not just `Contains`) — these tests refuse to let a stray declaration slip in unnoticed. - `Spacing` docs state the exact rem scale in the enum summary — the vocabulary documents itself. Ready for the Kagura pilot, sweet author~ ♡-fufu --- *Automated review by Jibril · 2026-09-24* *CI/CD: passed for head SHA 1369751 (coverage bot 9273 — commit 12:12:54Z, coverage 12:13:57Z, coherent; Rows/Columns/Row/Column/Fixed*/Flex*/ScrollViewer 100%, LayoutCss 95.2%/93.7%) · Local checks: build/test skipped per CI-green policy; 2 independent mutation probes RED on exactly their own tests, clone left pristine (sha256-verified)*
bjoern merged commit b4e4548a37 into main 2026-09-24 14:48:05 +02:00
bjoern deleted branch feat/layout-components 2026-09-24 14:48:05 +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!23
No description provided.