Give a list its own tab instead of a comma line

A list property in the inspector was a single text field holding a comma
separated line -- unreadable past a handful of items, and it could not
carry anything but text. It becomes a button ("3 items") that opens a
sixth centre tab, where every item is a row built by the very widget the
inspector would build for that item kind.

The renderer was free: a PropertyDef of kind 'list' already carries
`items`, so item i becomes a synthetic def of that kind -- number, text,
boolean and colour rows appear without a new branch.

The way into the tab goes through the state (EditorState.listSlot,
openList(), event 'list'), like every other tab switch here; a tab may
not call showTab itself. ListSlot lives next to Selection rather than in
the tab folder, or state.ts would have to import upwards.

A row's handlers must not close over a snapshot of the list: they outlive
a commit, since the tab only rebuilds when the length changes, and the
second edited row would otherwise revive the value the first replaced.

showTab switched five `visible` lines by hand, where a sixth tab is
silently forgotten; it is a Record<CentreTab, Widget> now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-10 10:30:57 +02:00
1 parent 5a440ba433
commit e52e95c0e9
15 files changed
+750 -70

No files matched your search

+20 -11
View File
@@ -17,7 +17,7 @@ import { Button, Label } from '../../ui';
import { createFieldRow, type FieldHandlers } from './fieldFactory';
import { addGap, addNote, addTitle, type InspectorHost } from './host';
import { buildPortSection } from './portSection';
import { previewingHandlers } from './previewHandlers';
import { listOpener, previewingHandlers } from './previewHandlers';
/** Structure-defining facts of a node selection: ports, connected inputs, declared variables. */
export function nodeSignature(project: Project, assetId: string, nodeId: string): string {
@@ -60,7 +60,7 @@ export function buildNodeSection(host: InspectorHost, assetId: string, nodeId: s
host,
effective,
node.params[param.key] ?? effective.default,
paramHandlers(host, assetId, nodeId, param.key),
paramHandlers(host, assetId, nodeId, effective),
);
host.fields.set(`param:${param.key}`, row);
host.body.add(row.row);
@@ -74,7 +74,7 @@ export function buildNodeSection(host: InspectorHost, assetId: string, nodeId: s
host,
inline,
node.params[port.key] ?? inline.default,
paramHandlers(host, assetId, nodeId, port.key),
paramHandlers(host, assetId, nodeId, inline),
);
host.fields.set(`input:${port.key}`, row);
host.body.add(row.row);
@@ -116,14 +116,23 @@ function resolveParamDef(param: PropertyDef, asset: Asset): PropertyDef {
}
/** Previews straight into the node's params; the command records the value from before the preview. */
function paramHandlers(host: InspectorHost, assetId: string, nodeId: string, key: string): FieldHandlers {
return previewingHandlers<GraphNode>(host, {
target: () => currentNode(host.state.project, assetId, nodeId)?.node,
read: (node) => node.params[key],
write: (node, value) => (node.params[key] = value),
remove: (node) => delete node.params[key],
commit: (value) => new SetNodeParamCommand(assetId, nodeId, key, value),
});
function paramHandlers(
host: InspectorHost,
assetId: string,
nodeId: string,
def: PropertyDef,
): FieldHandlers {
const key = def.key;
return {
...previewingHandlers<GraphNode>(host, {
target: () => currentNode(host.state.project, assetId, nodeId)?.node,
read: (node) => node.params[key],
write: (node, value) => (node.params[key] = value),
remove: (node) => delete node.params[key],
commit: (value) => new SetNodeParamCommand(assetId, nodeId, key, value),
}),
...listOpener(host.state, def, () => ({ kind: 'node', assetId, nodeId, key })),
};
}
/** The node together with the graph it sits in, because its ports are derived from both. */