chesspal 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. package/README.md +45 -82
  2. package/dist/cli/chat-hook.js +1 -1
  3. package/dist/cli/command-line.d.ts +1 -2
  4. package/dist/cli/command-line.js +2 -3
  5. package/dist/commands/agents.d.ts +26 -0
  6. package/dist/commands/agents.js +72 -0
  7. package/dist/commands/ai-move.d.ts +5 -14
  8. package/dist/commands/ai-move.js +28 -35
  9. package/dist/commands/ai-say.d.ts +3 -1
  10. package/dist/commands/ai-say.js +24 -6
  11. package/dist/commands/analyse-game.d.ts +4 -0
  12. package/dist/commands/analyse-game.js +43 -16
  13. package/dist/commands/annotate-game.d.ts +4 -0
  14. package/dist/commands/annotate-game.js +65 -12
  15. package/dist/commands/answer-lines.d.ts +28 -0
  16. package/dist/commands/answer-lines.js +144 -0
  17. package/dist/commands/coach.d.ts +45 -0
  18. package/dist/commands/coach.js +344 -0
  19. package/dist/commands/context.d.ts +16 -5
  20. package/dist/commands/context.js +1 -0
  21. package/dist/commands/delete-game.d.ts +7 -0
  22. package/dist/commands/delete-game.js +30 -0
  23. package/dist/commands/delete-review.d.ts +8 -0
  24. package/dist/commands/delete-review.js +34 -0
  25. package/dist/commands/drawings.d.ts +0 -2
  26. package/dist/commands/drawings.js +5 -3
  27. package/dist/commands/end-game.js +5 -2
  28. package/dist/commands/export-game.d.ts +1 -1
  29. package/dist/commands/export-game.js +4 -3
  30. package/dist/commands/game-summary.d.ts +21 -38
  31. package/dist/commands/game-summary.js +30 -167
  32. package/dist/commands/index.d.ts +15 -0
  33. package/dist/commands/index.js +47 -2
  34. package/dist/commands/init.d.ts +3 -3
  35. package/dist/commands/init.js +4 -4
  36. package/dist/commands/lead-input.d.ts +8 -0
  37. package/dist/commands/lead-input.js +21 -0
  38. package/dist/commands/look.js +4 -1
  39. package/dist/commands/new-game.d.ts +2 -3
  40. package/dist/commands/new-game.js +7 -4
  41. package/dist/commands/play-move.d.ts +8 -15
  42. package/dist/commands/play-move.js +5 -13
  43. package/dist/commands/player-move.d.ts +2 -6
  44. package/dist/commands/player-move.js +13 -6
  45. package/dist/commands/player-say.d.ts +1 -1
  46. package/dist/commands/player-say.js +1 -1
  47. package/dist/commands/ponder.d.ts +10 -0
  48. package/dist/commands/ponder.js +33 -21
  49. package/dist/commands/position.d.ts +1 -1
  50. package/dist/commands/position.js +3 -1
  51. package/dist/commands/profiles.d.ts +13 -4
  52. package/dist/commands/profiles.js +49 -15
  53. package/dist/commands/registry.d.ts +8 -3
  54. package/dist/commands/registry.js +12 -11
  55. package/dist/commands/say.d.ts +1 -1
  56. package/dist/commands/say.js +16 -16
  57. package/dist/commands/speech.d.ts +8 -0
  58. package/dist/commands/speech.js +20 -0
  59. package/dist/commands/status.d.ts +9 -3
  60. package/dist/commands/status.js +40 -15
  61. package/dist/commands/study-games.js +1 -1
  62. package/dist/commands/study.js +3 -19
  63. package/dist/core/facts.js +15 -15
  64. package/dist/core/game.d.ts +3 -1
  65. package/dist/core/game.js +8 -4
  66. package/dist/core/illegal.js +1 -1
  67. package/dist/core/index.d.ts +5 -4
  68. package/dist/core/index.js +5 -4
  69. package/dist/core/lead.d.ts +15 -0
  70. package/dist/core/lead.js +40 -0
  71. package/dist/core/levels.d.ts +1 -1
  72. package/dist/core/levels.js +1 -1
  73. package/dist/core/moods.d.ts +0 -2
  74. package/dist/core/moods.js +2 -2
  75. package/dist/core/openings.d.ts +0 -2
  76. package/dist/core/openings.js +1 -1
  77. package/dist/core/profile.js +1 -1
  78. package/dist/core/record.d.ts +1 -1
  79. package/dist/core/record.js +1 -1
  80. package/dist/core/review.d.ts +18 -3
  81. package/dist/core/review.js +52 -9
  82. package/dist/games/ai-model.d.ts +4 -0
  83. package/dist/games/ai-model.js +9 -0
  84. package/dist/games/analysis.d.ts +1 -1
  85. package/dist/games/archive.d.ts +25 -4
  86. package/dist/games/archive.js +99 -25
  87. package/dist/games/coach.d.ts +57 -0
  88. package/dist/games/coach.js +144 -0
  89. package/dist/games/current-game.d.ts +2 -0
  90. package/dist/games/current-game.js +7 -1
  91. package/dist/games/files.js +1 -0
  92. package/dist/games/index.d.ts +5 -4
  93. package/dist/games/index.js +5 -4
  94. package/dist/games/talk.d.ts +1 -5
  95. package/dist/games/talk.js +14 -14
  96. package/dist/home/open-folder.js +1 -1
  97. package/dist/host-files/{ponder-agents.d.ts → agents.d.ts} +10 -2
  98. package/dist/host-files/agents.js +53 -0
  99. package/dist/host-files/host-files.d.ts +7 -2
  100. package/dist/host-files/host-files.js +27 -12
  101. package/dist/host-files/index.d.ts +2 -2
  102. package/dist/host-files/index.js +3 -3
  103. package/dist/index.d.ts +1 -3
  104. package/dist/index.js +1 -0
  105. package/dist/page/app/anchor.js +88 -0
  106. package/dist/page/{app.js → app/app.js} +116 -231
  107. package/dist/page/app/keys.js +94 -0
  108. package/dist/page/app/phase.js +30 -0
  109. package/dist/page/app/served.js +73 -0
  110. package/dist/page/avatars/avatars.js +19 -0
  111. package/dist/page/avatars/thought.js +10 -0
  112. package/dist/page/base.css +160 -0
  113. package/dist/page/board/board.css +959 -0
  114. package/dist/page/board/board.js +125 -0
  115. package/dist/page/{explain.js → board/explain.js} +1 -1
  116. package/dist/page/board/marks.js +160 -0
  117. package/dist/page/board/motion.js +71 -0
  118. package/dist/page/board/pointer.js +239 -0
  119. package/dist/page/board/squares.js +26 -0
  120. package/dist/page/dialogs/analyses.js +195 -0
  121. package/dist/page/{dialog.js → dialogs/dialog.js} +1 -1
  122. package/dist/page/dialogs/dialogs.css +1228 -0
  123. package/dist/page/{new-game-dialog.js → dialogs/new-game.js} +3 -3
  124. package/dist/page/{profile-dialogs.js → dialogs/profiles.js} +40 -11
  125. package/dist/page/dialogs/search.js +31 -0
  126. package/dist/page/dialogs/settings.js +42 -0
  127. package/dist/page/dialogs/studied.js +39 -0
  128. package/dist/page/dialogs/study.js +51 -0
  129. package/dist/page/{badges.js → game/badges.js} +1 -1
  130. package/dist/page/{browse.js → game/browse.js} +1 -1
  131. package/dist/page/game/card-fold.js +40 -0
  132. package/dist/page/game/column.js +55 -0
  133. package/dist/page/{ending.js → game/ending.js} +2 -2
  134. package/dist/page/game/game.css +577 -0
  135. package/dist/page/{move-list.js → game/move-list.js} +10 -6
  136. package/dist/page/{plies.js → game/plies.js} +30 -10
  137. package/dist/page/game/result-tile.js +14 -0
  138. package/dist/page/helps/helps.css +689 -0
  139. package/dist/page/helps/helps.js +220 -0
  140. package/dist/page/helps/view.js +127 -0
  141. package/dist/page/layout/fold-tab.js +11 -0
  142. package/dist/page/{folds.js → layout/folds.js} +1 -1
  143. package/dist/page/{header.js → layout/header.js} +5 -4
  144. package/dist/page/layout/layout.css +834 -0
  145. package/dist/page/lead/lead.js +82 -0
  146. package/dist/page/lead/view.js +38 -0
  147. package/dist/page/main.js +2 -2
  148. package/dist/page/pieces/CREDITS.md +12 -29
  149. package/dist/page/pieces/capablanca-small.svg +20 -0
  150. package/dist/page/pieces/capablanca.svg +22 -0
  151. package/dist/page/pieces/nitida-small.svg +3 -2
  152. package/dist/page/pieces/nitida.svg +3 -2
  153. package/dist/page/{pieces.js → pieces/pieces.js} +2 -2
  154. package/dist/page/pieces/torretta-small.svg +15 -14
  155. package/dist/page/pieces/torretta.svg +17 -15
  156. package/dist/page/{player-card.js → players/card.js} +2 -6
  157. package/dist/page/{clock-time.js → players/clock-time.js} +2 -2
  158. package/dist/page/{profile.js → profile/profile.js} +9 -5
  159. package/dist/page/{settings.js → profile/settings.js} +5 -4
  160. package/dist/page/style.css +9 -4398
  161. package/dist/page/talk/talk.css +958 -0
  162. package/dist/page/{talk.js → talk/talk.js} +39 -46
  163. package/dist/page/text.js +137 -19
  164. package/dist/page/theme.css +292 -0
  165. package/dist/profiles/index.d.ts +1 -1
  166. package/dist/profiles/profiles.d.ts +15 -4
  167. package/dist/profiles/profiles.js +24 -7
  168. package/dist/server/bell.d.ts +10 -2
  169. package/dist/server/bell.js +28 -12
  170. package/dist/server/caller.d.ts +14 -3
  171. package/dist/server/caller.js +44 -11
  172. package/dist/server/mcp.d.ts +2 -3
  173. package/dist/server/mcp.js +53 -19
  174. package/dist/server/page/analyses.d.ts +23 -0
  175. package/dist/server/page/analyses.js +65 -0
  176. package/dist/server/page/files.d.ts +5 -0
  177. package/dist/server/page/files.js +92 -0
  178. package/dist/server/page/game.d.ts +6 -0
  179. package/dist/server/page/game.js +381 -0
  180. package/dist/server/{http.d.ts → page/http.d.ts} +13 -0
  181. package/dist/server/{http.js → page/http.js} +11 -0
  182. package/dist/server/{page.d.ts → page/page.d.ts} +8 -13
  183. package/dist/server/page/page.js +229 -0
  184. package/dist/server/{profile-routes.d.ts → page/profiles.d.ts} +2 -2
  185. package/dist/server/{profile-routes.js → page/profiles.js} +47 -18
  186. package/instructions/analysis.md +4 -0
  187. package/instructions/analyst-agent.md +8 -0
  188. package/instructions/analyst.md +3 -7
  189. package/instructions/coach.md +11 -0
  190. package/instructions/opponent.md +4 -4
  191. package/package.json +1 -1
  192. package/dist/host-files/ponder-agents.js +0 -40
  193. package/dist/page/avatars.js +0 -16
  194. package/dist/page/board.js +0 -566
  195. package/dist/page/fold-tab.js +0 -9
  196. package/dist/page/game-column.js +0 -44
  197. package/dist/page/helps-view.js +0 -51
  198. package/dist/page/helps.js +0 -90
  199. package/dist/page/pieces/cburnett-small.svg +0 -20
  200. package/dist/page/pieces/cburnett.svg +0 -20
  201. package/dist/page/settings-panel.js +0 -32
  202. package/dist/page/studied.js +0 -14
  203. package/dist/page/study-dialog.js +0 -58
  204. package/dist/server/analyses.d.ts +0 -17
  205. package/dist/server/analyses.js +0 -46
  206. package/dist/server/page.js +0 -657
  207. /package/dist/page/{torretta.js → avatars/torretta.js} +0 -0
  208. /package/dist/page/{opening-book.js → game/opening-book.js} +0 -0
  209. /package/dist/page/{model-name.js → players/model-name.js} +0 -0
  210. /package/dist/page/{sounds.js → sounds/sounds.js} +0 -0
  211. /package/dist/page/{voice.js → talk/voice.js} +0 -0
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
2
  import { useState } from "preact/hooks";
3
- import { aiLevels } from "../core/index.js";
3
+ import { aiLevels } from "../../core/index.js";
4
+ import { Piece } from "../pieces/pieces.js";
4
5
  import { Dialog } from "./dialog.js";
5
- import { Piece } from "./pieces.js";
6
6
  /** The clocks offered: long enough for an AI that takes 10 to 30 seconds a move (D-13), with the seconds added at each move. */
7
7
  export const clocks = ["10+5", "15+10", "30+20"];
8
8
  /** The dialog the + opens, with the choices of the game before; the game going on, if any, goes to the archive unfinished when the new one starts. */
@@ -10,7 +10,7 @@ export function NewGameDialog({ open, choice, going, onStart, onClose, text }) {
10
10
  return (_jsx(Dialog, { open: open, title: text.newGame, onClose: onClose, text: text, children: _jsx(NewGameForm, { choice: choice, going: going, onStart: onStart, text: text }) }));
11
11
  }
12
12
  const colours = ["white", "black", "random"];
13
- /** The colours by their king, the chosen set's silhouette in fixed colours, which both themes show (T-94); either colour by both kings. */
13
+ /** The colours by their king, the chosen set's silhouette in fixed colours, which both themes show; either colour by both kings. */
14
14
  const colourIcons = {
15
15
  white: _jsx(Piece, { letter: "K", small: true }),
16
16
  black: _jsx(Piece, { letter: "k", small: true }),
@@ -1,13 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-runtime";
2
2
  // The profiles on the page (D-23, D-45): the wizard that completes the first (D-71), the player's picture to change, and the menu that puts another in use or makes one.
3
3
  import { useEffect, useMemo, useState } from "preact/hooks";
4
- import { DrawnAvatar, PlayerPicture } from "./avatars.js";
4
+ import { DrawnAvatar, PlayerPicture } from "../avatars/avatars.js";
5
+ import { Torretta } from "../avatars/torretta.js";
6
+ import { avatarIds, chooseAvatar, imageAddress, levels, makeProfile, nameLimit, openDataFolder, ratingLimits, ratingSites, saveAccounts, sendPhoto, useProfile, } from "../profile/profile.js";
5
7
  import { Dialog } from "./dialog.js";
6
- import { avatarIds, chooseAvatar, imageAddress, levels, makeProfile, nameLimit, openDataFolder, ratingLimits, ratingSites, sendPhoto, useProfile } from "./profile.js";
7
- import { Torretta } from "./torretta.js";
8
8
  /**
9
9
  * The page at the first start, while the profile Chesspal made is new: who plays, and the game comes after (D-23, D-71); a game played before stays in it.
10
- * The system's account is only a suggestion for the name (T-120), and the page says where the games go (T-119).
10
+ * The system's account is only a suggestion for the name, and the page says where the games go.
11
11
  */
12
12
  export function Wizard({ text, hasGame, account, dataFolder, onMade }) {
13
13
  return (_jsxs("main", { class: "wizard", children: [_jsx(Torretta, { size: 56 }), _jsx("h1", { children: text.profile.welcome }), _jsxs("p", { class: "lead", children: [text.profile.lead, hasGame ? _jsx("span", { children: text.profile.gameMoves }) : null] }), _jsx(ProfileForm, { text: text, submit: text.profile.start, suggestion: account, onMade: onMade }), _jsx(DataFolder, { folders: dataFolder, text: text })] }));
@@ -31,8 +31,11 @@ export function PictureDialog({ open, profile, onClose, text }) {
31
31
  };
32
32
  return (_jsxs(Dialog, { open: open, title: text.profile.changePicture, onClose: onClose, text: text, children: [_jsx("div", { class: "avatars", children: avatarIds.map((id) => (_jsx("button", { class: "avatar-choice", type: "button", title: text.profile.avatars[id], "aria-pressed": profile.image === undefined && profile.avatar === id, onClick: () => void chooseAvatar(profile.id, id).then(done), children: _jsx(DrawnAvatar, { id: id, size: 52, label: text.profile.avatars[id] }) }, id))) }), _jsx(PhotoChoice, { on: profile.image !== undefined, current: imageAddress(profile), onPhoto: (photo) => void sendPhoto(profile.id, photo).then(done), text: text }), problem === undefined ? null : _jsx("p", { class: "problem", role: "alert", children: problem })] }));
33
33
  }
34
- /** The menu of the players, open while `open`: another put in use, or a new one made, never while a game goes on. */
35
- export function ProfilesDialog({ open, profiles, active, playing, dataFolder, onClose, text }) {
34
+ /**
35
+ * The menu of the players, open while `open`: another put in use, never while a game goes on, or a new one made, which waits for it to end; and the names of the one in
36
+ * use on Lichess and chess.com (D-93).
37
+ */
38
+ export function ProfilesDialog({ open, profiles, active, locked, dataFolder, onClose, text }) {
36
39
  const [making, setMaking] = useState(false);
37
40
  const [problem, setProblem] = useState(undefined);
38
41
  useEffect(() => {
@@ -46,9 +49,35 @@ export function ProfilesDialog({ open, profiles, active, playing, dataFolder, on
46
49
  }
47
50
  void useProfile(id).then((refused) => (refused === undefined ? onClose() : setProblem(refusalText(refused, text))));
48
51
  };
49
- return (_jsx(Dialog, { open: open, title: making ? text.profile.newProfile : text.profile.profiles, onBack: making ? () => setMaking(false) : undefined, onClose: onClose, text: text, children: making ? _jsx(ProfileForm, { text: text, submit: text.profile.create, onMade: onClose }) : (_jsxs(_Fragment, { children: [_jsx("p", { class: "note", children: text.profile.eachOwn }), _jsx("div", { class: "profile-list", role: "radiogroup", "aria-label": text.profile.profiles, children: profiles.map((profile) => (_jsxs("button", { class: "choice", type: "button", role: "radio", title: profile.name, "aria-checked": profile.id === active.id, disabled: playing && profile.id !== active.id, onClick: () => use(profile.id), children: [_jsx(PlayerPicture, { profile: profile, size: 32 }), _jsx("span", { children: profile.name }), profile.id === active.id ? checkIcon : null] }, profile.id))) }), playing ? _jsx("p", { class: "note", children: text.profile.whilePlaying }) : null, _jsx("button", { class: "command new-profile", type: "button", disabled: playing, onClick: () => setMaking(true), children: text.profile.newProfile }), problem === undefined ? null : _jsx("p", { class: "problem", role: "alert", children: problem }), _jsx(DataFolder, { folders: dataFolder, text: text })] })) }));
52
+ return (_jsx(Dialog, { open: open, title: making ? text.profile.newProfile : text.profile.profiles, onBack: making ? () => setMaking(false) : undefined, onClose: onClose, text: text, children: making ? _jsx(ProfileForm, { text: text, submit: text.profile.create, use: locked === undefined, onMade: onClose }) : (_jsxs(_Fragment, { children: [_jsx("p", { class: "note", children: text.profile.eachOwn }), _jsx("div", { class: "profile-list", role: "radiogroup", "aria-label": text.profile.profiles, children: profiles.map((profile) => (_jsxs("button", { class: "choice", type: "button", role: "radio", title: profile.name, "aria-checked": profile.id === active.id, disabled: locked !== undefined && profile.id !== active.id, onClick: () => use(profile.id), children: [_jsx(PlayerPicture, { profile: profile, size: 32 }), _jsx("span", { children: profile.name }), profile.id === active.id ? checkIcon : null] }, profile.id))) }), _jsx(AccountsForm, { profile: active, text: text }, `${active.id} ${String(open)}`), locked === undefined ? null : _jsx("p", { class: "note", children: locked }), _jsx("button", { class: "command new-profile", type: "button", onClick: () => setMaking(true), children: text.profile.newProfile }), problem === undefined ? null : _jsx("p", { class: "problem", role: "alert", children: problem }), _jsx(DataFolder, { folders: dataFolder, text: text })] })) }));
53
+ }
54
+ /** The player's names on Lichess and chess.com, each saved once changed, with a tick beside it: a game loaded from there with that name is theirs (D-93). */
55
+ function AccountsForm({ profile, text }) {
56
+ const words = text.profile;
57
+ const [names, setNames] = useState({ lichess: profile.accounts?.lichess ?? "", chesscom: profile.accounts?.chesscom ?? "" });
58
+ const [saved, setSaved] = useState(undefined);
59
+ const [problem, setProblem] = useState(undefined);
60
+ const save = (site, field) => {
61
+ // A name the sites would not give stays in its field, marked, until it reads.
62
+ if (!field.validity.valid) {
63
+ setSaved(undefined);
64
+ return;
65
+ }
66
+ const changed = { ...names, [site]: field.value.trim() };
67
+ setNames(changed);
68
+ setSaved(undefined);
69
+ void saveAccounts(profile.id, changed).then((refused) => {
70
+ setProblem(refused === undefined ? undefined : refusalText(refused, text));
71
+ setSaved(refused === undefined ? site : undefined);
72
+ });
73
+ };
74
+ return (_jsxs("div", { class: "accounts", children: [accountSites.map((site) => (_jsxs("label", { title: words.accountTitle, children: [_jsx("span", { children: siteNames[site] }), _jsx("input", { type: "text",
75
+ // Not bound to the names saved: a name the sites would not give stays as typed, marked.
76
+ defaultValue: names[site], "aria-label": siteNames[site], placeholder: words.username, maxLength: nameLimit, pattern: "\\s*[\\w\\-]*\\s*", autocomplete: "off", spellcheck: false, onChange: (event) => save(site, event.currentTarget) }), saved === site ? checkIcon : _jsx("span", { class: "tick" })] }, site))), problem === undefined ? null : _jsx("p", { class: "problem", role: "alert", children: problem })] }));
50
77
  }
51
- /** Where the games are, as the file manager walks there, and a button that opens it there (T-119). */
78
+ const accountSites = ["lichess", "chesscom"];
79
+ const siteNames = { lichess: "Lichess", chesscom: "Chess.com" };
80
+ /** Where the games are, as the file manager walks there, and a button that opens it there. */
52
81
  function DataFolder({ folders, text }) {
53
82
  const words = text.profile;
54
83
  const [problem, setProblem] = useState(undefined);
@@ -71,8 +100,8 @@ function DataFolder({ folders, text }) {
71
100
  };
72
101
  return (_jsxs("div", { class: "data-folder", children: [_jsxs("p", { class: "note", children: [words.gamesIn, " ", _jsx("b", { children: shown })] }), _jsx("button", { class: "command", type: "button", disabled: opened, onClick: open, children: opened ? words.opened : words.openFolder }), problem === undefined ? null : _jsx("p", { class: "problem", role: "alert", children: problem })] }));
73
102
  }
74
- /** The form that makes a profile and puts it in use: the wizard's, with the system's account as a suggestion (T-120), and the menu's for another player. */
75
- function ProfileForm({ text, submit, suggestion, onMade }) {
103
+ /** The form that makes a profile and puts it in use: the wizard's, with the system's account as a suggestion, and the menu's for another player. */
104
+ function ProfileForm({ text, submit, suggestion, use = true, onMade }) {
76
105
  const words = text.profile;
77
106
  const [name, setName] = useState("");
78
107
  const [avatar, setAvatar] = useState(avatarIds[0]);
@@ -86,7 +115,7 @@ function ProfileForm({ text, submit, suggestion, onMade }) {
86
115
  event.preventDefault();
87
116
  setBusy(true);
88
117
  const value = rating.trim() === "" ? undefined : Number(rating);
89
- void makeProfile({ name, level, site, ...(value === undefined ? {} : { rating: value }), ...(photo === undefined ? { avatar } : { photo }) }).then((refused) => {
118
+ void makeProfile({ name, level, site, ...(value === undefined ? {} : { rating: value }), ...(photo === undefined ? { avatar } : { photo }), use }).then((refused) => {
90
119
  setBusy(false);
91
120
  setProblem(refused === undefined ? undefined : refusalText(refused, text));
92
121
  if (refused === undefined) {
@@ -0,0 +1,31 @@
1
+ // A full-text search over a list, as the player types it: any of the words found (OR), the items that match more of them first.
2
+ /**
3
+ * The items that match any word of the query, in their order, those matching more words first; all of them for an empty query. A word matches a part of
4
+ * the item's text, or a longer form of one of its words: «Siciliana» finds «Sicilian», «Italiana» «Italian».
5
+ */
6
+ export function searched(items, query, textOf) {
7
+ const wanted = wordsOf(query);
8
+ if (wanted.length === 0) {
9
+ return items;
10
+ }
11
+ return items
12
+ .map((item, index) => ({ item, index, found: matches(wordsOf(textOf(item)), wanted) }))
13
+ .filter(({ found }) => found > 0)
14
+ .sort((one, other) => other.found - one.found || one.index - other.index)
15
+ .map(({ item }) => item);
16
+ }
17
+ // A word of the query finds a longer form of the item's word only past this length, and only so many letters longer: «Sicilian» and «-a».
18
+ const stemLength = 5;
19
+ const endingLength = 2;
20
+ /** How many words of the query the item's words hold. */
21
+ function matches(words, wanted) {
22
+ const text = words.join(" ");
23
+ return wanted.filter((word) => text.includes(word) || words.some((one) => isFormOf(word, one))).length;
24
+ }
25
+ function isFormOf(word, stem) {
26
+ return stem.length >= stemLength && word.startsWith(stem) && word.length - stem.length <= endingLength;
27
+ }
28
+ /** The words of a text, lowercase and without accents, split at anything that is not a letter or a digit. */
29
+ function wordsOf(text) {
30
+ return text.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((word) => word !== "");
31
+ }
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-runtime";
2
+ import { useEffect, useRef } from "preact/hooks";
3
+ import { Piece } from "../pieces/pieces.js";
4
+ import { looks, pieceSets } from "../profile/settings.js";
5
+ /** The panel of the settings, open while `open` is true: every change goes to `onChange` at once. */
6
+ export function SettingsPanel({ open, settings, onChange, onClose, text }) {
7
+ const dialog = useRef(null);
8
+ useEffect(() => {
9
+ const element = dialog.current;
10
+ if (open && !element.open) {
11
+ element.showModal();
12
+ }
13
+ else if (!open && element.open) {
14
+ element.close();
15
+ }
16
+ }, [open]);
17
+ const set = (key, value) => onChange({ ...settings, [key]: value });
18
+ return (_jsxs("dialog", { ref: dialog, class: "dialog", "aria-label": text.settings, onClose: onClose, onClick: (event) => event.target === dialog.current && onClose(), children: [_jsxs("header", { children: [_jsx("h2", { children: text.settings }), _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" }) }) })] }), _jsxs("div", { class: "setting-columns", children: [_jsxs(Section, { title: text.look, icon: lookIcon, children: [_jsxs("div", { class: "setting-group", children: [_jsx("p", { class: "setting-label", children: text.themeChoice }), _jsx(Segments, { label: text.themeChoice, children: ["system", "light", "dark"].map((theme) => _jsx(Choice, { on: settings.theme === theme, onPick: () => set("theme", theme), children: text.themes[theme] }, theme)) })] }), _jsxs("div", { class: "setting-group", children: [_jsxs("p", { class: "setting-label", children: [text.boardSkin, " ", _jsx("b", { children: nameOf(settings.board, text) })] }), _jsx(Swatches, { look: "board", label: text.boardSkin, settings: settings, onPick: (id) => set("board", id), text: text })] }), _jsxs("div", { class: "setting-group", children: [_jsx("p", { class: "setting-label", children: text.pieceSet }), _jsx("div", { class: "piece-sets", role: "radiogroup", "aria-label": text.pieceSet, children: pieceSets.map((pieceSet) => (_jsxs(Choice, { on: settings.set === pieceSet, onPick: () => set("set", pieceSet), children: [_jsxs("span", { class: "sample", children: [_jsx(Piece, { letter: "N", set: pieceSet }), _jsx(Piece, { letter: "n", set: pieceSet })] }), nameOf(pieceSet, text)] }, pieceSet))) })] }), _jsxs("div", { class: "setting-group setting-pair", children: [_jsxs("div", { children: [_jsx("p", { class: "setting-label", children: text.pieceColours }), _jsx(Swatches, { look: "pieces", label: text.pieceColours, settings: settings, onPick: (id) => set("pieces", id), text: text })] }), _jsxs("div", { children: [_jsx("p", { class: "setting-label", children: text.marksColour }), _jsx(Swatches, { look: "marks", label: text.marksColour, settings: settings, onPick: (id) => set("marks", id), text: text })] })] })] }), _jsxs("div", { class: "setting-column", children: [_jsxs(Section, { title: text.play, icon: playIcon, children: [_jsx(Switch, { on: settings.dots, onSwitch: (on) => set("dots", on), children: text.dots }), _jsx(Switch, { on: settings.squareNames, onSwitch: (on) => set("squareNames", on), children: text.squareNames }), _jsx(Switch, { on: settings.explain, onSwitch: (on) => set("explain", on), children: text.explain }), _jsx(Switch, { on: settings.book, onSwitch: (on) => set("book", on), children: text.bookBadges }), _jsx(Switch, { on: settings.helps, onSwitch: (on) => set("helps", on), children: text.helps }), _jsx(Switch, { on: settings.sound, onSwitch: (on) => set("sound", on), children: text.sounds }), _jsx(Switch, { on: settings.voice, onSwitch: (on) => set("voice", on), children: text.voice })] }), _jsx(Section, { title: text.languageChoice, icon: languageIcon, children: _jsx(Segments, { label: text.languageChoice, children: ["auto", "it", "en"].map((language) => _jsx(Choice, { on: settings.language === language, onPick: () => set("language", language), children: text.languages[language] }, language)) }) })] })] })] }));
19
+ }
20
+ // The sections' line icons: a palette, a pawn, a globe.
21
+ const lookIcon = (_jsxs(_Fragment, { children: [_jsx("path", { d: "M12 3a9 9 0 1 0 0 18c1.2 0 1.8-.8 1.8-1.7 0-1.2-1-1.6-1-2.6 0-1 .8-1.7 1.8-1.7H17a4 4 0 0 0 4-4C21 6.5 17 3 12 3Z" }), _jsx("circle", { cx: "7.5", cy: "11", r: "1.2" }), _jsx("circle", { cx: "10", cy: "7", r: "1.2" }), _jsx("circle", { cx: "14.5", cy: "7", r: "1.2" })] }));
22
+ const playIcon = (_jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "6.5", r: "2.8" }), _jsx("path", { d: "M9.6 10.5h4.8M10.3 10.5c0 3-1.3 5.5-2.8 7.5h9c-1.5-2-2.8-4.5-2.8-7.5M6.5 20.5h11" })] }));
23
+ const languageIcon = (_jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "12", r: "9" }), _jsx("path", { d: "M3 12h18M12 3c2.5 2.5 3.6 5.5 3.6 9s-1.1 6.5-3.6 9c-2.5-2.5-3.6-5.5-3.6-9S9.5 5.5 12 3Z" })] }));
24
+ function Section({ title, icon, children }) {
25
+ return (_jsxs("section", { class: "setting-card", children: [_jsxs("h3", { children: [_jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: icon }), title] }), children] }));
26
+ }
27
+ /** The choices of a look, each a tile or a dot drawn by the style sheet with the look's own colours, its name in its title and for a screen reader. */
28
+ function Swatches({ look, label, settings, onPick, text }) {
29
+ return (_jsx("div", { class: look === "board" ? "swatches skins" : "swatches", role: "radiogroup", "aria-label": label, children: looks[look].map((id) => (_jsx("button", { class: "swatch", type: "button", role: "radio", "aria-checked": settings[look] === id, title: nameOf(id, text), "aria-label": nameOf(id, text), onClick: () => onPick(id), [`data-${look}`]: id, children: look === "board" ? [0, 1, 2, 3].map((square) => _jsx("i", {}, square)) : null }, id))) }));
30
+ }
31
+ function Segments({ label, children }) {
32
+ return _jsx("div", { class: "segments", role: "radiogroup", "aria-label": label, children: children });
33
+ }
34
+ function Choice({ on, onPick, children }) {
35
+ return (_jsx("button", { type: "button", role: "radio", "aria-checked": on, onClick: onPick, children: children }));
36
+ }
37
+ function Switch({ on, onSwitch, children }) {
38
+ return (_jsxs("label", { class: "switch", children: [_jsx("span", { children: children }), _jsx("input", { type: "checkbox", role: "switch", checked: on, onChange: (event) => onSwitch(event.currentTarget.checked) })] }));
39
+ }
40
+ function nameOf(id, text) {
41
+ return text.names[id] ?? id;
42
+ }
@@ -0,0 +1,39 @@
1
+ /** The player's side in a game in analysis: none when they played neither side, or both. */
2
+ export function playerIn(sides) {
3
+ const white = sides.white.who === "player";
4
+ return white === (sides.black.who === "player") ? undefined : white ? "white" : "black";
5
+ }
6
+ /**
7
+ * Both sides of a game in analysis, and the one at the bottom: the player's, when one of them is, else White. The player is the side named Player, or by
8
+ * their name, or by their name on the site the game was played on (D-93).
9
+ */
10
+ export function studiedSides(tags, you, ai, sides, accounts) {
11
+ const sideOf = (side) => {
12
+ const tag = (side === "white" ? tags.White : tags.Black)?.trim() ?? "";
13
+ if (tag === "Player" || tag.toLowerCase() === you.trim().toLowerCase()) {
14
+ return { name: you, who: "player" };
15
+ }
16
+ if (accountSide(tags, accounts) === side) {
17
+ return { name: tag, who: "player" };
18
+ }
19
+ // A tag PGN leaves unknown names the side.
20
+ return tag === "AI" ? { name: ai, who: "ai" } : { name: /^[?\-\s]*$/.test(tag) ? sides[side] : tag, who: "other" };
21
+ };
22
+ const white = sideOf("white");
23
+ const black = sideOf("black");
24
+ return { white, black, bottom: black.who === "player" && white.who !== "player" ? "black" : "white" };
25
+ }
26
+ /**
27
+ * The side a game loaded from elsewhere names by one of the player's names on the sites (D-93): the one of its site when the Site tag says Lichess or
28
+ * chess.com, either otherwise; none when neither side, or both, has one.
29
+ */
30
+ export function accountSide(tags, accounts) {
31
+ const site = (tags.Site ?? "").toLowerCase();
32
+ const names = [
33
+ site.includes("chess.com") ? undefined : accounts?.lichess,
34
+ site.includes("lichess") ? undefined : accounts?.chesscom,
35
+ ].filter((name) => name !== undefined).map((name) => name.trim().toLowerCase());
36
+ const white = names.includes(tags.White?.trim().toLowerCase() ?? "");
37
+ const black = names.includes(tags.Black?.trim().toLowerCase() ?? "");
38
+ return white === black ? undefined : white ? "white" : "black";
39
+ }
@@ -0,0 +1,51 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
+ // The dialog of the analyses (D-92): the list of the games analysed or brought to study, and a step that loads a game played elsewhere as a PGN, pasted or
3
+ // read from a file, which the server keeps to study and puts in analysis.
4
+ import { useEffect, useRef, useState } from "preact/hooks";
5
+ import { Analyses } from "./analyses.js";
6
+ import { Dialog } from "./dialog.js";
7
+ /**
8
+ * The dialog the header's button opens; `onStudy` sends the choice and gives back why the server refused it, or undefined once it took it. The games played
9
+ * here name their sides as their analysis does: the player by `you`, the AI by its model when the game keeps it (D-82), else by `ai`.
10
+ */
11
+ export function StudyDialog({ open, onStudy, onClose, you, accounts, ai, text }) {
12
+ const [loading, setLoading] = useState(false);
13
+ // Each opening starts from the list.
14
+ useEffect(() => {
15
+ if (!open) {
16
+ setLoading(false);
17
+ }
18
+ }, [open]);
19
+ return (_jsx(Dialog, { open: open, title: loading ? text.study.bring : text.study.title, onBack: loading ? () => setLoading(false) : undefined, onClose: onClose, text: text, children: loading ? _jsx(LoadForm, { onStudy: onStudy, text: text }) : _jsx(Analyses, { onStudy: onStudy, onLoad: () => setLoading(true), you: you, accounts: accounts, ai: ai, text: text }) }));
20
+ }
21
+ /** The step that loads a game played elsewhere: its PGN pasted, or read from a file. */
22
+ function LoadForm({ onStudy, text }) {
23
+ const words = text.study;
24
+ const [pgn, setPgn] = useState("");
25
+ const [problem, setProblem] = useState(undefined);
26
+ const [sending, setSending] = useState(false);
27
+ const field = useRef(null);
28
+ // With a mouse only: on a phone the keyboard would cover the step.
29
+ useEffect(() => {
30
+ if (matchMedia("(pointer: fine)").matches) {
31
+ field.current?.focus();
32
+ }
33
+ }, []);
34
+ const read = (file) => {
35
+ void file?.text().then((read) => {
36
+ setPgn(read);
37
+ setProblem(undefined);
38
+ });
39
+ };
40
+ return (_jsxs("form", { class: "profile-form study", onSubmit: (event) => {
41
+ event.preventDefault();
42
+ setSending(true);
43
+ void onStudy({ pgn }).then((refused) => {
44
+ setSending(false);
45
+ setProblem(refused);
46
+ });
47
+ }, children: [_jsxs("label", { class: "field", children: [_jsx("span", { children: words.paste }), _jsx("textarea", { ref: field, rows: 10, value: pgn, spellcheck: false, placeholder: '[White "…"]\n[Black "…"]\n\n1. e4 e5 2. Nf3 …', onInput: (event) => {
48
+ setPgn(event.currentTarget.value);
49
+ setProblem(undefined);
50
+ } })] }), _jsxs("label", { class: "field file", children: [_jsx("span", { children: words.file }), _jsx("input", { type: "file", accept: ".pgn,application/x-chess-pgn,text/plain", onChange: (event) => read(event.currentTarget.files?.[0]) })] }), _jsx("p", { class: "note", children: words.note }), problem === undefined ? null : _jsx("p", { class: "problem", role: "alert", children: problem }), _jsx("button", { class: "start", type: "submit", disabled: pgn.trim() === "" || sending, children: words.start })] }));
51
+ }
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-runtime";
2
- import { badges } from "../core/index.js";
2
+ import { badges } from "../../core/index.js";
3
3
  /** A move's badge, as an inline SVG of class `move-badge`: 16px in a line of text, a third of the square in a `.square`. With a title it is an image with
4
4
  * that name; without, it is hidden from screen readers, for the text beside it to say it. */
5
5
  export function BadgeIcon({ badge, title }) {
@@ -1,4 +1,4 @@
1
- // Looking back through the game played (T-88): one step at a time or to either end, with the keys of Lichess and chess.com, only to look at it.
1
+ // Looking back through the game played: one step at a time or to either end, with the keys of Lichess and chess.com, only to look at it.
2
2
  /** The keys that step through the moves, as Lichess and chess.com have them: the arrows, and Home and End. */
3
3
  export const stepKeys = {
4
4
  ArrowLeft: "back",
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
+ import { useState } from "preact/hooks";
3
+ /** Whether the card is folded, as this browser left it, and what folds it or opens it again. */
4
+ export function useCardFold(card) {
5
+ const [folded, setFolded] = useState(() => read(card));
6
+ const toggle = () => {
7
+ remember(card, !folded);
8
+ setFolded(!folded);
9
+ };
10
+ return [folded, toggle];
11
+ }
12
+ /** A card's title as the button that folds the card and opens it again: its icon and name, then a chevron, down while the card is open. */
13
+ export function FoldButton({ folded, onFold, text, children }) {
14
+ return (_jsxs("button", { type: "button", class: "card-fold", "aria-expanded": !folded, title: folded ? text.openCard : text.foldCard, onClick: onFold, children: [children, _jsx("svg", { class: "chevron", viewBox: "0 0 16 16", "aria-hidden": "true", children: _jsx("path", { d: folded ? "M6 4L10 8L6 12" : "M4 6L8 10L12 6" }) })] }));
15
+ }
16
+ function read(card) {
17
+ try {
18
+ return localStorage.getItem(keyOf(card)) === "folded";
19
+ }
20
+ catch {
21
+ // A browser that keeps nothing for the page: the card opens.
22
+ return false;
23
+ }
24
+ }
25
+ function remember(card, folded) {
26
+ try {
27
+ if (folded) {
28
+ localStorage.setItem(keyOf(card), "folded");
29
+ }
30
+ else {
31
+ localStorage.removeItem(keyOf(card));
32
+ }
33
+ }
34
+ catch {
35
+ // The choice lasts as long as the page.
36
+ }
37
+ }
38
+ function keyOf(card) {
39
+ return `chesspal-card-${card}`;
40
+ }
@@ -0,0 +1,55 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "preact/jsx-runtime";
2
+ import { outOfBookMove } from "../../core/index.js";
3
+ import { LeadBar, LeadChart } from "../lead/lead.js";
4
+ import { standingAt } from "../lead/view.js";
5
+ import { Piece } from "../pieces/pieces.js";
6
+ import { FoldButton, useCardFold } from "./card-fold.js";
7
+ import { MoveList, Steps } from "./move-list.js";
8
+ import { ResultTile } from "./result-tile.js";
9
+ /**
10
+ * The column, which the player folds with its tab; under the board, where three columns would leave the board small, always open. A game in analysis
11
+ * (D-69) says above the moves that the AI is going over it, until its word on the whole game comes, which the talk shows.
12
+ */
13
+ export function GameColumn({ plies, result, how, lit, started, move, toMove, opening, helps, standings, analysis, shownPlies, onShow, onStep, onDownload, onSaveMoves, onAnalyse, text }) {
14
+ const bar = standings === undefined ? undefined : _jsx(LeadBar, { standing: standingAt(standings, shownPlies), shown: shownPlies, text: text });
15
+ return (_jsx("aside", { class: "side", id: "game", "aria-label": text.game, children: started === undefined && analysis === undefined
16
+ ? (_jsxs(_Fragment, { children: [_jsx("p", { class: "card empty", children: text.noGame }), onAnalyse === undefined ? null : _jsx(AnalyseButton, { label: text.study.analyseLast, onAnalyse: onAnalyse })] }))
17
+ : (_jsxs(_Fragment, { children: [_jsx(GameCard, { plies: plies, result: result, how: how, lit: lit, started: started, move: move, toMove: toMove, bar: bar, text: text }), analysis === undefined || standings === undefined ? null : _jsx(LeadChart, { plies: plies, standings: standings, shown: shownPlies, onShow: onShow, text: text }), opening === undefined ? null : _jsx(OpeningCard, { opening: opening, text: text }), helps, analysis === undefined || analysis.summary !== undefined ? null : (_jsxs("p", { class: "card review waiting", role: "status", children: [_jsx("span", { class: "spinner", "aria-hidden": "true" }), text.reviewing(analysis.by)] })), _jsx(MoveList, { plies: plies, result: result, shown: shownPlies, onShow: onShow, text: text }), onDownload === undefined && onSaveMoves === undefined ? null : (_jsxs("div", { class: "card analysis-actions", children: [onDownload === undefined ? null : _jsx(SaveButton, { label: text.study.download, hint: text.study.downloadHint, onSave: onDownload }), onSaveMoves === undefined ? null : _jsx(SaveButton, { label: text.saveMoves, hint: text.saveMovesHint, onSave: onSaveMoves })] })), onAnalyse === undefined ? null : _jsx(AnalyseButton, { label: text.study.analyseIt, onAnalyse: onAnalyse }), _jsx(Steps, { shown: shownPlies, total: plies.length, onStep: onStep, text: text })] })) }));
18
+ }
19
+ /** A button that saves the game as a file in the player's downloads, its tooltip saying what the file holds. */
20
+ function SaveButton({ label, hint, onSave }) {
21
+ return (_jsxs("button", { type: "button", title: hint, onClick: onSave, children: [_jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M12 4V15M7 10L12 15L17 10M5 19H19" }) }), label] }));
22
+ }
23
+ /** The button that opens the game over in analysis. */
24
+ function AnalyseButton({ label, onAnalyse }) {
25
+ return (_jsx("div", { class: "card analysis-actions", children: _jsxs("button", { type: "button", onClick: onAnalyse, children: [_jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M12 4V20M8 20H16M5 7H19M5 7L2.5 13A2.5 2 0 0 0 7.5 13ZM19 7L16.5 13A2.5 2 0 0 0 21.5 13Z" }) }), label] }) }));
26
+ }
27
+ /**
28
+ * The game's card, as tall as a player's: a little board of four squares with the king to move, the move and when the game started; at the end, the result on the
29
+ * square, the winner's king standing over the loser's, laid down, or both standing in a draw, how it ended and how many moves it lasted; lit up a moment when the
30
+ * game ends under the player's eyes.
31
+ */
32
+ function GameCard({ plies, result, how, lit, started, move, toMove, bar, text }) {
33
+ const when = started === undefined ? undefined : new Date(started).toLocaleString(text.language, { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" });
34
+ if (result === undefined) {
35
+ return (_jsxs("section", { class: "card game-card", "aria-label": text.game, children: [_jsx("span", { class: "tile moving", "aria-hidden": "true", children: _jsx(Piece, { letter: toMove === "white" ? "K" : "k" }) }), _jsxs("div", { class: "what", children: [_jsxs("p", { class: "head", children: [text.move, " ", _jsx("b", { class: "big", children: move }), _jsxs("span", { class: "visually-hidden", children: [", ", text.toMove(toMove)] })] }), when === undefined ? null : _jsx("p", { class: "when", children: text.started(when) }), bar] })] }));
36
+ }
37
+ const winner = result === "1-0" ? "white" : result === "0-1" ? "black" : undefined;
38
+ const first = plies[0];
39
+ const last = plies.at(-1);
40
+ const lasted = first === undefined || last === undefined ? 0 : last.number - first.number + 1;
41
+ return (_jsxs("section", { class: `card game-card${lit ? " lit" : ""}`, "aria-label": text.game, children: [_jsx(ResultTile, { result: winner === undefined ? "1/2-1/2" : result }), _jsxs("div", { class: "what", children: [_jsxs("p", { class: "head", children: [_jsx("b", { class: "big", children: result === "1/2-1/2" ? "½–½" : result.replace("-", "–") }), " \u00B7 ", winner === undefined ? text.draw : text.won(winner)] }), _jsxs("p", { class: "when", children: [how === undefined ? "" : `${text.ended[how]} · `, text.lasted(lasted)] }), bar] })] }));
42
+ }
43
+ /**
44
+ * The opening by Lichess's book: a book, crossed out once the game left it, and the family, which fold the card to them (Stefano); the variation under them,
45
+ * and the move the game left the book at.
46
+ */
47
+ function OpeningCard({ opening, text }) {
48
+ const [foldedHere, toggleFold] = useCardFold("opening");
49
+ const book = opening.inBook ? text.inBook : `${text.outOfBook} ${outOfBookMove(opening)}`;
50
+ // The family alone has nothing under it to fold: no button, no chevron (Stefano).
51
+ const foldable = opening.variation !== undefined || !opening.inBook;
52
+ const folded = foldable && foldedHere;
53
+ const title = (_jsxs(_Fragment, { children: [_jsx("span", { class: `book${opening.inBook ? "" : " out"}`, title: book, role: "img", "aria-label": book, children: _jsxs("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: [_jsx("path", { d: "M12 6.5C10 5 7 4.5 4 5V18C7 17.5 10 18 12 19.5C14 18 17 17.5 20 18V5C17 4.5 14 5 12 6.5ZM12 6.5V19.5" }), opening.inBook ? null : _jsx("path", { d: "M3.5 3.5L20.5 20.5" })] }) }), _jsx("b", { children: opening.family })] }));
54
+ return (_jsxs("section", { class: `card opening${folded ? " folded" : ""}`, "aria-label": text.opening, children: [_jsx("h3", { class: "family", children: foldable ? _jsx(FoldButton, { folded: folded, onFold: toggleFold, text: text, children: title }) : title }), folded || opening.variation === undefined ? null : _jsx("p", { class: "variation", children: opening.variation }), folded || opening.inBook ? null : _jsxs("p", { class: "out", children: [text.outOfBook, " ", outOfBookMove(opening)] })] }));
55
+ }
@@ -1,4 +1,4 @@
1
- // How a game ended, as the page names it (T-77): by the board, or by a resignation or a draw agreed, which only the record's Termination tag says (D-37).
1
+ // How a game ended, as the page names it: by the board, or by a resignation or a draw agreed, which only the record's Termination tag says (D-37).
2
2
  import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
3
3
  /** How the game ended, from the board or the record's Termination tag; undefined while it goes on, or when the record does not say. */
4
4
  export function howEnded(outcome, termination) {
@@ -10,7 +10,7 @@ export function howEnded(outcome, termination) {
10
10
  if (/^draw agreed$|by agreement|per accordo/i.test(termination ?? "")) {
11
11
  return "agreement";
12
12
  }
13
- // A flag that fell (T-37): "Black lost on time", "Black ran out of time and White cannot mate".
13
+ // A flag that fell: "Black lost on time", "Black ran out of time and White cannot mate".
14
14
  if (/ (lost on|ran out of|won on) time\b|time forfeit|per tempo/i.test(termination ?? "")) {
15
15
  return "time";
16
16
  }