@@ -34,6 +34,160 @@ export interface AudioTrackOption {
3434 label : string ;
3535}
3636
37+ interface FxNodeRowProps {
38+ node : HfAudioFxNode ;
39+ index : number ;
40+ open : boolean ;
41+ /** Last in the chain, so it cannot move further down. */
42+ last : boolean ;
43+ disabled ?: boolean ;
44+ onToggleOpen ( ) : void ;
45+ onUpdate ( index : number , patch : Partial < HfAudioFxNode > ) : void ;
46+ onMove ( index : number , delta : number ) : void ;
47+ onRemove ( index : number ) : void ;
48+ onPreview ( index : number , params : HfAudioFxParamValues ) : void ;
49+ }
50+
51+ /** Reorder arrow. Disabled at the end of the chain it would move past. */
52+ function FxMoveButton ( {
53+ label,
54+ glyph,
55+ disabled,
56+ onClick,
57+ } : {
58+ label : string ;
59+ glyph : string ;
60+ disabled : boolean ;
61+ onClick ( ) : void ;
62+ } ) {
63+ return (
64+ < button
65+ type = "button"
66+ className = "hf-fx-move px-1 font-mono text-[10px] text-panel-text-4 hover:text-panel-text-0 disabled:opacity-25"
67+ title = { label }
68+ disabled = { disabled }
69+ onClick = { onClick }
70+ >
71+ { glyph }
72+ </ button >
73+ ) ;
74+ }
75+
76+ /** Name, bypass, reorder and remove for one effect. */
77+ function FxNodeHeader ( {
78+ label,
79+ open,
80+ bypassed,
81+ first,
82+ last,
83+ disabled,
84+ onToggleOpen,
85+ onToggleBypass,
86+ onMove,
87+ onRemove,
88+ } : {
89+ label : string ;
90+ open : boolean ;
91+ bypassed : boolean ;
92+ first : boolean ;
93+ last : boolean ;
94+ disabled ?: boolean ;
95+ onToggleOpen ( ) : void ;
96+ onToggleBypass ( ) : void ;
97+ onMove ( delta : number ) : void ;
98+ onRemove ( ) : void ;
99+ } ) {
100+ return (
101+ < div className = "hf-fx-node-head flex min-h-7 items-center gap-1 px-1.5" >
102+ < button
103+ type = "button"
104+ className = "hf-fx-node-name flex-1 truncate text-left text-[11px] font-semibold text-panel-text-1 hover:text-panel-text-0"
105+ aria-expanded = { open }
106+ onClick = { onToggleOpen }
107+ >
108+ { label }
109+ </ button >
110+ < button
111+ type = "button"
112+ className = "hf-fx-bypass rounded-[3px] border border-panel-border-input px-1.5 py-0.5 font-mono text-[9px] text-panel-text-4 hover:text-panel-text-0 disabled:opacity-40"
113+ aria-pressed = { bypassed }
114+ title = { bypassed ? "Enable" : "Bypass" }
115+ disabled = { disabled }
116+ onClick = { onToggleBypass }
117+ >
118+ { bypassed ? "Off" : "On" }
119+ </ button >
120+ < FxMoveButton
121+ label = "Move up"
122+ glyph = "↑"
123+ disabled = { Boolean ( disabled ) || first }
124+ onClick = { ( ) => onMove ( - 1 ) }
125+ />
126+ < FxMoveButton
127+ label = "Move down"
128+ glyph = "↓"
129+ disabled = { Boolean ( disabled ) || last }
130+ onClick = { ( ) => onMove ( 1 ) }
131+ />
132+ < button
133+ type = "button"
134+ className = "hf-fx-remove px-1 font-mono text-[11px] text-panel-text-4 hover:text-red-400 disabled:opacity-40"
135+ title = "Remove"
136+ disabled = { disabled }
137+ onClick = { onRemove }
138+ >
139+ ×
140+ </ button >
141+ </ div >
142+ ) ;
143+ }
144+
145+ /** One effect in the chain: its header controls, and its knobs when open. */
146+ function FxNodeRow ( {
147+ node,
148+ index,
149+ open,
150+ last,
151+ disabled,
152+ onToggleOpen,
153+ onUpdate,
154+ onMove,
155+ onRemove,
156+ onPreview,
157+ } : FxNodeRowProps ) {
158+ const def = getAudioFxDef ( node . type ) ;
159+ if ( ! def ) return null ;
160+ const bypassed = node . enabled === false ;
161+ return (
162+ < div
163+ className = { `hf-fx-node rounded-[4px] border border-panel-border-input${ bypassed ? " opacity-50" : "" } ` }
164+ data-fx-node = { node . type }
165+ >
166+ < FxNodeHeader
167+ label = { def . label }
168+ open = { open }
169+ bypassed = { bypassed }
170+ first = { index === 0 }
171+ last = { last }
172+ disabled = { disabled }
173+ onToggleOpen = { onToggleOpen }
174+ onToggleBypass = { ( ) => onUpdate ( index , { enabled : bypassed } ) }
175+ onMove = { ( delta ) => onMove ( index , delta ) }
176+ onRemove = { ( ) => onRemove ( index ) }
177+ />
178+ { open ? (
179+ < FxParams
180+ def = { def }
181+ params = { node . params ?? defaultAudioFxParams ( node . type ) }
182+ disabled = { disabled || bypassed }
183+ onChange = { ( params : HfAudioFxParamValues ) => onPreview ( index , params ) }
184+ onCommit = { ( params : HfAudioFxParamValues ) => onUpdate ( index , { params } ) }
185+ />
186+ ) : null }
187+ </ div >
188+ ) ;
189+ }
190+
37191export interface FxSectionProps {
38192 chain : HfAudioFxChain ;
39193 /** Structural edits and gesture-end writes; this is the one that persists. */
@@ -128,76 +282,21 @@ export function FxSection({
128282 No effects on this track.
129283 </ p >
130284 ) : (
131- chain . nodes . map ( ( node , i ) => {
132- const def = getAudioFxDef ( node . type ) ;
133- if ( ! def ) return null ;
134- const bypassed = node . enabled === false ;
135- const open = openNode === i ;
136- return (
137- < div
138- key = { `${ node . type } -${ i } ` }
139- className = { `hf-fx-node rounded-[4px] border border-panel-border-input${ bypassed ? " opacity-50" : "" } ` }
140- data-fx-node = { node . type }
141- >
142- < div className = "hf-fx-node-head flex min-h-7 items-center gap-1 px-1.5" >
143- < button
144- type = "button"
145- className = "hf-fx-node-name flex-1 truncate text-left text-[11px] font-semibold text-panel-text-1 hover:text-panel-text-0"
146- aria-expanded = { open }
147- onClick = { ( ) => setOpenNode ( open ? null : i ) }
148- >
149- { def . label }
150- </ button >
151- < button
152- type = "button"
153- className = "hf-fx-bypass rounded-[3px] border border-panel-border-input px-1.5 py-0.5 font-mono text-[9px] text-panel-text-4 hover:text-panel-text-0 disabled:opacity-40"
154- aria-pressed = { bypassed }
155- title = { bypassed ? "Enable" : "Bypass" }
156- disabled = { disabled }
157- onClick = { ( ) => updateNode ( i , { enabled : bypassed } ) }
158- >
159- { bypassed ? "Off" : "On" }
160- </ button >
161- < button
162- type = "button"
163- className = "hf-fx-move px-1 font-mono text-[10px] text-panel-text-4 hover:text-panel-text-0 disabled:opacity-25"
164- title = "Move up"
165- disabled = { disabled || i === 0 }
166- onClick = { ( ) => moveNode ( i , - 1 ) }
167- >
168- ↑
169- </ button >
170- < button
171- type = "button"
172- className = "hf-fx-move px-1 font-mono text-[10px] text-panel-text-4 hover:text-panel-text-0 disabled:opacity-25"
173- title = "Move down"
174- disabled = { disabled || i === chain . nodes . length - 1 }
175- onClick = { ( ) => moveNode ( i , 1 ) }
176- >
177- ↓
178- </ button >
179- < button
180- type = "button"
181- className = "hf-fx-remove px-1 font-mono text-[11px] text-panel-text-4 hover:text-red-400 disabled:opacity-40"
182- title = "Remove"
183- disabled = { disabled }
184- onClick = { ( ) => removeNode ( i ) }
185- >
186- ×
187- </ button >
188- </ div >
189- { open ? (
190- < FxParams
191- def = { def }
192- params = { node . params ?? defaultAudioFxParams ( node . type ) }
193- disabled = { disabled || bypassed }
194- onChange = { ( params : HfAudioFxParamValues ) => previewNode ( i , params ) }
195- onCommit = { ( params : HfAudioFxParamValues ) => updateNode ( i , { params } ) }
196- />
197- ) : null }
198- </ div >
199- ) ;
200- } )
285+ chain . nodes . map ( ( node , i ) => (
286+ < FxNodeRow
287+ key = { `${ node . type } -${ i } ` }
288+ node = { node }
289+ index = { i }
290+ open = { openNode === i }
291+ last = { i === chain . nodes . length - 1 }
292+ disabled = { disabled }
293+ onToggleOpen = { ( ) => setOpenNode ( openNode === i ? null : i ) }
294+ onUpdate = { updateNode }
295+ onMove = { moveNode }
296+ onRemove = { removeNode }
297+ onPreview = { previewNode }
298+ />
299+ ) )
201300 ) }
202301 </ div >
203302
0 commit comments