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.
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();
