diff --git a/src/components/Sidebar/Sidebar.css b/src/components/Sidebar/Sidebar.css index beb3502..7b49869 100644 --- a/src/components/Sidebar/Sidebar.css +++ b/src/components/Sidebar/Sidebar.css @@ -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; +} diff --git a/src/components/Sidebar/Sidebar.tsx b/src/components/Sidebar/Sidebar.tsx index 35792df..295fed8 100644 --- a/src/components/Sidebar/Sidebar.tsx +++ b/src/components/Sidebar/Sidebar.tsx @@ -6,6 +6,7 @@ export type SidebarItem = { id: T; label: string; icon: React.ReactNode; + editable?: boolean; }; export type SidebarAppOption = { @@ -17,11 +18,75 @@ function SidebarButton({ item, active, onSelect, + onRename, }: { item: SidebarItem; active: boolean; onSelect: () => void; + onRename?: (name: string) => void; }) { + const [editing, setEditing] = useState(false); + const [inputValue, setInputValue] = useState(item.label); + const inputRef = useRef(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 ( +
+ {item.icon} + + setInputValue(e.target.value.slice(0, 8))} + onBlur={commitRename} + onKeyDown={handleKeyDown} + aria-label="Rename channel" + /> +
+ ); + } + return ( ); } @@ -230,6 +302,7 @@ export function Sidebar({ selectedAppId, onAppChange, onAppRename, + onItemRename, expanded, onToggle, }: { @@ -241,6 +314,7 @@ export function Sidebar({ 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({ item={item} active={activeId === item.id} onSelect={() => onSelect(item.id)} + onRename={ + item.editable && onItemRename ? (name) => onItemRename(item.id, name) : undefined + } /> ))} diff --git a/src/features/dsp408/components/DSP408Panel/DSP408Panel.tsx b/src/features/dsp408/components/DSP408Panel/DSP408Panel.tsx index 5f0948b..c2c7885 100644 --- a/src/features/dsp408/components/DSP408Panel/DSP408Panel.tsx +++ b/src/features/dsp408/components/DSP408Panel/DSP408Panel.tsx @@ -47,66 +47,18 @@ function DSP408Panel({ dsp, api }: { dsp: DSP408; api: DSP408Api }) { { id: 'delay', label: 'Delay', icon: }, { id: 'matrix', label: 'Matrix', icon: }, { id: 'geq', label: 'Graphic Equalizer', icon: }, - { - id: 'ina', - label: inputStates[InputChannel.InA].name, - icon: In A, - }, - { - id: 'inb', - label: inputStates[InputChannel.InB].name, - icon: In B, - }, - { - id: 'inc', - label: inputStates[InputChannel.InC].name, - icon: In C, - }, - { - id: 'ind', - label: inputStates[InputChannel.InD].name, - icon: In D, - }, - { - id: 'out1', - label: outputStates[OutputChannel.Out1].name, - icon: Out 1, - }, - { - id: 'out2', - label: outputStates[OutputChannel.Out2].name, - icon: Out 2, - }, - { - id: 'out3', - label: outputStates[OutputChannel.Out3].name, - icon: Out 3, - }, - { - id: 'out4', - label: outputStates[OutputChannel.Out4].name, - icon: Out 4, - }, - { - id: 'out5', - label: outputStates[OutputChannel.Out5].name, - icon: Out 5, - }, - { - id: 'out6', - label: outputStates[OutputChannel.Out6].name, - icon: Out 6, - }, - { - id: 'out7', - label: outputStates[OutputChannel.Out7].name, - icon: Out 7, - }, - { - id: 'out8', - label: outputStates[OutputChannel.Out8].name, - icon: Out 8, - }, + { id: 'ina', label: inputStates[InputChannel.InA].name, icon: In A, editable: true }, + { id: 'inb', label: inputStates[InputChannel.InB].name, icon: In B, editable: true }, + { id: 'inc', label: inputStates[InputChannel.InC].name, icon: In C, editable: true }, + { id: 'ind', label: inputStates[InputChannel.InD].name, icon: In D, editable: true }, + { id: 'out1', label: outputStates[OutputChannel.Out1].name, icon: Out 1, editable: true }, + { id: 'out2', label: outputStates[OutputChannel.Out2].name, icon: Out 2, editable: true }, + { id: 'out3', label: outputStates[OutputChannel.Out3].name, icon: Out 3, editable: true }, + { id: 'out4', label: outputStates[OutputChannel.Out4].name, icon: Out 4, editable: true }, + { id: 'out5', label: outputStates[OutputChannel.Out5].name, icon: Out 5, editable: true }, + { id: 'out6', label: outputStates[OutputChannel.Out6].name, icon: Out 6, editable: true }, + { id: 'out7', label: outputStates[OutputChannel.Out7].name, icon: Out 7, editable: true }, + { id: 'out8', label: outputStates[OutputChannel.Out8].name, icon: Out 8, editable: true }, { id: 'miscellaneous', label: 'Miscellaneous', icon: }, ]; @@ -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 }) => ( setSidebarExpanded((v) => !v)} />