@runmu.sh/ext-scene 1.0.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.0
4
+
5
+ - Present now comes before Exits, and a room with no exits shows no Exits heading at all, as on Underspire.
6
+ - The Scene adds itself the first time a session knows its room, however the game sends it (GMCP, MSDP or another extension), not only on GMCP Room messages. A new **Show panel** setting per world (off / auto / on) controls it.
7
+ - Someone arriving or an item dropping no longer scrolls the panel back to the top; moving to another room still does.
8
+ - Right-click or long-press an exit or a room item for the actions other extensions offer on them.
9
+ - Exit buttons are at least 24px tall and show the standard focus ring.
10
+ - Needs μClient with extension API 1.12. It no longer asks to read game output; it only sends the exits you click.
11
+
3
12
  ## 1.0.3
4
13
 
5
14
  - No longer bundled with μClient: `builtin` and `defaultEnabled` are gone from the manifest. Install Scene from Extensions → Discover like any other extension. The panel is unchanged.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Scene (`@runmu.sh/ext-scene`, id `scene`)
2
2
 
3
- The Scene panel of [μClient](https://runmu.sh) (R-SCENE): the room you are in, its exits and who is there.
3
+ The Scene panel of [μClient](https://runmu.sh): the room you are in, who is there and its exits.
4
4
 
5
- First-party, on the [marketplace](https://runmu.sh/marketplace/x/scene). Install it from **☰ → Extensions → Discover**; once installed it is on in every world and takes the Scene slot of the default layout (right top). Turn it off per world in **Extensions → Installed**.
5
+ First-party, on the [marketplace](https://runmu.sh/marketplace/x/scene). Install it from **☰ → Extensions → Discover**; once installed it is on in every world and adds itself (right top) the first time a session knows its room. Turn it off per world in **Extensions → Installed**.
6
6
 
7
7
  Until 2026-09-29 the panel was part of the client core, and until 1.0.3 it was bundled with the client. The core still tracks the room for every session; this extension only draws it.
8
8
 
@@ -20,17 +20,31 @@ The host keeps one scene per session and fills it from:
20
20
  Package names are case-insensitive. Entering a new room clears Present. The room title is also the session's location (the world switcher shows it).
21
21
 
22
22
  ## What it looks like
23
- On `--bg-elev`: the title in uppercase `--accent-bright` with a glow and a `--border-bright` rule under it, the area in small faint capitals, the atmosphere in italic `--fg-dim`, the description, and the pose line in italic with a 2px left rule. Then `┤EXITS├` and `┤PRESENT├` section heads (gold, the host's `.sec-head`) over `▸` lists. Each exit is a button: clicking it sends the direction as if typed. A hostile item carries a small `hostile` tag in `--alert`. An empty list says `none`, and before any room arrives the panel says NO ROOM YET (an upright, uppercase, tracked faint label).
23
+ On `--bg-elev`: the title in uppercase `--accent-bright` with a glow and a `--border-bright` rule under it, the area in small faint capitals, the atmosphere in italic `--fg-dim`, the description, and the pose line in italic with a 2px left rule. Then `┤PRESENT├` over a `▸` list of the people and room items present (a hostile item carries a small `hostile` tag in `--alert`; nobody reads `none`), and, when the room has exits, `┤EXITS├` over a `▸` list. Each exit is a button: clicking it sends the direction as if typed. Before any room arrives the panel says NO ROOM YET (an upright, uppercase, tracked faint label).
24
24
 
25
25
  The panel is the region `scene`, so **Alt+R** (*Go to scene*) focuses it.
26
26
 
27
27
  ## Behaviour
28
28
  - Panel `scene`, singleton, default position right top, Views order 10 (after Terminal, before Channels).
29
- - The first `Room.*` package of a session adds the panel if it is not open (R-AUTO-PANELS), once per world on this device, so a Scene you closed stays closed.
30
- - It sends nothing to the game except the exits you click. It adds no `Core.Supports` of its own (the backend's base set already has `Room 1`).
29
+ - **Show panel: auto** (per world, on the extension's settings page: off / auto / on). On auto the Scene joins Views and adds itself the first time a session knows its room, whatever the room came from (GMCP, MSDP, another extension); once per world on this device, so a Scene you closed stays closed.
30
+ - A redraw in the same room (someone arrives, an item drops) keeps your scroll position; moving to another room scrolls back to the top. When the game sends no room id, every update counts as a move.
31
+ - Right-click (or long-press) an exit or a room item for the menus other extensions add to them (`scene-exit` and `scene-item` targets).
32
+ - It sends nothing to the game except the exits you click, and reads no GMCP itself: the room adapters fill the scene.
33
+
34
+ ## Contract
35
+ The extension reads no GMCP and sends none, so it declares no GMCP packages or payload contracts; the room adapters declare `Room 1` and feed `mu.scene` (`SceneView`). Capability: `send-commands` (the exit buttons).
36
+
37
+ ## Exported API (`@runmu.sh/ext-scene`, types in `src/types.ts`)
38
+ No `ctx.exports` API. The module exports, for tests and other renderers:
39
+
40
+ | Export | |
41
+ |---|---|
42
+ | `renderScene(scene, go, opts?)` | The panel's children for one `SceneView` (`null` or `known: false` → NO ROOM YET). `go(dir)` is called on an exit click. `opts.css` passes the host classes (`mu.ui.css`), `opts.target(el, what)` is called for each exit button and present row (to mark context targets). The 1.0 two-argument form still works. |
43
+ | `presentOf(scene)` | People first, then room items, each name once: `{ name, hostile? }[]`. |
44
+ | `COPY`, `SCENE_CSS` | The drawn copy and the stylesheet (every rule under `.ext-panel[data-ext="scene"] .mu-scene`). |
31
45
 
32
46
  ## SDK
33
- Uses `mu.panels.register` (with `order`, SDK 1.4), `mu.scene.watch` (SDK 1.4), `mu.sessions.send`, `mu.gmcp.on`, `mu.panels.autoAdd` and `mu.ui.style`. It exports no API. `renderScene` and `presentOf` are exported for tests.
47
+ SDK 1.12: `mu.panels.register` with `show: 'auto'`, `mu.panels.touch`, `mu.sessions.each`, `mu.scene.watch` (and `SceneView.id`), `mu.sessions.send(dir, { sid })`, `mu.menus.target`, `h` and `mu.ui.css`, `mu.ui.style`.
34
48
 
35
49
  ## Develop
36
50
  Made with `npm create @runmu.sh/extension` ([the quickstart](https://runmu.sh/docs/extensions/quickstart)).
@@ -39,6 +53,7 @@ Made with `npm create @runmu.sh/extension` ([the quickstart](https://runmu.sh/do
39
53
  npm install
40
54
  npm run build # src/index.ts → dist/index.js, then the manifest check
41
55
  npm run typecheck # against @muclient/sdk (npm:@runmu.sh/sdk)
56
+ npm test # the headless host (@runmu.sh/dev/test) with happy-dom
42
57
  npm run dev # dev server on http://localhost:5199/ with hot reload
43
58
  ```
44
59
 
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/index.ts
2
- import { defineExtension } from "@muclient/sdk";
2
+ import { defineExtension, h } from "@muclient/sdk";
3
3
  var COPY = {
4
4
  title: "Scene",
5
5
  awaiting: "No room yet",
@@ -9,98 +9,128 @@ var COPY = {
9
9
  hostile: "hostile",
10
10
  go: (dir) => `go ${dir}`
11
11
  };
12
+ var R = '.ext-panel[data-ext="scene"] .mu-scene';
12
13
  var SCENE_CSS = `
13
- .mu-scene { height: 100%; overflow-y: auto; background: var(--bg-elev); padding: .7rem .85rem 1rem; font-size: .9rem; color: var(--fg); box-sizing: border-box; }
14
- .mu-scene:focus { outline: none; }
15
- .mu-scene .title { margin: 0 0 .5rem; padding-bottom: .4rem; font-weight: 400; font-size: .9rem; letter-spacing: .16em; text-transform: uppercase; color: var(--accent-bright); border-bottom: 1px solid var(--border-bright); }
16
- .mu-scene .area { font-size: .66rem; letter-spacing: .2em; text-transform: uppercase; color: var(--fg-faint); margin: -.2rem 0 .4rem; }
17
- .mu-scene .atmo { color: var(--fg-dim); font-style: italic; margin: 0 0 .4rem; }
18
- .mu-scene .desc { margin: 0 0 .4rem; white-space: pre-wrap; }
19
- .mu-scene .pose { color: var(--fg-dim); font-style: italic; margin: 0 0 .5rem; padding-left: .5rem; border-left: 2px solid var(--border-bright); }
20
- .mu-scene .list { list-style: none; margin: 0; padding: 0; }
21
- .mu-scene .list li { padding: .08rem 0; color: var(--fg); }
22
- .mu-scene .sigil { color: var(--accent); margin-right: .6ch; }
23
- .mu-scene .exit { color: inherit; text-align: left; transition: color .12s ease; }
24
- .mu-scene .exit:hover { color: var(--accent-bright); }
25
- .mu-scene .exit:focus-visible { outline: 2px solid var(--accent-bright); outline-offset: 1px; }
26
- .mu-scene .hostile { color: var(--alert); font-size: .72rem; margin-left: 6px; }
27
- .mu-scene .none { margin: .1rem 0; font-style: normal; }
28
- .mu-scene .awaiting { margin: 0; color: var(--fg-faint); font-style: normal; font-size: .64rem; letter-spacing: .14em; text-transform: uppercase; }
14
+ ${R} { height: 100%; overflow-y: auto; background: var(--bg-elev); padding: .7rem .85rem 1rem; font-family: var(--font-mono); font-size: .9rem; color: var(--fg); box-sizing: border-box; }
15
+ ${R}:focus { outline: none; }
16
+ ${R} .title { margin: 0 0 .5rem; padding-bottom: .4rem; font-weight: 400; font-size: .9rem; letter-spacing: .16em; text-transform: uppercase; color: var(--accent-bright); border-bottom: 1px solid var(--border-bright); }
17
+ ${R} .area { font-size: .66rem; letter-spacing: .2em; text-transform: uppercase; color: var(--fg-faint); margin: -.2rem 0 .4rem; }
18
+ ${R} .atmo { color: var(--fg-dim); font-style: italic; margin: 0 0 .4rem; }
19
+ ${R} .desc { margin: 0 0 .4rem; white-space: pre-wrap; }
20
+ ${R} .pose { color: var(--fg-dim); font-style: italic; margin: 0 0 .5rem; padding-left: .5rem; border-left: 2px solid var(--border-bright); }
21
+ ${R} .list { list-style: none; margin: 0; padding: 0; }
22
+ ${R} .list li { padding: .08rem 0; color: var(--fg); }
23
+ ${R} .sigil { color: var(--accent); margin-right: .6ch; }
24
+ ${R} .exit { display: inline-flex; align-items: center; min-height: 24px; color: inherit; text-align: left; transition: color .12s ease; }
25
+ ${R} .exit:hover { color: var(--accent-bright); }
26
+ ${R} .exit:focus-visible { outline: 2px solid var(--accent-bright); outline-offset: -2px; }
27
+ ${R} .hostile { color: var(--alert); font-size: .72rem; margin-left: 6px; }
28
+ ${R} .none { margin: .1rem 0; font-style: normal; color: var(--fg-faint); }
29
+ ${R} .awaiting { margin: 0; color: var(--fg-faint); font-style: normal; font-size: .64rem; letter-spacing: .14em; text-transform: uppercase; }
29
30
  `;
30
- function el(tag, props = {}, ...kids) {
31
- const e = document.createElement(tag);
32
- for (const [k, v] of Object.entries(props)) {
33
- if (v === void 0 || v === false) continue;
34
- if (k === "class") e.className = String(v);
35
- else e.setAttribute(k, v === true ? "" : String(v));
36
- }
37
- for (const k of kids) if (k !== null && k !== false) e.append(k);
38
- return e;
39
- }
31
+ var HOST_CSS = { secHead: "sec-head", secClose: "sec-close", empty: "empty", glow: "glow-text" };
40
32
  function presentOf(s) {
41
33
  const seen = /* @__PURE__ */ new Set();
42
34
  const out = [];
43
- for (const n of s.present) if (!seen.has(n)) {
35
+ for (const n of s.present ?? []) if (!seen.has(n)) {
44
36
  seen.add(n);
45
37
  out.push({ name: n });
46
38
  }
47
- for (const it of s.items) if (!seen.has(it.name)) {
39
+ for (const it of s.items ?? []) if (!seen.has(it.name)) {
48
40
  seen.add(it.name);
49
41
  out.push(it.hostile ? { name: it.name, hostile: true } : { name: it.name });
50
42
  }
51
43
  return out;
52
44
  }
53
- var sectionHead = (label) => el("div", { class: "sec-head", role: "heading", "aria-level": "3" }, label, el("span", { class: "sec-close", "aria-hidden": "true" }));
54
- var sigil = () => el("span", { class: "sigil", "aria-hidden": "true" }, "\u25B8");
55
- function renderScene(s, go) {
56
- if (!s || !s.known) return [el("p", { class: "empty awaiting", "data-testid": "scene-empty" }, COPY.awaiting)];
57
- const out = [el("h3", { class: "title glow-text", role: "heading", "aria-level": "2", "data-testid": "scene-title" }, s.title)];
58
- if (s.area) out.push(el("div", { class: "area", "data-testid": "scene-area" }, s.area));
59
- if (s.atmosphere) out.push(el("p", { class: "atmo" }, s.atmosphere));
60
- if (s.desc) out.push(el("p", { class: "desc" }, s.desc));
61
- if (s.pose) out.push(el("p", { class: "pose" }, s.pose));
62
- out.push(sectionHead(COPY.exits));
45
+ var sigil = () => h("span", { class: "sigil", "aria-hidden": "true" }, "\u25B8");
46
+ function renderScene(s, go, opts = {}) {
47
+ const c = { ...HOST_CSS, ...opts.css };
48
+ const mark = (el, what) => {
49
+ opts.target?.(el, what);
50
+ return el;
51
+ };
52
+ if (!s || !s.known) return [h("p", { class: `${c.empty} awaiting`, "data-testid": "scene-empty" }, COPY.awaiting)];
53
+ const head = (label) => h("div", { class: c.secHead, role: "heading", "aria-level": "3" }, label, h("span", { class: c.secClose, "aria-hidden": "true" }));
54
+ const out = [h("h3", { class: `title ${c.glow}`, role: "heading", "aria-level": "2", "data-testid": "scene-title" }, s.title)];
55
+ if (s.area) out.push(h("div", { class: "area", "data-testid": "scene-area" }, s.area));
56
+ if (s.atmosphere) out.push(h("p", { class: "atmo" }, s.atmosphere));
57
+ if (s.desc) out.push(h("p", { class: "desc" }, s.desc));
58
+ if (s.pose) out.push(h("p", { class: "pose" }, s.pose));
59
+ out.push(head(COPY.present));
60
+ const items = new Map((s.items ?? []).map((it) => [it.name, it]));
61
+ const people = new Set(s.present ?? []);
62
+ const present = presentOf(s);
63
+ if (present.length) {
64
+ const ul = h("ul", { class: "list", "data-testid": "scene-present" });
65
+ for (const p of present) {
66
+ const it = people.has(p.name) ? void 0 : items.get(p.name);
67
+ const li = h("li", { class: "entity-ref", "data-item-id": it?.id || void 0 }, sigil(), p.name, p.hostile ? h("span", { class: "hostile" }, COPY.hostile) : null);
68
+ ul.append(it ? mark(li, { item: it }) : mark(li, { person: p.name }));
69
+ }
70
+ out.push(ul);
71
+ } else out.push(h("p", { class: `${c.empty} none` }, COPY.none));
63
72
  if (s.exits.length) {
64
- const ul = el("ul", { class: "list", "data-testid": "scene-exits" });
73
+ out.push(head(COPY.exits));
74
+ const ul = h("ul", { class: "list", "data-testid": "scene-exits" });
65
75
  for (const dir of s.exits) {
66
- const b = el("button", { class: "exit", type: "button", title: COPY.go(dir) }, sigil(), dir);
67
- b.addEventListener("click", () => go(dir));
68
- ul.append(el("li", {}, b));
76
+ const b = h("button", { class: "exit", type: "button", title: COPY.go(dir), onclick: () => go(dir) }, sigil(), dir);
77
+ ul.append(h("li", {}, mark(b, { exit: dir })));
69
78
  }
70
79
  out.push(ul);
71
- } else out.push(el("p", { class: "empty none" }, COPY.none));
72
- out.push(sectionHead(COPY.present));
73
- const present = presentOf(s);
74
- if (present.length) {
75
- const ul = el("ul", { class: "list", "data-testid": "scene-present" });
76
- for (const p of present) ul.append(el("li", {}, sigil(), p.name, p.hostile ? el("span", { class: "hostile" }, COPY.hostile) : null));
77
- out.push(ul);
78
- } else out.push(el("p", { class: "empty none" }, COPY.none));
80
+ }
79
81
  return out;
80
82
  }
81
83
  var index_default = defineExtension({
82
84
  activate(ctx) {
83
85
  const mu = ctx.mu;
84
86
  mu.ui.style(SCENE_CSS);
87
+ const css = { secHead: mu.ui.css.secHead, secClose: mu.ui.css.secClose, empty: mu.ui.css.empty, glow: mu.ui.css.glow };
85
88
  const mount = (host, pc) => {
86
- const root = el("section", { class: "mu-scene", "aria-label": COPY.title, "data-focus-region": "scene", tabindex: "-1", "data-testid": "scene" });
89
+ const root = h("section", { class: "mu-scene", "aria-label": COPY.title, "data-focus-region": "scene", tabindex: "-1", "data-testid": "scene" });
87
90
  host.append(root);
88
91
  const sid = pc.sid;
89
- const go = (dir) => {
90
- if (sid) void mu.sessions.send(dir, sid);
91
- };
92
92
  if (!sid) {
93
- root.replaceChildren(...renderScene(null, go));
93
+ root.replaceChildren(...renderScene(null, () => {
94
+ }, { css }));
94
95
  return () => root.remove();
95
96
  }
96
- const off = mu.scene.watch((s) => root.replaceChildren(...renderScene(s, go)), sid);
97
+ const go = (dir) => {
98
+ void mu.sessions.send(dir, { sid });
99
+ };
100
+ let targets = [];
101
+ const untarget = () => {
102
+ for (const d of targets) d();
103
+ targets = [];
104
+ };
105
+ const target = (el, what) => {
106
+ if ("exit" in what) targets.push(mu.menus.target(el, { kind: "scene-exit", sid, exit: what.exit }));
107
+ else if ("item" in what) targets.push(mu.menus.target(el, { kind: "scene-item", sid, item: what.item }));
108
+ };
109
+ let room = null;
110
+ const off = mu.scene.watch((s) => {
111
+ const moved = !s.known || !s.id || s.id !== room;
112
+ room = s.known && s.id ? s.id : null;
113
+ const top = root.scrollTop;
114
+ untarget();
115
+ root.replaceChildren(...renderScene(s, go, { css, target }));
116
+ root.scrollTop = moved ? 0 : top;
117
+ }, sid);
97
118
  return () => {
98
119
  off();
120
+ untarget();
99
121
  root.remove();
100
122
  };
101
123
  };
102
- mu.panels.register({ id: "scene", title: COPY.title, singleton: true, defaultPosition: "right-top", order: 10, mount });
103
- mu.gmcp.on("Room", (_d, { sid }) => mu.panels.autoAdd("scene", sid));
124
+ mu.panels.register({ id: "scene", title: COPY.title, singleton: true, defaultPosition: "right-top", order: 10, show: "auto", mount });
125
+ ctx.subscriptions.push(mu.sessions.each((s) => {
126
+ let done = false;
127
+ return mu.scene.watch((v) => {
128
+ if (!done && v.known) {
129
+ done = true;
130
+ mu.panels.touch("scene", s.id);
131
+ }
132
+ }, s.id);
133
+ }));
104
134
  }
105
135
  });
106
136
  export {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@runmu.sh/ext-scene",
3
- "version": "1.0.3",
4
- "description": "The Scene panel: the room you are in (title, area, atmosphere, description, pose), its exits as buttons and who is present, from GMCP Room.*, Char.Items.*, MSDP or another extension.",
3
+ "version": "1.1.0",
4
+ "description": "The Scene panel: the room you are in (title, area, atmosphere, description, pose), who is present and its exits as buttons, from GMCP Room.*, Char.Items.*, MSDP or another extension.",
5
5
  "keywords": [
6
6
  "scene",
7
7
  "room",
@@ -23,6 +23,7 @@
23
23
  "exports": "./dist/index.js",
24
24
  "files": [
25
25
  "dist",
26
+ "src/types.ts",
26
27
  "README.md",
27
28
  "CHANGELOG.md"
28
29
  ],
@@ -30,23 +31,25 @@
30
31
  "build": "node scripts/build.mjs",
31
32
  "dev": "node scripts/dev.mjs",
32
33
  "typecheck": "tsc --noEmit",
33
- "check": "node scripts/build.mjs --check"
34
+ "check": "node scripts/build.mjs --check",
35
+ "test": "node --test test/*.test.mjs"
34
36
  },
35
37
  "devDependencies": {
36
- "@muclient/sdk": "npm:@runmu.sh/sdk@^1.6.0",
37
- "@runmu.sh/dev": "^0.1.0",
38
+ "@muclient/sdk": "npm:@runmu.sh/sdk@^1.12.0",
39
+ "@runmu.sh/dev": "^0.2.0",
38
40
  "esbuild": "^0.28.2",
39
- "typescript": "^5.9.0"
41
+ "typescript": "^5.9.0",
42
+ "happy-dom": "^20.0.0"
40
43
  },
41
44
  "engines": {
42
45
  "node": ">=20"
43
46
  },
44
47
  "muclient": {
45
48
  "id": "scene",
46
- "api": "^1.4",
49
+ "api": "^1.12",
47
50
  "source": "src/index.ts",
48
51
  "displayName": "Scene",
49
- "description": "The Scene panel: the current room, its exits as buttons and who is present, from GMCP Room.*, Char.Items.*, MSDP or another extension.",
52
+ "description": "The Scene panel: the current room, who is present and its exits as buttons, from GMCP Room.*, Char.Items.*, MSDP or another extension.",
50
53
  "categories": [
51
54
  "interface",
52
55
  "mapping"
@@ -55,17 +58,17 @@
55
58
  "panels": [
56
59
  {
57
60
  "id": "scene",
58
- "title": "Scene"
61
+ "title": "Scene",
62
+ "order": 10,
63
+ "defaultPosition": "right-top",
64
+ "singleton": true
59
65
  }
60
- ],
61
- "gmcp": [
62
- "Room 1"
63
66
  ]
64
67
  },
65
68
  "capabilities": [
66
- "read-output",
67
69
  "send-commands"
68
70
  ],
69
71
  "publisher": "bitmuse"
70
- }
72
+ },
73
+ "types": "./src/types.ts"
71
74
  }
package/src/types.ts ADDED
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The exported API of `@runmu.sh/ext-scene` (id `scene`). The extension reads no GMCP and sends none: it draws the
3
+ * host's scene model (`mu.scene.watch`, `SceneView`), which the room adapters fill from GMCP `Room.*`,
4
+ * `Char.Items.*`, MSDP and other extensions (`mu.scene.set`). This file and the README change together.
5
+ */
6
+ import type { SceneView } from '@muclient/sdk';
7
+
8
+ /** One row of ┤PRESENT├: a person (from `present`) or a room item (from `items`). */
9
+ export interface PresentEntry {
10
+ name: string;
11
+ /** Set on hostile room items; drawn as a small `hostile` tag. */
12
+ hostile?: boolean;
13
+ /** The room item's id, when the row is an item with one (people have none). @since 1.1.0 */
14
+ id?: string;
15
+ }
16
+
17
+ /** The class names `renderScene` uses for the host primitives; defaults are the host's (`mu.ui.css`). @since 1.1.0 */
18
+ export interface SceneCss {
19
+ secHead: string;
20
+ secClose: string;
21
+ empty: string;
22
+ glow: string;
23
+ }
24
+
25
+ /** Options for {@link renderScene} beyond the 1.0 signature. @since 1.1.0 */
26
+ export interface RenderOptions {
27
+ /** `mu.ui.css` (or the part of it used). */
28
+ css?: Partial<SceneCss>;
29
+ /**
30
+ * Called for every exit button and every present row as it is created, so the caller can mark it as a context
31
+ * target (`mu.menus.target`). Return a function to undo it; those run on the next render or unmount.
32
+ */
33
+ target?: (el: HTMLElement, what: { exit: string } | { item: SceneView['items'][number] } | { person: string }) => void | (() => void);
34
+ }
35
+
36
+ /** Drawn copy. */
37
+ export interface SceneCopy {
38
+ title: string;
39
+ awaiting: string;
40
+ present: string;
41
+ exits: string;
42
+ none: string;
43
+ hostile: string;
44
+ go(dir: string): string;
45
+ }