@nano133/nanoterra 0.9.0 → 0.9.3

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.
@@ -0,0 +1,89 @@
1
+ import type { Hat } from "../../looks/look.js";
2
+ export type ShopWords = {
3
+ /** The sheet's title, the Looks key's word and the sheet's name for a screen reader. The option `title` wins over it. */
4
+ title: string;
5
+ /** The close key's name for a screen reader. */
6
+ close: string;
7
+ /** The tabs' name for a screen reader. */
8
+ kinds: string;
9
+ /** The outfits' tab. */
10
+ outfits: string;
11
+ /** The tab of a wear slot of the game's own ("shovel"). The option `slots` wins over it. */
12
+ slotTab: (slot: string) => string;
13
+ /** The plain choice of a wear slot ("No shovel"). The option `slots` wins over it. */
14
+ slotPlain: (slot: string) => string;
15
+ /** The line under an outfit: what it changes (its hat, or the colour only). */
16
+ outfitLine: (hat: Hat | null) => string;
17
+ /** The line under an item of a wear slot. */
18
+ itemLine: (slot: string) => string;
19
+ /** Added to the line of a free look, and the plain choice's line. */
20
+ free: string;
21
+ /** Added to the line of a look the player owns. */
22
+ yours: string;
23
+ wear: string;
24
+ wearing: string;
25
+ /** The Buy key, with the look's price ("$0.25"). */
26
+ buy: (price: string) => string;
27
+ /** Under the list. */
28
+ looksNote: string;
29
+ /** Under the list and at Buy, while nobody is signed in. */
30
+ signInFirst: string;
31
+ /** The title of Buy and of the pay sheet. */
32
+ buyTitle: (look: string) => string;
33
+ making: string;
34
+ /** An order that did not start, with no words from the server. */
35
+ orderFailed: string;
36
+ /** The words of a refusal: the server's own (`text`, in English) and its HTTP status (0: no answer). Default: the server's words. */
37
+ refusal: (text: string, status: number) => string;
38
+ /** The key back to the looks. */
39
+ back: string;
40
+ /** The key that signs in again after "Sign in first." (401). */
41
+ signIn: string;
42
+ /** The line under the title. */
43
+ lookLine: (look: string) => string;
44
+ /** The amount in XNO, large ("0.25 XNO"). */
45
+ amount: (xno: string) => string;
46
+ /** The amount in dollars, beside it ("= $0.25"). */
47
+ usd: (usd: string) => string;
48
+ /** Above the amount, live only: the player must send this exact amount. */
49
+ sendExactly: string;
50
+ /** Under the stamp in test mode: nothing can be paid here. */
51
+ testNote: string;
52
+ /** The wallet key in test mode, off, with its reason. */
53
+ walletOff: string;
54
+ /** The wallet key (live). */
55
+ wallet: string;
56
+ /** The QR code's name for a screen reader. */
57
+ qrLabel: (xno: string) => string;
58
+ /** The fold with the amount and the address to type by hand. */
59
+ byHand: string;
60
+ amountLabel: string;
61
+ /** The address's label in the fold. */
62
+ toLabel: string;
63
+ /** In the fold: another amount does not match the order. */
64
+ byHandNote: string;
65
+ copy: string;
66
+ copied: string;
67
+ copyFailed: string;
68
+ /** The time left (live only): the order ends then. */
69
+ endsIn: (time: string) => string;
70
+ waiting: string;
71
+ noConnection: string;
72
+ stillWaiting: string;
73
+ checkAgain: string;
74
+ paid: string;
75
+ paidLine: (look: string) => string;
76
+ /** A payment after the order ended: it gives no look, and the creator sends it back. */
77
+ paidLate: string;
78
+ /** The order ended: a payment sent in time still counts. */
79
+ expired: string;
80
+ /** The price's sources ("CoinGecko", "Kraken", "KuCoin"). "Powered by CoinGecko" is added when CoinGecko is one and the words leave it out. */
81
+ prices: (sources: readonly string[]) => string;
82
+ };
83
+ /** CoinGecko's words: its API terms ask for them wherever its price shows. */
84
+ export declare const COINGECKO_CREDIT = "Powered by CoinGecko";
85
+ export declare const SHOP_WORDS: ShopWords;
86
+ /** The game's words over the defaults. A key the game leaves out, or gives as undefined, keeps its default. */
87
+ export declare function shopWords(over?: Partial<ShopWords>): ShopWords;
88
+ /** The price's line: the game's words, with "Powered by CoinGecko" added when CoinGecko is a source and the words leave it out. */
89
+ export declare function priceLine(words: ShopWords, sources: readonly string[]): string;
@@ -0,0 +1,79 @@
1
+ // Every word that the shop's sheets show: the looks sheet, Buy and the pay sheet. A game gives `words` to `mountShop` (or to `looksView` and `payPanel`)
2
+ // with the keys it wants to change, in its own words or language; the rest stay these. A text with a value in it is a function of that value.
3
+ //
4
+ // Two things a game cannot change, because a player's money depends on them: the stamp "TEST MODE: DO NOT PAY" (it is not a key), and the words
5
+ // "Powered by CoinGecko" while CoinGecko is one of the price feeds (CoinGecko's API terms ask for them: `priceLine` adds them when `prices` leaves
6
+ // them out).
7
+ //
8
+ // The defaults are short on purpose (Wes, 2026-11-01: fewer words): the pay sheet shows at most 30 words, and no sentence has more than 10.
9
+ const HATS = { cap: "with a cap", beanie: "with a beanie", crown: "with a crown", tophat: "with a top hat" };
10
+ const spaced = (slot) => slot.replace(/-/g, " ");
11
+ /** CoinGecko's words: its API terms ask for them wherever its price shows. */
12
+ export const COINGECKO_CREDIT = "Powered by CoinGecko";
13
+ export const SHOP_WORDS = {
14
+ title: "Looks",
15
+ close: "Close",
16
+ kinds: "Kinds of looks",
17
+ outfits: "Outfits",
18
+ slotTab: (slot) => spaced(slot.charAt(0).toUpperCase() + slot.slice(1)),
19
+ slotPlain: (slot) => `No ${spaced(slot)}`,
20
+ outfitLine: (hat) => (hat ? HATS[hat] : "colour only"),
21
+ itemLine: (slot) => spaced(slot),
22
+ free: "free",
23
+ yours: "yours",
24
+ wear: "Wear",
25
+ wearing: "Wearing",
26
+ buy: (price) => `Buy ${price}`,
27
+ looksNote: "Looks never change play. Pay in Nano, straight to the creator.",
28
+ signInFirst: "Sign in first to buy a look.",
29
+ buyTitle: (look) => `Buy ${look}`,
30
+ making: "Making your order…",
31
+ orderFailed: "The order did not start. Try again.",
32
+ refusal: (text) => text,
33
+ back: "Back",
34
+ signIn: "Sign in",
35
+ lookLine: () => "It never changes play.",
36
+ amount: (xno) => `${xno} XNO`,
37
+ usd: (usd) => `= ${usd}`,
38
+ sendExactly: "Send exactly",
39
+ testNote: "Nothing can be paid here.",
40
+ walletOff: "Wallet off: test mode",
41
+ wallet: "Open wallet",
42
+ qrLabel: (xno) => `QR code to pay ${xno} XNO`,
43
+ byHand: "Enter by hand",
44
+ amountLabel: "Amount",
45
+ toLabel: "To",
46
+ byHandNote: "Another amount does not match.",
47
+ copy: "Copy",
48
+ copied: "Copied",
49
+ copyFailed: "Copy failed",
50
+ endsIn: (time) => `Ends in ${time}.`,
51
+ waiting: "Waiting for payment…",
52
+ noConnection: "No connection. Trying again…",
53
+ stillWaiting: "Still waiting. Already paid? Press Check again.",
54
+ checkAgain: "Check again",
55
+ paid: "Paid. Thank you!",
56
+ paidLine: (look) => `${look} is yours.`,
57
+ paidLate: "Paid after the order ended, so no look. The creator sends it back.",
58
+ expired: "This order ended. Paid in time? Wait here: it still counts.",
59
+ prices: (sources) => {
60
+ const others = sources.filter((s) => s !== "CoinGecko");
61
+ const credit = sources.includes("CoinGecko") ? ` ${COINGECKO_CREDIT}.` : "";
62
+ return others.length ? `Price: ${others.join(", ")}.${credit}` : credit.trim();
63
+ },
64
+ };
65
+ /** The game's words over the defaults. A key the game leaves out, or gives as undefined, keeps its default. */
66
+ export function shopWords(over) {
67
+ const out = { ...SHOP_WORDS };
68
+ for (const [k, v] of Object.entries(over ?? {}))
69
+ if (v !== undefined && k in SHOP_WORDS)
70
+ out[k] = v;
71
+ return out;
72
+ }
73
+ /** The price's line: the game's words, with "Powered by CoinGecko" added when CoinGecko is a source and the words leave it out. */
74
+ export function priceLine(words, sources) {
75
+ const line = words.prices(sources).trim();
76
+ if (!sources.includes("CoinGecko") || line.includes(COINGECKO_CREDIT))
77
+ return line;
78
+ return `${line}${line ? " " : ""}${COINGECKO_CREDIT}.`;
79
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nano133/nanoterra",
3
- "version": "0.9.0",
3
+ "version": "0.9.3",
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, an options menu, emotes, the player's sign-in (a passkey or a wallet app) and a shop that sells looks for Nano with no key on the server.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,12 +1,13 @@
1
1
  // "@nano133/nanoterra/player-signin/panel": the player's sign-in sheet in the browser. `mountPlayerSignin` is the sheet (call `open()`);
2
- // `signInView` is its content, for a game that has a sheet of its own. Add "@nano133/nanoterra/player-signin/style.css" to the page.
2
+ // `signInView` is its content, for a game that has a sheet of its own. Add "@nano133/nanoterra/player-signin/style.css" to the page. Every word of
3
+ // the sheet can be the game's own: `words` (PlayerSigninWords; PLAYER_SIGNIN_WORDS are the defaults).
3
4
 
4
5
  export { mountPlayerSignin } from "./mount.js";
5
6
  export type { MountedPlayerSignin, PlayerSigninMountOptions } from "./mount.js";
6
7
  export { hostFits, passkeyName, signInView } from "./view.js";
7
8
  export { PASSKEY_PREVIEW_NOTE, previewHost } from "../host.js";
8
9
  export type { PlayerSigninView, PlayerSigninViewOptions, SignedIn } from "./view.js";
9
- export { faultWords } from "./words.js";
10
- export type { FaultWords } from "./words.js";
10
+ export { PLAYER_SIGNIN_WORDS, faultWords, playerSigninWords } from "./words.js";
11
+ export type { FaultText, FaultWords, PlayerSigninWords, PlayerSigninWordsOption } from "./words.js";
11
12
  export { readMemo } from "./memo.js";
12
13
  export type { SigninMemo } from "./memo.js";
@@ -1,14 +1,15 @@
1
1
  // The sign-in sheet of a game: `mountPlayerSignin(element, options)`. It draws no key of its own: the game's account key, the shop's "sign in
2
2
  // first" and anything else call `open()`, which resolves with the player once they are signed in, or null when the sheet closes. Plain DOM; a
3
3
  // modal sheet with a scrim: from the bottom on a phone held upright, in the middle on a wide or sideways screen. Escape, the close key and a press
4
- // on the scrim close it. Every key is 44 px or more.
4
+ // on the scrim close it. Phone first: text 16 px, every key 48 px or more each way.
5
5
 
6
6
  import { h, releaseFocus } from "../../admin/panel/dom.js";
7
7
  import { icon } from "../../admin/panel/icons.js";
8
8
  import { signInView, type PlayerSigninViewOptions, type SignedIn } from "./view.js";
9
+ import { playerSigninWords } from "./words.js";
9
10
 
10
11
  export type PlayerSigninMountOptions = PlayerSigninViewOptions & {
11
- /** The sheet's title. Default "Sign in". */
12
+ /** The sheet's title. Default: `words.title` ("Sign in"). */
12
13
  title?: string;
13
14
  /** How long "Signed in as …" shows before the sheet closes, in ms. Default 900. */
14
15
  closeAfterMs?: number;
@@ -36,6 +37,7 @@ export function mountPlayerSignin(root: HTMLElement, options: PlayerSigninMountO
36
37
  let pending: { promise: Promise<SignedIn | null>; resolve: (p: SignedIn | null) => void } | null = null;
37
38
  let before: Element | null = null;
38
39
  let closing: ReturnType<typeof setTimeout> | undefined;
40
+ const words = playerSigninWords(options.words);
39
41
 
40
42
  const settle = (p: SignedIn | null) => {
41
43
  const was = pending;
@@ -52,8 +54,8 @@ export function mountPlayerSignin(root: HTMLElement, options: PlayerSigninMountO
52
54
  },
53
55
  });
54
56
 
55
- const titleEl = h("h2", { class: "n133s-title", id: `n133s-title-${n}` }, options.title ?? "Sign in");
56
- const closeKey = h("button", { class: "n133s-close", type: "button", "aria-label": "Close" }, icon("close", 20));
57
+ const titleEl = h("h2", { class: "n133s-title", id: `n133s-title-${n}` }, options.title ?? words.title);
58
+ const closeKey = h("button", { class: "n133s-close", type: "button", "aria-label": words.close }, icon("close", 20));
57
59
  const scrim = h("div", { class: "n133s-scrim" });
58
60
  const panel = h("section", { class: "n133s-panel", role: "dialog", "aria-modal": "true", "aria-labelledby": `n133s-title-${n}` }, h("header", { class: "n133s-head" }, titleEl, closeKey), h("div", { class: "n133s-body" }, view.node));
59
61
  const wrap = h("div", { class: "n133-signin n133s-sheet", "data-open": "false" }, scrim, panel);
@@ -1,6 +1,6 @@
1
1
  /* The player's sign-in sheet (mountPlayerSignin in ./mount.ts) and its content (signInView in ./view.ts). Every rule is under .n133-signin, so nothing
2
2
  leaks into the game's page.
3
- Phone first: every key is 44 px or more and its words 16 px. On a phone held upright the sheet comes from the bottom; on a wide or sideways screen it
3
+ Phone first: text is 16 px, and every key, tile, link, close key and back key is 48 px or more each way. On a phone held upright the sheet comes from the bottom; on a wide or sideways screen it
4
4
  is a card in the middle. A scrim covers the game while it is open.
5
5
  Light and dark: the sheet follows data-n133-theme="light" | "dark" on the page's <html> when the page sets it, else the device's prefers-color-scheme.
6
6
  To theme it, set the --n133-* custom properties on the element that holds the sheet (or a parent): --n133-ink, --n133-ink-soft, --n133-paper,
@@ -129,8 +129,8 @@
129
129
  .n133-signin .n133s-close {
130
130
  display: inline-grid;
131
131
  place-items: center;
132
- width: 44px;
133
- height: 44px;
132
+ width: 48px;
133
+ height: 48px;
134
134
  border: 0;
135
135
  border-radius: 50%;
136
136
  background: transparent;
@@ -170,12 +170,12 @@
170
170
 
171
171
  .n133-signin .n133s-fine {
172
172
  margin: 0;
173
- font-size: 14px;
173
+ font-size: 16px;
174
174
  }
175
175
 
176
176
  .n133-signin .n133s-or {
177
177
  margin: 6px 0 -4px;
178
- font: 650 14px/1.3 var(--font);
178
+ font: 650 16px/1.3 var(--font);
179
179
  }
180
180
 
181
181
  .n133-signin .n133s-step {
@@ -251,7 +251,7 @@
251
251
  }
252
252
 
253
253
  .n133-signin .n133s-pk-words small {
254
- font-size: 14px;
254
+ font-size: 16px;
255
255
  opacity: 0.92;
256
256
  }
257
257
 
@@ -292,26 +292,26 @@
292
292
  border-radius: 12px;
293
293
  background: var(--ground);
294
294
  color: var(--ink);
295
- font: 800 15px/1 var(--font);
295
+ font: 800 16px/1 var(--font);
296
296
  }
297
297
 
298
298
  .n133-signin .n133s-name {
299
- font: 650 15px/1.2 var(--font);
299
+ font: 650 16px/1.2 var(--font);
300
300
  text-align: center;
301
301
  }
302
302
 
303
303
  .n133-signin .n133s-last {
304
- position: absolute;
305
- top: 6px;
306
- right: 8px;
307
- padding: 1px 7px;
304
+ padding: 0 8px;
308
305
  border-radius: 999px;
309
306
  background: var(--ground);
310
307
  color: var(--ink-soft);
311
- font: 700 11px/18px var(--font);
308
+ font: 700 16px/24px var(--font);
309
+ white-space: nowrap;
312
310
  }
313
311
 
312
+ /* "Last used": in the flow, so it never covers a word at 16 px: under the name in a tile, at the right end of the passkey's row. */
314
313
  .n133-signin .n133s-pk .n133s-last {
314
+ margin-left: auto;
315
315
  background: rgb(255 255 255 / 0.22);
316
316
  color: inherit;
317
317
  }
@@ -376,14 +376,15 @@
376
376
  align-self: flex-start;
377
377
  align-items: center;
378
378
  gap: 6px;
379
- min-height: 44px;
379
+ min-width: 48px;
380
+ min-height: 48px;
380
381
  margin: 0 0 -4px -10px;
381
- padding: 0 10px;
382
+ padding: 0 12px 0 10px;
382
383
  border: 0;
383
384
  border-radius: 10px;
384
385
  background: transparent;
385
386
  color: var(--ink-soft);
386
- font: 650 15px/1 var(--font);
387
+ font: 650 16px/1 var(--font);
387
388
  cursor: pointer;
388
389
  }
389
390
 
@@ -393,6 +394,10 @@
393
394
  }
394
395
 
395
396
  .n133-signin a:not(.n133s-btn) {
397
+ display: inline-flex;
398
+ align-items: center;
399
+ min-width: 48px;
400
+ min-height: 48px;
396
401
  color: var(--gem-dark);
397
402
  font-weight: 650;
398
403
  text-underline-offset: 2px;
@@ -443,7 +448,7 @@
443
448
  min-height: 1.4em;
444
449
  margin: 12px 0 0;
445
450
  color: var(--ink-soft);
446
- font-size: 15px;
451
+ font-size: 16px;
447
452
  }
448
453
 
449
454
  .n133-signin .n133s-status:empty {