v6.3 — light/dark theme tick switch in the header

This commit is contained in:
Z User 2026-10-27 08:22:36 +00:00
parent 87b5a7fe34
commit 0e4d37e269
50 changed files with 1360 additions and 36 deletions

View file

@ -79,6 +79,7 @@
} }
.dark { .dark {
color-scheme: dark;
--background: oklch(0.145 0 0); --background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0); --foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0); --card: oklch(0.205 0 0);
@ -112,6 +113,46 @@
--sidebar-ring: oklch(0.556 0 0); --sidebar-ring: oklch(0.556 0 0);
} }
/*
* 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.
*/
.light {
color-scheme: light;
/* inverted zinc ramp */
--color-zinc-50: #18181b;
--color-zinc-100: #18181b;
--color-zinc-200: #27272a;
--color-zinc-300: #3f3f46;
--color-zinc-400: #52525b;
--color-zinc-500: #71717a;
--color-zinc-600: #8b8b93;
--color-zinc-700: #d4d4d8;
--color-zinc-800: #e4e4e7;
--color-zinc-900: #fafafa;
--color-zinc-950: #ffffff;
/* deeper accents for white backgrounds */
--color-emerald-200: #a7f3d0;
--color-emerald-300: #10b981;
--color-emerald-400: #059669;
--color-rose-300: #f43f5e;
--color-rose-400: #e11d48;
--color-amber-300: #b45309;
--color-amber-400: #d97706;
}
@layer base { @layer base {
* { * {
@apply border-border outline-ring/50; @apply border-border outline-ring/50;

View file

@ -1,5 +1,6 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { Geist, Geist_Mono } from "next/font/google";
import { ThemeProvider } from "next-themes";
import "./globals.css"; import "./globals.css";
import { Toaster } from "@/components/ui/toaster"; import { Toaster } from "@/components/ui/toaster";
import { APP_NAME, APP_SHORT, APP_VERSION } from "@/lib/version"; import { APP_NAME, APP_SHORT, APP_VERSION } from "@/lib/version";
@ -30,12 +31,21 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html lang="en" className="dark" suppressHydrationWarning> <html lang="en" suppressHydrationWarning>
<body <body
className={`${geistSans.variable} ${geistMono.variable} antialiased bg-zinc-950 text-zinc-100 font-sans`} className={`${geistSans.variable} ${geistMono.variable} antialiased bg-zinc-950 text-zinc-100 font-sans`}
> >
{children} {/* class strategy: next-themes puts "dark" (default) or "light" on
<Toaster /> <html> pre-paint; globals.css remaps the palettes under .light */}
<ThemeProvider
attribute="class"
defaultTheme="dark"
enableSystem={false}
disableTransitionOnChange
>
{children}
<Toaster />
</ThemeProvider>
</body> </body>
</html> </html>
); );

View file

@ -74,7 +74,7 @@ export function BeatTab() {
type="button" type="button"
whileTap={{ scale: 0.965 }} whileTap={{ scale: 0.965 }}
onClick={tapBeat} 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-zinc-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 shadow-[0_0_24px_rgba(52,211,153,0.28)] transition-colors hover:bg-emerald-300"
title="Tap to set the tempo (or press Space)" title="Tap to set the tempo (or press Space)"
> >
<span className="flex items-center gap-2 text-sm font-bold tracking-[0.12em]"> <span className="flex items-center gap-2 text-sm font-bold tracking-[0.12em]">

View file

@ -1,13 +1,16 @@
// AVA — sticky app header: logo, audio source controls (segmented), demo BPM // OVE — sticky app header: logo, audio source controls (segmented), demo BPM
// slider, BPM readout, and the beat indicator + "Set Beat" button side by side // slider, theme tick switch, BPM readout, and the beat indicator + "Set Beat"
// (hard requirement: that pairing is always visible in the header). // button side by side (hard requirement: that pairing is always visible in
// the header).
'use client' 'use client'
import { useRef } from 'react' import { useRef, useSyncExternalStore } from 'react'
import { AudioWaveform, Disc3, FileAudio, Link2, Mic, Square, Timer } from 'lucide-react' import { AudioWaveform, Disc3, FileAudio, Link2, Mic, Moon, Square, Sun, Timer } from 'lucide-react'
import { useTheme } from 'next-themes'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Slider } from '@/components/ui/slider' import { Slider } from '@/components/ui/slider'
import { Switch } from '@/components/ui/switch'
import { BeatDot } from '@/components/ava/beat-dot' import { BeatDot } from '@/components/ava/beat-dot'
import { LinkDialog } from '@/components/ava/link-dialog' import { LinkDialog } from '@/components/ava/link-dialog'
import { tapBeat, useAudioSource } from '@/components/ava/audio-source' import { tapBeat, useAudioSource } from '@/components/ava/audio-source'
@ -140,6 +143,36 @@ function BpmReadout() {
) )
} }
/**
* Light-switch tick: left position (unchecked) keeps the dark theme, flipping
* right (checked) switches the page to the white theme. Persisted per visitor
* by next-themes (localStorage). The useSyncExternalStore mount guard returns
* false on the server / first client render and true after hydration, so the
* checkbox never causes a hydration mismatch.
*/
const emptySubscribe = () => () => {}
function ThemeToggle() {
const { resolvedTheme, setTheme } = useTheme()
const mounted = useSyncExternalStore(emptySubscribe, () => true, () => false)
const isLight = mounted && resolvedTheme === 'light'
return (
<div
className="flex h-11 items-center gap-1.5 rounded-lg border border-zinc-800 bg-zinc-900/60 px-2.5"
title={isLight ? 'Theme: light — flip left for dark' : 'Theme: dark — flip right for light'}
>
<Moon className="size-3.5 shrink-0 text-zinc-500" aria-hidden />
<Switch
checked={isLight}
onCheckedChange={(v) => setTheme(v ? 'light' : 'dark')}
aria-label={isLight ? 'Switch to dark theme' : 'Switch to light theme'}
className="data-[state=checked]:bg-emerald-400 data-[state=unchecked]:bg-zinc-600"
/>
<Sun className="size-3.5 shrink-0 text-zinc-500" aria-hidden />
</div>
)
}
export function AppHeader() { export function AppHeader() {
return ( return (
<header className="sticky top-0 z-40 border-b border-zinc-800/80 bg-zinc-950/80 backdrop-blur supports-[backdrop-filter]:bg-zinc-950/60"> <header className="sticky top-0 z-40 border-b border-zinc-800/80 bg-zinc-950/80 backdrop-blur supports-[backdrop-filter]:bg-zinc-950/60">
@ -147,7 +180,7 @@ export function AppHeader() {
{/* logo */} {/* logo */}
<div className="order-1 flex items-center gap-2.5"> <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="grid size-9 place-items-center rounded-lg bg-emerald-400 shadow-[0_0_16px_rgba(52,211,153,0.35)]">
<AudioWaveform className="size-5 text-zinc-950" aria-hidden /> <AudioWaveform className="size-5 text-emerald-950" aria-hidden />
</div> </div>
<div> <div>
<h1 className="text-base font-bold leading-none tracking-wide">{APP_SHORT}</h1> <h1 className="text-base font-bold leading-none tracking-wide">{APP_SHORT}</h1>
@ -162,13 +195,14 @@ export function AppHeader() {
<SourceControls /> <SourceControls />
</div> </div>
{/* BPM readout + beat indicator + Set Beat — always paired, always visible */} {/* theme switch + BPM readout + beat indicator + Set Beat — always visible */}
<div className="order-2 ml-auto flex items-center gap-2.5 md:order-3 sm:gap-3"> <div className="order-2 ml-auto flex items-center gap-2.5 md:order-3 sm:gap-3">
<ThemeToggle />
<BpmReadout /> <BpmReadout />
<BeatDot size={44} /> <BeatDot size={44} />
<Button <Button
onClick={tapBeat} onClick={tapBeat}
className="h-11 gap-1.5 bg-emerald-400 px-3 font-semibold text-zinc-950 hover:bg-emerald-300 sm:px-4" className="h-11 gap-1.5 bg-emerald-400 px-3 font-semibold text-emerald-950 hover:bg-emerald-300 sm:px-4"
title="Tap to set the tempo (or press Space)" title="Tap to set the tempo (or press Space)"
> >
<Timer className="size-4" /> <Timer className="size-4" />

View file

@ -65,7 +65,7 @@ function HelpTable({ rows, mono = true }: { rows: [string, string][]; mono?: boo
{rows.map(([name, desc]) => ( {rows.map(([name, desc]) => (
<tr key={name} className="border-b border-zinc-800/60 last:border-0 align-top"> <tr key={name} className="border-b border-zinc-800/60 last:border-0 align-top">
<td <td
className={`py-1 pr-3 whitespace-nowrap text-emerald-300/90 ${mono ? 'font-mono text-[11px]' : 'text-xs'}`} className={`py-1 pr-3 whitespace-nowrap text-emerald-400/90 ${mono ? 'font-mono text-[11px]' : 'text-xs'}`}
> >
{name} {name}
</td> </td>
@ -93,15 +93,15 @@ export function HelpPanel() {
<ol className="list-inside list-decimal space-y-1 text-[11px] leading-relaxed text-zinc-400"> <ol className="list-inside list-decimal space-y-1 text-[11px] leading-relaxed text-zinc-400">
<li>Write header keys (name, mode, blend, persist, linewidth) at the top.</li> <li>Write header keys (name, mode, blend, persist, linewidth) at the top.</li>
<li> <li>
Put setup code in <span className="font-mono text-emerald-300/90">[init]</span>, per-frame code in{' '} Put setup code in <span className="font-mono text-emerald-400/90">[init]</span>, per-frame code in{' '}
<span className="font-mono text-emerald-300/90">[frame]</span>, reactions in{' '} <span className="font-mono text-emerald-400/90">[frame]</span>, reactions in{' '}
<span className="font-mono text-emerald-300/90">[beat]</span>, and the geometry in{' '} <span className="font-mono text-emerald-400/90">[beat]</span>, and the geometry in{' '}
<span className="font-mono text-emerald-300/90">[point]</span>. <span className="font-mono text-emerald-400/90">[point]</span>.
</li> </li>
<li> <li>
Set <span className="font-mono text-emerald-300/90">n</span> (point count) in [init], drive it with{' '} Set <span className="font-mono text-emerald-400/90">n</span> (point count) in [init], drive it with{' '}
<span className="font-mono text-emerald-300/90">bass</span>,{' '} <span className="font-mono text-emerald-400/90">bass</span>,{' '}
<span className="font-mono text-emerald-300/90">beat</span> and <span className="font-mono text-emerald-300/90">v</span>. <span className="font-mono text-emerald-400/90">beat</span> and <span className="font-mono text-emerald-400/90">v</span>.
</li> </li>
<li>Hit Apply ▶ — errors are flagged next to the exact line.</li> <li>Hit Apply ▶ — errors are flagged next to the exact line.</li>
</ol> </ol>
@ -118,15 +118,15 @@ export function HelpPanel() {
<HelpSection title="Functions"> <HelpSection title="Functions">
<HelpTable rows={FUNCS} /> <HelpTable rows={FUNCS} />
<p className="mt-2 text-[11px] text-zinc-500"> <p className="mt-2 text-[11px] text-zinc-500">
Constants: <span className="font-mono text-emerald-300/90">pi tau e</span> Constants: <span className="font-mono text-emerald-400/90">pi tau e</span>
</p> </p>
</HelpSection> </HelpSection>
<HelpSection title="Operators"> <HelpSection title="Operators">
<p className="font-mono text-[11px] leading-relaxed text-zinc-400"> <p className="font-mono text-[11px] leading-relaxed text-zinc-400">
+ - * / % ^ &nbsp;(power) &nbsp;·&nbsp; == != &lt; &lt;= &gt; &gt;= &nbsp;·&nbsp; &amp;&amp; || ! &nbsp;·&nbsp; + - * / % ^ &nbsp;(power) &nbsp;·&nbsp; == != &lt; &lt;= &gt; &gt;= &nbsp;·&nbsp; &amp;&amp; || ! &nbsp;·&nbsp;
ternary <span className="text-emerald-300/90">c ? a : b</span> &nbsp;·&nbsp; assignment{' '} ternary <span className="text-emerald-400/90">c ? a : b</span> &nbsp;·&nbsp; assignment{' '}
<span className="text-emerald-300/90">=</span> (works inside if() and ternary branches) <span className="text-emerald-400/90">=</span> (works inside if() and ternary branches)
</p> </p>
<p className="mt-1.5 text-[11px] leading-relaxed text-zinc-500"> <p className="mt-1.5 text-[11px] leading-relaxed text-zinc-500">
Statements are separated by newlines or semicolons. Comments start with <span className="font-mono">{'#'}</span> or{' '} Statements are separated by newlines or semicolons. Comments start with <span className="font-mono">{'#'}</span> or{' '}
@ -137,7 +137,7 @@ export function HelpPanel() {
<HelpSection title="Example formulas"> <HelpSection title="Example formulas">
<div className="space-y-1.5"> <div className="space-y-1.5">
{EXAMPLES.map(([code, note]) => ( {EXAMPLES.map(([code, note]) => (
<div key={code} className="flex flex-col gap-0.5 rounded border border-zinc-800/70 bg-black/40 px-2.5 py-1.5"> <div key={code} className="flex flex-col gap-0.5 rounded border border-zinc-800/70 bg-zinc-800/40 px-2.5 py-1.5">
<code className="font-mono text-[11px] text-zinc-200">{code}</code> <code className="font-mono text-[11px] text-zinc-200">{code}</code>
<span className="text-[10px] text-zinc-500">{note}</span> <span className="text-[10px] text-zinc-500">{note}</span>
</div> </div>
@ -149,7 +149,7 @@ export function HelpPanel() {
<p className="mb-2 text-[11px] leading-relaxed text-zinc-400"> <p className="mb-2 text-[11px] leading-relaxed text-zinc-400">
A plain-text file: optional header keys before the first section, then any of the four sections in any order. A plain-text file: optional header keys before the first section, then any of the four sections in any order.
</p> </p>
<pre className="overflow-x-auto rounded border border-zinc-800/70 bg-black/40 p-2.5 font-mono text-[11px] leading-relaxed text-zinc-300"> <pre className="overflow-x-auto rounded border border-zinc-800/70 bg-zinc-800/40 p-2.5 font-mono text-[11px] leading-relaxed text-zinc-300">
{`# header keys {`# header keys
name: My Visualization name: My Visualization
author: you author: you

View file

@ -150,7 +150,7 @@ function LinkDialogBody() {
<Button <Button
type="submit" type="submit"
disabled={busy || !url.trim()} disabled={busy || !url.trim()}
className="shrink-0 gap-1.5 bg-emerald-400 font-semibold text-zinc-950 hover:bg-emerald-300" className="shrink-0 gap-1.5 bg-emerald-400 font-semibold text-emerald-950 hover:bg-emerald-300"
> >
{busy ? <Loader2 className="size-4 animate-spin" aria-hidden /> : null} {busy ? <Loader2 className="size-4 animate-spin" aria-hidden /> : null}
{busy ? 'Loading' : 'Load'} {busy ? 'Loading' : 'Load'}

View file

@ -21,14 +21,14 @@ export function NativeTab() {
<a <a
href="/api/rust-scaffold" href="/api/rust-scaffold"
download download
className="flex h-12 items-center justify-center gap-2 rounded-xl bg-emerald-400 font-semibold text-zinc-950 shadow-[0_0_20px_rgba(52,211,153,0.25)] transition-colors hover:bg-emerald-300" 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"
> >
<Download className="size-4" /> <Download className="size-4" />
Download Rust Scaffold (.zip) Download Rust Scaffold (.zip)
</a> </a>
<p className="text-[11px] text-zinc-500">{BLUEPRINT.downloadNote}</p> <p className="text-[11px] text-zinc-500">{BLUEPRINT.downloadNote}</p>
<blockquote className="rounded-lg border border-emerald-400/20 bg-emerald-400/5 p-3 text-[11px] leading-relaxed text-emerald-200/80"> <blockquote className="rounded-lg border border-emerald-400/20 bg-emerald-400/5 p-3 text-[11px] leading-relaxed text-emerald-700/90 dark:text-emerald-200/80">
The <span className="font-mono">.ava</span> scripts you write here run on both the web edition and the native The <span className="font-mono">.ava</span> scripts you write here run on both the web edition and the native
Rust engine — the script language is identical. Rust engine — the script language is identical.
</blockquote> </blockquote>

View file

@ -418,7 +418,7 @@ function NameDialog({
Cancel Cancel
</Button> </Button>
<Button <Button
className="h-9 gap-1.5 bg-emerald-400 text-zinc-950 hover:bg-emerald-300" className="h-9 gap-1.5 bg-emerald-400 text-emerald-950 hover:bg-emerald-300"
disabled={busy} disabled={busy}
onClick={() => void submit()} onClick={() => void submit()}
> >
@ -812,7 +812,7 @@ export function PresetsTab() {
<Button <Button
size="sm" size="sm"
onClick={() => loadPreset(selectedPreset)} onClick={() => loadPreset(selectedPreset)}
className="h-9 w-full gap-1.5 bg-emerald-400 text-zinc-950 hover:bg-emerald-300" className="h-9 w-full gap-1.5 bg-emerald-400 text-emerald-950 hover:bg-emerald-300"
title="Run this preset on the engine and open it in the editor" title="Run this preset on the engine and open it in the editor"
> >
<Play className="size-3.5" /> Load Preset <Play className="size-3.5" /> Load Preset
@ -962,7 +962,7 @@ export function PresetsTab() {
checked={randomize} checked={randomize}
onCheckedChange={(v) => useAvaStore.getState().setRandomize(v === true)} onCheckedChange={(v) => useAvaStore.getState().setRandomize(v === true)}
aria-label="Randomize presets" aria-label="Randomize presets"
className="size-4 data-[state=checked]:border-emerald-400 data-[state=checked]:bg-emerald-400 data-[state=checked]:text-zinc-950" className="size-4 data-[state=checked]:border-emerald-400 data-[state=checked]:bg-emerald-400 data-[state=checked]:text-emerald-950"
/> />
<Label htmlFor="ava-randomize" className="cursor-pointer text-xs text-zinc-300"> <Label htmlFor="ava-randomize" className="cursor-pointer text-xs text-zinc-300">
Randomize Randomize

View file

@ -278,7 +278,7 @@ export function ScriptTab() {
<Button <Button
size="sm" size="sm"
onClick={apply} onClick={apply}
className="h-9 gap-1.5 bg-emerald-400 px-3 text-zinc-950 hover:bg-emerald-300" className="h-9 gap-1.5 bg-emerald-400 px-3 text-emerald-950 hover:bg-emerald-300"
title="Validate and apply to the engine" title="Validate and apply to the engine"
> >
<Play className="size-3.5" /> Apply <Play className="size-3.5" /> Apply
@ -288,7 +288,7 @@ export function ScriptTab() {
size="sm" size="sm"
onClick={() => void saveToTarget()} onClick={() => void saveToTarget()}
disabled={saving} disabled={saving}
className="h-9 gap-1.5 bg-emerald-400 px-3 text-zinc-950 hover:bg-emerald-300" className="h-9 gap-1.5 bg-emerald-400 px-3 text-emerald-950 hover:bg-emerald-300"
title={`Overwrite "${editTarget.name}" with the current editor script`} title={`Overwrite "${editTarget.name}" with the current editor script`}
> >
<Save className="size-3.5" /> Save <Save className="size-3.5" /> Save
@ -440,7 +440,7 @@ export function ScriptTab() {
Cancel Cancel
</Button> </Button>
<Button <Button
className="h-9 gap-1.5 bg-emerald-400 text-zinc-950 hover:bg-emerald-300" className="h-9 gap-1.5 bg-emerald-400 text-emerald-950 hover:bg-emerald-300"
disabled={saveAsBusy} disabled={saveAsBusy}
onClick={() => void saveAsCopy()} onClick={() => void saveAsCopy()}
> >

View file

@ -108,7 +108,7 @@ export function VisualStage({ className }: { className?: string }) {
<div className="grid gap-2.5"> <div className="grid gap-2.5">
<Button <Button
onClick={() => void startDemo()} onClick={() => void startDemo()}
className="h-12 gap-2 bg-emerald-400 text-zinc-950 hover:bg-emerald-300" className="h-12 gap-2 bg-emerald-400 text-emerald-950 hover:bg-emerald-300"
> >
<Disc3 className="size-4" /> Demo Track <Disc3 className="size-4" /> Demo Track
</Button> </Button>

View file

@ -7,6 +7,8 @@
// (v6.0 = rename to Open Visualization Engine + version indicator). // (v6.0 = rename to Open Visualization Engine + version indicator).
// //
// History: // History:
// 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 // 6.2 — security hardening: read-only embedded library (server refuses all
// writes) + per-visitor "user created" presets stored in the browser // writes) + per-visitor "user created" presets stored in the browser
// 6.1 — preset library quality pass: every embedded stand-in is now a UNIQUE // 6.1 — preset library quality pass: every embedded stand-in is now a UNIQUE
@ -17,4 +19,4 @@
export const APP_NAME = 'Open Visualization Engine' export const APP_NAME = 'Open Visualization Engine'
export const APP_SHORT = 'OVE' export const APP_SHORT = 'OVE'
export const APP_VERSION = '6.2' export const APP_VERSION = '6.3'

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

File diff suppressed because it is too large Load diff

View file

@ -1030,3 +1030,207 @@ Stage Summary:
Settings (band/beat/default script) are per-browser now. Settings (band/beat/default script) are per-browser now.
- /api/presets/export intentionally kept: read-only maintainer backup of the - /api/presets/export intentionally kept: read-only maintainer backup of the
embedded library. embedded library.
---
Task ID: 16-d
Agent: general-purpose (E2E verifier)
Task: v6.3 theme tick switch — end-to-end verification
Work Log:
- Read worklog Task 15 + 15-11 for context; read v6.3 sources read-only
(header.tsx theme switch, globals.css .light remap, version.ts 6.3,
help-panel/script-tab patches).
- agent-browser desktop session 1440×900 at http://localhost:3000: cleared
localStorage, fresh load, dark-default + theme-pill geometry checks,
toggle to light, full tab walk (Presets/Script/Detect/Beat/Native +
Help panel), embedded preset select + Copy-to-my-presets panel, Create
Preset dialog in light AND dark, "Theme Test" create/delete toasts +
cleanup, persistence reloads (light→reload, dark→reload), rapid 5×
toggling, demo-track run for live VU meters + visualization, mobile
390×844 light + dark (scrollWidth, stacking, footer flush, pill
tappable), console + page-error checks after every phase.
- ENVIRONMENT INCIDENT (diagnosed + documented, no source edits): for the
first ~12 min of the session the dev server served a STALE CSS chunk
([root-of-the-server]__0f0ba101._.css, emitted 07:50:04, 158,839→
158,921 B) that contained the v6.3 `color-scheme: dark` addition but
NOT the `.light { --color-* }` block — so toggling set html.light +
checked state but the page stayed visually dark. Control test: the
current src/app/globals.css compiled through the project's own
@tailwindcss/postcss 4.1.18 (+ lightningcss 1.30.2) DOES emit .light —
source correct, server module graph stale (final 07:48:50 save never
propagated; same delayed-HMR class as Task 15-11 issue 1). A single
content-preserving `touch` of globals.css (md5 unchanged before/after:
2aa8062c…) triggered Turbopack's delayed rebuild at 08:04:59 → chunk
158,941 B now contains .light. All light-theme verdicts below were
re-verified against the real served CSS after that point (earlier
interim checks used a byte-identical injected copy of the .light block,
same results).
- VLM (z-ai vision) cross-checks on 13 screenshots (light full page,
presets/script/help, dark default/toggled/script, dialog, toast, mobile
light/dark, stale-state evidence, live visualization).
- Screenshots: tool-results/e2e-v63/ (37 files, 01…34).
- Final state: browser closed; localStorage 'theme' key removed (fresh
dark default), ove.userPresets.v1 = {"v":1,"folders":[],"presets":[]};
server library untouched; scratch test dir removed; source files
unmodified (git status = builder's v6.3 changes only).
Results:
- 1: PASS — fresh load (no 'theme' key): html.dark, body bg zinc-950
(#09090b), text zinc-100 (#f4f4f5), footer "version 6.3 | engine ok",
title "OVE — Open Visualization Engine (v6.3)".
- 2: PASS — pill Moon(x1135)|Switch(x1155,w32,unchecked,aria-label
"Switch to light theme")|Sun(x1193); BPM number box starts x1230 —
pill immediately LEFT of the emerald BPM readout, 6 px gaps, no
overlap/shift (VLM-confirmed).
- 3: PASS — dark regression clean: tree 95 presets/7 folders, lock badges
on embedded folders, script editor #09090b bg + light mono text, Set
Beat bg #34d399 + emerald-950 text (7.8:1), no white boxes (VLM:
"no regressions").
- 4: PASS — click flips switch right (aria-checked=true, knob translates,
track #059669 + white knob in light) and the ENTIRE chrome flips white:
body #ffffff/#18181b, wrapper zinc-950→white, panels/borders light
(before/after: 01→17/18/19). See env incident above for the stale-CSS
window at session start.
- 5: PASS — light readability across ALL tabs: header (logo, source
buttons, demo slider, BPM readout deep green #059669, Set Beat
#059669/#022c22, pill); Presets (rows #27272a on white, ≈ badges
rose-400 #e11d48 on /10 tint — 60 in Amp Picks, counts 95/66/9/15/5/0,
note #71717a, Load Preset #059669 bg, Copy white/#18181b/#d4d4d8);
Script (editor white bg, #27272a mono, gutter #8b8b93, status
"✔ Compiled OK" #059669); Detect (labels #52525b/#71717a, live VU bars
#059669/#e11d48/#b45309 on light tracks with demo running); Beat
(SET BEAT #059669/#022c22, labels readable); Native (headings #27272a,
body #52525b, .ava token deep green); Help (code blocks #e4e4e7/40 bg +
#27272a text — v6.3 patch works); footer readable + "version 6.3";
VisualStage canvas stays BLACK (rgb(0,0,0)) with light border #e4e4e7.
VLM: no unreadable/low-contrast text, no dark leftovers.
- 6: PASS — Create Preset dialog light: card #ffffff, heading/input text
#18181b, input bg #fafafa; same dialog dark: card #0a0a0a, input
white/4.5% bg + near-white text.
- 7: PASS (with notes) — no white-on-white/black-on-black anywhere; body
text ≥4.6:1; accents clearly readable. Notes: emerald buttons ≈4.0:1,
help-panel emerald-300/90 tokens ≈2.5:1 (see Issues 2-3).
- 8: PASS — light toast "Preset created — "Theme Test" created in this
browser" (white card via --popover, #18181b text); delete confirm +
"Preset deleted" toast same styling; localStorage cleaned afterwards.
- 9: PASS — light + reload: html.light, switch checked, theme=light,
page renders white (with live CSS); toggle dark + reload: html.dark,
unchecked, theme=dark, page dark.
- 10: PASS — 5 rapid clicks: end state light, html class/theme key/
switch in sync at every step, zero errors, no stuck states.
- 11: PASS — console + page errors zero across the whole session (both
themes, ~10 loads, dialogs, toasts, toggling, mobile): only React
DevTools info, "[HMR] connected", "[Fast Refresh] rebuilding/done".
- 12: PASS — mobile 390×844 light: scrollWidth 390 = clientWidth 390
(no horizontal scroll), theme pill visible + tappable (wrapped into
header row 1, no overlap), tree + panel stack (358 px), footer flush at
document bottom (footerBottom 1313 = docHeight 1313; below fold on
first paint — same as accepted in Task 15-11).
- 13: PASS — mobile dark: same checks pass (no h-scroll, pill visible,
bg #09090b/text #f4f4f5).
- 14: PASS — dev.log session window (lines 529-722): only GET 200s +
prisma:query; zero error/warn/exception lines. (Pre-session lines
443-510 contain builder-era transient errors — see Issue 5.)
Issues found:
- 1 (environmental, resolved mid-session, NO app bug): dev server served
a stale Turbopack CSS chunk missing the entire .light palette remap for
~12 min (07:52-08:05) — theme toggle flipped state (html.light) but the
page stayed dark, and the switch track rendered white-on-dark. Root
cause: the final 07:48:50 globals.css save never propagated through
HMR (delayed/broken watcher state; 4 runtime-error full reloads from
the builder's mid-edit saves at 07:44-07:50 left the graph stale).
Content-preserving touch + delayed rebuild fixed it at 08:04:59.
Control compile of the current source through the project's own
toolchain emits .light correctly. Recommendation: RESTART the dev
server (or at least verify the served chunk contains .light) before
the next session and before any VPS deploy of v6.3 — a stale build
would ship a theme switch that visually does nothing.
- 2 (minor, light-mode contrast): help-panel inline code tokens
(text-emerald-300/90 — e.g. [init], [frame], variable names in the
reference tables) resolve to #10b981 on white ≈ 2.5:1 — below WCAG AA
4.5:1 and even below 3:1; readable but washed-out. Consider deepening
the emerald-300 remap (e.g. to #059669) or switching these tokens to
emerald-400 in light mode.
- 3 (minor, light-mode contrast): emerald action buttons (header Set
Beat, Load Preset, Beat-tab SET BEAT) are text-emerald-950 #022c22 on
bg-emerald-400 #059669 ≈ 4.0:1 — just under AA 4.5:1 for small text
(passes 3:1 large-text; dark-mode equivalent is 7.8:1). Borderline,
likely acceptable for bold button labels.
- 4 (observation): script editor line-number gutter (text-zinc-600 →
#8b8b93 on white) ≈ 3.4:1 — auxiliary text, acceptable but dimmer
than body text.
- 5 (observation, pre-existing): dev.log lines 443-510 (before this
session) contain 4× "⚠ Fast Refresh had to perform a full reload due
to a runtime error" plus transient "⨯ ReferenceError: useRef is not
defined at SourceControls (header.tsx:63)" GET / 500s — artifacts of
the v6.3 builder's mid-edit saves. Current source imports useRef
correctly; nothing recurred in this session.
- 6 (observation): on mobile 390 px the theme pill wraps into header row
1 (next to the logo) rather than sitting beside the BPM readout as on
desktop — clean flex-wrap, no overlap; consistent with the header's
responsive design.
- 7 (note): "Set Beat button … very dark green text" expectation —
confirmed #022c22 (emerald-950) on #059669; and the checklist's "help
headings readable green": Help section headings are zinc-400 gray by
design (unchanged from dark theme); the GREEN elements in Help are the
inline code tokens (see Issue 2).
---
Task ID: 16
Agent: main (Z.ai Code)
Task: v6.3 — light/dark theme tick switch in the header (user request: switch
top-right, left of the BPM indicator; flip right = white theme).
Work Log:
- src/app/globals.css — light theme via palette REMAP under `.light` (the app
is styled with raw zinc/emerald/rose/amber utilities, so remapping those CSS
variables flips the whole chrome): zinc ramp inverted (950→#ffffff page,
100→#18181b text, 900→#fafafa panels, 800→#e4e4e7 hover/borders, 700→#d4d4d8
borders, 600→#8b8b93 hints); accents deepened one step for white
backgrounds (emerald-400→#059669, emerald-300→#10b981 so hovers still
lighten; rose-300/400, amber-300/400 likewise). color-scheme light/dark set
on .light/.dark. Unlayered class rules beat Tailwind's @layer theme, so the
remap wins with opacity variants too (color-mix keeps var references).
- src/app/layout.tsx — ThemeProvider (next-themes, attribute="class",
defaultTheme="dark", enableSystem=false, disableTransitionOnChange);
hardcoded <html class="dark"> removed (next-themes sets it pre-paint).
- src/components/ava/header.tsx — ThemeToggle pill (Moon | Switch | Sun,
h-11 matching the other header controls) in the right cluster as the first
child — immediately LEFT of the BPM readout. Left/unchecked = dark (default),
flipped right/checked = white theme. Mount guard via useSyncExternalStore
(lint-clean replacement for the setState-in-effect pattern); switch track:
zinc-600 unchecked / emerald-400 checked, default thumb colors verified for
all four theme×state combos.
- Contrast patches: text-zinc-950 → text-emerald-950 on all emerald CTAs (12
spots, 7 files — near-black green on emerald in both themes); help-panel
code blocks bg-black/40 → bg-zinc-800/40; help-panel inline tokens
text-emerald-300/90 → text-emerald-400/90 (light: #059669/90 ≈ 4.3:1,
was 2.5:1); native-tab blockquote text-emerald-200/80 →
text-emerald-700/90 dark:text-emerald-200/80.
- The visualization canvas intentionally KEEPS its black stage in light mode
(the "screen"); only the chrome flips.
- src/lib/version.ts → 6.3 (+history line). Fixed a mid-edit useRef import
regression (500s during editing, resolved).
- Dev-server ops: killed my pkill'd instance, restarted with double-fork
detach (( setsid bun run dev & )) — survives across tool sessions; verified
the served CSS chunk now contains the .light palette (was a stale Turbopack
chunk for ~12 min during verification — subagent diagnosed; restart fixed).
- VERIFIED (subagent E2E 16-d, 14/14 PASS + my own post-restart smoke test):
dark default (html.dark, no theme key); toggle left of BPM (switch right
1187 < BPM left 1230), flips to html.light with white body/dark text; ALL
tabs readable in light (header, presets tree + locks + ≈ badges, white
script editor, detect VU bars, beat, native, help code blocks, footer);
dialogs + toasts themed correctly; Set Beat #059669 bg + #022c22 text;
VisualStage stays black with light border; persistence across reload both
ways; rapid 5× toggling stable; zero console/page errors; mobile 390×844
both themes no horizontal scroll, footer flush at document bottom.
- Committed as "v6.3 — light/dark theme tick switch in the header".
Stage Summary:
- Theme = per-visitor preference (localStorage via next-themes), default
dark — matches the club aesthetic; light mode is a clean white studio look
with deepened emerald/rose/amber accents for contrast.
- 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.