@johnmorrisdotca/kumimoji 1.2.2 → 1.3.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 +33 -1
- package/CONTRIBUTING.md +11 -2
- package/README.md +80 -39
- package/dist/check.d.ts +26 -1
- package/dist/check.js +30 -3
- package/dist/computer.types.d.ts +24 -2
- package/dist/computerPlay.d.ts +45 -2
- package/dist/computerPlay.js +56 -6
- package/dist/computerTurn.d.ts +42 -3
- package/dist/computerTurn.js +47 -5
- package/dist/daily.d.ts +22 -2
- package/dist/daily.js +23 -3
- package/dist/element-define.d.ts +1 -1
- package/dist/element-define.js +3 -13
- package/dist/element.d.ts +85 -4
- package/dist/element.js +359 -77
- package/dist/embed-day.d.ts +4 -0
- package/dist/embed-day.js +23 -0
- package/dist/export.d.ts +123 -6
- package/dist/export.js +109 -9
- package/dist/generate.d.ts +14 -0
- package/dist/generate.js +32 -5
- package/dist/grid.d.ts +181 -10
- package/dist/grid.js +148 -11
- package/dist/help.d.ts +36 -3
- package/dist/help.js +36 -3
- package/dist/index.d.ts +22 -22
- package/dist/index.js +22 -22
- package/dist/judge.d.ts +14 -0
- package/dist/judge.js +14 -0
- package/dist/kana.d.ts +23 -1
- package/dist/kana.js +62 -7
- package/dist/kumimoji.types.d.ts +70 -5
- package/dist/party.d.ts +268 -14
- package/dist/party.js +296 -19
- package/dist/party.types.d.ts +50 -2
- package/dist/partyKept.d.ts +74 -4
- package/dist/partyKept.js +127 -12
- package/dist/partySeats.d.ts +92 -6
- package/dist/partySeats.js +83 -8
- package/dist/partyTurns.d.ts +171 -5
- package/dist/partyTurns.js +187 -8
- package/dist/placement.d.ts +50 -2
- package/dist/placement.js +35 -2
- package/dist/play.d.ts +247 -15
- package/dist/play.js +251 -18
- package/dist/random.d.ts +34 -3
- package/dist/random.js +23 -2
- package/dist/react.d.ts +42 -4
- package/dist/react.js +54 -7
- package/dist/showcase.d.ts +83 -7
- package/dist/showcase.js +61 -7
- package/dist/strings.d.ts +54 -5
- package/dist/strings.js +34 -3
- package/dist/tableView.d.ts +151 -8
- package/dist/tableView.js +129 -6
- package/dist/tileFace.d.ts +59 -5
- package/dist/tileFace.js +48 -4
- package/dist/tileFamily.d.ts +9 -0
- package/dist/tileFamily.js +9 -0
- package/dist/tileWords.d.ts +93 -6
- package/dist/tileWords.js +94 -12
- package/dist/tileWordsModule.d.ts +12 -2
- package/dist/tileWordsModule.js +14 -2
- package/dist/tiles.constants.d.ts +99 -5
- package/dist/tiles.constants.js +171 -13
- package/dist/turn.d.ts +82 -4
- package/dist/turn.js +88 -5
- package/dist/ui/board.d.ts +60 -5
- package/dist/ui/board.js +44 -4
- package/dist/ui/draw.d.ts +62 -0
- package/dist/ui/draw.js +229 -0
- package/dist/ui/mount.d.ts +41 -2
- package/dist/ui/mount.js +190 -265
- package/dist/ui/style.d.ts +6 -1
- package/dist/ui/style.js +6 -0
- package/dist/ui.d.ts +3 -3
- package/dist/ui.js +2 -2
- package/dist/version.d.ts +9 -2
- package/dist/version.js +9 -2
- package/dist/wilds.d.ts +22 -1
- package/dist/wilds.js +25 -3
- package/package.json +68 -61
package/dist/ui/mount.js
CHANGED
|
@@ -1,31 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { kumimojiFromJSON, kumimojiToJSON, kumimojiToText } from "../export.js";
|
|
2
2
|
import { generateKumimoji } from "../generate.js";
|
|
3
3
|
import { judgeWithWords } from "../judge.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { deal, isFinished, liftToHand, moveOnTable, placeFromHand, swapWithHand } from "../play.js";
|
|
5
|
+
import { kumimojiStrings } from "../strings.js";
|
|
6
6
|
import { KUMIMOJI_HANDS } from "../tiles.constants.js";
|
|
7
|
-
import { tileFace } from "../tileFace.js";
|
|
8
7
|
import { loadTileWords } from "../tileWords.js";
|
|
9
|
-
import {
|
|
8
|
+
import { button, node, tableDrawing } from "./draw.js";
|
|
10
9
|
import { KUMIMOJI_STYLE } from "./style.js";
|
|
11
|
-
/** What a wild nobody has given a letter yet shows in the hand. */
|
|
12
|
-
const WILD_MARK = "★";
|
|
13
|
-
function node(name, className, text) {
|
|
14
|
-
const made = document.createElement(name);
|
|
15
|
-
if (className !== undefined)
|
|
16
|
-
made.className = className;
|
|
17
|
-
if (text !== undefined)
|
|
18
|
-
made.textContent = text;
|
|
19
|
-
return made;
|
|
20
|
-
}
|
|
21
|
-
function button(text, onClick, primary = false, testId) {
|
|
22
|
-
const made = node("button", primary ? "km-button km-primary" : "km-button", text);
|
|
23
|
-
made.type = "button";
|
|
24
|
-
if (testId !== undefined)
|
|
25
|
-
made.dataset.testid = testId;
|
|
26
|
-
made.addEventListener("click", onClick);
|
|
27
|
-
return made;
|
|
28
|
-
}
|
|
29
10
|
/** Hand a string to the person as a file of that name. */
|
|
30
11
|
function save(name, type, text) {
|
|
31
12
|
const url = URL.createObjectURL(new Blob([text], { type }));
|
|
@@ -46,30 +27,60 @@ function save(name, type, text) {
|
|
|
46
27
|
* game loads back.
|
|
47
28
|
*
|
|
48
29
|
* The word list is fetched the first time a game in its language is dealt.
|
|
30
|
+
* @param target - The host element for this table.
|
|
31
|
+
* @param options - Initial deal or saved state, presentation and callbacks.
|
|
32
|
+
* @returns The native handle, including ready and presentation updates without redealing.
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* import { mountKumimoji } from "@johnmorrisdotca/kumimoji/ui";
|
|
36
|
+
* if (typeof document !== "undefined") {
|
|
37
|
+
* const table = mountKumimoji(document.createElement("div"), { keep: false });
|
|
38
|
+
* void table.ready.then(() => table.destroy());
|
|
39
|
+
* }
|
|
40
|
+
* console.log(typeof mountKumimoji);
|
|
41
|
+
* ```
|
|
49
42
|
*/
|
|
50
43
|
export function mountKumimoji(target, options = {}) {
|
|
51
|
-
let
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
44
|
+
let current = options;
|
|
45
|
+
let settings = {
|
|
46
|
+
language: current.language,
|
|
47
|
+
hand: current.hand,
|
|
48
|
+
level: current.level,
|
|
49
|
+
gameLength: current.gameLength,
|
|
50
|
+
diagonals: current.diagonals,
|
|
51
|
+
seed: current.seed,
|
|
52
|
+
};
|
|
53
|
+
const pageLocale = () => typeof document !== "undefined" && document.documentElement.lang.toLowerCase().startsWith("ja") ? "ja" : "en";
|
|
54
|
+
let own = current.strings ?? {};
|
|
55
|
+
const openingLocale = current.locale ?? pageLocale();
|
|
61
56
|
// Where a keyboard is: the square and the hand tile that Tab lands on, and the one the arrow keys move from.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
57
|
+
const session = {
|
|
58
|
+
play: null,
|
|
59
|
+
finishedAt: null,
|
|
60
|
+
started: 0,
|
|
61
|
+
words: null,
|
|
62
|
+
held: null,
|
|
63
|
+
onSquare: null,
|
|
64
|
+
onHand: 0,
|
|
65
|
+
say: kumimojiStrings(openingLocale, own),
|
|
66
|
+
locale: openingLocale,
|
|
67
|
+
note: "",
|
|
68
|
+
destroyed: false,
|
|
69
|
+
};
|
|
70
|
+
let dealt = null;
|
|
67
71
|
// Each deal and each game put on the table takes a number; one that is overtaken while its list loads gives way.
|
|
68
72
|
let asked = 0;
|
|
73
|
+
// The drawing is built once the elements exist. Until then nothing calls these.
|
|
74
|
+
const bridge = {
|
|
75
|
+
render: () => { },
|
|
76
|
+
renderStatus: () => { },
|
|
77
|
+
newGame: () => { },
|
|
78
|
+
};
|
|
79
|
+
const render = () => bridge.render();
|
|
69
80
|
const root = node("div", "km-root");
|
|
70
81
|
root.dataset.testid = "km-root";
|
|
71
82
|
root.dataset.state = "dealing";
|
|
72
|
-
for (const [name, value] of Object.entries(
|
|
83
|
+
for (const [name, value] of Object.entries(current.theme ?? {}))
|
|
73
84
|
root.style.setProperty(name, value);
|
|
74
85
|
const style = node("style");
|
|
75
86
|
style.textContent = KUMIMOJI_STYLE;
|
|
@@ -87,14 +98,20 @@ export function mountKumimoji(target, options = {}) {
|
|
|
87
98
|
const picker = node("div", "km-picker");
|
|
88
99
|
picker.dataset.testid = "km-picker";
|
|
89
100
|
const controls = node("div", "km-controls");
|
|
101
|
+
controls.hidden = current.controls === false;
|
|
90
102
|
controls.dataset.testid = "km-controls";
|
|
91
103
|
const keep = node("details", "km-keep");
|
|
92
104
|
keep.dataset.testid = "km-keep";
|
|
93
105
|
root.append(style, status, board, notes, picker, hand, controls);
|
|
94
|
-
if (
|
|
106
|
+
if (current.keep !== false)
|
|
95
107
|
root.append(keep);
|
|
96
108
|
target.append(root);
|
|
97
|
-
const savedNow = () =>
|
|
109
|
+
const savedNow = () => {
|
|
110
|
+
const now = session.play;
|
|
111
|
+
if (now === null || dealt === null)
|
|
112
|
+
return null;
|
|
113
|
+
return { deal: dealt, play: now, elapsedMs: (session.finishedAt ?? Date.now()) - session.started };
|
|
114
|
+
};
|
|
98
115
|
// The saving and loading are made once, so the fold stays open or shut as the person left it; only their words change.
|
|
99
116
|
const keepTitle = node("summary", "km-keep-title");
|
|
100
117
|
const keepNote = node("p", "km-note");
|
|
@@ -107,15 +124,15 @@ export function mountKumimoji(target, options = {}) {
|
|
|
107
124
|
file.dataset.testid = "km-file";
|
|
108
125
|
const read = async (text) => {
|
|
109
126
|
const loaded = await kumimojiFromJSON(text).catch(() => null);
|
|
110
|
-
if (destroyed)
|
|
127
|
+
if (session.destroyed)
|
|
111
128
|
return;
|
|
112
129
|
if (loaded === null) {
|
|
113
|
-
note = say.loadBad;
|
|
130
|
+
session.note = session.say.loadBad;
|
|
114
131
|
render();
|
|
115
132
|
return;
|
|
116
133
|
}
|
|
117
134
|
await handle.setGame(loaded);
|
|
118
|
-
note = say.loaded;
|
|
135
|
+
session.note = session.say.loaded;
|
|
119
136
|
render();
|
|
120
137
|
};
|
|
121
138
|
file.addEventListener("change", () => {
|
|
@@ -133,60 +150,59 @@ export function mountKumimoji(target, options = {}) {
|
|
|
133
150
|
const saveText = button("", () => {
|
|
134
151
|
const now = savedNow();
|
|
135
152
|
if (now !== null)
|
|
136
|
-
save(`kumimoji-${now.deal.seed}.txt`, "text/plain", kumimojiToText(now, say));
|
|
153
|
+
save(`kumimoji-${now.deal.seed}.txt`, "text/plain", kumimojiToText(now, session.say));
|
|
137
154
|
}, false, "km-save-text");
|
|
138
155
|
const load = button("", () => file.click(), false, "km-load");
|
|
139
156
|
const keepButtons = node("div", "km-controls");
|
|
140
157
|
keepButtons.append(saveJson, saveText, load);
|
|
141
158
|
keep.append(keepTitle, keepButtons, keepNote, file);
|
|
142
|
-
const
|
|
143
|
-
if (play !== null && finishedAt === null)
|
|
144
|
-
renderStatus();
|
|
145
|
-
}, 1000);
|
|
146
|
-
const verdictOf = (now) => judgeWithWords(now.tiles, words, { diagonals: settings.diagonals === true });
|
|
159
|
+
const verdictOf = (now) => judgeWithWords(now.tiles, session.words, { diagonals: settings.diagonals === true });
|
|
147
160
|
const change = (next) => {
|
|
148
|
-
play = next;
|
|
161
|
+
session.play = next;
|
|
149
162
|
const verdict = verdictOf(next);
|
|
150
|
-
if (finishedAt === null && isFinished(next, verdict)) {
|
|
151
|
-
finishedAt = Date.now();
|
|
152
|
-
|
|
163
|
+
if (session.finishedAt === null && isFinished(next, verdict)) {
|
|
164
|
+
session.finishedAt = Date.now();
|
|
165
|
+
current.onFinish?.({ deal: dealt, play: next, elapsedMs: session.finishedAt - session.started });
|
|
153
166
|
}
|
|
154
|
-
note = "";
|
|
167
|
+
session.note = "";
|
|
155
168
|
const now = savedNow();
|
|
156
169
|
if (now !== null)
|
|
157
|
-
|
|
170
|
+
current.onChange?.(now);
|
|
158
171
|
render();
|
|
159
172
|
};
|
|
160
173
|
const tapSquare = (square) => {
|
|
161
|
-
|
|
174
|
+
const now = session.play;
|
|
175
|
+
if (now === null || session.finishedAt !== null)
|
|
162
176
|
return;
|
|
163
|
-
const there =
|
|
177
|
+
const there = now.tiles.get(square);
|
|
178
|
+
const held = session.held;
|
|
164
179
|
if (held === null) {
|
|
165
180
|
if (there !== undefined)
|
|
166
|
-
held = { from: "table", square };
|
|
181
|
+
session.held = { from: "table", square };
|
|
167
182
|
render();
|
|
168
183
|
return;
|
|
169
184
|
}
|
|
170
185
|
if (held.from === "hand") {
|
|
171
186
|
const at = held.at;
|
|
172
|
-
const tile =
|
|
187
|
+
const tile = now.hand[at];
|
|
173
188
|
if (tile === undefined)
|
|
174
189
|
return;
|
|
175
190
|
// A wild is given a letter before it is laid.
|
|
176
191
|
if (tile === "*")
|
|
177
192
|
return;
|
|
178
|
-
held = null;
|
|
179
|
-
change(there === undefined ? placeFromHand(
|
|
193
|
+
session.held = null;
|
|
194
|
+
change(there === undefined ? placeFromHand(now, at, square) : swapWithHand(now, at, square));
|
|
180
195
|
return;
|
|
181
196
|
}
|
|
182
197
|
const from = held.square;
|
|
183
|
-
held = null;
|
|
184
|
-
change(from === square ? liftToHand(
|
|
198
|
+
session.held = null;
|
|
199
|
+
change(from === square ? liftToHand(now, square) : moveOnTable(now, from, square));
|
|
185
200
|
};
|
|
186
201
|
const tapHand = (at) => {
|
|
187
|
-
if (play === null || finishedAt !== null)
|
|
202
|
+
if (session.play === null || session.finishedAt !== null)
|
|
188
203
|
return;
|
|
189
|
-
held = held
|
|
204
|
+
const held = session.held;
|
|
205
|
+
session.held = held?.from === "hand" && held.at === at ? null : { from: "hand", at };
|
|
190
206
|
render();
|
|
191
207
|
};
|
|
192
208
|
board.addEventListener("click", (event) => {
|
|
@@ -209,7 +225,23 @@ export function mountKumimoji(target, options = {}) {
|
|
|
209
225
|
const cells = [...grid.children];
|
|
210
226
|
const cols = Number(grid.dataset.cols);
|
|
211
227
|
const at = cells.indexOf(cell);
|
|
212
|
-
const step = event.key === "ArrowLeft"
|
|
228
|
+
const step = event.key === "ArrowLeft"
|
|
229
|
+
? at % cols > 0
|
|
230
|
+
? -1
|
|
231
|
+
: 0
|
|
232
|
+
: event.key === "ArrowRight"
|
|
233
|
+
? at % cols < cols - 1
|
|
234
|
+
? 1
|
|
235
|
+
: 0
|
|
236
|
+
: event.key === "ArrowUp"
|
|
237
|
+
? at - cols >= 0
|
|
238
|
+
? -cols
|
|
239
|
+
: 0
|
|
240
|
+
: event.key === "ArrowDown"
|
|
241
|
+
? at + cols < cells.length
|
|
242
|
+
? cols
|
|
243
|
+
: 0
|
|
244
|
+
: null;
|
|
213
245
|
if (step === null)
|
|
214
246
|
return;
|
|
215
247
|
event.preventDefault();
|
|
@@ -218,18 +250,18 @@ export function mountKumimoji(target, options = {}) {
|
|
|
218
250
|
return;
|
|
219
251
|
cell.tabIndex = -1;
|
|
220
252
|
next.tabIndex = 0;
|
|
221
|
-
onSquare = next.dataset.square ?? null;
|
|
253
|
+
session.onSquare = next.dataset.square ?? null;
|
|
222
254
|
next.focus();
|
|
223
255
|
});
|
|
224
256
|
board.addEventListener("focusin", (event) => {
|
|
225
257
|
const square = event.target.closest("[data-square]")?.getAttribute("data-square");
|
|
226
258
|
if (square !== null && square !== undefined)
|
|
227
|
-
onSquare = square;
|
|
259
|
+
session.onSquare = square;
|
|
228
260
|
});
|
|
229
261
|
hand.addEventListener("focusin", (event) => {
|
|
230
262
|
const at = event.target.closest("[data-hand]")?.getAttribute("data-hand");
|
|
231
263
|
if (at !== null && at !== undefined)
|
|
232
|
-
onHand = Number(at);
|
|
264
|
+
session.onHand = Number(at);
|
|
233
265
|
});
|
|
234
266
|
hand.addEventListener("keydown", (event) => {
|
|
235
267
|
const tile = event.target.closest("[data-hand]");
|
|
@@ -237,7 +269,15 @@ export function mountKumimoji(target, options = {}) {
|
|
|
237
269
|
return;
|
|
238
270
|
const tiles = [...hand.querySelectorAll("[data-hand]")];
|
|
239
271
|
const at = tiles.indexOf(tile);
|
|
240
|
-
const to = event.key === "ArrowLeft"
|
|
272
|
+
const to = event.key === "ArrowLeft"
|
|
273
|
+
? at - 1
|
|
274
|
+
: event.key === "ArrowRight"
|
|
275
|
+
? at + 1
|
|
276
|
+
: event.key === "Home"
|
|
277
|
+
? 0
|
|
278
|
+
: event.key === "End"
|
|
279
|
+
? tiles.length - 1
|
|
280
|
+
: null;
|
|
241
281
|
if (to === null)
|
|
242
282
|
return;
|
|
243
283
|
event.preventDefault();
|
|
@@ -246,212 +286,83 @@ export function mountKumimoji(target, options = {}) {
|
|
|
246
286
|
return;
|
|
247
287
|
tile.tabIndex = -1;
|
|
248
288
|
next.tabIndex = 0;
|
|
249
|
-
onHand = to;
|
|
289
|
+
session.onHand = to;
|
|
250
290
|
next.focus();
|
|
251
291
|
});
|
|
252
292
|
root.addEventListener("keydown", (event) => {
|
|
253
|
-
if (event.key !== "Escape" || held === null)
|
|
293
|
+
if (event.key !== "Escape" || session.held === null)
|
|
254
294
|
return;
|
|
255
|
-
held = null;
|
|
295
|
+
session.held = null;
|
|
256
296
|
render();
|
|
257
297
|
});
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const time = kumimojiClock((finishedAt ?? Date.now()) - started);
|
|
267
|
-
status.textContent = finishedAt !== null ? kumimojiSay(say.finished, { time }) : kumimojiSay(left === 1 ? say.leftOne : say.left, { n: left, time });
|
|
268
|
-
}
|
|
269
|
-
function renderBoard(verdict) {
|
|
270
|
-
const model = boardModel(play.tiles, play.tiles.size === 0 ? null : verdict, words.glyphOf);
|
|
271
|
-
const grid = node("div", "km-grid");
|
|
272
|
-
grid.style.gridTemplateColumns = `repeat(${model.cols}, var(--km-square))`;
|
|
273
|
-
grid.setAttribute("role", "group");
|
|
274
|
-
grid.setAttribute("aria-label", say.tableLabel);
|
|
275
|
-
grid.setAttribute("aria-description", say.tableKeys);
|
|
276
|
-
grid.dataset.cols = String(model.cols);
|
|
277
|
-
const cells = [];
|
|
278
|
-
for (const square of model.squares) {
|
|
279
|
-
const cell = node("button", "km-square");
|
|
280
|
-
cell.type = "button";
|
|
281
|
-
cell.dataset.square = square.square;
|
|
282
|
-
if (square.tile !== null) {
|
|
283
|
-
cell.classList.add("km-tile");
|
|
284
|
-
if (square.wild)
|
|
285
|
-
cell.classList.add("km-wild");
|
|
286
|
-
if (square.mark !== null)
|
|
287
|
-
cell.classList.add(`km-${square.mark}`);
|
|
288
|
-
cell.textContent = square.glyph;
|
|
289
|
-
cell.dataset.tile = square.glyph;
|
|
290
|
-
cell.setAttribute("aria-label", square.wild ? kumimojiSay(say.wildTile, { tile: square.glyph }) : square.glyph);
|
|
291
|
-
}
|
|
292
|
-
else {
|
|
293
|
-
cell.setAttribute("aria-label", say.emptySquare);
|
|
294
|
-
}
|
|
295
|
-
if (held?.from === "table" && held.square === square.square)
|
|
296
|
-
cell.classList.add("km-held");
|
|
297
|
-
cell.tabIndex = -1;
|
|
298
|
-
cells.push(cell);
|
|
299
|
-
grid.append(cell);
|
|
300
|
-
}
|
|
301
|
-
// The one Tab lands on: where the keyboard was, else a tile, else the middle.
|
|
302
|
-
const stop = cells.find((cell) => cell.dataset.square === onSquare) ?? cells.find((cell) => cell.classList.contains("km-tile")) ?? cells[Math.floor(cells.length / 2)];
|
|
303
|
-
if (stop !== undefined) {
|
|
304
|
-
stop.tabIndex = 0;
|
|
305
|
-
onSquare = stop.dataset.square ?? null;
|
|
306
|
-
}
|
|
307
|
-
board.replaceChildren(grid);
|
|
308
|
-
notes.textContent = verdict.notWords.length > 0 ? kumimojiSay(say.notWords, { words: verdict.notWords.join(", ") }) : verdict.apart.size > 0 ? say.apart : "";
|
|
309
|
-
}
|
|
310
|
-
function renderHand() {
|
|
311
|
-
hand.replaceChildren();
|
|
312
|
-
hand.setAttribute("aria-label", say.handLabel);
|
|
313
|
-
play.hand.forEach((tile, at) => {
|
|
314
|
-
const face = tileFace(tile);
|
|
315
|
-
const made = node("button", "km-tile km-in-hand");
|
|
316
|
-
made.type = "button";
|
|
317
|
-
made.dataset.hand = String(at);
|
|
318
|
-
made.textContent = face.blank ? WILD_MARK : words.glyphOf(tile);
|
|
319
|
-
if (face.wild)
|
|
320
|
-
made.classList.add("km-wild");
|
|
321
|
-
if (held?.from === "hand" && held.at === at)
|
|
322
|
-
made.classList.add("km-held");
|
|
323
|
-
made.dataset.tile = face.blank ? "*" : words.glyphOf(tile);
|
|
324
|
-
made.setAttribute("aria-label", face.blank ? say.wildBlank : face.wild ? kumimojiSay(say.wildTile, { tile: words.glyphOf(tile) }) : words.glyphOf(tile));
|
|
325
|
-
made.tabIndex = at === Math.min(onHand, play.hand.length - 1) ? 0 : -1;
|
|
326
|
-
hand.append(made);
|
|
327
|
-
});
|
|
328
|
-
if (play.hand.length === 0 && finishedAt === null)
|
|
329
|
-
hand.append(node("span", "km-empty", say.noTiles));
|
|
330
|
-
}
|
|
331
|
-
function renderPicker() {
|
|
332
|
-
picker.replaceChildren();
|
|
333
|
-
if (held?.from !== "hand" || play === null)
|
|
334
|
-
return;
|
|
335
|
-
const at = held.at;
|
|
336
|
-
const tile = play.hand[at];
|
|
337
|
-
if (tile === undefined || !tileFace(tile).wild)
|
|
338
|
-
return;
|
|
339
|
-
picker.append(node("span", "km-picker-title", tile === "*" ? say.giveWild : say.changeWild));
|
|
340
|
-
for (const sound of words.wildOptions) {
|
|
341
|
-
const code = words.wildFor(sound);
|
|
342
|
-
if (code === null)
|
|
343
|
-
continue;
|
|
344
|
-
picker.append(button(sound, () => {
|
|
345
|
-
held = { from: "hand", at };
|
|
346
|
-
change(assignHandTile(play, at, code));
|
|
347
|
-
}));
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
function renderControls(verdict) {
|
|
351
|
-
controls.replaceChildren();
|
|
352
|
-
if (finishedAt !== null) {
|
|
353
|
-
controls.append(button(say.newGame, () => void handle.newGame(), true, "km-new"));
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
const now = play;
|
|
357
|
-
const drawButton = button(say.draw, () => {
|
|
358
|
-
held = null;
|
|
359
|
-
change(draw(now));
|
|
360
|
-
}, true, "km-draw");
|
|
361
|
-
drawButton.disabled = !mayDraw(now, verdict);
|
|
362
|
-
const tradeButton = button(say.trade, () => {
|
|
363
|
-
if (held?.from !== "hand")
|
|
364
|
-
return;
|
|
365
|
-
const at = held.at;
|
|
366
|
-
held = null;
|
|
367
|
-
change(trade(now, at));
|
|
368
|
-
}, false, "km-trade");
|
|
369
|
-
tradeButton.disabled = held?.from !== "hand" || !mayTrade(now);
|
|
370
|
-
controls.append(drawButton, tradeButton, button(say.sort, () => change(sortHand(now)), false, "km-sort"), button(say.liftAll, () => {
|
|
371
|
-
held = null;
|
|
372
|
-
change(liftAll(now));
|
|
373
|
-
}, false, "km-lift"), button(say.newGame, () => void handle.newGame(), false, "km-new"));
|
|
374
|
-
}
|
|
375
|
-
function renderKeep() {
|
|
376
|
-
keepTitle.textContent = say.keep;
|
|
377
|
-
saveJson.textContent = say.saveJson;
|
|
378
|
-
saveText.textContent = say.saveText;
|
|
379
|
-
load.textContent = say.load;
|
|
380
|
-
saveJson.disabled = play === null;
|
|
381
|
-
saveText.disabled = play === null;
|
|
382
|
-
keepNote.textContent = note;
|
|
383
|
-
}
|
|
384
|
-
/** Where the keyboard is, so that it can be put back after everything is drawn again: the zone, and which of it. */
|
|
385
|
-
function keyboardAt() {
|
|
386
|
-
const on = document.activeElement;
|
|
387
|
-
if (on === null || !root.contains(on))
|
|
388
|
-
return null;
|
|
389
|
-
const mark = (zone, id) => (id === undefined ? null : { zone, id });
|
|
390
|
-
if (board.contains(on))
|
|
391
|
-
return mark("board", on.dataset.square);
|
|
392
|
-
if (hand.contains(on))
|
|
393
|
-
return mark("hand", on.dataset.hand);
|
|
394
|
-
if (controls.contains(on))
|
|
395
|
-
return mark("controls", on.dataset.testid);
|
|
396
|
-
if (picker.contains(on))
|
|
397
|
-
return mark("picker", String([...picker.querySelectorAll("button")].indexOf(on)));
|
|
398
|
-
return null;
|
|
399
|
-
}
|
|
400
|
-
function keyboardBack(at) {
|
|
401
|
-
if (at === null)
|
|
402
|
-
return;
|
|
403
|
-
const zone = { board, hand, controls, picker }[at.zone];
|
|
404
|
-
const found = at.zone === "board" ? zone.querySelector(`[data-square="${at.id}"]`) : at.zone === "hand" ? zone.querySelector(`[data-hand="${at.id}"]`) : at.zone === "controls" ? zone.querySelector(`[data-testid="${at.id}"]`) : zone.querySelectorAll("button")[Number(at.id)];
|
|
405
|
-
// A button that has become disabled cannot hold the focus; the table keeps it rather than dropping it on the page.
|
|
406
|
-
if (found !== undefined && found !== null && !found.disabled)
|
|
407
|
-
found.focus({ preventScroll: true });
|
|
408
|
-
}
|
|
409
|
-
function render() {
|
|
410
|
-
if (destroyed)
|
|
411
|
-
return;
|
|
412
|
-
const where = keyboardAt();
|
|
413
|
-
root.lang = locale;
|
|
414
|
-
renderStatus();
|
|
415
|
-
renderKeep();
|
|
416
|
-
if (play === null || words === null)
|
|
417
|
-
return;
|
|
418
|
-
const verdict = verdictOf(play);
|
|
419
|
-
renderBoard(verdict);
|
|
420
|
-
renderPicker();
|
|
421
|
-
renderHand();
|
|
422
|
-
renderControls(verdict);
|
|
423
|
-
keyboardBack(where);
|
|
424
|
-
}
|
|
298
|
+
const drawing = tableDrawing(session, { root, status, board, notes, hand, picker, controls, keepTitle, saveJson, saveText, load, keepNote }, { verdictOf, change, newGame: () => bridge.newGame() });
|
|
299
|
+
bridge.render = () => drawing.render();
|
|
300
|
+
bridge.renderStatus = () => drawing.renderStatus();
|
|
301
|
+
const tick = setInterval(() => {
|
|
302
|
+
if (session.play !== null && session.finishedAt === null)
|
|
303
|
+
bridge.renderStatus();
|
|
304
|
+
}, 1000);
|
|
305
|
+
let initialReady = Promise.resolve();
|
|
425
306
|
const handle = {
|
|
426
|
-
|
|
307
|
+
get ready() {
|
|
308
|
+
return initialReady;
|
|
309
|
+
},
|
|
310
|
+
set: (changes) => {
|
|
311
|
+
for (const name of Object.keys(current.theme ?? {}))
|
|
312
|
+
if (changes.theme !== undefined && !(name in changes.theme))
|
|
313
|
+
root.style.removeProperty(name);
|
|
314
|
+
if (changes.theme !== undefined)
|
|
315
|
+
for (const [name, value] of Object.entries(changes.theme))
|
|
316
|
+
root.style.setProperty(name, value);
|
|
317
|
+
current = { ...current, ...changes };
|
|
318
|
+
if (changes.locale !== undefined)
|
|
319
|
+
session.locale = changes.locale;
|
|
320
|
+
if (changes.strings !== undefined)
|
|
321
|
+
own = changes.strings;
|
|
322
|
+
session.say = kumimojiStrings(session.locale, own);
|
|
323
|
+
if (current.keep === false)
|
|
324
|
+
keep.remove();
|
|
325
|
+
else if (keep.parentNode !== root)
|
|
326
|
+
root.append(keep);
|
|
327
|
+
controls.hidden = current.controls === false;
|
|
328
|
+
render();
|
|
329
|
+
},
|
|
330
|
+
play: () => session.play,
|
|
427
331
|
saved: savedNow,
|
|
428
332
|
setGame: async (saved) => {
|
|
429
333
|
const mine = ++asked;
|
|
430
334
|
const language = saved.deal.language;
|
|
431
|
-
settings = {
|
|
335
|
+
settings = {
|
|
336
|
+
language,
|
|
337
|
+
hand: saved.deal.size,
|
|
338
|
+
level: saved.deal.level,
|
|
339
|
+
gameLength: saved.deal.gameLength,
|
|
340
|
+
diagonals: saved.deal.diagonals === true,
|
|
341
|
+
seed: undefined,
|
|
342
|
+
};
|
|
432
343
|
const list = await loadTileWords(language);
|
|
433
|
-
if (destroyed || mine !== asked)
|
|
344
|
+
if (session.destroyed || mine !== asked)
|
|
434
345
|
return;
|
|
435
|
-
words = list;
|
|
346
|
+
session.words = list;
|
|
436
347
|
root.classList.toggle("km-japanese", language === "japanese");
|
|
437
348
|
dealt = saved.deal;
|
|
438
349
|
root.dataset.seed = String(saved.deal.seed);
|
|
439
|
-
held = null;
|
|
440
|
-
finishedAt = null;
|
|
441
|
-
started = Date.now() - saved.elapsedMs;
|
|
350
|
+
session.held = null;
|
|
351
|
+
session.finishedAt = null;
|
|
352
|
+
session.started = Date.now() - saved.elapsedMs;
|
|
442
353
|
// A game loaded already finished stays finished, at the time it took, and is not announced again.
|
|
443
|
-
if (isFinished(saved.play, judgeWithWords(saved.play.tiles,
|
|
444
|
-
finishedAt = Date.now();
|
|
445
|
-
play = saved.play;
|
|
446
|
-
note = "";
|
|
447
|
-
|
|
354
|
+
if (isFinished(saved.play, judgeWithWords(saved.play.tiles, list, { diagonals: settings.diagonals === true })))
|
|
355
|
+
session.finishedAt = Date.now();
|
|
356
|
+
session.play = saved.play;
|
|
357
|
+
session.note = "";
|
|
358
|
+
current.onChange?.(saved);
|
|
448
359
|
render();
|
|
449
360
|
},
|
|
450
361
|
setLocale: (next, strings) => {
|
|
451
|
-
locale = next;
|
|
362
|
+
session.locale = next;
|
|
452
363
|
if (strings !== undefined)
|
|
453
364
|
own = strings;
|
|
454
|
-
say = kumimojiStrings(locale, own);
|
|
365
|
+
session.say = kumimojiStrings(session.locale, own);
|
|
455
366
|
render();
|
|
456
367
|
},
|
|
457
368
|
newGame: async (changed = {}) => {
|
|
@@ -459,29 +370,43 @@ export function mountKumimoji(target, options = {}) {
|
|
|
459
370
|
settings = { ...settings, ...changed, seed: undefined };
|
|
460
371
|
const language = settings.language ?? "english";
|
|
461
372
|
const size = settings.hand ?? KUMIMOJI_HANDS.quick;
|
|
462
|
-
play = null;
|
|
463
|
-
held = null;
|
|
464
|
-
finishedAt = null;
|
|
465
|
-
note = "";
|
|
373
|
+
session.play = null;
|
|
374
|
+
session.held = null;
|
|
375
|
+
session.finishedAt = null;
|
|
376
|
+
session.note = "";
|
|
466
377
|
render();
|
|
467
378
|
const list = await loadTileWords(language);
|
|
468
|
-
if (destroyed || mine !== asked)
|
|
379
|
+
if (session.destroyed || mine !== asked)
|
|
469
380
|
return;
|
|
470
|
-
words = list;
|
|
381
|
+
session.words = list;
|
|
471
382
|
// A seed deals one game: the next is dealt from a new one unless asked for by seed again.
|
|
472
383
|
const seed = changed.seed ?? Math.floor(Math.random() * 2 ** 31);
|
|
473
384
|
root.classList.toggle("km-japanese", language === "japanese");
|
|
474
385
|
root.dataset.seed = String(seed);
|
|
475
|
-
dealt = generateKumimoji(size, settings.level ?? "medium", seed, {
|
|
476
|
-
|
|
386
|
+
dealt = generateKumimoji(size, settings.level ?? "medium", seed, {
|
|
387
|
+
language,
|
|
388
|
+
gameLength: settings.gameLength,
|
|
389
|
+
diagonals: settings.diagonals,
|
|
390
|
+
});
|
|
391
|
+
session.started = Date.now();
|
|
477
392
|
change(deal(dealt.givens, size));
|
|
478
393
|
},
|
|
479
394
|
destroy: () => {
|
|
480
|
-
destroyed = true;
|
|
395
|
+
session.destroyed = true;
|
|
481
396
|
clearInterval(tick);
|
|
482
397
|
root.remove();
|
|
483
398
|
},
|
|
484
399
|
};
|
|
485
|
-
|
|
400
|
+
bridge.newGame = () => {
|
|
401
|
+
void handle.newGame();
|
|
402
|
+
};
|
|
403
|
+
initialReady = current.saved === undefined ? handle.newGame(settings) : handle.setGame(current.saved);
|
|
404
|
+
void initialReady.catch((error) => {
|
|
405
|
+
if (session.destroyed)
|
|
406
|
+
return;
|
|
407
|
+
const problem = error instanceof Error ? error : new Error(String(error));
|
|
408
|
+
status.textContent = session.say.loadError;
|
|
409
|
+
current.onError?.(problem);
|
|
410
|
+
});
|
|
486
411
|
return handle;
|
|
487
412
|
}
|
package/dist/ui/style.d.ts
CHANGED
|
@@ -3,5 +3,10 @@
|
|
|
3
3
|
* variable so a page can wear it in its own colours (set them on the element
|
|
4
4
|
* or any ancestor, or pass them as `theme`). Light and dark follow the
|
|
5
5
|
* reader's system. Everything to be tapped is at least 44px.
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { KUMIMOJI_STYLE } from "@johnmorrisdotca/kumimoji/ui";
|
|
9
|
+
* console.log(KUMIMOJI_STYLE.includes(".km-root")); // true
|
|
10
|
+
* ```
|
|
6
11
|
*/
|
|
7
|
-
export declare const KUMIMOJI_STYLE = "\n.km-root {\n --km-square: 44px; --km-felt: #2f5d4a; --km-grid-line: rgba(255,255,255,.12); --km-tile: #f3e6c8; --km-tile-ink: #2a2118;\n --km-wild: #f6d27a; --km-wrong: #d9534f; --km-held: #ffcf3f; --km-ink: #1f2320; --km-panel: #f7f3ea;\n --km-line: rgba(20,20,20,.35); --km-accent: #2f5d4a; --km-accent-ink: #fff;\n --km-radius: 12px; --km-font: system-ui, -apple-system, \"Segoe UI\", sans-serif;\n display: grid; gap: 10px; min-width: 0; color: var(--km-ink); font-family: var(--km-font); font-size: 15px;\n}\n@media (prefers-color-scheme: dark) {\n .km-root { --km-felt: #1e3a2f; --km-ink: #ece8dc; --km-panel: #1d201e; --km-line: rgba(255,255,255,.25); --km-accent: #6fb08f; --km-accent-ink: #10150f; }\n}\n.km-root > * { min-width: 0; }\n.km-status { margin: 0; min-height: 1.4em; font-variant-numeric: tabular-nums; }\n.km-board { background: var(--km-felt); border-radius: var(--km-radius); padding: 10px; overflow: auto; max-height: 70vh; }\n.km-grid { display: grid; gap: 2px; width: max-content; margin: 0 auto; }\n.km-square { width: var(--km-square); height: var(--km-square); border: 0; border-radius: 5px; background: var(--km-grid-line); padding: 0; cursor: pointer; touch-action: manipulation; }\n.km-tile { display: grid; place-items: center; width: var(--km-square); height: var(--km-square); border-radius: 6px; border: 0; padding: 0;\n background: var(--km-tile); color: var(--km-tile-ink); font: 700 calc(var(--km-square) * .55)/1 var(--km-font); text-transform: uppercase;\n box-shadow: 0 2px 0 rgba(0,0,0,.35); cursor: pointer; touch-action: manipulation; }\n.km-japanese .km-tile { font-size: calc(var(--km-square) * .52); text-transform: none; }\n.km-wild { background: var(--km-wild); }\n.km-misspelt { box-shadow: 0 0 0 3px var(--km-wrong) inset, 0 2px 0 rgba(0,0,0,.35); }\n.km-apart { opacity: .6; }\n.km-held { outline: 3px solid var(--km-held); outline-offset: 1px; }\n.km-square:focus-visible, .km-in-hand:focus-visible, .km-button:focus-visible, .km-keep-title:focus-visible { outline: 3px solid var(--km-held); outline-offset: 2px; }\n.km-notes { margin: 0; min-height: 1.4em; font-size: 14px; opacity: .8; overflow-wrap: anywhere; }\n.km-hand { display: flex; flex-wrap: wrap; gap: 6px; min-height: calc(var(--km-square) + 16px); align-items: center; padding: 8px; border-radius: 10px; background: var(--km-panel); }\n.km-empty { opacity: .6; font-size: 14px; }\n.km-picker { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }\n.km-picker:empty { display: none; }\n.km-picker-title { width: 100%; font-size: 14px; }\n.km-picker .km-button { padding: 0 8px; text-transform: uppercase; }\n.km-japanese .km-picker .km-button { text-transform: none; }\n.km-controls { display: flex; flex-wrap: wrap; gap: 8px; }\n.km-button { font: inherit; min-height: 44px; min-width: 44px; padding: 0 14px; border-radius: 8px; border: 1px solid var(--km-line); background: var(--km-panel); color: var(--km-ink); cursor: pointer; touch-action: manipulation; }\n.km-button:disabled { opacity: .45; cursor: default; }\n.km-primary { background: var(--km-accent); color: var(--km-accent-ink); border-color: transparent; }\n.km-keep { border-radius: 10px; background: var(--km-panel); padding: 0 10px; }\n.km-keep[open] { padding-bottom: 10px; }\n.km-keep-title { min-height: 44px; display: flex; align-items: center; font-weight: 600; cursor: pointer; }\n.km-note { margin: 8px 0 0; min-height: 1.4em; font-size: 13px; }\n";
|
|
12
|
+
export declare const KUMIMOJI_STYLE = "\n.km-root {\n --km-square: 44px; --km-felt: #2f5d4a; --km-grid-line: rgba(255,255,255,.12); --km-tile: #f3e6c8; --km-tile-ink: #2a2118;\n --km-wild: #f6d27a; --km-wrong: #d9534f; --km-held: #ffcf3f; --km-ink: #1f2320; --km-panel: #f7f3ea;\n --km-line: rgba(20,20,20,.35); --km-accent: #2f5d4a; --km-accent-ink: #fff;\n --km-radius: 12px; --km-font: system-ui, -apple-system, \"Segoe UI\", sans-serif;\n display: grid; gap: 10px; min-width: 0; color: var(--km-ink); font-family: var(--km-font); font-size: 15px;\n}\n@media (prefers-color-scheme: dark) {\n .km-root { --km-felt: #1e3a2f; --km-ink: #ece8dc; --km-panel: #1d201e; --km-line: rgba(255,255,255,.25); --km-accent: #6fb08f; --km-accent-ink: #10150f; }\n}\n.km-root > * { min-width: 0; }\n.km-status { margin: 0; min-height: 1.4em; font-variant-numeric: tabular-nums; }\n.km-board { background: var(--km-felt); border-radius: var(--km-radius); padding: 10px; overflow: auto; max-height: 70vh; }\n.km-grid { display: grid; gap: 2px; width: max-content; margin: 0 auto; }\n.km-square { width: var(--km-square); height: var(--km-square); border: 0; border-radius: 5px; background: var(--km-grid-line); padding: 0; cursor: pointer; touch-action: manipulation; }\n.km-tile { display: grid; place-items: center; width: var(--km-square); height: var(--km-square); border-radius: 6px; border: 0; padding: 0;\n background: var(--km-tile); color: var(--km-tile-ink); font: 700 calc(var(--km-square) * .55)/1 var(--km-font); text-transform: uppercase;\n box-shadow: 0 2px 0 rgba(0,0,0,.35); cursor: pointer; touch-action: manipulation; }\n.km-japanese .km-tile { font-size: calc(var(--km-square) * .52); text-transform: none; }\n.km-wild { background: var(--km-wild); }\n.km-misspelt { box-shadow: 0 0 0 3px var(--km-wrong) inset, 0 2px 0 rgba(0,0,0,.35); }\n.km-apart { opacity: .6; }\n.km-held { outline: 3px solid var(--km-held); outline-offset: 1px; }\n.km-square:focus-visible, .km-in-hand:focus-visible, .km-button:focus-visible, .km-keep-title:focus-visible { outline: 3px solid var(--km-held); outline-offset: 2px; }\n.km-notes { margin: 0; min-height: 1.4em; font-size: 14px; opacity: .8; overflow-wrap: anywhere; }\n.km-hand { display: flex; flex-wrap: wrap; gap: 6px; min-height: calc(var(--km-square) + 16px); align-items: center; padding: 8px; border-radius: 10px; background: var(--km-panel); }\n.km-empty { opacity: .6; font-size: 14px; }\n.km-picker { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }\n.km-picker:empty { display: none; }\n.km-picker-title { width: 100%; font-size: 14px; }\n.km-picker .km-button { padding: 0 8px; text-transform: uppercase; }\n.km-japanese .km-picker .km-button { text-transform: none; }\n.km-controls[hidden] { display: none; }\n.km-controls { display: flex; flex-wrap: wrap; gap: 8px; }\n.km-button { font: inherit; min-height: 44px; min-width: 44px; padding: 0 14px; border-radius: 8px; border: 1px solid var(--km-line); background: var(--km-panel); color: var(--km-ink); cursor: pointer; touch-action: manipulation; }\n.km-button:disabled { opacity: .45; cursor: default; }\n.km-primary { background: var(--km-accent); color: var(--km-accent-ink); border-color: transparent; }\n.km-keep { border-radius: 10px; background: var(--km-panel); padding: 0 10px; }\n.km-keep[open] { padding-bottom: 10px; }\n.km-keep-title { min-height: 44px; display: flex; align-items: center; font-weight: 600; cursor: pointer; }\n.km-note { margin: 8px 0 0; min-height: 1.4em; font-size: 13px; }\n";
|
package/dist/ui/style.js
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
* variable so a page can wear it in its own colours (set them on the element
|
|
4
4
|
* or any ancestor, or pass them as `theme`). Light and dark follow the
|
|
5
5
|
* reader's system. Everything to be tapped is at least 44px.
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { KUMIMOJI_STYLE } from "@johnmorrisdotca/kumimoji/ui";
|
|
9
|
+
* console.log(KUMIMOJI_STYLE.includes(".km-root")); // true
|
|
10
|
+
* ```
|
|
6
11
|
*/
|
|
7
12
|
export const KUMIMOJI_STYLE = `
|
|
8
13
|
.km-root {
|
|
@@ -37,6 +42,7 @@ export const KUMIMOJI_STYLE = `
|
|
|
37
42
|
.km-picker-title { width: 100%; font-size: 14px; }
|
|
38
43
|
.km-picker .km-button { padding: 0 8px; text-transform: uppercase; }
|
|
39
44
|
.km-japanese .km-picker .km-button { text-transform: none; }
|
|
45
|
+
.km-controls[hidden] { display: none; }
|
|
40
46
|
.km-controls { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
41
47
|
.km-button { font: inherit; min-height: 44px; min-width: 44px; padding: 0 14px; border-radius: 8px; border: 1px solid var(--km-line); background: var(--km-panel); color: var(--km-ink); cursor: pointer; touch-action: manipulation; }
|
|
42
48
|
.km-button:disabled { opacity: .45; cursor: default; }
|