chesspal 0.3.5 → 0.4.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 (191) hide show
  1. package/README.md +1 -0
  2. package/dist/cli/cli.js +17 -4
  3. package/dist/cli/command-line.js +22 -12
  4. package/dist/cli/icon.d.ts +2 -0
  5. package/dist/cli/icon.js +11 -1
  6. package/dist/cli/prompt-hook.d.ts +17 -0
  7. package/dist/cli/prompt-hook.js +100 -0
  8. package/dist/commands/analysis/analyse.d.ts +2 -2
  9. package/dist/commands/analysis/analyse.js +10 -16
  10. package/dist/commands/analysis/annotate.d.ts +8 -0
  11. package/dist/commands/analysis/annotate.js +19 -3
  12. package/dist/commands/answers/agents.d.ts +17 -7
  13. package/dist/commands/answers/agents.js +46 -16
  14. package/dist/commands/answers/clock.js +2 -1
  15. package/dist/commands/answers/drawings.js +6 -2
  16. package/dist/commands/answers/index.d.ts +4 -3
  17. package/dist/commands/answers/index.js +4 -3
  18. package/dist/commands/answers/lines.d.ts +5 -3
  19. package/dist/commands/answers/lines.js +17 -9
  20. package/dist/commands/answers/opening-book.d.ts +13 -7
  21. package/dist/commands/answers/opening-book.js +34 -38
  22. package/dist/commands/answers/opening-tags.d.ts +4 -0
  23. package/dist/commands/answers/opening-tags.js +11 -0
  24. package/dist/commands/answers/play-move.d.ts +3 -1
  25. package/dist/commands/answers/play-move.js +9 -6
  26. package/dist/commands/answers/say.js +2 -1
  27. package/dist/commands/answers/summary.d.ts +7 -3
  28. package/dist/commands/answers/summary.js +16 -5
  29. package/dist/commands/answers/view.d.ts +1 -1
  30. package/dist/commands/answers/view.js +2 -2
  31. package/dist/commands/game/ai-move.js +4 -4
  32. package/dist/commands/game/coach.js +7 -6
  33. package/dist/commands/game/end.js +2 -2
  34. package/dist/commands/game/export.d.ts +1 -1
  35. package/dist/commands/game/index.d.ts +1 -1
  36. package/dist/commands/game/index.js +1 -1
  37. package/dist/commands/game/look.d.ts +7 -3
  38. package/dist/commands/game/look.js +9 -4
  39. package/dist/commands/game/new.js +42 -12
  40. package/dist/commands/game/player-move.d.ts +16 -0
  41. package/dist/commands/game/player-move.js +69 -19
  42. package/dist/commands/game/ponder.js +7 -4
  43. package/dist/commands/game/position.d.ts +2 -1
  44. package/dist/commands/game/position.js +9 -4
  45. package/dist/commands/game/status.d.ts +4 -2
  46. package/dist/commands/game/status.js +24 -14
  47. package/dist/commands/index.d.ts +3 -3
  48. package/dist/commands/index.js +3 -3
  49. package/dist/commands/profiles/past-games.d.ts +7 -2
  50. package/dist/commands/profiles/past-games.js +45 -18
  51. package/dist/commands/registry/context.d.ts +2 -0
  52. package/dist/commands/registry/context.js +5 -0
  53. package/dist/commands/registry/index.d.ts +1 -1
  54. package/dist/commands/registry/index.js +1 -1
  55. package/dist/commands/talk/ai-say.d.ts +1 -1
  56. package/dist/commands/talk/ai-say.js +7 -6
  57. package/dist/core/game.d.ts +13 -0
  58. package/dist/core/game.js +29 -3
  59. package/dist/core/index.d.ts +4 -4
  60. package/dist/core/index.js +3 -3
  61. package/dist/core/openings.d.ts +20 -6
  62. package/dist/core/openings.js +45 -22
  63. package/dist/core/pgn.d.ts +4 -0
  64. package/dist/core/pgn.js +35 -6
  65. package/dist/games/analysis.d.ts +7 -4
  66. package/dist/games/analysis.js +5 -3
  67. package/dist/games/archive.d.ts +25 -5
  68. package/dist/games/archive.js +52 -19
  69. package/dist/games/chat-start.d.ts +2 -2
  70. package/dist/games/chat-start.js +6 -1
  71. package/dist/games/current-game.d.ts +2 -2
  72. package/dist/games/current-game.js +3 -2
  73. package/dist/games/index.d.ts +3 -2
  74. package/dist/games/index.js +3 -2
  75. package/dist/games/position-shown.d.ts +24 -0
  76. package/dist/games/position-shown.js +51 -0
  77. package/dist/games/thoughts.d.ts +12 -4
  78. package/dist/games/thoughts.js +27 -10
  79. package/dist/home/index.d.ts +1 -0
  80. package/dist/home/index.js +1 -0
  81. package/dist/home/same-path.d.ts +7 -0
  82. package/dist/home/same-path.js +24 -0
  83. package/dist/host-files/host-files.d.ts +5 -0
  84. package/dist/host-files/host-files.js +21 -7
  85. package/dist/host-files/index.d.ts +1 -1
  86. package/dist/host-files/index.js +1 -1
  87. package/dist/page/app/anchor.js +15 -11
  88. package/dist/page/app/app.js +49 -14
  89. package/dist/page/app/served.js +30 -38
  90. package/dist/page/app/watch.js +97 -0
  91. package/dist/page/avatars/torretta.css +108 -0
  92. package/dist/page/base.css +1 -1
  93. package/dist/page/board/board.css +11 -8
  94. package/dist/page/board/marks.js +2 -2
  95. package/dist/page/board/pointer.js +2 -2
  96. package/dist/page/dialogs/analyses.js +5 -3
  97. package/dist/page/dialogs/dialogs.css +3 -1
  98. package/dist/page/dialogs/rows.js +72 -0
  99. package/dist/page/dialogs/site-games.js +3 -1
  100. package/dist/page/game/column.js +10 -8
  101. package/dist/page/game/game.css +137 -16
  102. package/dist/page/game/move-list.js +7 -5
  103. package/dist/page/game/plies.js +4 -0
  104. package/dist/page/game/review-progress.js +11 -0
  105. package/dist/page/guide/drawings/drawings.css +0 -1
  106. package/dist/page/guide/en.md +2 -2
  107. package/dist/page/guide/it.md +2 -2
  108. package/dist/page/helps/helps.css +5 -5
  109. package/dist/page/helps/helps.js +3 -181
  110. package/dist/page/helps/rows.js +16 -0
  111. package/dist/page/helps/strategy.js +105 -0
  112. package/dist/page/helps/tactics.js +69 -0
  113. package/dist/page/layout/header.js +49 -6
  114. package/dist/page/layout/icon.ico +0 -0
  115. package/dist/page/layout/layout.css +49 -17
  116. package/dist/page/layout/window.js +45 -6
  117. package/dist/page/lead/lead.js +41 -4
  118. package/dist/page/legend/legend.css +4 -16
  119. package/dist/page/legend/legend.js +28 -15
  120. package/dist/page/profile/settings.js +1 -0
  121. package/dist/page/sounds/sounds.js +1 -1
  122. package/dist/page/style.css +1 -0
  123. package/dist/page/table/action.js +42 -0
  124. package/dist/page/table/table.css +57 -39
  125. package/dist/page/table/window.js +67 -67
  126. package/dist/page/talk/talk.css +52 -118
  127. package/dist/page/talk/talk.js +65 -20
  128. package/dist/page/talk/voice.js +92 -21
  129. package/dist/page/text.js +34 -8
  130. package/dist/page/theme.css +13 -0
  131. package/dist/profiles/profiles.js +5 -1
  132. package/dist/server/bell/bell.d.ts +24 -29
  133. package/dist/server/bell/bell.js +79 -133
  134. package/dist/server/bell/index.d.ts +1 -1
  135. package/dist/server/bell/index.js +2 -2
  136. package/dist/server/mcp/mcp.js +49 -7
  137. package/dist/server/page/analyses.js +15 -4
  138. package/dist/server/page/chat.d.ts +5 -1
  139. package/dist/server/page/chat.js +5 -1
  140. package/dist/server/page/files.js +3 -1
  141. package/dist/server/page/game.d.ts +5 -0
  142. package/dist/server/page/game.js +95 -15
  143. package/dist/server/page/http.d.ts +1 -1
  144. package/dist/server/page/page.d.ts +14 -9
  145. package/dist/server/page/page.js +111 -26
  146. package/dist/server/page/table.d.ts +40 -5
  147. package/dist/server/page/table.js +65 -25
  148. package/dist/server/page/update.d.ts +1 -1
  149. package/dist/server/page/update.js +4 -4
  150. package/dist/server/page/windows.d.ts +22 -0
  151. package/dist/server/page/windows.js +45 -0
  152. package/dist/server/ports/index.d.ts +1 -0
  153. package/dist/server/ports/index.js +2 -0
  154. package/dist/server/ports/ports.d.ts +78 -0
  155. package/dist/server/ports/ports.js +222 -0
  156. package/dist/server/table/browser.d.ts +22 -6
  157. package/dist/server/table/browser.js +98 -20
  158. package/dist/server/table/hosts/chats.d.ts +5 -0
  159. package/dist/server/table/hosts/chats.js +11 -4
  160. package/dist/server/table/hosts/hooks.d.ts +6 -0
  161. package/dist/server/table/hosts/hooks.js +38 -0
  162. package/dist/server/table/hosts/hosts.d.ts +13 -5
  163. package/dist/server/table/hosts/hosts.js +16 -10
  164. package/dist/server/table/hosts/index.d.ts +1 -0
  165. package/dist/server/table/hosts/index.js +1 -0
  166. package/dist/server/table/hosts/play.d.ts +7 -3
  167. package/dist/server/table/hosts/play.js +63 -9
  168. package/dist/server/table/icon/index.d.ts +2 -0
  169. package/dist/server/table/icon/index.js +16 -2
  170. package/dist/server/table/index.d.ts +2 -2
  171. package/dist/server/table/index.js +1 -1
  172. package/dist/server/table/launch.d.ts +5 -4
  173. package/dist/server/table/launch.js +18 -5
  174. package/dist/server/table/link.js +4 -1
  175. package/dist/server/table/table.d.ts +7 -0
  176. package/dist/server/table/table.js +40 -12
  177. package/dist/sites/index.d.ts +1 -1
  178. package/dist/sites/index.js +1 -1
  179. package/dist/sites/{chesscom.d.ts → ratings/chesscom.d.ts} +2 -2
  180. package/dist/sites/{chesscom.js → ratings/chesscom.js} +1 -1
  181. package/dist/sites/{choice.d.ts → ratings/choice.d.ts} +1 -1
  182. package/dist/sites/{choice.js → ratings/choice.js} +1 -1
  183. package/dist/sites/ratings/index.d.ts +1 -0
  184. package/dist/sites/ratings/index.js +2 -0
  185. package/dist/sites/{lichess.d.ts → ratings/lichess.d.ts} +2 -2
  186. package/dist/sites/{lichess.js → ratings/lichess.js} +1 -1
  187. package/dist/sites/{sites.d.ts → ratings/ratings.d.ts} +2 -2
  188. package/dist/sites/{sites.js → ratings/ratings.js} +1 -1
  189. package/instructions/opponent.md +1 -1
  190. package/instructions/start.md +1 -1
  191. package/package.json +1 -1
@@ -120,7 +120,7 @@ button {
120
120
 
121
121
  @media (prefers-reduced-motion: reduce) {
122
122
  .spinner {
123
- animation-duration: 3s;
123
+ animation-duration: 3000ms;
124
124
  }
125
125
  }
126
126
 
@@ -375,23 +375,23 @@
375
375
  }
376
376
 
377
377
  .badge.crown {
378
- background: #e0a526;
379
- color: #3b2a00;
378
+ background: var(--king-crown);
379
+ color: var(--king-crown-ink);
380
380
  }
381
381
 
382
382
  /* The mate white on the red square, edged in red */
383
383
  .badge.mate {
384
384
  background: #fff;
385
- color: #c81e1e;
386
- box-shadow: 0 0 0 max(1.5px, 0.25cqw) #c81e1e, 0 0.2cqw 0.6cqw rgba(0, 0, 0, 0.4);
385
+ color: var(--king-mate);
386
+ box-shadow: 0 0 0 max(1.5px, 0.25cqw) var(--king-mate), 0 0.2cqw 0.6cqw rgba(0, 0, 0, 0.4);
387
387
  }
388
388
 
389
389
  .badge.flag {
390
- background: #4a4f57;
390
+ background: var(--king-flag);
391
391
  }
392
392
 
393
393
  .badge.half {
394
- background: #69727d;
394
+ background: var(--king-draw);
395
395
  }
396
396
 
397
397
  /* A move's badge (D-69): 16px beside a move, a third of a square in the corner of its arrival, edged in white as the kings' badges */
@@ -789,7 +789,7 @@
789
789
  .notice.right,
790
790
  .notice.left {
791
791
  top: calc((var(--row) + 0.5) * 12.5cqw);
792
- max-width: min(calc((var(--room) * 12.5cqw) - 12px), 340px);
792
+ max-width: min(calc(var(--room) * 12.5cqw - 12px), 340px);
793
793
  }
794
794
 
795
795
  .notice.right {
@@ -842,6 +842,7 @@
842
842
  left: 50%;
843
843
  top: 50%;
844
844
  transform: translate(-50%, -50%);
845
+ text-wrap: balance;
845
846
  }
846
847
 
847
848
  .notice.centre::after {
@@ -886,7 +887,9 @@
886
887
  }
887
888
 
888
889
  .promotion-piece:hover {
889
- box-shadow: var(--shadow), inset 0 0 0 max(2px, 0.625cqw) var(--hover-edge);
890
+ box-shadow:
891
+ var(--shadow),
892
+ inset 0 0 0 max(2px, 0.625cqw) var(--hover-edge);
890
893
  }
891
894
 
892
895
  /* White inside dark reads on every square and every theme. */
@@ -110,8 +110,8 @@ export function PromotionChoice({ side, column, down, onPick, text }) {
110
110
  onPick(undefined);
111
111
  }
112
112
  };
113
- addEventListener("keydown", escape);
114
- return () => removeEventListener("keydown", escape);
113
+ window.addEventListener("keydown", escape);
114
+ return () => window.removeEventListener("keydown", escape);
115
115
  }, []);
116
116
  return (_jsxs("div", { class: "promotion", role: "group", "aria-label": text.promoteTo, children: [_jsx("div", { class: "promotion-shade", onPointerDown: () => onPick(undefined) }), promotionPieces.map((piece, index) => (_jsx("button", { ref: index === 0 ? first : undefined, type: "button", class: "promotion-piece", style: { "--column": column, "--row": down ? index : 7 - index }, "aria-label": text.piece(piece.toUpperCase(), side), onClick: () => onPick(piece), children: _jsx(Piece, { letter: side === "white" ? piece.toUpperCase() : piece }) }, piece)))] }));
117
117
  }
@@ -96,10 +96,10 @@ export function useBoardPointer({ columns, ranks, pieces, player, selected, targ
96
96
  }
97
97
  };
98
98
  const landed = drag.back ? setTimeout(() => setDrag(undefined), reducedMotion() ? 0 : 260) : undefined;
99
- addEventListener("keydown", escape);
99
+ window.addEventListener("keydown", escape);
100
100
  document.documentElement.classList.add("dragging");
101
101
  return () => {
102
- removeEventListener("keydown", escape);
102
+ window.removeEventListener("keydown", escape);
103
103
  document.documentElement.classList.remove("dragging");
104
104
  clearTimeout(landed);
105
105
  };
@@ -6,6 +6,7 @@ import { accountSide } from "../../core/index.js";
6
6
  import { ResultTile } from "../game/result-tile.js";
7
7
  import { signature } from "../players/model-name.js";
8
8
  import { OutcomeBadge, outcomeOf } from "./outcome.js";
9
+ import { useRowKeys } from "./rows.js";
9
10
  import { searched } from "./search.js";
10
11
  import { playerIn, studiedSides } from "./studied.js";
11
12
  /** The list, read anew at each opening: the search, the row that loads a game, then a row for each game found, with its menu or its question. */
@@ -17,13 +18,14 @@ export function Analyses({ onStudy, onLoad, you, accounts, others, ai, text }) {
17
18
  const [asking, setAsking] = useState(undefined);
18
19
  const [busy, setBusy] = useState(false);
19
20
  const [problem, setProblem] = useState(undefined);
21
+ const rowKeys = useRowKeys();
20
22
  useClosing(menu !== undefined, () => setMenu(undefined));
21
23
  useClosing(asking !== undefined, () => setAsking(undefined));
22
24
  const act = async (name, asked) => {
23
25
  setBusy(true);
24
26
  const refused = asked === "remove" ? await deletion("/api/delete-game", name) : await deletion("/api/delete-review", name);
25
- // Analysing again opens the game without its review: the chat's analyst goes over it (D-89).
26
- const after = refused === undefined && asked === "redo" ? await onStudy({ game: name }) : refused;
27
+ // Analysing again opens the game without its review: the chat's analyst goes over it (D-89), asked for by the player.
28
+ const after = refused === undefined && asked === "redo" ? await onStudy({ game: name, redo: true }) : refused;
27
29
  setBusy(false);
28
30
  setAsking(undefined);
29
31
  setProblem(after);
@@ -44,7 +46,7 @@ export function Analyses({ onStudy, onLoad, you, accounts, others, ai, text }) {
44
46
  return { item, named, outcome };
45
47
  });
46
48
  const found = searched(rows, query, ({ item, named, outcome }) => searchText(item, named.sides, outcome, text));
47
- 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
49
+ 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, ...rowKeys, children: found.map(({ item, named, outcome }) => asking?.name === item.name
48
50
  ? (_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))
49
51
  : (_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
50
52
  ? (_jsx(RowMenu, { anchor: menu.anchor, withReview: item.reviewed || item.begun === true, onPick: (asked) => {
@@ -622,7 +622,9 @@ h3 + .switch {
622
622
 
623
623
  /* A game without a result: its star on a coin of the card's colour, readable where the four squares meet */
624
624
  .analyses .tile.open {
625
- background: radial-gradient(circle, var(--card) 0 11px, transparent 11.5px), conic-gradient(var(--dark-square) 0 25%, var(--light-square) 0 50%, var(--dark-square) 0 75%, var(--light-square) 0);
625
+ background:
626
+ radial-gradient(circle, var(--card) 0 11px, transparent 11.5px),
627
+ conic-gradient(var(--dark-square) 0 25%, var(--light-square) 0 50%, var(--dark-square) 0 75%, var(--light-square) 0);
626
628
  color: var(--soft);
627
629
  font-size: 18px;
628
630
  font-weight: 700;
@@ -0,0 +1,72 @@
1
+ import { useLayoutEffect, useRef } from "preact/hooks";
2
+ /**
3
+ * Makes the list's rows one stop of the Tab: the row's button focused last, or the first row's, takes the Tab, the others only the arrows. Up and down go
4
+ * to the row above and below, Home and End to the first and the last, left and right between a row's own buttons. A row that asks a question in its place
5
+ * keeps its buttons in the Tab's order.
6
+ */
7
+ export function useRowKeys() {
8
+ const ref = useRef(null);
9
+ const current = useRef(undefined);
10
+ // After each render the rows may have changed, by a search or a game deleted: the one stop is the button focused last, if still there, or the first.
11
+ useLayoutEffect(() => {
12
+ if (ref.current !== null) {
13
+ setStop(rowsOf(ref.current).flat(), current.current);
14
+ }
15
+ });
16
+ const onFocusIn = (event) => {
17
+ const buttons = ref.current === null ? [] : rowsOf(ref.current).flat();
18
+ if (buttons.includes(event.target)) {
19
+ current.current = event.target;
20
+ setStop(buttons, current.current);
21
+ }
22
+ };
23
+ const onKeyDown = (event) => {
24
+ const rows = ref.current === null ? [] : rowsOf(ref.current);
25
+ const row = rows.findIndex((buttons) => buttons.includes(event.target));
26
+ if (row === -1) {
27
+ return;
28
+ }
29
+ const next = target(event.key, rows, row, rows[row].indexOf(event.target));
30
+ if (next !== undefined) {
31
+ event.preventDefault();
32
+ next.focus();
33
+ }
34
+ };
35
+ return { ref, onKeyDown, onFocusIn };
36
+ }
37
+ /** Gives the Tab to the button wanted, if it is still in the list, else to the first. */
38
+ function setStop(buttons, wanted) {
39
+ const stop = wanted !== undefined && buttons.includes(wanted) ? wanted : buttons[0];
40
+ for (const button of buttons) {
41
+ button.tabIndex = button === stop ? 0 : -1;
42
+ }
43
+ }
44
+ /** The button a key goes to from the row and column focused, or none for a key the list leaves to the page. */
45
+ function target(key, rows, row, column) {
46
+ const at = (index) => {
47
+ const buttons = rows[Math.max(0, Math.min(rows.length - 1, index))];
48
+ return buttons[Math.min(column, buttons.length - 1)];
49
+ };
50
+ switch (key) {
51
+ case "ArrowDown":
52
+ return at(row + 1);
53
+ case "ArrowUp":
54
+ return at(row - 1);
55
+ case "Home":
56
+ return at(0);
57
+ case "End":
58
+ return at(rows.length - 1);
59
+ case "ArrowRight":
60
+ return rows[row][Math.min(column + 1, rows[row].length - 1)];
61
+ case "ArrowLeft":
62
+ return rows[row][Math.max(column - 1, 0)];
63
+ default:
64
+ return undefined;
65
+ }
66
+ }
67
+ /** The rows' own buttons, row by row, those that can take the focus: the row's, and its menu's button, never a menu's or a question's. */
68
+ function rowsOf(list) {
69
+ return [...list.querySelectorAll(":scope > li:not([role])")]
70
+ .map((row) => [...row.querySelectorAll(":scope > button, :scope > .row-actions > button")].filter((button) => !button.disabled))
71
+ .filter((buttons) => buttons.length > 0);
72
+ }
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-ru
3
3
  import { useEffect, useState } from "preact/hooks";
4
4
  import { Piece } from "../pieces/pieces.js";
5
5
  import { OutcomeBadge, outcomeOf } from "./outcome.js";
6
+ import { useRowKeys } from "./rows.js";
6
7
  /**
7
8
  * The list, asked of the server when the step opens and the profile has a name on a site. `picked` is the game being sent, whose row keeps the accent and
8
9
  * a spinner while the others wait; `problem`, why the server refused it, shown under the list; `onProfile` opens the profile, where the names go.
@@ -11,6 +12,7 @@ export function SiteGameList({ accounts, picked, problem, onPick, onProfile, tex
11
12
  const words = text.study;
12
13
  const named = accounts?.lichess !== undefined || accounts?.chesscom !== undefined;
13
14
  const answer = useSiteGames(named, words.siteGamesRefused);
15
+ const rowKeys = useRowKeys();
14
16
  if (!named) {
15
17
  return (_jsxs("div", { class: "field analyses site-games", children: [_jsx("span", { children: words.siteGames }), _jsxs("p", { class: "none", children: [`${words.siteGamesNoAccount} `, _jsx("button", { class: "add-names", type: "button", onClick: onProfile, children: words.siteGamesProfile })] })] }));
16
18
  }
@@ -20,7 +22,7 @@ export function SiteGameList({ accounts, picked, problem, onPick, onProfile, tex
20
22
  ? _jsx("p", { class: "site-problem", children: answer.refusal })
21
23
  : (_jsxs(_Fragment, { children: [_jsx(SiteProblems, { answer: answer, text: text }), answer.games.length === 0
22
24
  ? (answer.answered.length === 0 ? null : _jsx("p", { class: "none", children: words.siteGamesNone }))
23
- : (_jsx("ul", { class: `games${picked === undefined ? "" : " picking"}`, "aria-label": words.siteGames, children: answer.games.map((game) => _jsx(GameRow, { game: game, picked: picked, onPick: onPick, text: text }, game.name)) })), problem === undefined ? null : _jsx("p", { class: "problem", role: "alert", children: problem })] }))] }));
25
+ : (_jsx("ul", { class: `games${picked === undefined ? "" : " picking"}`, "aria-label": words.siteGames, ...rowKeys, children: answer.games.map((game) => _jsx(GameRow, { game: game, picked: picked, onPick: onPick, text: text }, game.name)) })), problem === undefined ? null : _jsx("p", { class: "problem", role: "alert", children: problem })] }))] }));
24
26
  }
25
27
  const siteNames = { "lichess": "Lichess", "chess.com": "Chess.com" };
26
28
  /** The sites that did not answer, both in one line, and those without a player by the profile's name, in the profile window's words. */
@@ -1,20 +1,21 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "preact/jsx-runtime";
2
- import { outOfBookMove } from "../../core/index.js";
2
+ import {} from "../../core/index.js";
3
3
  import { LeadBar, LeadChart } from "../lead/lead.js";
4
4
  import { standingAt } from "../lead/view.js";
5
5
  import { Piece } from "../pieces/pieces.js";
6
6
  import { FoldButton, useCardFold } from "./card-fold.js";
7
7
  import { MoveList, Steps } from "./move-list.js";
8
8
  import { ResultTile } from "./result-tile.js";
9
+ import { ReviewProgress } from "./review-progress.js";
9
10
  /**
10
11
  * 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
12
  * (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
  */
13
- export function GameColumn({ plies, result, how, lit, started, move, toMove, opening, helps, legend, standings, analysis, shownPlies, onShow, onStep, onDownload, onSaveMoves, onAnalyse, text }) {
14
+ export function GameColumn({ plies, result, how, lit, started, move, toMove, opening, outOfBook, helps, legend, standings, analysis, shownPlies, onShow, onStep, onDownload, onSaveMoves, onAnalyse, text }) {
14
15
  const bar = standings === undefined ? undefined : _jsx(LeadBar, { standing: standingAt(standings, shownPlies), shown: shownPlies, text: text });
15
16
  return (_jsx("aside", { class: "side", id: "game", "aria-label": text.game, children: started === undefined && analysis === undefined
16
17
  ? (_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: [_jsxs("div", { class: "side-cards", children: [_jsx(GameCard, { plies: plies, result: result, how: how, lit: lit, started: started, move: move, toMove: toMove, bar: bar, text: text }), analysis?.reviewing !== true ? null : (_jsxs("p", { class: "card review waiting", role: "status", children: [_jsx("span", { class: "spinner", "aria-hidden": "true" }), text.reviewing(analysis.by)] })), analysis?.onComplete === undefined ? null : (_jsxs("div", { class: "card review incomplete", children: [_jsx("p", { role: "status", children: text.incomplete }), _jsx("button", { type: "button", title: text.completeHint, "aria-label": text.completeHint, onClick: analysis.onComplete, children: text.complete })] })), 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] }), _jsxs("div", { class: "side-foot", children: [result !== undefined && plies.length === 0 ? null : _jsx(MoveList, { plies: plies, result: result, shown: shownPlies, onShow: onShow, tools: legend, 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
+ : (_jsxs(_Fragment, { children: [_jsxs("div", { class: "side-cards", children: [_jsx(GameCard, { plies: plies, result: result, how: how, lit: lit, started: started, move: move, toMove: toMove, bar: bar, text: text }), analysis?.reviewing !== true ? null : (_jsxs("div", { class: "card review waiting", children: [_jsx("p", { role: "status", children: text.reviewing(analysis.by) }), analysis.count === undefined ? null : _jsx(ReviewProgress, { count: analysis.count, label: text.reviewing(analysis.by) })] })), analysis?.onComplete === undefined ? null : (_jsxs("div", { class: "card review incomplete", children: [_jsxs("p", { children: [_jsx("span", { role: "status", children: text.incomplete }), _jsx("button", { type: "button", title: text.completeHint, "aria-label": text.completeHint, onClick: analysis.onComplete, children: text.complete })] }), analysis.count === undefined || analysis.count.done === 0 ? null : _jsx(ReviewProgress, { count: analysis.count, label: text.incomplete, stopped: true })] })), analysis === undefined || standings === undefined ? null : _jsx(LeadChart, { plies: plies, standings: standings, shown: shownPlies, onShow: onShow, text: text }), opening === undefined ? null : _jsx(OpeningCard, { opening: opening, ...(outOfBook === undefined ? {} : { outOfBook }), text: text }), helps] }), _jsxs("div", { class: "side-foot", children: [result !== undefined && plies.length === 0 ? null : _jsx(MoveList, { plies: plies, result: result, shown: shownPlies, onShow: onShow, tools: legend, 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
  }
19
20
  /** A button that saves the game as a file in the player's downloads, its tooltip saying what the file holds. */
20
21
  function SaveButton({ label, hint, onSave }) {
@@ -48,12 +49,13 @@ function GameCard({ plies, result, how, lit, started, move, toMove, bar, text })
48
49
  * 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,
49
50
  * and the move the game left the book at.
50
51
  */
51
- function OpeningCard({ opening, text }) {
52
+ function OpeningCard({ opening, outOfBook, text }) {
52
53
  const [foldedHere, toggleFold] = useCardFold("opening");
53
- const book = opening.inBook ? text.inBook : `${text.outOfBook} ${outOfBookMove(opening)}`;
54
+ const inBook = outOfBook === undefined;
55
+ const book = inBook ? text.inBook : `${text.outOfBook} ${outOfBook}`;
54
56
  // The family alone has nothing under it to fold: no button, no chevron (Stefano).
55
- const foldable = opening.variation !== undefined || !opening.inBook;
57
+ const foldable = opening.variation !== undefined || !inBook;
56
58
  const folded = foldable && foldedHere;
57
- 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 })] }));
58
- 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)] })] }));
59
+ const title = (_jsxs(_Fragment, { children: [_jsx("span", { class: `book${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" }), inBook ? null : _jsx("path", { d: "M3.5 3.5L20.5 20.5" })] }) }), _jsx("b", { children: opening.family })] }));
60
+ 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 || inBook ? null : _jsxs("p", { class: "out", children: [text.outOfBook, " ", outOfBook] })] }));
59
61
  }
@@ -129,8 +129,8 @@
129
129
  height: 14px;
130
130
  margin-top: 6px;
131
131
  border-radius: 7px;
132
- background: #3b4048;
133
- box-shadow: 0 0 0 1px rgb(138 144 153 / 0.6);
132
+ background: var(--side-black);
133
+ box-shadow: 0 0 0 1px var(--side-edge);
134
134
  overflow: hidden;
135
135
  }
136
136
 
@@ -139,7 +139,7 @@
139
139
  position: absolute;
140
140
  inset: 3px auto 3px calc(50% - 0.5px);
141
141
  width: 1px;
142
- background: #8a9099;
142
+ background: var(--side-both);
143
143
  }
144
144
 
145
145
  /* On a position after the one the AI estimated: the player's move, until the coach speaks again */
@@ -150,7 +150,7 @@
150
150
  .lead-bar .white {
151
151
  position: absolute;
152
152
  inset: 0 auto 0 0;
153
- background: #f5f5f2;
153
+ background: var(--side-white);
154
154
  transition: width 400ms ease;
155
155
  }
156
156
 
@@ -172,12 +172,12 @@
172
172
  .lead-bar .figure.white,
173
173
  .lead-bar .figure.even {
174
174
  left: 6px;
175
- color: #2a2f36;
175
+ color: var(--fixed-ink);
176
176
  }
177
177
 
178
178
  .lead-bar .figure.black {
179
179
  right: 6px;
180
- color: #f5f5f2;
180
+ color: var(--side-white);
181
181
  }
182
182
 
183
183
  /* The chart of the game in analysis: the white area is White's share, the line in the middle an even game; a dot on a move a badge marks, a line on the move
@@ -191,10 +191,13 @@
191
191
  position: relative;
192
192
  height: 72px;
193
193
  border-radius: 6px;
194
- background: #3b4048;
195
- box-shadow: 0 0 0 1px rgb(138 144 153 / 0.6);
194
+ background: var(--side-black);
195
+ box-shadow: 0 0 0 1px var(--side-edge);
196
196
  overflow: hidden;
197
197
  cursor: pointer;
198
+ /* A finger drawn across it runs through the game; up and down it scrolls the page */
199
+ touch-action: pan-y;
200
+ user-select: none;
198
201
  }
199
202
 
200
203
  .lead-chart svg {
@@ -204,7 +207,7 @@
204
207
  }
205
208
 
206
209
  .lead-chart .white-area {
207
- fill: #f5f5f2;
210
+ fill: var(--side-white);
208
211
  }
209
212
 
210
213
  .lead-chart .pending {
@@ -212,7 +215,7 @@
212
215
  }
213
216
 
214
217
  .lead-chart .even {
215
- stroke: #8a9099;
218
+ stroke: var(--side-both);
216
219
  stroke-width: 1;
217
220
  stroke-dasharray: 3 3;
218
221
  fill: none;
@@ -225,10 +228,15 @@
225
228
  width: 2px;
226
229
  margin-left: -1px;
227
230
  background: #ffc450;
228
- box-shadow: 0 0 0 1px rgb(20 23 27 / 0.6);
231
+ box-shadow: 0 0 0 1px rgba(20, 23, 27, 0.6);
229
232
  pointer-events: none;
230
233
  }
231
234
 
235
+ /* The move a click would show, under the mouse: the same line, fainter (Zoe) */
236
+ .lead-chart .ghost {
237
+ opacity: 0.5;
238
+ }
239
+
232
240
  .lead-chart .dot {
233
241
  position: absolute;
234
242
  width: 8px;
@@ -311,24 +319,104 @@
311
319
  line-height: 1.45;
312
320
  }
313
321
 
322
+ /* The line that says the AI reviews, and under it how far it has got */
314
323
  .review.waiting,
315
324
  .say.reviewing {
316
- display: flex;
317
- align-items: center;
325
+ display: grid;
318
326
  gap: 8px;
319
327
  margin: 0;
320
328
  color: var(--soft);
321
329
  }
322
330
 
323
- /* A review nobody is finishing: the note, and beside it the button that brings the game back to the AI (D-109); neutral, as no error (Zoe) */
331
+ /* A review nobody is finishing: the note, and beside it the button that brings the game back to the AI (D-109); neutral, as no error (Zoe). Under them,
332
+ how far it got */
324
333
  .review.incomplete,
325
334
  .say.incomplete {
335
+ display: grid;
336
+ gap: 8px;
337
+ margin: 0;
338
+ color: var(--soft);
339
+ }
340
+
341
+ .review.incomplete > p,
342
+ .say.incomplete > p {
326
343
  display: flex;
327
344
  align-items: center;
328
345
  justify-content: space-between;
329
346
  gap: 8px;
330
- margin: 0;
331
- color: var(--soft);
347
+ }
348
+
349
+ /* How far a review has got: a thin bar in the accent and the count, 12/33. Before the first move a short run goes back and forth; full, while the word on
350
+ the whole game is written, it breathes; stopped unfinished, it stands still in the soft ink */
351
+ .review-progress {
352
+ display: flex;
353
+ align-items: center;
354
+ gap: 8px;
355
+ }
356
+
357
+ .review-progress .track {
358
+ flex: 1;
359
+ height: 4px;
360
+ overflow: hidden;
361
+ border-radius: 2px;
362
+ background: color-mix(in srgb, var(--accent) 20%, transparent);
363
+ }
364
+
365
+ .review-progress .fill {
366
+ display: block;
367
+ width: 0;
368
+ height: 100%;
369
+ border-radius: inherit;
370
+ background: var(--accent);
371
+ transition: width 300ms ease-out;
372
+ }
373
+
374
+ .review-progress .count {
375
+ flex: none;
376
+ font-size: 12px;
377
+ font-variant-numeric: tabular-nums;
378
+ }
379
+
380
+ .review-progress.waiting .fill {
381
+ width: 30%;
382
+ animation: review-run 1400ms ease-in-out infinite alternate;
383
+ }
384
+
385
+ .review-progress.writing .fill {
386
+ animation: review-breath 1600ms ease-in-out infinite;
387
+ }
388
+
389
+ .review-progress.stopped .track {
390
+ background: color-mix(in srgb, var(--soft) 20%, transparent);
391
+ }
392
+
393
+ .review-progress.stopped .fill {
394
+ background: var(--soft);
395
+ }
396
+
397
+ @keyframes review-run {
398
+ to {
399
+ translate: 233%;
400
+ }
401
+ }
402
+
403
+ @keyframes review-breath {
404
+ 50% {
405
+ opacity: 0.45;
406
+ }
407
+ }
408
+
409
+ /* Still: before the first move a faint full bar, the count then grows without sliding */
410
+ @media (prefers-reduced-motion: reduce) {
411
+ .review-progress .fill {
412
+ transition: none;
413
+ animation: none;
414
+ }
415
+
416
+ .review-progress.waiting .fill {
417
+ width: 100%;
418
+ opacity: 0.35;
419
+ }
332
420
  }
333
421
 
334
422
  .review.incomplete button,
@@ -394,6 +482,8 @@
394
482
  overflow: hidden auto;
395
483
  scrollbar-width: none;
396
484
  font-size: 14px;
485
+ /* A row too long for a phone never makes the page wider */
486
+ contain: inline-size;
397
487
  }
398
488
 
399
489
  .moves ol {
@@ -438,6 +528,11 @@
438
528
  cursor: pointer;
439
529
  }
440
530
 
531
+ /* A move never breaks, its piece always beside its square: a long one takes room from its neighbour in the row */
532
+ .moves .ply {
533
+ white-space: nowrap;
534
+ }
535
+
441
536
  /* The move under the pointer in a neutral grey, never the accent, so it never reads as the move shown (Stefano) */
442
537
  .moves .ply:hover {
443
538
  background: color-mix(in oklab, var(--ink) 9%, transparent);
@@ -552,6 +647,32 @@
552
647
  }
553
648
  }
554
649
 
650
+ /* On a phone the rows give the moves all the room they can, so that two columns hold a move with its badge on one line (Zoe) */
651
+ @media (max-width: 479px) {
652
+ .moves ol {
653
+ column-gap: 8px;
654
+ }
655
+
656
+ .moves li {
657
+ grid-template-columns: 22px 1fr 1fr;
658
+ column-gap: 4px;
659
+ padding: 3px 4px;
660
+ }
661
+
662
+ .moves .ply {
663
+ gap: 3px;
664
+ padding: 1px 4px;
665
+ margin-left: -4px;
666
+ }
667
+ }
668
+
669
+ /* On the narrowest phones two columns would leave a move no room: one column */
670
+ @media (max-width: 359px) {
671
+ .moves ol {
672
+ columns: 1;
673
+ }
674
+ }
675
+
555
676
  /* A card's title in the game's column, its icon in the accent as the opening's book: the helps' lifebuoy, the moves' score sheet (Stefano) */
556
677
  .card-title {
557
678
  display: flex;
@@ -48,14 +48,16 @@ function Move({ ply, current, onShow, text }) {
48
48
  // A screen reader reads the move in SAN and the AI's badge by its name; the eye sees each piece's letter as its figure, and the badge's disc (D-69).
49
49
  return (_jsxs("button", { type: "button", class: current ? "ply current" : "ply", "aria-current": current ? "step" : undefined, onClick: onShow, children: [_jsx("span", { class: "visually-hidden", children: ply.review?.badge === undefined ? ply.san : `${ply.san}, ${text.badges[ply.review.badge]}` }), _jsx("span", { "aria-hidden": "true", children: _jsx(Figurines, { san: ply.san, side: ply.side }) }), ply.review?.badge !== undefined ? _jsx(BadgeIcon, { badge: ply.review.badge }) : ply.review?.note !== undefined ? _jsx(NoteIcon, {}) : null] }));
50
50
  }
51
- /** Scrolls the list, and only the list, as little as it takes to show its current move. */
51
+ /** Scrolls the list, and only the list, as little as it takes to show the whole row of its current move, and the result under the last one. */
52
52
  function keepInSight(list) {
53
- const box = list?.getBoundingClientRect();
54
- const move = list?.querySelector(".current")?.getBoundingClientRect();
55
- if (box === undefined || move === undefined) {
53
+ const row = list?.querySelector(".current")?.closest("li");
54
+ if (list === null || !row) {
56
55
  return;
57
56
  }
58
- list.scrollTop += Math.min(0, move.top - box.top) + Math.max(0, move.bottom - box.bottom);
57
+ const box = list.getBoundingClientRect();
58
+ const top = row.getBoundingClientRect().top;
59
+ const bottom = (row.nextElementSibling === null ? list.lastElementChild ?? row : row).getBoundingClientRect().bottom;
60
+ list.scrollTop += Math.min(0, top - box.top) + Math.max(0, bottom - box.bottom);
59
61
  }
60
62
  /** The plies in rows of one number: White's move and Black's. */
61
63
  function rowsOf(plies) {
@@ -91,3 +91,7 @@ export function playerSpent(plies, ai, talk) {
91
91
  function judged({ review }) {
92
92
  return review?.note !== undefined || (review?.badge !== undefined && review.badge !== "book");
93
93
  }
94
+ /** How far a review has got, as status tells the AI: the moves the analyst has said who is ahead after, of all the game's. */
95
+ export function reviewCount(plies) {
96
+ return { done: plies.filter((ply) => ply.lead !== undefined).length, total: plies.length };
97
+ }
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
+ /**
3
+ * The line that says the analyst works, and under it a thin bar with the count, 12/33: a bar that runs back and forth before the first move is said, full
4
+ * and breathing while the word on the whole game is written; still, in the soft ink, when the review stopped unfinished.
5
+ */
6
+ export function ReviewProgress({ count, label, stopped = false }) {
7
+ const { done, total } = count;
8
+ const started = done > 0;
9
+ const state = stopped ? " stopped" : !started ? " waiting" : done >= total ? " writing" : "";
10
+ return (_jsxs("span", { class: `review-progress${state}`, role: "progressbar", "aria-label": label, "aria-valuemin": 0, "aria-valuemax": total, ...(started || stopped ? { "aria-valuenow": done, "aria-valuetext": `${done}/${total}` } : {}), children: [_jsx("span", { class: "track", "aria-hidden": "true", children: _jsx("span", { class: "fill", style: started ? { width: `${(100 * done) / Math.max(total, 1)}%` } : undefined }) }), started || stopped ? _jsxs("span", { class: "count", "aria-hidden": "true", children: [done, "/", total] }) : null] }));
11
+ }
@@ -111,7 +111,6 @@
111
111
  stroke-width: 2.4;
112
112
  }
113
113
 
114
-
115
114
  .guide-art .knot,
116
115
  .guide-art .caret {
117
116
  fill: var(--accent);
@@ -18,7 +18,7 @@ No chess engine plays for the AI: every move and every judgment is its own. Ches
18
18
 
19
19
  ### Getting started
20
20
 
21
- 1. Click the **AI's name**, at the top left, and choose its chat among those on your computer: **Claude Code** or **Codex**. It opens in a window of its own: keep it open while you play.
21
+ 1. Click the **AI's name**, at the top left, and choose its chat among those on your computer: **Claude Code** or **Codex**. It opens in a window of its own: keep it open while you play. The first time it asks some questions. Whether you trust the folder: **Yes** (in Claude Code No is chosen already: go down to Yes with the arrow keys, then Enter). With Codex, about the hooks, Chesspal's two helpers: **Trust all and continue**. On Windows Codex also asks whether to set up its sandbox, the protected space it works in: choose the first option, and yes when Windows asks for permission.
22
22
  2. Press **New game**, choose your colour, the clock and the AI's level, and press **Start**.
23
23
  3. Move: the AI answers on the board, and with a few words under its name.
24
24
 
@@ -35,7 +35,7 @@ No chess engine plays for the AI: every move and every judgment is its own. Ches
35
35
 
36
36
  ![The box to write in under the AI's name, and its answer](talk)
37
37
 
38
- - You talk with the AI in the **box under its name**, a few words with each move: it reads them with your move. You can also write to it in its window.
38
+ - You talk with the AI in the **box under its name**, a few words with each move: it reads them with your move. You can also write to it in its window. With Codex, if you write there while it waits for your move, press **Esc** after Enter: else it reads your message only after your move.
39
39
  - A **move written** in the chat counts too: `e4`, `Nf3`, with the English letters of the pieces (K Q R B N), or in words.
40
40
  - **The AI's name** opens the choice of its chat: Claude Code or Codex, the model (which AI: some are faster, others stronger) and how much it reasons. The more it reasons, the slower it plays. If you change them, the chat starts afresh in the same window: the AI forgets what you talked about, and the game stays as it is.
41
41
  - At each **new game**, the chat the page opened starts afresh by itself, once the AI has stopped thinking: a long chat makes the AI slower.
@@ -18,7 +18,7 @@ Nessun motore di scacchi gioca al posto dell'AI: ogni mossa e ogni giudizio sono
18
18
 
19
19
  ### Per cominciare
20
20
 
21
- 1. Clicca il **nome dell'AI**, in alto a sinistra, e scegli la chat fra quelle che hai sul computer: **Claude Code** o **Codex**. Si apre in una finestra sua: tienila aperta mentre giochi.
21
+ 1. Clicca il **nome dell'AI**, in alto a sinistra, e scegli la chat fra quelle che hai sul computer: **Claude Code** o **Codex**. Si apre in una finestra sua: tienila aperta mentre giochi. La prima volta fa qualche domanda. Se ti fidi della cartella: **Sì** (in Claude Code è già scelto No: scendi su Yes con le frecce, poi Invio). Con Codex, sugli hook, i due aiutanti di Chesspal: **Trust all and continue**. Su Windows Codex chiede anche se preparare la sua sandbox, lo spazio protetto dove lavora: scegli la prima opzione, e sì quando Windows chiede il permesso.
22
22
  2. Premi **Nuova partita**, scegli il colore, l'orologio e il livello dell'AI, e premi **Inizia**.
23
23
  3. Muovi: l'AI risponde sulla scacchiera, e con qualche parola sotto il suo nome.
24
24
 
@@ -35,7 +35,7 @@ Nessun motore di scacchi gioca al posto dell'AI: ogni mossa e ogni giudizio sono
35
35
 
36
36
  ![La casella per scrivere sotto il nome dell'AI, e la sua risposta](talk)
37
37
 
38
- - Parli con l'AI nella **casella sotto il suo nome**, qualche parola a ogni mossa: le legge con la tua mossa. Puoi scriverle anche nella sua finestra.
38
+ - Parli con l'AI nella **casella sotto il suo nome**, qualche parola a ogni mossa: le legge con la tua mossa. Puoi scriverle anche nella sua finestra. Con Codex, se gli scrivi lì mentre aspetta la tua mossa, dopo Invio premi **Esc**: se no legge il messaggio solo dopo la tua mossa.
39
39
  - Anche una **mossa scritta** nella chat vale: `e4`, `Nf3`, con le lettere inglesi dei pezzi (K Q R B N), o a parole.
40
40
  - **Il nome dell'AI** apre la scelta della chat: Claude Code o Codex, il modello (quale AI: alcune sono più veloci, altre più forti) e quanto ragiona. Più ragiona, più gioca lenta. Se cambi, la chat riparte da capo nella stessa finestra: l'AI dimentica la chiacchierata di prima, la partita resta com'è.
41
41
  - A ogni **nuova partita** la chat aperta dalla pagina riparte da capo da sola, se l'AI ha finito di pensare: una chat lunga rende l'AI più lenta.