feat: add rename channel

This commit is contained in:
Lucas
2026-09-25 15:10:19 +02:00
parent fd7dd89f55
commit 24830172ae
3 changed files with 150 additions and 61 deletions
+51
View File
@@ -365,3 +365,54 @@
.app-root--sidebar-expanded .stage--with-sidebar { .app-root--sidebar-expanded .stage--with-sidebar {
margin-left: var(--sidebar-width-expanded); margin-left: var(--sidebar-width-expanded);
} }
/* ---------- Item (editing state) ---------- */
.sidebar-item--editing {
display: flex;
align-items: center;
justify-content: center;
gap: var(--sidebar-item-gap);
aspect-ratio: 1;
height: calc(var(--header-height) * 0.7);
border: none;
background: transparent;
padding: 0;
}
.sidebar-item-input {
width: 0;
flex: 1 1 auto;
min-width: 0;
appearance: none;
-webkit-appearance: none;
background: transparent;
border: none;
outline: none;
box-shadow: none;
text-align: center;
color: var(--text-primary);
font-family: var(--font-ui);
font-size: var(--font-sm);
font-weight: var(--font-weight-semibold);
opacity: 0;
max-width: 0;
transition:
opacity var(--dur-fast) var(--ease-standard),
max-width var(--dur-base) var(--ease-standard);
}
.sidebar-rail--expanded .sidebar-item-input {
width: auto;
opacity: 1;
max-width: 200px;
border: none;
}
+78 -1
View File
@@ -6,6 +6,7 @@ export type SidebarItem<T extends string = string> = {
id: T; id: T;
label: string; label: string;
icon: React.ReactNode; icon: React.ReactNode;
editable?: boolean;
}; };
export type SidebarAppOption = { export type SidebarAppOption = {
@@ -17,11 +18,75 @@ function SidebarButton<T extends string>({
item, item,
active, active,
onSelect, onSelect,
onRename,
}: { }: {
item: SidebarItem<T>; item: SidebarItem<T>;
active: boolean; active: boolean;
onSelect: () => void; onSelect: () => void;
onRename?: (name: string) => void;
}) { }) {
const [editing, setEditing] = useState(false);
const [inputValue, setInputValue] = useState(item.label);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (!editing) setInputValue(item.label);
}, [item.label, editing]);
function startEditing(e: React.MouseEvent) {
e.stopPropagation();
if (!onRename) return;
setInputValue(item.label);
setEditing(true);
requestAnimationFrame(() => {
inputRef.current?.focus();
inputRef.current?.select();
});
}
function commitRename() {
setEditing(false);
const trimmed = inputValue.trim();
if (!trimmed || trimmed === item.label) {
setInputValue(item.label);
return;
}
onRename?.(trimmed);
}
function handleKeyDown(e: React.KeyboardEvent) {
if (e.key === 'Enter') inputRef.current?.blur();
if (e.key === 'Escape') {
setInputValue(item.label);
setEditing(false);
}
}
if (editing) {
return (
<div className="sidebar-item sidebar-item--editing">
<span className="sidebar-icon">{item.icon}</span>
<input
ref={inputRef}
className="sidebar-item-input"
value={inputValue}
maxLength={8}
onChange={(e) => setInputValue(e.target.value.slice(0, 8))}
onBlur={commitRename}
onKeyDown={handleKeyDown}
aria-label="Rename channel"
/>
</div>
);
}
return ( return (
<button <button
onClick={onSelect} onClick={onSelect}
@@ -31,7 +96,14 @@ function SidebarButton<T extends string>({
className={'sidebar-item' + (active ? ' sidebar-item--active' : '')} className={'sidebar-item' + (active ? ' sidebar-item--active' : '')}
> >
<span className="sidebar-icon">{item.icon}</span> <span className="sidebar-icon">{item.icon}</span>
<span className="sidebar-label">{item.label}</span>
<span
className="sidebar-label"
onDoubleClick={onRename ? startEditing : undefined}
title={onRename ? `${item.label} (double click to rename)` : undefined}
>
{item.label}
</span>
</button> </button>
); );
} }
@@ -230,6 +302,7 @@ export function Sidebar<T extends string>({
selectedAppId, selectedAppId,
onAppChange, onAppChange,
onAppRename, onAppRename,
onItemRename,
expanded, expanded,
onToggle, onToggle,
}: { }: {
@@ -241,6 +314,7 @@ export function Sidebar<T extends string>({
selectedAppId?: string; selectedAppId?: string;
onAppChange: (id: string) => void; onAppChange: (id: string) => void;
onAppRename: (name: string) => void; onAppRename: (name: string) => void;
onItemRename?: (id: T, name: string) => void;
expanded: boolean; expanded: boolean;
onToggle: () => void; onToggle: () => void;
}) { }) {
@@ -298,6 +372,9 @@ export function Sidebar<T extends string>({
item={item} item={item}
active={activeId === item.id} active={activeId === item.id}
onSelect={() => onSelect(item.id)} onSelect={() => onSelect(item.id)}
onRename={
item.editable && onItemRename ? (name) => onItemRename(item.id, name) : undefined
}
/> />
))} ))}
</div> </div>
@@ -47,66 +47,18 @@ function DSP408Panel({ dsp, api }: { dsp: DSP408; api: DSP408Api }) {
{ id: 'delay', label: 'Delay', icon: <DelayIcon /> }, { id: 'delay', label: 'Delay', icon: <DelayIcon /> },
{ id: 'matrix', label: 'Matrix', icon: <MatrixIcon /> }, { id: 'matrix', label: 'Matrix', icon: <MatrixIcon /> },
{ id: 'geq', label: 'Graphic Equalizer', icon: <SlidersVerticalIcon /> }, { id: 'geq', label: 'Graphic Equalizer', icon: <SlidersVerticalIcon /> },
{ { id: 'ina', label: inputStates[InputChannel.InA].name, icon: <TextIcon>In A</TextIcon>, editable: true },
id: 'ina', { id: 'inb', label: inputStates[InputChannel.InB].name, icon: <TextIcon>In B</TextIcon>, editable: true },
label: inputStates[InputChannel.InA].name, { id: 'inc', label: inputStates[InputChannel.InC].name, icon: <TextIcon>In C</TextIcon>, editable: true },
icon: <TextIcon>In A</TextIcon>, { id: 'ind', label: inputStates[InputChannel.InD].name, icon: <TextIcon>In D</TextIcon>, editable: true },
}, { id: 'out1', label: outputStates[OutputChannel.Out1].name, icon: <TextIcon>Out 1</TextIcon>, editable: true },
{ { id: 'out2', label: outputStates[OutputChannel.Out2].name, icon: <TextIcon>Out 2</TextIcon>, editable: true },
id: 'inb', { id: 'out3', label: outputStates[OutputChannel.Out3].name, icon: <TextIcon>Out 3</TextIcon>, editable: true },
label: inputStates[InputChannel.InB].name, { id: 'out4', label: outputStates[OutputChannel.Out4].name, icon: <TextIcon>Out 4</TextIcon>, editable: true },
icon: <TextIcon>In B</TextIcon>, { id: 'out5', label: outputStates[OutputChannel.Out5].name, icon: <TextIcon>Out 5</TextIcon>, editable: true },
}, { id: 'out6', label: outputStates[OutputChannel.Out6].name, icon: <TextIcon>Out 6</TextIcon>, editable: true },
{ { id: 'out7', label: outputStates[OutputChannel.Out7].name, icon: <TextIcon>Out 7</TextIcon>, editable: true },
id: 'inc', { id: 'out8', label: outputStates[OutputChannel.Out8].name, icon: <TextIcon>Out 8</TextIcon>, editable: true },
label: inputStates[InputChannel.InC].name,
icon: <TextIcon>In C</TextIcon>,
},
{
id: 'ind',
label: inputStates[InputChannel.InD].name,
icon: <TextIcon>In D</TextIcon>,
},
{
id: 'out1',
label: outputStates[OutputChannel.Out1].name,
icon: <TextIcon>Out 1</TextIcon>,
},
{
id: 'out2',
label: outputStates[OutputChannel.Out2].name,
icon: <TextIcon>Out 2</TextIcon>,
},
{
id: 'out3',
label: outputStates[OutputChannel.Out3].name,
icon: <TextIcon>Out 3</TextIcon>,
},
{
id: 'out4',
label: outputStates[OutputChannel.Out4].name,
icon: <TextIcon>Out 4</TextIcon>,
},
{
id: 'out5',
label: outputStates[OutputChannel.Out5].name,
icon: <TextIcon>Out 5</TextIcon>,
},
{
id: 'out6',
label: outputStates[OutputChannel.Out6].name,
icon: <TextIcon>Out 6</TextIcon>,
},
{
id: 'out7',
label: outputStates[OutputChannel.Out7].name,
icon: <TextIcon>Out 7</TextIcon>,
},
{
id: 'out8',
label: outputStates[OutputChannel.Out8].name,
icon: <TextIcon>Out 8</TextIcon>,
},
{ id: 'miscellaneous', label: 'Miscellaneous', icon: <BoltIcon /> }, { id: 'miscellaneous', label: 'Miscellaneous', icon: <BoltIcon /> },
]; ];
@@ -136,6 +88,14 @@ function DSP408Panel({ dsp, api }: { dsp: DSP408; api: DSP408Api }) {
setPresetLoading(false); setPresetLoading(false);
}; };
const onItemRename = async (id: SidebarId, name: string) => {
const channel = channelMap[id];
if (!channel) return;
const padded = name.slice(0, 8).padEnd(8, '\0');
await api.setChannelName(channel, padded);
};
const renderChannel = (channel: { Input: InputChannel } | { Output: OutputChannel }) => ( const renderChannel = (channel: { Input: InputChannel } | { Output: OutputChannel }) => (
<ChannelSection <ChannelSection
dsp={dsp} dsp={dsp}
@@ -236,6 +196,7 @@ function DSP408Panel({ dsp, api }: { dsp: DSP408; api: DSP408Api }) {
selectedAppId={presets[selectedPreset]?.id} selectedAppId={presets[selectedPreset]?.id}
onAppChange={onAppChange} onAppChange={onAppChange}
onAppRename={api.setCurrentPresetName} onAppRename={api.setCurrentPresetName}
onItemRename={onItemRename}
expanded={sidebarExpanded} expanded={sidebarExpanded}
onToggle={() => setSidebarExpanded((v) => !v)} onToggle={() => setSidebarExpanded((v) => !v)}
/> />