Multi-pane Sub-objects driven by SLOT of SLOT.SUBTYPE
The bottom handle bar is now dynamic. Fixed CP + FO handles, then one
handle per kind of sub-object the focused node has. "Kind" is the SLOT
part of the AST's SLOT.SUBTYPE namespace: sub-objects sharing a SLOT
(e.g. `line.stem_note` + `line.motif`, or `articles.<subtypeA>` +
`articles.<subtypeB>`) collapse into one pane; different SLOTs split.
Score-level kinds emit, in this order: TU (tuning), ST (stage cone +
voices), IN (instruments — linked ones listed read-only), AR (articles),
BA (bars). info metadata is folded into the FO pane (was previously
listed as a sub-object).
Handles are 2-letter labels for now, intended as alt-text for icons in
a later release.
- `subobject-kinds.js` new: `getKindGroups(node)` returns ordered
`[{ kind, items }]`; `KIND_LABEL` maps kind -> 2-letter label.
- `PaneSubObjects.js` now parameterized by `kind` prop; renders one group.
- `AppShell.js` builds the handle bar from `getKindGroups(focusedNode)`;
drops back to FO when the active sub-pane disappears after a focus change.
- `ObjectShort.js` accepts a `readOnly` flag (used for linked instruments).
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
import { h, ref, onMounted } from 'vue';
|
||||
import { h, ref, computed, onMounted, watch } from 'vue';
|
||||
import { PaneCP } from './PaneCP.js';
|
||||
import { PaneFO } from './PaneFO.js';
|
||||
import { PaneSubObjects } from './PaneSubObjects.js';
|
||||
import { ImportDialog } from './ImportDialog.js';
|
||||
import { getKindGroups, KIND_LABEL } from '../subobject-kinds.js';
|
||||
|
||||
const PANES = [
|
||||
{ id: 'cp', label: 'Position' },
|
||||
{ id: 'fo', label: 'Object' },
|
||||
{ id: 'sub', label: 'Sub-objects' },
|
||||
const FIXED_PANES = [
|
||||
{ id: 'cp', label: 'CP', title: 'Current position' },
|
||||
{ id: 'fo', label: 'FO', title: 'Focused object' },
|
||||
];
|
||||
|
||||
export const AppShell = {
|
||||
@@ -16,6 +16,25 @@ export const AppShell = {
|
||||
const activePane = ref('cp');
|
||||
const showImport = ref(false);
|
||||
|
||||
function focusedNode() {
|
||||
const fp = props.store.focusPath;
|
||||
return fp.length ? fp[fp.length - 1] : props.store.scoreModel;
|
||||
}
|
||||
|
||||
const subPanes = computed(() => getKindGroups(focusedNode()).map(g => ({
|
||||
id: `sub:${g.kind}`,
|
||||
kind: g.kind,
|
||||
label: KIND_LABEL[g.kind] ?? g.kind.slice(0, 2).toUpperCase(),
|
||||
title: g.kind,
|
||||
})));
|
||||
|
||||
const panes = computed(() => [...FIXED_PANES, ...subPanes.value]);
|
||||
|
||||
// If active pane disappears after focus change, drop back to FO.
|
||||
watch(panes, ps => {
|
||||
if (!ps.some(p => p.id === activePane.value)) activePane.value = 'fo';
|
||||
});
|
||||
|
||||
function openImport() {
|
||||
if (!props.store.isDirty) showImport.value = true;
|
||||
}
|
||||
@@ -26,33 +45,34 @@ export const AppShell = {
|
||||
|
||||
return () => {
|
||||
const store = props.store;
|
||||
const ap = activePane.value;
|
||||
|
||||
const subPaneNodes = subPanes.value.map(p =>
|
||||
h('div', { key: p.id, class: ['se-pane', ap === p.id ? 'active' : null] },
|
||||
h(PaneSubObjects, { store, kind: p.kind, onFocusFO: () => { activePane.value = 'fo'; } })));
|
||||
|
||||
return h('div', { class: 'se-shell' }, [
|
||||
// Pane area
|
||||
h('div', { class: 'se-pane-area' }, [
|
||||
h('div', { class: ['se-pane', activePane.value === 'cp' ? 'active' : null] },
|
||||
h('div', { class: ['se-pane', ap === 'cp' ? 'active' : null] },
|
||||
h(PaneCP, { store, onImportClick: openImport, onFocusFO: () => { activePane.value = 'fo'; } })),
|
||||
h('div', { class: ['se-pane', activePane.value === 'fo' ? 'active' : null] },
|
||||
h('div', { class: ['se-pane', ap === 'fo' ? 'active' : null] },
|
||||
h(PaneFO, { store })),
|
||||
h('div', { class: ['se-pane', activePane.value === 'sub' ? 'active' : null] },
|
||||
h(PaneSubObjects, { store, onFocusFO: () => { activePane.value = 'fo'; } })),
|
||||
...subPaneNodes,
|
||||
]),
|
||||
|
||||
// Handle bar (tab switcher at bottom)
|
||||
h('div', { class: 'se-handle-bar' }, PANES.map(p =>
|
||||
h('div', { class: 'se-handle-bar' }, panes.value.map(p =>
|
||||
h('button', {
|
||||
key: p.id,
|
||||
class: ['se-handle', activePane.value === p.id ? 'active' : null],
|
||||
class: ['se-handle', ap === p.id ? 'active' : null],
|
||||
title: p.title,
|
||||
onClick: () => { activePane.value = p.id; },
|
||||
}, p.label)
|
||||
)),
|
||||
|
||||
// Error banner
|
||||
store.errorMessage
|
||||
? h('div', { class: 'se-error', style: 'margin:0' }, store.errorMessage)
|
||||
: null,
|
||||
|
||||
// Import dialog
|
||||
showImport.value
|
||||
? h(ImportDialog, { store, onClose: () => { showImport.value = false; } })
|
||||
: null,
|
||||
|
||||
Reference in New Issue
Block a user