HomeDeveloper docsscene.controls: Scene information control
Examples by kind 5 min read

scene.controls: Scene information control

Add a working button that reads the mounted scene and opens its dimensions and grid in the native modal.

What you will build

Add a working button that reads the mounted scene and opens its dimensions and grid in the native modal.

Surface: scene.controls · Mode: augment · Gravewright 0.3.0 / SDK 1.x.

When it mounts and who can use it

Select a scene and click Scene information. The mount supplies sceneId, width, height and grid. maps.get verifies the visible scene name; it never changes selection.

The owner installs the local ZIP under Modules → Install module → Install from ZIP. The GM then activates the exact version in the table extension list. This example requests no extra permissions or global handle; all reads use the mount’s supplied api. GMs and players receive only server-authorized data. Streamers do not receive this authenticated mount.

Download the complete package

Download source ZIP · manifest.json · main.mjs · styles.css · LICENSE.txt

Save all four files in the same folder. ZIP the folder contents, with manifest.json directly at the archive root. Do not include a wrapper folder, local state or private keys. Local upload is the owner’s trust decision; for catalog distribution, use the signing workflow in the module reference.

text
scene-controls/
  manifest.json
  main.mjs
  styles.css
  LICENSE.txt

manifest.json

json
{
  "id": "example.surface-scene-controls",
  "name": "Scene information control",
  "version": "1.0.1",
  "sdk": {
    "requires": ">=1.0.0 <2.0.0",
    "tested": "1.0.0"
  },
  "description": "Add a working button that reads the mounted scene and opens its dimensions and grid in the native modal.",
  "author": "Gravewright documentation",
  "license": "MIT",
  "entry": "main.mjs"
}

main.mjs

javascript
// Original documentation example. License: MIT. Target: Gravewright 0.3.0, SDK 1.x.
export default {
  start({ api, styles }) {
    styles.use("styles.css");
    api.i18n.register({
      "en": {
        "Scene information": "Scene information",
        "Close": "Close",
        "Grid": "Grid",
        "Refresh": "Refresh",
        "Loading…": "Loading…",
        "Error: {code}": "Error: {code}",
        "Scene information control": "Scene information control"
      },
      "pt-BR": {
        "Scene information": "Informações da cena",
        "Close": "Fechar",
        "Grid": "Grade",
        "Refresh": "Atualizar",
        "Loading…": "Carregando…",
        "Error: {code}": "Erro: {code}",
        "Scene information control": "Controle de informações da cena"
      }
    });
  },
  register({ register }) {
    register("scene.controls", { mode: "augment", mount: this.mount });
  },
  async mount({ root, context, api, storage, viewport, events, signal, onDispose }) {
    const el = (tag, text) => {
      const node = root.ownerDocument.createElement(tag);
      if (text !== undefined) node.textContent = text;
      return node;
    };
    const t = (key, params) => api.i18n.t(key, params);
    const panel = el("section"); panel.className = "gw-example";
    const heading = el("h3", t("Scene information control"));
    const output = el("pre"); output.setAttribute("role", "status");
    panel.append(heading, output); root.append(panel);
    onDispose(() => panel.remove());
    const report = error => {
      if (!signal.aborted) output.textContent = t("Error: {code}", { code: error.code ?? "unavailable" });
    };
    const button = el("button", t("Scene information"));
    button.type = "button";
    panel.append(button);
    for (const type of ["pointerdown", "mousedown", "click"]) {
      panel.addEventListener(type, event => event.stopPropagation(), { signal });
    }
    let modal;
    onDispose(() => modal?.close(true));
    button.addEventListener("click", async () => {
      button.disabled = true;
      try {
        const scene = await api.maps.get(context.sceneId, {}, { signal });
        if (signal.aborted) return;
        modal?.close(true);
        const section = el("section");
        section.className = "gw-example";
        section.setAttribute("aria-label", t("Scene information"));
        section.append(el("h3", scene.name), el("p", context.width + " × " + context.height + " · " + t("Grid") + ": " + context.grid.size + "px / " + context.grid.distance + " " + context.grid.unit));
        const close = el("button", t("Close"));
        close.type = "button";
        close.addEventListener("click", () => section.close(), { signal });
        section.append(close);
        modal = api.ui.openModal(section);
      } catch (error) { report(error); }
      finally { if (!signal.aborted) button.disabled = false; }
    }, { signal });
  },
  stop() {
    // Mount and module lifetimes already dispose styles, catalogs and subscriptions.
  }
};

styles.css

css
.gw-example { box-sizing: border-box; max-width: 640px; padding: var(--space-3); margin: var(--space-2); border: 1px solid var(--border); border-radius: var(--radius-md); color: var(--text); background: var(--surface); font: 14px/1.5 system-ui, sans-serif; }
.gw-example h3 { font-size: 16px; margin: var(--space-0) var(--space-0) var(--space-2); }
.gw-example pre { white-space: pre-wrap; overflow-wrap: anywhere; font: inherit; }
.gw-example button, .gw-example select, .gw-example input { color: inherit; background: var(--surface); font: inherit; padding: var(--space-1) var(--space-2); margin: var(--space-1); border: 1px solid var(--border); border-radius: var(--radius-sm); }
.gw-example button:focus-visible, .gw-example input:focus-visible, .gw-example select:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.gw-example--compact { padding: var(--space-1); }
.gw-example-center { position: absolute; transform: translate(-50%, -50%); pointer-events: none; color: var(--text); background: var(--surface); padding: var(--space-1); border-radius: var(--radius-sm); font: 14px/1.5 system-ui, sans-serif; }

Expected result and cleanup

Add a working button that reads the mounted scene and opens its dimensions and grid in the native modal.

The native interface remains usable. Closing a sheet, switching scenes, leaving the table or deactivating the module aborts the mount. Listeners use signal; events and owned elements are released through onDispose. Async reads check cancellation and discard old responses before rendering. Errors are displayed through error.code, including network failures.

Validate on a disposable table

  1. Pan, zoom and switch scenes. Open and close the modal with keyboard controls, then deactivate while it is open.

  2. Open/close three times, then deactivate while a read or modal is pending. Check that no old panel or error appears after disposal.

  3. Test empty state, long document names, keyboard navigation and a narrow viewport. Compare GM and player accounts.

  4. Check the browser console. These source examples have runtime contract tests; installation and visual behavior still require validation on the target host.

How to use AI for this surface

text
Adapt the scene.controls example for [concrete goal] on Gravewright 0.3.0.
Read registry.json, this surface’s context schema, frontend.json and the native services used.
Keep start/register/stop, mount-scoped api, signal, onDispose and native modals.
Provide the complete manifest and files. Do not use app.shell/inside.content in packages.
Do not invent payload fields, core RPG rules, permissions or APIs.
Test player/GM, empty data, abort, repeated mounting and this example’s behavior.
Explain limitations and which checks actually ran.

Continue

SDK · API · Examples by kind