From 5bec1a1d1e7877f813b81488eb4d43bc5d2c33bf Mon Sep 17 00:00:00 2001 From: c0dev0id Date: Mon, 29 Jun 2026 08:17:57 +0200 Subject: [PATCH] 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. --- static/ast-parser.js | 2 +- static/components/PaneCP.js | 4 ++++ static/components/PaneFO.js | 45 +++++++++++++++++++++++++++++++------ static/style.css | 30 +++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 8 deletions(-) diff --git a/static/ast-parser.js b/static/ast-parser.js index 9d3c84e..e85bd8e 100644 --- a/static/ast-parser.js +++ b/static/ast-parser.js @@ -2,7 +2,7 @@ const LINE_RE = /^(\d{2}) (\S+(?:\.\S+)*) ?(.*)/; 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 === 'False' || s === 'N' || s === 'off' || s === 'false') return false; if (s === '') return s; diff --git a/static/components/PaneCP.js b/static/components/PaneCP.js index 201d9f7..248bd83 100644 --- a/static/components/PaneCP.js +++ b/static/components/PaneCP.js @@ -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: [] }; } diff --git a/static/components/PaneFO.js b/static/components/PaneFO.js index 3ace2ca..6d4b6c9 100644 --- a/static/components/PaneFO.js +++ b/static/components/PaneFO.js @@ -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(); }; diff --git a/static/style.css b/static/style.css index 993c0e7..7eaab61 100644 --- a/static/style.css +++ b/static/style.css @@ -398,3 +398,33 @@ .se-toggle-label.active { 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; +}