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/components/bs-help-dialog.ts
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);