@johnmorrisdotca/kumimoji 1.1.1 → 1.2.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 +36 -1
- package/CONTRIBUTING.md +15 -0
- package/README.md +154 -40
- package/dist/daily.d.ts +14 -0
- package/dist/daily.js +22 -0
- package/dist/element-define.d.ts +1 -0
- package/dist/element-define.js +13 -0
- package/dist/element.d.ts +45 -0
- package/dist/element.js +130 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/strings.d.ts +1 -0
- package/dist/strings.js +2 -0
- package/dist/ui/mount.js +99 -0
- package/dist/ui/style.d.ts +1 -1
- package/dist/ui/style.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +24 -9
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,40 @@ All notable changes to this project are written down here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.2.1] - 2026-10-05
|
|
10
|
+
|
|
11
|
+
Nothing that was exported has changed.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- A test holds every `@johnmorrisdotca/kumimoji@N` version pin in the README to this package's major version.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- The family's list, in the README and in the demo's footer, names all twenty-four packages, Karakuri and Houseki included.
|
|
20
|
+
- The npm description is one sentence of 250 characters or fewer, so npm and its search show it whole; it is also the repository's About text. `homepage` is the demo site and `author` is `"John Morris"`, the same in every package.
|
|
21
|
+
- The GitHub Actions workflows use the current versions of the actions (checkout 7, setup-node 7, pnpm/action-setup 6; configure-pages 6, upload-pages-artifact 5 and deploy-pages 5 for Pages), which clears GitHub's Node 20 deprecation warning.
|
|
22
|
+
- Every entry has an `import` condition beside `default`.
|
|
23
|
+
|
|
24
|
+
## [1.2.0] - 2026-10-01
|
|
25
|
+
|
|
26
|
+
Nothing that was exported has changed; the rules, the word lists, every deal and every saved game are exactly as they were.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **`<kumimoji-table>`**, a game alone as a tag, with no framework: `@johnmorrisdotca/kumimoji/element/define` defines it (or `…/element` holds the class alone), and `language`, `hand`, `level`, `length`, `diagonals`, `seed`, `lang` and `keep` are attributes, read again when they change. It fires `kumimoji-change` after every change to the game and `kumimoji-finish` when it is won. A table the rules do not offer draws nothing.
|
|
31
|
+
- **A bag a day**: `kumimojiDailySeed(date)` is the UTC date as a number (2026-10-01 is `20261001`), the same seed for everybody and the same number as Tane's `dailySeed`; `kumimojiDay(date)` writes the day. The tag takes `seed="daily"`, and the demo has Today's game.
|
|
32
|
+
- **Played from a keyboard.** Tab lands on one square of the table and one tile of the hand; the arrow keys move between squares, and Left, Right, Home and End between the hand's tiles; Enter or Space picks up and lays; Escape lets go of a tile. The keyboard stays where it was after each move, on the squares, the hand and the buttons under the table. The table's notes are a polite live region. One new word in both languages (`tableKeys`) describes the table for a screen reader.
|
|
33
|
+
- **Demo:** a table in a tag, Today's game, and Copy link, which copies an address that deals the same game (its seed, tiles, hand, wilds, length and diagonals).
|
|
34
|
+
- **README:** an Accessibility section, "The element", "The day's seed", and the list of all sixteen packages of the family.
|
|
35
|
+
- A *word list* issue template, a pull request template, SECURITY.md and CODE_OF_CONDUCT.md as the family's shared text (held equal by a test), and the family's house rules in CONTRIBUTING.md.
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- **Node 22 or later** (`engines`), where it said 20, which is out of support and was never tested. The package's `sideEffects` also names the file that defines the tag.
|
|
40
|
+
- **A Help switch in the demo.** Beside the language chooser in the family header, shared by every demo. Off (the default) the page is as it was; on, each option row (the tiles, the hand, the wilds) says in one plain line what it does, in English or Japanese, and every button in it has the same words as its hover text. Kept on the device.
|
|
41
|
+
- The README's pictures are taken again, with the Help switch in the header.
|
|
42
|
+
|
|
9
43
|
## [1.1.1] - 2026-10-01
|
|
10
44
|
|
|
11
45
|
Nothing that was exported has changed.
|
|
@@ -107,7 +141,8 @@ The first release.
|
|
|
107
141
|
`@johnmorrisdotca/kumimoji/react`.
|
|
108
142
|
- A static demo for GitHub Pages.
|
|
109
143
|
|
|
110
|
-
[Unreleased]: https://github.com/johnmorrisdotca/kumimoji/compare/v1.
|
|
144
|
+
[Unreleased]: https://github.com/johnmorrisdotca/kumimoji/compare/v1.2.1...HEAD
|
|
145
|
+
[1.2.1]: https://github.com/johnmorrisdotca/kumimoji/compare/v1.2.0...v1.2.1
|
|
111
146
|
[1.1.1]: https://github.com/johnmorrisdotca/kumimoji/compare/v1.1.0...v1.1.1
|
|
112
147
|
[1.1.0]: https://github.com/johnmorrisdotca/kumimoji/releases/tag/v1.1.0
|
|
113
148
|
[1.0.1]: https://github.com/johnmorrisdotca/kumimoji/releases/tag/v1.0.1
|
package/CONTRIBUTING.md
CHANGED
|
@@ -21,6 +21,7 @@ pnpm test:table # the demo in real browsers: builds it, then taps it
|
|
|
21
21
|
pnpm test:package # npm pack, install the tarball, import and require every entry, read both lists
|
|
22
22
|
pnpm test:frameworks # the README's examples built in React, Vue, Svelte, Angular and a plain page
|
|
23
23
|
pnpm site # builds the demo into ./site
|
|
24
|
+
pnpm pictures # re-takes the README's two pictures from the built demo
|
|
24
25
|
pnpm dlx serve site # or any static server
|
|
25
26
|
```
|
|
26
27
|
|
|
@@ -68,6 +69,20 @@ pnpm dlx serve site # or any static server
|
|
|
68
69
|
- One change per pull request, with a line in `CHANGELOG.md` under
|
|
69
70
|
*Unreleased*.
|
|
70
71
|
|
|
72
|
+
## House rules, shared by every package of the family
|
|
73
|
+
|
|
74
|
+
- Open an issue first for anything bigger than a typo, so that we can agree on the shape before you spend time on it.
|
|
75
|
+
- No runtime dependencies. Every function that plays or checks a game is pure: it returns new values and never changes what it was given.
|
|
76
|
+
- Tests sit beside the code they test. A rule you change has a test that would have caught it.
|
|
77
|
+
- Words a player reads come in English and Japanese. If you cannot write the Japanese, say so in the pull request and someone will.
|
|
78
|
+
- Option values and names are kebab case.
|
|
79
|
+
- Art and sound are CC0 or public domain only, checked at the source, and credited in the README. No GPL or LGPL code.
|
|
80
|
+
- Needs Node 22 or later. A change a user would notice gets a line in `CHANGELOG.md`.
|
|
81
|
+
- **The list of the family in the README is made, not written.** `pnpm family:readme` writes it between its
|
|
82
|
+
markers from `scripts/family-template.mjs` (the names, the Japanese names and a line on each), and
|
|
83
|
+
`scripts/family-readme.mjs` is the same file in every package. To add a package or change a line, change the
|
|
84
|
+
template in every repository, bump `FAMILY_TEMPLATE_VERSION` and record the new hash in `src/family.test.js`.
|
|
85
|
+
|
|
71
86
|
## Releasing
|
|
72
87
|
|
|
73
88
|
Maintainers bump the version in `package.json` and `src/version.ts` and move
|
package/README.md
CHANGED
|
@@ -15,8 +15,8 @@ In English and in Japanese kana. The rules as pure, seeded TypeScript, two word
|
|
|
15
15
|
<p align="center"><a href="https://johnmorrisdotca.github.io/kumimoji/"><strong>Play a game →</strong></a> · <a href="https://johnmorrisdotca.github.io/kumimoji/api.html">API reference</a></p>
|
|
16
16
|
|
|
17
17
|
<p align="center">
|
|
18
|
-
<img src="docs/desktop.jpg" alt="An English game half way: a crossword on the green table, a
|
|
19
|
-
<img src="docs/phone.jpg" alt="A game in Japanese kana on a phone in dark mode" width="220">
|
|
18
|
+
<img src="docs/desktop.jpg" alt="An English game half way, under the demo's header with its language chooser, five cloth patches and Help switch: the set-up choices, a crossword of SYNDIC and the words that cross it on the green table, a tile in hand under it, and the buttons to draw, trade and sort" width="720">
|
|
19
|
+
<img src="docs/phone.jpg" alt="A game in Japanese kana on a phone in dark mode, in Japanese: a crossword half built on the green table, with a tile in hand and the buttons to draw, trade and sort" width="220">
|
|
20
20
|
</p>
|
|
21
21
|
|
|
22
22
|
A word game engine for the race to build a crossword. Every player lays their
|
|
@@ -92,9 +92,11 @@ Or with nothing to install, [play a game in the demo](https://johnmorrisdotca.gi
|
|
|
92
92
|
|
|
93
93
|
## Use it in your project
|
|
94
94
|
|
|
95
|
-
Kumimoji is
|
|
95
|
+
Kumimoji is five things, each usable without the others: **the rules**, plain
|
|
96
96
|
functions over plain values; **the word lists**, loaded when first needed;
|
|
97
|
-
**a table** you mount into any element;
|
|
97
|
+
**a table** you mount into any element; **the same table as a tag**,
|
|
98
|
+
`<kumimoji-table>`, for a page with no script of its own; and **React
|
|
99
|
+
components**.
|
|
98
100
|
|
|
99
101
|
### 1. The API alone
|
|
100
102
|
|
|
@@ -134,7 +136,22 @@ encodeGrid(tiles); // → "cat/2o/2w"
|
|
|
134
136
|
Without a bundler, import from the files as they are published:
|
|
135
137
|
`./node_modules/@johnmorrisdotca/kumimoji/dist/ui.js`, or a copy of `dist/`.
|
|
136
138
|
|
|
137
|
-
### 3.
|
|
139
|
+
### 3. As a tag
|
|
140
|
+
|
|
141
|
+
```html
|
|
142
|
+
<script type="module" src="https://cdn.jsdelivr.net/npm/@johnmorrisdotca/kumimoji@1/dist/element-define.js"></script>
|
|
143
|
+
|
|
144
|
+
<kumimoji-table language="english" hand="7" seed="2026"></kumimoji-table>
|
|
145
|
+
<kumimoji-table language="japanese" level="easy" seed="daily"></kumimoji-table>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The first is a game of English tiles dealt from a seed; the second is the
|
|
149
|
+
day's bag in kana. A table in a tag needs no framework and no bundler, and a
|
|
150
|
+
page with a framework can use it the same way. Every attribute is listed under
|
|
151
|
+
[The element](#the-element); the table fires `kumimoji-change` after each
|
|
152
|
+
change to the game and `kumimoji-finish` when it is won.
|
|
153
|
+
|
|
154
|
+
### 4. React
|
|
138
155
|
|
|
139
156
|
```jsx
|
|
140
157
|
import { useState } from "react";
|
|
@@ -157,7 +174,7 @@ draws an empty box and nothing needs a provider. In Next.js, use it from a
|
|
|
157
174
|
client component (`"use client"`). `KumimojiBoard` is the grid alone, for a
|
|
158
175
|
table of your own: see [The React components](#the-react-components).
|
|
159
176
|
|
|
160
|
-
###
|
|
177
|
+
### 5. Vue
|
|
161
178
|
|
|
162
179
|
```vue
|
|
163
180
|
<script setup>
|
|
@@ -179,7 +196,7 @@ onBeforeUnmount(() => table?.destroy());
|
|
|
179
196
|
</template>
|
|
180
197
|
```
|
|
181
198
|
|
|
182
|
-
###
|
|
199
|
+
### 6. Svelte
|
|
183
200
|
|
|
184
201
|
```svelte
|
|
185
202
|
<script>
|
|
@@ -198,7 +215,7 @@ onBeforeUnmount(() => table?.destroy());
|
|
|
198
215
|
<p id="left">{left}</p>
|
|
199
216
|
```
|
|
200
217
|
|
|
201
|
-
###
|
|
218
|
+
### 7. Angular
|
|
202
219
|
|
|
203
220
|
```typescript
|
|
204
221
|
import { Component, ElementRef, OnDestroy, afterNextRender, provideZonelessChangeDetection, signal, viewChild } from "@angular/core";
|
|
@@ -226,7 +243,7 @@ class App implements OnDestroy {
|
|
|
226
243
|
bootstrapApplication(App, { providers: [provideZonelessChangeDetection()] });
|
|
227
244
|
```
|
|
228
245
|
|
|
229
|
-
Each of the
|
|
246
|
+
Each of the six is taken from this page as it is written, built from the
|
|
230
247
|
packed tarball in a project of its own, and played by taps in Chromium and
|
|
231
248
|
WebKit, by `scripts/check-frameworks.mjs`, before a release names it. In each
|
|
232
249
|
the bundler splits the word lists into files of their own, which the browser
|
|
@@ -240,14 +257,15 @@ fetches when a game is first dealt.
|
|
|
240
257
|
text; what is read back is dealt again from its seed and checked against
|
|
241
258
|
its bag, never trusted.
|
|
242
259
|
- **No dependencies**, ES modules, a `default` export condition so that
|
|
243
|
-
`require()` loads it too (Node 22 and later)
|
|
260
|
+
`require()` loads it too (Node 22 and later), and a `sideEffects` list that
|
|
261
|
+
names only the files that define the tag and register the word lists.
|
|
244
262
|
- **Sizes.** The rules, the computer player and the table of players are
|
|
245
263
|
about 44 kB minified (16 kB gzipped); the table for a page is 34 kB (13 kB).
|
|
246
264
|
The English list is 606 kB (277 kB gzipped) and the Japanese 718 kB
|
|
247
265
|
(396 kB), each a file of its own that is fetched only when a game in that
|
|
248
266
|
language is first dealt.
|
|
249
267
|
- **Where it runs.** Current Chrome, Edge, Firefox and Safari, on a desk or a
|
|
250
|
-
phone. The rules have no DOM in them and run in Node
|
|
268
|
+
phone. The rules have no DOM in them and run in Node 22 and later, Deno,
|
|
251
269
|
Bun and web workers.
|
|
252
270
|
|
|
253
271
|
## Architecture
|
|
@@ -255,7 +273,7 @@ fetches when a game is first dealt.
|
|
|
255
273
|
The rules are plain functions over plain data with no DOM: a game is a value,
|
|
256
274
|
every move returns the next one, and the same seed deals the same bag in every
|
|
257
275
|
browser. Word lists are loaded only when a game opens, and the table is its own
|
|
258
|
-
entry (`/ui` for plain DOM, `/react` for React), so a page that wants only the
|
|
276
|
+
entry (`/ui` for plain DOM, `/element` for a tag, `/react` for React), so a page that wants only the
|
|
259
277
|
rules carries none of it. A computer player is pure too, so its turn replays
|
|
260
278
|
exactly after a reload.
|
|
261
279
|
|
|
@@ -265,6 +283,9 @@ src/
|
|
|
265
283
|
├── computer.types.ts what a computer did in one step of its turn
|
|
266
284
|
├── computerPlay.ts the word a computer player lays: from its own hand, with no randomness
|
|
267
285
|
├── computerTurn.ts a computer's whole turn, planned as steps the page can show one at a time
|
|
286
|
+
├── daily.ts one seed a day, the same for everybody
|
|
287
|
+
├── element-define.ts the "/element/define" entry: defines <kumimoji-table> on the page by being imported
|
|
288
|
+
├── element.ts the "/element" entry: the <kumimoji-table> element, a game alone in a tag
|
|
268
289
|
├── export.ts a game written out as JSON or plain text, and read back
|
|
269
290
|
├── generate.ts making a game from a seed: a crossword laid first, then its tiles become the bag
|
|
270
291
|
├── grid.ts the grid of tiles on a table with no edges, and how it is written as a string
|
|
@@ -299,7 +320,7 @@ src/
|
|
|
299
320
|
├── words.ts the "/words" entry: the word lists, for a place with no browser, such as a server or a test
|
|
300
321
|
└── ui/ the table that draws and plays a game
|
|
301
322
|
├── board.ts how a tile on the table is drawn: sound, in a run that is not a word, or apart
|
|
302
|
-
├── mount.ts the table itself: mounting it on a page,
|
|
323
|
+
├── mount.ts the table itself: mounting it on a page, the options it takes, and playing it from a keyboard
|
|
303
324
|
└── style.ts the table's own styles, every colour and size a CSS variable so a page can restyle it
|
|
304
325
|
```
|
|
305
326
|
|
|
@@ -335,25 +356,37 @@ issue and we will add you.
|
|
|
335
356
|
|
|
336
357
|
### The family
|
|
337
358
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
- [
|
|
345
|
-
|
|
346
|
-
- [
|
|
347
|
-
|
|
348
|
-
- [
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
- [
|
|
352
|
-
|
|
353
|
-
- [
|
|
354
|
-
|
|
355
|
-
- [
|
|
356
|
-
|
|
359
|
+
<!-- family:start (made by scripts/family-readme.mjs from scripts/family-template.mjs; change those, not this) -->
|
|
360
|
+
Kumimoji is one of twenty-four packages, each made for the same site, each at
|
|
361
|
+
[github.com/johnmorrisdotca](https://github.com/johnmorrisdotca). The code of every one is MIT.
|
|
362
|
+
|
|
363
|
+
- [Korokoro](https://github.com/johnmorrisdotca/korokoro) (コロコロ): dice, with notation, exact odds, real sounds and the dice of many games. [Demo](https://johnmorrisdotca.github.io/korokoro/).
|
|
364
|
+
- [Kyuubu](https://github.com/johnmorrisdotca/kyuubu) (キューブ): a turning cube for the browser, 2×2 to 7×7, with record solves to replay. [Demo](https://johnmorrisdotca.github.io/kyuubu/).
|
|
365
|
+
- [Hitotsu](https://github.com/johnmorrisdotca/hitotsu) (一つ): a colour-card shedding game for two to eight, with the house rules people play. [Demo](https://johnmorrisdotca.github.io/hitotsu/).
|
|
366
|
+
- [Toranpu](https://github.com/johnmorrisdotca/toranpu) (トランプ): a deck of playing cards, card games with computer players, and solitaires. [Demo](https://johnmorrisdotca.github.io/toranpu/).
|
|
367
|
+
- [Tane](https://github.com/johnmorrisdotca/tane) (種): seeded random numbers and daily seeds, the same in every browser and on every server. [Demo](https://johnmorrisdotca.github.io/tane/).
|
|
368
|
+
- [Narabe](https://github.com/johnmorrisdotca/narabe) (並べ): one rules engine for abstract board games, from gomoku and Reversi to Go and checkers. [Demo](https://johnmorrisdotca.github.io/narabe/).
|
|
369
|
+
- [Tenka](https://github.com/johnmorrisdotca/tenka) (天下): world conquest for two to six, on a map of the real world. [Demo](https://johnmorrisdotca.github.io/tenka/).
|
|
370
|
+
- [Kumimoji](https://github.com/johnmorrisdotca/kumimoji) (組み文字): a crossword tile race, in English and Japanese kana. [Demo](https://johnmorrisdotca.github.io/kumimoji/).
|
|
371
|
+
- [Tsunagi](https://github.com/johnmorrisdotca/tsunagi) (繋ぎ): a line-joining logic puzzle whose every level has exactly one answer. [Demo](https://johnmorrisdotca.github.io/tsunagi/).
|
|
372
|
+
- [Jarajara](https://github.com/johnmorrisdotca/jarajara) (ジャラジャラ): mahjong tiles drawn as SVG, stacked layouts, and the matching solitaire Awase. [Demo](https://johnmorrisdotca.github.io/jarajara/).
|
|
373
|
+
- [Suido](https://github.com/johnmorrisdotca/suido) (水道): a pipe puzzle: turn the pieces until the water reaches every drain. [Demo](https://johnmorrisdotca.github.io/suido/).
|
|
374
|
+
- [Domino](https://github.com/johnmorrisdotca/domino) (ドミノ): dominoes and Mexican Train. [Demo](https://johnmorrisdotca.github.io/domino/).
|
|
375
|
+
- [Kotoba](https://github.com/johnmorrisdotca/kotoba) (言葉): word lists and word-game rules in English, French, German and Japanese. [Demo](https://johnmorrisdotca.github.io/kotoba/).
|
|
376
|
+
- [Sugoroku](https://github.com/johnmorrisdotca/sugoroku) (双六): backgammon and its variants, with the doubling cube and match play. [Demo](https://johnmorrisdotca.github.io/sugoroku/).
|
|
377
|
+
- [Kazu](https://github.com/johnmorrisdotca/kazu) (数): grid number puzzles: Sudoku and its variants, Futoshiki and Skyscrapers. [Demo](https://johnmorrisdotca.github.io/kazu/).
|
|
378
|
+
- [Meikyuu](https://github.com/johnmorrisdotca/meikyuu) (迷宮): mazes on squares, hexagons, triangles and circles, made from a seed and drawn through with a finger or the mouse. [Demo](https://johnmorrisdotca.github.io/meikyuu/).
|
|
379
|
+
- [Hikidashi](https://github.com/johnmorrisdotca/hikidashi) (引き出し): a drawer of small Japanese text tools: era dates, kanji numerals, readings and sentence difficulty. [Demo](https://johnmorrisdotca.github.io/hikidashi/).
|
|
380
|
+
- [Chizu](https://github.com/johnmorrisdotca/chizu) (地図): maps of the world and of countries' regions, in English and Japanese, with a quiz and callouts. [Demo](https://johnmorrisdotca.github.io/chizu/).
|
|
381
|
+
- [Bushu](https://github.com/johnmorrisdotca/bushu) (部首): find a kanji by the parts it is made of. [Demo](https://johnmorrisdotca.github.io/bushu/).
|
|
382
|
+
- [Tobiishi](https://github.com/johnmorrisdotca/tobiishi) (飛び石): peg solitaire with nine boards and seeded solvable challenges. [Demo](https://johnmorrisdotca.github.io/tobiishi/).
|
|
383
|
+
- [Jirai](https://github.com/johnmorrisdotca/jirai) (地雷): minesweeper on shaped grids with verified no-guess boards. [Demo](https://johnmorrisdotca.github.io/jirai/).
|
|
384
|
+
- [Gunjin](https://github.com/johnmorrisdotca/gunjin) (軍人): five hidden-rank strategy games with pass-the-device play. [Demo](https://johnmorrisdotca.github.io/gunjin/).
|
|
385
|
+
- [Karakuri](https://github.com/johnmorrisdotca/karakuri) (からくり): eight hyper-casual puzzle games, some of them physics: draw a shield, pull pins, cut ropes, slide blocks, pour tubes. [Demo](https://johnmorrisdotca.github.io/karakuri/).
|
|
386
|
+
- [Houseki](https://github.com/johnmorrisdotca/houseki) (宝石): gem and stone matching puzzles: falling triplets, stone collapse, colour chains and gem swap. [Demo](https://johnmorrisdotca.github.io/houseki/).
|
|
387
|
+
|
|
388
|
+
**This package is Kumimoji.** The demos of all twenty-four share one header and footer, so each links the rest.
|
|
389
|
+
<!-- family:end -->
|
|
357
390
|
|
|
358
391
|
## Features
|
|
359
392
|
|
|
@@ -377,6 +410,12 @@ Kumimoji has siblings, each made for the same site, each MIT, each at
|
|
|
377
410
|
- **Help**, if chosen: the words the hand's own tiles spell.
|
|
378
411
|
- **A table to play on.** `mountKumimoji` plays a whole game alone in plain
|
|
379
412
|
DOM, saves it and loads it back; `KumimojiBoard` draws a grid in React.
|
|
413
|
+
- **A tag.** `<kumimoji-table>` is the same game in one element, with no
|
|
414
|
+
framework.
|
|
415
|
+
- **One bag a day.** `kumimojiDailySeed(new Date())` is the same seed for
|
|
416
|
+
everybody, worldwide.
|
|
417
|
+
- **Played from a keyboard.** Arrow keys between squares and tiles, Enter or
|
|
418
|
+
Space to pick up and lay; see [Accessibility](#accessibility).
|
|
380
419
|
- **The table's words in English and Japanese**, and any other language by a
|
|
381
420
|
table of your own. **Themeable**: every colour is a CSS variable.
|
|
382
421
|
|
|
@@ -485,6 +524,26 @@ The tiles are the 45 base hiragana. A word is spelt with the base of each of
|
|
|
485
524
|
its kana, so ことば is played, and listed, as ことは: a tile shows the other
|
|
486
525
|
forms it plays as in its corner (`CORNER_FORMS`, `formsOfTile`).
|
|
487
526
|
|
|
527
|
+
### The day's seed
|
|
528
|
+
|
|
529
|
+
| Export | What it does |
|
|
530
|
+
| --- | --- |
|
|
531
|
+
| `kumimojiDay(date)` | The day a moment falls on, in UTC, written `YYYY-MM-DD` |
|
|
532
|
+
| `kumimojiDailySeed(date)` | That day's seed: the date as a number, so 2026-10-01 is `20261001` |
|
|
533
|
+
|
|
534
|
+
```ts
|
|
535
|
+
import { kumimojiDailySeed } from "@johnmorrisdotca/kumimoji";
|
|
536
|
+
|
|
537
|
+
kumimojiDailySeed(new Date("2026-10-01T12:00:00Z")); // → 20261001
|
|
538
|
+
kumimojiDailySeed(new Date("2026-10-01T23:59:59Z")); // → 20261001
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
The day is the UTC date, so it changes at one moment for the whole world, and
|
|
542
|
+
everybody who deals from it with the same settings (language, hand, level and
|
|
543
|
+
length) gets the same bag in the same order. It is the same number as Tane's
|
|
544
|
+
`dailySeed`, so a page that uses both agrees; Kumimoji does not need Tane. The
|
|
545
|
+
race against the clock is each player's own.
|
|
546
|
+
|
|
488
547
|
### A table of players
|
|
489
548
|
|
|
490
549
|
```ts
|
|
@@ -552,7 +611,7 @@ The JSON, as `kumimojiToJSON` writes it:
|
|
|
552
611
|
{
|
|
553
612
|
"format": 1,
|
|
554
613
|
"game": "kumimoji",
|
|
555
|
-
"generator": "kumimoji 1.
|
|
614
|
+
"generator": "kumimoji 1.2.1",
|
|
556
615
|
"size": 3,
|
|
557
616
|
"level": "hard",
|
|
558
617
|
"seed": 44,
|
|
@@ -614,6 +673,11 @@ What that means for a project that uses this package:
|
|
|
614
673
|
This is a description, not legal advice; the licences themselves are what
|
|
615
674
|
hold.
|
|
616
675
|
|
|
676
|
+
A real word the table calls "not a word", or a word it accepts that it should
|
|
677
|
+
not: open a [*word list*](https://github.com/johnmorrisdotca/kumimoji/issues/new?template=word-list.md)
|
|
678
|
+
issue. The lists are not edited by hand, so a word may have to be changed at
|
|
679
|
+
its source.
|
|
680
|
+
|
|
617
681
|
### Loading them
|
|
618
682
|
|
|
619
683
|
In a browser, `loadTileWords(language)` fetches the list as its own module
|
|
@@ -653,6 +717,8 @@ as you type. The entries:
|
|
|
653
717
|
| `@johnmorrisdotca/kumimoji` | Dealing, playing, judging, the table of players, the computer player, keeping and export, and the table's words |
|
|
654
718
|
| `@johnmorrisdotca/kumimoji/words` | Reads both word lists from this package's files, for a server, a test or a script: `loadTileWordsFromModule` |
|
|
655
719
|
| `@johnmorrisdotca/kumimoji/ui` | `mountKumimoji`, a whole game alone in plain DOM, and `boardModel` |
|
|
720
|
+
| `@johnmorrisdotca/kumimoji/element` | `KumimojiTable`, the `<kumimoji-table>` element's class, to extend or to define under another name |
|
|
721
|
+
| `@johnmorrisdotca/kumimoji/element/define` | Defines `<kumimoji-table>` on the page by being imported; exports nothing |
|
|
656
722
|
| `@johnmorrisdotca/kumimoji/react` | `KumimojiBoard` and `KumimojiTable` |
|
|
657
723
|
|
|
658
724
|
### Dealing and judging
|
|
@@ -771,6 +837,32 @@ entry has `boardModel(tiles, verdict, glyphOf?)`, which works out the squares
|
|
|
771
837
|
to draw and how each tile stands, with `BOARD_MARGIN` and `BOARD_LEAST`, and
|
|
772
838
|
`KUMIMOJI_STYLE`, the table's stylesheet as a string.
|
|
773
839
|
|
|
840
|
+
### The element
|
|
841
|
+
|
|
842
|
+
`<kumimoji-table>` is `mountKumimoji` as a tag: `@johnmorrisdotca/kumimoji/element/define`
|
|
843
|
+
defines it, and `@johnmorrisdotca/kumimoji/element` holds the class alone. Each
|
|
844
|
+
attribute is read again when it changes, and a change to any but `lang` deals a
|
|
845
|
+
new game. A table the rules do not offer (a hand of 5, say) draws nothing.
|
|
846
|
+
|
|
847
|
+
| Attribute | Default | What it does |
|
|
848
|
+
| --- | --- | --- |
|
|
849
|
+
| `language` | `english` | The tiles: `english` or `japanese` kana |
|
|
850
|
+
| `hand` | `7` | The opening hand: 3, 7 or 11 |
|
|
851
|
+
| `level` | `medium` | Wild tiles: `easy` the most, `medium` some, `hard` none |
|
|
852
|
+
| `length` | `short` | `short`, `medium` or `full`, all 144 tiles |
|
|
853
|
+
| `diagonals` | off | `on` to read every diagonal run of three or more too |
|
|
854
|
+
| `seed` | a new one each game | A whole number to deal from, or `daily` for the day's seed |
|
|
855
|
+
| `lang` | the page's | The language of the table's words: `en` or `ja` |
|
|
856
|
+
| `keep` | on | `off` leaves saving and loading out |
|
|
857
|
+
|
|
858
|
+
It fires `kumimoji-change` after every change to the game, with the game as it
|
|
859
|
+
would be saved as `event.detail.saved`, and `kumimoji-finish` when the last
|
|
860
|
+
tile is laid and every run is a word, with the deal, the play and the time it
|
|
861
|
+
took as `event.detail`; both bubble. It has `saved` (the game as it would be
|
|
862
|
+
saved), `table` (the handle `mountKumimoji` returns), `newGame(options?)` and
|
|
863
|
+
`setGame(saved)`. To style it, set the table's variables on `kumimoji-table
|
|
864
|
+
.km-root`: see [Theming](#theming).
|
|
865
|
+
|
|
774
866
|
### The React components
|
|
775
867
|
|
|
776
868
|
```jsx
|
|
@@ -808,10 +900,31 @@ export function Board() {
|
|
|
808
900
|
else for its `<div>`. Options are read when it mounts; give it a new `key` to
|
|
809
901
|
start over with different ones.
|
|
810
902
|
|
|
903
|
+
## Accessibility
|
|
904
|
+
|
|
905
|
+
- **A keyboard plays it.** Every square and every tile in the hand is a button
|
|
906
|
+
with a name. Tab lands on one square of the table and on one tile of the
|
|
907
|
+
hand, the arrow keys move between squares (and Left, Right, Home and End
|
|
908
|
+
between the hand's tiles), Enter or Space picks a tile up and lays it, and
|
|
909
|
+
Escape lets go of a tile you have picked up. The keyboard stays where it was
|
|
910
|
+
after each move, including on the buttons under the table, and the table's
|
|
911
|
+
description says so.
|
|
912
|
+
- **What is happening is said.** The line above the table, with the tiles left
|
|
913
|
+
and the time, and the line saying which runs are not words, are polite live
|
|
914
|
+
regions, and so is the note after saving or loading.
|
|
915
|
+
- **Targets are big enough.** Every button is at least 44 pixels each way,
|
|
916
|
+
which a test holds at a phone's width.
|
|
917
|
+
- **Not by colour alone.** A run that is not a word is ringed and listed in
|
|
918
|
+
words; a tile apart from the crossword is faded and said so.
|
|
919
|
+
- **Motion.** There is none to reduce: the table has no animation.
|
|
920
|
+
- **Not yet.** Letters cannot be typed to lay a tile, and a screen reader hears
|
|
921
|
+
the table as a grid of squares, not as the words they make.
|
|
922
|
+
|
|
811
923
|
## Theming
|
|
812
924
|
|
|
813
|
-
Every colour is a CSS variable on `.km-root`. Set them in your stylesheet
|
|
814
|
-
|
|
925
|
+
Every colour is a CSS variable on `.km-root`. Set them in your stylesheet on
|
|
926
|
+
`.km-root` with a selector more specific than the table's own, such as
|
|
927
|
+
`#table .km-root`, or pass them as `theme`, which sets them
|
|
815
928
|
on the table itself and so wins in light and dark alike.
|
|
816
929
|
|
|
817
930
|
| Variable | What it sets | Light | Dark |
|
|
@@ -886,7 +999,7 @@ that language, whose licence lets its words be shipped.
|
|
|
886
999
|
Any current browser: Chrome, Edge, Firefox and Safari, on a desk or a phone.
|
|
887
1000
|
It needs ES2020 and dynamic `import()`. The table is tested in Chromium and in
|
|
888
1001
|
WebKit, Safari's engine, at phone size with touch and on a desktop. The rules
|
|
889
|
-
run in Node
|
|
1002
|
+
run in Node 22 and later, Deno, Bun and web workers, and load by `import` and
|
|
890
1003
|
by `require()`.
|
|
891
1004
|
|
|
892
1005
|
## The command line
|
|
@@ -907,12 +1020,12 @@ words.allowed.has("snowy"); // → true
|
|
|
907
1020
|
## Roadmap
|
|
908
1021
|
|
|
909
1022
|
- More languages for the tiles, each from a real dictionary of that language.
|
|
910
|
-
-
|
|
911
|
-
|
|
1023
|
+
- Typing a letter to lay that tile, in the plain-DOM table, which is
|
|
1024
|
+
otherwise played from a keyboard.
|
|
912
1025
|
- A table of players in plain DOM, beside the game alone.
|
|
913
1026
|
- A computer player at three strengths.
|
|
914
1027
|
- Word definitions for a finished crossword.
|
|
915
|
-
- A Vue wrapper
|
|
1028
|
+
- A Vue wrapper, beside the React one and the tag.
|
|
916
1029
|
|
|
917
1030
|
Left out on purpose: play between devices, which needs a server (a table is
|
|
918
1031
|
kept as text made to be sent, and Itsutsu sends it; the sending is yours); a
|
|
@@ -930,9 +1043,10 @@ pnpm install
|
|
|
930
1043
|
pnpm check # lint, types and tests
|
|
931
1044
|
pnpm test:table # the demo in real browsers, by taps
|
|
932
1045
|
pnpm site # build the demo into ./site, then serve it
|
|
1046
|
+
pnpm pictures # take the README's two pictures from the built demo
|
|
933
1047
|
```
|
|
934
1048
|
|
|
935
|
-
Please follow the [code of conduct](./CODE_OF_CONDUCT.md).
|
|
1049
|
+
Please follow the [code of conduct](./CODE_OF_CONDUCT.md). A way to make a check or a deal run for long, or markup that gets out of the drawing, is for the [security policy](./SECURITY.md), not a public issue.
|
|
936
1050
|
|
|
937
1051
|
## Changes
|
|
938
1052
|
|
package/dist/daily.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE SEED A DAY, THE SAME FOR EVERYBODY. A day is a calendar date in UTC and
|
|
3
|
+
* its seed is that date as a number: 2026-10-01 is 20261001, which
|
|
4
|
+
* `generateKumimoji` accepts. It is the same number Tane's `dailySeed` gives,
|
|
5
|
+
* so a page that uses both agrees; nothing here needs Tane.
|
|
6
|
+
*
|
|
7
|
+
* Everybody who deals from the day's seed with the same settings (language,
|
|
8
|
+
* hand, level, length) gets the same bag, in the same order. The race against
|
|
9
|
+
* the clock is then theirs.
|
|
10
|
+
*/
|
|
11
|
+
/** The day a moment falls on, in UTC, written `YYYY-MM-DD`. Throws a `RangeError` for an invalid `Date`. */
|
|
12
|
+
export declare function kumimojiDay(at: Date): string;
|
|
13
|
+
/** Today's seed, the date as a number: 2026-10-01 is 20261001. The day is UTC, so it is one seed worldwide. */
|
|
14
|
+
export declare function kumimojiDailySeed(at: Date): number;
|
package/dist/daily.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE SEED A DAY, THE SAME FOR EVERYBODY. A day is a calendar date in UTC and
|
|
3
|
+
* its seed is that date as a number: 2026-10-01 is 20261001, which
|
|
4
|
+
* `generateKumimoji` accepts. It is the same number Tane's `dailySeed` gives,
|
|
5
|
+
* so a page that uses both agrees; nothing here needs Tane.
|
|
6
|
+
*
|
|
7
|
+
* Everybody who deals from the day's seed with the same settings (language,
|
|
8
|
+
* hand, level, length) gets the same bag, in the same order. The race against
|
|
9
|
+
* the clock is then theirs.
|
|
10
|
+
*/
|
|
11
|
+
const pad = (value, width) => String(value).padStart(width, "0");
|
|
12
|
+
/** The day a moment falls on, in UTC, written `YYYY-MM-DD`. Throws a `RangeError` for an invalid `Date`. */
|
|
13
|
+
export function kumimojiDay(at) {
|
|
14
|
+
if (Number.isNaN(at.getTime()))
|
|
15
|
+
throw new RangeError("kumimoji: an invalid Date has no day");
|
|
16
|
+
return `${pad(at.getUTCFullYear(), 4)}-${pad(at.getUTCMonth() + 1, 2)}-${pad(at.getUTCDate(), 2)}`;
|
|
17
|
+
}
|
|
18
|
+
/** Today's seed, the date as a number: 2026-10-01 is 20261001. The day is UTC, so it is one seed worldwide. */
|
|
19
|
+
export function kumimojiDailySeed(at) {
|
|
20
|
+
const [year, month, day] = kumimojiDay(at).split("-").map(Number);
|
|
21
|
+
return year * 10000 + month * 100 + day;
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defines the `<kumimoji-table>` element on the page. Import it for its effect:
|
|
3
|
+
*
|
|
4
|
+
* ```html
|
|
5
|
+
* <script type="module" src="https://cdn.jsdelivr.net/npm/@johnmorrisdotca/kumimoji@1/dist/element-define.js"></script>
|
|
6
|
+
* <kumimoji-table language="english" hand="7"></kumimoji-table>
|
|
7
|
+
* ```
|
|
8
|
+
*
|
|
9
|
+
* A tag already defined is left as it is, and on a server, where there is no page, nothing happens.
|
|
10
|
+
*/
|
|
11
|
+
import { KumimojiTable } from "./element.js";
|
|
12
|
+
if (typeof customElements !== "undefined" && customElements.get("kumimoji-table") === undefined)
|
|
13
|
+
customElements.define("kumimoji-table", KumimojiTable);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type KumimojiTableHandle } from "./ui/mount.ts";
|
|
2
|
+
import type { KumimojiSaved } from "./export.ts";
|
|
3
|
+
declare const ElementBase: typeof HTMLElement;
|
|
4
|
+
/**
|
|
5
|
+
* THE `<kumimoji-table>` ELEMENT: a game of Kumimoji alone in a tag, with no
|
|
6
|
+
* framework. `@johnmorrisdotca/kumimoji/element/define` defines it; this entry
|
|
7
|
+
* holds the class alone, to extend or to define under another name. Safe to
|
|
8
|
+
* import on a server, where there is no page: the class then extends nothing.
|
|
9
|
+
*
|
|
10
|
+
* ```html
|
|
11
|
+
* <kumimoji-table language="english" hand="7" seed="2026"></kumimoji-table>
|
|
12
|
+
* <kumimoji-table language="japanese" level="easy" seed="daily"></kumimoji-table>
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* Attributes (each is read again when it changes; a change to any but `lang`
|
|
16
|
+
* deals a new game):
|
|
17
|
+
* - `language`: the tiles, `english` (default) or `japanese` kana.
|
|
18
|
+
* - `hand`: the opening hand, 3, 7 (default) or 11.
|
|
19
|
+
* - `level`: how many tiles are wild, `easy` (most), `medium` (default) or `hard` (none).
|
|
20
|
+
* - `length`: how much of the set the bag holds, `short` (default), `medium` or `full`.
|
|
21
|
+
* - `diagonals`: `on` to read every diagonal run of three or more too.
|
|
22
|
+
* - `seed`: a whole number to deal from, or `daily` for the day's seed (`kumimojiDailySeed`); a new one if left out.
|
|
23
|
+
* - `lang`: the language of the table's words, `en` or `ja`, or the page's. `keep`: `off` to leave saving and loading out.
|
|
24
|
+
*
|
|
25
|
+
* It fires `kumimoji-change` after every change to the game, with the game as it would
|
|
26
|
+
* be saved as `event.detail.saved`, and `kumimoji-finish` when the last tile is laid and every run
|
|
27
|
+
* is a word, with `event.detail` the deal, the play and the time it took. It has the methods
|
|
28
|
+
* `newGame()` and `setGame()`. A table the rules do not offer (a hand of 5, say) draws nothing.
|
|
29
|
+
*/
|
|
30
|
+
export declare class KumimojiTable extends ElementBase {
|
|
31
|
+
#private;
|
|
32
|
+
static observedAttributes: string[];
|
|
33
|
+
connectedCallback(): void;
|
|
34
|
+
disconnectedCallback(): void;
|
|
35
|
+
attributeChangedCallback(): void;
|
|
36
|
+
/** The mounted table's handle (`mountKumimoji`), or null while the attributes name no table the rules offer. */
|
|
37
|
+
get table(): KumimojiTableHandle | null;
|
|
38
|
+
/** The game as it would be saved, or null while there is none (or it is still being dealt). */
|
|
39
|
+
get saved(): KumimojiSaved | null;
|
|
40
|
+
/** A new game at the same table: see `KumimojiTableHandle.newGame`. */
|
|
41
|
+
newGame(options?: Parameters<KumimojiTableHandle["newGame"]>[0]): Promise<void>;
|
|
42
|
+
/** Put a game on the table: see `KumimojiTableHandle.setGame`. */
|
|
43
|
+
setGame(saved: KumimojiSaved): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
export {};
|
package/dist/element.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
2
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
3
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
4
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
5
|
+
};
|
|
6
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
7
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
8
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
9
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
10
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
11
|
+
};
|
|
12
|
+
var _KumimojiTable_instances, _KumimojiTable_table, _KumimojiTable_key, _KumimojiTable_queued, _KumimojiTable_seed, _KumimojiTable_refresh;
|
|
13
|
+
import { kumimojiDailySeed } from "./daily.js";
|
|
14
|
+
import { KUMIMOJI_HANDS } from "./tiles.constants.js";
|
|
15
|
+
import { mountKumimoji } from "./ui/mount.js";
|
|
16
|
+
const ElementBase = typeof HTMLElement === "undefined" ? class {
|
|
17
|
+
} : HTMLElement;
|
|
18
|
+
const oneOf = (value, allowed) => (allowed.includes(value) ? value : undefined);
|
|
19
|
+
const isOn = (value) => value !== null && ["true", "on", "1", "yes"].includes(value.toLowerCase());
|
|
20
|
+
const HANDS = Object.values(KUMIMOJI_HANDS);
|
|
21
|
+
/**
|
|
22
|
+
* THE `<kumimoji-table>` ELEMENT: a game of Kumimoji alone in a tag, with no
|
|
23
|
+
* framework. `@johnmorrisdotca/kumimoji/element/define` defines it; this entry
|
|
24
|
+
* holds the class alone, to extend or to define under another name. Safe to
|
|
25
|
+
* import on a server, where there is no page: the class then extends nothing.
|
|
26
|
+
*
|
|
27
|
+
* ```html
|
|
28
|
+
* <kumimoji-table language="english" hand="7" seed="2026"></kumimoji-table>
|
|
29
|
+
* <kumimoji-table language="japanese" level="easy" seed="daily"></kumimoji-table>
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* Attributes (each is read again when it changes; a change to any but `lang`
|
|
33
|
+
* deals a new game):
|
|
34
|
+
* - `language`: the tiles, `english` (default) or `japanese` kana.
|
|
35
|
+
* - `hand`: the opening hand, 3, 7 (default) or 11.
|
|
36
|
+
* - `level`: how many tiles are wild, `easy` (most), `medium` (default) or `hard` (none).
|
|
37
|
+
* - `length`: how much of the set the bag holds, `short` (default), `medium` or `full`.
|
|
38
|
+
* - `diagonals`: `on` to read every diagonal run of three or more too.
|
|
39
|
+
* - `seed`: a whole number to deal from, or `daily` for the day's seed (`kumimojiDailySeed`); a new one if left out.
|
|
40
|
+
* - `lang`: the language of the table's words, `en` or `ja`, or the page's. `keep`: `off` to leave saving and loading out.
|
|
41
|
+
*
|
|
42
|
+
* It fires `kumimoji-change` after every change to the game, with the game as it would
|
|
43
|
+
* be saved as `event.detail.saved`, and `kumimoji-finish` when the last tile is laid and every run
|
|
44
|
+
* is a word, with `event.detail` the deal, the play and the time it took. It has the methods
|
|
45
|
+
* `newGame()` and `setGame()`. A table the rules do not offer (a hand of 5, say) draws nothing.
|
|
46
|
+
*/
|
|
47
|
+
export class KumimojiTable extends ElementBase {
|
|
48
|
+
constructor() {
|
|
49
|
+
super(...arguments);
|
|
50
|
+
_KumimojiTable_instances.add(this);
|
|
51
|
+
_KumimojiTable_table.set(this, null);
|
|
52
|
+
_KumimojiTable_key.set(this, "");
|
|
53
|
+
_KumimojiTable_queued.set(this, false);
|
|
54
|
+
_KumimojiTable_seed.set(this, null);
|
|
55
|
+
}
|
|
56
|
+
connectedCallback() {
|
|
57
|
+
__classPrivateFieldGet(this, _KumimojiTable_instances, "m", _KumimojiTable_refresh).call(this);
|
|
58
|
+
}
|
|
59
|
+
disconnectedCallback() {
|
|
60
|
+
__classPrivateFieldGet(this, _KumimojiTable_table, "f")?.destroy();
|
|
61
|
+
__classPrivateFieldSet(this, _KumimojiTable_table, null, "f");
|
|
62
|
+
__classPrivateFieldSet(this, _KumimojiTable_key, "", "f");
|
|
63
|
+
}
|
|
64
|
+
attributeChangedCallback() {
|
|
65
|
+
if (!this.isConnected || __classPrivateFieldGet(this, _KumimojiTable_queued, "f"))
|
|
66
|
+
return;
|
|
67
|
+
__classPrivateFieldSet(this, _KumimojiTable_queued, true, "f");
|
|
68
|
+
queueMicrotask(() => {
|
|
69
|
+
__classPrivateFieldSet(this, _KumimojiTable_queued, false, "f");
|
|
70
|
+
__classPrivateFieldGet(this, _KumimojiTable_instances, "m", _KumimojiTable_refresh).call(this);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/** The mounted table's handle (`mountKumimoji`), or null while the attributes name no table the rules offer. */
|
|
74
|
+
get table() {
|
|
75
|
+
return __classPrivateFieldGet(this, _KumimojiTable_table, "f");
|
|
76
|
+
}
|
|
77
|
+
/** The game as it would be saved, or null while there is none (or it is still being dealt). */
|
|
78
|
+
get saved() {
|
|
79
|
+
return __classPrivateFieldGet(this, _KumimojiTable_table, "f")?.saved() ?? null;
|
|
80
|
+
}
|
|
81
|
+
/** A new game at the same table: see `KumimojiTableHandle.newGame`. */
|
|
82
|
+
newGame(options) {
|
|
83
|
+
return __classPrivateFieldGet(this, _KumimojiTable_table, "f")?.newGame(options) ?? Promise.resolve();
|
|
84
|
+
}
|
|
85
|
+
/** Put a game on the table: see `KumimojiTableHandle.setGame`. */
|
|
86
|
+
setGame(saved) {
|
|
87
|
+
return __classPrivateFieldGet(this, _KumimojiTable_table, "f")?.setGame(saved) ?? Promise.resolve();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
_KumimojiTable_table = new WeakMap(), _KumimojiTable_key = new WeakMap(), _KumimojiTable_queued = new WeakMap(), _KumimojiTable_seed = new WeakMap(), _KumimojiTable_instances = new WeakSet(), _KumimojiTable_refresh = function _KumimojiTable_refresh() {
|
|
91
|
+
const language = oneOf(this.getAttribute("language"), ["english", "japanese"]);
|
|
92
|
+
const handAttribute = this.getAttribute("hand");
|
|
93
|
+
const hand = handAttribute === null ? undefined : Number(handAttribute);
|
|
94
|
+
const level = oneOf(this.getAttribute("level"), ["easy", "medium", "hard"]);
|
|
95
|
+
const gameLength = oneOf(this.getAttribute("length"), ["short", "medium", "full"]);
|
|
96
|
+
const diagonals = isOn(this.getAttribute("diagonals"));
|
|
97
|
+
const seedAttribute = this.getAttribute("seed");
|
|
98
|
+
const asked = Number(seedAttribute);
|
|
99
|
+
const seed = seedAttribute === "daily"
|
|
100
|
+
? kumimojiDailySeed(new Date())
|
|
101
|
+
: seedAttribute !== null && Number.isInteger(asked) && asked >= 0 && asked <= 0xffffffff
|
|
102
|
+
? asked
|
|
103
|
+
: (__classPrivateFieldSet(this, _KumimojiTable_seed, __classPrivateFieldGet(this, _KumimojiTable_seed, "f") ?? Math.floor(Math.random() * 2 ** 31), "f"));
|
|
104
|
+
const lang = this.getAttribute("lang");
|
|
105
|
+
const locale = lang === "en" || lang === "ja" ? lang : undefined;
|
|
106
|
+
const keep = !["false", "off", "0", "no"].includes((this.getAttribute("keep") ?? "").toLowerCase());
|
|
107
|
+
const key = JSON.stringify([language, hand, level, gameLength, diagonals, seed, keep]);
|
|
108
|
+
if (key === __classPrivateFieldGet(this, _KumimojiTable_key, "f") && __classPrivateFieldGet(this, _KumimojiTable_table, "f") !== null) {
|
|
109
|
+
__classPrivateFieldGet(this, _KumimojiTable_table, "f").setLocale(locale ?? (document.documentElement.lang.toLowerCase().startsWith("ja") ? "ja" : "en"));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
__classPrivateFieldGet(this, _KumimojiTable_table, "f")?.destroy();
|
|
113
|
+
__classPrivateFieldSet(this, _KumimojiTable_table, null, "f");
|
|
114
|
+
__classPrivateFieldSet(this, _KumimojiTable_key, key, "f");
|
|
115
|
+
if (hand !== undefined && !HANDS.includes(hand))
|
|
116
|
+
return;
|
|
117
|
+
__classPrivateFieldSet(this, _KumimojiTable_table, mountKumimoji(this, {
|
|
118
|
+
language,
|
|
119
|
+
hand,
|
|
120
|
+
level,
|
|
121
|
+
gameLength,
|
|
122
|
+
diagonals,
|
|
123
|
+
seed,
|
|
124
|
+
locale,
|
|
125
|
+
keep,
|
|
126
|
+
onChange: (saved) => this.dispatchEvent(new CustomEvent("kumimoji-change", { detail: { saved }, bubbles: true })),
|
|
127
|
+
onFinish: (result) => this.dispatchEvent(new CustomEvent("kumimoji-finish", { detail: result, bubbles: true })),
|
|
128
|
+
}), "f");
|
|
129
|
+
};
|
|
130
|
+
KumimojiTable.observedAttributes = ["language", "hand", "level", "length", "diagonals", "seed", "lang", "keep"];
|
package/dist/index.d.ts
CHANGED
|
@@ -42,6 +42,7 @@ export * from "./computer.types.ts";
|
|
|
42
42
|
export * from "./computerPlay.ts";
|
|
43
43
|
export * from "./computerTurn.ts";
|
|
44
44
|
export * from "./random.ts";
|
|
45
|
+
export * from "./daily.ts";
|
|
45
46
|
export * from "./export.ts";
|
|
46
47
|
export * from "./strings.ts";
|
|
47
48
|
export { KUMIMOJI_VERSION } from "./version.ts";
|
package/dist/index.js
CHANGED
|
@@ -42,6 +42,7 @@ export * from "./computer.types.js";
|
|
|
42
42
|
export * from "./computerPlay.js";
|
|
43
43
|
export * from "./computerTurn.js";
|
|
44
44
|
export * from "./random.js";
|
|
45
|
+
export * from "./daily.js";
|
|
45
46
|
export * from "./export.js";
|
|
46
47
|
export * from "./strings.js";
|
|
47
48
|
export { KUMIMOJI_VERSION } from "./version.js";
|
package/dist/strings.d.ts
CHANGED
package/dist/strings.js
CHANGED
|
@@ -17,6 +17,7 @@ const EN = {
|
|
|
17
17
|
left: "{n} tiles left in the bag · {time}",
|
|
18
18
|
finished: "Finished in {time}. Every tile laid, every run a word.",
|
|
19
19
|
tableLabel: "The table",
|
|
20
|
+
tableKeys: "Arrow keys move between squares, Enter or Space taps one, and Escape lets go of a tile you have picked up.",
|
|
20
21
|
handLabel: "Your hand",
|
|
21
22
|
emptySquare: "Empty square",
|
|
22
23
|
wildTile: "{tile}, wild",
|
|
@@ -53,6 +54,7 @@ const JA = {
|
|
|
53
54
|
left: "袋の残り{n}枚 · {time}",
|
|
54
55
|
finished: "{time}で完成しました。すべてのタイルを置き、どの並びも単語になっています。",
|
|
55
56
|
tableLabel: "テーブル",
|
|
57
|
+
tableKeys: "矢印キーでマスを移動し、Enterキーかスペースキーでタップします。Escapeキーで、選んだタイルの選択を解除します。",
|
|
56
58
|
handLabel: "手札",
|
|
57
59
|
emptySquare: "空きマス",
|
|
58
60
|
wildTile: "{tile}(ワイルド)",
|
package/dist/ui/mount.js
CHANGED
|
@@ -58,6 +58,9 @@ export function mountKumimoji(target, options = {}) {
|
|
|
58
58
|
let dealt = null;
|
|
59
59
|
let play = null;
|
|
60
60
|
let held = null;
|
|
61
|
+
// Where a keyboard is: the square and the hand tile that Tab lands on, and the one the arrow keys move from.
|
|
62
|
+
let onSquare = null;
|
|
63
|
+
let onHand = 0;
|
|
61
64
|
let started = 0;
|
|
62
65
|
let finishedAt = null;
|
|
63
66
|
let destroyed = false;
|
|
@@ -77,6 +80,7 @@ export function mountKumimoji(target, options = {}) {
|
|
|
77
80
|
board.dataset.testid = "km-board";
|
|
78
81
|
const notes = node("p", "km-notes");
|
|
79
82
|
notes.dataset.testid = "km-notes";
|
|
83
|
+
notes.setAttribute("aria-live", "polite");
|
|
80
84
|
const hand = node("div", "km-hand");
|
|
81
85
|
hand.dataset.testid = "km-hand";
|
|
82
86
|
hand.setAttribute("role", "group");
|
|
@@ -195,6 +199,62 @@ export function mountKumimoji(target, options = {}) {
|
|
|
195
199
|
if (at !== null && at !== undefined)
|
|
196
200
|
tapHand(Number(at));
|
|
197
201
|
});
|
|
202
|
+
// Keyboard play. The squares and the hand's tiles are buttons, so Enter and Space tap them; each is one stop for
|
|
203
|
+
// Tab, and the arrow keys move between squares (by the grid) and between the hand's tiles. Escape puts a held tile down.
|
|
204
|
+
board.addEventListener("keydown", (event) => {
|
|
205
|
+
const cell = event.target.closest("[data-square]");
|
|
206
|
+
const grid = board.querySelector(".km-grid");
|
|
207
|
+
if (cell === null || grid === null || event.altKey || event.ctrlKey || event.metaKey)
|
|
208
|
+
return;
|
|
209
|
+
const cells = [...grid.children];
|
|
210
|
+
const cols = Number(grid.dataset.cols);
|
|
211
|
+
const at = cells.indexOf(cell);
|
|
212
|
+
const step = event.key === "ArrowLeft" ? (at % cols > 0 ? -1 : 0) : event.key === "ArrowRight" ? (at % cols < cols - 1 ? 1 : 0) : event.key === "ArrowUp" ? (at - cols >= 0 ? -cols : 0) : event.key === "ArrowDown" ? (at + cols < cells.length ? cols : 0) : null;
|
|
213
|
+
if (step === null)
|
|
214
|
+
return;
|
|
215
|
+
event.preventDefault();
|
|
216
|
+
const next = cells[at + step];
|
|
217
|
+
if (step === 0 || next === undefined)
|
|
218
|
+
return;
|
|
219
|
+
cell.tabIndex = -1;
|
|
220
|
+
next.tabIndex = 0;
|
|
221
|
+
onSquare = next.dataset.square ?? null;
|
|
222
|
+
next.focus();
|
|
223
|
+
});
|
|
224
|
+
board.addEventListener("focusin", (event) => {
|
|
225
|
+
const square = event.target.closest("[data-square]")?.getAttribute("data-square");
|
|
226
|
+
if (square !== null && square !== undefined)
|
|
227
|
+
onSquare = square;
|
|
228
|
+
});
|
|
229
|
+
hand.addEventListener("focusin", (event) => {
|
|
230
|
+
const at = event.target.closest("[data-hand]")?.getAttribute("data-hand");
|
|
231
|
+
if (at !== null && at !== undefined)
|
|
232
|
+
onHand = Number(at);
|
|
233
|
+
});
|
|
234
|
+
hand.addEventListener("keydown", (event) => {
|
|
235
|
+
const tile = event.target.closest("[data-hand]");
|
|
236
|
+
if (tile === null || event.altKey || event.ctrlKey || event.metaKey)
|
|
237
|
+
return;
|
|
238
|
+
const tiles = [...hand.querySelectorAll("[data-hand]")];
|
|
239
|
+
const at = tiles.indexOf(tile);
|
|
240
|
+
const to = event.key === "ArrowLeft" ? at - 1 : event.key === "ArrowRight" ? at + 1 : event.key === "Home" ? 0 : event.key === "End" ? tiles.length - 1 : null;
|
|
241
|
+
if (to === null)
|
|
242
|
+
return;
|
|
243
|
+
event.preventDefault();
|
|
244
|
+
const next = tiles[to];
|
|
245
|
+
if (next === undefined)
|
|
246
|
+
return;
|
|
247
|
+
tile.tabIndex = -1;
|
|
248
|
+
next.tabIndex = 0;
|
|
249
|
+
onHand = to;
|
|
250
|
+
next.focus();
|
|
251
|
+
});
|
|
252
|
+
root.addEventListener("keydown", (event) => {
|
|
253
|
+
if (event.key !== "Escape" || held === null)
|
|
254
|
+
return;
|
|
255
|
+
held = null;
|
|
256
|
+
render();
|
|
257
|
+
});
|
|
198
258
|
function renderStatus() {
|
|
199
259
|
root.dataset.state = play === null ? "dealing" : finishedAt !== null ? "finished" : "playing";
|
|
200
260
|
if (play === null) {
|
|
@@ -212,6 +272,9 @@ export function mountKumimoji(target, options = {}) {
|
|
|
212
272
|
grid.style.gridTemplateColumns = `repeat(${model.cols}, var(--km-square))`;
|
|
213
273
|
grid.setAttribute("role", "group");
|
|
214
274
|
grid.setAttribute("aria-label", say.tableLabel);
|
|
275
|
+
grid.setAttribute("aria-description", say.tableKeys);
|
|
276
|
+
grid.dataset.cols = String(model.cols);
|
|
277
|
+
const cells = [];
|
|
215
278
|
for (const square of model.squares) {
|
|
216
279
|
const cell = node("button", "km-square");
|
|
217
280
|
cell.type = "button";
|
|
@@ -231,8 +294,16 @@ export function mountKumimoji(target, options = {}) {
|
|
|
231
294
|
}
|
|
232
295
|
if (held?.from === "table" && held.square === square.square)
|
|
233
296
|
cell.classList.add("km-held");
|
|
297
|
+
cell.tabIndex = -1;
|
|
298
|
+
cells.push(cell);
|
|
234
299
|
grid.append(cell);
|
|
235
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
|
+
}
|
|
236
307
|
board.replaceChildren(grid);
|
|
237
308
|
notes.textContent = verdict.notWords.length > 0 ? kumimojiSay(say.notWords, { words: verdict.notWords.join(", ") }) : verdict.apart.size > 0 ? say.apart : "";
|
|
238
309
|
}
|
|
@@ -251,6 +322,7 @@ export function mountKumimoji(target, options = {}) {
|
|
|
251
322
|
made.classList.add("km-held");
|
|
252
323
|
made.dataset.tile = face.blank ? "*" : words.glyphOf(tile);
|
|
253
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;
|
|
254
326
|
hand.append(made);
|
|
255
327
|
});
|
|
256
328
|
if (play.hand.length === 0 && finishedAt === null)
|
|
@@ -309,9 +381,35 @@ export function mountKumimoji(target, options = {}) {
|
|
|
309
381
|
saveText.disabled = play === null;
|
|
310
382
|
keepNote.textContent = note;
|
|
311
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
|
+
}
|
|
312
409
|
function render() {
|
|
313
410
|
if (destroyed)
|
|
314
411
|
return;
|
|
412
|
+
const where = keyboardAt();
|
|
315
413
|
root.lang = locale;
|
|
316
414
|
renderStatus();
|
|
317
415
|
renderKeep();
|
|
@@ -322,6 +420,7 @@ export function mountKumimoji(target, options = {}) {
|
|
|
322
420
|
renderPicker();
|
|
323
421
|
renderHand();
|
|
324
422
|
renderControls(verdict);
|
|
423
|
+
keyboardBack(where);
|
|
325
424
|
}
|
|
326
425
|
const handle = {
|
|
327
426
|
play: () => play,
|
package/dist/ui/style.d.ts
CHANGED
|
@@ -4,4 +4,4 @@
|
|
|
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
6
|
*/
|
|
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-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";
|
|
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";
|
package/dist/ui/style.js
CHANGED
|
@@ -28,7 +28,7 @@ export const KUMIMOJI_STYLE = `
|
|
|
28
28
|
.km-misspelt { box-shadow: 0 0 0 3px var(--km-wrong) inset, 0 2px 0 rgba(0,0,0,.35); }
|
|
29
29
|
.km-apart { opacity: .6; }
|
|
30
30
|
.km-held { outline: 3px solid var(--km-held); outline-offset: 1px; }
|
|
31
|
-
.km-square:focus-visible, .km-button:focus-visible, .km-keep-title:focus-visible { outline: 3px solid var(--km-held); outline-offset: 2px; }
|
|
31
|
+
.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; }
|
|
32
32
|
.km-notes { margin: 0; min-height: 1.4em; font-size: 14px; opacity: .8; overflow-wrap: anywhere; }
|
|
33
33
|
.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); }
|
|
34
34
|
.km-empty { opacity: .6; font-size: 14px; }
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The version of this package, as `package.json` has it. A test holds the two together. */
|
|
2
|
-
export declare const KUMIMOJI_VERSION = "1.
|
|
2
|
+
export declare const KUMIMOJI_VERSION = "1.2.1";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The version of this package, as `package.json` has it. A test holds the two together. */
|
|
2
|
-
export const KUMIMOJI_VERSION = "1.
|
|
2
|
+
export const KUMIMOJI_VERSION = "1.2.1";
|
package/package.json
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnmorrisdotca/kumimoji",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Crossword tile race:
|
|
3
|
+
"version": "1.2.1",
|
|
4
|
+
"description": "Crossword tile race: each player builds a crossword from a bag of letter tiles, in English or Japanese kana, alone against the clock or at a table of two to eight. Seeded deals, a computer player and a playable table. Zero dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"word-game",
|
|
7
|
+
"custom-element",
|
|
8
|
+
"web-component",
|
|
7
9
|
"word-puzzle",
|
|
8
10
|
"crossword",
|
|
9
11
|
"crossword-game",
|
|
@@ -77,11 +79,7 @@
|
|
|
77
79
|
"url": "git+https://github.com/johnmorrisdotca/kumimoji.git"
|
|
78
80
|
},
|
|
79
81
|
"license": "MIT AND CC-BY-SA-4.0",
|
|
80
|
-
"author":
|
|
81
|
-
"name": "John Morris",
|
|
82
|
-
"email": "john@johnmorris.ca",
|
|
83
|
-
"url": "https://johnmorris.ca"
|
|
84
|
-
},
|
|
82
|
+
"author": "John Morris",
|
|
85
83
|
"type": "module",
|
|
86
84
|
"main": "./dist/index.js",
|
|
87
85
|
"module": "./dist/index.js",
|
|
@@ -89,19 +87,33 @@
|
|
|
89
87
|
"exports": {
|
|
90
88
|
".": {
|
|
91
89
|
"types": "./dist/index.d.ts",
|
|
90
|
+
"import": "./dist/index.js",
|
|
92
91
|
"default": "./dist/index.js"
|
|
93
92
|
},
|
|
94
93
|
"./words": {
|
|
95
94
|
"types": "./dist/words.d.ts",
|
|
95
|
+
"import": "./dist/words.js",
|
|
96
96
|
"default": "./dist/words.js"
|
|
97
97
|
},
|
|
98
98
|
"./ui": {
|
|
99
99
|
"types": "./dist/ui.d.ts",
|
|
100
|
+
"import": "./dist/ui.js",
|
|
100
101
|
"default": "./dist/ui.js"
|
|
101
102
|
},
|
|
102
103
|
"./react": {
|
|
103
104
|
"types": "./dist/react.d.ts",
|
|
105
|
+
"import": "./dist/react.js",
|
|
104
106
|
"default": "./dist/react.js"
|
|
107
|
+
},
|
|
108
|
+
"./element": {
|
|
109
|
+
"types": "./dist/element.d.ts",
|
|
110
|
+
"import": "./dist/element.js",
|
|
111
|
+
"default": "./dist/element.js"
|
|
112
|
+
},
|
|
113
|
+
"./element/define": {
|
|
114
|
+
"types": "./dist/element-define.d.ts",
|
|
115
|
+
"import": "./dist/element-define.js",
|
|
116
|
+
"default": "./dist/element-define.js"
|
|
105
117
|
}
|
|
106
118
|
},
|
|
107
119
|
"files": [
|
|
@@ -114,10 +126,11 @@
|
|
|
114
126
|
],
|
|
115
127
|
"sideEffects": [
|
|
116
128
|
"./dist/words.js",
|
|
117
|
-
"./dist/tileWordsModule.js"
|
|
129
|
+
"./dist/tileWordsModule.js",
|
|
130
|
+
"./dist/element-define.js"
|
|
118
131
|
],
|
|
119
132
|
"engines": {
|
|
120
|
-
"node": ">=
|
|
133
|
+
"node": ">=22"
|
|
121
134
|
},
|
|
122
135
|
"scripts": {
|
|
123
136
|
"build": "tsc -p tsconfig.json",
|
|
@@ -126,9 +139,11 @@
|
|
|
126
139
|
"test": "vitest run",
|
|
127
140
|
"test:table": "pnpm site && playwright test",
|
|
128
141
|
"test:package": "pnpm build && node scripts/check-package.mjs",
|
|
142
|
+
"family:readme": "node scripts/family-readme.mjs",
|
|
129
143
|
"test:frameworks": "pnpm build && node scripts/check-frameworks.mjs",
|
|
130
144
|
"check": "pnpm lint && pnpm typecheck && pnpm test",
|
|
131
145
|
"site": "pnpm build && node scripts/site.mjs",
|
|
146
|
+
"pictures": "pnpm site && node scripts/readme-pictures.mjs",
|
|
132
147
|
"docs:make": "node scripts/docs.mjs",
|
|
133
148
|
"prepublishOnly": "pnpm check && pnpm build"
|
|
134
149
|
},
|