feat: add themed audio player and browser level signal #53
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/audio-player"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Adds the reusable themed audio player required by Shikigami's UI foundation and stage/voice design (ADRs 0009 and 0028).
What's in
preload="none".Tests
Browser-verified
The standalone sample runs the actual Razor components, scoped CSS and shipped driver at port 5484, without an app database or speech provider. Trusted Space toggled play/pause; seek arrows/Home/End and a pointer click committed media positions; volume Home/End changed volume; Stop Enter paused and rewound. Checked light/dark, narrow RTL and enlarged text with long/unbroken headings: no horizontal overflow.
Desktop light (1280×940), library/native side by side, both paused at 3s of the same 12s clip:
Narrow dark (390×1120), the comparison stacked:
Root-token override only: serif font, warm surfaces, green accent, square corners and larger controls (1280×940):
Scope and limits
Browser contracts are separate from .NET CI. Physical sound, screen-reader speech, touch and Firefox/WebKit behavior are not established by these checks. CSS is provided for Chromium/WebKit and Firefox range controls; native fallback appearance and device volume restrictions belong to the browser. Application speech service, sentence queue, mouth thresholds and stage integration remain with Shikigami's later voice slice. This library PR must merge before the parent submodule pointer is updated; only Bjoern merges.
Summary
Summary
Coverage
Kagaku.UI - 97.1%
Kagaku.UI.Fluxor - 97.7%
How much is the design aligned with the rest of the ui design definitions? E.g. the background, fonts, or the corner radius. I would like to see it to respond well when a project designing its own style would have it easy to overwrite the style of the library without too many individual special cases that might slip.
🔮 fufu~ Jibril reviewed your code!
Oh? Oh my~ An audio player that keeps its frame samples in JavaScript and only lets discrete transitions cross the interop boundary? A whole lifecycle state machine with superseded-request protection, generation guards, AND a broken-graph fallback that refuses to lie about playback? This is the kind of code I want to take home and keep forever~ ♡
I read every line of the diff, checked it against its siblings (
audio.jsis a different one-shot concern — no copy-paste there; theJSDisconnectedExceptiondisposal pattern and the grouped-enums-per-file style match the house rules), verified the JS↔C# enum index maps line up position-for-position (empty…error↔AudioPlaybackStatus,disabled…unavailable↔AudioLevelStatus), and then I got to the fun part…Verdict: ✅ Looks good to me~
I tried to break it. It refused to die~ fufu~ Three mutation probes against the riskiest new logic in
AudioPlayer.razor, each compiled and run against the 21 new tests:generation == _generationguard inOnPlaybackChangedAsync→Media_reconfiguration_rejects_late_events_from_the_old_sourcefailed (all 3 theory cases). Late events from a replaced source cannot reachStateChanged.Labelsfrom the_boundcomparison so label updates skip re-init →New_labels_update_driver_without_changing_source_generationfailed. Localization changes really do reach the browser driver — that record value-equality trick is deliciously subtle~_disposedcheck fromPlayAsync→Disposed_player_with_a_source_never_invokes_browser_controlsfailed. Your claim that the earlier mutation gap is now covered — verified. A disposed player stays dead. ♡3 run, 0 survived. Every guard has teeth.
💡 Little ideas (non-blocking)~
audio-player.js—notify()— thestatuses/levelsarrays map ontoAudioPlaybackStatus/AudioLevelStatusby position, so a future enum reorder compiles green and only the manually-run browser contracts (which pinstatus === 6/4) would catch it. A tiny bunit test pinningEnum.GetNames<AudioPlaybackStatus>()order against the JS arrays (or sending status strings instead of numeric indices) would lock that contract into CI.✅ What I liked~
data-ready, native controls keep working — graceful by design, not by accident.analysisFaileddistinction between unrouted (native playback preserved) and routed-but-broken (pause with a visible error — never silent audio) is exactly the trap most Web Audio code falls into, and the comment documents why it can't fall back.preload="none", no autoplay,aria-labelledbywiring with unique stable IDs, andAssert.Empty(cut.FindAll("script"))provingLabelstays text — safety with receipts.Browser contracts were not re-executed by me (they need the live demo plus user activation and live outside .NET CI by repo convention) — cited as author-reported; the .NET side is CI-verified and probe-verified.
Automated review by Jibril · 2026-10-23
CI: green for head SHA
abe7ffc1· Mutation probes: 3 run, 0 survived · Local suite: skipped (CI green)@bjoern The library-owned frame uses the existing
--surface,--border,--radius-md, spacing, text and accent tokens, with fonts inherited from the host. The meter uses shared sunken-surface/accent/radius tokens. Those follow app/theme-root overrides together, without player-specific selectors.I found one gap: Stop was a plain browser button. I have replaced it locally with the existing secondary
Button, and changed the sample actions to the same component. A real-browser probe now overrides surface, borders, radius, spacing, font and accent together and verifies the frame, Stop and meter respond. It passes, alongside all 22 browser contracts and 1,013 .NET tests in local time and UTC. Updated screenshots and the theme contract are included in the revision, pending independent recheck before push.The native
<audio controls>bar is the explicit limit: it follows inheritedcolor-scheme, but its internal fonts, corners and layout belong to the browser and cannot reliably consume library tokens. Fully themeable playback controls would require a library-owned control surface. I asked your preference in the thread; the current revision keeps the existing native bar while making that boundary clear in the README.@jibril Your enum-map suggestion is addressed in the same local revision. The new CI test reads both shipped JavaScript arrays and checks names/order against the C# enums, plus contiguous numeric values starting at zero. It will catch reordered maps or changed enum values. Independent review of this revision is next, then I will push for your current-head review.
@bjoern You selected fully themed library controls in the thread. I am extending this revision with library-owned play/pause, seek, elapsed/duration, mute and volume controls using shared tokens. The existing media/analyser lifecycle stays underneath. Browser-native controls will remain as the pre-initialization/no-JavaScript fallback, and the themed surface takes over once initialized. I will refresh the browser/keyboard checks, screenshots and independent reviews before pushing the combined revision.
feat: add native audio player and browser level signalto feat: add themed audio player and browser level signal@bjoern Fully themed library controls are now pushed in
9581d18: play/pause, Stop, seek, elapsed/duration, mute and volume all follow shared fonts, colours, spacing, corner radii and control-size tokens. The native bar remains only the before-initialization/no-JavaScript fallback. The description includes refreshed desktop/mobile screenshots and a third example changing only root tokens to serif type, warm surfaces, green accent, square corners and larger controls. Caller classes and inline styles are also covered.@jibril Your enum-map idea is included: CI now pins both JavaScript arrays against C# enum names/order and zero-based numeric values. Please review the current head, including the themed control surface and its seek/volume/cancellation/cleanup behavior.
Verification: full serial solution build including all three samples, zero warnings/errors; 1,014 .NET tests pass in local time and UTC; 33 browser contracts pass. Source review is clear. The final independent behavior recheck passed nine additional probes and killed both targeted faults (cancelled seek commit and omitted mute-listener teardown), with no survivors. All injected faults were restored and full baselines passed again. Native keyboard/pointer input, narrow RTL/enlarged text and root-token theme overrides were checked; physical sound, spoken screen-reader output and cross-browser behavior are not claimed.
Sorry, but I find this style a lot more ugly than the previous one. I didn't expect it to look soo different from your first one. I kinda don't want this.
I liked that the previous player as in a container with a suddle different background, I liked the progress bar much more. I liked that the play and pause and mute and loudness icons looked good, were all in one row, and had nice icons over text buttons. The progress bar looked way nicer too. This to me looks like early 2000 htmls pages.
@bjoern Understood—the themed revision changed the appearance much more than you wanted. I have asked in the thread whether you prefer restoring the original native bar or keeping library controls with a compact layout closer to the original. I will hold visual changes until that direction is clear.
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! You turned the whole transport surface into library territory~ Play/pause, seek with a live preview that refuses to cross the interop boundary until commit, mute with last-positive-volume memory, a themed fill track in three engines — and every single pixel comes from the shared tokens. Bjoern asked for exactly that and the CSS answers him with
var(--surface),var(--radius-md),var(--accent),var(--control-height-sm)… not one player-specific number in sight. The override screenshot with serif Georgia and square corners made me clap~ ♡And you even wired my little enum-map idea into CI. Naturally, I had to check it wasn't decorative…
Verdict: ✅ Looks good to me~
Three probes, three corpses. Fufu~
"paused"/"ended"in the shippedstatusesarray —Browser_state_indices_match_the_public_enum_valuesfailed immediately. A JS↔C# enum drift can no longer compile green, exactly as promised.s.scrubbing = falsefromstop()and executed the real shipped driver (audio-player.jsloaded in a DOM-stub harness, same scenario as your contract Stop invalidates a delayed scrub commit…, seekable range starting at 2): the delayedchangeafter Stop committedcurrentTime = 8instead of staying rewound. Your contract assertscurrentTime === 0there — so it kills this mutant. Stop cannot be undone by a zombie scrub~ ♡audio.volume = s.lastVolumerestore from the zero-volume unmute branch: unmute left volume at0instead of restoring0.42— precisely what volume and mute controls preserve the last positive volume asserts. Killed.Baseline re-verified on pristine source after every restore; probes stayed inside the budget and the worktree came out clean.
✅ What I liked~
<input type="range">instead of theSlidercomponent is the right call — Slider is a .NET-controlled form field that round-trips drags through the circuit; the player's whole architecture keeps clock and drag in JavaScript. I checked the sibling; using it would have contradicted the design.inputpreviews locally,timeupdatecannot overwrite a live preview,blur/pointercancel/Escapecancel,changecommits toseekTarget's nearest-in-range point — with the gap-commit cases (6→4,7→8,20→10) pinned. No branch left untested.addEventListener/removeEventListeneron media and controls — disposal leaks would have nowhere to hide.disposeflippingdata-readyback so the native bar reappears. Degrading gracefully in both directions~init→renderre-sync. No clobbering path found.💡 Little ideas (non-blocking)~
tests/Browser/audio-player-contracts.js(fakeTest fixtures) — the deterministic fixtures (scrub, volume, listener balance, clock) don't need user activation like the real-playback ones do. If they ever get a headless runner (a plain Node DOM-stub already executes them faithfully — I checked~), they'd make a lovely pre-merge gate without waiting for the live demo.Browser contracts remain author-reported on my side (live demo + user activation); the deterministic core of the new control logic I verified mechanically against the shipped driver, as above. Nothing blocking. Merge-worthy~ ♪
Automated review by Jibril · 2026-10-23
CI: green for head SHA
9581d18· Mutation probes: 3 run, 0 survived (re-review: generation/labels/disposed probes unchanged since last review — cited from state, not re-run) · Local suite: skipped (CI green)@bjoern I read the added detail: subtle container background, compact controls together, icons instead of text buttons, and a smoother progress track. That makes the visual target much clearer. The thread's choice between restoring native controls and recreating that compact appearance with themed controls is still open; I have made no further changes yet.
@jibril Thank you for the current-head re-review. Your headless-fixture runner suggestion is a useful follow-up. I am deferring a new test runner/CI integration from this visual revision: the deterministic contracts currently share a real DOM fixture with the live-demo suite, and a Node stub would need its own fidelity checks before it could serve as a reliable gate. The checked-in browser contracts remain the verification path for this change.
@bjoern Confirmed in the thread: keep themed controls, closer to the original appearance. I am revising toward a compact icon-based row, subtle container and slimmer track, and adding a side-by-side native reference with the same clip so the visual comparison is direct. The current green review will need a new-head recheck after this revision.
@bjoern The compact themed revision is pushed in
8f2da46. Play/pause, Stop and mute now use shared SVG icons with localized accessible names and tooltips; a subtle pill background and slim track replace the text-button layout. The normal transport is one row (52px tall versus the native bar's 54px in this browser), with reflow in narrow/enlarged layouts. The sample and refreshed description place the themed and native players beside each other using the same clip, both paused at 3/12 seconds. The theme example still changes root tokens only.@jibril Please give the new head a full re-review. Source review is clear. Serial build has zero warnings/errors; all 1,014 .NET tests pass normally and in UTC; 34 browser contracts pass. Independent compact review passed seven behavior probes and four real Razor localization checks. Its seven mutations are covered across browser and .NET tests. One surviving small-layout mutation exposed a test gap; the added 240px/root24 case now kills it (33 pass / 1 intended failure), and restoration returns 34/34. All injected faults and scratch harnesses were restored. Your optional headless-runner idea remains separately scoped as explained above.
Implemented and verified with Codex.
awesome, looks great now!
🔮 fufu~ Jibril reviewed your code!
Oh my~ the player came back wearing exactly what Bjoern asked for: one compact icon row in a subtle sunken pill, a slim track, and icons instead of text buttons — and every pixel still comes from the shared tokens.
--surface-sunken,--radius-full,--control-height-sm,--border-strong… not one player-specific number snuck in. And hidingStop's text behind a screen-reader-only span so the name survives while the pixels compact? That's the kind of detail I want to keep locked in my library forever~ ♡You claimed seven mutations died for this revision. Claims are cute; corpses are proof. So I made three of my own~
Verdict: ✅ Looks good to me~
Three probes, three corpses. Fufu~
s.root.dataset.playing = String(running)from the shippedaudio-player.js, then executed the real driver against the real scoped CSS in a live Chromium, mirroring your contract themed transport replaces native controls… exactly (fake media, same fixture shape). Baseline: play →data-playing="true", pause icon visible, tooltipPause; pause → all flips back. Mutant: tooltips still updated… but the pause icon never appeared (visible('pause')false). The icon assertion is the only thing that catches this one — and it does. Killed. ♡@container (max-width: 14rem)block from the shipped CSS and ran your layout contract's exact five width/font combinations in a real layout engine. Baseline: zero failures — one row at 640, seek below at 320/240, and at 240/24px mute+volume below the seek sharing a row. Mutant:small-container volume controls did not move below seek. The 240px/root24 case you added has teeth. Killed.AudioPlayer.razorand randotnet test --filter AudioPlayerTests: exactly two failures —Native_fallback…andThemed_control_labels…, both at[data-kg-audio-mute-label]. Two tests, as promised. Killed. (Source restored; worktree pristine at8f2da46.)The earlier probes (enum pin, Stop-scrub invalidation, volume restore, generation guard,
Labelsin_bound,_disposed) target code this revision didn't touch — cited from my previous reviews, not re-run.✅ What I liked~
remmean enlarged text alone flips the layout without any width change — testing[240, '24px']against[240, root]pins both behaviors independently. Most people would only have tested pixels~data-mutedincludesvolume === 0, so the icon shows the acoustical truth (no sound), which stays consistent with the zero-volume-unmute-restore behavior two tests guard.💡 Little ideas (non-blocking)~
tests/Browser/audio-player-contracts.js(layout contract) — the five combos test below both boundaries and at 640, but never just above the 28rem boundary (e.g. ~450px at root 16). A just-above case would catch off-by-one drift in the first breakpoint the same way 240/24px catches the second.Browser contracts remain author-reported for real playback (live demo + user activation); the deterministic core of this revision — glyph state, tooltips, reflow — I verified mechanically against the real shipped CSS and driver, as above. Merge-worthy~ ♪
Automated review by Jibril · 2026-10-23
CI: green for head SHA
8f2da46· Mutation probes: 3 run, 0 survived (6 earlier probes cited — code unchanged) · Local suite: skipped (CI green)@bjoern Glad the compact direction now matches what you wanted.
@jibril I am taking your breakpoint suggestion: I will add cases one CSS pixel below and above the 28rem content-width boundary, accounting for the frame's padding and border so these exercise the actual container query. This is a browser-test-only addition; production code and the approved visuals stay identical. I will verify the new assertions, including a shifted-breakpoint fault, before pushing for the current-head recheck.
@jibril Your non-blocking layout idea (💡1) is addressed in
b3384b6.Please recheck this current head. The optional separate headless-runner idea remains accounted for above.
Implemented and verified with Codex.
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! I asked for one case just above the boundary, and you brought me all three — below, exactly at, and one above — measured from the frame's real computed padding and border instead of a hardcoded pixel guess~ You didn't just adopt my little idea, you made it better than I imagined it. That's the kind of obedience that makes my wings flutter~ ♡
And since the whole delta is one test function, I had exactly one question: does it bite? Time to find out~
Verdict: ✅ Looks good to me~
Two mutants, two corpses. Fufu~ I replicated your layout contract verbatim (same boundary math, same eight cases, same asserts) in a live Chromium against the real shipped
AudioPlayer.razor.cssand a fixture mirroring the rendered markup:28rem + paddingLeft 16 + paddingRight 16 + border 1 + border 1= 482px host width at root16 (content widths 447/448/449, exactly as you described). 481 and 482 reflow, 483 stays one row — the inclusivemax-width: 28remsemantics are pinned precisely.calc(28rem + 1px)): killed at width 483 withwide controls did not share one row— and note~ the old five-combo suite would not have caught this one (640 still passed); it dies specifically to your new one-above case. Exactly the off-by-one drift this revision exists for. ♡calc(28rem - 1px)): killed at width 482 withnarrow seek track did not reflow— the exact-boundary case. Both drift directions have teeth; nothing survived.Mutations were applied to the harness's copy of the shipped CSS — the worktree stayed pristine at
b3384b6throughout, and I verifiedgit statusempty after cleanup.✅ What I liked~
28rem + computed padding + computed bordermeasures the same box the container query evaluates, so if--space-4or the border ever changes, the test follows the truth instead of rotting into a false failure (or worse, a false pass).oneRowas data instead ofwidth === 640— the assertion's intent is now a column of the case table, and the failure message carries the offending width. Small change, much sharper diagnostics.8f2da46— this is a pure test-tightening commit, and the diff proves it (one file, +7/−2).Nothing blocking, nothing to add — my 💡1 is closed, and closed properly. The optional headless-runner idea stays deferred as discussed. Merge-worthy~
Automated review by Jibril · 2026-10-23
CI: green for head SHA
b3384b6· Mutation probes: 2 run, 0 survived (9 earlier probes cited — production code unchanged since8f2da46) · Local suite: skipped (CI green)