c0dev0id 06b2bab5d3 Redesign PaneCP as vertical focus-path menu; add score metadata to FO pane
PaneCP: replace horizontal breadcrumb with vertical list of short views
for each node in the focus path (type tag + label/identifier). Clicking
any item above current navigates up.

PaneFO: show score info fields (title, composer, source, encrypter) as
editable DL when at score level instead of 'Nothing selected'.
2026-06-23 18:45:13 +02:00

157 lines
6.2 KiB
JavaScript

import { h, ref } from 'vue';
import { ObjectExtended } from './ObjectExtended.js';
import { EnvelopeEditor } from './EnvelopeEditor.js';
import { LinkedInstrumentModal } from './LinkedInstrumentModal.js';
function scoreInfoFields(info) {
return [
{ key: 'title', value: info?.title ?? '', editable: true },
{ key: 'composer', value: info?.composer ?? '', editable: true },
{ key: 'source', value: info?.source ?? '', editable: true },
{ key: 'encrypter', value: info?.encrypter ?? '', editable: true },
];
}
function instrFields(instr) {
return [
{ key: 'name', value: instr.name, editable: false },
{ key: 'linked', value: instr.isLinked, editable: false, type: 'boolean' },
{ key: 'NOT_CHANGED_SINCE', value: instr.notChangedSince ?? '—', editable: false },
];
}
function variationFields(v) {
return [
{ key: 'depends_on', value: v.dependsOn ?? '—', editable: true },
];
}
export const PaneFO = {
props: ['store'],
setup(props) {
// Pending edit held while the linked-instrument modal is shown.
const pendingEdit = ref(null); // { instr, apply: fn }
function focused() {
const fp = props.store.focusPath;
return fp.length ? fp[fp.length - 1] : null;
}
// Returns a change handler that intercepts the first edit to a linked
// instrument and shows the embed-or-discard modal before applying it.
function makeChangeHandler(instr, apply) {
return () => {
if (instr.isLinked && !instr.isDirty) {
// Stash the apply callback and show the modal.
pendingEdit.value = { instr, apply };
} else {
apply();
instr.isDirty = true;
props.store.markDirty();
}
};
}
function embedInstrument(instr) {
instr.name = instr.name.split('/').pop();
instr.isLinked = false;
instr.isDirty = true;
pendingEdit.value.apply();
pendingEdit.value = null;
props.store.markDirty();
}
function discardEdit() {
pendingEdit.value = null;
}
return () => {
const node = focused();
const children = [];
if (!node || node.type === 'score') {
const model = props.store.scoreModel;
if (!model) return h('div', { class: 'se-fo-pane' }, 'No score loaded');
return h('div', { class: 'se-fo-pane' }, [
h('h4', { style: 'margin:0 0 0.5rem' }, 'Score'),
h(ObjectExtended, {
fields: scoreInfoFields(model.info),
onChange: ({ key, value }) => {
if (!model.info) model.info = {};
model.info[key] = value;
props.store.markDirty();
},
}),
]);
}
if (node.type === 'instrument') {
children.push(
h('h4', { style: 'margin:0 0 0.5rem' }, `Instrument: ${node.name}`),
h(ObjectExtended, { fields: instrFields(node), onChange: null }),
);
} else if (node.type === 'variation') {
// Find the ancestor instrument for linked-instrument gating.
const instr = props.store.scoreModel?.instruments.find(
i => i.variations?.includes(node) ||
i.variations?.some(v => v.subvariations?.includes(node))
) ?? null;
const onChange = instr
? makeChangeHandler(instr, () => { props.store.markDirty(); })
: () => props.store.markDirty();
children.push(
h('h4', { style: 'margin:0 0 0.5rem' }, 'Variation'),
h(ObjectExtended, { fields: variationFields(node), onChange: ({ key, value }) => {
if (key === 'depends_on') node.dependsOn = value;
onChange();
}}),
node.basicProperties
? h(EnvelopeEditor, { basicProperties: node.basicProperties, onChange })
: null,
);
} else if (node.type === 'label_spec') {
const instr = props.store.scoreModel?.instruments.find(
i => i.variations?.some(v =>
v.labelSpecs?.includes(node) ||
v.subvariations?.some(sv => sv.labelSpecs?.includes(node))
)
) ?? null;
const onChange = instr
? makeChangeHandler(instr, () => { props.store.markDirty(); })
: () => props.store.markDirty();
children.push(
h('h4', { style: 'margin:0 0 0.5rem' }, `Label: ${node.label}`),
node.basicProperties
? h(EnvelopeEditor, { basicProperties: node.basicProperties, onChange })
: null,
);
} else if (node.type === 'bar') {
children.push(
h('h4', { style: 'margin:0 0 0.5rem' }, `Bar: ${node.id}`),
h(ObjectExtended, { fields: [
{ key: 'id', value: node.id, editable: false },
], onChange: null }),
);
} else {
children.push(h('pre', { style: 'font-size:0.75rem;white-space:pre-wrap' },
JSON.stringify(node, null, 2)));
}
return h('div', { class: 'se-fo-pane' }, [
...children,
pendingEdit.value
? h(LinkedInstrumentModal, {
instrumentName: pendingEdit.value.instr.name,
onEmbed: () => embedInstrument(pendingEdit.value.instr),
onDiscard: discardEdit,
})
: null,
]);
};
},
};