v6.4 — beat detection suspends with no music (no ghost beats) + royal purple dark theme with light-yellow highlights

This commit is contained in:
Z User 2026-10-27 10:31:04 +00:00
parent 0e4d37e269
commit df5e6e3374
72 changed files with 264 additions and 62 deletions

View file

@ -44,6 +44,11 @@
}
:root {
/* v6.4 accent plumbing — same values as .dark (the default theme); the
.light block overrides both for the white theme. */
--accent-rgb: 253 224 71;
--scrollbar-thumb: #56406f;
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
@ -78,58 +83,100 @@
--sidebar-ring: oklch(0.708 0 0);
}
/*
* OVE dark theme (v6.4) — ROYAL PURPLE with light-yellow highlights.
*
* The app chrome is styled with the Tailwind zinc/emerald utilities directly,
* so — like the .light remap below — the dark palette is produced by remapping
* those ramps under `.dark` (unlayered class rules beat Tailwind's @layer
* theme, and color-mix keeps the var references so opacity variants follow):
* • zinc is re-hued into a royal-purple ramp — 950 is the deep purple page,
* 900 the panel purple, 800/700 borders, 500→50 lavender text;
* • emerald (the old green accent) becomes the LIGHT YELLOW highlight
* family — emerald-400 is #fde047 for CTAs/BPM/active states, hover
* lightens to #fef08a, and emerald-950 (text on the yellow buttons)
* becomes a deep royal purple;
* • the shadcn semantic tokens follow (background/card/border/primary…),
* so dialogs, toasts, sliders and switches pick up the same theme;
* • --accent-rgb carries the highlight color as an rgb triple for the
* .glow-* shadow utilities at the bottom of this file.
* The visualization canvas keeps its own black stage by design.
*/
.dark {
color-scheme: dark;
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
/* semantic tokens — royal purple chrome, light-yellow accent */
--background: #1b0f2e;
--foreground: #f5f1fb;
--card: #2a1a42;
--card-foreground: #f5f1fb;
--popover: #2a1a42;
--popover-foreground: #f5f1fb;
--primary: #fde047;
--primary-foreground: #241238;
--secondary: #35244e;
--secondary-foreground: #ece4f8;
--muted: #2a1a42;
--muted-foreground: #ab97d1;
--accent: #35244e;
--accent-foreground: #ece4f8;
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
--border: #453063;
--input: #453063;
--ring: #fde047;
--chart-1: #fde047;
--chart-2: #937cbb;
--chart-3: #facc15;
--chart-4: #74599f;
--chart-5: #c9b8e6;
--sidebar: #2a1a42;
--sidebar-foreground: #f5f1fb;
--sidebar-primary: #fde047;
--sidebar-primary-foreground: #241238;
--sidebar-accent: #35244e;
--sidebar-accent-foreground: #ece4f8;
--sidebar-border: #453063;
--sidebar-ring: #fde047;
/* zinc ramp → royal purple */
--color-zinc-50: #f5f1fb;
--color-zinc-100: #ece4f8;
--color-zinc-200: #ded2f0;
--color-zinc-300: #c9b8e6;
--color-zinc-400: #ab97d1;
--color-zinc-500: #937cbb;
--color-zinc-600: #74599f;
--color-zinc-700: #56406f;
--color-zinc-800: #3d2957;
--color-zinc-900: #2a1a42;
--color-zinc-950: #1b0f2e;
/* emerald → light-yellow highlights */
--color-emerald-200: #fef9c3;
--color-emerald-300: #fef08a;
--color-emerald-400: #fde047;
--color-emerald-500: #facc15;
--color-emerald-950: #241238;
/* highlight rgb triple for the .glow-* utilities + scrollbar thumb */
--accent-rgb: 253 224 71;
--scrollbar-thumb: #56406f;
}
/*
* OVE light theme (v6.3).
*
* The app is styled with the Tailwind zinc/emerald/rose/amber utilities
* directly (no semantic tokens for the app chrome), so the light theme is
* implemented by REMAPPING those palettes under `.light` (next-themes sets
* the class on <html>):
* • zinc is inverted — 950 (page) becomes white, 100 (text) becomes
* near-black; borders/hover/panels land on their light equivalents;
* • emerald/rose/amber accents are deepened one step so text and buttons
* keep WCAG-ish contrast on white (emerald-400 → emerald-600 etc.),
* hover states still lighten (emerald-300 → emerald-500);
* • the visualization canvas keeps its own black stage by design.
* OVE light theme (v6.3) — the white theme on the flip side of the header
* tick switch. Same remap technique: zinc is inverted (950 page → white,
* 100 text → near-black) and the emerald/rose/amber accents are deepened one
* step so text and buttons keep WCAG-ish contrast on white. The dark theme
* above is completely independent — flipping the switch swaps both ramps.
*/
.light {
color-scheme: light;
/* accent plumbing for the white theme — deep emerald highlights */
--accent-rgb: 5 150 105;
--scrollbar-thumb: #d4d4d8;
/* inverted zinc ramp */
--color-zinc-50: #18181b;
--color-zinc-100: #18181b;
@ -161,3 +208,31 @@
@apply bg-background text-foreground;
}
}
/*
* Theme-aware accent glows (v6.4). The old hardcoded emerald rgba() shadows
* could not follow the palette swap, so every glow now reads --accent-rgb:
* light yellow on royal purple in the dark theme, deep emerald on white in
* the light theme.
*/
.glow-logo {
box-shadow: 0 0 16px rgb(var(--accent-rgb) / 0.35);
}
.glow-segment {
box-shadow: inset 0 0 0 1px rgb(var(--accent-rgb) / 0.35);
}
.glow-cta {
box-shadow: 0 0 20px rgb(var(--accent-rgb) / 0.25);
}
.glow-setbeat {
box-shadow: 0 0 24px rgb(var(--accent-rgb) / 0.28);
}
.glow-beat-flash {
box-shadow: 0 0 18px 4px rgb(var(--accent-rgb) / 0.65);
}
.glow-beat-idle {
box-shadow: 0 0 8px 1px rgb(var(--accent-rgb) / 0.3);
}
.glow-status {
box-shadow: 0 0 8px rgb(var(--accent-rgb) / 0.8);
}

View file

@ -40,6 +40,12 @@ function useBeatWatcher() {
useEffect(() => {
if (!stats) return
// No source playing/loaded → beat detection is suspended (v6.4). Also
// drop the phase reference so a resumed source can't false-flash.
if (stats.source === 'idle') {
prevPhaseRef.current = null
return
}
const b = stats.beat
const prev = prevPhaseRef.current
prevPhaseRef.current = b.phase

View file

@ -22,6 +22,7 @@ export function BeatDot({ size = 44, className }: BeatDotProps) {
const phase = useAvaStore((s) => s.stats?.beat.phase ?? 0)
const beatFlash = useAvaStore((s) => s.beatFlash)
const bpm = useAvaStore((s) => s.stats?.beat.bpm ?? 0)
const idle = useAvaStore((s) => s.source) === 'idle'
// flash state is derived: a flash is "on" until its seq is cleared by the
// hold timeout (setState only ever happens inside the timeout callback)
@ -47,7 +48,11 @@ export function BeatDot({ size = 44, className }: BeatDotProps) {
className={cn('relative shrink-0 select-none', className)}
style={{ width: size, height: size }}
role="img"
aria-label={`Beat indicator — ${bpm > 0 ? `${bpm.toFixed(1)} BPM` : 'no tempo'}${flashSource === 'predicted' ? ', predicted' : ''}`}
aria-label={
idle
? 'Beat indicator — paused, no audio playing'
: `Beat indicator — ${bpm > 0 ? `${bpm.toFixed(1)} BPM` : 'no tempo'}${flashSource === 'predicted' ? ', predicted' : ''}`
}
>
{/* phase progress ring */}
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} className="absolute inset-0 -rotate-90">
@ -74,13 +79,16 @@ export function BeatDot({ size = 44, className }: BeatDotProps) {
/>
</svg>
{/* filled pulse dot (audio / tap beats) */}
{/* filled pulse dot (audio / tap beats) — dimmed while no source is
playing/loaded, since beat detection is suspended (v6.4) */}
<div
className={cn(
'absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-emerald-400 transition-all duration-150 ease-out',
flashing && !predicted
? 'opacity-100 shadow-[0_0_18px_4px_rgba(52,211,153,0.65)]'
: 'opacity-80 shadow-[0_0_8px_1px_rgba(52,211,153,0.3)]',
? 'glow-beat-flash opacity-100'
: idle
? 'opacity-35'
: 'glow-beat-idle opacity-80',
)}
style={{
width: dotBase,

View file

@ -30,6 +30,7 @@ export function BeatTab() {
const beatFlash = useAvaStore((s) => s.beatFlash)
const beatConfig = useAvaStore((s) => s.beatConfig)
const setBeatValue = useAvaStore((s) => s.setBeatValue)
const source = useAvaStore((s) => s.source)
const beat = stats?.beat
const bpm = beat?.bpm ?? 0
@ -37,11 +38,14 @@ export function BeatTab() {
const beatCount = stats?.beatCount ?? 0
const lastSource = beatFlash?.source ?? 'none'
const tapFeedback = lastTap
? lastTap.locked
? `Locked: ${lastTap.bpm.toFixed(1)} BPM`
: `Tap ${lastTap.tapCount}… keep tapping`
: 'Tap along with the music — Space works too'
const tapFeedback =
source === 'idle'
? 'No audio playing — start a source, then tap along'
: lastTap
? lastTap.locked
? `Locked: ${lastTap.bpm.toFixed(1)} BPM`
: `Tap ${lastTap.tapCount}… keep tapping`
: 'Tap along with the music — Space works too'
return (
<div className="space-y-5">
@ -74,7 +78,7 @@ export function BeatTab() {
type="button"
whileTap={{ scale: 0.965 }}
onClick={tapBeat}
className="flex h-14 w-full flex-col items-center justify-center gap-0.5 rounded-xl bg-emerald-400 font-bold text-emerald-950 shadow-[0_0_24px_rgba(52,211,153,0.28)] transition-colors hover:bg-emerald-300"
className="flex h-14 w-full flex-col items-center justify-center gap-0.5 rounded-xl bg-emerald-400 font-bold text-emerald-950 glow-setbeat transition-colors hover:bg-emerald-300"
title="Tap to set the tempo (or press Space)"
>
<span className="flex items-center gap-2 text-sm font-bold tracking-[0.12em]">
@ -162,7 +166,9 @@ export function BeatTab() {
<span className="font-semibold text-zinc-400">How it works:</span> beats are detected from the smoothed bass
energy crossing the sensitivity threshold. Consistent intervals lock a tempo (the confidence meter), and a
phase-locked predictor then keeps firing beats on the learned grid — shown as a hollow ring with a PRED badge —
whenever the bass disappears. <span className="font-semibold text-zinc-400">Set Beat</span> teaches the tempo
whenever the bass disappears. Detection and prediction only run while a source is playing —
they pause (and the BPM resets) whenever no music is loaded.{' '}
<span className="font-semibold text-zinc-400">Set Beat</span> teaches the tempo
directly: tap along and two or more taps lock the period and phase to your rhythm.
</p>
</div>

View file

@ -56,8 +56,10 @@ const BANDS: BandSpec[] = [
gainKey: 'midGain',
lowRange: { min: 100, max: 5000, step: 25 },
highRange: { min: 300, max: 8000, step: 50 },
barClass: 'bg-amber-300',
textClass: 'text-amber-300',
// v6.4: orange (was amber — too close to the yellow bass/highlight in
// the royal-purple dark theme; keeps the three VU bands distinct)
barClass: 'bg-orange-400',
textClass: 'text-orange-400',
},
{
key: 'high',

View file

@ -58,7 +58,9 @@ export function FooterBar() {
<span
className={cn(
'size-1.5 rounded-full',
error ? 'animate-pulse bg-rose-400 shadow-[0_0_8px_rgba(251,113,133,0.8)]' : 'bg-emerald-400 shadow-[0_0_8px_rgba(52,211,153,0.8)]',
error
? 'animate-pulse bg-rose-400 shadow-[0_0_8px_rgba(251,113,133,0.8)]'
: 'glow-status bg-emerald-400',
)}
aria-hidden
/>

View file

@ -42,7 +42,7 @@ function SourceSegmentButton({
className={cn(
'flex h-11 items-center gap-1.5 rounded-md px-3 text-xs font-medium transition-colors',
active
? 'bg-emerald-400/15 text-emerald-300 shadow-[inset_0_0_0_1px_rgba(52,211,153,0.35)]'
? 'glow-segment bg-emerald-400/15 text-emerald-300'
: tone === 'stop'
? 'text-rose-300/70 hover:bg-zinc-800 hover:text-rose-300'
: 'text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100',
@ -179,7 +179,7 @@ export function AppHeader() {
<div className="mx-auto flex max-w-[1800px] flex-wrap items-center gap-x-3 gap-y-2 px-3 py-2 sm:px-4">
{/* logo */}
<div className="order-1 flex items-center gap-2.5">
<div className="grid size-9 place-items-center rounded-lg bg-emerald-400 shadow-[0_0_16px_rgba(52,211,153,0.35)]">
<div className="glow-logo grid size-9 place-items-center rounded-lg bg-emerald-400">
<AudioWaveform className="size-5 text-emerald-950" aria-hidden />
</div>
<div>

View file

@ -21,7 +21,7 @@ export function NativeTab() {
<a
href="/api/rust-scaffold"
download
className="flex h-12 items-center justify-center gap-2 rounded-xl bg-emerald-400 font-semibold text-emerald-950 shadow-[0_0_20px_rgba(52,211,153,0.25)] transition-colors hover:bg-emerald-300"
className="glow-cta flex h-12 items-center justify-center gap-2 rounded-xl bg-emerald-400 font-semibold text-emerald-950 transition-colors hover:bg-emerald-300"
>
<Download className="size-4" />
Download Rust Scaffold (.zip)

View file

@ -1,7 +1,10 @@
// AVA — shared UI constants & tiny helpers for the components
// v6.4: the Firefox scrollbar thumb color follows the theme via
// --scrollbar-thumb (royal purple in dark, gray in light) — the WebKit
// classes below already use the remapped zinc-700 utility.
export const thinScrollbar =
'[scrollbar-width:thin] [scrollbar-color:#52525b_transparent] [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-zinc-700'
'[scrollbar-width:thin] [scrollbar-color:var(--scrollbar-thumb)_transparent] [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-zinc-700'
/** 1000 → "1.0 kHz", 100 → "100 Hz" */
export function fmtHz(v: number): string {

View file

@ -81,6 +81,27 @@ export class BeatTracker {
return this.beatCount
}
/** Constant frame returned while no audio source is playing/loaded. */
private static readonly STANDBY_FRAME: BeatFrame = {
beat: false,
source: 'none',
bpm: 0,
phase: 0,
confidence: 0,
}
/**
* Called by the engine every frame while NO audio source is playing or
* loaded (v6.4). Beat detection, the prediction grid and any tapped tempo
* all stop: the indicator goes dark, the BPM readout resets and everything
* learned is dropped — so a stale tempo can never leak ghost beats while
* idle (or into the next track once audio starts again).
*/
standby(): BeatFrame {
this.reset()
return BeatTracker.STANDBY_FRAME
}
getBpm(): number {
return this.bpm
}

View file

@ -156,7 +156,14 @@ export class AvaEngine {
// ---- audio + beat -----------------------------------------------------
const levels = this.audio.getBands(this.bandConfig, dt)
const beatFrame = this.beat.update(levels.bassAtt, now, dt, this.beatConfig)
// Beat detection (and the predictor grid) only runs while a source is
// playing/loaded. With no audio, the tracker goes to standby: no beats
// fire, the BPM resets to 0 and learned tempo is dropped — so nothing
// pulses to ghost beats when the music is stopped (v6.4).
const beatFrame =
this.audio.kind === 'idle'
? this.beat.standby()
: this.beat.update(levels.bassAtt, now, dt, this.beatConfig)
// ---- environment ------------------------------------------------------
const env = this.env

0
src/lib/ava/preset-io.ts Normal file → Executable file
View file

0
src/lib/ava/user-presets.ts Normal file → Executable file
View file

0
src/lib/preset-paths.ts Normal file → Executable file
View file

View file

@ -7,6 +7,9 @@
// (v6.0 = rename to Open Visualization Engine + version indicator).
//
// History:
// 6.4 — beat detection suspends when no music is playing/loaded (no ghost
// beats after Stop, BPM resets) + dark theme restyle: royal purple
// chrome with light-yellow highlights
// 6.3 — light/dark theme tick switch in the header (white theme via .light
// palette remap; per-visitor, persisted by next-themes)
// 6.2 — security hardening: read-only embedded library (server refuses all
@ -19,4 +22,4 @@
export const APP_NAME = 'Open Visualization Engine'
export const APP_SHORT = 'OVE'
export const APP_VERSION = '6.3'
export const APP_VERSION = '6.4'

0
tool-results/e2e-v62/01-desktop-presets.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

0
tool-results/e2e-v62/02-embedded-folder-selected.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

0
tool-results/e2e-v62/03-embedded-preset-selected.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

0
tool-results/e2e-v62/04-script-tab-created.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

0
tool-results/e2e-v62/05-save-blocked-toast.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 112 KiB

0
tool-results/e2e-v62/06-user-library-empty.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

0
tool-results/e2e-v62/07-mobile-firstpaint.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

0
tool-results/e2e-v62/08-mobile-bottom.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

0
tool-results/e2e-v62/09-mobile-embedded-selected.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

0
tool-results/e2e-v63/01-dark-default.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

0
tool-results/e2e-v63/02-dark-presets.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

0
tool-results/e2e-v63/03-light-after-toggle.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

0
tool-results/e2e-v63/03b-light-toggle-checked.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

0
tool-results/e2e-v63/04-light-simulated.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 107 KiB

0
tool-results/e2e-v63/05-light-presets-tree.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 109 KiB

0
tool-results/e2e-v63/06-light-preset-panel.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

0
tool-results/e2e-v63/07-light-script.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 129 KiB

0
tool-results/e2e-v63/08-light-help.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 127 KiB

0
tool-results/e2e-v63/09-light-detect.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

0
tool-results/e2e-v63/10-light-beat.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 121 KiB

0
tool-results/e2e-v63/11-light-native.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 151 KiB

0
tool-results/e2e-v63/11b-light-native-full.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 151 KiB

0
tool-results/e2e-v63/12-light-dialog.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

0
tool-results/e2e-v63/13-light-toast.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

0
tool-results/e2e-v63/13b-light-toast-mid.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 114 KiB

0
tool-results/e2e-v63/14-light-delete-dialog.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

0
tool-results/e2e-v63/15-light-after-reload.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

0
tool-results/e2e-v63/16-dark-after-reload.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

0
tool-results/e2e-v63/17-light-REAL-served-css.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 107 KiB

0
tool-results/e2e-v63/18-real-toggle-to-dark.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

0
tool-results/e2e-v63/19-real-toggle-back-to-light.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

0
tool-results/e2e-v63/20-real-light-dialog.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

0
tool-results/e2e-v63/21-real-dark-dialog.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

0
tool-results/e2e-v63/22-real-light-presets.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

0
tool-results/e2e-v63/23-real-light-script.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 128 KiB

0
tool-results/e2e-v63/24-real-light-help.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 129 KiB

0
tool-results/e2e-v63/25-dark-script.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 123 KiB

0
tool-results/e2e-v63/26-mobile-light-top.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

0
tool-results/e2e-v63/27-mobile-light-bottom.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

0
tool-results/e2e-v63/28-mobile-dark-top.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

0
tool-results/e2e-v63/29-mobile-dark-bottom.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

0
tool-results/e2e-v63/30-light-detect-demo-vu.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 752 KiB

After

Width:  |  Height:  |  Size: 752 KiB

0
tool-results/e2e-v63/31-light-detect-live.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 532 KiB

After

Width:  |  Height:  |  Size: 532 KiB

0
tool-results/e2e-v63/32-light-live-visual.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 670 KiB

After

Width:  |  Height:  |  Size: 670 KiB

0
tool-results/e2e-v63/33-real-light-tree-expanded.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 112 KiB

0
tool-results/e2e-v63/34-final-pristine-dark.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

0
tool-results/ove-containment.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 157 KiB

After

Width:  |  Height:  |  Size: 157 KiB

0
tool-results/ove-dragonscales.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 220 KiB

After

Width:  |  Height:  |  Size: 220 KiB

0
tool-results/ove-kingish.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 250 KiB

After

Width:  |  Height:  |  Size: 250 KiB

0
tool-results/read_1793080556522_6263211d285d.txt Normal file → Executable file
View file

0
tool-results/read_1793082276516_65b3b8ecd40e.txt Normal file → Executable file
View file

0
tool-results/read_1793082296314_a82167d4aa24.txt Normal file → Executable file
View file

0
tool-results/read_1793082298965_cc5347613a20.txt Normal file → Executable file
View file

0
tool-results/read_1793086254569_65b3b8ecd40e.txt Normal file → Executable file
View file

0
tool-results/read_1793086294893_e6288764dc88.txt Normal file → Executable file
View file

0
tool-results/read_1793087510034_3be5e504589d.txt Normal file → Executable file
View file

View file

@ -1234,3 +1234,72 @@ Stage Summary:
- Implementation note for future styling: new UI should keep using the
zinc/emerald utilities (not raw hex) so both themes keep working; anything
on the emerald buttons should use text-emerald-950.
---
Task ID: 16
Agent: main (Z.ai Code)
Task: v6.4 — (a) beat detection stops when no music is playing/loaded; (b) dark theme restyled to royal purple with light-yellow highlights
Work Log:
- Root cause of ghost beats: AvaEngine.loop fed BeatTracker.update() every frame
regardless of audio state, and the phase-locked predictor keeps firing after
confidence builds (music just stopped) or after Set Beat taps (taps block the
confidence decay path) — so the indicator pulsed to a stale grid with no source.
- src/lib/ava/beat.ts — new standby(): full reset + returns a constant
STANDBY_FRAME {beat:false, source:'none', bpm:0, phase:0, confidence:0}.
Called every idle frame; drops everything learned so stale tempo can never
leak ghost beats while idle OR into the next track (source switches relearn
fresh — a feature).
- src/lib/ava/engine.ts — loop gates the tracker: audio.kind === 'idle' →
standby(), else update(). env.beat/bpm/beatphase/beatconf go to 0 while idle;
stats.source still reports the kind.
- src/app/page.tsx — useBeatWatcher: early return on stats.source === 'idle'
(also drops prevPhaseRef so a resumed source can't false-flash).
- src/components/ava/beat-dot.tsx — dot dims to opacity-35 (no glow) while
idle (flashes still override), aria-label "Beat indicator — paused, no audio
playing"; theme-aware glow-beat-flash/glow-beat-idle classes.
- src/components/ava/beat-tab.tsx — Set Beat feedback while idle: "No audio
playing — start a source, then tap along"; SET BEAT glow class; "How it
works" documents the new pause behavior.
- Theme (dark → royal purple + light yellow), same proven remap technique as
v6.3 .light — src/app/globals.css .dark block rewritten:
• semantic tokens: background #1b0f2e, card/popover #2a1a42, secondary
#35244e, border/input #453063, primary/ring #fde047 (+purple foreground
#241238), chart/sidebar purple-yellow family → dialogs/toasts/sliders/
switches follow.
• zinc ramp → royal purple: 950 #1b0f2e (page/header/footer), 900 #2a1a42
(panels/tabs), 800 #3d2957 (borders/tracks), 700 #56406f, 600 #74599f,
500 #937cbb … 50 #f5f1fb (lavender text ramp).
• emerald → light yellow: 200 #fef9c3, 300 #fef08a, 400 #fde047 (CTAs, BPM,
active states, beat dot), 500 #facc15, 950 #241238 (deep purple text on
yellow buttons — the yellow CTAs' label color).
• new --accent-rgb (253 224 71 dark / 5 150 105 light) + --scrollbar-thumb
(also defaulted in :root); seven .glow-* utilities (logo, segment, cta,
setbeat, beat-flash, beat-idle, status) replace ALL hardcoded emerald
rgba() shadows (7 spots, 6 files) so glows follow the theme; Firefox
scrollbar-color now var-driven (shared.ts).
• light theme untouched apart from the two new vars — verified intact.
- src/components/ava/detect-tab.tsx — Mid band VU amber-300 → orange-400
(amber was indistinguishable from the new yellow bass/highlight; three
bands now yellow / orange / rose in dark, green / orange / rose in light).
- src/lib/version.ts → 6.4 (+history line). bun run lint clean.
Stage Summary:
- E2E (agent-browser, 1440×900 + 390×844), ALL PASS:
• Beat: demo + 5× Set Beat taps → locks 120.2 BPM, predictor carries;
Stop → ZERO ghost flashes over 8 s (previously ~16 predicted beats),
BPM "—", beat count 0, dot dimmed 0.35, aria "paused, no audio playing";
idle taps = one flash each, no sustained grid, honest feedback text.
• Demo auto-detect staying unlocked is PRE-EXISTING documented behavior
(worklog Task 8: demo bassline keeps rolling average high; Set Beat /
real music lock fine) — not a v6.4 regression.
• Dark: body #1b0f2e, logo tile/BPM/Native CTA/beat dot #fde047, CTA text
#241238, footer micro text lavender #937cbb/#c9b8e6 (VLM "gray" note was
the intentional muted style), VU bands distinct, zero console errors.
• Light: white body, emerald-600 accents, emerald glow — no yellow leakage.
• Mobile: no horizontal scroll, footer flush at viewport bottom when
scrolled to end; VLM review of dark+mobile screenshots: cohesive royal
purple + light yellow, no breakage.
- New styling rule going forward: keep using zinc/emerald utilities (they are
the remap surface); on yellow CTAs always text-emerald-950; for accent glows
use the .glow-* classes, never rgba() literals.