@hackersheet/next-document-content-kifu 0.1.0-alpha.15 → 0.1.0-alpha.17

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 (46) hide show
  1. package/dist/cjs/components/kifu/kifu.js +2 -2
  2. package/dist/cjs/components/shogi-player/adapters/kifu-adapter.d.ts +11 -0
  3. package/dist/cjs/components/shogi-player/adapters/kifu-adapter.js +104 -0
  4. package/dist/cjs/components/shogi-player/board-renderer.d.ts +60 -0
  5. package/dist/cjs/components/shogi-player/board-renderer.js +137 -0
  6. package/dist/cjs/components/shogi-player/button.d.ts +22 -1
  7. package/dist/cjs/components/shogi-player/button.js +2 -1
  8. package/dist/cjs/components/shogi-player/canvas-utils.d.ts +29 -0
  9. package/dist/cjs/components/shogi-player/{index.js → canvas-utils.js} +26 -16
  10. package/dist/cjs/components/shogi-player/hands-renderer.d.ts +42 -0
  11. package/dist/cjs/components/shogi-player/hands-renderer.js +86 -0
  12. package/dist/cjs/components/shogi-player/moves-area.d.ts +24 -3
  13. package/dist/cjs/components/shogi-player/moves-area.js +15 -20
  14. package/dist/cjs/components/shogi-player/shogi-board-canvas.d.ts +20 -13
  15. package/dist/cjs/components/shogi-player/shogi-board-canvas.js +9 -119
  16. package/dist/cjs/components/shogi-player/shogi-hands-canvas.d.ts +20 -12
  17. package/dist/cjs/components/shogi-player/shogi-hands-canvas.js +7 -79
  18. package/dist/cjs/components/shogi-player/shogi-player.d.ts +22 -0
  19. package/dist/cjs/components/shogi-player/shogi-player.js +51 -41
  20. package/dist/cjs/components/shogi-player/types.d.ts +169 -0
  21. package/dist/cjs/components/shogi-player/types.js +16 -0
  22. package/dist/esm/components/kifu/kifu.mjs +1 -1
  23. package/dist/esm/components/shogi-player/adapters/kifu-adapter.d.mts +11 -0
  24. package/dist/esm/components/shogi-player/adapters/kifu-adapter.mjs +80 -0
  25. package/dist/esm/components/shogi-player/board-renderer.d.mts +60 -0
  26. package/dist/esm/components/shogi-player/board-renderer.mjs +109 -0
  27. package/dist/esm/components/shogi-player/button.d.mts +22 -1
  28. package/dist/esm/components/shogi-player/button.mjs +2 -1
  29. package/dist/esm/components/shogi-player/canvas-utils.d.mts +29 -0
  30. package/dist/esm/components/shogi-player/canvas-utils.mjs +22 -0
  31. package/dist/esm/components/shogi-player/hands-renderer.d.mts +42 -0
  32. package/dist/esm/components/shogi-player/hands-renderer.mjs +60 -0
  33. package/dist/esm/components/shogi-player/moves-area.d.mts +24 -3
  34. package/dist/esm/components/shogi-player/moves-area.mjs +16 -17
  35. package/dist/esm/components/shogi-player/shogi-board-canvas.d.mts +20 -13
  36. package/dist/esm/components/shogi-player/shogi-board-canvas.mjs +12 -116
  37. package/dist/esm/components/shogi-player/shogi-hands-canvas.d.mts +20 -12
  38. package/dist/esm/components/shogi-player/shogi-hands-canvas.mjs +4 -76
  39. package/dist/esm/components/shogi-player/shogi-player.d.mts +22 -0
  40. package/dist/esm/components/shogi-player/shogi-player.mjs +52 -42
  41. package/dist/esm/components/shogi-player/types.d.mts +169 -0
  42. package/dist/esm/components/shogi-player/types.mjs +0 -0
  43. package/package.json +3 -3
  44. package/dist/cjs/components/shogi-player/index.d.ts +0 -2
  45. package/dist/esm/components/shogi-player/index.d.mts +0 -2
  46. package/dist/esm/components/shogi-player/index.mjs +0 -4
@@ -1,11 +1,32 @@
1
- import { IMoveFormat } from 'json-kifu-format/dist/src/Formats';
2
1
  import React from 'react';
3
2
 
3
+ /**
4
+ * Props for the MovesArea component
5
+ * @property readableMoves - Array of human-readable move strings (e.g., "☗7六歩")
6
+ * @property tesuu - Current move number
7
+ * @property onTesuuChange - Callback when the user selects a different move
8
+ */
4
9
  type MovesAreaProps = {
5
- moves: IMoveFormat[];
10
+ readableMoves: string[];
6
11
  tesuu: number;
7
12
  onTesuuChange?: (tesuu: number) => void;
8
13
  };
14
+ /**
15
+ * Scrollable list of moves showing the game record with current position highlighting
16
+ *
17
+ * @component
18
+ * @param props - Component props
19
+ * @returns A scrollable moves list with keyboard navigation support
20
+ *
21
+ * @example
22
+ * ```tsx
23
+ * <MovesArea
24
+ * readableMoves={['開始局面', '☗7六歩', '☖3四歩']}
25
+ * tesuu={1}
26
+ * onTesuuChange={(move) => setCurrentMove(move)}
27
+ * />
28
+ * ```
29
+ */
9
30
  declare function MovesArea(props: MovesAreaProps): React.JSX.Element;
10
31
 
11
- export { MovesArea, type MovesAreaProps };
32
+ export { type MovesAreaProps, MovesArea as default };
@@ -1,8 +1,6 @@
1
1
  "use client";
2
- import { JKFPlayer } from "json-kifu-format";
3
- import React, { Fragment, useEffect, useRef } from "react";
2
+ import React, { useEffect, useRef } from "react";
4
3
  function MovesArea(props) {
5
- const moves = props.moves;
6
4
  const scrollRef = useRef(null);
7
5
  const containerRef = useRef(null);
8
6
  useEffect(() => {
@@ -16,29 +14,30 @@ function MovesArea(props) {
16
14
  });
17
15
  }
18
16
  }, [props.tesuu]);
19
- const current = 0 === props.tesuu ? " bg-amber-600" : "";
17
+ const initialCurrent = 0 === props.tesuu ? " bg-amber-600" : "";
20
18
  return /* @__PURE__ */ React.createElement("div", { className: "absolute overflow-y-auto h-full w-full border-2 border-black text-black", ref: containerRef }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-0 text-xs" }, /* @__PURE__ */ React.createElement(
21
19
  "div",
22
20
  {
23
21
  onClick: () => props.onTesuuChange && props.onTesuuChange(0),
24
- className: "col-span-500 grid grid-cols-subgrid gap-2 py-1 px-2 cursor-pointer hover:bg-amber-100" + current
22
+ className: "col-span-500 grid grid-cols-subgrid gap-2 py-1 px-2 cursor-pointer hover:bg-amber-100" + initialCurrent
25
23
  },
26
- /* @__PURE__ */ React.createElement("div", null, 0 === props.tesuu && /* @__PURE__ */ React.createElement("div", { ref: scrollRef })),
27
- /* @__PURE__ */ React.createElement("div", null, "\u958B\u59CB\u5C40\u9762")
28
- ), moves.map((move, index) => {
29
- if (index === 0) return;
30
- const current2 = index === props.tesuu ? " bg-amber-600" : "";
31
- return /* @__PURE__ */ React.createElement(Fragment, { key: index }, /* @__PURE__ */ React.createElement(
24
+ /* @__PURE__ */ React.createElement("div", null, 0 === props.tesuu && /* @__PURE__ */ React.createElement("span", { ref: scrollRef, className: "sr-only", "aria-hidden": "true" })),
25
+ /* @__PURE__ */ React.createElement("div", null, props.readableMoves[0])
26
+ ), props.readableMoves.slice(1).map((moveText, index) => {
27
+ const moveIndex = index + 1;
28
+ const moveCurrent = moveIndex === props.tesuu ? " bg-amber-600" : "";
29
+ return /* @__PURE__ */ React.createElement(
32
30
  "div",
33
31
  {
34
- className: "col-span-500 grid grid-cols-subgrid border-black gap-2 border-t py-1 px-2 cursor-pointer hover:bg-amber-100" + current2,
35
- onClick: () => props.onTesuuChange && props.onTesuuChange(index)
32
+ key: moveIndex,
33
+ className: "col-span-500 grid grid-cols-subgrid border-black gap-2 border-t py-1 px-2 cursor-pointer hover:bg-amber-100" + moveCurrent,
34
+ onClick: () => props.onTesuuChange && props.onTesuuChange(moveIndex)
36
35
  },
37
- /* @__PURE__ */ React.createElement("div", { className: "flex" }, index === props.tesuu && /* @__PURE__ */ React.createElement("div", { ref: scrollRef }), /* @__PURE__ */ React.createElement("div", { className: "tabular-nums text-right flex-auto" }, index)),
38
- /* @__PURE__ */ React.createElement("div", null, JKFPlayer.moveToReadableKifu(move))
39
- ));
36
+ /* @__PURE__ */ React.createElement("div", { className: "flex" }, moveIndex === props.tesuu && /* @__PURE__ */ React.createElement("span", { ref: scrollRef, className: "sr-only", "aria-hidden": "true" }), /* @__PURE__ */ React.createElement("div", { className: "tabular-nums text-right flex-auto" }, moveIndex)),
37
+ /* @__PURE__ */ React.createElement("div", null, moveText)
38
+ );
40
39
  })));
41
40
  }
42
41
  export {
43
- MovesArea
42
+ MovesArea as default
44
43
  };
@@ -1,17 +1,24 @@
1
- import { IMoveMoveFormat } from 'json-kifu-format/dist/src/Formats';
2
1
  import React from 'react';
3
- import { Piece } from 'shogi.js';
2
+ import { ShogiBoardCanvasProps } from './types.mjs';
3
+ import 'shogi.js';
4
4
 
5
- type Props = {
6
- size?: number;
7
- boardColor?: string;
8
- lineColor?: string;
9
- fontFamily?: string;
10
- fontSizeRatio?: number;
11
- pieces: Piece[][];
12
- isSente?: boolean;
13
- currentMove?: IMoveMoveFormat;
14
- };
15
- declare const ShogiBoardCanvas: React.FC<Props>;
5
+ /**
6
+ * Canvas component for rendering the shogi board with pieces
7
+ *
8
+ * @component
9
+ * @param props - Component props
10
+ * @returns Canvas element displaying the shogi board
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * <ShogiBoardCanvas
15
+ * size={360}
16
+ * pieces={boardState}
17
+ * isSente={true}
18
+ * currentMove={lastMove}
19
+ * />
20
+ * ```
21
+ */
22
+ declare const ShogiBoardCanvas: React.FC<ShogiBoardCanvasProps>;
16
23
 
17
24
  export { ShogiBoardCanvas as default };
@@ -1,117 +1,13 @@
1
1
  "use client";
2
- import { JKFPlayer } from "json-kifu-format";
3
2
  import React from "react";
4
- import { Color } from "shogi.js";
5
- const getCanvasDimensions = (size) => {
6
- const dpr = window.devicePixelRatio || 1;
7
- return { width: size * dpr, height: size * dpr, dpr };
8
- };
9
- const getBoardLayout = (size) => {
10
- const margin = size * 0.06;
11
- const boardSize = size - margin * 2;
12
- const cell = boardSize / 9;
13
- return { margin, boardSize, cell };
14
- };
15
- const drawBackground = (ctx, size, boardColor) => {
16
- ctx.fillStyle = boardColor;
17
- ctx.fillRect(0, 0, size, size);
18
- };
19
- const drawBoard = (ctx, margin, boardSize, lineColor) => {
20
- ctx.strokeStyle = lineColor;
21
- ctx.lineWidth = 2;
22
- ctx.strokeRect(margin, margin, boardSize, boardSize);
23
- ctx.lineWidth = 1;
24
- Array.from({ length: 8 }).forEach((_, i) => {
25
- const offset = (i + 1) * (boardSize / 9);
26
- ctx.beginPath();
27
- ctx.moveTo(margin + offset, margin);
28
- ctx.lineTo(margin + offset, margin + boardSize);
29
- ctx.stroke();
30
- ctx.beginPath();
31
- ctx.moveTo(margin, margin + offset);
32
- ctx.lineTo(margin + boardSize, margin + offset);
33
- ctx.stroke();
34
- });
35
- };
36
- const drawPieces = (ctx, pieces, margin, cell, fontFamily, fontSizeRatio, isSente) => {
37
- ctx.textAlign = "center";
38
- ctx.textBaseline = "middle";
39
- pieces.forEach((row, rowIndex) => {
40
- row.forEach((piece, colIndex) => {
41
- if (!piece) return;
42
- const x = isSente ? 8 - rowIndex : rowIndex;
43
- const y = isSente ? colIndex : 8 - colIndex;
44
- const px = margin + x * cell + cell / 2;
45
- const py = margin + y * cell + cell / 2;
46
- ctx.save();
47
- ctx.translate(px, py);
48
- if (isSente && piece.color === Color.White) {
49
- ctx.rotate(Math.PI);
50
- } else if (!isSente && piece.color === Color.Black) {
51
- ctx.rotate(Math.PI);
52
- }
53
- const kan = JKFPlayer.kindToKan(piece.kind);
54
- ctx.fillStyle = "#000";
55
- if (kan.length === 2) {
56
- const baseFontSize = cell * fontSizeRatio * 0.5;
57
- ctx.font = `${baseFontSize}px ${fontFamily}`;
58
- const scaleX = 2;
59
- const scaleY = 1;
60
- const offsetY = cell * 0.18;
61
- ctx.save();
62
- ctx.scale(scaleX, scaleY);
63
- ctx.fillText(kan[0], 0 / scaleX, -offsetY / scaleY);
64
- ctx.restore();
65
- ctx.save();
66
- ctx.scale(scaleX, scaleY);
67
- ctx.fillText(kan[1], 0 / scaleX, offsetY / scaleY);
68
- ctx.restore();
69
- } else {
70
- const fontSize = cell * fontSizeRatio;
71
- ctx.font = `${fontSize}px ${fontFamily}`;
72
- ctx.fillText(kan, 0, 0);
73
- }
74
- ctx.restore();
75
- });
76
- });
77
- };
78
- const drawCoordinates = (ctx, margin, boardSize, cell, fontFamily, isSente) => {
79
- ctx.fillStyle = "#000";
80
- ctx.font = `${cell * 0.35}px ${fontFamily}`;
81
- ctx.textAlign = "center";
82
- ctx.textBaseline = "middle";
83
- Array.from({ length: 9 }).forEach((_, i) => {
84
- const x = margin + i * cell + cell / 2;
85
- const y = margin / 2;
86
- const label = isSente ? JKFPlayer.numToZen(9 - i) : JKFPlayer.numToZen(i + 1);
87
- ctx.fillText(label, x, y);
88
- });
89
- Array.from({ length: 9 }).forEach((_, i) => {
90
- const x = margin + boardSize + margin / 2;
91
- const y = margin + i * cell + cell / 2;
92
- const label = isSente ? JKFPlayer.numToKan(i + 1) : JKFPlayer.numToKan(9 - i);
93
- ctx.fillText(label, x, y);
94
- });
95
- };
96
- const drawHighlightedCell = (ctx, margin, cell, isSente, currentMove) => {
97
- if (!currentMove) return;
98
- if (currentMove.to) {
99
- const toRow = currentMove.to.x - 1;
100
- const toCol = currentMove.to.y - 1;
101
- const toX = isSente ? 8 - toRow : toRow;
102
- const toY = isSente ? toCol : 8 - toCol;
103
- ctx.fillStyle = "rgba(255,0,0,0.1)";
104
- ctx.fillRect(margin + toX * cell, margin + toY * cell, cell, cell);
105
- }
106
- if (currentMove.from) {
107
- const fromRow = currentMove.from.x - 1;
108
- const fromCol = currentMove.from.y - 1;
109
- const fromX = isSente ? 8 - fromRow : fromRow;
110
- const fromY = isSente ? fromCol : 8 - fromCol;
111
- ctx.fillStyle = "rgba(255,0,0,0.1)";
112
- ctx.fillRect(margin + fromX * cell, margin + fromY * cell, cell, cell);
113
- }
114
- };
3
+ import {
4
+ drawBoardBackground,
5
+ drawBoardGrid,
6
+ drawBoardPieces,
7
+ drawBoardCoordinates,
8
+ drawHighlightedCell
9
+ } from "./board-renderer.mjs";
10
+ import { getCanvasDimensions, getBoardLayout } from "./canvas-utils.mjs";
115
11
  const ShogiBoardCanvas = ({
116
12
  size = 360,
117
13
  boardColor = "#f9d27a",
@@ -136,11 +32,11 @@ const ShogiBoardCanvas = ({
136
32
  ctx.scale(dpr, dpr);
137
33
  ctx.clearRect(0, 0, size, size);
138
34
  const { margin, boardSize, cell } = getBoardLayout(size);
139
- drawBackground(ctx, size, boardColor);
140
- drawBoard(ctx, margin, boardSize, lineColor);
35
+ drawBoardBackground(ctx, size, boardColor);
36
+ drawBoardGrid(ctx, margin, boardSize, lineColor);
141
37
  drawHighlightedCell(ctx, margin, cell, isSente, currentMove);
142
- drawPieces(ctx, pieces, margin, cell, fontFamily, fontSizeRatio, isSente);
143
- drawCoordinates(ctx, margin, boardSize, cell, fontFamily, isSente);
38
+ drawBoardPieces(ctx, pieces, margin, cell, fontFamily, fontSizeRatio, isSente);
39
+ drawBoardCoordinates(ctx, margin, boardSize, cell, fontFamily, isSente);
144
40
  }, [size, boardColor, lineColor, fontFamily, fontSizeRatio, pieces, isSente, currentMove]);
145
41
  return /* @__PURE__ */ React.createElement("canvas", { ref: canvasRef, width: size, height: size });
146
42
  };
@@ -1,16 +1,24 @@
1
1
  import React from 'react';
2
- import { Piece } from 'shogi.js';
2
+ import { ShogiHandsCanvasProps } from './types.mjs';
3
+ import 'shogi.js';
3
4
 
4
- type Props = {
5
- size?: number;
6
- boardColor?: string;
7
- lineColor?: string;
8
- fontFamily?: string;
9
- fontSizeRatio?: number;
10
- hands: Piece[][];
11
- isSente?: boolean;
12
- isTop?: boolean;
13
- };
14
- declare const ShogiHandsCanvas: React.FC<Props>;
5
+ /**
6
+ * Canvas component for rendering captured pieces (hands)
7
+ *
8
+ * @component
9
+ * @param props - Component props
10
+ * @returns Canvas element displaying the hands (captured pieces area)
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * <ShogiHandsCanvas
15
+ * size={360}
16
+ * hands={capturedPieces}
17
+ * isSente={true}
18
+ * isTop={true}
19
+ * />
20
+ * ```
21
+ */
22
+ declare const ShogiHandsCanvas: React.FC<ShogiHandsCanvasProps>;
15
23
 
16
24
  export { ShogiHandsCanvas as default };
@@ -1,78 +1,7 @@
1
1
  "use client";
2
- import { JKFPlayer } from "json-kifu-format";
3
2
  import React from "react";
4
- import { Color } from "shogi.js";
5
- const getCanvasDimensions = (size) => {
6
- const dpr = window.devicePixelRatio || 1;
7
- return { width: size * dpr, height: size * dpr, dpr };
8
- };
9
- const getHandsLayout = (size) => {
10
- const margin = size * 0.06;
11
- const boardSize = size - margin * 2;
12
- const cellSize = boardSize / 9;
13
- const handsHeight = cellSize + margin + 2;
14
- return { margin, handsHeight, boardSize, cellSize };
15
- };
16
- const drawBackground = (ctx, width, height, color) => {
17
- ctx.fillStyle = color;
18
- ctx.fillRect(0, 0, width, height);
19
- };
20
- const drawHandsFrame = (ctx, x, y, width, height, lineColor, isTop) => {
21
- ctx.strokeStyle = lineColor;
22
- ctx.lineWidth = 2;
23
- if (isTop) {
24
- ctx.strokeRect(x, y, width, height);
25
- } else {
26
- ctx.strokeRect(x, 2, width, height);
27
- }
28
- };
29
- const drawCoordinates = (ctx, fontSize, fontFamily) => {
30
- ctx.fillStyle = "#000";
31
- ctx.font = `${fontSize}px ${fontFamily}`;
32
- ctx.textAlign = "center";
33
- ctx.textBaseline = "middle";
34
- };
35
- const drawPieces = (ctx, hands, margin, boardSize, cell, fontFamily, fontSizeRatio, isSente, isTop) => {
36
- ctx.textAlign = "center";
37
- ctx.textBaseline = "middle";
38
- const pieces = isSente && isTop || !isSente && !isTop ? hands[Color.White] : hands[Color.Black];
39
- if (!pieces || pieces.length === 0) return;
40
- const grouped = pieces.reduce(
41
- (acc, piece) => {
42
- if (!piece) return acc;
43
- const key = piece.kind;
44
- if (!acc[key]) acc[key] = { count: 0, color: piece.color };
45
- acc[key].count += 1;
46
- return acc;
47
- },
48
- {}
49
- );
50
- const order = ["OU", "HI", "KA", "KI", "GI", "KE", "KY", "FU"];
51
- const kinds = order.filter((kind) => grouped[kind]);
52
- kinds.forEach((kind, index) => {
53
- const { count, color } = grouped[kind];
54
- const px = isTop ? margin + boardSize - (index * cell + cell / 2) : margin + index * cell + cell / 2;
55
- const py = isTop ? margin + cell / 2 : cell / 2 + 2;
56
- ctx.save();
57
- ctx.translate(px, py);
58
- if (isSente && color === Color.White) {
59
- ctx.rotate(Math.PI);
60
- } else if (!isSente && color === Color.Black) {
61
- ctx.rotate(Math.PI);
62
- }
63
- const fontSize = cell * fontSizeRatio;
64
- const kan = JKFPlayer.kindToKan(kind);
65
- ctx.font = `${fontSize}px ${fontFamily}`;
66
- ctx.fillText(kan, 0, 0);
67
- if (count > 1) {
68
- ctx.font = `${fontSize * 0.5}px ${fontFamily}`;
69
- const countOffsetX = cell * 0;
70
- const countOffsetY = cell * 0.8;
71
- ctx.fillText(String(count), countOffsetX, countOffsetY);
72
- }
73
- ctx.restore();
74
- });
75
- };
3
+ import { getCanvasDimensions, getHandsLayout } from "./canvas-utils.mjs";
4
+ import { drawHandsBackground, drawHandsFrame, drawHandsPieces } from "./hands-renderer.mjs";
76
5
  const ShogiHandsCanvas = ({
77
6
  size = 360,
78
7
  boardColor = "#f9d27a",
@@ -97,10 +26,9 @@ const ShogiHandsCanvas = ({
97
26
  canvas.style.height = `${handsHeight}px`;
98
27
  ctx.scale(dpr, dpr);
99
28
  ctx.clearRect(0, 0, size, handsHeight);
100
- drawBackground(ctx, size, handsHeight, boardColor);
29
+ drawHandsBackground(ctx, size, handsHeight, boardColor);
101
30
  drawHandsFrame(ctx, margin, margin, boardSize, cellSize, lineColor, isTop);
102
- drawCoordinates(ctx, cellSize * 0.35, fontFamily);
103
- drawPieces(ctx, hands, margin, boardSize, cellSize, fontFamily, fontSizeRatio, isSente, isTop);
31
+ drawHandsPieces(ctx, hands, margin, boardSize, cellSize, fontFamily, fontSizeRatio, isSente, isTop);
104
32
  }, [size, boardColor, lineColor, fontFamily, fontSizeRatio, isSente, hands, isTop]);
105
33
  return /* @__PURE__ */ React.createElement("canvas", { ref: canvasRef });
106
34
  };
@@ -1,10 +1,32 @@
1
1
  import React from 'react';
2
+ import { KifuAdapterFactory } from './types.mjs';
3
+ import 'shogi.js';
2
4
 
5
+ /**
6
+ * Props for the ShogiPlayer component
7
+ * @property kifuText - KIF format kifu (game record) text
8
+ * @property size - Canvas size in CSS pixels (default: 360)
9
+ * @property tesuu - Initial move number to display
10
+ * @property adapterFactory - Factory function for creating KifuAdapter (for DI/testing)
11
+ */
3
12
  type ShogiPlayerProps = {
4
13
  kifuText: string;
5
14
  size?: number;
6
15
  tesuu?: number;
16
+ adapterFactory?: KifuAdapterFactory;
7
17
  };
18
+ /**
19
+ * Shogi game viewer component with board, hands, and move navigation
20
+ *
21
+ * @component
22
+ * @param props - Component props
23
+ * @returns Interactive shogi player with keyboard and button navigation
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * <ShogiPlayer kifuText={kifString} tesuu={10} />
28
+ * ```
29
+ */
8
30
  declare function ShogiPlayer(props: ShogiPlayerProps): React.JSX.Element;
9
31
 
10
32
  export { type ShogiPlayerProps, ShogiPlayer as default };
@@ -1,46 +1,31 @@
1
1
  "use client";
2
- import { JKFPlayer } from "json-kifu-format";
3
- import React, { useCallback, useEffect, useState } from "react";
2
+ import React, { useCallback, useEffect, useRef, useState } from "react";
3
+ import { createKifuAdapter } from "./adapters/kifu-adapter.mjs";
4
4
  import Button from "./button.mjs";
5
- import { MovesArea } from "./moves-area.mjs";
5
+ import MovesArea from "./moves-area.mjs";
6
6
  import ShogiBoardCanvas from "./shogi-board-canvas.mjs";
7
7
  import ShogiHandsCanvas from "./shogi-hands-canvas.mjs";
8
8
  function ShogiPlayer(props) {
9
- const [player] = useState(JKFPlayer.parse(props.kifuText.trim()));
10
- const [pieces, setPieces] = useState(player.shogi.board);
11
- const [hands, setHands] = useState(player.shogi.hands);
12
- const [moves, setMoves] = useState(player.kifu.moves);
13
- const [tesuu, setTesuu] = useState(player.tesuu);
14
- const [currentMove, setCurrentMove] = useState(player.getMove());
9
+ const factory = props.adapterFactory ?? createKifuAdapter;
10
+ const adapterRef = useRef(factory(props.kifuText));
11
+ const [gameState, setGameState] = useState(() => adapterRef.current.getState());
15
12
  const [isSente, setIsSente] = useState(true);
16
- const [maxTesuu, setMaxTesuu] = useState(player.getMaxTesuu());
17
- const [comments, setComments] = useState(player.getComments());
18
- const size = props.size ? props.size : 360;
19
- const updateState = () => {
20
- setPieces([...player.shogi.board]);
21
- setHands([...player.shogi.hands]);
22
- setMoves([...player.kifu.moves]);
23
- setCurrentMove(player.getMove());
24
- setMaxTesuu(player.getMaxTesuu());
25
- setComments(player.getComments());
26
- setTesuu(player.tesuu);
27
- };
28
- const handleForward = () => {
29
- player.forward();
30
- updateState();
31
- };
32
- const handleBackward = () => {
33
- player.backward();
34
- updateState();
35
- };
36
- const handleGoto = (tesuu2) => {
37
- player.goto(tesuu2);
38
- updateState();
39
- };
40
- const handeleToggle = () => {
41
- setIsSente(!isSente);
42
- updateState();
43
- };
13
+ const size = props.size ?? 360;
14
+ const handleForward = useCallback(() => {
15
+ const newState = adapterRef.current.forward();
16
+ setGameState(newState);
17
+ }, []);
18
+ const handleBackward = useCallback(() => {
19
+ const newState = adapterRef.current.backward();
20
+ setGameState(newState);
21
+ }, []);
22
+ const handleGoto = useCallback((tesuu) => {
23
+ const newState = adapterRef.current.goto(tesuu);
24
+ setGameState(newState);
25
+ }, []);
26
+ const handleToggle = useCallback(() => {
27
+ setIsSente((prev) => !prev);
28
+ }, []);
44
29
  const handleKeydown = useCallback(
45
30
  (event) => {
46
31
  event.preventDefault();
@@ -52,7 +37,7 @@ function ShogiPlayer(props) {
52
37
  break;
53
38
  case "Down":
54
39
  case "ArrowDown":
55
- handleGoto(maxTesuu);
40
+ handleGoto(gameState.maxMoveIndex);
56
41
  break;
57
42
  case "Left":
58
43
  case "ArrowLeft":
@@ -64,18 +49,43 @@ function ShogiPlayer(props) {
64
49
  handleForward();
65
50
  break;
66
51
  case "r":
67
- handeleToggle();
52
+ handleToggle();
68
53
  break;
69
54
  }
70
55
  },
71
- [maxTesuu, isSente]
56
+ [gameState.maxMoveIndex, handleGoto, handleBackward, handleForward, handleToggle]
72
57
  );
73
58
  useEffect(() => {
74
59
  if (props.tesuu !== void 0) {
75
60
  handleGoto(props.tesuu);
76
61
  }
77
- }, [props.tesuu]);
78
- return /* @__PURE__ */ React.createElement("div", { className: "flex flex-col sm:flex-row w-fit", tabIndex: 1, onKeyDown: handleKeydown }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-col" }, /* @__PURE__ */ React.createElement("div", { className: "bg-[#f9d27a] text-black text-xs text-right p-1" }, isSente ? "\u2616 " + player.kifu.header["\u5F8C\u624B"] : "\u2617 " + player.kifu.header["\u5148\u624B"]), /* @__PURE__ */ React.createElement(ShogiHandsCanvas, { size, hands, isSente, isTop: true }), /* @__PURE__ */ React.createElement(ShogiBoardCanvas, { size, pieces, isSente, currentMove }), /* @__PURE__ */ React.createElement(ShogiHandsCanvas, { size, hands, isSente, isTop: false }), /* @__PURE__ */ React.createElement("div", { className: "bg-[#f9d27a] text-black text-xs p-1" }, isSente ? "\u2617 " + player.kifu.header["\u5148\u624B"] : "\u2616 " + player.kifu.header["\u5F8C\u624B"])), /* @__PURE__ */ React.createElement("div", { className: "flex flex-col sm:w-fit bg-[#f9d27a] p-4 gap-4" }, /* @__PURE__ */ React.createElement("div", { className: "hidden sm:block flex-1 relative w-full" }, /* @__PURE__ */ React.createElement(MovesArea, { moves, tesuu, onTesuuChange: handleGoto })), /* @__PURE__ */ React.createElement("div", { className: "hidden sm:block text-black border-black border-2 p-1 text-xs max-h-40 w-0 min-w-full overflow-auto" }, comments.map((comment, index) => /* @__PURE__ */ React.createElement("div", { key: index }, comment)), comments.length === 0 && tesuu !== 0 && /* @__PURE__ */ React.createElement("div", null, "\xA0"), tesuu === 0 && Object.entries(player.kifu.header).map(([key, value], i) => /* @__PURE__ */ React.createElement("div", { key: i, className: "whitespace-nowrap" }, key, ": ", value))), /* @__PURE__ */ React.createElement("div", { className: "flex gap-2" }, /* @__PURE__ */ React.createElement(Button, { onClick: () => handleGoto(0) }, "\u6700\u521D"), /* @__PURE__ */ React.createElement(Button, { onClick: handleBackward }, "\u524D"), /* @__PURE__ */ React.createElement(Button, { onClick: handleForward }, "\u6B21"), /* @__PURE__ */ React.createElement(Button, { onClick: () => handleGoto(maxTesuu) }, "\u6700\u5F8C"), /* @__PURE__ */ React.createElement(Button, { onClick: handeleToggle }, "\u53CD\u8EE2"))));
62
+ }, [props.tesuu, handleGoto]);
63
+ useEffect(() => {
64
+ return () => {
65
+ adapterRef.current.dispose();
66
+ };
67
+ }, []);
68
+ const topPlayerName = isSente ? `\u2616 ${gameState.header.goteName}` : `\u2617 ${gameState.header.senteName}`;
69
+ const bottomPlayerName = isSente ? `\u2617 ${gameState.header.senteName}` : `\u2616 ${gameState.header.goteName}`;
70
+ return /* @__PURE__ */ React.createElement(
71
+ "div",
72
+ {
73
+ className: "flex flex-col sm:flex-row w-fit",
74
+ tabIndex: 0,
75
+ role: "application",
76
+ "aria-label": "Shogi player - use arrow keys to navigate, space or right arrow to move forward, left arrow to move backward, 'r' to flip board",
77
+ onKeyDown: handleKeydown
78
+ },
79
+ /* @__PURE__ */ React.createElement("div", { className: "flex flex-col" }, /* @__PURE__ */ React.createElement("div", { className: "bg-[#f9d27a] text-black text-xs text-right p-1" }, topPlayerName), /* @__PURE__ */ React.createElement(ShogiHandsCanvas, { size, hands: gameState.hands, isSente, isTop: true }), /* @__PURE__ */ React.createElement(ShogiBoardCanvas, { size, pieces: gameState.board, isSente, currentMove: gameState.currentMove }), /* @__PURE__ */ React.createElement(ShogiHandsCanvas, { size, hands: gameState.hands, isSente, isTop: false }), /* @__PURE__ */ React.createElement("div", { className: "bg-[#f9d27a] text-black text-xs p-1" }, bottomPlayerName)),
80
+ /* @__PURE__ */ React.createElement("div", { className: "flex flex-col sm:w-fit bg-[#f9d27a] p-4 gap-4" }, /* @__PURE__ */ React.createElement("div", { className: "hidden sm:block flex-1 relative w-full" }, /* @__PURE__ */ React.createElement(
81
+ MovesArea,
82
+ {
83
+ readableMoves: gameState.readableMoves,
84
+ tesuu: gameState.currentMoveIndex,
85
+ onTesuuChange: handleGoto
86
+ }
87
+ )), /* @__PURE__ */ React.createElement("div", { className: "hidden sm:block text-black border-black border-2 p-1 text-xs max-h-40 w-0 min-w-full overflow-auto" }, gameState.comments.map((comment, index) => /* @__PURE__ */ React.createElement("div", { key: index }, comment)), gameState.comments.length === 0 && gameState.currentMoveIndex !== 0 && /* @__PURE__ */ React.createElement("div", null, "\xA0"), gameState.currentMoveIndex === 0 && Object.entries(gameState.header).map(([key, value], i) => /* @__PURE__ */ React.createElement("div", { key: i, className: "whitespace-nowrap" }, key, ": ", value))), /* @__PURE__ */ React.createElement("div", { className: "flex gap-2" }, /* @__PURE__ */ React.createElement(Button, { onClick: () => handleGoto(0) }, "\u6700\u521D"), /* @__PURE__ */ React.createElement(Button, { onClick: handleBackward }, "\u524D"), /* @__PURE__ */ React.createElement(Button, { onClick: handleForward }, "\u6B21"), /* @__PURE__ */ React.createElement(Button, { onClick: () => handleGoto(gameState.maxMoveIndex) }, "\u6700\u5F8C"), /* @__PURE__ */ React.createElement(Button, { onClick: handleToggle }, "\u53CD\u8EE2")))
88
+ );
79
89
  }
80
90
  export {
81
91
  ShogiPlayer as default