@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.
- package/CHANGELOG.md +40 -0
- package/README.md +55 -7
- package/ai/SPEC.md +6 -0
- package/dist/player-signin/panel/index.d.ts +2 -2
- package/dist/player-signin/panel/index.js +3 -2
- package/dist/player-signin/panel/mount.d.ts +1 -1
- package/dist/player-signin/panel/mount.js +5 -3
- package/dist/player-signin/panel/view.d.ts +3 -0
- package/dist/player-signin/panel/view.js +49 -46
- package/dist/player-signin/panel/words.d.ts +96 -1
- package/dist/player-signin/panel/words.js +109 -24
- package/dist/shop/panel/index.d.ts +2 -0
- package/dist/shop/panel/index.js +3 -0
- package/dist/shop/panel/looks.d.ts +3 -0
- package/dist/shop/panel/looks.js +11 -11
- package/dist/shop/panel/mount.d.ts +18 -2
- package/dist/shop/panel/mount.js +48 -21
- package/dist/shop/panel/pay.d.ts +3 -0
- package/dist/shop/panel/pay.js +38 -25
- package/dist/shop/panel/words.d.ts +89 -0
- package/dist/shop/panel/words.js +79 -0
- package/package.json +1 -1
- package/src/player-signin/panel/index.ts +4 -3
- package/src/player-signin/panel/mount.ts +6 -4
- package/src/player-signin/panel/style.css +22 -17
- package/src/player-signin/panel/view.ts +60 -55
- package/src/player-signin/panel/words.ts +202 -24
- package/src/shop/panel/index.ts +4 -0
- package/src/shop/panel/looks.ts +14 -14
- package/src/shop/panel/mount.ts +60 -21
- package/src/shop/panel/pay.ts +51 -30
- package/src/shop/panel/style.css +24 -20
- package/src/shop/panel/words.ts +172 -0
package/src/shop/panel/mount.ts
CHANGED
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
// owned() the looks the player owns (the game's GET /api/me gives them) signedIn() whether a player is signed in
|
|
5
5
|
// signIn() the game's own sign-in (the kit's sign-in path, or the game's): true when the player is signed in after it. The shop draws no sign-in.
|
|
6
6
|
// worn/wear what the player wears; `onOwned(lookId)` when a paid order gave a look (wear it)
|
|
7
|
+
// onSignInCancel(lookId) the game's own Buy (`buy(lookId)`) asked for a sign-in and the player closed it: the game shows its own sheet again
|
|
7
8
|
//
|
|
8
|
-
// `key: false` draws no key: the game opens the sheet from its own key with `open()`, as the options panel allows.
|
|
9
|
-
//
|
|
9
|
+
// `key: false` draws no key: the game opens the sheet from its own key with `open()`, as the options panel allows. `buy(lookId)` starts on the next frame:
|
|
10
|
+
// a game's own <dialog> that closed just before has had its `close` event by then (Chromium fires it at the frame), so its handler cannot take the focus.
|
|
11
|
+
// The sheet is a <dialog>: it rises from the bottom on a phone held upright and stands in the middle on a wide screen; on a phone held sideways the pay
|
|
12
|
+
// sheet puts the QR code beside the words. Phone first: text 16 px, every key 48 px or more each way.
|
|
13
|
+
//
|
|
14
|
+
// `words` changes any word of the two sheets (./words.ts: SHOP_WORDS are the defaults), for a game's own words or language.
|
|
10
15
|
|
|
11
16
|
import { ApiError, createApi } from "../../admin/panel/api.js";
|
|
12
17
|
import { h, releaseFocus } from "../../admin/panel/dom.js";
|
|
@@ -15,6 +20,7 @@ import { checkLooks, wearOf, type Look } from "../../looks/look.js";
|
|
|
15
20
|
import type { PayView } from "../handler.js";
|
|
16
21
|
import { looksView, slotsOf, type SlotWords } from "./looks.js";
|
|
17
22
|
import { payPanel } from "./pay.js";
|
|
23
|
+
import { shopWords, type ShopWords } from "./words.js";
|
|
18
24
|
|
|
19
25
|
export type ShopMountOptions = {
|
|
20
26
|
/** The game's looks (looks.json as it was read). The server decides every price; these are for the list. */
|
|
@@ -26,6 +32,16 @@ export type ShopMountOptions = {
|
|
|
26
32
|
signedIn(): boolean;
|
|
27
33
|
/** The game's sign-in: true when a player is signed in after it. With none, Buy asks the player to sign in and stops. */
|
|
28
34
|
signIn?: () => Promise<boolean>;
|
|
35
|
+
/**
|
|
36
|
+
* The game's own Buy (`buy(lookId)`) asked for a sign-in, and the player closed it without signing in: the game shows its own looks sheet again.
|
|
37
|
+
* The kit's list does not open then (the kit's sheet closes if it was open). A Buy in the kit's own list goes back to that list, as before.
|
|
38
|
+
*/
|
|
39
|
+
onSignInCancel?: (lookId: string, look: Look) => void;
|
|
40
|
+
/**
|
|
41
|
+
* The server refused an order with 401: the player's session ended, though `signedIn()` said yes. The game marks the player signed out. The sheet's
|
|
42
|
+
* "Sign in" key then calls `signIn()` without asking `signedIn()` first, so a stale "yes" cannot post the same refused order again.
|
|
43
|
+
*/
|
|
44
|
+
onSessionEnd?: () => void;
|
|
29
45
|
/** Whether the player wears this look now. */
|
|
30
46
|
worn(look: Look): boolean;
|
|
31
47
|
/** Wear a look the player owns. */
|
|
@@ -40,8 +56,10 @@ export type ShopMountOptions = {
|
|
|
40
56
|
picture?: (look: Look) => HTMLElement | null;
|
|
41
57
|
/** Words for the tabs of the wear slots. */
|
|
42
58
|
slots?: Record<string, SlotWords>;
|
|
43
|
-
/** The sheet's title. Default "Looks". */
|
|
59
|
+
/** The sheet's title. Default: `words.title` ("Looks"). */
|
|
44
60
|
title?: string;
|
|
61
|
+
/** The game's words for the two sheets, over the defaults (SHOP_WORDS): `{ buyTitle: (look) => look, sendExactly: "Pay exactly" }`. */
|
|
62
|
+
words?: Partial<ShopWords>;
|
|
45
63
|
/** Draw the Looks key. Default true; false: the game calls `open()` from its own key. */
|
|
46
64
|
key?: boolean;
|
|
47
65
|
/** "light", "dark", or "device" (the default): the page's `data-n133-theme` when it has one, else the device's setting. */
|
|
@@ -58,7 +76,10 @@ export type MountedShop = {
|
|
|
58
76
|
/** Opens the sheet on the looks. */
|
|
59
77
|
open(): void;
|
|
60
78
|
close(): void;
|
|
61
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* Opens the sheet on Buy for one look (the game's own "Buy" elsewhere), on the next frame: close the game's own <dialog> first. A sign-in closed
|
|
81
|
+
* without a sign-in calls `onSignInCancel` and opens nothing.
|
|
82
|
+
*/
|
|
62
83
|
buy(lookId: string): void;
|
|
63
84
|
/** Draws the looks again (after the player signed in, or wore something). */
|
|
64
85
|
refresh(): void;
|
|
@@ -75,12 +96,14 @@ export function mountShop(root: HTMLElement, options: ShopMountOptions): Mounted
|
|
|
75
96
|
let stopPay: (() => void) | null = null;
|
|
76
97
|
let destroyed = false;
|
|
77
98
|
const cleanup: (() => void)[] = [];
|
|
99
|
+
const words = shopWords(options.words);
|
|
100
|
+
const sheetTitle = options.title ?? words.title;
|
|
78
101
|
|
|
79
102
|
const title = h("h2", { class: "n133s-title", tabindex: "-1" });
|
|
80
|
-
const closeKey = h("button", { class: "n133s-close", type: "button", "aria-label":
|
|
103
|
+
const closeKey = h("button", { class: "n133s-close", type: "button", "aria-label": words.close }, icon("close", 20));
|
|
81
104
|
const body = h("div", { class: "n133s-body" });
|
|
82
|
-
const dialog = h("dialog", { class: "n133s-sheet", "aria-label":
|
|
83
|
-
const key = options.key === false ? null : h("button", { class: "n133s-key", type: "button", "aria-haspopup": "dialog" }, icon("shirt", 20), h("span", { class: "n133s-key-word" },
|
|
105
|
+
const dialog = h("dialog", { class: "n133s-sheet", "aria-label": sheetTitle }, h("header", { class: "n133s-head" }, title, closeKey), body);
|
|
106
|
+
const key = options.key === false ? null : h("button", { class: "n133s-key", type: "button", "aria-haspopup": "dialog" }, icon("shirt", 20), h("span", { class: "n133s-key-word" }, sheetTitle));
|
|
84
107
|
const wrap = h("div", { class: "n133-shop" }, key, dialog);
|
|
85
108
|
root.append(wrap);
|
|
86
109
|
|
|
@@ -147,7 +170,7 @@ export function mountShop(root: HTMLElement, options: ShopMountOptions): Mounted
|
|
|
147
170
|
function showLooks() {
|
|
148
171
|
if (destroyed) return;
|
|
149
172
|
show(
|
|
150
|
-
|
|
173
|
+
sheetTitle,
|
|
151
174
|
looksView({
|
|
152
175
|
looks,
|
|
153
176
|
owned: ownedNow(),
|
|
@@ -163,9 +186,10 @@ export function mountShop(root: HTMLElement, options: ShopMountOptions): Mounted
|
|
|
163
186
|
showLooks();
|
|
164
187
|
}
|
|
165
188
|
: undefined,
|
|
166
|
-
buy: (l) => void buy(l),
|
|
189
|
+
buy: (l) => void buy(l, false),
|
|
167
190
|
picture: options.picture,
|
|
168
191
|
slots: options.slots,
|
|
192
|
+
words,
|
|
169
193
|
tab,
|
|
170
194
|
onTab: (s) => {
|
|
171
195
|
tab = s;
|
|
@@ -177,23 +201,31 @@ export function mountShop(root: HTMLElement, options: ShopMountOptions): Mounted
|
|
|
177
201
|
}
|
|
178
202
|
|
|
179
203
|
const back = (message: string, extra?: HTMLElement | null) =>
|
|
180
|
-
h("div", { class: "n133s-message" }, h("p", { class: "n133s-status" }, message), extra ?? null, h("button", { class: "n133s-btn n133s-wide", type: "button", onclick: () => showLooks() },
|
|
204
|
+
h("div", { class: "n133s-message" }, h("p", { class: "n133s-status" }, message), extra ?? null, h("button", { class: "n133s-btn n133s-wide", type: "button", onclick: () => showLooks() }, words.back));
|
|
181
205
|
|
|
182
|
-
|
|
206
|
+
/**
|
|
207
|
+
* `game`: the game's own Buy started it (`buy(lookId)`), not the kit's list: a closed sign-in goes back to the game, not to the list.
|
|
208
|
+
* `ended`: the server said the session ended (401): sign in again whatever `signedIn()` says.
|
|
209
|
+
*/
|
|
210
|
+
async function buy(look: Look, game: boolean, ended = false) {
|
|
183
211
|
if (destroyed) return;
|
|
184
212
|
tab = slotsOf(looks).length > 1 ? wearOf(look) : undefined;
|
|
185
|
-
if (!options.signedIn()) {
|
|
186
|
-
if (!options.signIn) return show(
|
|
213
|
+
if (ended || !options.signedIn()) {
|
|
214
|
+
if (!options.signIn) return show(words.buyTitle(look.name), back(words.signInFirst));
|
|
187
215
|
let ok = false;
|
|
188
216
|
try {
|
|
189
217
|
ok = await options.signIn();
|
|
190
218
|
} catch {
|
|
191
219
|
ok = false;
|
|
192
220
|
}
|
|
193
|
-
if (!ok || !options.signedIn())
|
|
221
|
+
if (!ok || !options.signedIn()) {
|
|
222
|
+
if (!game) return showLooks();
|
|
223
|
+
close();
|
|
224
|
+
return options.onSignInCancel?.(look.id, look);
|
|
225
|
+
}
|
|
194
226
|
}
|
|
195
227
|
const box = h("div", { class: "n133s-turnstile", hidden: true });
|
|
196
|
-
show(
|
|
228
|
+
show(words.buyTitle(look.name), h("div", {}, h("p", { class: "n133s-status" }, words.making), box));
|
|
197
229
|
try {
|
|
198
230
|
const turnstile = options.personCheck ? await options.personCheck(box) : null;
|
|
199
231
|
const order = await api.post<PayView>("", { lookId: look.id, turnstile });
|
|
@@ -201,18 +233,21 @@ export function mountShop(root: HTMLElement, options: ShopMountOptions): Mounted
|
|
|
201
233
|
const pay = payPanel(order, {
|
|
202
234
|
lookName: look.name,
|
|
203
235
|
check: (id) => api.post<PayView>(`/${id}/check`),
|
|
204
|
-
errorText: (e) => (e instanceof ApiError ? e.message : null),
|
|
236
|
+
errorText: (e) => (e instanceof ApiError ? words.refusal(e.message, e.status) : null),
|
|
237
|
+
words,
|
|
205
238
|
onPaid: () => {
|
|
206
239
|
bought.add(look.id);
|
|
207
240
|
options.onOwned?.(look.id, look);
|
|
208
241
|
},
|
|
209
242
|
});
|
|
210
|
-
show(
|
|
243
|
+
show(words.buyTitle(look.name), pay.node);
|
|
211
244
|
stopPay = pay.stop;
|
|
212
245
|
} catch (e) {
|
|
213
|
-
const message = e instanceof ApiError ? e.message : e instanceof Error && e.message ? e.message :
|
|
214
|
-
const
|
|
215
|
-
|
|
246
|
+
const message = e instanceof ApiError ? words.refusal(e.message, e.status) : e instanceof Error && e.message ? words.refusal(e.message, 0) : words.orderFailed;
|
|
247
|
+
const gone = e instanceof ApiError && e.status === 401;
|
|
248
|
+
if (gone) options.onSessionEnd?.();
|
|
249
|
+
const again = gone && options.signIn ? h("button", { class: "n133s-btn n133s-primary n133s-wide", type: "button", onclick: () => void buy(look, game, true) }, words.signIn) : null;
|
|
250
|
+
show(words.buyTitle(look.name), back(message, again));
|
|
216
251
|
}
|
|
217
252
|
}
|
|
218
253
|
|
|
@@ -221,7 +256,11 @@ export function mountShop(root: HTMLElement, options: ShopMountOptions): Mounted
|
|
|
221
256
|
close,
|
|
222
257
|
buy(lookId) {
|
|
223
258
|
const look = looks.find((l) => l.id === lookId);
|
|
224
|
-
|
|
259
|
+
// On the next frame (rAF; a hidden page has none): a <dialog> of the game's that closed just before has had its `close` event by then.
|
|
260
|
+
if (!look) return;
|
|
261
|
+
const go = () => void buy(look, true);
|
|
262
|
+
if (typeof requestAnimationFrame === "function" && !globalThis.document?.hidden) requestAnimationFrame(go);
|
|
263
|
+
else setTimeout(go, 0);
|
|
225
264
|
},
|
|
226
265
|
refresh() {
|
|
227
266
|
if (isOpen() && body.querySelector(".n133s-looks-view")) showLooks();
|
package/src/shop/panel/pay.ts
CHANGED
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
//
|
|
4
4
|
// TEST MODE: the server sends no address and no link, and this sheet shows a large "TEST MODE: DO NOT PAY" stamp where the QR code would be, with the
|
|
5
5
|
// wallet key off. Never change that: a payment to an address that nobody holds the key for is lost for good. The sheet decides it from the ORDER (its
|
|
6
|
-
// `test`, a missing `uri` or a missing `address`), never from a setting of the page.
|
|
6
|
+
// `test`, a missing `uri` or a missing `address`), never from a setting of the page. The stamp's words are not among the game's words (./words.ts).
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { h } from "../../admin/panel/dom.js";
|
|
9
9
|
import { usdText } from "../../looks/look.js";
|
|
10
10
|
import type { PayView } from "../handler.js";
|
|
11
11
|
import { qrSvg } from "./qr.js";
|
|
12
|
+
import { priceLine, shopWords, type ShopWords } from "./words.js";
|
|
12
13
|
|
|
13
14
|
export const PAY_TIMING = { pollMs: 5000, checkForMs: 2 * 60_000 } as const;
|
|
14
15
|
|
|
@@ -23,6 +24,8 @@ export type PayOptions = {
|
|
|
23
24
|
errorText?(e: unknown): string | null;
|
|
24
25
|
/** The time now in ms (a test gives its own). */
|
|
25
26
|
now?: () => number;
|
|
27
|
+
/** The game's words (./words.ts): the keys it leaves out keep the defaults. */
|
|
28
|
+
words?: Partial<ShopWords>;
|
|
26
29
|
};
|
|
27
30
|
|
|
28
31
|
export type PayPanel = { node: HTMLElement; stop(): void };
|
|
@@ -37,44 +40,62 @@ export function payPanel(start: PayView, options: PayOptions): PayPanel {
|
|
|
37
40
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
38
41
|
let clockTimer: ReturnType<typeof setInterval> | undefined;
|
|
39
42
|
let checkingUntil = 0;
|
|
43
|
+
const w = shopWords(options.words);
|
|
44
|
+
const test = isTestOrder(order);
|
|
40
45
|
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
|
|
46
|
+
/** The order's state. In test mode nothing is checked, so it has nothing to say and takes no room. */
|
|
47
|
+
const status = h("p", { class: "n133s-status", role: "status", "aria-live": "polite", hidden: test });
|
|
48
|
+
const again = h("button", { class: "n133s-btn n133s-wide", type: "button", hidden: true }, w.checkAgain);
|
|
49
|
+
/** The time left, live only: in test mode nothing is paid, so nothing waits for the end. */
|
|
50
|
+
const ends = h("p", { class: "n133s-fine n133s-time-left", hidden: test });
|
|
51
|
+
const amount = h(
|
|
52
|
+
"div",
|
|
53
|
+
{ class: "n133s-amount" },
|
|
54
|
+
test ? null : h("span", { class: "n133s-exact" }, w.sendExactly),
|
|
55
|
+
h("strong", {}, w.amount(order.xno)),
|
|
56
|
+
h("span", {}, w.usd(usdText(Number(order.usd)))),
|
|
57
|
+
);
|
|
58
|
+
const copyKey = (value: string) => {
|
|
59
|
+
const key = h("button", { class: "n133s-btn n133s-small", type: "button" }, w.copy);
|
|
60
|
+
key.addEventListener("click", async () => {
|
|
61
|
+
try {
|
|
62
|
+
await navigator.clipboard.writeText(value);
|
|
63
|
+
key.textContent = w.copied;
|
|
64
|
+
} catch {
|
|
65
|
+
key.textContent = w.copyFailed;
|
|
66
|
+
}
|
|
67
|
+
setTimeout(() => (key.textContent = w.copy), 1500);
|
|
68
|
+
});
|
|
69
|
+
return key;
|
|
70
|
+
};
|
|
45
71
|
|
|
46
72
|
let payArea: HTMLElement;
|
|
47
73
|
/** The main key (the wallet link; off in test mode): it stands in a foot that stays in view, so a phone on its side never hides it. */
|
|
48
74
|
let wallet: HTMLElement;
|
|
49
|
-
const test = isTestOrder(order);
|
|
50
75
|
if (test) {
|
|
51
76
|
payArea = h(
|
|
52
77
|
"div",
|
|
53
78
|
{ class: "n133s-test-pay" },
|
|
54
79
|
h("div", { class: "n133s-qr n133s-qr-blocked", "aria-hidden": "true" }, h("div", { class: "n133s-qr-fake" })),
|
|
55
80
|
h("div", { class: "n133s-stamp", role: "alert" }, h("span", {}, "TEST MODE:"), " ", h("span", {}, "DO NOT PAY")),
|
|
56
|
-
h("p", { class: "n133s-fine" },
|
|
81
|
+
h("p", { class: "n133s-fine" }, w.testNote),
|
|
57
82
|
);
|
|
58
|
-
wallet = h("button", { class: "n133s-btn n133s-primary n133s-wide", type: "button", disabled: true },
|
|
83
|
+
wallet = h("button", { class: "n133s-btn n133s-primary n133s-wide", type: "button", disabled: true }, w.walletOff);
|
|
59
84
|
} else {
|
|
60
|
-
const copyAmount = h("button", { class: "n133s-btn n133s-small", type: "button" }, "Copy");
|
|
61
|
-
copyAmount.addEventListener("click", () => copy(order.xno, copyAmount));
|
|
62
|
-
const copyAddress = h("button", { class: "n133s-btn n133s-small", type: "button" }, "Copy");
|
|
63
|
-
copyAddress.addEventListener("click", () => copy(order.address!, copyAddress));
|
|
64
85
|
payArea = h(
|
|
65
86
|
"div",
|
|
66
87
|
{ class: "n133s-live-pay" },
|
|
67
|
-
h("div", { class: "n133s-qr" }, qrSvg(order.uri!,
|
|
88
|
+
h("div", { class: "n133s-qr" }, qrSvg(order.uri!, w.qrLabel(order.xno))),
|
|
68
89
|
h(
|
|
69
90
|
"details",
|
|
70
91
|
{ class: "n133s-by-hand" },
|
|
71
|
-
h("summary", {},
|
|
72
|
-
h("div", { class: "n133s-row" }, h("span", { class: "n133s-label" },
|
|
73
|
-
h("div", { class: "n133s-row" }, h("span", { class: "n133s-label" },
|
|
74
|
-
h("p", { class: "n133s-fine" },
|
|
92
|
+
h("summary", {}, w.byHand),
|
|
93
|
+
h("div", { class: "n133s-row" }, h("span", { class: "n133s-label" }, w.amountLabel), h("code", {}, w.amount(order.xno)), copyKey(order.xno)),
|
|
94
|
+
h("div", { class: "n133s-row" }, h("span", { class: "n133s-label" }, w.toLabel), h("code", { class: "n133s-address" }, order.address!), copyKey(order.address!)),
|
|
95
|
+
h("p", { class: "n133s-fine" }, w.byHandNote),
|
|
75
96
|
),
|
|
76
97
|
);
|
|
77
|
-
wallet = h("a", { class: "n133s-btn n133s-primary n133s-wide", href: order.uri! },
|
|
98
|
+
wallet = h("a", { class: "n133s-btn n133s-primary n133s-wide", href: order.uri! }, w.wallet);
|
|
78
99
|
}
|
|
79
100
|
|
|
80
101
|
const node = h(
|
|
@@ -83,17 +104,17 @@ export function payPanel(start: PayView, options: PayOptions): PayPanel {
|
|
|
83
104
|
h(
|
|
84
105
|
"div",
|
|
85
106
|
{ class: "n133s-pay-main" },
|
|
86
|
-
h("p", { class: "n133s-pay-what" },
|
|
107
|
+
h("p", { class: "n133s-pay-what" }, w.lookLine(options.lookName)),
|
|
87
108
|
amount,
|
|
88
109
|
payArea,
|
|
89
110
|
),
|
|
90
111
|
h(
|
|
91
112
|
"div",
|
|
92
113
|
{ class: "n133s-pay-side" },
|
|
93
|
-
|
|
114
|
+
ends,
|
|
94
115
|
status,
|
|
95
116
|
again,
|
|
96
|
-
h("p", { class: "n133s-fine n133s-prices" },
|
|
117
|
+
h("p", { class: "n133s-fine n133s-prices" }, priceLine(w, order.prices)),
|
|
97
118
|
h("div", { class: "n133s-foot" }, wallet),
|
|
98
119
|
),
|
|
99
120
|
);
|
|
@@ -109,7 +130,7 @@ export function payPanel(start: PayView, options: PayOptions): PayPanel {
|
|
|
109
130
|
const ms = Math.max(0, order.expiresAt - now());
|
|
110
131
|
const m = Math.floor(ms / 60_000);
|
|
111
132
|
const s = Math.floor((ms % 60_000) / 1000);
|
|
112
|
-
|
|
133
|
+
ends.textContent = w.endsIn(`${m}:${String(s).padStart(2, "0")}`);
|
|
113
134
|
};
|
|
114
135
|
tick();
|
|
115
136
|
clockTimer = setInterval(tick, 1000);
|
|
@@ -118,7 +139,7 @@ export function payPanel(start: PayView, options: PayOptions): PayPanel {
|
|
|
118
139
|
order = o;
|
|
119
140
|
if (o.status === "paid") {
|
|
120
141
|
stop();
|
|
121
|
-
payArea.replaceChildren(h("div", { class: "n133s-paid" }, h("strong", {},
|
|
142
|
+
payArea.replaceChildren(h("div", { class: "n133s-paid" }, h("strong", {}, w.paid), h("p", {}, w.paidLine(options.lookName))));
|
|
122
143
|
wallet.hidden = true;
|
|
123
144
|
status.textContent = "";
|
|
124
145
|
again.hidden = true;
|
|
@@ -127,13 +148,13 @@ export function payPanel(start: PayView, options: PayOptions): PayPanel {
|
|
|
127
148
|
}
|
|
128
149
|
if (o.status === "paid_late") {
|
|
129
150
|
stop();
|
|
130
|
-
status.textContent =
|
|
151
|
+
status.textContent = w.paidLate;
|
|
131
152
|
again.hidden = true;
|
|
132
153
|
return true;
|
|
133
154
|
}
|
|
134
155
|
if (o.status === "expired") {
|
|
135
156
|
stop();
|
|
136
|
-
status.textContent =
|
|
157
|
+
status.textContent = w.expired;
|
|
137
158
|
again.hidden = false;
|
|
138
159
|
return false;
|
|
139
160
|
}
|
|
@@ -145,13 +166,13 @@ export function payPanel(start: PayView, options: PayOptions): PayPanel {
|
|
|
145
166
|
try {
|
|
146
167
|
const o = await options.check(order.id);
|
|
147
168
|
if (show(o)) return;
|
|
148
|
-
if (o.status === "open") status.textContent = test ? "
|
|
169
|
+
if (o.status === "open") status.textContent = test ? "" : w.waiting;
|
|
149
170
|
} catch (e) {
|
|
150
|
-
status.textContent = options.errorText?.(e) ??
|
|
171
|
+
status.textContent = options.errorText?.(e) ?? w.noConnection;
|
|
151
172
|
}
|
|
152
173
|
if (now() < checkingUntil) timer = setTimeout(poll, PAY_TIMING.pollMs);
|
|
153
174
|
else if (!stopped) {
|
|
154
|
-
status.textContent =
|
|
175
|
+
status.textContent = w.stillWaiting;
|
|
155
176
|
again.hidden = false;
|
|
156
177
|
}
|
|
157
178
|
}
|
|
@@ -164,8 +185,8 @@ export function payPanel(start: PayView, options: PayOptions): PayPanel {
|
|
|
164
185
|
void poll();
|
|
165
186
|
}
|
|
166
187
|
again.addEventListener("click", startChecking);
|
|
188
|
+
// Test mode makes no checks: nothing can be paid, so nothing is waited for.
|
|
167
189
|
if (!test) startChecking();
|
|
168
|
-
else status.textContent = "Test mode: the game does not wait for a payment.";
|
|
169
190
|
|
|
170
191
|
return { node, stop };
|
|
171
192
|
}
|
package/src/shop/panel/style.css
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* The Looks key and the shop's sheet (mountShop in ./mount.ts). Every rule is under .n133-shop, so nothing leaks into the game's page.
|
|
2
|
-
Phone first:
|
|
2
|
+
Phone first: text is 16 px (the stamp and the amount bigger), and every key, tab, close key and fold is 48 px or more each way. On a phone held upright the sheet rises from the bottom; on a wide screen it
|
|
3
3
|
stands in the middle; on a phone held sideways the pay sheet puts the QR code beside the words, and the wallet key stays in view.
|
|
4
4
|
Light and dark: mountShop sets data-theme="light" | "dark" on .n133-shop (the game's word, the page's data-n133-theme, or the device's setting).
|
|
5
5
|
To theme it, set the --n133-* custom properties on the element that holds the key (or a parent): --n133-ink, --n133-ink-soft, --n133-paper,
|
|
@@ -64,14 +64,14 @@
|
|
|
64
64
|
display: inline-flex;
|
|
65
65
|
align-items: center;
|
|
66
66
|
gap: 8px;
|
|
67
|
-
min-width:
|
|
68
|
-
min-height:
|
|
69
|
-
padding: 0
|
|
67
|
+
min-width: 48px;
|
|
68
|
+
min-height: 48px;
|
|
69
|
+
padding: 0 16px;
|
|
70
70
|
border: 1px solid var(--line);
|
|
71
71
|
border-radius: 999px;
|
|
72
72
|
background: var(--paper);
|
|
73
73
|
color: var(--ink);
|
|
74
|
-
font: 650
|
|
74
|
+
font: 650 16px/1 var(--font);
|
|
75
75
|
cursor: pointer;
|
|
76
76
|
touch-action: manipulation;
|
|
77
77
|
}
|
|
@@ -143,8 +143,8 @@
|
|
|
143
143
|
.n133-shop .n133s-close {
|
|
144
144
|
display: grid;
|
|
145
145
|
place-items: center;
|
|
146
|
-
width:
|
|
147
|
-
height:
|
|
146
|
+
width: 48px;
|
|
147
|
+
height: 48px;
|
|
148
148
|
border: 0;
|
|
149
149
|
border-radius: 12px;
|
|
150
150
|
background: transparent;
|
|
@@ -172,7 +172,7 @@
|
|
|
172
172
|
|
|
173
173
|
.n133-shop .n133s-fine {
|
|
174
174
|
color: var(--ink-soft);
|
|
175
|
-
font-size:
|
|
175
|
+
font-size: 16px;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
/* ---- keys ---- */
|
|
@@ -182,6 +182,7 @@
|
|
|
182
182
|
align-items: center;
|
|
183
183
|
justify-content: center;
|
|
184
184
|
gap: 8px;
|
|
185
|
+
min-width: 48px;
|
|
185
186
|
min-height: 48px;
|
|
186
187
|
padding: 0 18px;
|
|
187
188
|
border: 2px solid var(--line);
|
|
@@ -227,9 +228,7 @@
|
|
|
227
228
|
}
|
|
228
229
|
|
|
229
230
|
.n133-shop .n133s-small {
|
|
230
|
-
|
|
231
|
-
padding: 0 12px;
|
|
232
|
-
font-size: 14px;
|
|
231
|
+
padding: 0 14px;
|
|
233
232
|
}
|
|
234
233
|
|
|
235
234
|
/* ---- the looks ---- */
|
|
@@ -245,13 +244,14 @@
|
|
|
245
244
|
|
|
246
245
|
.n133-shop .n133s-tab {
|
|
247
246
|
flex: none;
|
|
248
|
-
min-
|
|
249
|
-
|
|
247
|
+
min-width: 48px;
|
|
248
|
+
min-height: 48px;
|
|
249
|
+
padding: 0 16px;
|
|
250
250
|
border: 1px solid var(--line);
|
|
251
251
|
border-radius: 999px;
|
|
252
252
|
background: var(--ground);
|
|
253
253
|
color: var(--ink);
|
|
254
|
-
font: 650
|
|
254
|
+
font: 650 16px/1 var(--font);
|
|
255
255
|
cursor: pointer;
|
|
256
256
|
}
|
|
257
257
|
|
|
@@ -295,7 +295,7 @@
|
|
|
295
295
|
|
|
296
296
|
.n133-shop .n133s-look-text small {
|
|
297
297
|
color: var(--ink-soft);
|
|
298
|
-
font-size:
|
|
298
|
+
font-size: 16px;
|
|
299
299
|
}
|
|
300
300
|
|
|
301
301
|
.n133-shop .n133s-swatch {
|
|
@@ -366,6 +366,11 @@
|
|
|
366
366
|
font-weight: 700;
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
+
/* The time left: its digits keep their width while it counts down. */
|
|
370
|
+
.n133-shop .n133s-time-left {
|
|
371
|
+
font-variant-numeric: tabular-nums;
|
|
372
|
+
}
|
|
373
|
+
|
|
369
374
|
.n133-shop .n133s-qr {
|
|
370
375
|
display: grid;
|
|
371
376
|
place-items: center;
|
|
@@ -429,8 +434,8 @@
|
|
|
429
434
|
}
|
|
430
435
|
|
|
431
436
|
.n133-shop .n133s-by-hand summary {
|
|
432
|
-
min-height:
|
|
433
|
-
line-height:
|
|
437
|
+
min-height: 48px;
|
|
438
|
+
line-height: 48px;
|
|
434
439
|
font-weight: 700;
|
|
435
440
|
cursor: pointer;
|
|
436
441
|
}
|
|
@@ -443,17 +448,16 @@
|
|
|
443
448
|
}
|
|
444
449
|
|
|
445
450
|
.n133-shop .n133s-label {
|
|
446
|
-
width:
|
|
451
|
+
width: 64px;
|
|
447
452
|
flex: none;
|
|
448
453
|
color: var(--ink-soft);
|
|
449
|
-
font-size: 14px;
|
|
450
454
|
}
|
|
451
455
|
|
|
452
456
|
.n133-shop .n133s-row code {
|
|
453
457
|
flex: 1;
|
|
454
458
|
min-width: 0;
|
|
455
459
|
word-break: break-all;
|
|
456
|
-
font-size:
|
|
460
|
+
font-size: 16px;
|
|
457
461
|
}
|
|
458
462
|
|
|
459
463
|
.n133-shop .n133s-paid {
|