chesspal 0.2.3 → 0.3.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.
Files changed (214) hide show
  1. package/README.md +11 -12
  2. package/dist/cli/cli.js +83 -3
  3. package/dist/cli/command-line.js +26 -9
  4. package/dist/cli/icon.d.ts +25 -0
  5. package/dist/cli/icon.js +80 -0
  6. package/dist/commands/agents.d.ts +19 -2
  7. package/dist/commands/agents.js +41 -14
  8. package/dist/commands/ai-say.d.ts +2 -1
  9. package/dist/commands/ai-say.js +3 -4
  10. package/dist/commands/analyse-game.d.ts +6 -0
  11. package/dist/commands/analyse-game.js +51 -15
  12. package/dist/commands/annotate-game.d.ts +4 -0
  13. package/dist/commands/annotate-game.js +60 -20
  14. package/dist/commands/answer-lines.d.ts +10 -4
  15. package/dist/commands/answer-lines.js +30 -4
  16. package/dist/commands/coach.js +6 -4
  17. package/dist/commands/context.d.ts +11 -1
  18. package/dist/commands/context.js +5 -0
  19. package/dist/commands/delete-review.js +9 -4
  20. package/dist/commands/game-summary.d.ts +9 -2
  21. package/dist/commands/game-summary.js +11 -0
  22. package/dist/commands/index.d.ts +4 -3
  23. package/dist/commands/index.js +8 -5
  24. package/dist/commands/init.js +2 -4
  25. package/dist/commands/instructions.d.ts +9 -0
  26. package/dist/commands/instructions.js +30 -0
  27. package/dist/commands/lead-input.d.ts +1 -1
  28. package/dist/commands/lead-input.js +7 -4
  29. package/dist/commands/look.d.ts +9 -5
  30. package/dist/commands/look.js +19 -9
  31. package/dist/commands/page-link.js +6 -5
  32. package/dist/commands/past-games.d.ts +1 -1
  33. package/dist/commands/past-games.js +12 -5
  34. package/dist/commands/player-move.js +2 -3
  35. package/dist/commands/ponder.js +1 -1
  36. package/dist/commands/position.js +2 -2
  37. package/dist/commands/profiles.d.ts +23 -9
  38. package/dist/commands/profiles.js +76 -35
  39. package/dist/commands/ratings.d.ts +8 -0
  40. package/dist/commands/ratings.js +45 -0
  41. package/dist/commands/registry.d.ts +1 -1
  42. package/dist/commands/registry.js +3 -2
  43. package/dist/commands/say.d.ts +11 -6
  44. package/dist/commands/say.js +21 -13
  45. package/dist/commands/status.d.ts +6 -2
  46. package/dist/commands/status.js +70 -26
  47. package/dist/commands/study.d.ts +11 -1
  48. package/dist/commands/study.js +39 -7
  49. package/dist/commands/view.d.ts +17 -5
  50. package/dist/commands/view.js +67 -10
  51. package/dist/core/facts.d.ts +14 -2
  52. package/dist/core/facts.js +32 -7
  53. package/dist/core/game.d.ts +5 -0
  54. package/dist/core/game.js +9 -0
  55. package/dist/core/index.d.ts +2 -1
  56. package/dist/core/index.js +1 -1
  57. package/dist/core/placement.d.ts +63 -0
  58. package/dist/core/placement.js +259 -0
  59. package/dist/core/possibilities.d.ts +38 -0
  60. package/dist/core/possibilities.js +254 -0
  61. package/dist/core/review.d.ts +6 -1
  62. package/dist/core/review.js +21 -6
  63. package/dist/games/ai-model.d.ts +4 -2
  64. package/dist/games/ai-model.js +10 -1
  65. package/dist/games/analysis.d.ts +31 -4
  66. package/dist/games/analysis.js +44 -5
  67. package/dist/games/archive.d.ts +6 -3
  68. package/dist/games/archive.js +13 -5
  69. package/dist/games/current-game.d.ts +26 -2
  70. package/dist/games/current-game.js +57 -21
  71. package/dist/games/files.d.ts +7 -0
  72. package/dist/games/files.js +48 -1
  73. package/dist/games/index.d.ts +4 -4
  74. package/dist/games/index.js +4 -4
  75. package/dist/games/table-talk.d.ts +2 -0
  76. package/dist/games/table-talk.js +5 -1
  77. package/dist/games/talk.d.ts +4 -3
  78. package/dist/games/talk.js +3 -3
  79. package/dist/home/data-folder.d.ts +2 -0
  80. package/dist/home/data-folder.js +5 -1
  81. package/dist/home/index.d.ts +1 -1
  82. package/dist/home/index.js +1 -1
  83. package/dist/host-files/agents.d.ts +2 -2
  84. package/dist/host-files/agents.js +12 -6
  85. package/dist/host-files/host-files.d.ts +5 -3
  86. package/dist/host-files/host-files.js +15 -8
  87. package/dist/host-files/index.d.ts +2 -2
  88. package/dist/host-files/index.js +2 -2
  89. package/dist/page/app/app.js +59 -22
  90. package/dist/page/app/keys.js +16 -2
  91. package/dist/page/app/served.js +2 -2
  92. package/dist/page/app/title.js +17 -0
  93. package/dist/page/avatars/torretta.js +4 -4
  94. package/dist/page/board/board.js +2 -1
  95. package/dist/page/board/marks.js +4 -24
  96. package/dist/page/board/signs.js +23 -0
  97. package/dist/page/dialogs/analyses.js +4 -4
  98. package/dist/page/dialogs/dialog.js +4 -1
  99. package/dist/page/dialogs/dialogs.css +84 -10
  100. package/dist/page/dialogs/profiles.js +95 -42
  101. package/dist/page/game/column.js +6 -2
  102. package/dist/page/game/game.css +12 -0
  103. package/dist/page/game/move-list.js +2 -2
  104. package/dist/page/game/plies.js +11 -0
  105. package/dist/page/guide/blocks.js +67 -0
  106. package/dist/page/guide/en.md +66 -0
  107. package/dist/page/guide/foot.js +18 -0
  108. package/dist/page/guide/guide.css +200 -0
  109. package/dist/page/guide/guide.js +89 -0
  110. package/dist/page/guide/it.md +66 -0
  111. package/dist/page/guide/markdown.js +67 -0
  112. package/dist/page/helps/helps.css +10 -11
  113. package/dist/page/helps/helps.js +15 -15
  114. package/dist/page/layout/favicon.svg +13 -0
  115. package/dist/page/layout/header.js +3 -3
  116. package/dist/page/layout/icon.icns +0 -0
  117. package/dist/page/layout/icon.ico +0 -0
  118. package/dist/page/layout/icon.png +0 -0
  119. package/dist/page/layout/layout.css +140 -9
  120. package/dist/page/layout/window.js +45 -0
  121. package/dist/page/legend/legend.css +203 -0
  122. package/dist/page/legend/legend.js +169 -0
  123. package/dist/page/legend/view.js +54 -0
  124. package/dist/page/main.js +4 -1
  125. package/dist/page/players/card.js +1 -1
  126. package/dist/page/profile/profile.js +14 -9
  127. package/dist/page/profile/settings.js +9 -0
  128. package/dist/page/style.css +4 -0
  129. package/dist/page/table/opener.css +65 -0
  130. package/dist/page/table/opener.js +59 -0
  131. package/dist/page/talk/chat.css +97 -0
  132. package/dist/page/talk/talk.css +38 -26
  133. package/dist/page/talk/talk.js +31 -7
  134. package/dist/page/text.js +120 -18
  135. package/dist/page/theme.css +7 -0
  136. package/dist/profiles/index.d.ts +2 -1
  137. package/dist/profiles/index.js +2 -1
  138. package/dist/profiles/profiles.d.ts +10 -15
  139. package/dist/profiles/profiles.js +30 -15
  140. package/dist/profiles/ratings.d.ts +35 -0
  141. package/dist/profiles/ratings.js +61 -0
  142. package/dist/server/bell.d.ts +24 -3
  143. package/dist/server/bell.js +84 -10
  144. package/dist/server/caller.d.ts +2 -1
  145. package/dist/server/caller.js +3 -2
  146. package/dist/server/mcp.d.ts +7 -3
  147. package/dist/server/mcp.js +106 -56
  148. package/dist/server/page/analyses.d.ts +2 -0
  149. package/dist/server/page/analyses.js +20 -5
  150. package/dist/server/page/attach.d.ts +4 -0
  151. package/dist/server/page/attach.js +112 -0
  152. package/dist/server/page/chat.d.ts +60 -0
  153. package/dist/server/page/chat.js +79 -0
  154. package/dist/server/page/events.d.ts +7 -0
  155. package/dist/server/page/events.js +20 -0
  156. package/dist/server/page/files.js +10 -1
  157. package/dist/server/page/game.js +62 -29
  158. package/dist/server/page/page.d.ts +47 -7
  159. package/dist/server/page/page.js +74 -51
  160. package/dist/server/page/profiles.d.ts +5 -0
  161. package/dist/server/page/profiles.js +26 -10
  162. package/dist/server/page/table.d.ts +39 -0
  163. package/dist/server/page/table.js +102 -0
  164. package/dist/server/table/browser.d.ts +6 -0
  165. package/dist/server/table/browser.js +48 -0
  166. package/dist/server/table/hosts/chats.d.ts +7 -0
  167. package/dist/server/table/hosts/chats.js +31 -0
  168. package/dist/server/table/hosts/find.d.ts +2 -0
  169. package/dist/server/table/hosts/find.js +20 -0
  170. package/dist/server/table/hosts/hosts.d.ts +34 -0
  171. package/dist/server/table/hosts/hosts.js +49 -0
  172. package/dist/server/table/hosts/index.d.ts +3 -0
  173. package/dist/server/table/hosts/index.js +4 -0
  174. package/dist/server/table/hosts/play.d.ts +25 -0
  175. package/dist/server/table/hosts/play.js +152 -0
  176. package/dist/server/table/hosts/seat.d.ts +25 -0
  177. package/dist/server/table/hosts/seat.js +74 -0
  178. package/dist/server/table/hosts/terminal.d.ts +6 -0
  179. package/dist/server/table/hosts/terminal.js +63 -0
  180. package/dist/server/table/icon/index.d.ts +32 -0
  181. package/dist/server/table/icon/index.js +83 -0
  182. package/dist/server/table/icon/linux.d.ts +5 -0
  183. package/dist/server/table/icon/linux.js +47 -0
  184. package/dist/server/table/icon/macos.d.ts +3 -0
  185. package/dist/server/table/icon/macos.js +38 -0
  186. package/dist/server/table/icon/windows.d.ts +3 -0
  187. package/dist/server/table/icon/windows.js +48 -0
  188. package/dist/server/table/index.d.ts +6 -0
  189. package/dist/server/table/index.js +7 -0
  190. package/dist/server/table/launch.d.ts +19 -0
  191. package/dist/server/table/launch.js +78 -0
  192. package/dist/server/table/link.d.ts +10 -0
  193. package/dist/server/table/link.js +230 -0
  194. package/dist/server/table/table.d.ts +35 -0
  195. package/dist/server/table/table.js +94 -0
  196. package/dist/sites/chesscom.d.ts +4 -0
  197. package/dist/sites/chesscom.js +25 -0
  198. package/dist/sites/choice.d.ts +17 -0
  199. package/dist/sites/choice.js +13 -0
  200. package/dist/sites/index.d.ts +2 -0
  201. package/dist/sites/index.js +4 -0
  202. package/dist/sites/lichess.d.ts +4 -0
  203. package/dist/sites/lichess.js +32 -0
  204. package/dist/sites/request.d.ts +11 -0
  205. package/dist/sites/request.js +15 -0
  206. package/dist/sites/sites.d.ts +6 -0
  207. package/dist/sites/sites.js +33 -0
  208. package/instructions/analysis.md +1 -1
  209. package/instructions/analyst-agent.md +1 -1
  210. package/instructions/coach.md +2 -1
  211. package/instructions/opponent.md +6 -6
  212. package/instructions/ponder.md +1 -0
  213. package/instructions/start.md +1 -0
  214. package/package.json +1 -1
@@ -6,9 +6,10 @@ import { Thinker, Thought } from "../avatars/thought.js";
6
6
  import { Torretta } from "../avatars/torretta.js";
7
7
  import { Board } from "../board/board.js";
8
8
  import { explain } from "../board/explain.js";
9
+ import { kingSigns } from "../board/signs.js";
9
10
  import { piecesOf } from "../board/squares.js";
10
11
  import { NewGameDialog } from "../dialogs/new-game.js";
11
- import { NameButton, PictureButton, PictureDialog, ProfilesDialog, Wizard } from "../dialogs/profiles.js";
12
+ import { NameButton, PictureButton, ProfileDialog, ProfilesDialog, Wizard } from "../dialogs/profiles.js";
12
13
  import { SettingsPanel } from "../dialogs/settings.js";
13
14
  import { playerIn, studiedSides } from "../dialogs/studied.js";
14
15
  import { StudyDialog } from "../dialogs/study.js";
@@ -18,25 +19,30 @@ import { GameColumn } from "../game/column.js";
18
19
  import { howEnded, useDeclined, useJustEnded, useSawEnd } from "../game/ending.js";
19
20
  import { Steps } from "../game/move-list.js";
20
21
  import { useOpeningBook } from "../game/opening-book.js";
21
- import { aiSpoke, aiWords, judgmentsOf, playerSpent, playerWords, pliesOf, withBook, withJudgments, withoutReviews } from "../game/plies.js";
22
+ import { aiSpoke, aiWords, analysisTalk, judgmentsOf, playerSpent, playerWords, pliesOf, withBook, withJudgments, withoutReviews } from "../game/plies.js";
23
+ import { GuideDialog } from "../guide/guide.js";
22
24
  import { BareButton, HelpsCard, HintLine, MapButton } from "../helps/helps.js";
23
25
  import { helpsOf } from "../helps/view.js";
24
26
  import { FoldTab } from "../layout/fold-tab.js";
25
27
  import { nextFold, rememberFold, startFolds } from "../layout/folds.js";
26
28
  import { Header } from "../layout/header.js";
27
29
  import { standingsOf } from "../lead/view.js";
30
+ import { MarksLegend } from "../legend/legend.js";
31
+ import { legendOf } from "../legend/view.js";
28
32
  import { ChosenSet } from "../pieces/pieces.js";
29
33
  import { ClockFace, PlayerCard } from "../players/card.js";
30
34
  import { signature } from "../players/model-name.js";
31
35
  import { keepSettings } from "../profile/profile.js";
32
36
  import { languagesOf, saveSettings, settingsFrom, showSettings } from "../profile/settings.js";
33
37
  import { endSoundOf, playSound, soundOf, wakeSounds } from "../sounds/sounds.js";
38
+ import { ChatOpener } from "../table/opener.js";
34
39
  import { AiNote, AiWords, Compose, PointSquare, TalkFaces } from "../talk/talk.js";
35
40
  import { textFor } from "../text.js";
36
41
  import { anchorName, useAnchor } from "./anchor.js";
37
- import { useBrowseKeys, useFoldKeys, useMapKey, useWheelSteps } from "./keys.js";
42
+ import { useBrowseKeys, useFoldKeys, useLegendKey, useMapKey, useWheelSteps } from "./keys.js";
38
43
  import { phaseOf, verdictOf } from "./phase.js";
39
44
  import { post, sendMessage, sendMove, useServed } from "./served.js";
45
+ import { tabTitle, useTabTitle } from "./title.js";
40
46
  /**
41
47
  * The page: the board of the game in the player's folder, where the player moves on their turn, the talk with the AI beside it, and the game's column; in the
42
48
  * settings the player chose, `start` as this browser remembered them (D-56).
@@ -135,6 +141,9 @@ export function App({ start }) {
135
141
  const [mapping, setMapping] = useState(false);
136
142
  // The board without its drawings while their button is held (Stefano).
137
143
  const [bare, setBare] = useState(false);
144
+ // The legend of the marks on the board, open until the player closes it (D-99).
145
+ const [legendOpen, setLegendOpen] = useState(false);
146
+ useLegendKey(() => setLegendOpen((open) => !open));
138
147
  const past = useMemo(() => viewing === undefined ? undefined : new Game({ tags: live.record.tags, moves: live.record.moves.slice(0, viewing) }), [live, viewing]);
139
148
  const next = useMemo(() => viewing === undefined ? undefined : new Game({ tags: live.record.tags, moves: live.record.moves.slice(0, viewing + 1) }).lastMove, [live, viewing]);
140
149
  const shown = useMemo(() => past ?? (pending === undefined ? live : withMove(live, pending)), [live, pending, past]);
@@ -196,6 +205,9 @@ export function App({ start }) {
196
205
  // game a link opens is there to look at: the analyst starts on the player's yes alone (D-89; Zoe and Federico).
197
206
  const playedName = game === undefined || game === null ? undefined : anchorName(game.name);
198
207
  useAnchor({ ready: served !== undefined, shown: studied !== undefined ? anchorName(studied.name) : idle ? undefined : playedName, played: playedName, going }, viewing, (plies) => goTo(plies ?? total), async (wanted) => await post("/api/study", { game: wanted, look: true }, text, name) === undefined);
208
+ // The tab names the game and the move shown, as the cards name its players; between games, Chesspal alone.
209
+ const titled = idle || served === undefined ? undefined : studiedSides(live.record.tags, served.profile.name, playedAs, text.sides, served.profile.accounts);
210
+ useTabTitle(titled === undefined ? undefined : tabTitle(titled.white.name, titled.black.name, at === 0 ? undefined : plies[at - 1]));
199
211
  // The header's theme switch picks the other of the two the page shows, leaving the system's.
200
212
  const switchTheme = () => change({ ...settings, theme: document.documentElement.dataset.theme === "dark" ? "light" : "dark" });
201
213
  // An action of the player's on the game: the game the server sends next shows it; a refusal shows on the board.
@@ -206,7 +218,9 @@ export function App({ start }) {
206
218
  const result = resultOf(live);
207
219
  const ended = isOver(result);
208
220
  const how = ended ? howEnded(live.outcome, live.record.tags.Termination) : undefined;
209
- const winner = result === "1-0" ? "white" : result === "0-1" ? "black" : undefined;
221
+ // A game closed before any move counts for no one, as Lichess aborts it: no winner, no kings' signs.
222
+ const aborted = ended && plies.length === 0;
223
+ const winner = aborted ? undefined : result === "1-0" ? "white" : result === "0-1" ? "black" : undefined;
210
224
  const justEnded = useJustEnded(played === undefined || played === null ? undefined : over ? "over" : "going");
211
225
  const offer = played?.drawOffer;
212
226
  const declined = useDeclined(offer, total, ended);
@@ -222,16 +236,18 @@ export function App({ start }) {
222
236
  }
223
237
  },
224
238
  };
225
- const { kind, verdict } = verdictOf(phase, how, winner, offer, declined, text);
239
+ const { kind, verdict } = aborted && phase === "over" ? { kind: "over", verdict: text.aborted } : verdictOf(phase, how, winner, offer, declined, text);
226
240
  const header = (_jsx(Header, { text: text, kind: kind, verdict: verdict, ...(!ended && (offer === "ai" || declined) ? { who: name } : {}), actions: actions, onNewGame: served === undefined ? undefined : () => setDialog("new-game"),
227
241
  // Between games the new one stands out in the player's card (D-77): in the header only when the talk, and the card with it, is folded away.
228
- standOut: studied !== undefined || (between && folds.talk !== "open"), onStudy: served === undefined || going ? undefined : () => setDialog("study"), flipped: flipped, onFlip: () => setFlipped(!flipped), sound: sound, onSound: () => change({ ...settings, sound: !sound }), onTheme: switchTheme, onSettings: () => setSetting(true), ...(served === undefined ? {} : { profile: served.profile, onProfile: () => setDialog("profiles") }) }));
242
+ standOut: studied !== undefined || (between && folds.talk !== "open"), onStudy: served === undefined || going ? undefined : () => setDialog("study"), flipped: flipped, onFlip: () => setFlipped(!flipped), sound: sound, onSound: () => change({ ...settings, sound: !sound }), onTheme: switchTheme, onSettings: () => setSetting(true), onGuide: () => setDialog("guide"), ...(served === undefined ? {} : { profile: served.profile, onProfile: () => setDialog("profiles") }) }));
229
243
  const panel = _jsx(SettingsPanel, { open: setting, settings: settings, onChange: change, onClose: () => setSetting(false), text: text });
244
+ // The guide opens before the page has its data too.
245
+ const guide = _jsx(GuideDialog, { open: dialog === "guide", onClose: () => setDialog(undefined), text: text });
230
246
  if (problem !== undefined) {
231
247
  return _jsx("p", { class: "problem", children: problem });
232
248
  }
233
249
  if (served === undefined || game === undefined) {
234
- return (_jsxs(_Fragment, { children: [header, panel] }));
250
+ return (_jsxs(_Fragment, { children: [header, panel, guide] }));
235
251
  }
236
252
  // At the first start the page asks who plays (D-23), to complete the profile Chesspal made (D-71): the game comes after.
237
253
  if (served.profile.new === true) {
@@ -253,6 +269,22 @@ export function App({ start }) {
253
269
  const review = reviewed?.review;
254
270
  // An open hint draws on the board as it is, in place of the last move's drawings.
255
271
  const drawn = hintOpen && hint !== undefined && viewing === undefined && (hint.arrows !== undefined || hint.squares !== undefined) ? hint : review;
272
+ const drawnByCoach = hint !== undefined && drawn === hint;
273
+ // The game over, when this is its last position: the kings' signs, on the board and in the legend.
274
+ const boardEnd = ended && !aborted && viewing === undefined && pending === undefined ? { how, winner, player, fresh: justEnded } : undefined;
275
+ // The legend has the marks the board shows: none of the helps' or the drawings while the board is bare (D-99).
276
+ const shownBare = bare && helps !== undefined;
277
+ const legendRows = legendOf({
278
+ ...(review?.badge === undefined || shown.lastMove === undefined ? {} : { badge: { badge: review.badge, square: shown.lastMove.to } }),
279
+ ...(helps === undefined || shownBare ? {} : { helps }),
280
+ ...(drawn === undefined || shownBare ? {} : { drawn }),
281
+ control: mapping && helps !== undefined,
282
+ ...(boardEnd === undefined ? {} : { ends: kingSigns(piecesOf(shown.position.fen), shown.position.turn, boardEnd) }),
283
+ });
284
+ // In a game the coach draws on the player's moves, the AI on its own.
285
+ const drawer = drawnByCoach || (going && reviewed?.side === player) ? text.legend.drawnByCoach : text.legend.drawnBy(analyst);
286
+ const legendAt = (under) => (_jsx(MarksLegend, { rows: legendRows, open: legendOpen, onOpen: setLegendOpen, drawn: { words: drawnByCoach ? text.helpsCard.drawn : text.drawn, by: drawer }, onPoint: setPointed, under: under, text: text }));
287
+ const legend = legendAt(false);
256
288
  const summary = studied === undefined ? undefined : gameReviewOf(live.record);
257
289
  const closing = studied === undefined ? undefined : closingOf(live.record);
258
290
  // In analysis the AI's card says who played that game, as its PGN keeps it (D-82), not who plays now.
@@ -270,7 +302,10 @@ export function App({ start }) {
270
302
  const clockWaits = clock?.waiting === true && !ended && studied === undefined && viewing === undefined && live.position.turn === ai;
271
303
  const clockOf = (side) => clock === undefined ? undefined : (_jsx(ClockFace, { clock: clock, side: side, toMove: !ended && live.position.turn === side, held: pending !== undefined || (asleep && side === ai), onFlag: reload, text: text }));
272
304
  // Where to wake the chat, under the AI's card (over the board when the talk is folded); the button that starts the clocks, under the player's (Zoe).
273
- const asleepNote = asleep ? _jsx("p", { class: "asleep", role: "status", children: text.asleep(name) }) : null;
305
+ // On the table with no chat at it, the note opens one instead (D-104).
306
+ const table = offline ? undefined : served.table;
307
+ const opener = table !== undefined && !table.chat ? _jsx(ChatOpener, { table: table, settings: settings, onChange: change, name: name, text: text }) : undefined;
308
+ const asleepNote = opener ?? (asleep ? _jsx("p", { class: "asleep", role: "status", children: sides === undefined ? text.asleep(name) : text.asleepInAnalysis(name) }) : null);
274
309
  const startClocks = clockWaits
275
310
  ? (_jsxs("button", { type: "button", class: "command primary start-clocks", onClick: () => act("/api/start", {}), children: [_jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M8 5.5v13l10.5-6.5z" }) }), text.startClocks] }))
276
311
  : null;
@@ -322,7 +357,8 @@ export function App({ start }) {
322
357
  setRefusal({ text: words, square: from, piece: pieces.get(from), explanation: explain(illegal, to, king) });
323
358
  }
324
359
  };
325
- const sendWords = () => {
360
+ // In analysis the words go with the game and the move shown (D-101).
361
+ const sendWords = (position) => {
326
362
  const words = draft;
327
363
  // /clear and the other commands are for Claude's app: written here they would reach the AI as words (Sebastiano's playtest).
328
364
  if (isCommand(words)) {
@@ -330,7 +366,7 @@ export function App({ start }) {
330
366
  return;
331
367
  }
332
368
  setUnsent(undefined);
333
- void sendMessage(words, text, name).then((refused) => {
369
+ void sendMessage(words, text, name, position).then((refused) => {
334
370
  setUnsent(refused?.text);
335
371
  clearSent(refused?.text, words);
336
372
  });
@@ -344,12 +380,10 @@ export function App({ start }) {
344
380
  setDialog(undefined);
345
381
  act("/api/new", { ai_colour: colour === "random" ? "random" : colour === "white" ? "black" : "white", level: chosen, ...(control === undefined ? {} : { clock: control }) });
346
382
  };
347
- // In analysis the AI's note stays under the card on top and its word on the game over the one below, however the board is turned (D-69).
348
- const top = flipped ? player : ai;
349
- const bottom = flipped ? ai : player;
383
+ // In analysis the AI's note goes with the other side's card and the talk with the player's, turned round with the board as in a game (D-101, Zoe).
350
384
  const studiedCard = (side, place) => sides === undefined ? null : (_jsx(PlayerCard, { place: place,
351
- // The player changes their picture in analysis too, from their own side's card.
352
- picture: sides[side].who === "player" ? _jsx(PictureButton, { profile: profile, onOpen: () => setDialog("picture"), text: text }) : pictureOf(sides[side], profile, 48, mood), name: sides[side].name, signed: sides[side].who === "ai" && playedBy !== undefined ? signature(playedBy.model, playedBy.effort) : undefined, side: side, taken: material.taken[side], ahead: ahead(side), badges: _jsx(BadgeCounts, { plies: plies, side: side, names: text.badges }), text: text }));
385
+ // The player opens their profile in analysis too, from their own side's card.
386
+ picture: sides[side].who === "player" ? _jsx(PictureButton, { profile: profile, onOpen: () => setDialog("profile"), text: text }) : pictureOf(sides[side], profile, 48, mood), name: sides[side].name, signed: sides[side].who === "ai" && playedBy !== undefined ? signature(playedBy.model, playedBy.effort) : undefined, side: side, taken: material.taken[side], ahead: ahead(side), badges: _jsx(BadgeCounts, { plies: plies, side: side, names: text.badges }), text: text }));
353
387
  // A game brought to analyse: the page shows it once the server has it in analysis; a refusal stays in the dialog.
354
388
  const studyGame = async (studied) => {
355
389
  const refused = await post("/api/study", studied, text, name);
@@ -358,22 +392,25 @@ export function App({ start }) {
358
392
  }
359
393
  return refused?.text;
360
394
  };
361
- return (_jsx(ChosenSet.Provider, { value: settings.set, children: _jsxs(PointSquare.Provider, { value: pointSquare, children: [panel, _jsx(PictureDialog, { open: dialog === "picture", profile: profile, onClose: () => setDialog(undefined), text: text }), _jsx(ProfilesDialog, { open: dialog === "profiles", profiles: served.profiles, active: profile, locked: going ? text.profile.whilePlaying : undefined, dataFolder: served.dataFolder, onClose: () => setDialog(undefined), text: text }), _jsx(NewGameDialog, { open: dialog === "new-game", choice: choice, going: going, onStart: startNewGame, onClose: () => setDialog(undefined), text: text }), _jsx(StudyDialog, { open: dialog === "study", onStudy: studyGame, onClose: () => setDialog(undefined), you: profile.name, accounts: profile.accounts, ai: name, text: text }), offline ? _jsx("p", { class: "offline", role: "alert", children: text.offline(name) }) : null, served.outdated === true ? _jsx("p", { class: "outdated", role: "status", children: text.outdated(name) }) : null, _jsxs("main", { class: `app${flipped ? " flipped" : ""}${folds.talk === "open" ? "" : ` talk-${folds.talk}`}${folds.game === "folded" ? " folded" : ""}`, inert: offline, children: [header, sides === undefined
395
+ return (_jsx(ChosenSet.Provider, { value: settings.set, children: _jsxs(PointSquare.Provider, { value: pointSquare, children: [panel, _jsx(ProfileDialog, { open: dialog === "profile", profile: profile, onClose: () => setDialog(undefined), text: text }), _jsx(ProfilesDialog, { open: dialog === "profiles", profiles: served.profiles, active: profile, locked: going ? text.profile.whilePlaying : undefined, dataFolder: served.dataFolder, onEdit: () => setDialog("profile"), onClose: () => setDialog(undefined), text: text }), _jsx(NewGameDialog, { open: dialog === "new-game", choice: choice, going: going, onStart: startNewGame, onClose: () => setDialog(undefined), text: text }), guide, _jsx(StudyDialog, { open: dialog === "study", onStudy: studyGame, onClose: () => setDialog(undefined), you: profile.name, accounts: profile.accounts, ai: name, text: text }), offline ? _jsx("p", { class: "offline", role: "alert", children: text.offline(name) }) : null, served.outdated === true ? _jsx("p", { class: "outdated", role: "status", children: text.outdated(name) }) : null, _jsxs("main", { class: `app${flipped ? " flipped" : ""}${folds.talk === "open" ? "" : ` talk-${folds.talk}`}${folds.game === "folded" ? " folded" : ""}`, inert: offline, children: [header, sides === undefined
362
396
  ? (_jsx(TalkFaces, { name: name, aiPicture: thinker(40), you: profile.name, picture: picture, ...(idle ? {} : { ai, player }), aiMoves: going && !playerToMove, playerMoves: playerToMove, unread: unread || asleep, text: text }))
363
- : (_jsx(TalkFaces, { name: sides[ai].name, aiPicture: pictureOf(sides[ai], profile, 40, mood), you: sides[player].name, picture: pictureOf(sides[player], profile, 40, mood), ai: ai, player: player, aiMoves: false, playerMoves: false, unread: asleep, text: text })), _jsx(FoldTab, { column: "talk", folded: folds.talk === "folded", label: thinking === undefined || folds.talk !== "folded" ? text.talk : `${text.talk}: ${text.thinking[thinking]}`, unread: (unread || asleep) && folds.talk === "folded", ...(thinking === undefined || folds.talk !== "folded" ? {} : { mark: _jsx(Thought, { thinking: thinking, words: text.thinking }) }), onFold: fold }), _jsx(FoldTab, { column: "game", folded: folds.game === "folded", label: text.game, onFold: fold }), _jsx("span", { class: "visually-hidden", "aria-live": "polite", children: unread ? text.wrote(name) : "" }), _jsx("div", { class: sides === undefined ? "talk" : "talk studied", id: "talk", children: sides !== undefined
364
- ? (_jsxs(_Fragment, { children: [_jsxs("section", { class: "ai-block", "aria-label": sides[top].name, children: [studiedCard(top, "ai"), asleepNote, _jsx(AiNote, { ...(reviewed === undefined ? {} : { ply: reviewed }), ...(shownOpening === undefined ? {} : { opening: shownOpening }), ...(at === total && closing !== undefined ? { closing } : {}), ...(at === 0 && summary !== undefined ? { summary } : {}), ...(summary === undefined ? { reviewing: true } : {}), name: analyst, by: annotator, mood: mood, ...(settings.voice ? { voice: text.language } : {}), text: text })] }), _jsx("section", { class: "you-block", "aria-label": sides[bottom].name, children: studiedCard(bottom, "you") })] }))
397
+ : (_jsx(TalkFaces, { name: sides[ai].name, aiPicture: pictureOf(sides[ai], profile, 40, mood), you: sides[player].name, picture: pictureOf(sides[player], profile, 40, mood), ai: ai, player: player, aiMoves: false, playerMoves: false, unread: asleep, text: text })), _jsx(FoldTab, { column: "talk", folded: folds.talk === "folded", label: thinking === undefined || folds.talk !== "folded" ? text.talk : `${text.talk}: ${text.thinking[thinking]}`, unread: (unread || asleep) && folds.talk === "folded", ...(thinking === undefined || folds.talk !== "folded" ? {} : { mark: _jsx(Thought, { thinking: thinking, words: text.thinking }) }), onFold: fold }), _jsx(FoldTab, { column: "game", folded: folds.game === "folded", label: text.game, onFold: fold }), _jsx("span", { class: "visually-hidden", "aria-live": "polite", children: unread ? text.wrote(name) : "" }), _jsx("div", { class: sides === undefined ? "talk" : "talk studied", id: "talk", children: sides !== undefined && studied !== undefined
398
+ ? (_jsxs(_Fragment, { children: [_jsxs("section", { class: "ai-block", "aria-label": sides[ai].name, children: [studiedCard(ai, "ai"), asleepNote, _jsx(AiNote, { ...(reviewed === undefined ? {} : { ply: reviewed }), ...(shownOpening === undefined ? {} : { opening: shownOpening }), ...(at === total && closing !== undefined ? { closing } : {}), ...(at === 0 && summary !== undefined ? { summary } : {}), ...(summary === undefined ? { reviewing: true } : {}), name: analyst, by: annotator, mood: mood, ...(settings.voice ? { voice: text.language } : {}), text: text })] }), _jsxs("section", { class: "you-block", "aria-label": sides[player].name, children: [_jsx("div", { class: "chat", children: _jsx(Compose, { lines: analysisTalk(talk, studied.name), newestFirst: false, spent: 0, draft: draft, onDraft: (words) => {
399
+ setDraft(words);
400
+ setUnsent(undefined);
401
+ }, onSend: () => sendWords({ game: studied.name, at }), canMove: false, name: name, problem: unsent, about: { ...(at === 0 ? {} : { ply: plies[at - 1] }), onShow: goTo }, text: text }) }), studiedCard(player, "you")] })] }))
365
402
  : (_jsxs(_Fragment, { children: [_jsxs("section", { class: "ai-block", "aria-label": name, children: [_jsx(PlayerCard, { place: "ai", picture: thinker(48), name: name, signed: signed, side: idle ? undefined : ai, ...(idle ? {} : { level: text.start.levels[level] }), taken: material.taken[ai], ahead: ahead(ai), clock: clockOf(ai), text: text }), asleepNote, _jsx(AiWords, { words: aiWords(plies, ai, talk), newestFirst: !flipped, ...(settings.voice ? { voice: text.language } : {}) })] }), _jsxs("section", { class: "you-block", "aria-label": profile.name, children: [_jsx(Compose, { lines: said, newestFirst: flipped,
366
403
  // Between games, and before the first, the player writes freely, a line at a time, as the AI does (D-74, D-77).
367
404
  spent: going ? playerSpent(plies, ai, talk) : 0, draft: draft, onDraft: (words) => {
368
405
  setDraft(words);
369
406
  setUnsent(undefined);
370
- }, onSend: sendWords, canMove: playerToMove, name: name, problem: unsent, text: text }), _jsx(PlayerCard, { place: "you", picture: _jsx(PictureButton, { profile: profile, onOpen: () => setDialog("picture"), text: text }), name: _jsx(NameButton, { profile: profile, onOpen: () => setDialog("profiles"), text: text }), side: idle ? undefined : player, taken: material.taken[player], ahead: ahead(player), turn: playerToMove, clock: _jsxs(_Fragment, { children: [clockOf(player), startClocks, nextGame] }), text: text })] })] })) }), _jsxs("div", {
407
+ }, onSend: sendWords, canMove: playerToMove, name: name, problem: unsent, text: text }), _jsx(PlayerCard, { place: "you", picture: _jsx(PictureButton, { profile: profile, onOpen: () => setDialog("profile"), text: text }), name: _jsx(NameButton, { profile: profile, onOpen: () => setDialog("profiles"), text: text }), side: idle ? undefined : player, taken: material.taken[player], ahead: ahead(player), turn: playerToMove, clock: _jsxs(_Fragment, { children: [clockOf(player), startClocks, nextGame] }), text: text })] })] })) }), _jsxs("div", {
371
408
  // Between games the board goes off (D-77): a game the page saw end once its effects are over, its final position less off than the table set.
372
409
  class: between && !justEnded ? "board-wrap off" : "board-wrap", onWheel: wheel,
373
410
  // A press on the board while the player looks back brings the game as it is: the next one plays.
374
- onPointerDownCapture: viewing === undefined || studied !== undefined ? undefined : (event) => event.button === 0 && goTo(total), children: [_jsx(Board, { fen: shown.position.fen, bottom: flipped ? ai : player, lastMove: shown.lastMove, pointed: pointed, ...(helps === undefined ? {} : { helps }), ...(mapping && helps !== undefined ? { control: helps.strategy.control } : {}), bare: bare && helps !== undefined, ...(review?.badge === undefined ? {} : { badge: { badge: review.badge, name: text.badges[review.badge] } }), ...(drawn === undefined ? {} : { drawn }), nextMove: next, jumped: jumped, check: shown.position.check, end: ended && viewing === undefined && pending === undefined ? { how, winner, player, fresh: justEnded } : undefined, player: playerToMove && pending === undefined && viewing === undefined ? player : undefined, selected: selected, targets: selected === undefined || viewing !== undefined ? [] : live.legalMoves(selected), dots: settings.dots, names: settings.squareNames, notice: viewing === undefined ? refusal : undefined, promotion: promoting === undefined || viewing !== undefined || !playerToMove ? undefined : { ...promoting, side: player }, onPromote: (piece) => piece === undefined ? setPromoting(undefined) : play(promoting.from, promoting.to, piece), onSelect: select, onMove: play, onRefuse: refuse, text: text }), folds.talk !== "open" && (asleep || clockWaits)
375
- ? (_jsxs("div", { class: "over-board", children: [asleep ? _jsx("p", { class: "asleep", role: "status", children: text.asleep(name) }) : null, startClocks] }))
376
- : null] }), studied === undefined ? null : (_jsx("div", { class: "board-steps", children: _jsx(Steps, { shown: at, total: total, onStep: step, text: text }) })), helps === undefined ? null : (_jsxs("div", { class: "board-map", children: [_jsx(BareButton, { bare: bare, onBare: setBare, text: text }), _jsx(MapButton, { mapping: mapping, onMap: setMapping, text: text }), hint === undefined || viewing !== undefined ? null : _jsx(HintLine, { hint: hint, open: hintOpen, onOpen: openTheHint, text: text })] })), _jsx(GameColumn, { plies: idle ? [] : plies, result: ended ? result : undefined, how: how, lit: justEnded, started: idle ? undefined : played?.started, move: live.position.moveNumber, toMove: live.position.turn, opening: opening, helps: helps === undefined ? undefined : (_jsx(HelpsCard, { helps: helps, judged: judgments, hint: viewing === undefined ? hint : undefined, hintOpen: hintOpen, onHint: openTheHint, onPoint: setPointed, onMap: setMapping, mapping: mapping, onBare: setBare, bare: bare, text: text })), ...(standings === undefined ? {} : { standings }), ...(studied === undefined ? {} : { analysis: summary === undefined ? { by: analyst } : { by: analyst, summary } }), shownPlies: at, onShow: goTo, onStep: step, ...(studied === undefined ? {} : { onDownload: () => download(studied.pgn, pgnName(live.record.tags, "analysis")) }), ...(total === 0 ? {} : { onSaveMoves: () => download(writePgn(movesOnly(live.record)), pgnName(live.record.tags)) }), ...(between && game !== null ? { onAnalyse: () => void studyGame({ last: true }) } : {}), text: text })] })] }) }));
411
+ onPointerDownCapture: viewing === undefined || studied !== undefined ? undefined : (event) => event.button === 0 && goTo(total), children: [_jsx(Board, { fen: shown.position.fen, bottom: flipped ? ai : player, lastMove: shown.lastMove, pointed: pointed, ...(helps === undefined ? {} : { helps }), ...(mapping && helps !== undefined ? { control: helps.strategy.control } : {}), bare: bare && helps !== undefined, ...(review?.badge === undefined ? {} : { badge: { badge: review.badge, name: text.badges[review.badge] } }), ...(drawn === undefined ? {} : { drawn }), nextMove: next, jumped: jumped, check: shown.position.check, end: boardEnd, player: playerToMove && pending === undefined && viewing === undefined ? player : undefined, selected: selected, targets: selected === undefined || viewing !== undefined ? [] : live.legalMoves(selected), dots: settings.dots, names: settings.squareNames, notice: viewing === undefined ? refusal : undefined, promotion: promoting === undefined || viewing !== undefined || !playerToMove ? undefined : { ...promoting, side: player }, onPromote: (piece) => piece === undefined ? setPromoting(undefined) : play(promoting.from, promoting.to, piece), onSelect: select, onMove: play, onRefuse: refuse, text: text }), folds.talk !== "open" && (asleep || opener !== undefined || clockWaits)
412
+ ? (_jsxs("div", { class: "over-board", children: [opener ?? (asleep ? _jsx("p", { class: "asleep", role: "status", children: text.asleep(name) }) : null), startClocks] }))
413
+ : null] }), studied === undefined ? null : (_jsx("div", { class: "board-steps", children: _jsx(Steps, { shown: at, total: total, onStep: step, text: text }) })), helps === undefined ? null : (_jsxs("div", { class: "board-map", children: [legendAt(true), _jsx(BareButton, { bare: bare, onBare: setBare, text: text }), _jsx(MapButton, { mapping: mapping, onMap: setMapping, text: text }), hint === undefined || viewing !== undefined ? null : _jsx(HintLine, { hint: hint, open: hintOpen, onOpen: openTheHint, text: text })] })), _jsx(GameColumn, { plies: idle ? [] : plies, result: ended ? result : undefined, how: how, lit: justEnded, started: idle ? undefined : played?.started, move: live.position.moveNumber, toMove: live.position.turn, opening: opening, helps: helps === undefined ? undefined : (_jsx(HelpsCard, { helps: helps, judged: judgments, hint: viewing === undefined ? hint : undefined, hintOpen: hintOpen, onHint: openTheHint, onPoint: setPointed, onMap: setMapping, mapping: mapping, onBare: setBare, bare: bare, legend: legend, text: text })), ...(helps === undefined && legendRows.length > 0 ? { legend } : {}), ...(standings === undefined ? {} : { standings }), ...(studied === undefined ? {} : { analysis: summary === undefined ? { by: analyst } : { by: analyst, summary } }), shownPlies: at, onShow: goTo, onStep: step, ...(studied === undefined ? {} : { onDownload: () => download(studied.pgn, pgnName(live.record.tags, "analysis")) }), ...(total === 0 ? {} : { onSaveMoves: () => download(writePgn(movesOnly(live.record)), pgnName(live.record.tags)) }), ...(between && game !== null && replayed.record.moves.length > 0 ? { onAnalyse: () => void studyGame({ last: true }) } : {}), text: text })] })] }) }));
377
414
  }
378
415
  /** The picture of a player of the game in analysis: the player's own, the AI's Torretta with its mood, or the plain face of someone else. */
379
416
  function pictureOf({ who }, profile, size, mood) {
@@ -1,5 +1,5 @@
1
- // The page's keys and wheel: [ and ] fold the side columns, M holds the helps' map (D-78), the arrows, Home and End step through the moves and f
2
- // and x turn the board, and the wheel over the board steps too.
1
+ // The page's keys and wheel: [ and ] fold the side columns, M holds the helps' map (D-78), ? opens the legend (D-99), the arrows, Home and End step
2
+ // through the moves and f and x turn the board, and the wheel over the board steps too.
3
3
  import { useEffect, useRef } from "preact/hooks";
4
4
  import { flipKeys, stepKeys } from "../game/browse.js";
5
5
  import { foldKeys, gameUnderBoard, oneColumn } from "../layout/folds.js";
@@ -52,6 +52,20 @@ export function useMapKey(helping, map) {
52
52
  };
53
53
  }, [helping]);
54
54
  }
55
+ /** ? opens the legend of the marks on the board and closes it, while the page shows its ? (D-99). */
56
+ export function useLegendKey(toggle) {
57
+ useEffect(() => {
58
+ const press = (event) => {
59
+ const shown = [...document.querySelectorAll(".legend-button")].some((button) => button.getClientRects().length > 0);
60
+ if (shown && forThePage(event) && event.key === "?") {
61
+ event.preventDefault();
62
+ toggle();
63
+ }
64
+ };
65
+ window.addEventListener("keydown", press);
66
+ return () => window.removeEventListener("keydown", press);
67
+ });
68
+ }
55
69
  /** The keys of Lichess and chess.com: the arrows, Home and End step through the moves, f and x turn the board round. */
56
70
  export function useBrowseKeys(step, flip) {
57
71
  useEffect(() => {
@@ -68,6 +68,6 @@ export function sendMove(move, words, text, name) {
68
68
  return post("/api/move", message === "" ? { move } : { move, message }, text, name);
69
69
  }
70
70
  /** Sends the player's words alone: the reason they were refused, or undefined. */
71
- export function sendMessage(words, text, name) {
72
- return post("/api/message", { text: words.trim() }, text, name);
71
+ export function sendMessage(words, text, name, shown) {
72
+ return post("/api/message", { text: words.trim(), ...shown }, text, name);
73
73
  }
@@ -0,0 +1,17 @@
1
+ // The tab's title names the game and the move shown, Stefano – Claude, 16. ♞f3, so that the player's bookmarks and tabs tell its games apart.
2
+ import { useEffect } from "preact/hooks";
3
+ import { figurinesOf } from "../../core/index.js";
4
+ /** The title of a game: its players by their first names, White first, so that the move fits in a tab (Zoe), and the move shown with its number, none before the first. */
5
+ export function tabTitle(white, black, shown) {
6
+ const players = `${firstName(white)} – ${firstName(black)}`;
7
+ return shown === undefined ? players : `${players}, ${shown.number}${shown.side === "white" ? "." : "..."} ${figurinesOf(shown.san)}`;
8
+ }
9
+ /** Keeps the tab's title, Chesspal's name while no game shows. */
10
+ export function useTabTitle(title) {
11
+ useEffect(() => {
12
+ document.title = title ?? "Chesspal";
13
+ }, [title]);
14
+ }
15
+ function firstName(name) {
16
+ return name.trim().split(/\s+/)[0] || name;
17
+ }
@@ -18,8 +18,8 @@ function useChanged(mood) {
18
18
  function Face({ mood }) {
19
19
  switch (mood) {
20
20
  case "happy":
21
- // Eyes closed in two arcs, a wide open smile.
22
- return (_jsxs(_Fragment, { children: [_jsx("path", { class: "eyes line", d: "M50.5 78.5a4.5 4.5 0 0 1 9 0M68.5 78.5a4.5 4.5 0 0 1 9 0" }), _jsx("path", { class: "mouth", d: "M57 84h14a7 7 0 0 1-14 0Z" })] }));
21
+ // Eyes closed in two low arcs, a wide open smile with its corners just rounded.
22
+ return (_jsxs(_Fragment, { children: [_jsx("path", { class: "eyes line", d: "M50 77.5q5-5.4 10 0M68 77.5q5-5.4 10 0" }), _jsx("path", { class: "mouth", d: "M56.6 83.2H71.4Q73 83.2 72.9 84.6A9 7.8 0 0 1 55.1 84.6Q55 83.2 56.6 83.2Z" })] }));
23
23
  case "surprised":
24
24
  // Wide eyes and a round mouth.
25
25
  return (_jsxs(_Fragment, { children: [_jsxs("g", { class: "eyes", children: [_jsx("circle", { cx: "54.5", cy: "75.5", r: "5.8" }), _jsx("circle", { cx: "73.5", cy: "75.5", r: "5.8" })] }), _jsx("ellipse", { class: "mouth", cx: "64", cy: "88.5", rx: "3.4", ry: "4" })] }));
@@ -27,8 +27,8 @@ function Face({ mood }) {
27
27
  // Brows raised in the middle, smaller eyes, a frown; the light turns red.
28
28
  return (_jsxs(_Fragment, { children: [_jsx("path", { class: "brows", d: "M50 70L58 67M70 67L78 70" }), _jsxs("g", { class: "eyes", children: [_jsx("circle", { cx: "55", cy: "78.5", r: "3.8" }), _jsx("circle", { cx: "73", cy: "78.5", r: "3.8" })] }), _jsx("path", { class: "mouth line", d: "M56.5 91q7.5-6 15 0" })] }));
29
29
  case "thoughtful":
30
- // A glance to the side under one raised brow, the mouth pushed to the other side; the light breathes.
31
- return (_jsxs(_Fragment, { children: [_jsx("path", { class: "brows", d: "M71 68L80 66" }), _jsxs("g", { class: "eyes", children: [_jsx("circle", { cx: "58", cy: "76.5", r: "3.9" }), _jsx("circle", { cx: "76", cy: "76.5", r: "3.9" })] }), _jsx("path", { class: "mouth line", d: "M52.5 89L63 87" })] }));
30
+ // The neutral face tilted on the screen, a smaller mouth: the head bent of one who wonders; the light breathes.
31
+ return (_jsxs("g", { transform: "rotate(-12 64 79)", children: [_jsxs("g", { class: "eyes", children: [_jsx("circle", { cx: "55", cy: "74.5", r: "4.4" }), _jsx("circle", { cx: "73", cy: "74.5", r: "4.4" })] }), _jsx("rect", { class: "mouth", x: "60", y: "86.3", width: "8", height: "3.4", rx: "1.7" })] }));
32
32
  case "sad":
33
33
  // No brows, the face sunk low: eyes half shut under lids that droop outwards, a deep frown; the light goes out.
34
34
  return (_jsxs(_Fragment, { children: [_jsx("path", { class: "eyes", d: "M60.3 77.1A5.6 5.6 0 0 1 49.7 80.9ZM78.3 80.9A5.6 5.6 0 0 1 67.7 77.1Z" }), _jsx("path", { class: "mouth line", d: "M57.5 92.5q6.5-6.5 13 0" })] }));
@@ -6,9 +6,10 @@ import { BadgeIcon } from "../game/badges.js";
6
6
  import { pinOn, riskOn, skewerOn } from "../helps/view.js";
7
7
  import { Piece } from "../pieces/pieces.js";
8
8
  import { bubblePlace, keptSquares } from "./explain.js";
9
- import { Arrows, Badge, Bubble, ControlCount, controlOf, kingSigns, Lifted, noSigns, Notch, PinsAndSkewers, PromotionChoice, Pushpin, Skewer } from "./marks.js";
9
+ import { Arrows, Badge, Bubble, ControlCount, controlOf, Lifted, Notch, PinsAndSkewers, PromotionChoice, Pushpin, Skewer } from "./marks.js";
10
10
  import { backOf, motionOf, noMotion, playedOn, rearranged } from "./motion.js";
11
11
  import { useBoardPointer } from "./pointer.js";
12
+ import { kingSigns, noSigns } from "./signs.js";
12
13
  import { files, kingToMove, piecesOf } from "./squares.js";
13
14
  /**
14
15
  * The board of a position, seen from one side, with the last move's squares marked. While the player may move, their pieces answer the pointer: a press
@@ -5,7 +5,10 @@ import { Piece } from "../pieces/pieces.js";
5
5
  export function Notch({ tone }) {
6
6
  return (_jsx("span", { class: tone === undefined ? "help-notch" : `help-notch ${tone}`, "aria-hidden": "true", children: _jsxs("svg", { viewBox: "0 0 10 10", children: [_jsx("path", { d: "M0.5 0.5V9.5H9.5Z" }), tone === undefined ? null : _jsx("path", { class: "edge", d: "M0.5 0.5V9.5H9.5Z" }), _jsx("circle", { cx: "2.9", cy: "7.1", r: "1.1" })] }) }));
7
7
  }
8
- /** The pushpin stuck in a pinned piece from the top left corner, its needle toward the piece, in the tone of its corner at risk (D-91); in ink in no side's. */
8
+ /**
9
+ * The pushpin stuck in a pinned piece from the top left corner, its needle toward the piece, in the tone of its corner at risk (D-91); in ink in no side's;
10
+ * in the card's line, in its colour.
11
+ */
9
12
  export function Pushpin({ tone }) {
10
13
  return _jsx(Stuck, { kind: "help-pin", shape: pushpin, tone: tone });
11
14
  }
@@ -122,29 +125,6 @@ export function Lifted({ drag, letter }) {
122
125
  const classes = ["lifted", up && !drag.back ? "up" : "", drag.touch ? "touch" : "", drag.back ? "back" : ""].filter((name) => name !== "").join(" ");
123
126
  return (_jsx("div", { class: classes, style: { "--x": `${drag.at[0]}px`, "--y": `${drag.at[1]}px` }, children: _jsx(Piece, { letter: letter }) }));
124
127
  }
125
- /** The kings' signs at the end, by their squares: the loser's king lies down when mated or resigning; both kings of a draw stand, the stalemated one stuck. */
126
- export function kingSigns(pieces, toMove, end) {
127
- const kingOf = (side) => [...pieces].find(([, letter]) => letter === (side === "white" ? "K" : "k"))?.[0];
128
- const signs = new Map();
129
- const sign = (side, badge, classes) => {
130
- const square = kingOf(side);
131
- if (square !== undefined) {
132
- signs.set(square, { badge, classes });
133
- }
134
- };
135
- if (end.winner === undefined) {
136
- sign("white", "half", end.how === "stalemate" && toMove === "white" ? "drawn stalled" : "drawn");
137
- sign("black", "half", end.how === "stalemate" && toMove === "black" ? "drawn stalled" : "drawn");
138
- return signs;
139
- }
140
- const loser = end.winner === "white" ? "black" : "white";
141
- sign(end.winner, "crown", "winner");
142
- if (end.how === "checkmate" || end.how === "resignation") {
143
- sign(loser, end.how === "checkmate" ? "mate" : "flag", "fallen");
144
- }
145
- return signs;
146
- }
147
- export const noSigns = new Map();
148
128
  /** Arrows off one square, the AI's on a white edge (D-78): every edge under every arrow, so one edge goes round them all where they meet. */
149
129
  function ArrowGroup({ shapes, edged }) {
150
130
  return (_jsxs("g", { children: [edged
@@ -0,0 +1,23 @@
1
+ /** The kings' signs at the end, by their squares: the loser's king lies down when mated or resigning; both kings of a draw stand, the stalemated one stuck. */
2
+ export function kingSigns(pieces, toMove, end) {
3
+ const kingOf = (side) => [...pieces].find(([, letter]) => letter === (side === "white" ? "K" : "k"))?.[0];
4
+ const signs = new Map();
5
+ const sign = (side, badge, classes) => {
6
+ const square = kingOf(side);
7
+ if (square !== undefined) {
8
+ signs.set(square, { badge, classes });
9
+ }
10
+ };
11
+ if (end.winner === undefined) {
12
+ sign("white", "half", end.how === "stalemate" && toMove === "white" ? "drawn stalled" : "drawn");
13
+ sign("black", "half", end.how === "stalemate" && toMove === "black" ? "drawn stalled" : "drawn");
14
+ return signs;
15
+ }
16
+ const loser = end.winner === "white" ? "black" : "white";
17
+ sign(end.winner, "crown", "winner");
18
+ if (end.how === "checkmate" || end.how === "resignation") {
19
+ sign(loser, end.how === "checkmate" ? "mate" : "flag", "fallen");
20
+ }
21
+ return signs;
22
+ }
23
+ export const noSigns = new Map();
@@ -45,7 +45,7 @@ export function Analyses({ onStudy, onLoad, you, accounts, ai, text }) {
45
45
  return (_jsxs("div", { class: "analyses", children: [_jsxs("label", { class: "search", children: [_jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M10.5 17A6.5 6.5 0 1 0 10.5 4A6.5 6.5 0 0 0 10.5 17ZM15.5 15.5L20 20" }) }), _jsx("input", { type: "search", value: query, placeholder: words.search, "aria-label": words.search, spellcheck: false, onInput: (event) => setQuery(event.currentTarget.value) })] }), _jsxs("button", { class: "load-row", type: "button", onClick: onLoad, children: [_jsx("span", { class: "plus", "aria-hidden": "true", children: _jsx("svg", { class: "icon", viewBox: "0 0 24 24", children: _jsx("path", { d: "M12 5V19M5 12H19" }) }) }), _jsxs("span", { children: [_jsx("b", { children: words.bring }), _jsx("small", { children: words.bringNote })] })] }), problem === undefined ? null : _jsx("p", { class: "problem", role: "alert", children: problem }), analyses.length > 0 && found.length === 0 ? _jsx("p", { class: "none", children: words.noMatch }) : null, found.length === 0 ? null : (_jsx("ul", { class: "games", "aria-label": words.yours, children: found.map(({ item, named, outcome }) => asking?.name === item.name
46
46
  ? (_jsx(Question, { asked: asking.asked, game: named.sides, when: dayOf(item.date, text.language), by: by(item), busy: busy, onYes: () => void act(item.name, asking.asked), onNo: () => setAsking(undefined), words: words }, item.name))
47
47
  : (_jsxs("li", { class: "game", children: [_jsxs("button", { class: "reopen", type: "button", disabled: busy, onClick: () => open(item.name), children: [_jsx(ResultTile, { result: item.result }), _jsxs("span", { class: "what", children: [_jsx("b", { class: "who", children: named.sides }), _jsx("span", { class: "meta", children: [dayOf(item.date, text.language), words.moves(item.moves), item.opening].filter((part) => part !== undefined).join(" · ") }), item.reviewed ? _jsx(Reviewed, { by: by(item), words: words }) : null] }), _jsxs("span", { class: "score", children: [_jsx("b", { children: scoreOf(item.result) }), outcome === undefined ? _jsx("span", {}) : _jsx(OutcomeBadge, { outcome: outcome, words: words })] })] }), _jsx("span", { class: "row-actions", children: _jsx("button", { class: `icon-button${menu?.name === item.name ? " on" : ""}`, type: "button", title: words.more, "aria-label": words.more, "aria-expanded": menu?.name === item.name, disabled: busy, onClick: (event) => setMenu(menu?.name === item.name ? undefined : { name: item.name, anchor: event.currentTarget }), children: _jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M5 12H5.01M12 12H12.01M19 12H19.01" }) }) }) }), menu?.name === item.name
48
- ? (_jsx(RowMenu, { anchor: menu.anchor, reviewed: item.reviewed, onPick: (asked) => {
48
+ ? (_jsx(RowMenu, { anchor: menu.anchor, withReview: item.reviewed || item.begun === true, onPick: (asked) => {
49
49
  setMenu(undefined);
50
50
  setAsking({ name: item.name, asked });
51
51
  }, onClose: () => setMenu(undefined), words: words }))
@@ -53,9 +53,9 @@ export function Analyses({ onStudy, onLoad, you, accounts, ai, text }) {
53
53
  }
54
54
  /**
55
55
  * A row's menu, over the dialog and the page in the browser's top layer, under its button or above it when there is no room: analyse again and delete the
56
- * analysis, for a game reviewed; delete the game, apart and in red. Scrolling closes it, which would leave its button.
56
+ * analysis, for a game reviewed or whose review was begun; delete the game, apart and in red. Scrolling closes it, which would leave its button.
57
57
  */
58
- function RowMenu({ anchor, reviewed, onPick, onClose, words }) {
58
+ function RowMenu({ anchor, withReview, onPick, onClose, words }) {
59
59
  const menu = useRef(null);
60
60
  useLayoutEffect(() => {
61
61
  const element = menu.current;
@@ -69,7 +69,7 @@ function RowMenu({ anchor, reviewed, onPick, onClose, words }) {
69
69
  window.removeEventListener("resize", onClose);
70
70
  };
71
71
  }, []);
72
- return (_jsxs("div", { ref: menu, class: "row-menu", popover: "manual", children: [reviewed
72
+ return (_jsxs("div", { ref: menu, class: "row-menu", popover: "manual", children: [withReview
73
73
  ? (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", onClick: () => onPick("redo"), children: [_jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M19 12A7 7 0 1 1 16.5 6.6M17 3V7H13" }) }), _jsxs("span", { children: [words.redo, _jsx("small", { children: words.redoNote })] })] }), _jsxs("button", { type: "button", onClick: () => onPick("unreview"), children: [_jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M5 19H19M8 15L4.5 11.5L12 4L18 10L11 17H8ZM8.5 7.5L14.5 13.5" }) }), _jsxs("span", { children: [words.unreview, _jsx("small", { children: words.unreviewNote })] })] }), _jsx("hr", {})] }))
74
74
  : null, _jsxs("button", { class: "danger", type: "button", onClick: () => onPick("remove"), children: [_jsx(TrashIcon, {}), _jsx("span", { children: words.remove })] })] }));
75
75
  }
@@ -3,6 +3,9 @@ import { useEffect, useRef } from "preact/hooks";
3
3
  /** A dialog over the page, as the settings' one: Esc and a click outside close it; a step inside it goes back with the arrow beside its title. */
4
4
  export function Dialog({ open, title, onBack, onClose, text, children }) {
5
5
  const dialog = useRef(null);
6
+ // Open as the page wants it: a dialog the page closed itself, to open another, does not close that one when the browser says it closed.
7
+ const wanted = useRef(open);
8
+ wanted.current = open;
6
9
  useEffect(() => {
7
10
  const element = dialog.current;
8
11
  if (open && !element.open) {
@@ -22,5 +25,5 @@ export function Dialog({ open, title, onBack, onClose, text, children }) {
22
25
  onClose();
23
26
  }
24
27
  };
25
- return (_jsxs("dialog", { ref: dialog, class: "dialog", "aria-label": title, onClose: onClose, onClick: closeOutside, children: [_jsxs("header", { children: [onBack === undefined ? null : (_jsx("button", { class: "icon-button", type: "button", title: text.profile.back, "aria-label": text.profile.back, onClick: onBack, children: _jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M15 6L9 12L15 18" }) }) })), _jsx("h2", { children: title }), _jsx("button", { class: "icon-button", type: "button", title: text.close, "aria-label": text.close, onClick: onClose, children: _jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M6 6L18 18M18 6L6 18" }) }) })] }), open ? children : null] }));
28
+ return (_jsxs("dialog", { ref: dialog, class: "dialog", "aria-label": title, onClose: () => wanted.current && onClose(), onClick: closeOutside, children: [_jsxs("header", { children: [onBack === undefined ? null : (_jsx("button", { class: "icon-button", type: "button", title: text.profile.back, "aria-label": text.profile.back, onClick: onBack, children: _jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M15 6L9 12L15 18" }) }) })), _jsx("h2", { children: title }), _jsx("button", { class: "icon-button", type: "button", title: text.close, "aria-label": text.close, onClick: onClose, children: _jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M6 6L18 18M18 6L6 18" }) }) })] }), open ? children : null] }));
26
29
  }
@@ -1005,12 +1005,6 @@ h3 + .switch {
1005
1005
  outline-offset: 2px;
1006
1006
  }
1007
1007
 
1008
- .rating {
1009
- display: grid;
1010
- grid-template-columns: 1fr 1fr;
1011
- gap: 8px;
1012
- }
1013
-
1014
1008
  .profile-form .start {
1015
1009
  width: 100%;
1016
1010
  padding: 13px;
@@ -1089,13 +1083,53 @@ h3 + .switch {
1089
1083
  height: 32px;
1090
1084
  }
1091
1085
 
1092
- /* The names on Lichess and chess.com of the player in use, under their line: the site, the name, a tick once saved (D-93) */
1093
- .dialog .accounts {
1086
+ /* The player in use, with the pencil that opens their profile beside their line */
1087
+ .profile-row {
1088
+ display: flex;
1089
+ gap: 6px;
1090
+ }
1091
+
1092
+ .profile-row .choice {
1093
+ flex: 1;
1094
+ }
1095
+
1096
+ .profile-row .edit-profile {
1097
+ flex: none;
1098
+ align-self: center;
1099
+ }
1100
+
1101
+ /* The profile window: the name with its tick once saved, as the names on the sites have */
1102
+ .profile-edit .field {
1094
1103
  display: grid;
1095
- grid-template-columns: auto 1fr 18px;
1104
+ grid-template-columns: 1fr 18px;
1105
+ align-items: center;
1106
+ gap: 0 10px;
1107
+ }
1108
+
1109
+ .profile-edit .field > span {
1110
+ grid-column: 1 / -1;
1111
+ }
1112
+
1113
+ .profile-edit .field .in-use {
1114
+ width: 18px;
1115
+ height: 18px;
1116
+ color: var(--accent);
1117
+ }
1118
+
1119
+ .profile-edit input[type="text"]:invalid {
1120
+ border-color: var(--low-time);
1121
+ }
1122
+
1123
+ /* The names on Lichess and chess.com: the site and the name; in the profile window the rating read there, and a tick once saved (D-93, D-105) */
1124
+ .accounts {
1125
+ display: grid;
1126
+ grid-template-columns: auto 1fr;
1096
1127
  align-items: center;
1097
1128
  gap: 6px 10px;
1098
- margin: -4px 0 12px;
1129
+ }
1130
+
1131
+ .accounts.read {
1132
+ grid-template-columns: auto 1fr minmax(4ch, auto) 18px;
1099
1133
  }
1100
1134
 
1101
1135
  .accounts label {
@@ -1132,6 +1166,46 @@ h3 + .switch {
1132
1166
  grid-column: 1 / -1;
1133
1167
  }
1134
1168
 
1169
+ /* The rating a site gives, its speed in its title; a cross for a name it does not know, a dash or the rating soft when it did not answer, dots while read */
1170
+ .accounts .site-mark {
1171
+ color: var(--ink);
1172
+ font-size: 15px;
1173
+ font-weight: 600;
1174
+ font-variant-numeric: tabular-nums;
1175
+ text-align: right;
1176
+ }
1177
+
1178
+ .accounts .site-mark span {
1179
+ color: inherit;
1180
+ font-size: inherit;
1181
+ }
1182
+
1183
+ .accounts .site-mark.missing {
1184
+ color: var(--low-time);
1185
+ }
1186
+
1187
+ .accounts .site-mark.unreached,
1188
+ .accounts .site-mark.pending {
1189
+ color: var(--soft);
1190
+ }
1191
+
1192
+ /* The cross and the dash in words, under the names */
1193
+ .site-problem {
1194
+ margin: 6px 0 0;
1195
+ color: var(--low-time);
1196
+ font-size: 13px;
1197
+ }
1198
+
1199
+ .site-problem.unreached {
1200
+ color: var(--soft);
1201
+ }
1202
+
1203
+ .privacy {
1204
+ margin: 8px 0 0;
1205
+ color: var(--soft);
1206
+ font-size: 13px;
1207
+ }
1208
+
1135
1209
  .dialog .new-profile {
1136
1210
  border-color: var(--line);
1137
1211
  color: var(--ink);