Refactoring round 5 covers the code since round 4 without changing behaviour: four identical copies of dot() and three of "nearest point on a segment" stood in the collision math, in the hit area of the collider outlines and in the polygon handles, so they moved into the new module src/core/util/vec.ts; the warning line, the busy state, info() and the caret reset were written out line by line in both the sign-in screen and the password dialog and are now FormStatus; the closing row "Cancel plus a confirming button, optionally one on the left" was built by hand in FormDialog, FilePicker, OpenProjectDialog and PasswordDialog and is now dialogButtons(); the header bar was drawn by the toolbar, the TabHeader and the Breadcrumb each on its own and is now drawHeaderBar(); AddColliderCommand and AddRigidbodyCommand share a base, each subclass only builds its own asset; Editor.ts derives the panes from assetEditors instead of repeating eight entries and clamps its splitters with clamp(); jscpd over src falls from 0.19 % to 0.14 %; dead code is gone - listScenes() has been orphaned since the Scene picker fell away (#183), DEFAULT_FILTER (#152) was never used, the two test hooks projectPickerRowAt/rowPosition and particleCompiles are read by no tool, and four functions lost an export they did not need; seven comments that told history ("used to", "no longer", "since #...") describe the state of things now, and that this changed nothing was proved by a rebuild - the bundle is md5 identical, b4ad0dfe... before and after; the README said Logout sits at the far right of the toolbar, which has been the Account menu since #182, and wiki/Architecture.md was missing src/core/util/ altogether; 4 new unit tests cover vec.ts, and switching the clamp on the segment off makes exactly one of them fail while none of the 76 neighbouring tests notice, so that clamping was covered nowhere before (#85)
game-maker / gate (push) Successful in 2m0s
game-maker / suite (push) Successful in 7m17s

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-20 17:24:42 +02:00
1 parent da35f10d1a
commit ae3aa7d3bd
32 files changed
+240 -246

No files matched your search

+46
View File
@@ -0,0 +1,46 @@
import { Label, requestRender, type TextField, ui } from '../../ui';
/**
* What a form that can be refused shows about its last attempt – the sign-in screen and the
* password dialog: the warning line under the fields, and whether an answer is still awaited.
*/
export class FormStatus {
readonly message = new Label('', { style: 'warning' });
private waiting = false;
/** `onChange` redoes what the form shows of it: its button while waiting, its layout after a message. */
constructor(private readonly onChange: (busy: boolean) => void) {}
get busy(): boolean {
return this.waiting;
}
/** What the check scripts read. */
info(): { message: string; busy: boolean } {
return { message: this.message.text, busy: this.waiting };
}
say(message: string): void {
this.message.setText(message);
this.changed();
}
setBusy(busy: boolean): void {
this.waiting = busy;
this.changed();
}
private changed(): void {
this.onChange(this.waiting);
requestRender();
}
}
/**
* Puts the caret back into a field after a refused attempt. A field blurs itself once its Enter
* has been handled, and that happens after this call returns – so the focus has to be set again
* behind it, or the next thing typed goes nowhere.
*/
export function refocus(field: TextField): void {
queueMicrotask(() => ui().focus.focus(field));
}
+15 -34
View File
@@ -1,6 +1,7 @@
import { Graphics } from 'pixi.js';
import { type AuthStore, WrongCredentialsError } from '../../core/store/AuthStore';
import { Button, drawControlBox, Label, requestRender, Spacer, TextField, Theme, ui, VStack } from '../../ui';
import { Button, drawControlBox, Label, Spacer, TextField, Theme, ui, VStack } from '../../ui';
import { FormStatus, refocus } from './FormStatus';
import { loginError, loginProblem } from './loginRules';
const CARD_WIDTH = 280;
@@ -31,9 +32,11 @@ class Card extends VStack {
export class LoginScreen extends VStack {
private readonly user: TextField;
private readonly password: TextField;
private readonly message: Label;
private readonly submit: Button;
private busy = false;
private readonly status = new FormStatus((busy) => {
this.submit.setText(busy ? 'Signing in…' : 'Sign in');
this.submit.setEnabled(!busy);
});
constructor(
private readonly auth: AuthStore,
@@ -64,8 +67,7 @@ export class LoginScreen extends VStack {
});
card.add(this.password);
this.message = new Label('', { style: 'warning' });
card.add(this.message);
card.add(this.status.message);
this.submit = new Button('Sign in', {
variant: 'primary',
width: CARD_WIDTH - Theme.padding * 4,
@@ -84,49 +86,28 @@ export class LoginScreen extends VStack {
/** What the check scripts read: the message under the form and whether it is waiting. */
info(): { message: string; busy: boolean } {
return { message: this.message.text, busy: this.busy };
return this.status.info();
}
private async signIn(): Promise<void> {
if (this.busy) return;
if (this.status.busy) return;
const username = this.user.value.trim();
const password = this.password.value;
const problem = loginProblem(username, password);
if (problem) {
this.say(problem);
this.refocus(username === '' ? this.user : this.password);
this.status.say(problem);
refocus(username === '' ? this.user : this.password);
return;
}
this.setBusy(true);
this.status.setBusy(true);
try {
const user = await this.auth.logIn(username, password);
this.onSignedIn(user);
} catch (error) {
this.say(loginError(error, error instanceof WrongCredentialsError));
this.setBusy(false);
this.status.say(loginError(error, error instanceof WrongCredentialsError));
this.status.setBusy(false);
// A second attempt starts where the first one went wrong, not in an empty form.
this.refocus(this.password);
refocus(this.password);
}
}
/**
* Puts the caret back into a field after a refused attempt. A field blurs itself once its Enter
* has been handled, and that happens after this call returns – so the focus has to be set again
* behind it, or the next thing typed goes nowhere.
*/
private refocus(field: TextField): void {
queueMicrotask(() => ui().focus.focus(field));
}
private setBusy(busy: boolean): void {
this.busy = busy;
this.submit.setText(busy ? 'Signing in…' : 'Sign in');
this.submit.setEnabled(!busy);
requestRender();
}
private say(message: string): void {
this.message.setText(message);
requestRender();
}
}
+17 -41
View File
@@ -1,6 +1,7 @@
import { WrongCredentialsError } from '../../core/store/AuthStore';
import { Button, HStack, Label, requestRender, Spacer, TextField, Theme, ui } from '../../ui';
import { FormDialog } from '../FormDialog';
import { Button, Label, TextField, ui } from '../../ui';
import { dialogButtons, FormDialog } from '../FormDialog';
import { FormStatus, refocus } from './FormStatus';
import { MIN_PASSWORD_LENGTH, passwordError, passwordProblem } from './loginRules';
const FIELD_WIDTH = 200;
@@ -16,9 +17,11 @@ export class PasswordDialog extends FormDialog {
private readonly current: TextField;
private readonly next: TextField;
private readonly repeat: TextField;
private readonly message: Label;
private readonly confirm: Button;
private busy = false;
private readonly status = new FormStatus((busy) => {
this.confirm.setEnabled(!busy);
this.refreshLayout();
});
constructor(
private readonly change: (current: string, next: string) => Promise<void>,
@@ -38,17 +41,10 @@ export class PasswordDialog extends FormDialog {
this.content.add(this.formRow(this.fieldLabel('New'), this.next));
this.content.add(this.formRow(this.fieldLabel('Repeat'), this.repeat));
this.message = new Label('', { style: 'warning' });
this.content.add(this.message);
this.content.add(this.status.message);
const buttons = new HStack({ gap: Theme.gap, align: 'center' });
buttons.add(new Spacer(), 1);
const cancel = new Button('Cancel');
cancel.onClick = this.closeThen();
buttons.add(cancel);
this.confirm = new Button('Change password', { variant: 'primary', onClick: () => void this.submit() });
buttons.add(this.confirm);
this.content.add(buttons);
this.content.add(dialogButtons(this.closeThen(), this.confirm));
}
/** Puts the caret in the first field, so a keyboard alone can go through the form. */
@@ -58,7 +54,7 @@ export class PasswordDialog extends FormDialog {
/** What the check scripts read: the message under the form and whether it is waiting. */
info(): { message: string; busy: boolean } {
return { message: this.message.text, busy: this.busy };
return this.status.info();
}
private secretField(onSubmit: () => void): TextField {
@@ -66,44 +62,24 @@ export class PasswordDialog extends FormDialog {
}
private async submit(): Promise<void> {
if (this.busy) return;
if (this.status.busy) return;
const current = this.current.value;
const next = this.next.value;
const problem = passwordProblem(current, next, this.repeat.value);
if (problem) {
this.say(problem);
this.refocus(current === '' ? this.current : this.next);
this.status.say(problem);
refocus(current === '' ? this.current : this.next);
return;
}
this.setBusy(true);
this.status.setBusy(true);
try {
await this.change(current, next);
this.close();
this.onChanged();
} catch (error) {
this.say(passwordError(error, error instanceof WrongCredentialsError));
this.setBusy(false);
this.refocus(this.current);
this.status.say(passwordError(error, error instanceof WrongCredentialsError));
this.status.setBusy(false);
refocus(this.current);
}
}
/**
* Puts the caret back after a refused attempt. A field blurs itself once its Enter has been
* handled, and that happens after this call returns (see `LoginScreen.refocus`).
*/
private refocus(field: TextField): void {
queueMicrotask(() => ui().focus.focus(field));
}
private setBusy(busy: boolean): void {
this.busy = busy;
this.confirm.setEnabled(!busy);
requestRender();
}
private say(message: string): void {
this.message.setText(message);
this.refreshLayout();
requestRender();
}
}