Left-align menu entries, add a Dropdown field, scrollable and searchable menus

Menu entries are left-aligned and live in a ScrollContainer capped at 320 px, so a
long list (the parent picker holds every object of the scene) scrolls instead of
running off the screen. `Menu(items, { searchable: true })` puts a filter box above
the scroll area; Enter takes the first match. Filtering forces a relayout because
two queries with the same hit count leave the height unchanged, which would keep
the stale row positions.

Fields that open a menu are a new `Dropdown` widget: the value sits flush left and
the chevron is drawn flush right instead of being written into the label. A menu
opened below a field is never narrower than that field.

New debug hooks `overlayTexts()` / `overlayRect()` make the popup contents
assertable; e2e step 11 covers the cap, the scrolling and the search box.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-08 11:31:14 +02:00
1 parent d54a620ebd
commit 15ffcd5739
16 files changed
+297 -39

No files matched your search

+4 -3
View File
@@ -31,7 +31,7 @@ import {
deviatesFromTemplate,
resolveSceneObjectProps,
} from '../../core/properties/resolve';
import { Button, Menu, type MenuItem, TextField } from '../../ui';
import { Button, Dropdown, Menu, type MenuItem, TextField } from '../../ui';
import { addCollider, createContainer, removeObjects } from '../actions';
import { createFieldRow, type FieldActions, type FieldRow, labelledRow } from './fieldFactory';
import { addGap, addNote, addTitle, type InspectorHost } from './host';
@@ -368,7 +368,7 @@ function parentRow(host: InspectorHost, scene: SceneAsset, object: SceneObject)
const { row } = labelledRow('Parent');
const nameOf = (o: SceneObject) => o.name || findAsset(host.state.project, o.assetId)?.name || '?';
const parent = findObject(scene, object.parentId);
const button = new Button(`${parent ? nameOf(parent) : 'None'} ▾`);
const button = new Dropdown(parent ? nameOf(parent) : 'None');
button.onClick = () => {
const items: MenuItem[] = [
{ label: 'None', onSelect: () => host.state.execute(new SetParentCommand(scene.id, object.id, '')) },
@@ -381,7 +381,8 @@ function parentRow(host: InspectorHost, scene: SceneAsset, object: SceneObject)
onSelect: () => host.state.execute(new SetParentCommand(scene.id, object.id, candidate.id)),
});
}
new Menu(items).openBelow(button);
// Searchable: this lists every object of the scene, which grows with the project.
new Menu(items, { searchable: true }).openBelow(button);
};
row.add(button, 1);
return row;