Back to Black Site Map Editor
SOURCE / PINNED RELEASE

Made of little things.

Black Site Map Editor

Release
32a57733b7e9…
Author-recorded commit
3f54890f542f…
License
LICENSE
Author’s source reference
nostr://npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr/wss%3A%2F%2Fgit.shakespeare.diy%2F/black-site-map-editor

Archive hash verified: 7a4b1abbac7db5b0…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

src/main.ts
import './styles.css';
import { runtimeHasDomain } from './domain-availability.js';
import { editor } from './editor-controller';
import { getViewer, watchViewer, loadTutorialSeen } from './map-host';
import { subscribeMapIntents } from './map-intent';
import { installShortcuts } from './shortcuts';
import type { ShortcutAction } from './shortcuts';
import { tools, toolKeys, toolShortcutId } from './tools';
import { rendered } from './components/bs-app';
// Black Site owns its palette in every shell theme.
const FOLLOW_HOST_THEME = false;
void FOLLOW_HOST_THEME;

// Keyboard shortcuts: NAP-KEYS actions when the host offers them, built-in keys otherwise.
// Button-backed actions click the rendered button, so they honour its disabled state.
const press = (id: string) => () => {
    const target = document.getElementById(id) as HTMLButtonElement | null;
    if (target && !target.disabled) target.click();
};
const nudge = (dx: number, dy: number) => () => editor.nudge(dx, dy);
const shortcutActions: (ShortcutAction & { inDialog?: boolean })[] = [
    { id: 'map.new', label: 'New map', group: 'Map', defaultKey: 'Shift+N', run: press('new-map') },
    {
        id: 'map.library',
        label: 'Open maps',
        group: 'Map',
        defaultKey: 'Ctrl+O',
        run: press('library-open'),
    },
    {
        id: 'map.settings',
        label: 'Map settings',
        group: 'Map',
        defaultKey: 'Ctrl+,',
        run: press('settings-open'),
    },
    {
        id: 'map.save-draft',
        label: 'Save draft',
        group: 'Map',
        defaultKey: 'Ctrl+S',
        run: press('save-draft'),
    },
    {
        id: 'map.download',
        label: 'Download map file',
        group: 'Map',
        defaultKey: 'Ctrl+Shift+S',
        run: press('export-json'),
    },
    {
        id: 'map.check',
        label: 'Check route',
        group: 'Map',
        defaultKey: 'Shift+C',
        run: press('validate'),
    },
    {
        id: 'map.publish',
        label: 'Publish or update',
        group: 'Map',
        defaultKey: 'Ctrl+Enter',
        run: press('publish'),
    },
    { id: 'edit.undo', label: 'Undo', group: 'Edit', defaultKey: 'Ctrl+Z', run: press('undo') },
    {
        id: 'edit.redo',
        label: 'Redo',
        group: 'Edit',
        defaultKey: 'Ctrl+Shift+Z',
        run: press('redo'),
    },
    {
        id: 'edit.redo-alt',
        label: 'Redo (alternate)',
        group: 'Edit',
        defaultKey: 'Ctrl+Y',
        run: press('redo'),
    },
    {
        id: 'selection.delete',
        label: 'Delete selected object',
        group: 'Edit',
        defaultKey: 'Delete',
        run: press('delete-object'),
    },
    {
        id: 'selection.nudge-up',
        label: 'Move selection up',
        group: 'Edit',
        defaultKey: 'ArrowUp',
        run: nudge(0, -1),
    },
    {
        id: 'selection.nudge-down',
        label: 'Move selection down',
        group: 'Edit',
        defaultKey: 'ArrowDown',
        run: nudge(0, 1),
    },
    {
        id: 'selection.nudge-left',
        label: 'Move selection left',
        group: 'Edit',
        defaultKey: 'ArrowLeft',
        run: nudge(-1, 0),
    },
    {
        id: 'selection.nudge-right',
        label: 'Move selection right',
        group: 'Edit',
        defaultKey: 'ArrowRight',
        run: nudge(1, 0),
    },
    {
        id: 'selection.jump-up',
        label: 'Move selection up 5 tiles',
        group: 'Edit',
        defaultKey: 'Shift+ArrowUp',
        run: nudge(0, -5),
    },
    {
        id: 'selection.jump-down',
        label: 'Move selection down 5 tiles',
        group: 'Edit',
        defaultKey: 'Shift+ArrowDown',
        run: nudge(0, 5),
    },
    {
        id: 'selection.jump-left',
        label: 'Move selection left 5 tiles',
        group: 'Edit',
        defaultKey: 'Shift+ArrowLeft',
        run: nudge(-5, 0),
    },
    {
        id: 'selection.jump-right',
        label: 'Move selection right 5 tiles',
        group: 'Edit',
        defaultKey: 'Shift+ArrowRight',
        run: nudge(5, 0),
    },
    { id: 'view.zoom-in', label: 'Zoom in', group: 'View', defaultKey: '+', run: press('zoom-in') },
    {
        id: 'view.zoom-out',
        label: 'Zoom out',
        group: 'View',
        defaultKey: '-',
        run: press('zoom-out'),
    },
    { id: 'view.fit', label: 'Fit map to view', group: 'View', defaultKey: '0', run: press('fit') },
    {
        id: 'view.grid',
        label: 'Show or hide grid',
        group: 'View',
        defaultKey: 'G',
        run: press('grid-toggle'),
    },
    ...tools.map(([key, label]) => ({
        id: toolShortcutId(key),
        label: `${label} tool`,
        group: 'Tools',
        defaultKey: toolKeys[key],
        run: () => editor.setTool(key),
    })),
    {
        id: 'help.shortcuts',
        label: 'Show keyboard shortcuts',
        group: 'Help',
        defaultKey: 'Shift+?',
        run: () => editor.ui.shortcuts?.show(),
        inDialog: true,
    },
    {
        id: 'help.tutorial',
        label: 'Help & tutorial',
        group: 'Help',
        defaultKey: 'F1',
        run: () => editor.ui.help?.show(),
    },
];
for (const action of shortcutActions) {
    const run = action.run;
    // Host-delivered actions can arrive at any time, so the same guards apply as for local keys.
    action.run = () => {
        if (editor.state.busy || (editor.anyDialogOpen && !action.inDialog)) return;
        editor.guard(run)();
    };
}

let identitySubscription: { close: () => void } | undefined;
let intentSubscription: { close: () => void } | undefined;
async function boot() {
    const app = document.querySelector<HTMLDivElement>('#app')!;
    app.replaceChildren(document.createElement('bs-app'));
    await rendered(app);
    editor.ui.canvas?.fit();
    editor.shortcuts = installShortcuts(shortcutActions, () => editor.bumpShortcuts());
    editor.bumpShortcuts();
    if (runtimeHasDomain('identity')) {
        try {
            identitySubscription = watchViewer(editor.identityChanged);
            const epoch = editor.identityEpoch;
            const key = await getViewer();
            if (epoch === editor.identityEpoch) {
                editor.identityChanged(key);
                if (!key) editor.setIdentityLabel('Sign in to share maps');
            }
        } catch (error) {
            editor.setIdentityLabel('Sign-in unavailable');
            editor.fail(error);
        }
    } else {
        editor.setIdentityLabel('Sign-in unavailable');
        editor.status('Sign-in is unavailable here. You can still create a map and save a file.');
    }
    const bootWorkspace = editor.workspaceVersion;
    intentSubscription = subscribeMapIntents(editor.receiveMapIntent, editor.fail);
    await autoOpenTutorial(bootWorkspace);
}
/** Show the tutorial once for new players, unless a map request or another dialog already needs their attention. */
async function autoOpenTutorial(bootWorkspace: number) {
    if (!runtimeHasDomain('storage')) return;
    let seen = true;
    try {
        seen = await loadTutorialSeen();
    } catch {
        return;
    }
    if (
        seen ||
        editor.intentPending ||
        editor.workspaceVersion !== bootWorkspace ||
        editor.anyDialogOpen
    )
        return;
    editor.ui.help?.show();
}
window.addEventListener('pagehide', () => {
    identitySubscription?.close();
    intentSubscription?.close();
    editor.shortcuts.close();
});
void boot();