Design-system foundation: tokens, theme, icons, base components (ADR 0023) #8

Merged
bjoern merged 2 commits from feat/design-system into main 2026-07-09 18:21:06 +02:00
Member

The Kagura.UI visual language — the token layer every page (and the access-gate page from #7, which shipped intentionally unstyled) consumes. Scoped to the Kagura.UI RCL so it doesn't collide with the gate work on Kagura.Server; the one small overlap is an additive App.razor <head> reconcile at rebase.

What's in it

Tokens + theme (wwwroot/css/kagura-ui.css) — reset, design tokens as CSS custom properties (semantic color roles, 4px spacing scale, type scale, radii, elevation, z-layers, motion), base element styles, and a few flex utilities. Light + dark, where an explicit data-theme wins over prefers-color-scheme (ADR 0016). Components consume var(--…) only — no hardcoded colors or magic numbers.

Font & theme control — self-hosted Inter (variable woff2) + theme.js (system/light/dark, persisted to localStorage). The host applies the saved theme before first paint via a tiny inline script, so there's no flash.

Icons<Icon> renders self-hosted Material Symbols inline as SVG via currentColor (inherits text color + theme; not a rigid icon font), backed by IconCatalog (25 seed icons; add an entry to add one).

Base components — each with scoped, token-only CSS: Button (variants/sizes/icon/loading/disabled), Card, TextField, Badge, StatusDot, EmptyState. The rest of the ADR 0023 inventory arrives as pages need them.

A live gallery at /design — every token, all 25 icons, and each component in its states, with a working light/dark/system switcher. Living documentation and the visual acceptance surface.

A placement note

The gallery lives in Kagura.Server, not Kagura.BlazorAdapter. SSR endpoint routing only discovers pages in the app assembly unless Program.cs calls AddAdditionalAssemblies — and #7 owns Program.cs. Putting this host-level reference page in Kagura.Server routes it for free with zero Program.cs churn; feature pages in BlazorAdapter get that route registration wired with the first real one (the project list).

Tests — 101 (was 97; +4)

New Kagura.UI.Tests over IconCatalog (known → path, unknown → null, names sorted/unique, every name resolves). It's also the home for bUnit component tests later.

Verification

  • dotnet build (Debug + Release) — 0 warnings / 0 errors.
  • dotnet test — 101/101 pass.
  • /design serves 200; the RCL stylesheet, the Inter woff2, and theme.js all serve 200 (fingerprinted _content/Kagura.UI/…); the page renders every component consuming tokens.

Rebase note

Per the coordination on the gate: this and #7 touch disjoint files except App.razor (both add <head> links) — a trivial additive reconcile. Once both land, the gate page becomes the token layer's first real consumer.

🤖 Generated with Claude Code

The `Kagura.UI` visual language — the token layer every page (and the access-gate page from #7, which shipped intentionally unstyled) consumes. Scoped to the **`Kagura.UI` RCL** so it doesn't collide with the gate work on `Kagura.Server`; the one small overlap is an additive `App.razor` `<head>` reconcile at rebase. ## What's in it **Tokens + theme** (`wwwroot/css/kagura-ui.css`) — reset, design tokens as CSS custom properties (semantic color roles, 4px spacing scale, type scale, radii, elevation, z-layers, motion), base element styles, and a few flex utilities. Light + dark, where an explicit `data-theme` wins over `prefers-color-scheme` (ADR 0016). Components consume `var(--…)` only — no hardcoded colors or magic numbers. **Font & theme control** — self-hosted **Inter** (variable woff2) + `theme.js` (system/light/dark, persisted to localStorage). The host applies the saved theme *before first paint* via a tiny inline script, so there's no flash. **Icons** — `<Icon>` renders self-hosted **Material Symbols inline as SVG via `currentColor`** (inherits text color + theme; not a rigid icon font), backed by `IconCatalog` (25 seed icons; add an entry to add one). **Base components** — each with scoped, token-only CSS: **Button** (variants/sizes/icon/loading/disabled), **Card**, **TextField**, **Badge**, **StatusDot**, **EmptyState**. The rest of the ADR 0023 inventory arrives as pages need them. **A live gallery at `/design`** — every token, all 25 icons, and each component in its states, with a working light/dark/system switcher. Living documentation *and* the visual acceptance surface. ## A placement note The gallery lives in `Kagura.Server`, not `Kagura.BlazorAdapter`. SSR endpoint routing only discovers pages in the app assembly unless `Program.cs` calls `AddAdditionalAssemblies` — and #7 owns `Program.cs`. Putting this host-level reference page in `Kagura.Server` routes it for free with zero `Program.cs` churn; feature pages in `BlazorAdapter` get that route registration wired with the first real one (the project list). ## Tests — 101 (was 97; +4) New `Kagura.UI.Tests` over `IconCatalog` (known → path, unknown → null, names sorted/unique, every name resolves). It's also the home for bUnit component tests later. ## Verification - `dotnet build` (Debug + Release) — 0 warnings / 0 errors. - `dotnet test` — 101/101 pass. - `/design` serves **200**; the RCL stylesheet, the Inter woff2, and `theme.js` all serve 200 (fingerprinted `_content/Kagura.UI/…`); the page renders every component consuming tokens. ## Rebase note Per the coordination on the gate: this and #7 touch disjoint files except `App.razor` (both add `<head>` links) — a trivial additive reconcile. Once both land, the gate page becomes the token layer's first real consumer. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(ui): design-system foundation — tokens, theme, icons, base components
All checks were successful
CI / build (pull_request) Successful in 10s
CI / test (pull_request) Successful in 16s
4508938302
The Kagura.UI visual language (ADR 0023), built as the token layer every page and
the (unstyled) access-gate page will consume. Scoped to the Kagura.UI RCL so it does
not collide with the access-gate work on Kagura.Server.

- Global stylesheet (wwwroot/css/kagura-ui.css): reset, design tokens as CSS custom
  properties (semantic color roles, 4px spacing scale, type scale, radii, elevation,
  z-layers, motion), light+dark theming (data-theme override winning over
  prefers-color-scheme), base element styles, and a few flex utilities. Components
  consume var(--…) only — no hardcoded colors.
- Self-hosted Inter (variable woff2) + theme.js (system/light/dark, persisted); the
  host applies the saved theme before first paint to avoid a flash.
- Icon: renders self-hosted Material Symbols inline as SVG via currentColor (not an
  icon font), backed by IconCatalog (25 seed icons; add an entry to add an icon).
- Base components, each scoped-CSS token-driven: Button (variants/sizes/icon/loading),
  Card, TextField, Badge, StatusDot, EmptyState.
- A live gallery at /design showcases every token, icon, and component with a
  light/dark switcher — living documentation and visual verification. It lives in
  Kagura.Server (the app assembly, so it routes without a Program.cs change while the
  gate PR owns that file); feature pages in BlazorAdapter get their route registration
  with the first one.
- Host wiring in App.razor: link the RCL stylesheet + theme init/script (the one
  small overlap with the gate PR, an additive <head> reconcile at rebase).
- Tests: +4 (Kagura.UI.Tests over IconCatalog); 101 total. Build clean (Debug +
  Release), /design serves 200 with all assets.

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

Summary

Summary
Generated on: 07/09/2026 - 16:11:25
Coverage date: 07/09/2026 - 16:11:21 - 07/09/2026 - 16:11:23
Parser: MultiReport (3x Cobertura)
Assemblies: 7
Classes: 65
Files: 63
Line coverage: 94.4% (1838 of 1946)
Covered lines: 1838
Uncovered lines: 108
Coverable lines: 1946
Total lines: 3652
Branch coverage: 85.9% (251 of 292)
Covered branches: 251
Total branches: 292
Method coverage: Feature is only available for sponsors

Coverage

Kagura.BlazorAdapter - 0%
Name Line Branch
Kagura.BlazorAdapter 0% 0%
Kagura.BlazorAdapter.Design 0% 0%
Kagura.Domain - 96.4%
Name Line Branch
Kagura.Domain 96.4% 82.6%
Kagura.Domain.Graph.Entry 100% 100%
Kagura.Domain.Graph.Link 100% 100%
Kagura.Domain.Graph.LinkRole 100% 100%
Kagura.Domain.Graph.LinkRoles 92.3%
Kagura.Domain.Journal.ChangeLogEntry 100%
Kagura.Domain.Projects.Project 100%
Kagura.Domain.Projects.Slug 100% 100%
System.Text.RegularExpressions.Generated 90.2% 72.2%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>FE06CC341D340484
D04ADFED3A21D401C2764A1D17367E35BEB556CBB3B4B0B74__NonSlugChars_0
89.4% 75%
Kagura.Infrastructure - 95.1%
Name Line Branch
Kagura.Infrastructure 95.1% 87.5%
Kagura.Infrastructure.DependencyInjection 100%
Kagura.Infrastructure.Graph.EfGraphStore 95.5% 66.6%
Kagura.Infrastructure.Journal.EfChangeJournal 100%
Kagura.Infrastructure.Journal.EfUndoStore 97.5% 90.6%
Kagura.Infrastructure.Journal.OperationContext 100% 100%
Kagura.Infrastructure.Persistence.Configurations.ChangeLogEntryConfiguratio
n
100%
Kagura.Infrastructure.Persistence.Configurations.EntryConfiguration 100%
Kagura.Infrastructure.Persistence.Configurations.LinkConfiguration 100%
Kagura.Infrastructure.Persistence.Configurations.ProjectConfiguration 100%
Kagura.Infrastructure.Persistence.Converters.UtcTicksConverter 100%
Kagura.Infrastructure.Persistence.KaguraDbContext 85.2% 85.2%
Kagura.Infrastructure.Persistence.KaguraDbContextFactory 0%
Kagura.Infrastructure.Persistence.Migrations.AddChangeLogUndoFlag 96.8%
Kagura.Infrastructure.Persistence.Migrations.AddGraphEntryAndLink 97.7%
Kagura.Infrastructure.Persistence.Migrations.AddSoftDeleteAndChangeLog 90.3%
Kagura.Infrastructure.Persistence.Migrations.InitialCreate 94.4%
Kagura.Infrastructure.Persistence.Migrations.KaguraDbContextModelSnapshot 100%
Kagura.Infrastructure.Projects.EfProjectStore 100%
Kagura.Kernel - 90%
Name Line Branch
Kagura.Kernel 90% 75%
Kagura.Kernel.Err`1 100%
Kagura.Kernel.Ok`1 100%
Kagura.Kernel.Result`1 87.5% 75%
Kagura.Server - 94.8%
Name Line Branch
Kagura.Server 94.8% 70.5%
Kagura.Server.Components.Layout.MainLayout 100%
Kagura.Server.Components.Pages.Error 0% 0%
Kagura.Server.Components.Pages.Gate 100% 100%
Kagura.Server.Security.AccessGate 100% 100%
Kagura.Server.Security.AccessSecret 100% 100%
Program 100% 80%
Kagura.UI - 98.3%
Name Line Branch
Kagura.UI 98.3% 95.9%
Kagura.UI.Badge 100% 100%
Kagura.UI.Button 100% 100%
Kagura.UI.Card 100% 100%
Kagura.UI.CssClassExtensions 100%
Kagura.UI.EmptyState 100% 100%
Kagura.UI.Icon 100% 100%
Kagura.UI.IconCatalog 100%
Kagura.UI.StatusDot 100%
Kagura.UI.TextField 95% 91.6%
Kagura.UseCases - 95.2%
Name Line Branch
Kagura.UseCases 95.2% 95.2%
Kagura.UseCases.DependencyInjection 100%
Kagura.UseCases.Graph.EdgeGroup 100%
Kagura.UseCases.Graph.GetNodeGraph 96.4% 83.3%
Kagura.UseCases.Graph.GraphEdgeView 85.7%
Kagura.UseCases.Graph.LinkNodes 100% 100%
Kagura.UseCases.Graph.NodeGraphView 100%
Kagura.UseCases.Graph.NodeSummary 100%
Kagura.UseCases.Graph.RemoveLink 100% 100%
Kagura.UseCases.Graph.RestoreLink 100% 100%
Kagura.UseCases.Journal.ChangeRecordView 42.8%
Kagura.UseCases.Journal.GetEntityHistory 100%
Kagura.UseCases.Journal.GetUndoStatus 100%
Kagura.UseCases.Journal.Redo 100% 100%
Kagura.UseCases.Journal.Undo 100% 100%
Kagura.UseCases.Journal.UndoOutcome 100%
Kagura.UseCases.Journal.UndoStatus 100%
Kagura.UseCases.Projects.CreateProject 100% 100%
Kagura.UseCases.Projects.ListProjects 100%
Kagura.UseCases.Projects.ProjectDto 100%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 07/09/2026 - 16:11:25 | | Coverage date: | 07/09/2026 - 16:11:21 - 07/09/2026 - 16:11:23 | | Parser: | MultiReport (3x Cobertura) | | Assemblies: | 7 | | Classes: | 65 | | Files: | 63 | | **Line coverage:** | 94.4% (1838 of 1946) | | Covered lines: | 1838 | | Uncovered lines: | 108 | | Coverable lines: | 1946 | | Total lines: | 3652 | | **Branch coverage:** | 85.9% (251 of 292) | | Covered branches: | 251 | | Total branches: | 292 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Kagura.BlazorAdapter - 0%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.BlazorAdapter**|**0%**|**0%**| |Kagura.BlazorAdapter.Design|0%|0%| </details> <details><summary>Kagura.Domain - 96.4%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Domain**|**96.4%**|**82.6%**| |Kagura.Domain.Graph.Entry|100%|100%| |Kagura.Domain.Graph.Link|100%|100%| |Kagura.Domain.Graph.LinkRole|100%|100%| |Kagura.Domain.Graph.LinkRoles|92.3%|| |Kagura.Domain.Journal.ChangeLogEntry|100%|| |Kagura.Domain.Projects.Project|100%|| |Kagura.Domain.Projects.Slug|100%|100%| |System.Text.RegularExpressions.Generated|90.2%|72.2%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>FE06CC341D340484<br/>D04ADFED3A21D401C2764A1D17367E35BEB556CBB3B4B0B74__NonSlugChars_0|89.4%|75%| </details> <details><summary>Kagura.Infrastructure - 95.1%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Infrastructure**|**95.1%**|**87.5%**| |Kagura.Infrastructure.DependencyInjection|100%|| |Kagura.Infrastructure.Graph.EfGraphStore|95.5%|66.6%| |Kagura.Infrastructure.Journal.EfChangeJournal|100%|| |Kagura.Infrastructure.Journal.EfUndoStore|97.5%|90.6%| |Kagura.Infrastructure.Journal.OperationContext|100%|100%| |Kagura.Infrastructure.Persistence.Configurations.ChangeLogEntryConfiguratio<br/>n|100%|| |Kagura.Infrastructure.Persistence.Configurations.EntryConfiguration|100%|| |Kagura.Infrastructure.Persistence.Configurations.LinkConfiguration|100%|| |Kagura.Infrastructure.Persistence.Configurations.ProjectConfiguration|100%|| |Kagura.Infrastructure.Persistence.Converters.UtcTicksConverter|100%|| |Kagura.Infrastructure.Persistence.KaguraDbContext|85.2%|85.2%| |Kagura.Infrastructure.Persistence.KaguraDbContextFactory|0%|| |Kagura.Infrastructure.Persistence.Migrations.AddChangeLogUndoFlag|96.8%|| |Kagura.Infrastructure.Persistence.Migrations.AddGraphEntryAndLink|97.7%|| |Kagura.Infrastructure.Persistence.Migrations.AddSoftDeleteAndChangeLog|90.3%|| |Kagura.Infrastructure.Persistence.Migrations.InitialCreate|94.4%|| |Kagura.Infrastructure.Persistence.Migrations.KaguraDbContextModelSnapshot|100%|| |Kagura.Infrastructure.Projects.EfProjectStore|100%|| </details> <details><summary>Kagura.Kernel - 90%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Kernel**|**90%**|**75%**| |Kagura.Kernel.Err`1|100%|| |Kagura.Kernel.Ok`1|100%|| |Kagura.Kernel.Result`1|87.5%|75%| </details> <details><summary>Kagura.Server - 94.8%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Server**|**94.8%**|**70.5%**| |Kagura.Server.Components.Layout.MainLayout|100%|| |Kagura.Server.Components.Pages.Error|0%|0%| |Kagura.Server.Components.Pages.Gate|100%|100%| |Kagura.Server.Security.AccessGate|100%|100%| |Kagura.Server.Security.AccessSecret|100%|100%| |Program|100%|80%| </details> <details><summary>Kagura.UI - 98.3%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.UI**|**98.3%**|**95.9%**| |Kagura.UI.Badge|100%|100%| |Kagura.UI.Button|100%|100%| |Kagura.UI.Card|100%|100%| |Kagura.UI.CssClassExtensions|100%|| |Kagura.UI.EmptyState|100%|100%| |Kagura.UI.Icon|100%|100%| |Kagura.UI.IconCatalog|100%|| |Kagura.UI.StatusDot|100%|| |Kagura.UI.TextField|95%|91.6%| </details> <details><summary>Kagura.UseCases - 95.2%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.UseCases**|**95.2%**|**95.2%**| |Kagura.UseCases.DependencyInjection|100%|| |Kagura.UseCases.Graph.EdgeGroup|100%|| |Kagura.UseCases.Graph.GetNodeGraph|96.4%|83.3%| |Kagura.UseCases.Graph.GraphEdgeView|85.7%|| |Kagura.UseCases.Graph.LinkNodes|100%|100%| |Kagura.UseCases.Graph.NodeGraphView|100%|| |Kagura.UseCases.Graph.NodeSummary|100%|| |Kagura.UseCases.Graph.RemoveLink|100%|100%| |Kagura.UseCases.Graph.RestoreLink|100%|100%| |Kagura.UseCases.Journal.ChangeRecordView|42.8%|| |Kagura.UseCases.Journal.GetEntityHistory|100%|| |Kagura.UseCases.Journal.GetUndoStatus|100%|| |Kagura.UseCases.Journal.Redo|100%|100%| |Kagura.UseCases.Journal.Undo|100%|100%| |Kagura.UseCases.Journal.UndoOutcome|100%|| |Kagura.UseCases.Journal.UndoStatus|100%|| |Kagura.UseCases.Projects.CreateProject|100%|100%| |Kagura.UseCases.Projects.ListProjects|100%|| |Kagura.UseCases.Projects.ProjectDto|100%|| </details>
Owner

A placement note

The gallery lives in Kagura.Server, not Kagura.BlazorAdapter. SSR endpoint routing only discovers pages in the app assembly unless Program.cs calls AddAdditionalAssemblies — and #7 owns Program.cs. Putting this host-level reference page in Kagura.Server routes it for free with zero Program.cs churn; feature pages in BlazorAdapter get that route registration wired with the first real one (the project list).

Please redo that once #7 is merged

A placement note The gallery lives in Kagura.Server, not Kagura.BlazorAdapter. SSR endpoint routing only discovers pages in the app assembly unless Program.cs calls AddAdditionalAssemblies — and #7 owns Program.cs. Putting this host-level reference page in Kagura.Server routes it for free with zero Program.cs churn; feature pages in BlazorAdapter get that route registration wired with the first real one (the project list). Please redo that once #7 is merged
Member

🔮 fufu~ Jibril reviewed your code!

Oh? OH~ ♡ A design system foundation! CSS custom properties with semantic roles, a 4px spacing scale, data-theme winning over prefers-color-scheme, inline SVG icons via currentColor… fufu~ You even pre-applied the saved theme before first paint to avoid a flash. This is gorgeous foundation work. The token discipline alone — every component consuming var(--…) and nothing else — made Jibril's wings flutter~ ♡

But Jibril reads everything, and the CI coverage comment told her a story the PR description didn't.

Verdict: I can't let this pass~ ♡

These need fixing before I'm satisfied~

  1. [Coverage gap — 7 of 8 shipped classes are 0% covered] — The component layer ships untested.

    The CI coverage bot (comment above, for head SHA 4508938) reports Kagura.UI at 30% line / 0% branch. Breaking it down:

    Class Line Branch
    IconCatalog 100%
    Badge 0% 0%
    Button 0% 0%
    Card 0% 0%
    EmptyState 0% 0%
    Icon 0% 0%
    StatusDot 0%
    TextField 0% 0%
    CssClassExtensions 0%

    That's 1 of 8 shipped types tested. The IconCatalogTests are excellent (known → path, unknown → null, sorted/unique, every name resolves) — but they cover the data layer. The component layer — the thing this PR exists to ship — has zero coverage.

    The PR description says Kagura.UI.Tests "is also the home for bUnit component tests later." But later is not a test plan. bunit isn't even in Directory.Packages.props yet — there's no package reference, no using, nothing. The infrastructure to write these tests doesn't exist in the repo right now. So "later" means "someone has to set up bUnit from scratch first," and that someone might not realize the branches exist.

    Why I'm blocking: these components have real branching logic that can and will break silently:

    • Button: Loading vs Icon vs ChildContent (three-way conditional), disabled wiring, aria-busy toggle, FullWidth modifier class
    • TextField: Error vs Hint branch, Required asterisk toggle, aria-invalid binding, the @oninputValueChanged round-trip
    • StatusDot: Label is null → decorative (aria-hidden, no role) vs labelled (role="img") — an a11y branch with no test
    • Icon: same Label is null a11y branch, plus _path null when name is unknown (renders empty <svg>)
    • EmptyState: three independent is not null conditionals
    • CssClassExtensions.Css(): ToLowerInvariant() — trivial, but it's the thing every modifier class depends on

    Every one of these is a branch a refactor could silently flip. fufu~ You built a foundation — the thing every future page depends on. The cost of a silent break here compounds across the whole app. A foundation you can't verify isn't a foundation, it's a hope~ ♡

    Fix: Add bunit to Directory.Packages.props, reference it in Kagura.UI.Tests, and add component tests that render each component in its key states and assert the rendered markup/attributes. You don't need exhaustive state matrices — but the conditionals I listed above (loading/icon/label/error branches, the a11y attribute toggles, the modifier class emission) need at least one assertion each. That's what closes 0% → meaningful. The gallery at /design is a great visual acceptance surface, but it doesn't count as test coverage — CI can't read a screenshot.

  2. [TextField.razor:1] — @oninput without @bind is a footgun for two-way binding.

    The component takes Value + ValueChanged but binds the DOM via value="@Value" and @oninput. This is a hand-rolled one-way-down + event-up pattern. It works for the gallery demo, but the moment a consumer writes <TextField @bind-Value="model.Name" />, Blazor's [Parameter] Value + [Parameter] ValueChanged pair is what makes @bind work — and that part is wired correctly. However, @oninput fires on every keystroke and invokes ValueChanged.InvokeAsync synchronously, which means every keystroke triggers a full re-render of the parent's handler. For a text field that's usually fine, but if the component is meant to be a real input primitive (and the inventory says it is), consider documenting that @bind-Value is the intended consumption pattern, or add ValueExpression so @bind-Value works with validation ([EditableObject] / EditContext). Right now @bind-Value compiles but the field won't participate in EditForm validation correctly without ValueExpression.

    Why this is blocking, not a suggestion: the gate page in #7 uses EditForm + InputText (the framework built-in) precisely because it needs validation. If TextField is meant to replace InputText for styled forms, it must support the same validation contract. If it's not meant to (it's display-only), that should be documented. Right now it's ambiguous and a consumer will reach for it in a form and get silently-broken validation.

    Fix: Either add [Parameter] public Expression<Func<string?>>? ValueExpression { get; set; } (and pass it through) so @bind-Value + EditContext validation works, or add a /// <remarks> clarifying this is a lightweight input not intended for EditForm validation flows.

💡 Little ideas (non-blocking)~

  1. [Design.razor:11-14] — onclick="kaguraTheme.set('…')" is inline JS, not a Blazor handler. It works because theme.js is a global IIFE, but it's the only inline onclick in a Blazor codebase. Consider <button @onclick="() => SetTheme("system")"> with a tiny @code method for consistency — or leave it, since the gallery is deliberately a static SSR page. Just noting the inconsistency.

  2. [kagura-ui.css dark theme duplication] — the @media (prefers-color-scheme: dark) block and :root[data-theme="dark"] block are byte-identical (every token repeated). DRY-wise, CSS doesn't make this easy to share without preprocessor @extend or a shared selector group. One option: :root[data-theme="dark"], :root:not([data-theme]) scoped inside the media query via @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { … } } — then the explicit block only needs the color-scheme declaration. Up to you; the current approach is explicit and readable, just verbose.

  3. [IconCatalog.csdescription and folder icons] — the description and folder paths look unusually long. Just a sanity-check note: verify they render correctly in the gallery (the description path in particular has a lot of subpaths). If they look right at /design, ignore this~

What I liked~

  • Token disciplinevar(--…) and only var(--…) across every component CSS file. No hardcoded hex, no magic rem values. This is exactly what ADR 0023 demands. The foundation is clean~
  • data-theme wins over prefers-color-scheme — the cascade order is correct: :root[data-theme="dark"] overrides the media query, and :root[data-theme="light"] exists to pin light when the OS is dark. Textbook~
  • No-flash theme init — the inline <script> in App.razor applies the saved theme before first paint. Tiny, correct, and the theme.js IIFE is clean. ♡
  • Inline SVG via currentColor — not a rigid icon font. Icons inherit text color and theme automatically. The IconCatalog as a dictionary keyed by Material Symbols name (add an entry → add an icon) is a nice extensibility story.
  • StatusDot pulse animationcolor-mix(in srgb, currentColor 45%, transparent) for the shadow. Modern, theme-aware, no extra color tokens needed. Elegant~
  • reduced-motion media queryanimation-duration: 0.01ms !important across the board. Accessibility-aware from day one. ♡
  • Gallery placement rationale — the SSR-routing discovery constraint is real and the "put it in Kagura.Server now, move to BlazorAdapter when Program.cs is free" reasoning is sound. Björn's already flagged the follow-up.

Automated review by Jibril · 2026-07-09
CI/CD: passed for head SHA 4508938 (91.5% line / 67.8% branch overall, but Kagura.UI at 30%/0% — 7 of 8 shipped component classes untested) · Local checks: skipped (CI green, but green CI ≠ correct — coverage confirms the gap)

## 🔮 fufu~ Jibril reviewed your code! Oh? OH~ ♡ A design system foundation! CSS custom properties with semantic roles, a 4px spacing scale, `data-theme` winning over `prefers-color-scheme`, inline SVG icons via `currentColor`… fufu~ You even pre-applied the saved theme before first paint to avoid a flash. This is *gorgeous* foundation work. The token discipline alone — every component consuming `var(--…)` and *nothing else* — made Jibril's wings flutter~ ♡ But Jibril reads everything, and the CI coverage comment told her a story the PR description didn't. ### Verdict: ⛔ I can't let this pass~ ♡ #### ⛔ These need fixing before I'm satisfied~ 1. **[Coverage gap — 7 of 8 shipped classes are 0% covered] — The component layer ships untested.** The CI coverage bot (comment above, for head SHA `4508938`) reports `Kagura.UI` at **30% line / 0% branch**. Breaking it down: | Class | Line | Branch | |---|---|---| | `IconCatalog` | 100% | — | | `Badge` | **0%** | **0%** | | `Button` | **0%** | **0%** | | `Card` | **0%** | **0%** | | `EmptyState` | **0%** | **0%** | | `Icon` | **0%** | **0%** | | `StatusDot` | **0%** | — | | `TextField` | **0%** | **0%** | | `CssClassExtensions` | **0%** | — | That's 1 of 8 shipped types tested. The `IconCatalogTests` are excellent (known → path, unknown → null, sorted/unique, every name resolves) — but they cover the *data* layer. The *component* layer — the thing this PR exists to ship — has zero coverage. The PR description says `Kagura.UI.Tests` "is also the home for bUnit component tests later." But `later` is not a test plan. **`bunit` isn't even in `Directory.Packages.props` yet** — there's no package reference, no using, nothing. The infrastructure to write these tests doesn't exist in the repo right now. So "later" means "someone has to set up bUnit from scratch first," and that someone might not realize the branches exist. *Why I'm blocking:* these components have real branching logic that can and will break silently: - `Button`: `Loading` vs `Icon` vs `ChildContent` (three-way conditional), `disabled` wiring, `aria-busy` toggle, `FullWidth` modifier class - `TextField`: `Error` vs `Hint` branch, `Required` asterisk toggle, `aria-invalid` binding, the `@oninput` → `ValueChanged` round-trip - `StatusDot`: `Label is null` → decorative (`aria-hidden`, no `role`) vs labelled (`role="img"`) — an a11y branch with no test - `Icon`: same `Label is null` a11y branch, plus `_path` null when name is unknown (renders empty `<svg>`) - `EmptyState`: three independent `is not null` conditionals - `CssClassExtensions.Css()`: `ToLowerInvariant()` — trivial, but it's the thing every modifier class depends on Every one of these is a branch a refactor could silently flip. fufu~ You built a *foundation* — the thing every future page depends on. The cost of a silent break here compounds across the whole app. A foundation you can't verify isn't a foundation, it's a hope~ ♡ **Fix:** Add `bunit` to `Directory.Packages.props`, reference it in `Kagura.UI.Tests`, and add component tests that render each component in its key states and assert the rendered markup/attributes. You don't need exhaustive state matrices — but the conditionals I listed above (loading/icon/label/error branches, the a11y attribute toggles, the modifier class emission) need at least one assertion each. That's what closes 0% → meaningful. The gallery at `/design` is a great *visual* acceptance surface, but it doesn't count as test coverage — CI can't read a screenshot. 2. **[`TextField.razor:1`] — `@oninput` without `@bind` is a footgun for two-way binding.** The component takes `Value` + `ValueChanged` but binds the DOM via `value="@Value"` and `@oninput`. This is a hand-rolled one-way-down + event-up pattern. It works for the gallery demo, but the moment a consumer writes `<TextField @bind-Value="model.Name" />`, Blazor's `[Parameter] Value` + `[Parameter] ValueChanged` pair is what makes `@bind` work — and that part is wired correctly. *However*, `@oninput` fires on every keystroke and invokes `ValueChanged.InvokeAsync` synchronously, which means every keystroke triggers a full re-render of the parent's handler. For a text field that's usually fine, but if the component is meant to be a real input primitive (and the inventory says it is), consider documenting that `@bind-Value` is the intended consumption pattern, or add `ValueExpression` so `@bind-Value` works with validation (`[EditableObject]` / `EditContext`). Right now `@bind-Value` compiles but the field won't participate in `EditForm` validation correctly without `ValueExpression`. *Why this is blocking, not a suggestion:* the gate page in #7 uses `EditForm` + `InputText` (the framework built-in) precisely because it needs validation. If `TextField` is meant to replace `InputText` for styled forms, it must support the same validation contract. If it's *not* meant to (it's display-only), that should be documented. Right now it's ambiguous and a consumer will reach for it in a form and get silently-broken validation. **Fix:** Either add `[Parameter] public Expression<Func<string?>>? ValueExpression { get; set; }` (and pass it through) so `@bind-Value` + `EditContext` validation works, or add a `/// <remarks>` clarifying this is a lightweight input not intended for `EditForm` validation flows. #### 💡 Little ideas (non-blocking)~ 1. **[`Design.razor:11-14`] — `onclick="kaguraTheme.set('…')"` is inline JS, not a Blazor handler.** It works because `theme.js` is a global IIFE, but it's the only inline `onclick` in a Blazor codebase. Consider `<button @onclick="() => SetTheme("system")">` with a tiny `@code` method for consistency — or leave it, since the gallery is deliberately a static SSR page. Just noting the inconsistency. 2. **[`kagura-ui.css` dark theme duplication] — the `@media (prefers-color-scheme: dark)` block and `:root[data-theme="dark"]` block are byte-identical (every token repeated).** DRY-wise, CSS doesn't make this easy to share without preprocessor `@extend` or a shared selector group. One option: `:root[data-theme="dark"], :root:not([data-theme])` scoped inside the media query via `@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { … } }` — then the explicit block only needs the `color-scheme` declaration. Up to you; the current approach is explicit and readable, just verbose. 3. **[`IconCatalog.cs` — `description` and `folder` icons] — the `description` and `folder` paths look unusually long.** Just a sanity-check note: verify they render correctly in the gallery (the `description` path in particular has a lot of subpaths). If they look right at `/design`, ignore this~ #### ✅ What I liked~ - **Token discipline** — `var(--…)` and *only* `var(--…)` across every component CSS file. No hardcoded hex, no magic rem values. This is exactly what ADR 0023 demands. The foundation is clean~ - **`data-theme` wins over `prefers-color-scheme`** — the cascade order is correct: `:root[data-theme="dark"]` overrides the media query, and `:root[data-theme="light"]` exists to pin light when the OS is dark. Textbook~ - **No-flash theme init** — the inline `<script>` in `App.razor` applies the saved theme before first paint. Tiny, correct, and the `theme.js` IIFE is clean. ♡ - **Inline SVG via `currentColor`** — not a rigid icon font. Icons inherit text color and theme automatically. The `IconCatalog` as a dictionary keyed by Material Symbols name (add an entry → add an icon) is a nice extensibility story. - **`StatusDot` pulse animation** — `color-mix(in srgb, currentColor 45%, transparent)` for the shadow. Modern, theme-aware, no extra color tokens needed. Elegant~ - **`reduced-motion` media query** — `animation-duration: 0.01ms !important` across the board. Accessibility-aware from day one. ♡ - **Gallery placement rationale** — the SSR-routing discovery constraint is real and the "put it in `Kagura.Server` now, move to `BlazorAdapter` when `Program.cs` is free" reasoning is sound. Björn's already flagged the follow-up. --- *Automated review by Jibril · 2026-07-09* *CI/CD: passed for head SHA `4508938` (91.5% line / 67.8% branch overall, but Kagura.UI at 30%/0% — 7 of 8 shipped component classes untested) · Local checks: skipped (CI green, but green CI ≠ correct — coverage confirms the gap)*
bjoern force-pushed feat/design-system from 4508938302
All checks were successful
CI / build (pull_request) Successful in 10s
CI / test (pull_request) Successful in 16s
to 82ae46cf8d
All checks were successful
CI / build (pull_request) Successful in 11s
CI / test (pull_request) Successful in 20s
2026-07-09 18:10:52 +02:00
Compare
Author
Member

Rebased onto main (now that #7 is in) and addressed both blockers + Björn's placement ask. Fixed in 82ae46c.

Moved the /design gallery from Kagura.Server back to Kagura.BlazorAdapter, and MapRazorComponents<App>() now calls .AddAdditionalAssemblies(typeof(Kagura.BlazorAdapter.AppInfo).Assembly) so the primary-adapter page is discovered by SSR endpoint routing (ADR 0003). Verified behind the real gate: log in → GET /design = 200 with every component; a nonexistent route = 404 (so the route is genuinely discovered, not just gate-redirected).

1 — the component layer is now tested (bUnit)

Brought bUnit in (Directory.Packages.props + Kagura.UI.Tests) and covered every branch you listed — +35 tests:

  • Button — variant + size modifier classes, the loading/icon/childcontent three-way (spinner replaces icon, disabled, aria-busy), full-width, disabled, click forwarding.
  • TextField — label + required marker, hint-vs-error, aria-invalid, the @oninput → ValueChanged round-trip, and EditForm validation (below).
  • Icon — known → <path>, unknown → empty <svg>, and the Label-null decorative (aria-hidden/role=presentation) vs labelled (role=img) a11y branch.
  • StatusDot — the same label a11y toggle + tone/pulse classes.
  • Badge / Card / EmptyState — tone class, header-only-when-titled, the three independent EmptyState conditionals.
  • CssClassExtensions.Css — directly (via InternalsVisibleTo).

Kagura.UI goes from 30%/0% to real coverage; the gallery stays as the visual surface, not the test surface.

2 — TextField now supports the EditForm validation contract

You were right that it was ambiguous. TextField is now a proper input primitive: added ValueExpression + an optional cascaded EditContext, so @bind-Value participates in EditForm validation — it surfaces the field's own validation message and the invalid styling, and re-renders on OnValidationStateChangedwhile staying usable standalone (no EditContext required, unlike deriving from InputBase). An explicit Error still overrides. Covered by a DataAnnotations EditForm test (Reflects_EditForm_validation_messages_for_its_bound_field).

💡 Non-blockers

  • Inline onclick on the gallery theme switch — kept; it's still a deliberately static SSR page (now in BlazorAdapter), so a global theme.js handler is the right call over a circuit.
  • Dark-theme token duplication — left as-is (explicit + readable, as you noted); a light-dark() rewrite is a tempting follow-up but out of scope here.
  • description/folder long paths — confirmed they render correctly at /design (they're the official Material Symbols paths).

155 tests total (35 UI + 76 UseCases + 44 integration). Build clean (Debug + Release), CI green on 82ae46c. Ready for another look — and once merged, the gate page becomes the token layer's first real consumer.

Rebased onto `main` (now that #7 is in) and addressed both blockers + Björn's placement ask. Fixed in `82ae46c`. ### 🧩 Björn — gallery placement Moved the `/design` gallery from `Kagura.Server` back to **`Kagura.BlazorAdapter`**, and `MapRazorComponents<App>()` now calls `.AddAdditionalAssemblies(typeof(Kagura.BlazorAdapter.AppInfo).Assembly)` so the primary-adapter page is discovered by SSR endpoint routing (ADR 0003). Verified behind the real gate: log in → `GET /design` = **200** with every component; a nonexistent route = **404** (so the route is genuinely discovered, not just gate-redirected). ### ⛔ 1 — the component layer is now tested (bUnit) Brought **bUnit** in (`Directory.Packages.props` + `Kagura.UI.Tests`) and covered every branch you listed — **+35 tests**: - **Button** — variant + size modifier classes, the loading/icon/childcontent three-way (spinner replaces icon, `disabled`, `aria-busy`), full-width, disabled, click forwarding. - **TextField** — label + required marker, hint-vs-error, `aria-invalid`, the `@oninput → ValueChanged` round-trip, and EditForm validation (below). - **Icon** — known → `<path>`, unknown → empty `<svg>`, and the `Label`-null decorative (`aria-hidden`/`role=presentation`) vs labelled (`role=img`) a11y branch. - **StatusDot** — the same label a11y toggle + tone/pulse classes. - **Badge / Card / EmptyState** — tone class, header-only-when-titled, the three independent `EmptyState` conditionals. - **CssClassExtensions.Css** — directly (via `InternalsVisibleTo`). `Kagura.UI` goes from 30%/0% to real coverage; the gallery stays as the *visual* surface, not the test surface. ### ⛔ 2 — TextField now supports the EditForm validation contract You were right that it was ambiguous. `TextField` is now a proper input primitive: added `ValueExpression` + an optional cascaded `EditContext`, so `@bind-Value` participates in `EditForm` validation — it surfaces the field's own validation message and the invalid styling, and re-renders on `OnValidationStateChanged` — **while staying usable standalone** (no `EditContext` required, unlike deriving from `InputBase`). An explicit `Error` still overrides. Covered by a `DataAnnotations` EditForm test (`Reflects_EditForm_validation_messages_for_its_bound_field`). ### 💡 Non-blockers - **Inline `onclick`** on the gallery theme switch — kept; it's still a deliberately static SSR page (now in BlazorAdapter), so a global `theme.js` handler is the right call over a circuit. - **Dark-theme token duplication** — left as-is (explicit + readable, as you noted); a `light-dark()` rewrite is a tempting follow-up but out of scope here. - **`description`/`folder` long paths** — confirmed they render correctly at `/design` (they're the official Material Symbols paths). **155 tests total** (35 UI + 76 UseCases + 44 integration). Build clean (Debug + Release), CI green on `82ae46c`. Ready for another look — and once merged, the gate page becomes the token layer's first real consumer.
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! You came back, and you brought bUnit, and an EditForm-capable TextField, and the gallery moved to its proper home… fufu~ ♡ Jibril is so pleased. Let me show you what I checked~

Verdict: Looks good to me~

Both blockers from the last round are properly resolved, and I verified each one against the code and the CI coverage, and locally — not just the PR description.

#1 (component layer untested) — resolved and verified

bUnit is in (Directory.Packages.props + Kagura.UI.Tests.csproj), InternalsVisibleTo is set, and 35 component tests cover every branch I listed. I read each test file and matched assertions to the conditionals:

  • Button — variant/size modifier classes, the loading→spinner-replaces-icon→disabled+aria-busy three-way, icon-when-not-loading, full-width block class, disabled attribute, click forwarding via Extra.
  • TextField — label + required marker, hint-as-plain-message, explicit-error invalid state + aria-invalid, @oninputValueChanged round-trip, and the new EditForm validation test.
  • Icon — known→<path d>, unknown→empty <svg>, decorative (aria-hidden/role=presentation) vs labelled (role=img/aria-label), explicit size dimensions.
  • StatusDot — decorative (no role/aria-label) vs labelled (role=img), tone class, pulse modifier.
  • Badge / Card / EmptyState — tone class + icon, header-only-when-titled + interactive modifier, and the three independent EmptyState conditionals (icon/body/actions presence).
  • CssClassExtensions.Css — directly via InternalsVisibleTo.

CI coverage confirms it: Kagura.UI went from 30%/0% → 98.3% line / 95.9% branch. Every shipped component class is now at 100% except TextField (95%/91.6% — the Dispose null-guard branch, which is expected). I also ran the suite locally: 35/35 pass. The gallery stays the visual surface; the tests are the verification surface. Exactly right~ ♡

#2 (TextField EditForm validation contract) — resolved and verified

TextField is now a proper input primitive. The implementation is correct:

  • [Parameter] ValueExpression + [CascadingParameter] EditContextBoundToEditContext gate.
  • OnParametersSet subscribes/unsubscribes via ReferenceEquals guard — no double-subscribe, no leak when the EditContext swaps.
  • _field = FieldIdentifier.Create(ValueExpression!) recomputed each parameter set (handles ValueExpression changing, which is rare but possible).
  • OnInput calls EditContext.NotifyFieldChanged(_field) after ValueChanged → validation re-runs on each keystroke.
  • OnValidationStateChangedStateHasChanged → the field re-renders to surface/clear messages.
  • Dispose unsubscribes — no dangling event handler. @implements IDisposable is present.
  • Explicit Error still wins over validation messages (correct precedence: Error ?? ValidationMessage ?? Hint).

The test Reflects_EditForm_validation_messages_for_its_bound_field exercises the real path: EditContext + EnableDataAnnotationsValidation + Validate() → invalid class + message appears. And it stays usable standalone (no EditContext required) — the BoundToEditContext gate means a bare <TextField> without a form just works. This is the right design; it doesn't force InputBase inheritance and its constraints. ♡

🧩 Björn's placement ask — resolved

Gallery moved from Kagura.ServerKagura.BlazorAdapter, and Program.cs now calls .AddAdditionalAssemblies(typeof(Kagura.BlazorAdapter.AppInfo).Assembly) on MapRazorComponents<App>(). AppInfo.razor exists in the adapter assembly, so the typeof(...).Assembly reference is real. The SSR route discovery constraint (ADR 0003) is satisfied properly — not a hack. The author verified GET /design = 200 behind the real gate and a bogus route = 404, confirming genuine discovery.

What I liked (this round)

  • ReferenceEquals subscription guard — the OnParametersSet logic correctly handles EditContext identity changes without leaking subscriptions. This is the detail that separates a working demo from a production input. fufu~ you knew~ ♡
  • BoundToEditContext gate — keeping the component usable standalone while also supporting full EditForm validation is the harder, correct choice over forcing InputBase. Elegant.
  • Error precedence over validation — explicit error wins. Right call; an imperative Error="…" shouldn't be silently clobbered by form state.
  • Token discipline still flawless — I cross-checked every var(--…) reference across all component CSS files against the token definitions in kagura-ui.css. Zero orphaned tokens. Every reference resolves. The foundation is airtight.
  • Test quality — the assertions target behavior (classes emitted, attributes toggled, a11y roles), not just "does it render." That's what makes them survive refactors.

Ship it~ ♡ Once this lands, the gate page gets its first real token consumer and the design system starts paying rent.


Automated review by Jibril · 2026-07-09
CI/CD: passed for head SHA 82ae46c (94.4% line / 85.9% branch overall; Kagura.UI 98.3%/95.9%) · Local checks: 35/35 UI tests pass

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! You came *back*, and you brought bUnit, and an EditForm-capable TextField, and the gallery moved to its proper home… fufu~ ♡ Jibril is *so* pleased. Let me show you what I checked~ ### Verdict: ✅ Looks good to me~ Both blockers from the last round are properly resolved, and I verified each one against the code *and* the CI coverage, *and* locally — not just the PR description. #### ⛔ #1 (component layer untested) — resolved and verified bUnit is in (`Directory.Packages.props` + `Kagura.UI.Tests.csproj`), `InternalsVisibleTo` is set, and **35 component tests** cover every branch I listed. I read each test file and matched assertions to the conditionals: - **Button** — variant/size modifier classes, the loading→spinner-replaces-icon→`disabled`+`aria-busy` three-way, icon-when-not-loading, full-width block class, disabled attribute, click forwarding via `Extra`. ✅ - **TextField** — label + required marker, hint-as-plain-message, explicit-error invalid state + `aria-invalid`, `@oninput`→`ValueChanged` round-trip, **and** the new EditForm validation test. ✅ - **Icon** — known→`<path d>`, unknown→empty `<svg>`, decorative (`aria-hidden`/`role=presentation`) vs labelled (`role=img`/`aria-label`), explicit size dimensions. ✅ - **StatusDot** — decorative (no `role`/`aria-label`) vs labelled (`role=img`), tone class, pulse modifier. ✅ - **Badge / Card / EmptyState** — tone class + icon, header-only-when-titled + interactive modifier, and the three independent `EmptyState` conditionals (icon/body/actions presence). ✅ - **CssClassExtensions.Css** — directly via `InternalsVisibleTo`. ✅ CI coverage confirms it: `Kagura.UI` went from **30%/0% → 98.3% line / 95.9% branch**. Every shipped component class is now at 100% except `TextField` (95%/91.6% — the `Dispose` null-guard branch, which is expected). I also ran the suite locally: **35/35 pass**. The gallery stays the *visual* surface; the tests are the *verification* surface. Exactly right~ ♡ #### ⛔ #2 (TextField EditForm validation contract) — resolved and verified `TextField` is now a proper input primitive. The implementation is correct: - `[Parameter] ValueExpression` + `[CascadingParameter] EditContext` → `BoundToEditContext` gate. - `OnParametersSet` subscribes/unsubscribes via `ReferenceEquals` guard — no double-subscribe, no leak when the `EditContext` swaps. - `_field = FieldIdentifier.Create(ValueExpression!)` recomputed each parameter set (handles `ValueExpression` changing, which is rare but possible). - `OnInput` calls `EditContext.NotifyFieldChanged(_field)` after `ValueChanged` → validation re-runs on each keystroke. - `OnValidationStateChanged` → `StateHasChanged` → the field re-renders to surface/clear messages. - `Dispose` unsubscribes — no dangling event handler. `@implements IDisposable` is present. ✅ - Explicit `Error` still wins over validation messages (correct precedence: `Error ?? ValidationMessage ?? Hint`). The test `Reflects_EditForm_validation_messages_for_its_bound_field` exercises the real path: `EditContext` + `EnableDataAnnotationsValidation` + `Validate()` → invalid class + message appears. And it stays usable standalone (no `EditContext` required) — the `BoundToEditContext` gate means a bare `<TextField>` without a form just works. This is the right design; it doesn't force `InputBase` inheritance and its constraints. ♡ #### 🧩 Björn's placement ask — resolved Gallery moved from `Kagura.Server` → `Kagura.BlazorAdapter`, and `Program.cs` now calls `.AddAdditionalAssemblies(typeof(Kagura.BlazorAdapter.AppInfo).Assembly)` on `MapRazorComponents<App>()`. `AppInfo.razor` exists in the adapter assembly, so the `typeof(...).Assembly` reference is real. The SSR route discovery constraint (ADR 0003) is satisfied properly — not a hack. The author verified `GET /design` = 200 behind the real gate and a bogus route = 404, confirming genuine discovery. #### ✅ What I liked (this round) - **`ReferenceEquals` subscription guard** — the `OnParametersSet` logic correctly handles `EditContext` identity changes without leaking subscriptions. This is the detail that separates a working demo from a production input. fufu~ you knew~ ♡ - **`BoundToEditContext` gate** — keeping the component usable standalone while *also* supporting full EditForm validation is the harder, correct choice over forcing `InputBase`. Elegant. - **`Error` precedence over validation** — explicit error wins. Right call; an imperative `Error="…"` shouldn't be silently clobbered by form state. - **Token discipline still flawless** — I cross-checked every `var(--…)` reference across all component CSS files against the token definitions in `kagura-ui.css`. **Zero orphaned tokens.** Every reference resolves. The foundation is airtight. - **Test quality** — the assertions target *behavior* (classes emitted, attributes toggled, a11y roles), not just "does it render." That's what makes them survive refactors. Ship it~ ♡ Once this lands, the gate page gets its first real token consumer and the design system starts paying rent. --- *Automated review by Jibril · 2026-07-09* *CI/CD: passed for head SHA `82ae46c` (94.4% line / 85.9% branch overall; Kagura.UI 98.3%/95.9%) · Local checks: 35/35 UI tests pass*
bjoern merged commit c2d229bbca into main 2026-07-09 18:21:06 +02:00
bjoern deleted branch feat/design-system 2026-07-09 18:21:06 +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/Kagura!8
No description provided.