@nano133/nanoterra 0.8.0 → 0.8.1
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 +7 -0
- package/LICENSE +3 -0
- package/README.md +21 -19
- package/dist/admin/panel/dom.d.ts +2 -0
- package/dist/admin/panel/dom.js +6 -0
- package/dist/emotes/panel/mount.d.ts +5 -0
- package/dist/emotes/panel/mount.js +7 -3
- package/dist/friends/panel/mount.d.ts +5 -0
- package/dist/friends/panel/mount.js +5 -2
- package/dist/options/panel/mount.d.ts +5 -0
- package/dist/options/panel/mount.js +5 -3
- package/package.json +1 -1
- package/src/admin/panel/dom.ts +6 -0
- package/src/emotes/panel/mount.ts +9 -2
- package/src/friends/panel/mount.ts +8 -2
- package/src/options/panel/mount.ts +8 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.1 (2026-11-01)
|
|
4
|
+
|
|
5
|
+
- **A closed panel gives the keys back to the game.** The options panel, the friends panel and the emote wheel put the focus back on their key when they
|
|
6
|
+
closed; Space (a game's jump) then pressed that key and opened the panel again (in the starter, after the player moved Jump off Space). Now the focus
|
|
7
|
+
leaves the key and the panel. `focusAfterClose: "key"` (on `mountOptions`, `mountFriends` and `mountEmotes`) keeps the old way, for a page with no keys of
|
|
8
|
+
its own.
|
|
9
|
+
|
|
3
10
|
## 0.8.0 (2026-11-01)
|
|
4
11
|
|
|
5
12
|
- **An options menu for a game** (`@nano133/nanoterra/options`, `/options/panel` and `/options/style.css`). Wes: "build options menu and emotes in kit"
|
package/LICENSE
CHANGED
|
@@ -19,3 +19,6 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
21
|
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
The emote clips (src/emotes/clips.ts, dist/emotes/clips.js) and the emote icons (src/emotes/icons.webp) are
|
|
24
|
+
nano133's own work, made in Blender, and are covered by this licence like the code.
|
package/README.md
CHANGED
|
@@ -10,9 +10,9 @@ themselves. This package gives your game these things:
|
|
|
10
10
|
- **The player's character** (`/character`): the same toon body that the player has on the island, drawn with Three.js. Six bases, faces, hair, clothes, sizes and colours.
|
|
11
11
|
- **The looks ladder** (`/looks`): the rules of `looks.json`, the price steps, and what a change does to the players who own a look.
|
|
12
12
|
- **Held input that does not stick** (`/input`, since 0.3.1): lets go of the keys and the stick when the player leaves the page, so the character never walks on by itself, and pauses a free round.
|
|
13
|
-
- **A friends list** (`/friends`,
|
|
14
|
-
- **An options menu** (`/options`,
|
|
15
|
-
- **Emotes** (`/emotes`,
|
|
13
|
+
- **A friends list** (`/friends`, since 0.6.0): a Friends key that holds the player's Nanoterra friends (a nano133 window), or a full friends list of the game's own players on the game's own database, as the admin's Game card chooses.
|
|
14
|
+
- **An options menu** (`/options`, since 0.8.0): the game's settings as data, with defaults for each device, kept in the player's browser, and a ready Options key and panel (Basic and Advanced); common settings to opt into, and a section another module adds.
|
|
15
|
+
- **Emotes** (`/emotes`, since 0.8.0): Nano Island's eleven emotes on the player's character (the Wave, Cheer, Clap, Dance and seven more, made in Blender), and the emote wheel of eight that the player picks, which also plays a game's own animations.
|
|
16
16
|
|
|
17
17
|
It replaces `@nano133/character` 0.1.0, `@nano133/ticket` 0.1.0 and `@nano133/looks` 0.1.0
|
|
18
18
|
(see "Moving from the three old packages"). Sign-in, checkout and late returns stay in their own packages
|
|
@@ -71,15 +71,15 @@ Each path loads only what it needs. A page that reads a ticket loads no Three.js
|
|
|
71
71
|
| `@nano133/nanoterra/admin/d1`, `/admin/firestore` | the admin store for D1 (the starter's tables) or Firestore (a small interface, no Firebase dependency): `createD1Store`, `createFirestoreStore` (since 0.2.0) | no | 1.5 kB each |
|
|
72
72
|
| `@nano133/nanoterra/admin/panel` | the admin page, drawn in the browser into one element, and its stylesheet `@nano133/nanoterra/admin/style.css` (since 0.2.0) | no | 18.9 kB with the tabbed layout (10.6 kB on 0.3.0, 8.5 kB on 0.2.0) + 5.9 kB of CSS (minified) |
|
|
73
73
|
| `@nano133/nanoterra/admin/conformance` | test cases that a game runs against its own admin (a dev dependency in practice; since 0.2.0) | no | n/a |
|
|
74
|
-
| `@nano133/nanoterra/friends` | the game's own friends list: the handler, the rules, a memory store (server code;
|
|
75
|
-
| `@nano133/nanoterra/friends/d1`, `/friends/firestore` | the friends store for D1 (with the migration `/friends/d1.sql`) or Firestore (a small interface, no Firebase dependency): `createD1Friends`, `createFirestoreFriends` (
|
|
76
|
-
| `@nano133/nanoterra/friends/panel` | the Friends key and panel: the nano133 window, or the game's own list, and its stylesheet `@nano133/nanoterra/friends/style.css` (
|
|
77
|
-
| `@nano133/nanoterra/friends/conformance` | the friends rules as test cases, for a store a game writes itself (
|
|
78
|
-
| `@nano133/nanoterra/options` | the options store (no DOM), the common settings, `reflectOptions` and the panel's `mountOptions` (
|
|
79
|
-
| `@nano133/nanoterra/options/panel` | the Options key and panel alone, and its stylesheet `@nano133/nanoterra/options/style.css` (
|
|
80
|
-
| `@nano133/nanoterra/emotes` | the emotes' list, the wheel's eight places, the multiplayer message and the player's options: no DOM, no Three.js (
|
|
81
|
-
| `@nano133/nanoterra/emotes/player` | the emotes on the kit's character: `createEmotes`, `playRemote`, `loadEmotes` (
|
|
82
|
-
| `@nano133/nanoterra/emotes/wheel` | the Emotes key and the emote wheel: `mountEmotes`, and its stylesheet `@nano133/nanoterra/emotes/style.css` (
|
|
74
|
+
| `@nano133/nanoterra/friends` | the game's own friends list: the handler, the rules, a memory store (server code; since 0.6.0) | no | 3.7 kB |
|
|
75
|
+
| `@nano133/nanoterra/friends/d1`, `/friends/firestore` | the friends store for D1 (with the migration `/friends/d1.sql`) or Firestore (a small interface, no Firebase dependency): `createD1Friends`, `createFirestoreFriends` (since 0.6.0) | no | 3.0 kB each |
|
|
76
|
+
| `@nano133/nanoterra/friends/panel` | the Friends key and panel: the nano133 window, or the game's own list, and its stylesheet `@nano133/nanoterra/friends/style.css` (since 0.6.0) | no | 5.0 kB + 4.8 kB of CSS (minified) |
|
|
77
|
+
| `@nano133/nanoterra/friends/conformance` | the friends rules as test cases, for a store a game writes itself (since 0.6.0) | no | n/a |
|
|
78
|
+
| `@nano133/nanoterra/options` | the options store (no DOM), the common settings, `reflectOptions` and the panel's `mountOptions` (since 0.8.0) | no | 2.9 kB (`createOptions` alone), 8.7 kB (all) |
|
|
79
|
+
| `@nano133/nanoterra/options/panel` | the Options key and panel alone, and its stylesheet `@nano133/nanoterra/options/style.css` (since 0.8.0) | no | 2.3 kB of CSS (9.4 kB minified) |
|
|
80
|
+
| `@nano133/nanoterra/emotes` | the emotes' list, the wheel's eight places, the multiplayer message and the player's options: no DOM, no Three.js (since 0.8.0) | no | 1.4 kB |
|
|
81
|
+
| `@nano133/nanoterra/emotes/player` | the emotes on the kit's character: `createEmotes`, `playRemote`, `loadEmotes` (since 0.8.0) | yes | 2.9 kB; the clips load on the first emote, a chunk of their own: 84 kB, 18.8 kB gzip |
|
|
82
|
+
| `@nano133/nanoterra/emotes/wheel` | the Emotes key and the emote wheel: `mountEmotes`, and its stylesheet `@nano133/nanoterra/emotes/style.css` (since 0.8.0) | no | 5.9 kB + 7.4 kB of CSS (minified); the pictures (`/emotes/icons.webp`, 34 kB) load when the wheel first opens |
|
|
83
83
|
|
|
84
84
|
Measured 2026-10-26 (the friends paths 2026-10-30, the options and the emotes paths 2026-11-01): `esbuild --bundle --minify --format=esm --external:three`, then `gzip -9`, without Three.js.
|
|
85
85
|
All four paths together: 23.6 kB. A test of the package proves that the record, the ticket, the looks and the small part import no package at all.
|
|
@@ -672,7 +672,7 @@ width (a container query), not the window's: on a phone two cards a row and the
|
|
|
672
672
|
|
|
673
673
|
The wallet sign-in stays yours (`signIn`), because only your game can start a request in the ADMIN scope with its own cookie. The panel never signs anyone in.
|
|
674
674
|
|
|
675
|
-
**Light and dark** (
|
|
675
|
+
**Light and dark** (since 0.7.0). The top bar has a theme key (44 px, a half circle, a sun or a moon; its name says the choice: "Theme: Dark", "Theme: Device (dark
|
|
676
676
|
now)"). A press opens a menu of three: **Device** (the default: it follows the device's setting, `prefers-color-scheme`, and a live change of it), **Light** and **Dark**.
|
|
677
677
|
The signed-out screen has the same key. The choice is kept in this browser only, in `localStorage` under `n133-admin-theme`: no server call and no admin setting. A
|
|
678
678
|
browser that refuses storage (some private windows) reads Device, and a choice there holds for the page.
|
|
@@ -703,7 +703,7 @@ html[data-n133-theme="dark"] body.admin-page {
|
|
|
703
703
|
The pictures: a look's flat picture and the character previews (`createLookPreview` draws with a clear background) stand on the `stage` colour, which is a mid grey
|
|
704
704
|
blue in dark, so a dark outfit and the outlines still show. The panel draws no QR code.
|
|
705
705
|
|
|
706
|
-
## The friends list (`/friends`;
|
|
706
|
+
## The friends list (`/friends`; since 0.6.0)
|
|
707
707
|
|
|
708
708
|
A Friends key for the game, and a list behind it. The admin's **Game** card chooses which list (`friends` in the settings; Nanoterra by default):
|
|
709
709
|
|
|
@@ -826,7 +826,7 @@ no live connection: only Nanoterra's do.
|
|
|
826
826
|
**Questions.** Remove, Block and (in both modes) Join ask the player first, in the panel itself: the question and a key for each answer; Escape or closing the
|
|
827
827
|
panel answers no. The kit never opens a browser dialog. To ask in your game's own dialog, pass `confirm(question)`, which returns a boolean or a promise of one.
|
|
828
828
|
|
|
829
|
-
## The options menu (`/options`;
|
|
829
|
+
## The options menu (`/options`; since 0.8.0)
|
|
830
830
|
|
|
831
831
|
A game's settings as data, kept in the player's browser, and a ready Options key and panel. A game declares its settings; the kit resolves their defaults for
|
|
832
832
|
the device, keeps only what the player changed, and draws them in two tabs, **Basic** and **Advanced**. A game with its own menu uses the store alone.
|
|
@@ -907,8 +907,10 @@ sheet from the bottom on a phone held upright, from the right on a wide or sidew
|
|
|
907
907
|
(the arrow keys, Home and End; one tab alone has no tab bar); each tab draws its rows without a group first, then each group under its heading. **A change applies
|
|
908
908
|
at once** (there is no Save key), and a change from elsewhere shows in place: the rows are drawn once, a slider under a finger is never drawn again, and the panel
|
|
909
909
|
never scrolls back to the top. **Reset to defaults** asks in the panel ("Put every option back to its default?", Reset and Cancel), never in a browser dialog;
|
|
910
|
-
Escape answers the question first, then closes the panel. `key: false` draws no key, for a game that puts "Options" in its own pause menu and calls `open()
|
|
911
|
-
|
|
910
|
+
Escape answers the question first, then closes the panel. `key: false` draws no key, for a game that puts "Options" in its own pause menu and calls `open()`.
|
|
911
|
+
**When it closes, the focus leaves the key and the panel** (to the page), so Space and the game's other keys reach the game again: a focused key would take
|
|
912
|
+
Space and open the panel again. The friends panel and the emote wheel do the same. `focusAfterClose: "key"` gives the focus back to the key (or, with no
|
|
913
|
+
key, to what had it before), for a page with no keys of its own. `onOpen` and `onClose` let a game pause under the panel; `onRangeDone(key, value)` hears a slider let go (play a sound at the new
|
|
912
914
|
level). The panel holds the focus while it is open. Every key, switch, choice and slider is 44 px or more, and a row's label is 16 px.
|
|
913
915
|
|
|
914
916
|
**Light and dark.** The panel follows `data-n133-theme="light" | "dark"` on the page's `<html>` when the page sets it (the admin panel does), else the device's
|
|
@@ -923,7 +925,7 @@ returns the call that stops it:
|
|
|
923
925
|
html[data-n133-opt-reduced-motion="true"] .my-hud * { animation: none !important; transition: none !important; }
|
|
924
926
|
```
|
|
925
927
|
|
|
926
|
-
## Emotes (`/emotes`;
|
|
928
|
+
## Emotes (`/emotes`; since 0.8.0)
|
|
927
929
|
|
|
928
930
|
Nano Island's emotes for a game: the same eleven, on the same character, with the same wheel. **The Wave** is the character's own `wave()`; **Cheer, Clap,
|
|
929
931
|
Dance, Bow, Shrug, Laugh, Point, Sit, Facepalm and Spin** are clips made in Blender on the character's own skeleton (the island makes them from this package's
|
|
@@ -995,7 +997,7 @@ middle disc with the name of the place under the finger and **Edit**.
|
|
|
995
997
|
panel sets too, else `prefers-color-scheme`). It returns `open()`, `close()`, `isOpen`, `slots` and `destroy()`.
|
|
996
998
|
- Theme it with the same `--n133-*` properties as the admin panel (and their `--n133-dark-*` twins), and `--n133-emotes-z` for its z-index (default 1000).
|
|
997
999
|
|
|
998
|
-
The clips and the icons are part of the package
|
|
1000
|
+
The clips and the icons are nano133's own work, made in Blender for Nano Island, and are part of the package under its MIT licence.
|
|
999
1001
|
|
|
1000
1002
|
**The admin's switch.** The Game card has **"Allow Nanoterra emotes"** (`emotes` in the settings: `"on"`, the default, or `"off"`). Read it with
|
|
1001
1003
|
`loadGameSettings` and pass it to the wheel as `nanoterraEmotes`. When it is off, also do not call `playRemote` and do not send the player's emotes: the game shows no
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
type Attrs = Record<string, string | number | boolean | undefined | ((e: Event) => void)>;
|
|
2
2
|
type Child = Node | string | null | undefined | false;
|
|
3
3
|
export declare function h<K extends keyof HTMLElementTagNameMap>(tag: K, attrs?: Attrs, ...children: Child[]): HTMLElementTagNameMap[K];
|
|
4
|
+
/** Takes the keyboard focus out of `within` (to the page), so a game's own keys reach the game again. */
|
|
5
|
+
export declare function releaseFocus(within: Element): void;
|
|
4
6
|
/** "nano_1abc…wxyz" */
|
|
5
7
|
export declare const shortAddress: (a: string) => string;
|
|
6
8
|
export declare function copy(text: string, button: HTMLButtonElement): Promise<void>;
|
package/dist/admin/panel/dom.js
CHANGED
|
@@ -18,6 +18,12 @@ export function h(tag, attrs = {}, ...children) {
|
|
|
18
18
|
el.append(c);
|
|
19
19
|
return el;
|
|
20
20
|
}
|
|
21
|
+
/** Takes the keyboard focus out of `within` (to the page), so a game's own keys reach the game again. */
|
|
22
|
+
export function releaseFocus(within) {
|
|
23
|
+
const now = document.activeElement;
|
|
24
|
+
if (now instanceof HTMLElement && within.contains(now))
|
|
25
|
+
now.blur();
|
|
26
|
+
}
|
|
21
27
|
/** "nano_1abc…wxyz" */
|
|
22
28
|
export const shortAddress = (a) => `${a.slice(0, 10)}…${a.slice(-6)}`;
|
|
23
29
|
export async function copy(text, button) {
|
|
@@ -34,6 +34,11 @@ export type EmotesMountOptions = {
|
|
|
34
34
|
theme?: "light" | "dark" | "device";
|
|
35
35
|
/** Where the wheel is kept (default: the page's localStorage). null keeps it only for the life of the page. */
|
|
36
36
|
storage?: Pick<Storage, "getItem" | "setItem"> | null;
|
|
37
|
+
/**
|
|
38
|
+
* Where the keyboard focus goes when the wheel closes. "game" (the default): out of the key and the wheel, to the page, so Space and the game's
|
|
39
|
+
* other keys reach the game again (a focused key would take Space and open the wheel again). "key": back to the key, for a page with no keys of its own.
|
|
40
|
+
*/
|
|
41
|
+
focusAfterClose?: "game" | "key";
|
|
37
42
|
};
|
|
38
43
|
export type MountedEmotes = {
|
|
39
44
|
open(): void;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
//
|
|
10
10
|
// The list is Nanoterra's (each id names one of EMOTES: its name and picture come from the kit), the game's own ({ id, label, icon }), or both. The
|
|
11
11
|
// wheel only calls `onPlay(id)`: the game plays it (Nanoterra's on the kit's character: createEmotes(...).play, from "@nano133/nanoterra/emotes/player").
|
|
12
|
-
import { h } from "../../admin/panel/dom.js";
|
|
12
|
+
import { h, releaseFocus } from "../../admin/panel/dom.js";
|
|
13
13
|
import { icon } from "../../admin/panel/icons.js";
|
|
14
14
|
import { EMOTE_ICONS_URL } from "../icons.js";
|
|
15
15
|
import { EMOTES, emoteOf, isEmote } from "../list.js";
|
|
@@ -242,8 +242,12 @@ export function mountEmotes(root, options) {
|
|
|
242
242
|
layer.hidden = true;
|
|
243
243
|
layer.dataset.open = "false";
|
|
244
244
|
key.setAttribute("aria-expanded", "false");
|
|
245
|
-
if (
|
|
246
|
-
key.
|
|
245
|
+
if (options.focusAfterClose === "key") {
|
|
246
|
+
if (focusKey && !key.hidden)
|
|
247
|
+
key.focus();
|
|
248
|
+
}
|
|
249
|
+
else
|
|
250
|
+
releaseFocus(wrap);
|
|
247
251
|
}
|
|
248
252
|
editKey.addEventListener("click", () => {
|
|
249
253
|
edit = !edit;
|
|
@@ -21,6 +21,11 @@ export type FriendsMountOptions = {
|
|
|
21
21
|
fetch?: typeof fetch;
|
|
22
22
|
/** Asks the player a yes or no question (default: a question in the panel, with a key for each answer; never a browser dialog). */
|
|
23
23
|
confirm?: (question: string) => boolean | Promise<boolean>;
|
|
24
|
+
/**
|
|
25
|
+
* Where the keyboard focus goes when the panel closes. "game" (the default): out of the key and the panel, to the page, so Space and the game's
|
|
26
|
+
* other keys reach the game again (a focused key would take Space and open the panel again). "key": back to the key, for a page with no keys of its own.
|
|
27
|
+
*/
|
|
28
|
+
focusAfterClose?: "game" | "key";
|
|
24
29
|
};
|
|
25
30
|
export type MountedFriends = {
|
|
26
31
|
/** Sets where the player is (friends see it). Null: no place. */
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
//
|
|
12
12
|
// The panel is phone first: a sheet from the bottom on a phone held upright, from the side on a wide or sideways screen; every key is 44 px.
|
|
13
13
|
import { ApiError, createApi } from "../../admin/panel/api.js";
|
|
14
|
-
import { h } from "../../admin/panel/dom.js";
|
|
14
|
+
import { h, releaseFocus } from "../../admin/panel/dom.js";
|
|
15
15
|
import { icon } from "../../admin/panel/icons.js";
|
|
16
16
|
import { readWindowMessage, toWindow, whereText, windowUrl } from "./window.js";
|
|
17
17
|
export const FRIENDS_TIMING = { viewOpenMs: 20_000, viewClosedMs: 60_000, pulseMs: 45_000 };
|
|
@@ -69,7 +69,10 @@ export function mountFriends(root, options) {
|
|
|
69
69
|
panel.dataset.open = "false";
|
|
70
70
|
key.setAttribute("aria-expanded", "false");
|
|
71
71
|
onClose();
|
|
72
|
-
key
|
|
72
|
+
if (options.focusAfterClose === "key")
|
|
73
|
+
key.focus();
|
|
74
|
+
else
|
|
75
|
+
releaseFocus(wrap);
|
|
73
76
|
};
|
|
74
77
|
key.addEventListener("click", () => (isOpen ? close() : open()));
|
|
75
78
|
closeKey.addEventListener("click", close);
|
|
@@ -11,6 +11,11 @@ export type OptionsMountOptions = {
|
|
|
11
11
|
onRangeDone?: (key: string, value: number) => void;
|
|
12
12
|
/** Asks the player a yes or no question (default: a question in the panel, with a key for each answer; never a browser dialog). */
|
|
13
13
|
confirm?: (question: string) => boolean | Promise<boolean>;
|
|
14
|
+
/**
|
|
15
|
+
* Where the keyboard focus goes when the panel closes. "game" (the default): out of the key and the panel, to the page, so Space and the game's
|
|
16
|
+
* other keys reach the game again (a focused key would take Space and open the panel again). "key": back to the key, for a page with no keys of its own.
|
|
17
|
+
*/
|
|
18
|
+
focusAfterClose?: "game" | "key";
|
|
14
19
|
};
|
|
15
20
|
export type MountedOptions = {
|
|
16
21
|
open(): void;
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// "Reset to defaults" asks in the panel (a question and a key for each answer), never in a browser dialog. Escape answers the question first, then closes.
|
|
12
12
|
// The panel is phone first: a sheet from the bottom on a phone held upright, from the right on a wide or sideways screen; every key is 44 px or more.
|
|
13
13
|
// `key: false` draws no key, for a game that opens the panel from its own pause menu (`open()`).
|
|
14
|
-
import { h } from "../../admin/panel/dom.js";
|
|
14
|
+
import { h, releaseFocus } from "../../admin/panel/dom.js";
|
|
15
15
|
import { icon } from "../../admin/panel/icons.js";
|
|
16
16
|
import { keyLabel } from "../core.js";
|
|
17
17
|
export const TAB_WORDS = { basic: "Basic", advanced: "Advanced" };
|
|
@@ -273,8 +273,10 @@ export function mountOptions(root, store, options = {}) {
|
|
|
273
273
|
panel.dataset.open = scrim.dataset.open = "false";
|
|
274
274
|
key?.setAttribute("aria-expanded", "false");
|
|
275
275
|
options.onClose?.();
|
|
276
|
-
|
|
277
|
-
|
|
276
|
+
if (options.focusAfterClose === "key")
|
|
277
|
+
(key ?? (returnTo?.isConnected ? returnTo : null))?.focus();
|
|
278
|
+
else
|
|
279
|
+
releaseFocus(wrap);
|
|
278
280
|
};
|
|
279
281
|
key?.addEventListener("click", () => (isOpen ? close() : open()));
|
|
280
282
|
closeKey.addEventListener("click", close);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nano133/nanoterra",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Nanoterra for a creator's game, in one package: the player's sign-in ticket, the player's character (a toon body on the island's skeleton), the looks ladder, the admin, the friends list and an options menu.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
package/src/admin/panel/dom.ts
CHANGED
|
@@ -16,6 +16,12 @@ export function h<K extends keyof HTMLElementTagNameMap>(tag: K, attrs: Attrs =
|
|
|
16
16
|
return el;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/** Takes the keyboard focus out of `within` (to the page), so a game's own keys reach the game again. */
|
|
20
|
+
export function releaseFocus(within: Element): void {
|
|
21
|
+
const now = document.activeElement;
|
|
22
|
+
if (now instanceof HTMLElement && within.contains(now)) now.blur();
|
|
23
|
+
}
|
|
24
|
+
|
|
19
25
|
/** "nano_1abc…wxyz" */
|
|
20
26
|
export const shortAddress = (a: string): string => `${a.slice(0, 10)}…${a.slice(-6)}`;
|
|
21
27
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// The list is Nanoterra's (each id names one of EMOTES: its name and picture come from the kit), the game's own ({ id, label, icon }), or both. The
|
|
11
11
|
// wheel only calls `onPlay(id)`: the game plays it (Nanoterra's on the kit's character: createEmotes(...).play, from "@nano133/nanoterra/emotes/player").
|
|
12
12
|
|
|
13
|
-
import { h } from "../../admin/panel/dom.js";
|
|
13
|
+
import { h, releaseFocus } from "../../admin/panel/dom.js";
|
|
14
14
|
import { icon } from "../../admin/panel/icons.js";
|
|
15
15
|
import { EMOTE_ICONS_URL } from "../icons.js";
|
|
16
16
|
import { EMOTES, emoteOf, isEmote, type EmoteId } from "../list.js";
|
|
@@ -47,6 +47,11 @@ export type EmotesMountOptions = {
|
|
|
47
47
|
theme?: "light" | "dark" | "device";
|
|
48
48
|
/** Where the wheel is kept (default: the page's localStorage). null keeps it only for the life of the page. */
|
|
49
49
|
storage?: Pick<Storage, "getItem" | "setItem"> | null;
|
|
50
|
+
/**
|
|
51
|
+
* Where the keyboard focus goes when the wheel closes. "game" (the default): out of the key and the wheel, to the page, so Space and the game's
|
|
52
|
+
* other keys reach the game again (a focused key would take Space and open the wheel again). "key": back to the key, for a page with no keys of its own.
|
|
53
|
+
*/
|
|
54
|
+
focusAfterClose?: "game" | "key";
|
|
50
55
|
};
|
|
51
56
|
|
|
52
57
|
export type MountedEmotes = {
|
|
@@ -288,7 +293,9 @@ export function mountEmotes(root: HTMLElement, options: EmotesMountOptions): Mou
|
|
|
288
293
|
layer.hidden = true;
|
|
289
294
|
layer.dataset.open = "false";
|
|
290
295
|
key.setAttribute("aria-expanded", "false");
|
|
291
|
-
if (
|
|
296
|
+
if (options.focusAfterClose === "key") {
|
|
297
|
+
if (focusKey && !key.hidden) key.focus();
|
|
298
|
+
} else releaseFocus(wrap);
|
|
292
299
|
}
|
|
293
300
|
|
|
294
301
|
editKey.addEventListener("click", () => {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// The panel is phone first: a sheet from the bottom on a phone held upright, from the side on a wide or sideways screen; every key is 44 px.
|
|
13
13
|
|
|
14
14
|
import { ApiError, createApi, type Api } from "../../admin/panel/api.js";
|
|
15
|
-
import { h } from "../../admin/panel/dom.js";
|
|
15
|
+
import { h, releaseFocus } from "../../admin/panel/dom.js";
|
|
16
16
|
import { icon } from "../../admin/panel/icons.js";
|
|
17
17
|
import type { FriendRow, FriendView } from "../types.js";
|
|
18
18
|
import { readWindowMessage, toWindow, whereText, windowUrl, type ToWindow } from "./window.js";
|
|
@@ -39,6 +39,11 @@ export type FriendsMountOptions = {
|
|
|
39
39
|
fetch?: typeof fetch;
|
|
40
40
|
/** Asks the player a yes or no question (default: a question in the panel, with a key for each answer; never a browser dialog). */
|
|
41
41
|
confirm?: (question: string) => boolean | Promise<boolean>;
|
|
42
|
+
/**
|
|
43
|
+
* Where the keyboard focus goes when the panel closes. "game" (the default): out of the key and the panel, to the page, so Space and the game's
|
|
44
|
+
* other keys reach the game again (a focused key would take Space and open the panel again). "key": back to the key, for a page with no keys of its own.
|
|
45
|
+
*/
|
|
46
|
+
focusAfterClose?: "game" | "key";
|
|
42
47
|
};
|
|
43
48
|
|
|
44
49
|
export type MountedFriends = {
|
|
@@ -108,7 +113,8 @@ export function mountFriends(root: HTMLElement, options: FriendsMountOptions): M
|
|
|
108
113
|
panel.dataset.open = "false";
|
|
109
114
|
key.setAttribute("aria-expanded", "false");
|
|
110
115
|
onClose();
|
|
111
|
-
key.focus();
|
|
116
|
+
if (options.focusAfterClose === "key") key.focus();
|
|
117
|
+
else releaseFocus(wrap);
|
|
112
118
|
};
|
|
113
119
|
key.addEventListener("click", () => (isOpen ? close() : open()));
|
|
114
120
|
closeKey.addEventListener("click", close);
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// The panel is phone first: a sheet from the bottom on a phone held upright, from the right on a wide or sideways screen; every key is 44 px or more.
|
|
13
13
|
// `key: false` draws no key, for a game that opens the panel from its own pause menu (`open()`).
|
|
14
14
|
|
|
15
|
-
import { h } from "../../admin/panel/dom.js";
|
|
15
|
+
import { h, releaseFocus } from "../../admin/panel/dom.js";
|
|
16
16
|
import { icon } from "../../admin/panel/icons.js";
|
|
17
17
|
import { keyLabel, type ChoiceDef, type KeyDef, type OptionDef, type OptionsStore, type OptionValue, type RangeDef, type Tab, type ToggleDef } from "../core.js";
|
|
18
18
|
|
|
@@ -28,6 +28,11 @@ export type OptionsMountOptions = {
|
|
|
28
28
|
onRangeDone?: (key: string, value: number) => void;
|
|
29
29
|
/** Asks the player a yes or no question (default: a question in the panel, with a key for each answer; never a browser dialog). */
|
|
30
30
|
confirm?: (question: string) => boolean | Promise<boolean>;
|
|
31
|
+
/**
|
|
32
|
+
* Where the keyboard focus goes when the panel closes. "game" (the default): out of the key and the panel, to the page, so Space and the game's
|
|
33
|
+
* other keys reach the game again (a focused key would take Space and open the panel again). "key": back to the key, for a page with no keys of its own.
|
|
34
|
+
*/
|
|
35
|
+
focusAfterClose?: "game" | "key";
|
|
31
36
|
};
|
|
32
37
|
|
|
33
38
|
export type MountedOptions = {
|
|
@@ -318,8 +323,8 @@ export function mountOptions(root: HTMLElement, store: OptionsStore, options: Op
|
|
|
318
323
|
panel.dataset.open = scrim.dataset.open = "false";
|
|
319
324
|
key?.setAttribute("aria-expanded", "false");
|
|
320
325
|
options.onClose?.();
|
|
321
|
-
|
|
322
|
-
|
|
326
|
+
if (options.focusAfterClose === "key") (key ?? (returnTo?.isConnected ? returnTo : null))?.focus();
|
|
327
|
+
else releaseFocus(wrap);
|
|
323
328
|
};
|
|
324
329
|
key?.addEventListener("click", () => (isOpen ? close() : open()));
|
|
325
330
|
closeKey.addEventListener("click", close);
|