The editor gets a name of its own that fits beside Brandmauer, Glutspeicher and Feuerwehr. This is stage 1, 2 and 4 of #422: the name, the URL and the stored state in the browser. Repository, CI runners, containers and the GAME_MAKER_* variables follow in stage 3. - All paths come from SITE_PATH in src/core/site.ts: the API endpoint, the data folders, the play page and the base in vite.config.ts. A later move is one line instead of fifteen. - The window title, the sign-in screen, the playground and the README say Feuerwerk. The package, __feuerwerkPlay, the take worklet and the writing app in Ogg and Matroska follow. - The session cookie is now feuerwerk_session, so everyone signs in once more. - localStorage keys are now feuerwerk.*. moveOldStoredKeys copies the old game-maker.* keys over on the first start, so settings, panel sizes and save games survive. - tools/ci/serve.sh serves the build under /feuerwerk/. The variable GAME_MAKER_BASE_URL keeps its name until stage 3. Outside the repository, nockal.com/feuerwerk/ is a second symlink to the same tree. nginx sends the bare /game-maker/ folder there with a 301. Everything else under /game-maker/ is still served directly, so shared play links and the API keep working. Finding: when nginx redirects a path without a trailing slash or with a query, the Brandmauer answers with its own 307 to the same URL plus a slash, placed after the query. A play link would lose its token that way, which is why only the bare folder is redirected. Tests: - Gate on melektaus-atx: CHECK EXIT 0, 307 files, 4756 tests (4 new in site.test.ts). - Counter-check: when the migration misses the old prefix, 3 of the 4 new tests fail. - e2e 60 under /feuerwerk/: ALL PASSED, 20 checks. - From outside: /feuerwerk/ 200, /game-maker/ 301, an old play link 200. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
127 lines
4.2 KiB
TypeScript
127 lines
4.2 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 });
|
||
|
||
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);
|
||
|
||
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);
|
||
|
||
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',
|
||
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('Feuerwerk – UI playground', { style: 'bodyBold', padding: Theme.padding }));
|
||
root.add(row, 1);
|
||
root.add(status);
|
||
root.resize(app.screen.width, app.screen.height);
|
||
}
|