chesspal 0.3.5 → 0.3.6

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 (102) hide show
  1. package/README.md +1 -0
  2. package/dist/cli/cli.js +10 -2
  3. package/dist/cli/command-line.js +17 -9
  4. package/dist/cli/icon.d.ts +2 -0
  5. package/dist/cli/icon.js +11 -1
  6. package/dist/commands/analysis/analyse.d.ts +2 -2
  7. package/dist/commands/analysis/analyse.js +3 -3
  8. package/dist/commands/answers/agents.d.ts +10 -2
  9. package/dist/commands/answers/agents.js +30 -7
  10. package/dist/commands/answers/index.d.ts +1 -1
  11. package/dist/commands/answers/index.js +1 -1
  12. package/dist/commands/answers/lines.js +3 -2
  13. package/dist/commands/answers/summary.d.ts +2 -1
  14. package/dist/commands/answers/view.d.ts +1 -1
  15. package/dist/commands/answers/view.js +2 -2
  16. package/dist/commands/game/look.d.ts +7 -3
  17. package/dist/commands/game/look.js +9 -4
  18. package/dist/commands/game/new.js +1 -1
  19. package/dist/commands/game/ponder.js +3 -3
  20. package/dist/commands/game/position.d.ts +2 -1
  21. package/dist/commands/game/position.js +9 -4
  22. package/dist/commands/game/status.d.ts +3 -1
  23. package/dist/commands/game/status.js +19 -9
  24. package/dist/commands/profiles/past-games.d.ts +7 -2
  25. package/dist/commands/profiles/past-games.js +26 -10
  26. package/dist/commands/talk/ai-say.js +1 -1
  27. package/dist/games/analysis.d.ts +7 -4
  28. package/dist/games/analysis.js +5 -3
  29. package/dist/games/index.d.ts +1 -1
  30. package/dist/games/index.js +1 -1
  31. package/dist/games/thoughts.d.ts +12 -4
  32. package/dist/games/thoughts.js +27 -10
  33. package/dist/home/index.d.ts +1 -0
  34. package/dist/home/index.js +1 -0
  35. package/dist/home/same-path.d.ts +2 -0
  36. package/dist/home/same-path.js +5 -0
  37. package/dist/host-files/host-files.js +8 -3
  38. package/dist/page/app/anchor.js +15 -11
  39. package/dist/page/app/app.js +20 -8
  40. package/dist/page/app/served.js +23 -36
  41. package/dist/page/app/watch.js +97 -0
  42. package/dist/page/avatars/torretta.css +108 -0
  43. package/dist/page/board/board.css +7 -6
  44. package/dist/page/board/marks.js +2 -2
  45. package/dist/page/board/pointer.js +2 -2
  46. package/dist/page/dialogs/analyses.js +5 -3
  47. package/dist/page/dialogs/rows.js +72 -0
  48. package/dist/page/dialogs/site-games.js +3 -1
  49. package/dist/page/game/game.css +51 -10
  50. package/dist/page/game/move-list.js +7 -5
  51. package/dist/page/guide/en.md +1 -1
  52. package/dist/page/guide/it.md +1 -1
  53. package/dist/page/helps/helps.css +5 -5
  54. package/dist/page/helps/helps.js +3 -181
  55. package/dist/page/helps/rows.js +16 -0
  56. package/dist/page/helps/strategy.js +105 -0
  57. package/dist/page/helps/tactics.js +69 -0
  58. package/dist/page/layout/header.js +49 -6
  59. package/dist/page/layout/layout.css +17 -2
  60. package/dist/page/layout/window.js +4 -4
  61. package/dist/page/lead/lead.js +41 -4
  62. package/dist/page/sounds/sounds.js +1 -1
  63. package/dist/page/style.css +1 -0
  64. package/dist/page/talk/talk.css +0 -109
  65. package/dist/page/text.js +10 -0
  66. package/dist/page/theme.css +13 -0
  67. package/dist/profiles/profiles.js +5 -1
  68. package/dist/server/bell/bell.d.ts +0 -2
  69. package/dist/server/bell/bell.js +17 -7
  70. package/dist/server/bell/index.d.ts +1 -1
  71. package/dist/server/bell/index.js +1 -1
  72. package/dist/server/page/chat.d.ts +5 -1
  73. package/dist/server/page/chat.js +5 -1
  74. package/dist/server/page/game.js +18 -3
  75. package/dist/server/page/table.d.ts +2 -1
  76. package/dist/server/page/table.js +4 -2
  77. package/dist/server/page/update.d.ts +1 -1
  78. package/dist/server/page/update.js +4 -4
  79. package/dist/server/table/hosts/chats.d.ts +5 -0
  80. package/dist/server/table/hosts/chats.js +11 -4
  81. package/dist/server/table/hosts/play.d.ts +3 -1
  82. package/dist/server/table/hosts/play.js +17 -5
  83. package/dist/server/table/icon/index.d.ts +2 -0
  84. package/dist/server/table/icon/index.js +16 -2
  85. package/dist/server/table/index.d.ts +1 -1
  86. package/dist/server/table/index.js +1 -1
  87. package/dist/server/table/link.js +3 -1
  88. package/dist/server/table/table.js +2 -2
  89. package/dist/sites/index.d.ts +1 -1
  90. package/dist/sites/index.js +1 -1
  91. package/dist/sites/{chesscom.d.ts → ratings/chesscom.d.ts} +2 -2
  92. package/dist/sites/{chesscom.js → ratings/chesscom.js} +1 -1
  93. package/dist/sites/{choice.d.ts → ratings/choice.d.ts} +1 -1
  94. package/dist/sites/{choice.js → ratings/choice.js} +1 -1
  95. package/dist/sites/ratings/index.d.ts +1 -0
  96. package/dist/sites/ratings/index.js +2 -0
  97. package/dist/sites/{lichess.d.ts → ratings/lichess.d.ts} +2 -2
  98. package/dist/sites/{lichess.js → ratings/lichess.js} +1 -1
  99. package/dist/sites/{sites.d.ts → ratings/ratings.d.ts} +2 -2
  100. package/dist/sites/{sites.js → ratings/ratings.js} +1 -1
  101. package/instructions/start.md +1 -1
  102. package/package.json +1 -1
@@ -0,0 +1,108 @@
1
+ /* The Torretta, whose eyes blink now and then, and its moods (D-70): a new face that pops in, never fading out, a red light when worried, out when sad, a light that breathes when
2
+ thoughtful */
3
+ .torretta .antenna {
4
+ fill: none;
5
+ stroke: var(--torretta-shade);
6
+ stroke-width: 4;
7
+ stroke-linecap: round;
8
+ }
9
+
10
+ .torretta .light {
11
+ fill: var(--torretta-light);
12
+ transition: fill 0.2s;
13
+ }
14
+
15
+ .torretta.mood-worried .light {
16
+ fill: var(--torretta-alarm);
17
+ }
18
+
19
+ .torretta.mood-thoughtful .light {
20
+ animation: ponder 2.4s ease-in-out infinite;
21
+ }
22
+
23
+ .torretta.mood-sad .light {
24
+ fill: var(--torretta-shade);
25
+ }
26
+
27
+ @keyframes ponder {
28
+ 50% {
29
+ fill: var(--torretta-ponder);
30
+ }
31
+ }
32
+
33
+ .torretta .expression.changed {
34
+ transform-box: fill-box;
35
+ transform-origin: center;
36
+ animation: face-in 0.16s ease-out;
37
+ }
38
+
39
+ @keyframes face-in {
40
+ from {
41
+ transform: scale(0.8);
42
+ }
43
+ }
44
+
45
+ /* Its bust on a player's card: a disc of the player's picture's colour, which cuts what falls out of it */
46
+ .torretta.bust {
47
+ clip-path: circle(50%);
48
+ }
49
+
50
+ .torretta .back {
51
+ fill: var(--say);
52
+ }
53
+
54
+ .torretta .shade {
55
+ fill: var(--torretta-shade);
56
+ }
57
+
58
+ .torretta .body {
59
+ fill: var(--torretta-body);
60
+ }
61
+
62
+ .torretta .screen {
63
+ fill: var(--torretta-screen);
64
+ }
65
+
66
+ .torretta .eyes,
67
+ .torretta .mouth {
68
+ fill: var(--torretta-face);
69
+ }
70
+
71
+ .torretta .expression .line,
72
+ .torretta .brows {
73
+ fill: none;
74
+ stroke: var(--torretta-face);
75
+ stroke-width: 3.4;
76
+ stroke-linecap: round;
77
+ stroke-linejoin: round;
78
+ }
79
+
80
+ .torretta .eyes {
81
+ transform-box: fill-box;
82
+ transform-origin: center;
83
+ animation: blink 6s infinite;
84
+ }
85
+
86
+ @keyframes blink {
87
+ 0%,
88
+ 95%,
89
+ 100% {
90
+ transform: scaleY(1);
91
+ }
92
+
93
+ 97% {
94
+ transform: scaleY(0.1);
95
+ }
96
+ }
97
+
98
+ @media (prefers-reduced-motion: reduce) {
99
+ .torretta .eyes,
100
+ .torretta .expression.changed,
101
+ .torretta.mood-thoughtful .light {
102
+ animation: none;
103
+ }
104
+
105
+ .torretta .light {
106
+ transition: none;
107
+ }
108
+ }
@@ -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 */
@@ -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 {
@@ -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) => {
@@ -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. */
@@ -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;
@@ -229,6 +232,11 @@
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;
@@ -394,6 +402,8 @@
394
402
  overflow: hidden auto;
395
403
  scrollbar-width: none;
396
404
  font-size: 14px;
405
+ /* A row too long for a phone never makes the page wider */
406
+ contain: inline-size;
397
407
  }
398
408
 
399
409
  .moves ol {
@@ -438,6 +448,11 @@
438
448
  cursor: pointer;
439
449
  }
440
450
 
451
+ /* A move never breaks, its piece always beside its square: a long one takes room from its neighbour in the row */
452
+ .moves .ply {
453
+ white-space: nowrap;
454
+ }
455
+
441
456
  /* The move under the pointer in a neutral grey, never the accent, so it never reads as the move shown (Stefano) */
442
457
  .moves .ply:hover {
443
458
  background: color-mix(in oklab, var(--ink) 9%, transparent);
@@ -552,6 +567,32 @@
552
567
  }
553
568
  }
554
569
 
570
+ /* 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) */
571
+ @media (max-width: 479px) {
572
+ .moves ol {
573
+ column-gap: 8px;
574
+ }
575
+
576
+ .moves li {
577
+ grid-template-columns: 22px 1fr 1fr;
578
+ column-gap: 4px;
579
+ padding: 3px 4px;
580
+ }
581
+
582
+ .moves .ply {
583
+ gap: 3px;
584
+ padding: 1px 4px;
585
+ margin-left: -4px;
586
+ }
587
+ }
588
+
589
+ /* On the narrowest phones two columns would leave a move no room: one column */
590
+ @media (max-width: 359px) {
591
+ .moves ol {
592
+ columns: 1;
593
+ }
594
+ }
595
+
555
596
  /* 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
597
  .card-title {
557
598
  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) {
@@ -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**. 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
 
@@ -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ì**. 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
 
@@ -801,26 +801,26 @@
801
801
  /* The thread that keeps the black half in sight on the dark theme, as the lead bar's */
802
802
  .control-bar,
803
803
  .row-tile {
804
- border: 1px solid rgb(138 144 153 / 0.6);
804
+ border: 1px solid var(--side-edge);
805
805
  }
806
806
 
807
807
  /* The bar's colours, which the lines' tiles share */
808
808
  .control-bar .white,
809
809
  .row-tile .white {
810
- background: #f4f6f8;
810
+ background: var(--side-white);
811
811
  color: var(--fixed-ink);
812
812
  }
813
813
 
814
814
  .control-bar .both,
815
815
  .row-tile .both {
816
- background: #8d98a7;
816
+ background: var(--side-both);
817
817
  color: var(--fixed-ink);
818
818
  }
819
819
 
820
820
  .control-bar .black,
821
821
  .row-tile .black {
822
- background: var(--fixed-ink);
823
- color: #f4f6f8;
822
+ background: var(--side-black);
823
+ color: var(--side-white);
824
824
  }
825
825
 
826
826
  /* A line's tile: a half for each side, the player's first; the open files in one half as wide as the two */