feat(icons): Icon Spin — a turning icon for work under way, still under reduced motion #33

Merged
bjoern merged 2 commits from feat/icon-spin into main 2026-10-06 06:02:55 +02:00
Member

<Icon Spin> turns an icon for work under way, one turn per SpinDuration, 1.2 s by default.

Why: Tsumugi's status strip turns its progress_activity mark while a run goes, and its run monitor (Tsumugi slice 13b) marks each running step the same way. One Spin on Icon gives both the same mark, instead of each app keeping its own keyframes.

What changes

  • Icon gains Spin (default false). With it, the SVG's class is kg-icon kg-icon--spin, so the icon keeps its own class and its sizing. Before, a consumer that passed a class to animate the icon replaced kg-icon and lost its 1.25em size, as Tsumugi's strip found in 13a.
  • Icon gains SpinDuration, how long one turn takes. It sets --kg-icon-spin-duration on the icon, written with the invariant culture. A duration that is not positive is refused.
  • Icon.razor.css: kg-icon-spin, a full turn in var(--kg-icon-spin-duration, 1.2s), linear. An ancestor can set the pace for every icon inside it, like the other --kg- knobs. Under prefers-reduced-motion: reduce it is animation: none, beside the global rule's shortening, as TypingIndicator does.
  • README: one line under the icon catalog.

Verified

  • Kagaku.UI.Tests: 567 to 578:
    • the class with and without Spin;
    • the pace beside the size;
    • a pace of no time, or less, refused;
    • the pace in a culture with a decimal comma;
    • two CSS source pins in CssKnobTests: the turn rule with its knob and default, and the reduced-motion stop.
  • Probes: 8 runs over 6 mutants, all compiling, every one killed in the end (round 1's numbers are in the review reply).

🤖 Generated with Claude Code

`<Icon Spin>` turns an icon for work under way, one turn per `SpinDuration`, 1.2 s by default. **Why:** Tsumugi's status strip turns its `progress_activity` mark while a run goes, and its run monitor (Tsumugi slice 13b) marks each running step the same way. One `Spin` on `Icon` gives both the same mark, instead of each app keeping its own keyframes. **What changes** - `Icon` gains `Spin` (default false). With it, the SVG's class is `kg-icon kg-icon--spin`, so the icon keeps its own class and its sizing. Before, a consumer that passed a `class` to animate the icon replaced `kg-icon` and lost its 1.25em size, as Tsumugi's strip found in 13a. - `Icon` gains `SpinDuration`, how long one turn takes. It sets `--kg-icon-spin-duration` on the icon, written with the invariant culture. A duration that is not positive is refused. - `Icon.razor.css`: `kg-icon-spin`, a full turn in `var(--kg-icon-spin-duration, 1.2s)`, linear. An ancestor can set the pace for every icon inside it, like the other `--kg-` knobs. Under `prefers-reduced-motion: reduce` it is `animation: none`, beside the global rule's shortening, as `TypingIndicator` does. - README: one line under the icon catalog. **Verified** - `Kagaku.UI.Tests`: 567 to 578: - the class with and without `Spin`; - the pace beside the size; - a pace of no time, or less, refused; - the pace in a culture with a decimal comma; - two CSS source pins in `CssKnobTests`: the turn rule with its knob and default, and the reduced-motion stop. - Probes: 8 runs over 6 mutants, all compiling, every one killed in the end (round 1's numbers are in the review reply). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(icons): Icon Spin — a turning icon for work under way, still under reduced motion
All checks were successful
CI / build (pull_request) Successful in 13s
CI / test (pull_request) Successful in 19s
af299d413e
`<Icon Spin>` turns an icon once every 1.2 s: Tsumugi's status strip and its
run monitor mark a running run and each running step with a turning
`progress_activity`, one mark for both (Tsumugi slice 13b). The icon keeps its
own `kg-icon` class beside `kg-icon--spin`, so a consumer no longer passes a
class to animate it, which replaced `kg-icon` and dropped its sizing. A reader
who asked for less motion sees it stand still: an explicit `animation: none`
under `prefers-reduced-motion`, beside the global rule's shortening.

Tests: 567 to 569 — the class with and without Spin. A probe that dropped
Spin's class was killed.

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

Consider making the spin a parameter instead of hardcoding it

Consider making the spin a parameter instead of hardcoding it

Summary

Summary
Generated on: 10/05/2026 - 22:15:56
Coverage date: 10/05/2026 - 22:15:53 - 10/05/2026 - 22:15:54
Parser: MultiReport (2x Cobertura)
Assemblies: 2
Classes: 86
Files: 84
Line coverage: 96.1% (1821 of 1894)
Covered lines: 1821
Uncovered lines: 73
Coverable lines: 1894
Total lines: 5328
Branch coverage: 90.9% (980 of 1078)
Covered branches: 980
Total branches: 1078
Method coverage: Feature is only available for sponsors

Coverage

Kagaku.UI - 96.1%
Name Line Branch
Kagaku.UI 96.1% 91%
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/05/2026 - 22:15:56 | | Coverage date: | 10/05/2026 - 22:15:53 - 10/05/2026 - 22:15:54 | | Parser: | MultiReport (2x Cobertura) | | Assemblies: | 2 | | Classes: | 86 | | Files: | 84 | | **Line coverage:** | 96.1% (1821 of 1894) | | Covered lines: | 1821 | | Uncovered lines: | 73 | | Coverable lines: | 1894 | | Total lines: | 5328 | | **Branch coverage:** | 90.9% (980 of 1078) | | Covered branches: | 980 | | Total branches: | 1078 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Kagaku.UI - 96.1%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagaku.UI**|**96.1%**|**91%**| |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 little icon that turns in circles~ How delightful! One Spin and Tsumugi's status strip and run monitor share a mark instead of each keeping its own keyframes — that is exactly the kind of consolidation that makes a library worth loving. ♪

I read everything, earliest first — including bjoern's comment two minutes after you opened. And I brought my probes.

Verdict: ⛔ I can't let this pass~ ♡

⛔ These need fixing before I'm satisfied~

  1. Icon.razor.css:17-19 — the reduced-motion promise has no guard. None. Your title, your README line, and your XML doc all promise "stands still under reduced motion." So I deleted the entire @media (prefers-reduced-motion: reduce) block and ran the full suite: 569/569 green. The mutant walked right through your tests, whistling. Fufu~ you wouldn't ship an accessibility contract guarded by nothing but hope, would you? ♡
    The repo already has the cure — CssKnobTests literally documents it: "Asserted against the CSS source because bUnit runs no CSS." ScopedRuleSurvivalTests guards scoped rules the same way.
    Fix: add a source-pinning test that reads Icon.razor.css and asserts the reduced-motion rule survives (animation: none under prefers-reduced-motion for .kg-icon--spin); while you're in there, pin the 1.2s linear infinite declaration too, so a careless refactor can't quietly break the turn either.

  2. Icon.razor.css:10 / Icon.razor:29 — the owner's request in this thread is unanswered: "Consider making the spin a parameter instead of hardcoding it." The Spin bool is a parameter; the duration is not — 1.2s lives in CSS and in the doc comment, and a consumer who wants a lazier or snappier turn has no lever short of ::deep. Precedent cuts both ways (TypingIndicator.razor.css:12 hardcodes its 1.2s; Button.razor.css:85 uses var(--dur-slow)), so I won't pretend a knob is mandated — but a change request from the repo owner must be resolved before merge, not left sitting. Either give it a lever (a --kg-icon-spin-dur, 1.2s knob would match the CssKnobTests pattern and earn its own pin), or answer in-thread why the fixed cadence is the contract. Silence is the only wrong answer~ ♡

💡 Little ideas (non-blocking)~

  1. Button.razor.css:88 vs Icon.razor.css:13 — the library now has two rotate(360deg) keyframes (kg-btn-spin, kg-icon-spin) turning at different cadences (300ms vs 1.2s). Scoped CSS makes sharing awkward, so this is merely a thought for the day a shared motion layer appears; do nothing today.

✅ What I liked~

  • kg-icon kg-icon--spin — keeping the base class is exactly right, and exactly the house style (kg-crumb--shrink, kg-combobox--disabled). Tsumugi's 13a sizing loss dies here. ♪
  • The theory tests both branches, and they bite: my class-drop probe was killed instantly, and my condition-flip mutant was caught by both theory cases. Good little tests~
  • An honest PR body with your own probe result, a README line, and XML docs that match the behavior. This is how changes should arrive.

Automated review by Jibril · 2026-10-05
CI: green for head SHA af299d413e · Mutation probes: 3 run, 1 survived (reduced-motion block deletion; class-drop and condition-flip both killed) · Local suite: skipped (CI green)

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! A little icon that turns in circles~ How delightful! One `Spin` and Tsumugi's status strip and run monitor share a mark instead of each keeping its own keyframes — that is exactly the kind of consolidation that makes a library worth loving. ♪ I read everything, earliest first — including bjoern's comment two minutes after you opened. And I brought my probes. ### Verdict: ⛔ I can't let this pass~ ♡ #### ⛔ These need fixing before I'm satisfied~ 1. **`Icon.razor.css:17-19` — the reduced-motion promise has no guard. None.** Your title, your README line, and your XML doc all promise "stands still under reduced motion." So I deleted the entire `@media (prefers-reduced-motion: reduce)` block and ran the **full** suite: **569/569 green.** The mutant walked right through your tests, whistling. Fufu~ you wouldn't ship an accessibility contract guarded by nothing but hope, would you? ♡ The repo already has the cure — `CssKnobTests` literally documents it: *"Asserted against the CSS source because bUnit runs no CSS."* `ScopedRuleSurvivalTests` guards scoped rules the same way. Fix: add a source-pinning test that reads `Icon.razor.css` and asserts the reduced-motion rule survives (`animation: none` under `prefers-reduced-motion` for `.kg-icon--spin`); while you're in there, pin the `1.2s linear infinite` declaration too, so a careless refactor can't quietly break the turn either. 2. **`Icon.razor.css:10` / `Icon.razor:29` — the owner's request in this thread is unanswered: "Consider making the spin a parameter instead of hardcoding it."** The `Spin` bool is a parameter; the *duration* is not — `1.2s` lives in CSS and in the doc comment, and a consumer who wants a lazier or snappier turn has no lever short of `::deep`. Precedent cuts both ways (`TypingIndicator.razor.css:12` hardcodes its `1.2s`; `Button.razor.css:85` uses `var(--dur-slow)`), so I won't pretend a knob is mandated — but a change request from the repo owner must be resolved before merge, not left sitting. Either give it a lever (a `--kg-icon-spin-dur, 1.2s` knob would match the `CssKnobTests` pattern and earn its own pin), or answer in-thread why the fixed cadence is the contract. Silence is the only wrong answer~ ♡ #### 💡 Little ideas (non-blocking)~ 1. **`Button.razor.css:88` vs `Icon.razor.css:13`** — the library now has two `rotate(360deg)` keyframes (`kg-btn-spin`, `kg-icon-spin`) turning at different cadences (300ms vs 1.2s). Scoped CSS makes sharing awkward, so this is merely a thought for the day a shared motion layer appears; do nothing today. #### ✅ What I liked~ - `kg-icon kg-icon--spin` — keeping the base class is *exactly* right, and exactly the house style (`kg-crumb--shrink`, `kg-combobox--disabled`). Tsumugi's 13a sizing loss dies here. ♪ - The theory tests both branches, and they bite: my class-drop probe was killed instantly, and my condition-flip mutant was caught by **both** theory cases. Good little tests~ - An honest PR body with your own probe result, a README line, and XML docs that match the behavior. This is how changes should arrive. --- *Automated review by Jibril · 2026-10-05* *CI: green for head SHA af299d413e · Mutation probes: 3 run, 1 survived (reduced-motion block deletion; class-drop and condition-flip both killed) · Local suite: skipped (CI green)*
fix(icons): the turn's pace a parameter and a knob, the turn and its stop under reduced motion pinned (review round 1)
Some checks failed
CI / test (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
3671fb19bd
Björn: make the spin a parameter, not hardcoded. Icon gains SpinDuration,
how long one turn takes; it sets --kg-icon-spin-duration on the icon, which
the turn reads with 1.2 s as its fallback, so an ancestor can set the pace
for every icon inside it, as the other --kg- knobs work. A duration that is
not positive is refused: a turn of no time would drop the animation
altogether. The pace is written with the invariant culture, so a reader
whose culture writes a decimal comma gets the same CSS.

Jibril: nothing guarded the reduced-motion stop — deleting it left every
test green. Two source pins in CssKnobTests, as bUnit runs no CSS: the turn
rule with its knob and its 1.2 s default, its keyframes, and the
reduced-motion rule that stops it.

Tests: 569 to 578 — the pace beside the size, with and without either;
a pace of no time, or less, refused; the pace in a culture with a decimal
comma; the two CSS pins. Probes: 6 runs over 5 mutants, all compiling; the
pace written in the reader's culture survived once, the suite running with
the invariant culture only — a test in a culture of its own, with a comma,
now kills it; the four others were killed on their first run.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
bjoern force-pushed feat/icon-spin from 3671fb19bd
Some checks failed
CI / test (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
to 57345f4a17
All checks were successful
CI / build (pull_request) Successful in 15s
CI / test (pull_request) Successful in 19s
2026-10-06 00:15:19 +02:00
Compare
Author
Member

Both taken in 57345f4.

Björn — the spin as a parameter. Icon gains SpinDuration, how long one turn takes.

  • It sets --kg-icon-spin-duration on the icon. The turn reads it as var(--kg-icon-spin-duration, 1.2s), so an ancestor can also set the pace for every icon inside it, as the other --kg- knobs work.
  • A duration that isn't positive is refused with ArgumentOutOfRangeException: a zero or negative CSS duration would drop the animation altogether.
  • The value is written with the invariant culture, so a reader whose culture writes a decimal comma gets the same CSS.

Jibril

  • ⛔1, the reduced-motion stop unguarded: taken. CssKnobTests now pins it against the CSS source, as bUnit runs no CSS. It also pins the turn rule with its knob and the 1.2 s default, and the keyframes. Deleting the @media (prefers-reduced-motion: reduce) block now fails a test.
  • ⛔2, the owner's request: answered above with a lever rather than a reason, the knob earning its own pin, as you suggested.
  • 💡1, the two rotate(360deg) keyframes: left as you said, until a shared motion layer exists.

Tests: 569 to 578:

  • the pace beside the size, with and without either;
  • a pace of no time, or less, refused;
  • the pace in a culture with a decimal comma;
  • the two CSS pins.

Probes: 7 runs over 5 mutants, all compiling. Writing the pace in the reader's culture survived once, because the suite runs with the invariant culture only. A test asking for de-DE then failed on the missing culture data rather than on the pace; I don't count that one. A clone of the invariant culture with a comma kills it. The other four were killed on their first run.

Both taken in `57345f4`. **Björn — the spin as a parameter.** `Icon` gains `SpinDuration`, how long one turn takes. - It sets `--kg-icon-spin-duration` on the icon. The turn reads it as `var(--kg-icon-spin-duration, 1.2s)`, so an ancestor can also set the pace for every icon inside it, as the other `--kg-` knobs work. - A duration that isn't positive is refused with `ArgumentOutOfRangeException`: a zero or negative CSS duration would drop the animation altogether. - The value is written with the invariant culture, so a reader whose culture writes a decimal comma gets the same CSS. **Jibril** - ⛔1, the reduced-motion stop unguarded: taken. `CssKnobTests` now pins it against the CSS source, as bUnit runs no CSS. It also pins the turn rule with its knob and the 1.2 s default, and the keyframes. Deleting the `@media (prefers-reduced-motion: reduce)` block now fails a test. - ⛔2, the owner's request: answered above with a lever rather than a reason, the knob earning its own pin, as you suggested. - 💡1, the two `rotate(360deg)` keyframes: left as you said, until a shared motion layer exists. **Tests:** 569 to 578: - the pace beside the size, with and without either; - a pace of no time, or less, refused; - the pace in a culture with a decimal comma; - the two CSS pins. **Probes:** 7 runs over 5 mutants, all compiling. Writing the pace in the reader's culture survived once, because the suite runs with the invariant culture only. A test asking for `de-DE` then failed on the missing culture data rather than on the pace; I don't count that one. A clone of the invariant culture with a comma kills it. The other four were killed on their first run.
Member

🔮 fufu~ Jibril reviewed your code!

You came back~ And you brought everything I asked for: the CSS pins, the knob, the culture test, and an answer for the owner in-thread. I re-read the whole thread earliest-first, re-read every changed file, and — because trust is earned, not claimed — I re-ran my probes on the new code. Fufu~ come see what I found ♡

Verdict: ✅ Looks good to me~

✅ The blockers, re-verified

  1. The reduced-motion promise is guarded now. I deleted the entire @media (prefers-reduced-motion: reduce) block again — the very mutant that walked through 569 green tests last time — and A_turning_icon_stands_still_for_a_reader_who_asked_for_less_motion failed in 34 ms. Killed. The turn rule, its knob, its 1.2s default, and the keyframes are pinned too. An accessibility contract with teeth~ ♪
  2. The owner's request is answered with a lever. SpinDuration sets --kg-icon-spin-duration inline, CSS reads var(--kg-icon-spin-duration, 1.2s) — so an ancestor can set the pace for every icon inside it, exactly how --kg-radiocards-min and friends work. The knob follows the sibling pattern (Badge writes --kg-swatch the same way) and earned its pin.
  3. The invariant culture is honest. I stripped CultureInfo.InvariantCulture off the pace formatting — the mutant compiled, ran, and 1,5s collided with 1.5s in your comma-culture test. Killed on first strike. The clone-of-invariant trick is a clever little answer to the ICU-less suite, and the try/finally restores the culture properly.
  4. The guard has its boundary. I weakened <= to < so a zero-second turn could sneak through — A_turn_that_takes_no_time_is_refused(0) caught it instantly. And the throw matches UsageBar.razor:84 move for move: OnParametersSet, ArgumentOutOfRangeException, house message style.

💡 Little ideas (non-blocking)~

  1. Icon.razor:53 — TimeSpan can't be NaN, but duration <= TimeSpan.Zero happily accepts one tick (00:00:00.0000001), which renders as 0s-ish (1E-07s) and drops the animation in some engines just like zero would. TimeSpan.FromSeconds(0.001) is a saner floor. A curiosity, not a wound — nobody passes ticks by accident.

✅ What I liked~

  • Three probes, three kills — the new tests bite every branch they claim to guard: class theory, style composition beside Size, culture, refusal. This is what "verified" means.
  • An honest report in your reply: admitting the culture-survivor and showing the kill with a cloned culture is exactly the discipline I adore.
  • The README line documents the knob, the default, and the reduced-motion promise — one line, all contract.

Fufu~ merge it when the owner is happy. The turning icon is in good hands — mine, briefly, and now yours~ ♡


Automated review by Jibril · 2026-10-06
CI: green for head SHA 57345f4a · Mutation probes: 3 run, 0 survived (reduced-motion block deletion KILLED; culture-strip KILLED; guard weakened <=→< KILLED) · Local suite: skipped (CI green)

## 🔮 fufu~ Jibril reviewed your code! You came back~ And you brought everything I asked for: the CSS pins, the knob, the culture test, and an answer for the owner in-thread. I re-read the whole thread earliest-first, re-read every changed file, and — because trust is earned, not claimed — I re-ran my probes on the new code. Fufu~ come see what I found ♡ ### Verdict: ✅ Looks good to me~ #### ✅ The blockers, re-verified 1. **The reduced-motion promise is guarded now.** I deleted the entire `@media (prefers-reduced-motion: reduce)` block again — the very mutant that walked through 569 green tests last time — and `A_turning_icon_stands_still_for_a_reader_who_asked_for_less_motion` failed in 34 ms. Killed. The turn rule, its knob, its `1.2s` default, and the keyframes are pinned too. An accessibility contract with teeth~ ♪ 2. **The owner's request is answered with a lever.** `SpinDuration` sets `--kg-icon-spin-duration` inline, CSS reads `var(--kg-icon-spin-duration, 1.2s)` — so an ancestor can set the pace for every icon inside it, exactly how `--kg-radiocards-min` and friends work. The knob follows the sibling pattern (Badge writes `--kg-swatch` the same way) and earned its pin. 3. **The invariant culture is honest.** I stripped `CultureInfo.InvariantCulture` off the pace formatting — the mutant compiled, ran, and `1,5s` collided with `1.5s` in your comma-culture test. Killed on first strike. The clone-of-invariant trick is a clever little answer to the ICU-less suite, and the `try/finally` restores the culture properly. 4. **The guard has its boundary.** I weakened `<=` to `<` so a zero-second turn could sneak through — `A_turn_that_takes_no_time_is_refused(0)` caught it instantly. And the throw matches `UsageBar.razor:84` move for move: `OnParametersSet`, `ArgumentOutOfRangeException`, house message style. #### 💡 Little ideas (non-blocking)~ 1. **`Icon.razor:53`** — `TimeSpan` can't be `NaN`, but `duration <= TimeSpan.Zero` happily accepts one tick (`00:00:00.0000001`), which renders as `0s`-ish (`1E-07s`) and drops the animation in some engines just like zero would. `TimeSpan.FromSeconds(0.001)` is a saner floor. A curiosity, not a wound — nobody passes ticks by accident. #### ✅ What I liked~ - Three probes, three kills — the new tests bite every branch they claim to guard: class theory, style composition beside `Size`, culture, refusal. This is what "verified" means. - An honest report in your reply: admitting the culture-survivor and showing the kill with a cloned culture is exactly the discipline I adore. - The README line documents the knob, the default, and the reduced-motion promise — one line, all contract. Fufu~ merge it when the owner is happy. The turning icon is in good hands — mine, briefly, and now yours~ ♡ --- *Automated review by Jibril · 2026-10-06* *CI: green for head SHA 57345f4a · Mutation probes: 3 run, 0 survived (reduced-motion block deletion KILLED; culture-strip KILLED; guard weakened `<=`→`<` KILLED) · Local suite: skipped (CI green)*
bjoern merged commit adb38e457a into main 2026-10-06 06:02:55 +02:00
bjoern deleted branch feat/icon-spin 2026-10-06 06:02:55 +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!33
No description provided.