feat: Maestro cycling note animation - quarter, eighth, beamed eighths, sixteenth
The note icon now cycles through four musical note variants during loading: quarter note → eighth note → beamed eighths → sixteenth note. Each variant bounces on staff lines while cycling every 800ms. Idle state shows a static eighth note. No more generic spinner.
This commit is contained in:
1 parent
98cc6b031c
commit
55f4a98b38
1 file changed
+51
-18
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react'
|
import { useState, useEffect, useRef } from 'react'
|
||||||
import type { PromptStructure } from './StructuredPromptEditor'
|
import type { PromptStructure } from './StructuredPromptEditor'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -19,26 +19,39 @@ interface Props {
|
|||||||
onStructuredResult?: (fields: PromptStructure, mode: 'generate' | 'complete') => void
|
onStructuredResult?: (fields: PromptStructure, mode: 'generate' | 'complete') => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function NoteIcon({ bouncing }: { bouncing: boolean }) {
|
// ── Note Variants ─────────────────────────────────────────────────────
|
||||||
return (
|
|
||||||
<svg
|
const NOTES = [
|
||||||
width="14"
|
// Quarter note (♩)
|
||||||
height="14"
|
'M5.5 13 C4 13 3 12 3 11.5 C3 11 4 10.5 5.5 10.5 C7 10.5 8 11 8 11.5 C8 12 7 13 5.5 13 Z M7.8 11 L7.8 4',
|
||||||
viewBox="0 0 18 18"
|
// Eighth note (♪)
|
||||||
className={bouncing ? 'animate-note-bounce' : ''}
|
'M5.5 13 C4 13 3 12 3 11.5 C3 11 4 10.5 5.5 10.5 C7 10.5 8 11 8 11.5 C8 12 7 13 5.5 13 Z M7.8 11 L7.8 4 C10 5.5 9.5 7.5 7.8 8',
|
||||||
aria-hidden="true"
|
// Beamed eighths (♫)
|
||||||
>
|
'M3 13 C1.5 13 0.5 12 0.5 11.5 C0.5 11 1.5 10.5 3 10.5 C4.5 10.5 5.5 11 5.5 11.5 C5.5 12 4.5 13 3 13 Z M5.3 11 L5.3 4 M10 12.5 C8.5 12.5 7.5 11.5 7.5 11 C7.5 10.5 8.5 10 10 10 C11.5 10 12.5 10.5 12.5 11 C12.5 11.5 11.5 12.5 10 12.5 Z M12.3 11 L12.3 4 M5.3 4 L12.3 4',
|
||||||
{/* Staff lines */}
|
// Sixteenth note (♬)
|
||||||
|
'M5.5 13 C4 13 3 12 3 11.5 C3 11 4 10.5 5.5 10.5 C7 10.5 8 11 8 11.5 C8 12 7 13 5.5 13 Z M7.8 11 L7.8 4 M7.8 7 C10 6 9.5 5.5 7.8 5 M7.8 9 C10 8 9.5 7.5 7.8 7',
|
||||||
|
]
|
||||||
|
|
||||||
|
const STAFF_LINES = (
|
||||||
|
<>
|
||||||
<line x1="2" y1="5" x2="16" y2="5" stroke="currentColor" strokeWidth="0.7" opacity="0.4" />
|
<line x1="2" y1="5" x2="16" y2="5" stroke="currentColor" strokeWidth="0.7" opacity="0.4" />
|
||||||
<line x1="2" y1="8" x2="16" y2="8" stroke="currentColor" strokeWidth="0.7" opacity="0.4" />
|
<line x1="2" y1="8" x2="16" y2="8" stroke="currentColor" strokeWidth="0.7" opacity="0.4" />
|
||||||
<line x1="2" y1="11" x2="16" y2="11" stroke="currentColor" strokeWidth="0.7" opacity="0.4" />
|
<line x1="2" y1="11" x2="16" y2="11" stroke="currentColor" strokeWidth="0.7" opacity="0.4" />
|
||||||
<line x1="2" y1="14" x2="16" y2="14" stroke="currentColor" strokeWidth="0.7" opacity="0.4" />
|
<line x1="2" y1="14" x2="16" y2="14" stroke="currentColor" strokeWidth="0.7" opacity="0.4" />
|
||||||
{/* Note head */}
|
</>
|
||||||
<ellipse cx="5.5" cy="13" rx="2.5" ry="2" fill="currentColor" transform="rotate(-15 5.5 13)" />
|
)
|
||||||
{/* Stem */}
|
|
||||||
<line x1="7.8" y1="12.5" x2="7.8" y2="4" stroke="currentColor" strokeWidth="1.2" />
|
function NoteIcon({ noteIndex, bouncing }: { noteIndex: number; bouncing: boolean }) {
|
||||||
{/* Flag */}
|
return (
|
||||||
<path d="M7.8 4 C10 5.5, 9.5 7.5, 7.8 8" fill="none" stroke="currentColor" strokeWidth="1" />
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
className={bouncing ? 'animate-note-bounce' : ''}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{STAFF_LINES}
|
||||||
|
<path d={NOTES[noteIndex]} fill="none" stroke="currentColor" strokeWidth="0.8" />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -52,6 +65,26 @@ export default function MaestroButton({
|
|||||||
onStructuredResult,
|
onStructuredResult,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [isMaestroing, setIsMaestroing] = useState(false)
|
const [isMaestroing, setIsMaestroing] = useState(false)
|
||||||
|
const [noteIndex, setNoteIndex] = useState(1) // Start on eighth note (♪)
|
||||||
|
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||||
|
|
||||||
|
// Cycle through note variants while loading
|
||||||
|
useEffect(() => {
|
||||||
|
if (isMaestroing) {
|
||||||
|
intervalRef.current = setInterval(() => {
|
||||||
|
setNoteIndex(prev => (prev + 1) % NOTES.length)
|
||||||
|
}, 800) // Change note every 800ms (slightly slower than bounce)
|
||||||
|
} else {
|
||||||
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current)
|
||||||
|
intervalRef.current = null
|
||||||
|
}
|
||||||
|
setNoteIndex(1) // Reset to eighth note when idle
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (intervalRef.current) clearInterval(intervalRef.current)
|
||||||
|
}
|
||||||
|
}, [isMaestroing])
|
||||||
|
|
||||||
const handleMaestro = async () => {
|
const handleMaestro = async () => {
|
||||||
setIsMaestroing(true)
|
setIsMaestroing(true)
|
||||||
@@ -109,7 +142,7 @@ export default function MaestroButton({
|
|||||||
: "Let Maestro complete your prompt with musical details"
|
: "Let Maestro complete your prompt with musical details"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<NoteIcon bouncing={isMaestroing} />
|
<NoteIcon noteIndex={noteIndex} bouncing={isMaestroing} />
|
||||||
<span className="truncate">
|
<span className="truncate">
|
||||||
{isMaestroing
|
{isMaestroing
|
||||||
? (mode === 'generate' ? 'Dreaming...' : 'Composing...')
|
? (mode === 'generate' ? 'Dreaming...' : 'Composing...')
|
||||||
|
|||||||
Reference in new issue
Block a user