domma-cms 1.8.32 → 1.8.33
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 +5 -0
- package/docs/DommaFeatures.md +13 -1
- package/package.json +1 -1
- package/plugins/_lib/admin/games/games.css +1 -1
- package/plugins/_lib/admin/games/kit.js +329 -124
- package/plugins/_lib/games/http.js +11 -1
- package/plugins/_lib/games/plays.js +28 -7
- package/plugins/crossword/CLAUDE.md +37 -0
- package/plugins/crossword/admin/css/index.css +18 -1
- package/plugins/crossword/admin/views/crossword.js +337 -205
- package/plugins/crossword/docs/guide.md +53 -0
- package/plugins/crossword/inject/body.html +1 -1
- package/plugins/crossword/plugin.js +38 -5
- package/plugins/crossword/plugin.json +4 -3
- package/plugins/crossword/public/crossword.css +36 -1
- package/plugins/crossword/public/crossword.js +468 -6
- package/plugins/free-tier.lock.json +43 -1
- package/plugins/quiz/CLAUDE.md +43 -0
- package/plugins/quiz/admin/css/index.css +13 -1
- package/plugins/quiz/admin/views/quiz.js +423 -245
- package/plugins/quiz/docs/guide.md +61 -0
- package/plugins/quiz/inject/body.html +1 -1
- package/plugins/quiz/plugin.js +39 -5
- package/plugins/quiz/plugin.json +4 -3
- package/plugins/quiz/public/quiz.css +36 -1
- package/plugins/quiz/public/quiz.js +298 -6
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
* Writes go one at a time (`withLock`), so two answers sent at once cannot
|
|
13
13
|
* both be marked.
|
|
14
14
|
*
|
|
15
|
+
* A TEST play (`test: true`) is an admin trying the game from its Tool. It
|
|
16
|
+
* plays exactly like any other, but it is nobody's go: it never counts for a
|
|
17
|
+
* board, a place, the goes-per-name limit, the badge or the admin's list of
|
|
18
|
+
* plays, and the next test of the same game clears it away.
|
|
19
|
+
*
|
|
15
20
|
* @module _lib/games/plays
|
|
16
21
|
*/
|
|
17
22
|
|
|
@@ -53,7 +58,13 @@ export class PlayStore {
|
|
|
53
58
|
|
|
54
59
|
forget() { this.cache = null; }
|
|
55
60
|
|
|
61
|
+
/** The real plays of a game - test plays left out. */
|
|
56
62
|
async forGame(gameId) {
|
|
63
|
+
return (await this.all()).filter(p => p.gameId === gameId && p.test !== true);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Every play of a game, tests included. */
|
|
67
|
+
async everyForGame(gameId) {
|
|
57
68
|
return (await this.all()).filter(p => p.gameId === gameId);
|
|
58
69
|
}
|
|
59
70
|
|
|
@@ -81,17 +92,18 @@ export class PlayStore {
|
|
|
81
92
|
}
|
|
82
93
|
|
|
83
94
|
/**
|
|
84
|
-
* @param {{gameId: string, name: string, maxScore?: number, state?: object, userId?: string}} play
|
|
95
|
+
* @param {{gameId: string, name: string, maxScore?: number, state?: object, userId?: string, test?: boolean}} play
|
|
96
|
+
* `test`: an admin trying the game - see the module comment
|
|
85
97
|
* @returns {Promise<object>} the play, token included - for the player, once
|
|
86
98
|
*/
|
|
87
|
-
async start({gameId, name, maxScore = 0, state = {}, userId = ''}) {
|
|
99
|
+
async start({gameId, name, maxScore = 0, state = {}, userId = '', test = false}) {
|
|
88
100
|
return this.withLock(async () => {
|
|
89
101
|
const now = new Date().toISOString();
|
|
90
102
|
const saved = await createEntry(this.slug, {
|
|
91
103
|
gameId, name, nameKey: nameKey(name), userId,
|
|
92
104
|
status: 'playing', startedAt: now, finishedAt: '', timeMs: 0, score: 0, maxScore,
|
|
93
|
-
hidden: false, token: randomBytes(24).toString('base64url'), state
|
|
94
|
-
}, {source: 'public'});
|
|
105
|
+
hidden: false, ...(test ? {test: true} : {}), token: randomBytes(24).toString('base64url'), state
|
|
106
|
+
}, {source: test ? 'admin' : 'public'});
|
|
95
107
|
this.forget();
|
|
96
108
|
return flat(saved);
|
|
97
109
|
});
|
|
@@ -129,12 +141,21 @@ export class PlayStore {
|
|
|
129
141
|
this.forget();
|
|
130
142
|
}
|
|
131
143
|
|
|
132
|
-
/** Every play of a game gone: a fresh board. */
|
|
144
|
+
/** Every play of a game gone: a fresh board. Answers how many real plays went. */
|
|
133
145
|
async reset(gameId) {
|
|
134
|
-
const mine = await this.
|
|
146
|
+
const mine = await this.everyForGame(gameId);
|
|
135
147
|
await this.withLock(async () => { for (const p of mine) await deleteEntry(this.slug, p.id).catch(() => {}); });
|
|
136
148
|
this.forget();
|
|
137
|
-
return mine.length;
|
|
149
|
+
return mine.filter(p => p.test !== true).length;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** A game's test plays gone (before the next test starts). */
|
|
153
|
+
async clearTests(gameId) {
|
|
154
|
+
const tests = (await this.everyForGame(gameId)).filter(p => p.test === true);
|
|
155
|
+
if (!tests.length) return 0;
|
|
156
|
+
await this.withLock(async () => { for (const p of tests) await deleteEntry(this.slug, p.id).catch(() => {}); });
|
|
157
|
+
this.forget();
|
|
158
|
+
return tests.length;
|
|
138
159
|
}
|
|
139
160
|
|
|
140
161
|
async board(gameId, {period = 'all', limit = 10} = {}) {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# crossword - AI Assistant Guide
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Crosswords on the public site, built from a word + clue list, timed by the server, with a fastest-solver
|
|
6
|
+
leaderboard. 1.1.0 (2026-10-21), `tier: free`, disabled on arrival. Sibling of `quiz`; shares `plugins/_lib/games`.
|
|
7
|
+
|
|
8
|
+
## Files
|
|
9
|
+
|
|
10
|
+
- `public/lib/layout.js` - PURE, isomorphic, SERVED: `generate(words, {seed})` (greedy, deterministic from the
|
|
11
|
+
seed, clues never affect the layout), `cleanAnswer` (letters + enumeration), `solutionGrid`, `shapeOf`. The admin
|
|
12
|
+
builder imports it over HTTP for the live preview; the server runs the same function on save, so a layout is
|
|
13
|
+
never taken from the browser. It holds no puzzle, which is why it may live under `public/`.
|
|
14
|
+
- `lib/crossword.js` - PURE: `cleanPuzzle`, `isOpen`, `publicPuzzle` (shape + clues, no letters), `readCells`,
|
|
15
|
+
`compare`, `catchUp`, `finish`, `view`.
|
|
16
|
+
- `lib/store.js` - `crossword-puzzles` + `plays = new PlayStore('crossword-plays', {order: 'time'})`.
|
|
17
|
+
- `plugin.js` - routes (header comment), shortcodes, the `fastest` notification source.
|
|
18
|
+
- `public/crossword.js` - the player: the screens are domma-reactive; the grid is a `Board` class over real
|
|
19
|
+
`<input>`s (keyboard rules in its header - all focus and keystrokes, which bindings do not reach), letters
|
|
20
|
+
remembered per play in storage. Exports `mount(el, {test})`, which the admin's Test screen uses.
|
|
21
|
+
- `admin/views/crossword.js` - list, editor with live grid, test and plays screens, all domma-reactive through the
|
|
22
|
+
admin kit (`_lib/admin/games/kit.js`). The preview grid is a data-each over cells.
|
|
23
|
+
|
|
24
|
+
## Contracts
|
|
25
|
+
|
|
26
|
+
- **No letters before solving.** `/public/crossword/:slug` gives only the size; the clues come with the play (so
|
|
27
|
+
nobody studies them off the clock); `solution` is in `/result` only when `status === 'finished'`. The tests
|
|
28
|
+
assert no answer string appears in the play payload - keep that test.
|
|
29
|
+
- **Time = server elapsed + `state.penaltyMs`.** Check adds `checkPenalty` seconds (403 when `checks` is off); a
|
|
30
|
+
FULL but wrong submit adds `wrongPenalty`; a grid with empties only reports the count.
|
|
31
|
+
- Cells travel as `height` strings of `width` chars, `A-Z` or space; anything else is 400.
|
|
32
|
+
- The grid uses fixed ink/paper colours (`--cw-ink`, `--cw-paper`) whatever the theme - a themed grid inverted
|
|
33
|
+
under dark themes.
|
|
34
|
+
- Same shortcode prefix trap as quiz: `crossword-leaderboard` is registered with `{priority: 5}`.
|
|
35
|
+
- **Test plays** (1.1.0): `GET/POST /puzzles/:id/test` (manage), as in quiz's CLAUDE.md - the saved crossword
|
|
36
|
+
whatever its status, never on a board, not in the plays list, no notification, cleared by the next test.
|
|
37
|
+
`/result` answers `test: true` and `wouldPlace`.
|
|
@@ -1 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
/* Crossword admin - the word list and the live grid. Shared pieces are in /plugins/_lib/admin/games/games.css. */
|
|
2
|
+
.cwa-words { display: flex; flex-direction: column; gap: .4rem; }
|
|
3
|
+
.cwa-word { display: grid; grid-template-columns: 1.6rem minmax(7rem, 12rem) 3.5rem minmax(0, 1fr) auto; gap: .4rem; align-items: center; }
|
|
4
|
+
.cwa-n { color: var(--dm-text-muted); font-size: var(--dm-text-sm); text-align: right; }
|
|
5
|
+
.cwa-answer { text-transform: uppercase; font-family: var(--dm-font-mono, monospace); letter-spacing: .04em; }
|
|
6
|
+
.cwa-enum { color: var(--dm-text-muted); font-size: var(--dm-text-xs); white-space: nowrap; }
|
|
7
|
+
@media (max-width: 700px) { .cwa-word { grid-template-columns: 1.4rem 1fr auto; } .cwa-enum { display: none; } .cwa-clue { grid-column: 2 / 4; } }
|
|
8
|
+
|
|
9
|
+
.cwa-side { position: sticky; top: .5rem; }
|
|
10
|
+
.cwa-preview { overflow: auto; max-height: 70vh; }
|
|
11
|
+
/* Fixed ink and paper, as on the public grid: a themed grid came out inverted under dark themes. */
|
|
12
|
+
.cwa-grid { --cwa-ink: #111827; --cwa-paper: #ffffff; display: grid; gap: 0; width: max-content; border: 2px solid var(--cwa-ink); background: var(--cwa-ink); }
|
|
13
|
+
.cwa-cell { position: relative; width: 1.7rem; height: 1.7rem; display: grid; place-items: center; font-weight: 700; font-size: .85rem;
|
|
14
|
+
background: var(--cwa-paper); color: var(--cwa-ink); border: 1px solid color-mix(in srgb, var(--cwa-ink) 35%, transparent); box-sizing: border-box; }
|
|
15
|
+
.cwa-cell.is-block { background: var(--cwa-ink); border-color: var(--cwa-ink); }
|
|
16
|
+
.cwa-num { position: absolute; top: 1px; left: 2px; font-size: .5rem; font-weight: 600; line-height: 1; color: #4b5563; }
|
|
17
|
+
.cwa-unplaced { margin: 0; padding: .5rem .75rem; border-radius: 6px; font-size: var(--dm-text-sm);
|
|
18
|
+
background: color-mix(in srgb, var(--dm-warning) 15%, transparent); color: color-mix(in srgb, var(--dm-warning) 55%, var(--dm-text)); }
|