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
@@ -1,12 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-runtime";
2
2
  import { useState } from "preact/hooks";
3
- import { Notch, Pushpin, Skewer } from "../board/marks.js";
4
3
  import { BadgeIcon, NoteIcon } from "../game/badges.js";
5
4
  import { FoldButton, useCardFold } from "../game/card-fold.js";
6
5
  import { Figurines } from "../game/move-list.js";
7
- import { Figurine } from "../pieces/pieces.js";
8
6
  import { Legend, Marked } from "../talk/talk.js";
9
- import { groupOf, pinSquares, riskSquares, squaresOf, strategyOf } from "./view.js";
7
+ import { RowIcon } from "./rows.js";
8
+ import { Strategy } from "./strategy.js";
9
+ import { Tactics } from "./tactics.js";
10
10
  /**
11
11
  * The helps' card, between the opening and the moves: the coach's words, then the tactics in lines, the strategy in rows and the control map, seen while
12
12
  * its button or M is held, and the board without its drawings while their button is. A line under the pointer lights its squares on the board. Its title
@@ -60,185 +60,7 @@ function Judged({ ply, text }) {
60
60
  const head = (_jsxs(_Fragment, { children: [badge === undefined ? _jsx(NoteIcon, {}) : _jsx(BadgeIcon, { badge: badge }), _jsxs("b", { children: [_jsxs("span", { class: "move-number", children: [ply.number, ply.side === "white" ? "." : "..."] }), _jsx(Figurines, { san: ply.san, side: ply.side })] }), " ", badge === undefined ? null : text.badges[badge]] }));
61
61
  return (_jsx("div", { class: `move-note judged ${badge ?? ""}`, style: badge === undefined ? undefined : { "--row": `var(--badge-${badge})` }, children: note === undefined ? _jsx("span", { class: "head", children: head }) : (_jsxs("details", { children: [_jsx("summary", { class: "head", children: head }), _jsx("span", { class: "note", children: _jsx(Marked, { text: note }) })] })) }));
62
62
  }
63
- /**
64
- * The tactics in lines (D-91): one a kind of fact and a side, its count, red when it threatens the player and green when it is their chance; a click opens
65
- * its pieces, each with its squares (Stefano).
66
- */
67
- function Tactics({ helps, opened, onOpen, onPoint, text }) {
68
- const words = text.helpsCard;
69
- const point = pointing(onPoint);
70
- const calm = !helps.lines.some((line) => line.kind === "risk" || line.kind === "pin" || line.kind === "skewer");
71
- return (_jsxs("ul", { class: "help-rows", children: [calm
72
- ? (_jsxs("li", { class: "safe", children: [_jsxs(RowIcon, { children: [_jsx("circle", { cx: "8", cy: "8", r: "6.5" }), _jsx("path", { d: "M5.2 8.2l2 2 3.8-4" })] }), words.nothingAtRisk] }))
73
- : null, helps.lines.map((line) => {
74
- const key = line.kind === "loose" ? "loose" : `${line.kind} ${line.side}`;
75
- const open = opened.has(key);
76
- return (_jsxs("li", { class: `help-line ${line.kind} ${line.tone}${open ? " open" : ""}`, children: [_jsxs("button", { type: "button", class: "line-head", "aria-expanded": open, onClick: () => onOpen(key, !open), ...point(squaresOf(line)), children: [_jsx(LineIcon, { kind: line.kind }), _jsx("span", { class: "line-name", children: line.kind === "loose" ? words.loose : words.line(line.kind, line.side, helps.you, line.kind !== "check" || line.toMove) }), _jsx(LineCount, { line: line, helps: helps, words: words })] }), open ? _jsx(LineDetails, { line: line, helps: helps, point: point, text: text }) : null] }, key));
77
- })] }));
78
- }
79
- /** What the pointer on a line or a cell does: it lights the line's squares; a touch keeps them lit until the position changes, as a square in the AI's words. */
80
- function pointing(onPoint) {
81
- return (squares) => ({
82
- onPointerEnter: () => onPoint(squares),
83
- onPointerLeave: (event) => event.pointerType === "mouse" && onPoint([]),
84
- });
85
- }
86
- /** A line's sign, as the board draws it: the corner of a piece at risk, the pushpin, the skewer; a check's plus, the loose pieces' i; its colour is the line's. */
87
- function LineIcon({ kind }) {
88
- switch (kind) {
89
- case "risk":
90
- return _jsx(Notch, {});
91
- case "pin":
92
- return _jsx(Pushpin, {});
93
- case "skewer":
94
- return _jsx(Skewer, {});
95
- case "loose":
96
- return _jsx(InfoIcon, {});
97
- case "check":
98
- return (_jsxs(RowIcon, { children: [_jsx("circle", { cx: "8", cy: "8", r: "6.5" }), _jsx("path", { d: "M8 5v6M5 8h6" })] }));
99
- }
100
- }
101
- /** How many: the loose pieces by side, each after its pawn, the player's first (Zoe); the others in one number. */
102
- function LineCount({ line, helps, words }) {
103
- if (line.kind !== "loose") {
104
- return _jsx("b", { class: "count", children: line.items.length });
105
- }
106
- const name = (side) => helps.you === undefined ? words[side] : side === helps.you ? words.yours : words.theirs;
107
- const counts = helps.sides.map((side) => ({ side, count: line.items.filter((piece) => piece.side === side).length })).filter(({ count }) => count > 0);
108
- return (_jsx("span", { class: "counts", children: counts.map(({ side, count }) => (_jsxs("span", { title: name(side), "aria-label": `${name(side)} ${count}`, children: [_jsx(Figurine, { letter: "P", side: side }), _jsx("b", { class: "count", children: count })] }, side))) }));
109
- }
110
- /** A line opened: its pieces, each lighting its own squares; the checks in figures; the loose pieces by side. */
111
- function LineDetails({ line, helps, point, text }) {
112
- const words = text.helpsCard;
113
- switch (line.kind) {
114
- case "risk":
115
- return (_jsx("ul", { class: "line-details", children: line.items.map((risk) => (_jsxs("li", { ...point(riskSquares(risk)), children: [text.piece(risk.piece.piece, risk.piece.side), " ", _jsx(Squares, { squares: [risk.piece.square] }), " \u00B7 ", words.attackers, ": ", _jsx(Squares, { squares: risk.piece.attackers }), risk.why === "undefended" ? null : (_jsxs(_Fragment, { children: [" ", "\u00B7 ", words.defenders, ": ", _jsx(Squares, { squares: risk.piece.defenders })] })), " \u00B7 ", words[risk.why]] }, risk.piece.square))) }));
116
- case "pin":
117
- return (_jsx("ul", { class: "line-details", children: line.items.map((pin) => (_jsxs("li", { ...point(pinSquares(pin)), children: [text.piece(pin.piece.piece, pin.piece.side), " ", _jsx(Squares, { squares: [pin.piece.square] }), " ", words.pinnedTo(pin.to.piece), " ", _jsx(Squares, { squares: [pin.to.square] }), ", ", words.pinnedBy(pin.by.piece), " ", _jsx(Squares, { squares: [pin.by.square] })] }, pin.piece.square))) }));
118
- case "skewer":
119
- return (_jsx("ul", { class: "line-details", children: line.items.map((skewer) => (_jsxs("li", { ...point(pinSquares(skewer)), children: [text.piece(skewer.piece.piece, skewer.piece.side), " ", _jsx(Squares, { squares: [skewer.piece.square] }), " ", words.skeweredBefore(skewer.to.piece), " ", _jsx(Squares, { squares: [skewer.to.square] }), ", ", words.pinnedBy(skewer.by.piece), " ", _jsx(Squares, { squares: [skewer.by.square] })] }, skewer.piece.square))) }));
120
- case "check":
121
- return (_jsx("p", { class: "line-details", children: line.items.map((check) => (_jsx("span", { class: "help-move", children: _jsx(Figurines, { san: check, side: line.side }) }, check))) }));
122
- case "loose":
123
- return (_jsx("p", { class: "line-details", children: helps.sides.map((side) => _jsx(Pieces, { pieces: line.items.filter((piece) => piece.side === side) }, side)) }));
124
- }
125
- }
126
63
  /** The helps' sign before the card's title: a lifebuoy (Stefano). */
127
64
  function HelpsIcon() {
128
65
  return (_jsxs("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: [_jsx("circle", { cx: "12", cy: "12", r: "8.5" }), _jsx("circle", { cx: "12", cy: "12", r: "3.5" }), _jsx("path", { d: "M6 6L9.5 9.5M18 6L14.5 9.5M6 18L9.5 14.5M18 18L14.5 14.5" })] }));
129
66
  }
130
- /** The icon before a line of facts, in its kind's colour (Stefano): a pin's link, nothing at risk's tick, a check's plus, the loose pieces' i. */
131
- function RowIcon({ children }) {
132
- return (_jsx("span", { class: "row-icon", "aria-hidden": "true", children: _jsx("svg", { viewBox: "0 0 16 16", children: children }) }));
133
- }
134
- /** An i in a ring, before the loose pieces and the strategy's notes. */
135
- function InfoIcon() {
136
- return (_jsxs(RowIcon, { children: [_jsx("circle", { cx: "8", cy: "8", r: "6.5" }), _jsx("path", { d: "M8 7.3V11M8 4.9v.1" })] }));
137
- }
138
- /** Pieces of one side, each its figure and its square. */
139
- function Pieces({ pieces }) {
140
- return pieces.length === 0 ? null : (_jsx("span", { class: "help-pieces", children: pieces.map((piece) => (_jsxs("span", { class: "help-move", children: [_jsx(Figurine, { letter: piece.piece, side: piece.side }), _jsx("b", { class: "square-name", children: piece.square })] }, piece.square))) }));
141
- }
142
- function Squares({ squares }) {
143
- return _jsx("b", { class: "square-name", children: squares.join(", ") });
144
- }
145
- /**
146
- * The strategy (Stefano): the phases with the current one in a pill, the bar of the squares each side controls, then the lines in groups, each a name and a
147
- * tile of a half per side, and the notes as the tactics'. The player's side comes first, with no colour of good or bad (D-91).
148
- */
149
- function Strategy({ helps, onPoint, text }) {
150
- const words = text.helpsCard;
151
- const profile = helps.strategy;
152
- const { draw, pawnEnding } = profile;
153
- const point = pointing(onPoint);
154
- // A line opens under it at a click or a tap, on its name's meaning and each side's squares or files, since a phone has no pointer for a title (Rosa).
155
- const [explained, setExplained] = useState(undefined);
156
- const lines = strategyOf(profile);
157
- const sideName = (side) => side === helps.you ? `${words[side]} ${words.you}` : words[side];
158
- const notes = [
159
- profile.oppositeBishops ? words.oppositeBishops : undefined,
160
- draw.repetitions > 1 ? words.repetitions(draw.repetitions) : undefined,
161
- draw.fiftyMovesLeft <= fiftyMovesShown ? words.fiftyMoves(draw.fiftyMovesLeft) : undefined,
162
- draw.insufficient ? words.insufficient : undefined,
163
- ...(pawnEnding?.squares ?? []).map((square) => words.pawnSquare(square.pawn, square.caught)),
164
- pawnEnding?.opposition === undefined ? undefined : words.opposition(pawnEnding.opposition),
165
- ].filter((note) => note !== undefined);
166
- return (_jsxs("div", { class: "strategy", children: [_jsx(Phases, { current: profile.phase, words: words }), _jsx(ControlBar, { controlled: profile.controlled, sides: helps.sides, you: helps.you, words: words }), groups.map((group) => {
167
- const shown = lines.filter((line) => groupOf[line.row] === group);
168
- return shown.length === 0 ? null : (_jsx("ul", { class: "strategy-rows", children: shown.map((line) => {
169
- const [name, meaning] = words.rows[line.row];
170
- const open = explained === line.row;
171
- return (_jsxs("li", { class: open ? "open" : undefined, ...point(squaresIn(line)), children: [_jsxs("div", { class: "row-head", children: [_jsx("button", { type: "button", class: "term", title: meaning, "aria-expanded": open, onClick: () => setExplained(open ? undefined : line.row), children: name }), _jsx(Tile, { line: line, sides: helps.sides, sideName: sideName, words: words })] }), open ? _jsx(Meaning, { line: line, meaning: meaning, sides: helps.sides, sideName: sideName }) : null] }, line.row));
172
- }) }, group));
173
- }), notes.length === 0 ? null : (_jsx("ul", { class: "help-rows", children: notes.map((note) => (_jsxs("li", { class: "info", children: [_jsx(InfoIcon, {}), note] }, note))) }))] }));
174
- }
175
- // The strategy's groups in their order, apart only by a gap.
176
- const groups = ["pawns", "pieces", "king"];
177
- const phases = ["opening", "middlegame", "endgame"];
178
- // The fifty-move count shows only when the draw is near: ten moves each, or less.
179
- const fiftyMovesShown = 20;
180
- /** The squares a side's cells of a strategy's line name, to light on the board; files and islands light none. */
181
- function squaresIn(line) {
182
- if (line.row === "openFiles") {
183
- return [];
184
- }
185
- return [line.white, line.black].flatMap((cell) => typeof cell === "object" ? cell.filter((item) => /^[a-h][1-8]$/.test(item)) : []);
186
- }
187
- /** The game's three phases in a row of arrows: the current one full, those gone by lit, those to come not. */
188
- function Phases({ current, words }) {
189
- return (_jsx("ol", { class: "phases", children: phases.map((phase, at) => (_jsx("li", { class: at < phases.indexOf(current) ? "done" : undefined, "aria-current": phase === current ? "step" : undefined, children: words.phases[phase] }, phase))) }));
190
- }
191
- /** The squares only one side controls and those both do, in a bar as long as their counts, the player's side on the left; each part's title says it in words. */
192
- function ControlBar({ controlled, sides, you, words }) {
193
- const { white, black, contested } = controlled;
194
- const [left, right] = sides;
195
- const parts = [{ who: left, count: controlled[left] }, { who: "both", count: contested }, { who: right, count: controlled[right] }];
196
- return (_jsx("span", { class: "control-bar", role: "img", "aria-label": words.controlled(white, black, contested), children: parts.filter(({ count }) => count > 0).map(({ who, count }) => _jsx("span", { class: who, style: { flexGrow: count }, title: words.controlledBy(who, count, who === you), children: count }, who)) }));
197
- }
198
- /** A line's tile: a half for each side, the player's first, in the bar's colours; the open files, both sides', in one half as wide as the two. */
199
- function Tile({ line, sides, sideName, words }) {
200
- if (line.row === "openFiles") {
201
- return (_jsx("span", { class: "row-tile", children: _jsxs("span", { class: "both", title: words.bothSides, children: [_jsx("span", { class: "visually-hidden", children: `${words.bothSides} ` }), _jsx("b", { class: "count", children: line.both.length })] }) }));
202
- }
203
- return (_jsx("span", { class: "row-tile", children: sides.map((side) => (_jsxs("span", { class: side, title: sideName(side), children: [_jsx("span", { class: "visually-hidden", children: `${sideName(side)} ` }), _jsx(Cell, { cell: line[side], words: words })] }, side))) }));
204
- }
205
- /** A line opened: its meaning, then each listed side's squares or files in chips; the open files with no side's name. */
206
- function Meaning({ line, meaning, sides, sideName }) {
207
- if (line.row === "openFiles") {
208
- return (_jsxs("div", { class: "meaning", children: [meaning, _jsx("span", { class: "listed", children: _jsx(Chips, { items: line.both }) })] }));
209
- }
210
- return (_jsxs("div", { class: "meaning", children: [meaning, sides.map((side) => {
211
- const cell = line[side];
212
- return typeof cell === "object"
213
- ? (_jsxs("span", { class: "listed", children: [sideName(side), ": ", _jsx(Chips, { items: cell })] }, side))
214
- : null;
215
- })] }));
216
- }
217
- /** Squares, files or islands in chips; none as a faint dash. */
218
- function Chips({ items }) {
219
- return items.length === 0 ? _jsx("span", { class: "none", children: "\u2013" }) : _jsx(_Fragment, { children: items.map((item) => _jsx("b", { class: "chip", children: item }, item)) });
220
- }
221
- /** A cell of the strategy (Stefano): a count in bold, a yes as a tick, nothing or not yet as a faint dash, no more as a faint cross: marks, not words. */
222
- function Cell({ cell, words }) {
223
- const none = _jsx("span", { class: "none", children: "\u2013" });
224
- if (typeof cell === "object") {
225
- return cell.length === 0 ? none : _jsx("b", { class: "count", children: cell.length });
226
- }
227
- if (typeof cell === "number") {
228
- return cell === 0 ? none : _jsx("b", { class: "count", children: cell });
229
- }
230
- if (typeof cell === "boolean") {
231
- return cell
232
- ? (_jsx("span", { class: "yes", role: "img", "aria-label": words.yes, title: words.yes, children: _jsx("svg", { viewBox: "0 0 16 16", children: _jsx("path", { d: "M3.5 8.5l3 3 6-7" }) }) }))
233
- : none;
234
- }
235
- if (cell === "unknown") {
236
- return none;
237
- }
238
- if (cell === "not-yet") {
239
- return _jsx("span", { class: "none", role: "img", "aria-label": words.notYet, title: words.notYet, children: "\u2013" });
240
- }
241
- return cell === "no-more"
242
- ? (_jsx("span", { class: "no", role: "img", "aria-label": words.noMore, title: words.noMore, children: _jsx("svg", { viewBox: "0 0 16 16", children: _jsx("path", { d: "M4.5 4.5l7 7M11.5 4.5l-7 7" }) }) }))
243
- : _jsx("b", { class: "chip", children: cell });
244
- }
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
+ /** What the pointer on a line or a cell does: it lights the line's squares; a touch keeps them lit until the position changes, as a square in the AI's words. */
3
+ export function pointing(onPoint) {
4
+ return (squares) => ({
5
+ onPointerEnter: () => onPoint(squares),
6
+ onPointerLeave: (event) => event.pointerType === "mouse" && onPoint([]),
7
+ });
8
+ }
9
+ /** The icon before a line of facts, in its kind's colour (Stefano): a pin's link, nothing at risk's tick, a check's plus, the loose pieces' i. */
10
+ export function RowIcon({ children }) {
11
+ return (_jsx("span", { class: "row-icon", "aria-hidden": "true", children: _jsx("svg", { viewBox: "0 0 16 16", children: children }) }));
12
+ }
13
+ /** An i in a ring, before the loose pieces and the strategy's notes. */
14
+ export function InfoIcon() {
15
+ return (_jsxs(RowIcon, { children: [_jsx("circle", { cx: "8", cy: "8", r: "6.5" }), _jsx("path", { d: "M8 7.3V11M8 4.9v.1" })] }));
16
+ }
@@ -0,0 +1,105 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-runtime";
2
+ // The helps' strategy: the game's phase, the squares each side controls, and the positional facts in lines of a tile each, the player's side first.
3
+ import { useState } from "preact/hooks";
4
+ import { InfoIcon, pointing } from "./rows.js";
5
+ import { groupOf, strategyOf } from "./view.js";
6
+ /**
7
+ * The strategy (Stefano): the phases with the current one in a pill, the bar of the squares each side controls, then the lines in groups, each a name and a
8
+ * tile of a half per side, and the notes as the tactics'. The player's side comes first, with no colour of good or bad (D-91).
9
+ */
10
+ export function Strategy({ helps, onPoint, text }) {
11
+ const words = text.helpsCard;
12
+ const profile = helps.strategy;
13
+ const { draw, pawnEnding } = profile;
14
+ const point = pointing(onPoint);
15
+ // A line opens under it at a click or a tap, on its name's meaning and each side's squares or files, since a phone has no pointer for a title (Rosa).
16
+ const [explained, setExplained] = useState(undefined);
17
+ const lines = strategyOf(profile);
18
+ const sideName = (side) => side === helps.you ? `${words[side]} ${words.you}` : words[side];
19
+ const notes = [
20
+ profile.oppositeBishops ? words.oppositeBishops : undefined,
21
+ draw.repetitions > 1 ? words.repetitions(draw.repetitions) : undefined,
22
+ draw.fiftyMovesLeft <= fiftyMovesShown ? words.fiftyMoves(draw.fiftyMovesLeft) : undefined,
23
+ draw.insufficient ? words.insufficient : undefined,
24
+ ...(pawnEnding?.squares ?? []).map((square) => words.pawnSquare(square.pawn, square.caught)),
25
+ pawnEnding?.opposition === undefined ? undefined : words.opposition(pawnEnding.opposition),
26
+ ].filter((note) => note !== undefined);
27
+ return (_jsxs("div", { class: "strategy", children: [_jsx(Phases, { current: profile.phase, words: words }), _jsx(ControlBar, { controlled: profile.controlled, sides: helps.sides, you: helps.you, words: words }), groups.map((group) => {
28
+ const shown = lines.filter((line) => groupOf[line.row] === group);
29
+ return shown.length === 0 ? null : (_jsx("ul", { class: "strategy-rows", children: shown.map((line) => {
30
+ const [name, meaning] = words.rows[line.row];
31
+ const open = explained === line.row;
32
+ return (_jsxs("li", { class: open ? "open" : undefined, ...point(squaresIn(line)), children: [_jsxs("div", { class: "row-head", children: [_jsx("button", { type: "button", class: "term", title: meaning, "aria-expanded": open, onClick: () => setExplained(open ? undefined : line.row), children: name }), _jsx(Tile, { line: line, sides: helps.sides, sideName: sideName, words: words })] }), open ? _jsx(Meaning, { line: line, meaning: meaning, sides: helps.sides, sideName: sideName }) : null] }, line.row));
33
+ }) }, group));
34
+ }), notes.length === 0 ? null : (_jsx("ul", { class: "help-rows", children: notes.map((note) => (_jsxs("li", { class: "info", children: [_jsx(InfoIcon, {}), note] }, note))) }))] }));
35
+ }
36
+ // The strategy's groups in their order, apart only by a gap.
37
+ const groups = ["pawns", "pieces", "king"];
38
+ const phases = ["opening", "middlegame", "endgame"];
39
+ // The fifty-move count shows only when the draw is near: ten moves each, or less.
40
+ const fiftyMovesShown = 20;
41
+ /** The squares a side's cells of a strategy's line name, to light on the board; files and islands light none. */
42
+ function squaresIn(line) {
43
+ if (line.row === "openFiles") {
44
+ return [];
45
+ }
46
+ return [line.white, line.black].flatMap((cell) => typeof cell === "object" ? cell.filter((item) => /^[a-h][1-8]$/.test(item)) : []);
47
+ }
48
+ /** The game's three phases in a row of arrows: the current one full, those gone by lit, those to come not. */
49
+ function Phases({ current, words }) {
50
+ return (_jsx("ol", { class: "phases", children: phases.map((phase, at) => (_jsx("li", { class: at < phases.indexOf(current) ? "done" : undefined, "aria-current": phase === current ? "step" : undefined, children: words.phases[phase] }, phase))) }));
51
+ }
52
+ /** The squares only one side controls and those both do, in a bar as long as their counts, the player's side on the left; each part's title says it in words. */
53
+ function ControlBar({ controlled, sides, you, words }) {
54
+ const { white, black, contested } = controlled;
55
+ const [left, right] = sides;
56
+ const parts = [{ who: left, count: controlled[left] }, { who: "both", count: contested }, { who: right, count: controlled[right] }];
57
+ return (_jsx("span", { class: "control-bar", role: "img", "aria-label": words.controlled(white, black, contested), children: parts.filter(({ count }) => count > 0).map(({ who, count }) => _jsx("span", { class: who, style: { flexGrow: count }, title: words.controlledBy(who, count, who === you), children: count }, who)) }));
58
+ }
59
+ /** A line's tile: a half for each side, the player's first, in the bar's colours; the open files, both sides', in one half as wide as the two. */
60
+ function Tile({ line, sides, sideName, words }) {
61
+ if (line.row === "openFiles") {
62
+ return (_jsx("span", { class: "row-tile", children: _jsxs("span", { class: "both", title: words.bothSides, children: [_jsx("span", { class: "visually-hidden", children: `${words.bothSides} ` }), _jsx("b", { class: "count", children: line.both.length })] }) }));
63
+ }
64
+ return (_jsx("span", { class: "row-tile", children: sides.map((side) => (_jsxs("span", { class: side, title: sideName(side), children: [_jsx("span", { class: "visually-hidden", children: `${sideName(side)} ` }), _jsx(Cell, { cell: line[side], words: words })] }, side))) }));
65
+ }
66
+ /** A line opened: its meaning, then each listed side's squares or files in chips; the open files with no side's name. */
67
+ function Meaning({ line, meaning, sides, sideName }) {
68
+ if (line.row === "openFiles") {
69
+ return (_jsxs("div", { class: "meaning", children: [meaning, _jsx("span", { class: "listed", children: _jsx(Chips, { items: line.both }) })] }));
70
+ }
71
+ return (_jsxs("div", { class: "meaning", children: [meaning, sides.map((side) => {
72
+ const cell = line[side];
73
+ return typeof cell === "object"
74
+ ? (_jsxs("span", { class: "listed", children: [sideName(side), ": ", _jsx(Chips, { items: cell })] }, side))
75
+ : null;
76
+ })] }));
77
+ }
78
+ /** Squares, files or islands in chips; none as a faint dash. */
79
+ function Chips({ items }) {
80
+ return items.length === 0 ? _jsx("span", { class: "none", children: "\u2013" }) : _jsx(_Fragment, { children: items.map((item) => _jsx("b", { class: "chip", children: item }, item)) });
81
+ }
82
+ /** A cell of the strategy (Stefano): a count in bold, a yes as a tick, nothing or not yet as a faint dash, no more as a faint cross: marks, not words. */
83
+ function Cell({ cell, words }) {
84
+ const none = _jsx("span", { class: "none", children: "\u2013" });
85
+ if (typeof cell === "object") {
86
+ return cell.length === 0 ? none : _jsx("b", { class: "count", children: cell.length });
87
+ }
88
+ if (typeof cell === "number") {
89
+ return cell === 0 ? none : _jsx("b", { class: "count", children: cell });
90
+ }
91
+ if (typeof cell === "boolean") {
92
+ return cell
93
+ ? (_jsx("span", { class: "yes", role: "img", "aria-label": words.yes, title: words.yes, children: _jsx("svg", { viewBox: "0 0 16 16", children: _jsx("path", { d: "M3.5 8.5l3 3 6-7" }) }) }))
94
+ : none;
95
+ }
96
+ if (cell === "unknown") {
97
+ return none;
98
+ }
99
+ if (cell === "not-yet") {
100
+ return _jsx("span", { class: "none", role: "img", "aria-label": words.notYet, title: words.notYet, children: "\u2013" });
101
+ }
102
+ return cell === "no-more"
103
+ ? (_jsx("span", { class: "no", role: "img", "aria-label": words.noMore, title: words.noMore, children: _jsx("svg", { viewBox: "0 0 16 16", children: _jsx("path", { d: "M4.5 4.5l7 7M11.5 4.5l-7 7" }) }) }))
104
+ : _jsx("b", { class: "chip", children: cell });
105
+ }
@@ -0,0 +1,69 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-runtime";
2
+ import { Notch, Pushpin, Skewer } from "../board/marks.js";
3
+ import { Figurines } from "../game/move-list.js";
4
+ import { Figurine } from "../pieces/pieces.js";
5
+ import { InfoIcon, pointing, RowIcon } from "./rows.js";
6
+ import { pinSquares, riskSquares, squaresOf } from "./view.js";
7
+ /**
8
+ * The tactics in lines (D-91): one a kind of fact and a side, its count, red when it threatens the player and green when it is their chance; a click opens
9
+ * its pieces, each with its squares (Stefano).
10
+ */
11
+ export function Tactics({ helps, opened, onOpen, onPoint, text }) {
12
+ const words = text.helpsCard;
13
+ const point = pointing(onPoint);
14
+ const calm = !helps.lines.some((line) => line.kind === "risk" || line.kind === "pin" || line.kind === "skewer");
15
+ return (_jsxs("ul", { class: "help-rows", children: [calm
16
+ ? (_jsxs("li", { class: "safe", children: [_jsxs(RowIcon, { children: [_jsx("circle", { cx: "8", cy: "8", r: "6.5" }), _jsx("path", { d: "M5.2 8.2l2 2 3.8-4" })] }), words.nothingAtRisk] }))
17
+ : null, helps.lines.map((line) => {
18
+ const key = line.kind === "loose" ? "loose" : `${line.kind} ${line.side}`;
19
+ const open = opened.has(key);
20
+ return (_jsxs("li", { class: `help-line ${line.kind} ${line.tone}${open ? " open" : ""}`, children: [_jsxs("button", { type: "button", class: "line-head", "aria-expanded": open, onClick: () => onOpen(key, !open), ...point(squaresOf(line)), children: [_jsx(LineIcon, { kind: line.kind }), _jsx("span", { class: "line-name", children: line.kind === "loose" ? words.loose : words.line(line.kind, line.side, helps.you, line.kind !== "check" || line.toMove) }), _jsx(LineCount, { line: line, helps: helps, words: words })] }), open ? _jsx(LineDetails, { line: line, helps: helps, point: point, text: text }) : null] }, key));
21
+ })] }));
22
+ }
23
+ /** A line's sign, as the board draws it: the corner of a piece at risk, the pushpin, the skewer; a check's plus, the loose pieces' i; its colour is the line's. */
24
+ function LineIcon({ kind }) {
25
+ switch (kind) {
26
+ case "risk":
27
+ return _jsx(Notch, {});
28
+ case "pin":
29
+ return _jsx(Pushpin, {});
30
+ case "skewer":
31
+ return _jsx(Skewer, {});
32
+ case "loose":
33
+ return _jsx(InfoIcon, {});
34
+ case "check":
35
+ return (_jsxs(RowIcon, { children: [_jsx("circle", { cx: "8", cy: "8", r: "6.5" }), _jsx("path", { d: "M8 5v6M5 8h6" })] }));
36
+ }
37
+ }
38
+ /** How many: the loose pieces by side, each after its pawn, the player's first (Zoe); the others in one number. */
39
+ function LineCount({ line, helps, words }) {
40
+ if (line.kind !== "loose") {
41
+ return _jsx("b", { class: "count", children: line.items.length });
42
+ }
43
+ const name = (side) => helps.you === undefined ? words[side] : side === helps.you ? words.yours : words.theirs;
44
+ const counts = helps.sides.map((side) => ({ side, count: line.items.filter((piece) => piece.side === side).length })).filter(({ count }) => count > 0);
45
+ return (_jsx("span", { class: "counts", children: counts.map(({ side, count }) => (_jsxs("span", { title: name(side), "aria-label": `${name(side)} ${count}`, children: [_jsx(Figurine, { letter: "P", side: side }), _jsx("b", { class: "count", children: count })] }, side))) }));
46
+ }
47
+ /** A line opened: its pieces, each lighting its own squares; the checks in figures; the loose pieces by side. */
48
+ function LineDetails({ line, helps, point, text }) {
49
+ const words = text.helpsCard;
50
+ switch (line.kind) {
51
+ case "risk":
52
+ return (_jsx("ul", { class: "line-details", children: line.items.map((risk) => (_jsxs("li", { ...point(riskSquares(risk)), children: [text.piece(risk.piece.piece, risk.piece.side), " ", _jsx(Squares, { squares: [risk.piece.square] }), " \u00B7 ", words.attackers, ": ", _jsx(Squares, { squares: risk.piece.attackers }), risk.why === "undefended" ? null : (_jsxs(_Fragment, { children: [" ", "\u00B7 ", words.defenders, ": ", _jsx(Squares, { squares: risk.piece.defenders })] })), " \u00B7 ", words[risk.why]] }, risk.piece.square))) }));
53
+ case "pin":
54
+ return (_jsx("ul", { class: "line-details", children: line.items.map((pin) => (_jsxs("li", { ...point(pinSquares(pin)), children: [text.piece(pin.piece.piece, pin.piece.side), " ", _jsx(Squares, { squares: [pin.piece.square] }), " ", words.pinnedTo(pin.to.piece), " ", _jsx(Squares, { squares: [pin.to.square] }), ", ", words.pinnedBy(pin.by.piece), " ", _jsx(Squares, { squares: [pin.by.square] })] }, pin.piece.square))) }));
55
+ case "skewer":
56
+ return (_jsx("ul", { class: "line-details", children: line.items.map((skewer) => (_jsxs("li", { ...point(pinSquares(skewer)), children: [text.piece(skewer.piece.piece, skewer.piece.side), " ", _jsx(Squares, { squares: [skewer.piece.square] }), " ", words.skeweredBefore(skewer.to.piece), " ", _jsx(Squares, { squares: [skewer.to.square] }), ", ", words.pinnedBy(skewer.by.piece), " ", _jsx(Squares, { squares: [skewer.by.square] })] }, skewer.piece.square))) }));
57
+ case "check":
58
+ return (_jsx("p", { class: "line-details", children: line.items.map((check) => (_jsx("span", { class: "help-move", children: _jsx(Figurines, { san: check, side: line.side }) }, check))) }));
59
+ case "loose":
60
+ return (_jsx("p", { class: "line-details", children: helps.sides.map((side) => _jsx(Pieces, { pieces: line.items.filter((piece) => piece.side === side) }, side)) }));
61
+ }
62
+ }
63
+ /** Pieces of one side, each its figure and its square. */
64
+ function Pieces({ pieces }) {
65
+ return pieces.length === 0 ? null : (_jsx("span", { class: "help-pieces", children: pieces.map((piece) => (_jsxs("span", { class: "help-move", children: [_jsx(Figurine, { letter: piece.piece, side: piece.side }), _jsx("b", { class: "square-name", children: piece.square })] }, piece.square))) }));
66
+ }
67
+ function Squares({ squares }) {
68
+ return _jsx("b", { class: "square-name", children: squares.join(", ") });
69
+ }
@@ -1,22 +1,65 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "preact/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "preact/hooks";
2
3
  import { PlayerPicture } from "../avatars/avatars.js";
3
4
  import { Torretta } from "../avatars/torretta.js";
4
5
  import { UpdateMark } from "../table/update.js";
5
6
  /**
6
7
  * The header, with how a game over ended, or that none goes on (D-77), or the AI's draw offer with the player's answer, the player's draw
7
- * and resignation in a game going on (D-65), a new game once the page has the folder's, the command that turns the board round and says whether it is
8
+ * and resignation in a game going on (D-65), which the header asks to confirm in the place of the word and the commands, a new game once the page has the folder's, the command that turns the board round and says whether it is
8
9
  * turned, on the table the mark of a newer Chesspal (D-108), the switches of the sounds and the theme, the gear of the settings (D-56) and the book of
9
10
  * the guide.
10
11
  */
11
12
  export function Header({ text, verdict, kind, who, actions, onNewGame, standOut = false, onStudy, flipped, onFlip, sound, onSound, onTheme, onSettings, onGuide, profile, onProfile, update, onUpdate }) {
12
- return (_jsxs("header", { class: "top", children: [_jsx(Torretta, { size: 34 }), _jsx("b", { class: "title", children: "Chesspal" }), _jsxs("p", { class: `verdict${kind === undefined ? "" : ` ${kind}`}`, role: "status", children: [verdict === undefined || kind === undefined ? null : _jsx(Icon, { class: "kind", children: kindIcons[kind] }), who === undefined || verdict === undefined ? verdict : (_jsxs(_Fragment, { children: [_jsx("span", { class: "ai-name", children: who }), _jsx("span", { children: verdict })] }))] }), actions === undefined ? null : _jsx(Actions, { actions: actions, text: text }), onNewGame === undefined ? null : (_jsxs("button", { class: standOut ? "command primary" : "command", type: "button", title: text.newGame, onClick: onNewGame, children: [_jsx(Icon, { class: "new", children: _jsx("path", { d: "M12 5V19M5 12H19" }) }), _jsx("span", { children: text.newGame })] })), onStudy === undefined ? null : (_jsx("button", { class: "icon-button", type: "button", title: text.study.title, "aria-label": text.study.title, "aria-haspopup": "dialog", onClick: onStudy, children: _jsxs(Icon, { class: "study", children: [_jsx("circle", { cx: "10.5", cy: "10.5", r: "5.5" }), _jsx("path", { d: "M14.5 14.5L20 20" })] }) })), _jsx("button", { class: "icon-button flip-button", type: "button", title: text.flip, "aria-label": text.flip, "aria-pressed": flipped, onClick: onFlip, children: _jsx(Icon, { class: "flip", children: _jsx("path", { d: "M7 4V17M7 17L4 14M7 17L10 14M17 20V7M17 7L14 10M17 7L20 10" }) }) }), update === undefined || onUpdate === undefined ? null : _jsx(UpdateMark, { version: update.version, installing: update.installing, onOpen: onUpdate, text: text }), _jsx("button", { class: "icon-button sound-button", type: "button", title: text.sounds, "aria-label": text.sounds, "aria-pressed": sound, onClick: onSound, children: _jsxs(Icon, { class: "speaker", children: [_jsx("path", { d: "M5 9.5H8.5L13 5.5V18.5L8.5 14.5H5Z" }), sound ? _jsx("path", { d: "M16.5 9A4 4 0 0 1 16.5 15M19 6.5A7.5 7.5 0 0 1 19 17.5" }) : _jsx("path", { d: "M15.5 9L21.5 15M21.5 9L15.5 15" })] }) }), _jsxs("button", { class: "icon-button theme-button", type: "button", title: text.theme, "aria-label": text.theme, onClick: onTheme, children: [_jsxs(Icon, { class: "sun", children: [_jsx("circle", { cx: "12", cy: "12", r: "4" }), _jsx("path", { d: "M12 2V4M12 20V22M2 12H4M20 12H22M4.9 4.9L6.3 6.3M17.7 17.7L19.1 19.1M4.9 19.1L6.3 17.7M17.7 6.3L19.1 4.9" })] }), _jsx(Icon, { class: "moon", children: _jsx("path", { d: "M20 14.5A8 8 0 1 1 9.5 4A6.5 6.5 0 0 0 20 14.5Z" }) })] }), _jsx("button", { class: "icon-button", type: "button", title: text.settings, "aria-label": text.settings, onClick: onSettings, children: _jsxs(Icon, { class: "gear", children: [_jsx("circle", { cx: "12", cy: "12", r: "3" }), _jsx("path", { d: "M9.72 5.38L10.19 2.67L13.81 2.67L14.28 5.38L15.07 5.71L17.31 4.12L19.88 6.69L18.29 8.93L18.62 9.72L21.33 10.19L21.33 13.81L18.62 14.28L18.29 15.07L19.88 17.31L17.31 19.88L15.07 18.29L14.28 18.62L13.81 21.33L10.19 21.33L9.72 18.62L8.93 18.29L6.69 19.88L4.12 17.31L5.71 15.07L5.38 14.28L2.67 13.81L2.67 10.19L5.38 9.72L5.71 8.93L4.12 6.69L6.69 4.12L8.93 5.71Z" })] }) }), _jsx("button", { class: "icon-button guide-button", type: "button", title: text.guide.title, "aria-label": text.guide.title, "aria-haspopup": "dialog", onClick: onGuide, children: _jsx(Icon, { class: "book", children: _jsx("path", { d: "M12 6.5C10 5 7 4.5 3.5 5V18.5C7 18 10 18.5 12 20C14 18.5 17 18 20.5 18.5V5C17 4.5 14 5 12 6.5ZM12 6.5V20" }) }) }), profile === undefined || onProfile === undefined ? null : (_jsx("button", { class: "icon-button player", type: "button", title: `${profile.name}: ${text.profile.profiles}`, "aria-label": `${profile.name}: ${text.profile.profiles}`, "aria-haspopup": "dialog", onClick: onProfile, children: _jsx(PlayerPicture, { profile: profile, size: 28 }) }))] }));
13
+ // The flag pressed asks the player in the header, as the AI's draw offer does; a game that ends meanwhile drops the question.
14
+ const [resigning, setResigning] = useState(false);
15
+ const asking = resigning && actions !== undefined;
16
+ // After "play on" the flag takes the focus back.
17
+ const backToFlag = useRef(false);
18
+ useEffect(() => {
19
+ if (actions === undefined) {
20
+ setResigning(false);
21
+ }
22
+ }, [actions === undefined]);
23
+ const answer = (resign) => {
24
+ setResigning(false);
25
+ backToFlag.current = !resign;
26
+ if (resign) {
27
+ actions?.onResign();
28
+ }
29
+ };
30
+ const shownKind = asking ? "over" : kind;
31
+ const shownVerdict = asking ? text.resignGame : verdict;
32
+ const shownWho = asking ? undefined : who;
33
+ return (_jsxs("header", { class: "top", children: [_jsx(Torretta, { size: 34 }), _jsx("b", { class: "title", children: "Chesspal" }), _jsxs("p", { class: `verdict${shownKind === undefined ? "" : ` ${shownKind}`}`, role: "status", children: [shownVerdict === undefined || shownKind === undefined ? null : _jsx(Icon, { class: "kind", children: kindIcons[shownKind] }), shownWho === undefined || shownVerdict === undefined ? shownVerdict : (_jsxs(_Fragment, { children: [_jsx("span", { class: "ai-name", children: shownWho }), _jsx("span", { children: shownVerdict })] }))] }), actions === undefined ? null : asking ? _jsx(ResignQuestion, { onAnswer: answer, text: text }) : _jsx(Actions, { actions: actions, onResign: () => setResigning(true), backToFlag: backToFlag, text: text }), onNewGame === undefined ? null : (_jsxs("button", { class: standOut ? "command primary" : "command", type: "button", title: text.newGame, onClick: onNewGame, children: [_jsx(Icon, { class: "new", children: _jsx("path", { d: "M12 5V19M5 12H19" }) }), _jsx("span", { children: text.newGame })] })), onStudy === undefined ? null : (_jsx("button", { class: "icon-button", type: "button", title: text.study.title, "aria-label": text.study.title, "aria-haspopup": "dialog", onClick: onStudy, children: _jsxs(Icon, { class: "study", children: [_jsx("circle", { cx: "10.5", cy: "10.5", r: "5.5" }), _jsx("path", { d: "M14.5 14.5L20 20" })] }) })), _jsx("button", { class: "icon-button flip-button", type: "button", title: text.flip, "aria-label": text.flip, "aria-pressed": flipped, onClick: onFlip, children: _jsx(Icon, { class: "flip", children: _jsx("path", { d: "M7 4V17M7 17L4 14M7 17L10 14M17 20V7M17 7L14 10M17 7L20 10" }) }) }), update === undefined || onUpdate === undefined ? null : _jsx(UpdateMark, { version: update.version, installing: update.installing, onOpen: onUpdate, text: text }), _jsx("button", { class: "icon-button sound-button", type: "button", title: text.sounds, "aria-label": text.sounds, "aria-pressed": sound, onClick: onSound, children: _jsxs(Icon, { class: "speaker", children: [_jsx("path", { d: "M5 9.5H8.5L13 5.5V18.5L8.5 14.5H5Z" }), sound ? _jsx("path", { d: "M16.5 9A4 4 0 0 1 16.5 15M19 6.5A7.5 7.5 0 0 1 19 17.5" }) : _jsx("path", { d: "M15.5 9L21.5 15M21.5 9L15.5 15" })] }) }), _jsxs("button", { class: "icon-button theme-button", type: "button", title: text.theme, "aria-label": text.theme, onClick: onTheme, children: [_jsxs(Icon, { class: "sun", children: [_jsx("circle", { cx: "12", cy: "12", r: "4" }), _jsx("path", { d: "M12 2V4M12 20V22M2 12H4M20 12H22M4.9 4.9L6.3 6.3M17.7 17.7L19.1 19.1M4.9 19.1L6.3 17.7M17.7 6.3L19.1 4.9" })] }), _jsx(Icon, { class: "moon", children: _jsx("path", { d: "M20 14.5A8 8 0 1 1 9.5 4A6.5 6.5 0 0 0 20 14.5Z" }) })] }), _jsx("button", { class: "icon-button", type: "button", title: text.settings, "aria-label": text.settings, onClick: onSettings, children: _jsxs(Icon, { class: "gear", children: [_jsx("circle", { cx: "12", cy: "12", r: "3" }), _jsx("path", { d: "M9.72 5.38L10.19 2.67L13.81 2.67L14.28 5.38L15.07 5.71L17.31 4.12L19.88 6.69L18.29 8.93L18.62 9.72L21.33 10.19L21.33 13.81L18.62 14.28L18.29 15.07L19.88 17.31L17.31 19.88L15.07 18.29L14.28 18.62L13.81 21.33L10.19 21.33L9.72 18.62L8.93 18.29L6.69 19.88L4.12 17.31L5.71 15.07L5.38 14.28L2.67 13.81L2.67 10.19L5.38 9.72L5.71 8.93L4.12 6.69L6.69 4.12L8.93 5.71Z" })] }) }), _jsx("button", { class: "icon-button guide-button", type: "button", title: text.guide.title, "aria-label": text.guide.title, "aria-haspopup": "dialog", onClick: onGuide, children: _jsx(Icon, { class: "book", children: _jsx("path", { d: "M12 6.5C10 5 7 4.5 3.5 5V18.5C7 18 10 18.5 12 20C14 18.5 17 18 20.5 18.5V5C17 4.5 14 5 12 6.5ZM12 6.5V20" }) }) }), profile === undefined || onProfile === undefined ? null : (_jsx("button", { class: "icon-button player", type: "button", title: `${profile.name}: ${text.profile.profiles}`, "aria-label": `${profile.name}: ${text.profile.profiles}`, "aria-haspopup": "dialog", onClick: onProfile, children: _jsx(PlayerPicture, { profile: profile, size: 28 }) }))] }));
13
34
  }
14
- /** The answer to the AI's draw offer while it stands, else the player's own offer; then the resignation. */
15
- function Actions({ actions, text }) {
16
- const { offer, canOffer, onOffer, onAnswer, onResign } = actions;
35
+ /** The answer to the AI's draw offer while it stands, else the player's own offer; then the flag, which asks before the player resigns. */
36
+ function Actions({ actions, onResign, backToFlag, text }) {
37
+ const { offer, canOffer, onOffer, onAnswer } = actions;
38
+ const flag = useRef(null);
39
+ useEffect(() => {
40
+ if (backToFlag.current) {
41
+ backToFlag.current = false;
42
+ flag.current?.focus();
43
+ }
44
+ }, []);
17
45
  return (_jsxs(_Fragment, { children: [offer === "ai"
18
46
  ? (_jsxs(_Fragment, { children: [_jsxs("button", { class: "command answer", type: "button", title: text.acceptDraw, onClick: () => onAnswer(true), children: [_jsx(Icon, { class: "accept", children: _jsx("path", { d: "M5 12.5L10 17L19 7" }) }), _jsx("span", { children: text.acceptDraw })] }), _jsxs("button", { class: "command answer", type: "button", title: text.declineDrawTitle, onClick: () => onAnswer(false), children: [_jsx(Icon, { class: "decline", children: _jsx("path", { d: "M7 7L17 17M17 7L7 17" }) }), _jsx("span", { children: text.declineDraw })] })] }))
19
- : (_jsxs("button", { class: "command", type: "button", title: offer === "player" ? text.drawOffered : canOffer ? text.offerDraw : text.offerDrawWhen, "aria-pressed": offer === "player", "aria-disabled": !canOffer, onClick: offer === "player" ? undefined : onOffer, children: [_jsx(Icon, { class: "half", children: _jsx("path", { d: "M5 7L7.5 5V11M16 5L8 19M13.5 14.5C13.5 13 17.5 12.5 17.5 15C17.5 16.5 13.5 18 13.5 19H18" }) }), _jsx("span", { children: text.offerDraw })] })), _jsxs("button", { class: "command", type: "button", title: text.resign, onClick: onResign, children: [_jsx(Icon, { class: "resign", children: _jsx("path", { d: "M6 21V4M6 4H18L15.5 8.5L18 13H6" }) }), _jsx("span", { children: text.resign })] })] }));
47
+ : (_jsxs("button", { class: "command", type: "button", title: offer === "player" ? text.drawOffered : canOffer ? text.offerDraw : text.offerDrawWhen, "aria-pressed": offer === "player", "aria-disabled": !canOffer, onClick: offer === "player" ? undefined : onOffer, children: [_jsx(Icon, { class: "half", children: _jsx("path", { d: "M5 7L7.5 5V11M16 5L8 19M13.5 14.5C13.5 13 17.5 12.5 17.5 15C17.5 16.5 13.5 18 13.5 19H18" }) }), _jsx("span", { children: text.offerDraw })] })), _jsxs("button", { ref: flag, class: "command", type: "button", title: text.resign, onClick: onResign, children: [_jsx(Icon, { class: "resign", children: _jsx("path", { d: "M6 21V4M6 4H18L15.5 8.5L18 13H6" }) }), _jsx("span", { children: text.resign })] })] }));
48
+ }
49
+ /** The question the flag asks, as the answer to a draw offer: resign, or play on, which has the focus and Esc too. */
50
+ function ResignQuestion({ onAnswer, text }) {
51
+ const playOn = useRef(null);
52
+ useEffect(() => {
53
+ playOn.current?.focus();
54
+ const escape = (event) => {
55
+ if (event.key === "Escape") {
56
+ onAnswer(false);
57
+ }
58
+ };
59
+ window.addEventListener("keydown", escape);
60
+ return () => window.removeEventListener("keydown", escape);
61
+ }, []);
62
+ return (_jsxs(_Fragment, { children: [_jsxs("button", { class: "command answer", type: "button", title: text.resign, onClick: () => onAnswer(true), children: [_jsx(Icon, { class: "accept", children: _jsx("path", { d: "M5 12.5L10 17L19 7" }) }), _jsx("span", { children: text.resign })] }), _jsxs("button", { ref: playOn, class: "command answer", type: "button", title: text.keepPlaying, onClick: () => onAnswer(false), children: [_jsx(Icon, { class: "decline", children: _jsx("path", { d: "M7 7L17 17M17 7L7 17" }) }), _jsx("span", { children: text.keepPlaying })] })] }));
20
63
  }
21
64
  // A pair of scales, a flag, two hands, a pause.
22
65
  const kindIcons = {
@@ -975,12 +975,14 @@
975
975
  margin: 0;
976
976
  }
977
977
 
978
+ /* The steps, and under them the chart of the game */
978
979
  .board-steps {
979
- display: block;
980
+ display: grid;
981
+ gap: var(--between);
980
982
  grid-area: steps;
981
983
  }
982
984
 
983
- .app:has(.talk.studied) .side .steps {
985
+ .app:has(.talk.studied) .side :is(.steps, .lead-card) {
984
986
  display: none;
985
987
  }
986
988
  }
@@ -1041,3 +1043,16 @@
1041
1043
  justify-content: center;
1042
1044
  }
1043
1045
  }
1046
+
1047
+ /* On the narrowest phones a table's header holds one more button, the newer Chesspal's: the buttons draw a little closer and smaller, the picture stays on the line */
1048
+ @media (max-width: 359px) {
1049
+ .top {
1050
+ column-gap: 2px;
1051
+ }
1052
+
1053
+ .top .command,
1054
+ .top .icon-button {
1055
+ width: 30px;
1056
+ height: 30px;
1057
+ }
1058
+ }
@@ -5,15 +5,15 @@ const mark = "opened";
5
5
  const wanted = { width: 1600, height: 900 };
6
6
  /** Sizes the window the table opened and takes the mark off the address, so that a reload keeps the size the player gave it. */
7
7
  export function fitOpenedWindow() {
8
- const address = new URL(location.href);
8
+ const address = new URL(window.location.href);
9
9
  if (!address.searchParams.has(mark)) {
10
10
  return;
11
11
  }
12
12
  address.searchParams.delete(mark);
13
- history.replaceState(history.state, "", address.pathname + address.search + address.hash);
14
- const place = windowPlace(screen.availWidth, screen.availHeight);
13
+ window.history.replaceState(window.history.state, "", address.pathname + address.search + address.hash);
14
+ const place = windowPlace(window.screen.availWidth, window.screen.availHeight);
15
15
  // Chrome and Edge say where the window's screen starts: on a second screen the window stays there (Zoe).
16
- const { availLeft = 0, availTop = 0 } = screen;
16
+ const { availLeft = 0, availTop = 0 } = window.screen;
17
17
  window.resizeTo(place.width, place.height);
18
18
  window.moveTo(availLeft + place.left, availTop + place.top);
19
19
  }