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:
parent
b822e4d919
commit
5bec1a1d1e
@ -2,7 +2,7 @@ const LINE_RE = /^(\d{2}) (\S+(?:\.\S+)*) ?(.*)/;
|
|||||||
const DEBUG_RE = /^\d{2} # DEBUG/;
|
const DEBUG_RE = /^\d{2} # DEBUG/;
|
||||||
|
|
||||||
|
|
||||||
function coerce(s) {
|
export function coerce(s) {
|
||||||
if (s === 'True' || s === 'Y' || s === 'on' || s === 'true') return true;
|
if (s === 'True' || s === 'Y' || s === 'on' || s === 'true') return true;
|
||||||
if (s === 'False' || s === 'N' || s === 'off' || s === 'false') return false;
|
if (s === 'False' || s === 'N' || s === 'off' || s === 'false') return false;
|
||||||
if (s === '') return s;
|
if (s === '') return s;
|
||||||
|
|||||||
@ -34,6 +34,10 @@ function shortView(node) {
|
|||||||
return { typeTag: 'motif', label: node.label, meta: node.isStatic ? [{ key: 'static', value: '✓' }] : [] };
|
return { typeTag: 'motif', label: node.label, meta: node.isStatic ? [{ key: 'static', value: '✓' }] : [] };
|
||||||
case 'stem_note':
|
case 'stem_note':
|
||||||
return { typeTag: 'stem_note', label: String(node.pitch), meta: [] };
|
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:
|
default:
|
||||||
return { typeTag: node.type, label: node.type, meta: [] };
|
return { typeTag: node.type, label: node.type, meta: [] };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { EnvelopeEditor } from './EnvelopeEditor.js';
|
|||||||
import { ShapeEditor } from './ShapeEditor.js';
|
import { ShapeEditor } from './ShapeEditor.js';
|
||||||
import { LinkedInstrumentModal } from './LinkedInstrumentModal.js';
|
import { LinkedInstrumentModal } from './LinkedInstrumentModal.js';
|
||||||
import { stressorToString } from '../exporter.js';
|
import { stressorToString } from '../exporter.js';
|
||||||
|
import { coerce } from '../ast-parser.js';
|
||||||
|
|
||||||
const H4 = { style: 'margin:0 0 0.5rem' };
|
const H4 = { style: 'margin:0 0 0.5rem' };
|
||||||
|
|
||||||
@ -155,24 +156,54 @@ export const PaneFO = {
|
|||||||
);
|
);
|
||||||
} else if (node.type === 'article') {
|
} else if (node.type === 'article') {
|
||||||
const markDirty = () => props.store.markDirty();
|
const markDirty = () => props.store.markDirty();
|
||||||
|
const rowStyle = 'display:flex;gap:0.4rem;align-items:center;padding:0.2rem 0';
|
||||||
children.push(
|
children.push(
|
||||||
h('h4', H4, `Article: ${node.name}`),
|
h('h4', H4, `Article: ${node.name}`),
|
||||||
h('ul', { class: 'se-article-props', style: 'list-style:none;padding:0;margin:0' },
|
h('ul', { class: 'se-article-props', style: 'list-style:none;padding:0;margin:0' }, [
|
||||||
node.properties.map((p, idx) => {
|
...node.properties.map((p, idx) => {
|
||||||
const isOverwrite = p.scope === 'overwrites';
|
const isOverwrite = p.scope === 'overwrites';
|
||||||
const flip = () => { p.scope = isOverwrite ? 'defaults' : 'overwrites'; markDirty(); };
|
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' }, [
|
const onKeyInput = (e) => { p.name = e.target.value; markDirty(); };
|
||||||
h('span', { style: 'flex:1' }, `${p.name} = ${p.value}`),
|
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', {
|
h('button', {
|
||||||
class: 'se-toggle',
|
class: 'se-toggle',
|
||||||
role: 'switch',
|
role: 'switch',
|
||||||
'aria-checked': String(isOverwrite),
|
'aria-checked': String(isOverwrite),
|
||||||
onClick: flip,
|
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') {
|
} else if (node.type === 'bar') {
|
||||||
const markBarDirty = () => { node.isDirty = true; props.store.markDirty(); };
|
const markBarDirty = () => { node.isDirty = true; props.store.markDirty(); };
|
||||||
|
|||||||
@ -398,3 +398,33 @@
|
|||||||
.se-toggle-label.active {
|
.se-toggle-label.active {
|
||||||
color: #d8d8d8;
|
color: #d8d8d8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.se-prop-key, .se-prop-val {
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #d8d8d8;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
padding: 0.15rem 0.3rem;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
.se-prop-key:focus, .se-prop-val:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #7a7a7a;
|
||||||
|
background: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.se-btn-remove {
|
||||||
|
background: transparent;
|
||||||
|
color: #888;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 0.4rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.se-btn-remove:hover {
|
||||||
|
color: #e06060;
|
||||||
|
border-color: #602020;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user