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>
131 lines
4.4 KiB
TypeScript
131 lines
4.4 KiB
TypeScript
import type { Application } from 'pixi.js';
|
||
import {
|
||
Button,
|
||
Checkbox,
|
||
ColorField,
|
||
Dialog,
|
||
HStack,
|
||
Label,
|
||
ListView,
|
||
Menu,
|
||
NumberField,
|
||
Panel,
|
||
ScrollContainer,
|
||
TextField,
|
||
Theme,
|
||
type VStack,
|
||
toHex,
|
||
} from './ui';
|
||
|
||
/**
|
||
* Developer page (`?playground`) that shows every widget of the toolkit for manual
|
||
* testing: focus cycling, typing, scrubbing, color picking, scrolling, popups.
|
||
*/
|
||
export function mountPlayground(app: Application, root: VStack): void {
|
||
const status = new Label('Interact with the widgets – events are logged here.', {
|
||
style: 'muted',
|
||
padding: Theme.padding,
|
||
});
|
||
const log = (message: string) => status.setText(message);
|
||
|
||
const row = new HStack({ gap: Theme.padding, padding: Theme.padding });
|
||
|
||
// Column 1: inputs
|
||
const inputs = new Panel({ title: 'Inputs' });
|
||
inputs.content.add(new Label('Text field'));
|
||
inputs.content.add(
|
||
new TextField('Crate', {
|
||
placeholder: 'Name',
|
||
onInput: (v) => log(`text input: ${v}`),
|
||
onChange: (v) => log(`text change: ${v}`),
|
||
}),
|
||
);
|
||
inputs.content.add(new Label('Number field (drag to scrub)'));
|
||
inputs.content.add(
|
||
new NumberField(100, {
|
||
min: 1,
|
||
step: 1,
|
||
onInput: (v) => log(`number input: ${v}`),
|
||
onChange: (v) => log(`number change: ${v}`),
|
||
}),
|
||
);
|
||
inputs.content.add(
|
||
new NumberField(1, { min: 0.1, max: 10, step: 0.1, onChange: (v) => log(`zoom change: ${v}`) }),
|
||
);
|
||
inputs.content.add(new Label('Color field'));
|
||
inputs.content.add(
|
||
new ColorField(0xe0553f, {
|
||
onInput: (c) => log(`color input: ${toHex(c)}`),
|
||
onChange: (c) => log(`color change: ${toHex(c)}`),
|
||
}),
|
||
);
|
||
const checkRow = new HStack({ gap: Theme.gap, align: 'center' });
|
||
const checkbox = new Checkbox(true);
|
||
checkbox.onChange = (v) => log(`checkbox: ${v}`);
|
||
checkRow.add(checkbox);
|
||
checkRow.add(new Label('Checkbox'), 1);
|
||
inputs.content.add(checkRow);
|
||
row.add(inputs, 1);
|
||
|
||
// Column 2: buttons + popups
|
||
const actions = new Panel({ title: 'Buttons & popups' });
|
||
actions.content.add(new Button('Default', { onClick: () => log('default clicked') }));
|
||
actions.content.add(new Button('Primary', { variant: 'primary', onClick: () => log('primary clicked') }));
|
||
const toggle = new Button('Toggle me');
|
||
toggle.onClick = () => {
|
||
toggle.setToggled(!toggle.toggled);
|
||
log(`toggled: ${toggle.toggled}`);
|
||
};
|
||
actions.content.add(toggle);
|
||
actions.content.add(new Button('Ghost', { variant: 'ghost', onClick: () => log('ghost clicked') }));
|
||
const disabled = new Button('Disabled');
|
||
disabled.setEnabled(false);
|
||
actions.content.add(disabled);
|
||
const menuButton = new Button('Open menu…');
|
||
menuButton.onClick = () => {
|
||
new Menu([
|
||
{ label: 'Scene', onSelect: () => log('menu: scene') },
|
||
{ label: 'Camera', onSelect: () => log('menu: camera') },
|
||
{ label: 'Sprite', onSelect: () => log('menu: sprite') },
|
||
{ label: 'Delete', danger: true, onSelect: () => log('menu: delete') },
|
||
]).openBelow(menuButton);
|
||
};
|
||
actions.content.add(menuButton);
|
||
actions.content.add(
|
||
new Button('Open dialog…', {
|
||
onClick: () =>
|
||
new Dialog('Delete asset?', 'Every placed instance is removed as well.', [
|
||
{ label: 'Cancel' },
|
||
{ label: 'Delete', danger: true, onClick: () => log('dialog: deleted') },
|
||
]).openCentered(),
|
||
}),
|
||
);
|
||
row.add(actions, 1);
|
||
|
||
// Column 3: list in a scroll container
|
||
const listPanel = new Panel({ title: 'List (scroll)', padding: 0 });
|
||
const scroll = new ScrollContainer();
|
||
const list = new ListView();
|
||
list.setItems(
|
||
Array.from({ length: 40 }, (_, i) => ({
|
||
id: `item-${i}`,
|
||
label: `Item ${i + 1}${i % 7 === 0 ? ' with a rather long name that must truncate' : ''}`,
|
||
hint: i % 3 === 0 ? 'sprite' : i % 3 === 1 ? 'camera' : 'scene',
|
||
swatch: i % 3 === 0 ? (0x3fa0e0 + i * 0x040201) & 0xffffff : undefined,
|
||
depth: i % 5 === 4 ? 1 : 0,
|
||
})),
|
||
);
|
||
list.onSelect = (id) => log(`list select: ${id}`);
|
||
list.onActivate = (id) => log(`list activate: ${id}`);
|
||
list.onContextMenu = (id, x, y) =>
|
||
new Menu([{ label: `Context for ${id}`, onSelect: () => log(`context: ${id}`) }]).open(x, y);
|
||
scroll.setContent(list);
|
||
listPanel.content.add(scroll, 1);
|
||
row.add(listPanel, 1);
|
||
|
||
root.add(new Label('Game Maker – UI playground', { style: 'bodyBold', padding: Theme.padding }));
|
||
root.add(row, 1);
|
||
root.add(status);
|
||
root.resize(app.screen.width, app.screen.height);
|
||
}
|