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 { html } from 'lit';
import { editor, type DialogView } from '../editor-controller';
import { tutorialSteps } from '../help-tutorial';
import { LightElement } from './light-element';
const total = tutorialSteps.length;
/** Vertical-tab tutorial. `show` starts at the first step or a named step. */
export class BsHelpDialog extends LightElement implements DialogView {
static properties = { current: { state: true } };
declare current: number;
constructor() {
super();
this.current = 0;
}
connectedCallback() {
super.connectedCallback();
editor.ui.help = this;
}
get #dialog() {
return this.querySelector<HTMLDialogElement>('dialog')!;
}
get isOpen() {
return this.#dialog.open;
}
show(stepId?: string) {
const index = tutorialSteps.findIndex((step) => step.id === stepId);
if (!this.isOpen) this.#dialog.showModal();
void this.#select(index < 0 ? 0 : index, true);
}
close() {
if (this.isOpen) this.#dialog.close();
}
async #select(index: number, focus = false) {
this.current = Math.max(0, Math.min(total - 1, index));
await this.updateComplete;
this.querySelector('.help-panels')!.scrollTop = 0;
const tab = this.querySelector<HTMLElement>(`#help-tab-${tutorialSteps[this.current].id}`)!;
tab.scrollIntoView({ block: 'nearest' });
if (focus) tab.focus();
}
#navKey(event: KeyboardEvent) {
const moves: Record<string, number> = {
ArrowUp: this.current - 1,
ArrowLeft: this.current - 1,
ArrowDown: this.current + 1,
ArrowRight: this.current + 1,
Home: 0,
End: total - 1,
};
if (!(event.key in moves)) return;
event.preventDefault();
void this.#select((moves[event.key] + total) % total, true);
}
render() {
const current = this.current;
const last = current === total - 1;
return html`<dialog
id="help"
class="help-dialog"
aria-labelledby="help-title"
@close=${() => editor.tutorialClosed()}
@keydown=${(event: KeyboardEvent) => {
// Keep scrolling keys inside the tutorial so editor shortcuts never swallow them.
if (/^(Arrow|Page|Home$|End$| $)/.test(event.key)) event.stopPropagation();
}}
>
<div class="dialog-head">
<h2 id="help-title">HOW TO BUILD A MAP</h2>
<button id="help-close" @click=${() => this.close()}>Close</button>
</div>
<div class="help-layout">
<div
class="help-tabs"
role="tablist"
aria-orientation="vertical"
aria-label="Tutorial steps"
@keydown=${(e: KeyboardEvent) => this.#navKey(e)}
>
${tutorialSteps.map(
(step, index) =>
html`<button
id="help-tab-${step.id}"
role="tab"
aria-controls="help-panel-${step.id}"
aria-label="Step ${index + 1}: ${step.title}"
aria-selected=${index === current}
tabindex=${index === current ? 0 : -1}
class=${index === current ? 'active' : ''}
@click=${() => this.#select(index)}
>
<span class="help-step-number">${index + 1}</span
><span class="help-step-title">${step.title}</span>
</button>`,
)}
</div>
<div class="help-panels">
${tutorialSteps.map(
(step, index) =>
html`<section
id="help-panel-${step.id}"
class="help-panel"
role="tabpanel"
aria-labelledby="help-tab-${step.id}"
tabindex="0"
?hidden=${index !== current}
>
<small class="help-progress">STEP ${index + 1} OF ${total}</small>
<h3>${step.title}</h3>
${step.body}
</section>`,
)}
</div>
</div>
<div class="help-footer">
<button
id="help-back"
?disabled=${current === 0}
@click=${() => this.#select(current - 1)}
>
Back
</button>
<button
id="help-next"
class="primary"
.textContent=${last ? 'Start editing' : 'Next'}
@click=${() => (last ? this.close() : this.#select(current + 1))}
></button>
</div>
</dialog>`;
}
}
customElements.define('bs-help-dialog', BsHelpDialog);
