@nano133/nanoterra 0.8.1 → 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 +92 -0
- package/README.md +390 -6
- package/ai/SPEC.md +71 -3
- package/dist/admin/panel/mount.d.ts +3 -0
- package/dist/admin/panel/mount.js +4 -2
- package/dist/player-signin/adapters/d1.d.ts +10 -0
- package/dist/player-signin/adapters/d1.js +108 -0
- package/dist/player-signin/adapters/firestore.d.ts +45 -0
- package/dist/player-signin/adapters/firestore.js +168 -0
- package/dist/player-signin/conformance.d.ts +11 -0
- package/dist/player-signin/conformance.js +160 -0
- package/dist/player-signin/handler.d.ts +64 -0
- package/dist/player-signin/handler.js +364 -0
- package/dist/player-signin/host.d.ts +4 -0
- package/dist/player-signin/host.js +10 -0
- package/dist/player-signin/index.d.ts +9 -0
- package/dist/player-signin/index.js +15 -0
- package/dist/player-signin/memory.d.ts +15 -0
- package/dist/player-signin/memory.js +101 -0
- package/dist/player-signin/panel/index.d.ts +9 -0
- package/dist/player-signin/panel/index.js +8 -0
- package/dist/player-signin/panel/memo.d.ts +12 -0
- package/dist/player-signin/panel/memo.js +41 -0
- package/dist/player-signin/panel/mount.d.ts +21 -0
- package/dist/player-signin/panel/mount.js +107 -0
- package/dist/player-signin/panel/view.d.ts +48 -0
- package/dist/player-signin/panel/view.js +363 -0
- package/dist/player-signin/panel/words.d.ts +103 -0
- package/dist/player-signin/panel/words.js +111 -0
- package/dist/player-signin/scheme-check.d.ts +3 -0
- package/dist/player-signin/scheme-check.js +14 -0
- package/dist/player-signin/scheme.d.ts +13 -0
- package/dist/player-signin/scheme.js +28 -0
- package/dist/player-signin/types.d.ts +116 -0
- package/dist/player-signin/types.js +33 -0
- package/dist/shop/adapters/d1.d.ts +25 -0
- package/dist/shop/adapters/d1.js +200 -0
- package/dist/shop/adapters/firestore.d.ts +53 -0
- package/dist/shop/adapters/firestore.js +245 -0
- package/dist/shop/address.d.ts +14 -0
- package/dist/shop/address.js +104 -0
- package/dist/shop/amount.d.ts +26 -0
- package/dist/shop/amount.js +59 -0
- package/dist/shop/conformance.d.ts +13 -0
- package/dist/shop/conformance.js +203 -0
- package/dist/shop/handler.d.ts +76 -0
- package/dist/shop/handler.js +156 -0
- package/dist/shop/index.d.ts +19 -0
- package/dist/shop/index.js +21 -0
- package/dist/shop/match.d.ts +23 -0
- package/dist/shop/match.js +58 -0
- package/dist/shop/memory.d.ts +13 -0
- package/dist/shop/memory.js +106 -0
- package/dist/shop/nodes.d.ts +47 -0
- package/dist/shop/nodes.js +106 -0
- package/dist/shop/orders.d.ts +47 -0
- package/dist/shop/orders.js +82 -0
- package/dist/shop/panel/index.d.ts +9 -0
- package/dist/shop/panel/index.js +14 -0
- package/dist/shop/panel/looks.d.ts +33 -0
- package/dist/shop/panel/looks.js +61 -0
- package/dist/shop/panel/mount.d.ts +68 -0
- package/dist/shop/panel/mount.js +221 -0
- package/dist/shop/panel/pay.d.ts +27 -0
- package/dist/shop/panel/pay.js +128 -0
- package/dist/shop/panel/qr.d.ts +9 -0
- package/dist/shop/panel/qr.js +281 -0
- package/dist/shop/panel/words.d.ts +89 -0
- package/dist/shop/panel/words.js +79 -0
- package/dist/shop/price.d.ts +23 -0
- package/dist/shop/price.js +43 -0
- package/dist/shop/scan.d.ts +24 -0
- package/dist/shop/scan.js +80 -0
- package/dist/shop/security.d.ts +10 -0
- package/dist/shop/security.js +56 -0
- package/dist/shop/store.d.ts +101 -0
- package/dist/shop/store.js +4 -0
- package/dist/shop/worker.d.ts +43 -0
- package/dist/shop/worker.js +76 -0
- package/package.json +21 -6
- package/src/admin/panel/mount.ts +7 -2
- package/src/player-signin/adapters/d1.sql +45 -0
- package/src/player-signin/adapters/d1.ts +120 -0
- package/src/player-signin/adapters/firestore.ts +194 -0
- package/src/player-signin/conformance.ts +184 -0
- package/src/player-signin/handler.ts +404 -0
- package/src/player-signin/host.ts +12 -0
- package/src/player-signin/index.ts +20 -0
- package/src/player-signin/memory.ts +94 -0
- package/src/player-signin/panel/index.ts +13 -0
- package/src/player-signin/panel/memo.ts +49 -0
- package/src/player-signin/panel/mount.ts +129 -0
- package/src/player-signin/panel/style.css +478 -0
- package/src/player-signin/panel/view.ts +433 -0
- package/src/player-signin/panel/words.ts +210 -0
- package/src/player-signin/scheme-check.ts +14 -0
- package/src/player-signin/scheme.ts +32 -0
- package/src/player-signin/types.ts +103 -0
- package/src/shop/adapters/d1.sql +52 -0
- package/src/shop/adapters/d1.ts +245 -0
- package/src/shop/adapters/firestore.ts +281 -0
- package/src/shop/address.ts +102 -0
- package/src/shop/amount.ts +66 -0
- package/src/shop/conformance.ts +232 -0
- package/src/shop/handler.ts +202 -0
- package/src/shop/index.ts +30 -0
- package/src/shop/match.ts +63 -0
- package/src/shop/memory.ts +102 -0
- package/src/shop/nodes.ts +115 -0
- package/src/shop/orders.ts +93 -0
- package/src/shop/panel/index.ts +19 -0
- package/src/shop/panel/looks.ts +106 -0
- package/src/shop/panel/mount.ts +280 -0
- package/src/shop/panel/pay.ts +192 -0
- package/src/shop/panel/qr.ts +256 -0
- package/src/shop/panel/style.css +538 -0
- package/src/shop/panel/words.ts +172 -0
- package/src/shop/price.ts +52 -0
- package/src/shop/scan.ts +90 -0
- package/src/shop/security.ts +50 -0
- package/src/shop/store.ts +99 -0
- package/src/shop/worker.ts +107 -0
|
@@ -0,0 +1,538 @@
|
|
|
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: 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
|
+
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
|
+
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
|
+
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,
|
|
6
|
+
--n133-ground, --n133-line, --n133-gem, --n133-gem-dark, --n133-on-gem, --n133-sun (the Buy key), --n133-warn (the test stamp), --n133-radius,
|
|
7
|
+
--n133-font. Those set the LIGHT theme; the dark one reads the twins with "dark-" in the name (--n133-dark-ink and so on), as the admin panel does.
|
|
8
|
+
The QR code stays dark on white in both themes: a wallet's camera reads it best that way. */
|
|
9
|
+
|
|
10
|
+
.n133-shop {
|
|
11
|
+
--font: var(--n133-font, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif);
|
|
12
|
+
--radius: var(--n133-radius, 18px);
|
|
13
|
+
--ink: var(--n133-ink, #16202e);
|
|
14
|
+
--ink-soft: var(--n133-ink-soft, #4a5568);
|
|
15
|
+
--paper: var(--n133-paper, #ffffff);
|
|
16
|
+
--ground: var(--n133-ground, #eef2f7);
|
|
17
|
+
--line: var(--n133-line, #dbe4ee);
|
|
18
|
+
--gem: var(--n133-gem, #20c9a6);
|
|
19
|
+
--gem-dark: var(--n133-gem-dark, #0e8a72);
|
|
20
|
+
--on-gem: var(--n133-on-gem, #062b23);
|
|
21
|
+
--sun: var(--n133-sun, #ffd23f);
|
|
22
|
+
--on-sun: var(--n133-on-sun, #3a2a00);
|
|
23
|
+
--warn: var(--n133-warn, #d6223b);
|
|
24
|
+
--paid: var(--n133-paid, #e6fbf5);
|
|
25
|
+
--shade: 10 20 35;
|
|
26
|
+
display: inline-block;
|
|
27
|
+
color: var(--ink);
|
|
28
|
+
color-scheme: light;
|
|
29
|
+
font: 16px/1.45 var(--font);
|
|
30
|
+
-webkit-text-size-adjust: 100%;
|
|
31
|
+
-webkit-tap-highlight-color: transparent;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.n133-shop[data-theme="dark"] {
|
|
35
|
+
--ink: var(--n133-dark-ink, #e7edf5);
|
|
36
|
+
--ink-soft: var(--n133-dark-ink-soft, #a9b5c5);
|
|
37
|
+
--paper: var(--n133-dark-paper, #171f2b);
|
|
38
|
+
--ground: var(--n133-dark-ground, #0f151e);
|
|
39
|
+
--line: var(--n133-dark-line, #34415a);
|
|
40
|
+
--gem: var(--n133-dark-gem, #20c9a6);
|
|
41
|
+
--gem-dark: var(--n133-dark-gem-dark, #3fd8b6);
|
|
42
|
+
--on-gem: var(--n133-dark-on-gem, #062b23);
|
|
43
|
+
--sun: var(--n133-dark-sun, #ffd23f);
|
|
44
|
+
--on-sun: var(--n133-dark-on-sun, #3a2a00);
|
|
45
|
+
--warn: var(--n133-dark-warn, #ff5d73);
|
|
46
|
+
--paid: var(--n133-dark-paid, #0f3a31);
|
|
47
|
+
--shade: 0 0 0;
|
|
48
|
+
color-scheme: dark;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.n133-shop *,
|
|
52
|
+
.n133-shop *::before,
|
|
53
|
+
.n133-shop *::after {
|
|
54
|
+
box-sizing: border-box;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.n133-shop [hidden] {
|
|
58
|
+
display: none !important;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* ---- the key ---- */
|
|
62
|
+
|
|
63
|
+
.n133-shop .n133s-key {
|
|
64
|
+
display: inline-flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
gap: 8px;
|
|
67
|
+
min-width: 48px;
|
|
68
|
+
min-height: 48px;
|
|
69
|
+
padding: 0 16px;
|
|
70
|
+
border: 1px solid var(--line);
|
|
71
|
+
border-radius: 999px;
|
|
72
|
+
background: var(--paper);
|
|
73
|
+
color: var(--ink);
|
|
74
|
+
font: 650 16px/1 var(--font);
|
|
75
|
+
cursor: pointer;
|
|
76
|
+
touch-action: manipulation;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.n133-shop :is(.n133s-key, .n133s-btn, .n133s-close, .n133s-tab, summary):focus-visible {
|
|
80
|
+
outline: 3px solid var(--gem-dark);
|
|
81
|
+
outline-offset: 2px;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* ---- the sheet ---- */
|
|
85
|
+
|
|
86
|
+
.n133-shop .n133s-sheet {
|
|
87
|
+
width: min(480px, 100%);
|
|
88
|
+
max-width: 100%;
|
|
89
|
+
max-height: min(92vh, 760px);
|
|
90
|
+
max-height: min(92dvh, 760px);
|
|
91
|
+
margin: auto auto 0;
|
|
92
|
+
padding: 0;
|
|
93
|
+
border: 0;
|
|
94
|
+
border-radius: var(--radius) var(--radius) 0 0;
|
|
95
|
+
background: var(--paper);
|
|
96
|
+
color: var(--ink);
|
|
97
|
+
box-shadow: 0 10px 30px rgb(var(--shade) / 0.25);
|
|
98
|
+
overflow: hidden;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.n133-shop .n133s-sheet[open] {
|
|
102
|
+
display: flex;
|
|
103
|
+
flex-direction: column;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.n133-shop .n133s-sheet::backdrop {
|
|
107
|
+
background: rgb(10 20 35 / 0.45);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@media (min-width: 700px) {
|
|
111
|
+
.n133-shop .n133s-sheet {
|
|
112
|
+
margin: auto;
|
|
113
|
+
border-radius: var(--radius);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* A phone on its side: the sheet uses the whole height, and stands in the middle. */
|
|
118
|
+
@media (orientation: landscape) and (max-height: 520px) {
|
|
119
|
+
.n133-shop .n133s-sheet {
|
|
120
|
+
width: min(760px, calc(100% - 16px));
|
|
121
|
+
max-height: calc(100vh - 12px);
|
|
122
|
+
max-height: calc(100dvh - 12px);
|
|
123
|
+
margin: auto;
|
|
124
|
+
border-radius: var(--radius);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.n133-shop .n133s-head {
|
|
129
|
+
display: flex;
|
|
130
|
+
flex: none;
|
|
131
|
+
align-items: center;
|
|
132
|
+
justify-content: space-between;
|
|
133
|
+
gap: 8px;
|
|
134
|
+
padding: 8px 8px 4px 20px;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.n133-shop .n133s-title {
|
|
138
|
+
margin: 0;
|
|
139
|
+
outline: none;
|
|
140
|
+
font: 750 21px/1.2 var(--font);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.n133-shop .n133s-close {
|
|
144
|
+
display: grid;
|
|
145
|
+
place-items: center;
|
|
146
|
+
width: 48px;
|
|
147
|
+
height: 48px;
|
|
148
|
+
border: 0;
|
|
149
|
+
border-radius: 12px;
|
|
150
|
+
background: transparent;
|
|
151
|
+
color: var(--ink);
|
|
152
|
+
cursor: pointer;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.n133-shop .n133s-body {
|
|
156
|
+
flex: 1 1 auto;
|
|
157
|
+
min-height: 0;
|
|
158
|
+
padding: 4px 20px max(16px, env(safe-area-inset-bottom));
|
|
159
|
+
overflow: auto;
|
|
160
|
+
overscroll-behavior: contain;
|
|
161
|
+
touch-action: pan-y;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.n133-shop p {
|
|
165
|
+
margin: 8px 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.n133-shop .n133s-status {
|
|
169
|
+
min-height: 1.4em;
|
|
170
|
+
font-weight: 600;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.n133-shop .n133s-fine {
|
|
174
|
+
color: var(--ink-soft);
|
|
175
|
+
font-size: 16px;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* ---- keys ---- */
|
|
179
|
+
|
|
180
|
+
.n133-shop .n133s-btn {
|
|
181
|
+
display: inline-flex;
|
|
182
|
+
align-items: center;
|
|
183
|
+
justify-content: center;
|
|
184
|
+
gap: 8px;
|
|
185
|
+
min-width: 48px;
|
|
186
|
+
min-height: 48px;
|
|
187
|
+
padding: 0 18px;
|
|
188
|
+
border: 2px solid var(--line);
|
|
189
|
+
border-radius: 999px;
|
|
190
|
+
background: var(--paper);
|
|
191
|
+
color: var(--ink);
|
|
192
|
+
font: 700 16px/1 var(--font);
|
|
193
|
+
text-decoration: none;
|
|
194
|
+
white-space: nowrap;
|
|
195
|
+
cursor: pointer;
|
|
196
|
+
touch-action: manipulation;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.n133-shop .n133s-btn:disabled {
|
|
200
|
+
opacity: 0.55;
|
|
201
|
+
cursor: default;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/* A main key that is off (the wallet key in test mode) must never look like one that works, in either theme. */
|
|
205
|
+
.n133-shop .n133s-primary:disabled {
|
|
206
|
+
border-color: var(--line);
|
|
207
|
+
border-style: dashed;
|
|
208
|
+
background: var(--ground);
|
|
209
|
+
color: var(--ink-soft);
|
|
210
|
+
opacity: 1;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.n133-shop .n133s-primary {
|
|
214
|
+
border-color: var(--gem-dark);
|
|
215
|
+
background: var(--gem);
|
|
216
|
+
color: var(--on-gem);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.n133-shop .n133s-buy {
|
|
220
|
+
border-color: #b98a00;
|
|
221
|
+
background: var(--sun);
|
|
222
|
+
color: var(--on-sun);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.n133-shop .n133s-wide {
|
|
226
|
+
width: 100%;
|
|
227
|
+
margin-top: 10px;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.n133-shop .n133s-small {
|
|
231
|
+
padding: 0 14px;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/* ---- the looks ---- */
|
|
235
|
+
|
|
236
|
+
.n133-shop .n133s-tabs {
|
|
237
|
+
display: flex;
|
|
238
|
+
gap: 6px;
|
|
239
|
+
margin: 4px -20px 6px;
|
|
240
|
+
padding: 0 20px 6px;
|
|
241
|
+
overflow-x: auto;
|
|
242
|
+
scrollbar-width: none;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.n133-shop .n133s-tab {
|
|
246
|
+
flex: none;
|
|
247
|
+
min-width: 48px;
|
|
248
|
+
min-height: 48px;
|
|
249
|
+
padding: 0 16px;
|
|
250
|
+
border: 1px solid var(--line);
|
|
251
|
+
border-radius: 999px;
|
|
252
|
+
background: var(--ground);
|
|
253
|
+
color: var(--ink);
|
|
254
|
+
font: 650 16px/1 var(--font);
|
|
255
|
+
cursor: pointer;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.n133-shop .n133s-tab[aria-selected="true"] {
|
|
259
|
+
border-color: var(--gem-dark);
|
|
260
|
+
background: var(--gem);
|
|
261
|
+
color: var(--on-gem);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.n133-shop .n133s-looks {
|
|
265
|
+
margin: 0;
|
|
266
|
+
padding: 0;
|
|
267
|
+
list-style: none;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.n133-shop .n133s-look {
|
|
271
|
+
display: flex;
|
|
272
|
+
align-items: center;
|
|
273
|
+
gap: 12px;
|
|
274
|
+
padding: 10px 0;
|
|
275
|
+
border-bottom: 1px solid var(--line);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.n133-shop .n133s-look-worn .n133s-look-text strong::after {
|
|
279
|
+
content: " ✓";
|
|
280
|
+
color: var(--gem-dark);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.n133-shop .n133s-look-text {
|
|
284
|
+
display: flex;
|
|
285
|
+
flex: 1;
|
|
286
|
+
flex-direction: column;
|
|
287
|
+
min-width: 0;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.n133-shop .n133s-look-text strong {
|
|
291
|
+
overflow: hidden;
|
|
292
|
+
text-overflow: ellipsis;
|
|
293
|
+
white-space: nowrap;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.n133-shop .n133s-look-text small {
|
|
297
|
+
color: var(--ink-soft);
|
|
298
|
+
font-size: 16px;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.n133-shop .n133s-swatch {
|
|
302
|
+
position: relative;
|
|
303
|
+
display: grid;
|
|
304
|
+
flex: none;
|
|
305
|
+
place-items: center;
|
|
306
|
+
width: 44px;
|
|
307
|
+
height: 44px;
|
|
308
|
+
border: 2px solid rgb(0 0 0 / 0.12);
|
|
309
|
+
border-radius: 50%;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.n133-shop .n133s-swatch-item {
|
|
313
|
+
border-radius: 12px;
|
|
314
|
+
background: var(--ground);
|
|
315
|
+
color: var(--ink-soft);
|
|
316
|
+
font: 800 18px/1 var(--font);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.n133-shop .n133s-swatch-plain {
|
|
320
|
+
border: 2px dashed var(--line);
|
|
321
|
+
background: transparent;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.n133-shop .n133s-swatch-hat {
|
|
325
|
+
position: absolute;
|
|
326
|
+
left: 9px;
|
|
327
|
+
top: -6px;
|
|
328
|
+
width: 22px;
|
|
329
|
+
height: 12px;
|
|
330
|
+
border: 2px solid rgb(0 0 0 / 0.15);
|
|
331
|
+
border-radius: 12px 12px 3px 3px;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.n133-shop .n133s-hat-tophat {
|
|
335
|
+
top: -12px;
|
|
336
|
+
height: 18px;
|
|
337
|
+
border-radius: 3px;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.n133-shop .n133s-hat-crown {
|
|
341
|
+
border-radius: 2px;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/* ---- pay ---- */
|
|
345
|
+
|
|
346
|
+
.n133-shop .n133s-pay-what {
|
|
347
|
+
margin-top: 0;
|
|
348
|
+
color: var(--ink-soft);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.n133-shop .n133s-amount {
|
|
352
|
+
display: flex;
|
|
353
|
+
flex-wrap: wrap;
|
|
354
|
+
align-items: baseline;
|
|
355
|
+
gap: 6px;
|
|
356
|
+
margin: 6px 0 12px;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.n133-shop .n133s-amount strong {
|
|
360
|
+
font-size: 30px;
|
|
361
|
+
font-variant-numeric: tabular-nums;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.n133-shop .n133s-amount span {
|
|
365
|
+
color: var(--ink-soft);
|
|
366
|
+
font-weight: 700;
|
|
367
|
+
}
|
|
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
|
+
|
|
374
|
+
.n133-shop .n133s-qr {
|
|
375
|
+
display: grid;
|
|
376
|
+
place-items: center;
|
|
377
|
+
width: min(248px, 70vw);
|
|
378
|
+
aspect-ratio: 1;
|
|
379
|
+
margin: 0 auto;
|
|
380
|
+
padding: 8px;
|
|
381
|
+
border: 2px solid var(--line);
|
|
382
|
+
border-radius: 16px;
|
|
383
|
+
background: #ffffff;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.n133-shop .n133s-qr-code {
|
|
387
|
+
display: block;
|
|
388
|
+
width: 100%;
|
|
389
|
+
height: 100%;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.n133-shop .n133s-test-pay {
|
|
393
|
+
position: relative;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.n133-shop .n133s-qr-blocked {
|
|
397
|
+
overflow: hidden;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.n133-shop .n133s-qr-fake {
|
|
401
|
+
width: 100%;
|
|
402
|
+
height: 100%;
|
|
403
|
+
border-radius: 8px;
|
|
404
|
+
background: repeating-conic-gradient(#c8ced6 0 25%, #eef1f4 0 50%) 0 0 / 22px 22px;
|
|
405
|
+
filter: blur(3px);
|
|
406
|
+
opacity: 0.7;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/* TEST MODE: DO NOT PAY. Large, red, over the place of the QR code. Never hide it or make it small. */
|
|
410
|
+
.n133-shop .n133s-stamp {
|
|
411
|
+
position: absolute;
|
|
412
|
+
left: 50%;
|
|
413
|
+
top: min(124px, 35vw);
|
|
414
|
+
width: max-content;
|
|
415
|
+
max-width: 96%;
|
|
416
|
+
padding: 10px 16px;
|
|
417
|
+
border: 4px solid var(--warn);
|
|
418
|
+
border-radius: 10px;
|
|
419
|
+
background: rgb(255 255 255 / 0.94);
|
|
420
|
+
color: #d6223b;
|
|
421
|
+
font: 900 clamp(20px, 6.4vw, 28px)/1.1 var(--font);
|
|
422
|
+
letter-spacing: 0.02em;
|
|
423
|
+
text-align: center;
|
|
424
|
+
transform: translate(-50%, -50%) rotate(-8deg);
|
|
425
|
+
box-shadow: 0 6px 18px rgb(214 34 59 / 0.25);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.n133-shop .n133s-stamp span {
|
|
429
|
+
display: block;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.n133-shop .n133s-by-hand {
|
|
433
|
+
margin-top: 12px;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.n133-shop .n133s-by-hand summary {
|
|
437
|
+
min-height: 48px;
|
|
438
|
+
line-height: 48px;
|
|
439
|
+
font-weight: 700;
|
|
440
|
+
cursor: pointer;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.n133-shop .n133s-row {
|
|
444
|
+
display: flex;
|
|
445
|
+
align-items: center;
|
|
446
|
+
gap: 8px;
|
|
447
|
+
margin: 6px 0;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.n133-shop .n133s-label {
|
|
451
|
+
width: 64px;
|
|
452
|
+
flex: none;
|
|
453
|
+
color: var(--ink-soft);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
.n133-shop .n133s-row code {
|
|
457
|
+
flex: 1;
|
|
458
|
+
min-width: 0;
|
|
459
|
+
word-break: break-all;
|
|
460
|
+
font-size: 16px;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.n133-shop .n133s-paid {
|
|
464
|
+
padding: 18px;
|
|
465
|
+
border-radius: 16px;
|
|
466
|
+
background: var(--paid);
|
|
467
|
+
text-align: center;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.n133-shop .n133s-paid strong {
|
|
471
|
+
font-size: 22px;
|
|
472
|
+
color: var(--gem-dark);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/* The wallet key stays in view at the foot of the sheet. */
|
|
476
|
+
.n133-shop .n133s-foot {
|
|
477
|
+
position: sticky;
|
|
478
|
+
bottom: calc(-1 * max(16px, env(safe-area-inset-bottom)));
|
|
479
|
+
margin: 8px -20px 0;
|
|
480
|
+
padding: 0 20px max(12px, env(safe-area-inset-bottom));
|
|
481
|
+
background: linear-gradient(to bottom, transparent, var(--paper) 14px);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
.n133-shop .n133s-foot .n133s-wide {
|
|
485
|
+
margin-top: 6px;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/* A phone on its side: the QR code (or the stamp) alone on the left, as big as the height allows; the words, the amount and the wallet key on the right. */
|
|
489
|
+
@media (orientation: landscape) and (max-height: 520px) {
|
|
490
|
+
.n133-shop .n133s-pay {
|
|
491
|
+
display: grid;
|
|
492
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
493
|
+
grid-template-areas: "qr what" "qr amount" "qr side";
|
|
494
|
+
grid-template-rows: auto auto 1fr;
|
|
495
|
+
gap: 0 20px;
|
|
496
|
+
align-items: start;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
.n133-shop .n133s-pay-main {
|
|
500
|
+
display: contents;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
.n133-shop .n133s-pay-what {
|
|
504
|
+
grid-area: what;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
.n133-shop .n133s-amount {
|
|
508
|
+
grid-area: amount;
|
|
509
|
+
margin: 0 0 4px;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
.n133-shop :is(.n133s-live-pay, .n133s-test-pay) {
|
|
513
|
+
grid-area: qr;
|
|
514
|
+
width: min(250px, calc(100vh - 96px));
|
|
515
|
+
width: min(250px, calc(100dvh - 96px));
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.n133-shop .n133s-pay-side {
|
|
519
|
+
grid-area: side;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
.n133-shop .n133s-qr {
|
|
523
|
+
width: 100%;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
.n133-shop .n133s-stamp {
|
|
527
|
+
top: 50%;
|
|
528
|
+
font-size: 22px;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
.n133-shop .n133s-amount strong {
|
|
532
|
+
font-size: 24px;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
.n133-shop .n133s-pay-side .n133s-fine:first-child {
|
|
536
|
+
margin-top: 0;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
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
|
+
|
|
10
|
+
import type { Hat } from "../../looks/look.js";
|
|
11
|
+
|
|
12
|
+
export type ShopWords = {
|
|
13
|
+
// ---- the looks sheet ----
|
|
14
|
+
/** The sheet's title, the Looks key's word and the sheet's name for a screen reader. The option `title` wins over it. */
|
|
15
|
+
title: string;
|
|
16
|
+
/** The close key's name for a screen reader. */
|
|
17
|
+
close: string;
|
|
18
|
+
/** The tabs' name for a screen reader. */
|
|
19
|
+
kinds: string;
|
|
20
|
+
/** The outfits' tab. */
|
|
21
|
+
outfits: string;
|
|
22
|
+
/** The tab of a wear slot of the game's own ("shovel"). The option `slots` wins over it. */
|
|
23
|
+
slotTab: (slot: string) => string;
|
|
24
|
+
/** The plain choice of a wear slot ("No shovel"). The option `slots` wins over it. */
|
|
25
|
+
slotPlain: (slot: string) => string;
|
|
26
|
+
/** The line under an outfit: what it changes (its hat, or the colour only). */
|
|
27
|
+
outfitLine: (hat: Hat | null) => string;
|
|
28
|
+
/** The line under an item of a wear slot. */
|
|
29
|
+
itemLine: (slot: string) => string;
|
|
30
|
+
/** Added to the line of a free look, and the plain choice's line. */
|
|
31
|
+
free: string;
|
|
32
|
+
/** Added to the line of a look the player owns. */
|
|
33
|
+
yours: string;
|
|
34
|
+
wear: string;
|
|
35
|
+
wearing: string;
|
|
36
|
+
/** The Buy key, with the look's price ("$0.25"). */
|
|
37
|
+
buy: (price: string) => string;
|
|
38
|
+
/** Under the list. */
|
|
39
|
+
looksNote: string;
|
|
40
|
+
/** Under the list and at Buy, while nobody is signed in. */
|
|
41
|
+
signInFirst: string;
|
|
42
|
+
|
|
43
|
+
// ---- Buy ----
|
|
44
|
+
/** The title of Buy and of the pay sheet. */
|
|
45
|
+
buyTitle: (look: string) => string;
|
|
46
|
+
making: string;
|
|
47
|
+
/** An order that did not start, with no words from the server. */
|
|
48
|
+
orderFailed: string;
|
|
49
|
+
/** The words of a refusal: the server's own (`text`, in English) and its HTTP status (0: no answer). Default: the server's words. */
|
|
50
|
+
refusal: (text: string, status: number) => string;
|
|
51
|
+
/** The key back to the looks. */
|
|
52
|
+
back: string;
|
|
53
|
+
/** The key that signs in again after "Sign in first." (401). */
|
|
54
|
+
signIn: string;
|
|
55
|
+
|
|
56
|
+
// ---- the pay sheet ----
|
|
57
|
+
/** The line under the title. */
|
|
58
|
+
lookLine: (look: string) => string;
|
|
59
|
+
/** The amount in XNO, large ("0.25 XNO"). */
|
|
60
|
+
amount: (xno: string) => string;
|
|
61
|
+
/** The amount in dollars, beside it ("= $0.25"). */
|
|
62
|
+
usd: (usd: string) => string;
|
|
63
|
+
/** Above the amount, live only: the player must send this exact amount. */
|
|
64
|
+
sendExactly: string;
|
|
65
|
+
/** Under the stamp in test mode: nothing can be paid here. */
|
|
66
|
+
testNote: string;
|
|
67
|
+
/** The wallet key in test mode, off, with its reason. */
|
|
68
|
+
walletOff: string;
|
|
69
|
+
/** The wallet key (live). */
|
|
70
|
+
wallet: string;
|
|
71
|
+
/** The QR code's name for a screen reader. */
|
|
72
|
+
qrLabel: (xno: string) => string;
|
|
73
|
+
/** The fold with the amount and the address to type by hand. */
|
|
74
|
+
byHand: string;
|
|
75
|
+
amountLabel: string;
|
|
76
|
+
/** The address's label in the fold. */
|
|
77
|
+
toLabel: string;
|
|
78
|
+
/** In the fold: another amount does not match the order. */
|
|
79
|
+
byHandNote: string;
|
|
80
|
+
copy: string;
|
|
81
|
+
copied: string;
|
|
82
|
+
copyFailed: string;
|
|
83
|
+
/** The time left (live only): the order ends then. */
|
|
84
|
+
endsIn: (time: string) => string;
|
|
85
|
+
waiting: string;
|
|
86
|
+
noConnection: string;
|
|
87
|
+
stillWaiting: string;
|
|
88
|
+
checkAgain: string;
|
|
89
|
+
paid: string;
|
|
90
|
+
paidLine: (look: string) => string;
|
|
91
|
+
/** A payment after the order ended: it gives no look, and the creator sends it back. */
|
|
92
|
+
paidLate: string;
|
|
93
|
+
/** The order ended: a payment sent in time still counts. */
|
|
94
|
+
expired: string;
|
|
95
|
+
/** The price's sources ("CoinGecko", "Kraken", "KuCoin"). "Powered by CoinGecko" is added when CoinGecko is one and the words leave it out. */
|
|
96
|
+
prices: (sources: readonly string[]) => string;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const HATS: Record<Hat, string> = { cap: "with a cap", beanie: "with a beanie", crown: "with a crown", tophat: "with a top hat" };
|
|
100
|
+
const spaced = (slot: string) => slot.replace(/-/g, " ");
|
|
101
|
+
|
|
102
|
+
/** CoinGecko's words: its API terms ask for them wherever its price shows. */
|
|
103
|
+
export const COINGECKO_CREDIT = "Powered by CoinGecko";
|
|
104
|
+
|
|
105
|
+
export const SHOP_WORDS: ShopWords = {
|
|
106
|
+
title: "Looks",
|
|
107
|
+
close: "Close",
|
|
108
|
+
kinds: "Kinds of looks",
|
|
109
|
+
outfits: "Outfits",
|
|
110
|
+
slotTab: (slot) => spaced(slot.charAt(0).toUpperCase() + slot.slice(1)),
|
|
111
|
+
slotPlain: (slot) => `No ${spaced(slot)}`,
|
|
112
|
+
outfitLine: (hat) => (hat ? HATS[hat] : "colour only"),
|
|
113
|
+
itemLine: (slot) => spaced(slot),
|
|
114
|
+
free: "free",
|
|
115
|
+
yours: "yours",
|
|
116
|
+
wear: "Wear",
|
|
117
|
+
wearing: "Wearing",
|
|
118
|
+
buy: (price) => `Buy ${price}`,
|
|
119
|
+
looksNote: "Looks never change play. Pay in Nano, straight to the creator.",
|
|
120
|
+
signInFirst: "Sign in first to buy a look.",
|
|
121
|
+
|
|
122
|
+
buyTitle: (look) => `Buy ${look}`,
|
|
123
|
+
making: "Making your order…",
|
|
124
|
+
orderFailed: "The order did not start. Try again.",
|
|
125
|
+
refusal: (text) => text,
|
|
126
|
+
back: "Back",
|
|
127
|
+
signIn: "Sign in",
|
|
128
|
+
|
|
129
|
+
lookLine: () => "It never changes play.",
|
|
130
|
+
amount: (xno) => `${xno} XNO`,
|
|
131
|
+
usd: (usd) => `= ${usd}`,
|
|
132
|
+
sendExactly: "Send exactly",
|
|
133
|
+
testNote: "Nothing can be paid here.",
|
|
134
|
+
walletOff: "Wallet off: test mode",
|
|
135
|
+
wallet: "Open wallet",
|
|
136
|
+
qrLabel: (xno) => `QR code to pay ${xno} XNO`,
|
|
137
|
+
byHand: "Enter by hand",
|
|
138
|
+
amountLabel: "Amount",
|
|
139
|
+
toLabel: "To",
|
|
140
|
+
byHandNote: "Another amount does not match.",
|
|
141
|
+
copy: "Copy",
|
|
142
|
+
copied: "Copied",
|
|
143
|
+
copyFailed: "Copy failed",
|
|
144
|
+
endsIn: (time) => `Ends in ${time}.`,
|
|
145
|
+
waiting: "Waiting for payment…",
|
|
146
|
+
noConnection: "No connection. Trying again…",
|
|
147
|
+
stillWaiting: "Still waiting. Already paid? Press Check again.",
|
|
148
|
+
checkAgain: "Check again",
|
|
149
|
+
paid: "Paid. Thank you!",
|
|
150
|
+
paidLine: (look) => `${look} is yours.`,
|
|
151
|
+
paidLate: "Paid after the order ended, so no look. The creator sends it back.",
|
|
152
|
+
expired: "This order ended. Paid in time? Wait here: it still counts.",
|
|
153
|
+
prices: (sources) => {
|
|
154
|
+
const others = sources.filter((s) => s !== "CoinGecko");
|
|
155
|
+
const credit = sources.includes("CoinGecko") ? ` ${COINGECKO_CREDIT}.` : "";
|
|
156
|
+
return others.length ? `Price: ${others.join(", ")}.${credit}` : credit.trim();
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/** The game's words over the defaults. A key the game leaves out, or gives as undefined, keeps its default. */
|
|
161
|
+
export function shopWords(over?: Partial<ShopWords>): ShopWords {
|
|
162
|
+
const out = { ...SHOP_WORDS };
|
|
163
|
+
for (const [k, v] of Object.entries(over ?? {})) if (v !== undefined && k in SHOP_WORDS) (out as Record<string, unknown>)[k] = v;
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The price's line: the game's words, with "Powered by CoinGecko" added when CoinGecko is a source and the words leave it out. */
|
|
168
|
+
export function priceLine(words: ShopWords, sources: readonly string[]): string {
|
|
169
|
+
const line = words.prices(sources).trim();
|
|
170
|
+
if (!sources.includes("CoinGecko") || line.includes(COINGECKO_CREDIT)) return line;
|
|
171
|
+
return `${line}${line ? " " : ""}${COINGECKO_CREDIT}.`;
|
|
172
|
+
}
|