Describe every value kind in one table instead of spreading it over the code

A kind like colour or texture used to be spelled out in five parallel
lists, its port type, its empty value, its glyph, its label and its list
tag - 22 literal places for texture alone. core/properties/kinds.ts now
holds one row per kind and the rest is derived from it, so adding a kind
is one row. The five literal nodes collapse into literals.ts, the six
kind menus into one helper.

Number, Text, Boolean is now the one order everywhere; the variable and
port menus used to list Boolean before Text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-12 14:47:17 +02:00
1 parent 8a7185948d
commit 3ed8b68ecf
39 files changed
+699 -565

No files matched your search

+29 -31
View File
@@ -18,33 +18,39 @@ import {
VStack,
type Widget,
} from '../../ui';
import { type FieldValue, type ListValue, type PropertyValue, textOf } from '../../core/model/Properties';
import {
type FieldValue,
type ListValue,
type PropertyValue,
textOf,
untag,
} from '../../core/model/Properties';
import { nextFieldName } from '../../core/commands/naming';
import {
FIELD_KINDS,
type FieldKind,
kindMark,
LIST_ITEM_KINDS,
type ListItemKind,
} from '../../core/properties/PropertySchema';
} from '../../core/properties/kinds';
import {
type ListItem,
itemKindOf,
itemValue,
makeFlatItem,
makeItem,
structItemFields,
structItemOf,
} from '../../core/properties/listItems';
import { flatDef } from '../../core/properties/PropertySchema';
import {
addValue,
createField,
type FieldContext,
NAME_WIDTH,
primitiveDef,
READ_ONLY,
removeButton,
} from '../inspector/fieldFactory';
import { KIND_LABELS, kindMark, markPicker } from '../inspector/variableRows';
import { kindMenu, markPicker } from '../inspector/variableRows';
import type { EditorState } from '../state';
import { type ListAccess, resolveList } from './slot';
@@ -103,9 +109,7 @@ export class ListEditor extends VStack {
// Built on click, not once: which kinds fit depends on the list the tab is showing.
this.addButton.onClick = () => {
const kinds = this.access()?.kinds ?? LIST_ITEM_KINDS;
new Menu(
kinds.map((kind) => ({ label: KIND_LABELS[kind], onSelect: () => this.addItem(kind) })),
).openBelow(this.addButton);
new Menu(kindMenu(kinds, (kind) => this.addItem(kind))).openBelow(this.addButton);
};
footer.add(this.addButton);
footer.add(new Spacer(), 1);
@@ -210,13 +214,11 @@ export class ListEditor extends VStack {
row.add(label);
const mark = markPicker(
kindMark(kind),
access.kinds
.filter((next) => next !== kind)
.map((next) => ({
label: KIND_LABELS[next],
// Through `itemValue`, so a color keeps its number when it becomes one.
onSelect: () => this.replaceItem(access, index, makeItem(next, itemValue(item))),
})),
// Through `untag`, so a color keeps its number when it becomes one.
kindMenu(
access.kinds.filter((next) => next !== kind),
(next) => this.replaceItem(access, index, makeItem(next, untag(item))),
),
);
if (tall) mark.minHeight = Theme.rowHeight;
row.add(mark);
@@ -224,12 +226,12 @@ export class ListEditor extends VStack {
else {
const field = createField(
this.ctx,
primitiveDef(String(index), kind),
itemValue(item) ?? '',
flatDef(String(index), kind),
untag(item) ?? '',
access.itemHandlers(index),
);
addValue(row, field);
this.setters.push((next) => field.setValue(itemValue(next) ?? ''));
this.setters.push((next) => field.setValue(untag(next) ?? ''));
this.rowWidgets.push(rowWidgets(mark, field.widget));
}
row.add(removeButton(() => access.replace(access.value.filter((_, i) => i !== index))));
@@ -255,14 +257,14 @@ export class ListEditor extends VStack {
line.minHeight = Theme.rowHeight;
const fieldMark = markPicker(
kindMark(field.kind),
FIELD_KINDS.filter((next) => next !== field.kind).map((next) => ({
label: KIND_LABELS[next],
onSelect: () =>
kindMenu(
FIELD_KINDS.filter((next) => next !== field.kind),
(next) =>
this.replaceItem(access, index, {
...bundle,
[field.key]: makeFlatItem(next, itemValue(bundle[field.key])),
[field.key]: makeFlatItem(next, untag(bundle[field.key])),
}),
})),
),
);
const name = nameField(field.key, (to) => {
if (to && to !== field.key) this.replaceItem(access, index, renamedField(bundle, field.key, to));
@@ -271,8 +273,8 @@ export class ListEditor extends VStack {
line.add(name);
const sub = createField(
this.ctx,
primitiveDef(field.key, field.kind),
itemValue(bundle[field.key]) ?? '',
flatDef(field.key, field.kind),
untag(bundle[field.key]) ?? '',
handlers.structFieldHandlers?.(field) ?? READ_ONLY,
);
setters.set(field.key, addValue(line, sub));
@@ -290,7 +292,7 @@ export class ListEditor extends VStack {
row.add(stack, 1);
this.setters.push((next) => {
const now = structItemOf(next);
for (const field of fields) setters.get(field.key)?.(itemValue(now[field.key]) ?? '');
for (const field of fields) setters.get(field.key)?.(untag(now[field.key]) ?? '');
});
this.rowWidgets.push(widgets);
}
@@ -319,11 +321,7 @@ function nameField(key: string, rename: (next: string) => void): TextField {
function addFieldMenu(add: (kind: FieldKind) => void): Dropdown {
const button = new Dropdown('+ Field', { variant: 'ghost' });
button.onClick = () => {
new Menu(FIELD_KINDS.map((kind) => ({ label: KIND_LABELS[kind], onSelect: () => add(kind) }))).openBelow(
button,
);
};
button.onClick = () => new Menu(kindMenu(FIELD_KINDS, add)).openBelow(button);
return button;
}
+2 -8
View File
@@ -10,15 +10,9 @@ import { behaviorLookup } from '../../core/model/behaviorPrograms';
import type { ObjectRef } from '../../core/model/instance';
import { isList, type ListValue, type PropertyValue, type StructValue } from '../../core/model/Properties';
import { findAsset } from '../../core/model/Project';
import { FIELD_KINDS, LIST_ITEM_KINDS, type ListItemKind } from '../../core/properties/kinds';
import { fieldLike, type ListItem, itemLike, structItemOf } from '../../core/properties/listItems';
import {
FIELD_KINDS,
LIST_ITEM_KINDS,
type ListItemKind,
type PropertyDef,
schemaForAsset,
structOf,
} from '../../core/properties/PropertySchema';
import { type PropertyDef, schemaForAsset, structOf } from '../../core/properties/PropertySchema';
import { resolveAssetProps } from '../../core/properties/resolve';
import type { FieldHandlers } from '../inspector/fieldFactory';
import { currentProps, currentRef } from '../inspector/objectQueries';