PaneCP article label + PaneFO editable property rows

Article entries in the CP path list now show the article label
(letter/extension) instead of just "article", with a property count
in the meta column. In the FO pane, each property is rendered as an
editable row: key input, value input (coerced via ast-parser.coerce),
scope toggle, morphing default/overwrite label, and per-row remove
button. A "+ add property" button appends a blank defaults entry.
Every edit marks the score dirty.

`coerce` is now exported from ast-parser.js so the FO pane can apply
the same type-inference rules to user input as the parser does to
AST values.
This commit is contained in:
c0dev0id
2026-06-29 08:17:57 +02:00
parent b822e4d919
commit 5bec1a1d1e
4 changed files with 73 additions and 8 deletions

View File

@@ -34,6 +34,10 @@ function shortView(node) {
return { typeTag: 'motif', label: node.label, meta: node.isStatic ? [{ key: 'static', value: '✓' }] : [] };
case 'stem_note':
return { typeTag: 'stem_note', label: String(node.pitch), meta: [] };
case 'article': {
const n = node.properties?.length ?? 0;
return { typeTag: 'article', label: node.name, meta: n ? [{ key: 'props', value: n }] : [] };
}
default:
return { typeTag: node.type, label: node.type, meta: [] };
}

View File

@@ -4,6 +4,7 @@ import { EnvelopeEditor } from './EnvelopeEditor.js';
import { ShapeEditor } from './ShapeEditor.js';
import { LinkedInstrumentModal } from './LinkedInstrumentModal.js';
import { stressorToString } from '../exporter.js';
import { coerce } from '../ast-parser.js';
const H4 = { style: 'margin:0 0 0.5rem' };
@@ -155,24 +156,54 @@ export const PaneFO = {
);
} else if (node.type === 'article') {
const markDirty = () => props.store.markDirty();
const rowStyle = 'display:flex;gap:0.4rem;align-items:center;padding:0.2rem 0';
children.push(
h('h4', H4, `Article: ${node.name}`),
h('ul', { class: 'se-article-props', style: 'list-style:none;padding:0;margin:0' },
node.properties.map((p, idx) => {
h('ul', { class: 'se-article-props', style: 'list-style:none;padding:0;margin:0' }, [
...node.properties.map((p, idx) => {
const isOverwrite = p.scope === 'overwrites';
const flip = () => { p.scope = isOverwrite ? 'defaults' : 'overwrites'; markDirty(); };
return h('li', { key: idx, style: 'display:flex;gap:0.5rem;align-items:center;padding:0.25rem 0' }, [
h('span', { style: 'flex:1' }, `${p.name} = ${p.value}`),
const onKeyInput = (e) => { p.name = e.target.value; markDirty(); };
const onValInput = (e) => { p.value = coerce(e.target.value); markDirty(); };
const onRemove = () => { node.properties.splice(idx, 1); markDirty(); };
return h('li', { key: idx, style: rowStyle }, [
h('input', {
class: 'se-prop-key',
value: p.name,
style: 'flex:1;min-width:6em',
onInput: onKeyInput,
}),
h('span', null, '='),
h('input', {
class: 'se-prop-val',
value: String(p.value),
style: 'flex:1;min-width:6em',
onInput: onValInput,
}),
h('button', {
class: 'se-toggle',
role: 'switch',
'aria-checked': String(isOverwrite),
onClick: flip,
}),
h('span', { class: 'se-toggle-label active' }, isOverwrite ? 'overwrite' : 'default'),
h('span', { class: 'se-toggle-label active', style: 'min-width:4.5em' },
isOverwrite ? 'overwrite' : 'default'),
h('button', {
class: 'se-btn-remove',
title: 'Remove property',
onClick: onRemove,
}, '×'),
]);
})
),
}),
h('li', { key: '__add', style: rowStyle },
h('button', {
class: 'se-btn',
onClick: () => {
node.properties.push({ name: '', value: '', scope: 'defaults' });
markDirty();
},
}, '+ add property')),
]),
);
} else if (node.type === 'bar') {
const markBarDirty = () => { node.isDirty = true; props.store.markDirty(); };