feat: add rename channel
This commit is contained in:
@@ -365,3 +365,54 @@
|
||||
.app-root--sidebar-expanded .stage--with-sidebar {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ export type SidebarItem<T extends string = string> = {
|
||||
id: T;
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
editable?: boolean;
|
||||
};
|
||||
|
||||
export type SidebarAppOption = {
|
||||
@@ -17,11 +18,75 @@ function SidebarButton<T extends string>({
|
||||
item,
|
||||
active,
|
||||
onSelect,
|
||||
onRename,
|
||||
}: {
|
||||
item: SidebarItem<T>;
|
||||
active: boolean;
|
||||
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 (
|
||||
<button
|
||||
onClick={onSelect}
|
||||
@@ -31,7 +96,14 @@ function SidebarButton<T extends string>({
|
||||
className={'sidebar-item' + (active ? ' sidebar-item--active' : '')}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -230,6 +302,7 @@ export function Sidebar<T extends string>({
|
||||
selectedAppId,
|
||||
onAppChange,
|
||||
onAppRename,
|
||||
onItemRename,
|
||||
expanded,
|
||||
onToggle,
|
||||
}: {
|
||||
@@ -241,6 +314,7 @@ export function Sidebar<T extends string>({
|
||||
selectedAppId?: string;
|
||||
onAppChange: (id: string) => void;
|
||||
onAppRename: (name: string) => void;
|
||||
onItemRename?: (id: T, name: string) => void;
|
||||
expanded: boolean;
|
||||
onToggle: () => void;
|
||||
}) {
|
||||
@@ -298,6 +372,9 @@ export function Sidebar<T extends string>({
|
||||
item={item}
|
||||
active={activeId === item.id}
|
||||
onSelect={() => onSelect(item.id)}
|
||||
onRename={
|
||||
item.editable && onItemRename ? (name) => onItemRename(item.id, name) : undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -47,66 +47,18 @@ function DSP408Panel({ dsp, api }: { dsp: DSP408; api: DSP408Api }) {
|
||||
{ id: 'delay', label: 'Delay', icon: <DelayIcon /> },
|
||||
{ id: 'matrix', label: 'Matrix', icon: <MatrixIcon /> },
|
||||
{ id: 'geq', label: 'Graphic Equalizer', icon: <SlidersVerticalIcon /> },
|
||||
{
|
||||
id: 'ina',
|
||||
label: inputStates[InputChannel.InA].name,
|
||||
icon: <TextIcon>In A</TextIcon>,
|
||||
},
|
||||
{
|
||||
id: 'inb',
|
||||
label: inputStates[InputChannel.InB].name,
|
||||
icon: <TextIcon>In B</TextIcon>,
|
||||
},
|
||||
{
|
||||
id: 'inc',
|
||||
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: 'ina', label: inputStates[InputChannel.InA].name, icon: <TextIcon>In A</TextIcon>, editable: true },
|
||||
{ id: 'inb', label: inputStates[InputChannel.InB].name, icon: <TextIcon>In B</TextIcon>, editable: true },
|
||||
{ id: 'inc', label: inputStates[InputChannel.InC].name, icon: <TextIcon>In C</TextIcon>, editable: true },
|
||||
{ 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: 'out3', label: outputStates[OutputChannel.Out3].name, icon: <TextIcon>Out 3</TextIcon>, editable: true },
|
||||
{ id: 'out4', label: outputStates[OutputChannel.Out4].name, icon: <TextIcon>Out 4</TextIcon>, editable: true },
|
||||
{ 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: 'out8', label: outputStates[OutputChannel.Out8].name, icon: <TextIcon>Out 8</TextIcon>, editable: true },
|
||||
{ id: 'miscellaneous', label: 'Miscellaneous', icon: <BoltIcon /> },
|
||||
];
|
||||
|
||||
@@ -136,6 +88,14 @@ function DSP408Panel({ dsp, api }: { dsp: DSP408; api: DSP408Api }) {
|
||||
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 }) => (
|
||||
<ChannelSection
|
||||
dsp={dsp}
|
||||
@@ -236,6 +196,7 @@ function DSP408Panel({ dsp, api }: { dsp: DSP408; api: DSP408Api }) {
|
||||
selectedAppId={presets[selectedPreset]?.id}
|
||||
onAppChange={onAppChange}
|
||||
onAppRename={api.setCurrentPresetName}
|
||||
onItemRename={onItemRename}
|
||||
expanded={sidebarExpanded}
|
||||
onToggle={() => setSidebarExpanded((v) => !v)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user