@johnmorrisdotca/kumimoji 1.1.0 → 1.2.0
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 +34 -1
- package/CONTRIBUTING.md +11 -0
- package/README.md +200 -37
- 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 +15 -3
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,38 @@ All notable changes to this project are written down here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.2.0] - 2026-10-01
|
|
10
|
+
|
|
11
|
+
Nothing that was exported has changed; the rules, the word lists, every deal and every saved game are exactly as they were.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`<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.
|
|
16
|
+
- **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.
|
|
17
|
+
- **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.
|
|
18
|
+
- **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).
|
|
19
|
+
- **README:** an Accessibility section, "The element", "The day's seed", and the list of all sixteen packages of the family.
|
|
20
|
+
- 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.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **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.
|
|
25
|
+
- **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.
|
|
26
|
+
- The README's pictures are taken again, with the Help switch in the header.
|
|
27
|
+
|
|
28
|
+
## [1.1.1] - 2026-10-01
|
|
29
|
+
|
|
30
|
+
Nothing that was exported has changed.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **An API reference page**, `api.html` on the demo site: every export of every entry point, with its signature and its doc comment, made from the source when the site is built so it cannot fall behind the code. The README and the demo's header link to it, and a test holds it to the source.
|
|
35
|
+
- **An Architecture section in the README**: how the source is split and what each file is for, held to the real files by a test.
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- The family's footer lists Jarajara.
|
|
40
|
+
|
|
9
41
|
## [1.1.0] - 2026-09-30
|
|
10
42
|
|
|
11
43
|
Nothing that was exported has changed, a seed deals the bag it always dealt,
|
|
@@ -94,7 +126,8 @@ The first release.
|
|
|
94
126
|
`@johnmorrisdotca/kumimoji/react`.
|
|
95
127
|
- A static demo for GitHub Pages.
|
|
96
128
|
|
|
97
|
-
[Unreleased]: https://github.com/johnmorrisdotca/kumimoji/compare/v1.1.
|
|
129
|
+
[Unreleased]: https://github.com/johnmorrisdotca/kumimoji/compare/v1.1.1...HEAD
|
|
130
|
+
[1.1.1]: https://github.com/johnmorrisdotca/kumimoji/compare/v1.1.0...v1.1.1
|
|
98
131
|
[1.1.0]: https://github.com/johnmorrisdotca/kumimoji/releases/tag/v1.1.0
|
|
99
132
|
[1.0.1]: https://github.com/johnmorrisdotca/kumimoji/releases/tag/v1.0.1
|
|
100
133
|
[1.0.0]: https://github.com/johnmorrisdotca/kumimoji/releases/tag/v1.0.0
|
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,16 @@ 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
|
+
|
|
71
82
|
## Releasing
|
|
72
83
|
|
|
73
84
|
Maintainers bump the version in `package.json` and `src/version.ts` and move
|
package/README.md
CHANGED
|
@@ -12,11 +12,11 @@ In English and in Japanese kana. The rules as pure, seeded TypeScript, two word
|
|
|
12
12
|
<img alt="TypeScript" src="https://img.shields.io/badge/types-TypeScript-3178c6">
|
|
13
13
|
</p>
|
|
14
14
|
|
|
15
|
-
<p align="center"><a href="https://johnmorrisdotca.github.io/kumimoji/"><strong>Play a game →</strong></a></p>
|
|
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,16 +257,77 @@ 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
|
|
|
271
|
+
## Architecture
|
|
272
|
+
|
|
273
|
+
The rules are plain functions over plain data with no DOM: a game is a value,
|
|
274
|
+
every move returns the next one, and the same seed deals the same bag in every
|
|
275
|
+
browser. Word lists are loaded only when a game opens, and the table is its own
|
|
276
|
+
entry (`/ui` for plain DOM, `/element` for a tag, `/react` for React), so a page that wants only the
|
|
277
|
+
rules carries none of it. A computer player is pure too, so its turn replays
|
|
278
|
+
exactly after a reload.
|
|
279
|
+
|
|
280
|
+
```text
|
|
281
|
+
src/
|
|
282
|
+
├── check.ts whether a finished grid completes a game: the one check a server also runs
|
|
283
|
+
├── computer.types.ts what a computer did in one step of its turn
|
|
284
|
+
├── computerPlay.ts the word a computer player lays: from its own hand, with no randomness
|
|
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
|
|
289
|
+
├── export.ts a game written out as JSON or plain text, and read back
|
|
290
|
+
├── generate.ts making a game from a seed: a crossword laid first, then its tiles become the bag
|
|
291
|
+
├── grid.ts the grid of tiles on a table with no edges, and how it is written as a string
|
|
292
|
+
├── help.ts arranging the hand into a word, for a player who asks for help
|
|
293
|
+
├── index.ts the main entry: the rules, the generator, the computer player and the table's pure helpers
|
|
294
|
+
├── judge.ts judging a grid against its language's word list, run by run
|
|
295
|
+
├── kana.ts Japanese tiles: the 45 base kana, and how every other kana is played as one of them
|
|
296
|
+
├── kumimoji.types.ts the game's types: how much of the set a bag holds, and the rest
|
|
297
|
+
├── party.ts pass and play: two to eight people round one device, one bag, a hand and a table each
|
|
298
|
+
├── party.types.ts what a pass-and-play game was set up as
|
|
299
|
+
├── partyKept.ts a pass-and-play game kept in the browser as one string of JSON
|
|
300
|
+
├── partySeats.ts people and computers joining and leaving a pass-and-play game between turns
|
|
301
|
+
├── partyTurns.ts how a pass-and-play turn ends, and how the game does, and who won
|
|
302
|
+
├── placement.ts where a word can cross a crossword without spoiling it
|
|
303
|
+
├── play.ts a game being played: the bag, the hand and the table, and every move on them
|
|
304
|
+
├── random.ts a seeded random, so one seed deals one game in every browser
|
|
305
|
+
├── react.tsx the "/react" entry: a grid to draw in React
|
|
306
|
+
├── showcase.ts what the rules page shows of the game, worked out from the game's own tables
|
|
307
|
+
├── strings.ts every word Kumimoji shows a person, in English and Japanese
|
|
308
|
+
├── tableView.ts how the table is looked at: which squares are shown, and how big a tile is drawn
|
|
309
|
+
├── tileFace.ts what a tile code shows, without loading a word list
|
|
310
|
+
├── tileFamily.ts which tile of the set a tile is, without loading a word list
|
|
311
|
+
├── tileWords.ts the word list, loaded once when a game opens
|
|
312
|
+
├── tileWordsModule.ts the same lists where there is no browser, imported only by a server or a test
|
|
313
|
+
├── tiles.constants.ts every number the game is played by: tile mixes, hand sizes, lengths of a game
|
|
314
|
+
├── turn.ts turning the table while every tile stays upright
|
|
315
|
+
├── ui.ts the "/ui" entry: Kumimoji played in the browser, in plain DOM
|
|
316
|
+
├── version.ts the version of this package, as package.json has it
|
|
317
|
+
├── wilds.ts a wild tile with no letter read as whatever makes the grid words
|
|
318
|
+
├── words.en.data.ts the English word list
|
|
319
|
+
├── words.ja.data.ts the Japanese word list and tile mix
|
|
320
|
+
├── words.ts the "/words" entry: the word lists, for a place with no browser, such as a server or a test
|
|
321
|
+
└── ui/ the table that draws and plays a game
|
|
322
|
+
├── board.ts how a tile on the table is drawn: sound, in a run that is not a word, or apart
|
|
323
|
+
├── mount.ts the table itself: mounting it on a page, the options it takes, and playing it from a keyboard
|
|
324
|
+
└── style.ts the table's own styles, every colour and size a CSS variable so a page can restyle it
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Tests sit beside the code they test (`*.test.ts`). `scripts/` builds the demo
|
|
328
|
+
and checks the package as npm packs it, `demo/` is the page published on
|
|
329
|
+
GitHub Pages, and `table/` taps it in real browsers.
|
|
330
|
+
|
|
253
331
|
## The name
|
|
254
332
|
|
|
255
333
|
*Kumimoji* (組み文字) is made of *kumi* (組み, from 組む, to put together or
|
|
@@ -278,25 +356,27 @@ issue and we will add you.
|
|
|
278
356
|
|
|
279
357
|
### The family
|
|
280
358
|
|
|
281
|
-
Kumimoji
|
|
359
|
+
Kumimoji is one of sixteen packages, each made for the same site, each MIT, each at
|
|
282
360
|
[github.com/johnmorrisdotca](https://github.com/johnmorrisdotca):
|
|
283
361
|
|
|
284
|
-
- [Korokoro](https://github.com/johnmorrisdotca/korokoro) (コロコロ, the sound
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
- [
|
|
288
|
-
|
|
289
|
-
- [
|
|
290
|
-
|
|
291
|
-
- [
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
- [
|
|
295
|
-
|
|
296
|
-
- [
|
|
297
|
-
|
|
298
|
-
- [
|
|
299
|
-
|
|
362
|
+
- [Korokoro](https://github.com/johnmorrisdotca/korokoro) (コロコロ, the sound of something small rolling): dice, with notation, exact odds and games.
|
|
363
|
+
- [Kyuubu](https://github.com/johnmorrisdotca/kyuubu) (キューブ, how Japanese says "cube"): a turning cube for the browser, 2×2 to 7×7.
|
|
364
|
+
- [Hitotsu](https://github.com/johnmorrisdotca/hitotsu) (一つ, "one"): a colour-card game, named for the call a player makes with one card left.
|
|
365
|
+
- [Toranpu](https://github.com/johnmorrisdotca/toranpu) (トランプ, the everyday Japanese word for a deck of playing cards): card games as pure rules.
|
|
366
|
+
- [Tane](https://github.com/johnmorrisdotca/tane) (種, a seed, the kind you plant): seeded random numbers and daily seeds.
|
|
367
|
+
- [Narabe](https://github.com/johnmorrisdotca/narabe) (並べ, "line them up"): a rules engine for gomoku, Reversi, Go, checkers and many more.
|
|
368
|
+
- [Tenka](https://github.com/johnmorrisdotca/tenka) (天下, "under heaven"): a world-conquest game for two to six.
|
|
369
|
+
- [Kumimoji](https://github.com/johnmorrisdotca/kumimoji) (組み文字, "letters put together"): a crossword tile race in English and Japanese.
|
|
370
|
+
- [Tsunagi](https://github.com/johnmorrisdotca/tsunagi) (繋ぎ, "joining"): a line-joining puzzle.
|
|
371
|
+
- [Jarajara](https://github.com/johnmorrisdotca/jarajara) (ジャラジャラ, the rattle of mahjong tiles being shuffled): mahjong tiles and a matching solitaire.
|
|
372
|
+
- [Suido](https://github.com/johnmorrisdotca/suido) (水道, "waterworks"): a pipe puzzle.
|
|
373
|
+
- [Domino](https://github.com/johnmorrisdotca/domino) (ドミノ, the Japanese word for dominoes): dominoes and Mexican Train.
|
|
374
|
+
- [Kotoba](https://github.com/johnmorrisdotca/kotoba) (言葉, "words"): word lists and word-game rules.
|
|
375
|
+
- [Sugoroku](https://github.com/johnmorrisdotca/sugoroku) (双六, backgammon's Japanese name): backgammon and its variants.
|
|
376
|
+
- [Kazu](https://github.com/johnmorrisdotca/kazu) (数, "number"): grid number puzzles, Sudoku and five more.
|
|
377
|
+
- [Meikyuu](https://github.com/johnmorrisdotca/meikyuu) (迷宮, "labyrinth"): mazes to draw a line through.
|
|
378
|
+
|
|
379
|
+
**This package is Kumimoji.** The demos of all sixteen share one header and footer, so each links the rest.
|
|
300
380
|
|
|
301
381
|
## Features
|
|
302
382
|
|
|
@@ -320,6 +400,12 @@ Kumimoji has siblings, each made for the same site, each MIT, each at
|
|
|
320
400
|
- **Help**, if chosen: the words the hand's own tiles spell.
|
|
321
401
|
- **A table to play on.** `mountKumimoji` plays a whole game alone in plain
|
|
322
402
|
DOM, saves it and loads it back; `KumimojiBoard` draws a grid in React.
|
|
403
|
+
- **A tag.** `<kumimoji-table>` is the same game in one element, with no
|
|
404
|
+
framework.
|
|
405
|
+
- **One bag a day.** `kumimojiDailySeed(new Date())` is the same seed for
|
|
406
|
+
everybody, worldwide.
|
|
407
|
+
- **Played from a keyboard.** Arrow keys between squares and tiles, Enter or
|
|
408
|
+
Space to pick up and lay; see [Accessibility](#accessibility).
|
|
323
409
|
- **The table's words in English and Japanese**, and any other language by a
|
|
324
410
|
table of your own. **Themeable**: every colour is a CSS variable.
|
|
325
411
|
|
|
@@ -428,6 +514,26 @@ The tiles are the 45 base hiragana. A word is spelt with the base of each of
|
|
|
428
514
|
its kana, so ことば is played, and listed, as ことは: a tile shows the other
|
|
429
515
|
forms it plays as in its corner (`CORNER_FORMS`, `formsOfTile`).
|
|
430
516
|
|
|
517
|
+
### The day's seed
|
|
518
|
+
|
|
519
|
+
| Export | What it does |
|
|
520
|
+
| --- | --- |
|
|
521
|
+
| `kumimojiDay(date)` | The day a moment falls on, in UTC, written `YYYY-MM-DD` |
|
|
522
|
+
| `kumimojiDailySeed(date)` | That day's seed: the date as a number, so 2026-10-01 is `20261001` |
|
|
523
|
+
|
|
524
|
+
```ts
|
|
525
|
+
import { kumimojiDailySeed } from "@johnmorrisdotca/kumimoji";
|
|
526
|
+
|
|
527
|
+
kumimojiDailySeed(new Date("2026-10-01T12:00:00Z")); // → 20261001
|
|
528
|
+
kumimojiDailySeed(new Date("2026-10-01T23:59:59Z")); // → 20261001
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
The day is the UTC date, so it changes at one moment for the whole world, and
|
|
532
|
+
everybody who deals from it with the same settings (language, hand, level and
|
|
533
|
+
length) gets the same bag in the same order. It is the same number as Tane's
|
|
534
|
+
`dailySeed`, so a page that uses both agrees; Kumimoji does not need Tane. The
|
|
535
|
+
race against the clock is each player's own.
|
|
536
|
+
|
|
431
537
|
### A table of players
|
|
432
538
|
|
|
433
539
|
```ts
|
|
@@ -495,7 +601,7 @@ The JSON, as `kumimojiToJSON` writes it:
|
|
|
495
601
|
{
|
|
496
602
|
"format": 1,
|
|
497
603
|
"game": "kumimoji",
|
|
498
|
-
"generator": "kumimoji 1.
|
|
604
|
+
"generator": "kumimoji 1.2.0",
|
|
499
605
|
"size": 3,
|
|
500
606
|
"level": "hard",
|
|
501
607
|
"seed": 44,
|
|
@@ -557,6 +663,11 @@ What that means for a project that uses this package:
|
|
|
557
663
|
This is a description, not legal advice; the licences themselves are what
|
|
558
664
|
hold.
|
|
559
665
|
|
|
666
|
+
A real word the table calls "not a word", or a word it accepts that it should
|
|
667
|
+
not: open a [*word list*](https://github.com/johnmorrisdotca/kumimoji/issues/new?template=word-list.md)
|
|
668
|
+
issue. The lists are not edited by hand, so a word may have to be changed at
|
|
669
|
+
its source.
|
|
670
|
+
|
|
560
671
|
### Loading them
|
|
561
672
|
|
|
562
673
|
In a browser, `loadTileWords(language)` fetches the list as its own module
|
|
@@ -586,6 +697,8 @@ turn tiles into words: `glyphOf`, `soundOf`, `wordOf`, `wildFor`, `isWild`,
|
|
|
586
697
|
|
|
587
698
|
## API
|
|
588
699
|
|
|
700
|
+
The [API reference](https://johnmorrisdotca.github.io/kumimoji/api.html) lists every export of every entry point with its signature and its doc comment. It is made from the source by `pnpm site`, so it cannot fall behind the code.
|
|
701
|
+
|
|
589
702
|
Every function, type and constant has a doc comment, so an editor shows this
|
|
590
703
|
as you type. The entries:
|
|
591
704
|
|
|
@@ -594,6 +707,8 @@ as you type. The entries:
|
|
|
594
707
|
| `@johnmorrisdotca/kumimoji` | Dealing, playing, judging, the table of players, the computer player, keeping and export, and the table's words |
|
|
595
708
|
| `@johnmorrisdotca/kumimoji/words` | Reads both word lists from this package's files, for a server, a test or a script: `loadTileWordsFromModule` |
|
|
596
709
|
| `@johnmorrisdotca/kumimoji/ui` | `mountKumimoji`, a whole game alone in plain DOM, and `boardModel` |
|
|
710
|
+
| `@johnmorrisdotca/kumimoji/element` | `KumimojiTable`, the `<kumimoji-table>` element's class, to extend or to define under another name |
|
|
711
|
+
| `@johnmorrisdotca/kumimoji/element/define` | Defines `<kumimoji-table>` on the page by being imported; exports nothing |
|
|
597
712
|
| `@johnmorrisdotca/kumimoji/react` | `KumimojiBoard` and `KumimojiTable` |
|
|
598
713
|
|
|
599
714
|
### Dealing and judging
|
|
@@ -712,6 +827,32 @@ entry has `boardModel(tiles, verdict, glyphOf?)`, which works out the squares
|
|
|
712
827
|
to draw and how each tile stands, with `BOARD_MARGIN` and `BOARD_LEAST`, and
|
|
713
828
|
`KUMIMOJI_STYLE`, the table's stylesheet as a string.
|
|
714
829
|
|
|
830
|
+
### The element
|
|
831
|
+
|
|
832
|
+
`<kumimoji-table>` is `mountKumimoji` as a tag: `@johnmorrisdotca/kumimoji/element/define`
|
|
833
|
+
defines it, and `@johnmorrisdotca/kumimoji/element` holds the class alone. Each
|
|
834
|
+
attribute is read again when it changes, and a change to any but `lang` deals a
|
|
835
|
+
new game. A table the rules do not offer (a hand of 5, say) draws nothing.
|
|
836
|
+
|
|
837
|
+
| Attribute | Default | What it does |
|
|
838
|
+
| --- | --- | --- |
|
|
839
|
+
| `language` | `english` | The tiles: `english` or `japanese` kana |
|
|
840
|
+
| `hand` | `7` | The opening hand: 3, 7 or 11 |
|
|
841
|
+
| `level` | `medium` | Wild tiles: `easy` the most, `medium` some, `hard` none |
|
|
842
|
+
| `length` | `short` | `short`, `medium` or `full`, all 144 tiles |
|
|
843
|
+
| `diagonals` | off | `on` to read every diagonal run of three or more too |
|
|
844
|
+
| `seed` | a new one each game | A whole number to deal from, or `daily` for the day's seed |
|
|
845
|
+
| `lang` | the page's | The language of the table's words: `en` or `ja` |
|
|
846
|
+
| `keep` | on | `off` leaves saving and loading out |
|
|
847
|
+
|
|
848
|
+
It fires `kumimoji-change` after every change to the game, with the game as it
|
|
849
|
+
would be saved as `event.detail.saved`, and `kumimoji-finish` when the last
|
|
850
|
+
tile is laid and every run is a word, with the deal, the play and the time it
|
|
851
|
+
took as `event.detail`; both bubble. It has `saved` (the game as it would be
|
|
852
|
+
saved), `table` (the handle `mountKumimoji` returns), `newGame(options?)` and
|
|
853
|
+
`setGame(saved)`. To style it, set the table's variables on `kumimoji-table
|
|
854
|
+
.km-root`: see [Theming](#theming).
|
|
855
|
+
|
|
715
856
|
### The React components
|
|
716
857
|
|
|
717
858
|
```jsx
|
|
@@ -749,10 +890,31 @@ export function Board() {
|
|
|
749
890
|
else for its `<div>`. Options are read when it mounts; give it a new `key` to
|
|
750
891
|
start over with different ones.
|
|
751
892
|
|
|
893
|
+
## Accessibility
|
|
894
|
+
|
|
895
|
+
- **A keyboard plays it.** Every square and every tile in the hand is a button
|
|
896
|
+
with a name. Tab lands on one square of the table and on one tile of the
|
|
897
|
+
hand, the arrow keys move between squares (and Left, Right, Home and End
|
|
898
|
+
between the hand's tiles), Enter or Space picks a tile up and lays it, and
|
|
899
|
+
Escape lets go of a tile you have picked up. The keyboard stays where it was
|
|
900
|
+
after each move, including on the buttons under the table, and the table's
|
|
901
|
+
description says so.
|
|
902
|
+
- **What is happening is said.** The line above the table, with the tiles left
|
|
903
|
+
and the time, and the line saying which runs are not words, are polite live
|
|
904
|
+
regions, and so is the note after saving or loading.
|
|
905
|
+
- **Targets are big enough.** Every button is at least 44 pixels each way,
|
|
906
|
+
which a test holds at a phone's width.
|
|
907
|
+
- **Not by colour alone.** A run that is not a word is ringed and listed in
|
|
908
|
+
words; a tile apart from the crossword is faded and said so.
|
|
909
|
+
- **Motion.** There is none to reduce: the table has no animation.
|
|
910
|
+
- **Not yet.** Letters cannot be typed to lay a tile, and a screen reader hears
|
|
911
|
+
the table as a grid of squares, not as the words they make.
|
|
912
|
+
|
|
752
913
|
## Theming
|
|
753
914
|
|
|
754
|
-
Every colour is a CSS variable on `.km-root`. Set them in your stylesheet
|
|
755
|
-
|
|
915
|
+
Every colour is a CSS variable on `.km-root`. Set them in your stylesheet on
|
|
916
|
+
`.km-root` with a selector more specific than the table's own, such as
|
|
917
|
+
`#table .km-root`, or pass them as `theme`, which sets them
|
|
756
918
|
on the table itself and so wins in light and dark alike.
|
|
757
919
|
|
|
758
920
|
| Variable | What it sets | Light | Dark |
|
|
@@ -827,7 +989,7 @@ that language, whose licence lets its words be shipped.
|
|
|
827
989
|
Any current browser: Chrome, Edge, Firefox and Safari, on a desk or a phone.
|
|
828
990
|
It needs ES2020 and dynamic `import()`. The table is tested in Chromium and in
|
|
829
991
|
WebKit, Safari's engine, at phone size with touch and on a desktop. The rules
|
|
830
|
-
run in Node
|
|
992
|
+
run in Node 22 and later, Deno, Bun and web workers, and load by `import` and
|
|
831
993
|
by `require()`.
|
|
832
994
|
|
|
833
995
|
## The command line
|
|
@@ -848,12 +1010,12 @@ words.allowed.has("snowy"); // → true
|
|
|
848
1010
|
## Roadmap
|
|
849
1011
|
|
|
850
1012
|
- More languages for the tiles, each from a real dictionary of that language.
|
|
851
|
-
-
|
|
852
|
-
|
|
1013
|
+
- Typing a letter to lay that tile, in the plain-DOM table, which is
|
|
1014
|
+
otherwise played from a keyboard.
|
|
853
1015
|
- A table of players in plain DOM, beside the game alone.
|
|
854
1016
|
- A computer player at three strengths.
|
|
855
1017
|
- Word definitions for a finished crossword.
|
|
856
|
-
- A Vue wrapper
|
|
1018
|
+
- A Vue wrapper, beside the React one and the tag.
|
|
857
1019
|
|
|
858
1020
|
Left out on purpose: play between devices, which needs a server (a table is
|
|
859
1021
|
kept as text made to be sent, and Itsutsu sends it; the sending is yours); a
|
|
@@ -871,9 +1033,10 @@ pnpm install
|
|
|
871
1033
|
pnpm check # lint, types and tests
|
|
872
1034
|
pnpm test:table # the demo in real browsers, by taps
|
|
873
1035
|
pnpm site # build the demo into ./site, then serve it
|
|
1036
|
+
pnpm pictures # take the README's two pictures from the built demo
|
|
874
1037
|
```
|
|
875
1038
|
|
|
876
|
-
Please follow the [code of conduct](./CODE_OF_CONDUCT.md).
|
|
1039
|
+
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.
|
|
877
1040
|
|
|
878
1041
|
## Changes
|
|
879
1042
|
|
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.0";
|
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.0";
|
package/package.json
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnmorrisdotca/kumimoji",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Crossword tile race: a word game engine where 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, word checking, wild tiles, a computer player, and a playable table for React, Vue, Svelte, Angular or plain HTML. 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",
|
|
@@ -102,6 +104,14 @@
|
|
|
102
104
|
"./react": {
|
|
103
105
|
"types": "./dist/react.d.ts",
|
|
104
106
|
"default": "./dist/react.js"
|
|
107
|
+
},
|
|
108
|
+
"./element": {
|
|
109
|
+
"types": "./dist/element.d.ts",
|
|
110
|
+
"default": "./dist/element.js"
|
|
111
|
+
},
|
|
112
|
+
"./element/define": {
|
|
113
|
+
"types": "./dist/element-define.d.ts",
|
|
114
|
+
"default": "./dist/element-define.js"
|
|
105
115
|
}
|
|
106
116
|
},
|
|
107
117
|
"files": [
|
|
@@ -114,10 +124,11 @@
|
|
|
114
124
|
],
|
|
115
125
|
"sideEffects": [
|
|
116
126
|
"./dist/words.js",
|
|
117
|
-
"./dist/tileWordsModule.js"
|
|
127
|
+
"./dist/tileWordsModule.js",
|
|
128
|
+
"./dist/element-define.js"
|
|
118
129
|
],
|
|
119
130
|
"engines": {
|
|
120
|
-
"node": ">=
|
|
131
|
+
"node": ">=22"
|
|
121
132
|
},
|
|
122
133
|
"scripts": {
|
|
123
134
|
"build": "tsc -p tsconfig.json",
|
|
@@ -129,6 +140,7 @@
|
|
|
129
140
|
"test:frameworks": "pnpm build && node scripts/check-frameworks.mjs",
|
|
130
141
|
"check": "pnpm lint && pnpm typecheck && pnpm test",
|
|
131
142
|
"site": "pnpm build && node scripts/site.mjs",
|
|
143
|
+
"pictures": "pnpm site && node scripts/readme-pictures.mjs",
|
|
132
144
|
"docs:make": "node scripts/docs.mjs",
|
|
133
145
|
"prepublishOnly": "pnpm check && pnpm build"
|
|
134
146
|
},
|