Add ESLint and Prettier, and close the node test gaps

Tooling:
- Prettier as the single formatting authority (110 columns, single quotes,
  measured against the existing style); Markdown excluded because table-cell
  padding turns the README glossary into 1000-character lines.
- Type-aware ESLint ending in eslint-config-prettier, so a rule always means
  "this is a bug", never "this looks different".
- .editorconfig and an `npm run check` that runs typecheck, lint, format and
  tests in one go.

Findings fixed on the first run: three more duplicate imports, four
String(unknown) calls (parameter types narrowed instead of casts), two dead
initialisers, two redundant casts, a console.log for a real error. Two
warnings were false here and are disabled with a reason in place.

Tests 132 -> 269:
- node-catalog.test.ts covers every node with a run(), with a guard that fails
  when a new node has no row.
- behavior-events.test.ts covers the ten event nodes through a running World,
  checking timing rather than existence.
- Input's state machine (press/release/releaseAll) is reachable without DOM
  events now; the DOM handlers are thin adapters.

Compare fell back to == without a configured operator although the schema
default is <; aligned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-08 10:02:22 +02:00
1 parent f140c2fd77
commit d54a620ebd
113 files changed
+5049 -666

No files matched your search

+21 -4
View File
@@ -13,12 +13,21 @@ export class AssetPanel extends Panel {
private readonly list = new ListView();
private readonly scroll = new ScrollContainer();
constructor(private readonly state: EditorState, private readonly placementPoint: () => Vec2) {
constructor(
private readonly state: EditorState,
private readonly placementPoint: () => Vec2,
) {
super({ title: 'Assets', padding: 0, gap: 0 });
this.minWidth = 160;
const add = new Button('+', { variant: 'ghost', width: 28 });
add.onClick = () => new Menu(ASSET_TYPES.map((type) => ({ label: ASSET_TYPE_LABELS[type], onSelect: () => this.createAsset(type) }))).openBelow(add);
add.onClick = () =>
new Menu(
ASSET_TYPES.map((type) => ({
label: ASSET_TYPE_LABELS[type],
onSelect: () => this.createAsset(type),
})),
).openBelow(add);
this.header.add(add);
this.list.emptyText = 'No assets – press +';
@@ -81,9 +90,17 @@ export class AssetPanel extends Panel {
if (!asset) return;
const items: MenuItem[] = [];
if (asset.type === 'scene') {
items.push({ label: 'Set as active scene', onSelect: () => this.state.execute(new SetActiveSceneCommand(id)), disabled: this.state.project.activeSceneId === id });
items.push({
label: 'Set as active scene',
onSelect: () => this.state.execute(new SetActiveSceneCommand(id)),
disabled: this.state.project.activeSceneId === id,
});
} else {
items.push({ label: 'Place in scene', onSelect: () => this.place(id), disabled: !this.state.activeScene });
items.push({
label: 'Place in scene',
onSelect: () => this.place(id),
disabled: !this.state.activeScene,
});
}
items.push({ label: 'Delete…', danger: true, onSelect: () => confirmDeleteAsset(this.state, id) });
new Menu(items).open(x, y);
+27 -6
View File
@@ -49,7 +49,10 @@ export class HierarchyPanel extends Panel {
state.on('project', () => this.refresh());
// A different project brings different objects – start folded again.
state.on('load', () => { this.expanded.clear(); this.refresh(); });
state.on('load', () => {
this.expanded.clear();
this.refresh();
});
state.on('selection', () => (this.revealSelection() ? this.refresh() : this.syncSelection()));
this.refresh();
}
@@ -88,13 +91,31 @@ export class HierarchyPanel extends Panel {
const selected = state.selectedObjectIds.includes(id) ? state.selectedObjectIds : [id];
const many = selected.length > 1;
new Menu([
{ label: 'Select asset', onSelect: () => object && state.select({ kind: 'asset', assetId: object.assetId }) },
{ label: 'Add collider', disabled: !type || type === 'collider' || many, onSelect: () => addCollider(state, scene.id, id) },
{ label: many ? `Create container from ${selected.length} objects` : 'Create container', onSelect: () => createContainer(state, scene.id, selected) },
{ label: 'Unparent (move to top level)', disabled: !object?.parentId || many, onSelect: () => state.execute(new SetParentCommand(scene.id, id, '')) },
{
label: 'Select asset',
onSelect: () => object && state.select({ kind: 'asset', assetId: object.assetId }),
},
{
label: 'Add collider',
disabled: !type || type === 'collider' || many,
onSelect: () => addCollider(state, scene.id, id),
},
{
label: many ? `Create container from ${selected.length} objects` : 'Create container',
onSelect: () => createContainer(state, scene.id, selected),
},
{
label: 'Unparent (move to top level)',
disabled: !object?.parentId || many,
onSelect: () => state.execute(new SetParentCommand(scene.id, id, '')),
},
{ label: 'Expand all', onSelect: () => this.expandAll() },
{ label: 'Collapse all', onSelect: () => this.collapseAll() },
{ label: many ? `Remove ${selected.length} objects from scene` : 'Remove from scene', danger: true, onSelect: () => removeObjects(state, scene.id, selected) },
{
label: many ? `Remove ${selected.length} objects from scene` : 'Remove from scene',
danger: true,
onSelect: () => removeObjects(state, scene.id, selected),
},
]).open(x, y);
}
+13 -3
View File
@@ -34,7 +34,10 @@ export class Toolbar extends HStack {
private readonly notice: Label;
private readonly fps: Label;
constructor(private readonly state: EditorState, actions: ToolbarActions) {
constructor(
private readonly state: EditorState,
actions: ToolbarActions,
) {
super({ gap: Theme.gap, padding: Theme.padding, align: 'center' });
this.minHeight = Theme.controlHeight + Theme.padding * 2;
this.addChildAt(this.bg, 0);
@@ -53,7 +56,10 @@ export class Toolbar extends HStack {
this.add(new Spacer(Theme.padding));
this.add(new Label('Scene', { style: 'muted' }));
this.sceneButton = new Button('—', { width: 160, onClick: () => actions.openSceneMenu(this.sceneButton) });
this.sceneButton = new Button('—', {
width: 160,
onClick: () => actions.openSceneMenu(this.sceneButton),
});
this.add(this.sceneButton);
this.add(new Spacer(Theme.padding));
@@ -63,7 +69,11 @@ export class Toolbar extends HStack {
this.add(this.redoButton);
this.add(new Spacer(), 1);
this.playButton = new Button('▶ Play', { variant: 'primary', width: 110, onClick: () => actions.togglePlay() });
this.playButton = new Button('▶ Play', {
variant: 'primary',
width: 110,
onClick: () => actions.togglePlay(),
});
this.add(this.playButton);
this.add(new Spacer(), 1);