@my-games/sudoku-react 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @my-games/sudoku-react
2
2
 
3
- > 数独的 React 18/19 组件适配器。**当前为 `0.0.0` 占位版本**,`GameSudoku` 仅渲染占位文案。
3
+ > 数独的 React 18/19 组件适配器(函数组件 + Hooks + `forwardRef`),基于 `@my-games/sudoku-core`。
4
4
 
5
5
  ```bash
6
6
  pnpm add @my-games/sudoku-react react react-dom
@@ -11,12 +11,42 @@ import { GameSudoku } from '@my-games/sudoku-react';
11
11
  import '@my-games/sudoku-react/style.css';
12
12
 
13
13
  export function Demo() {
14
- return <GameSudoku title="Sudoku" />;
14
+ return (
15
+ <GameSudoku
16
+ difficulty="easy"
17
+ saveKey="my-app:sudoku:save"
18
+ onDigit={(d, changed) => console.log('fill', d, changed)}
19
+ onSolved={(p) => console.log('won', p.difficulty, p.seconds + 's')}
20
+ />
21
+ );
15
22
  }
16
23
  ```
17
24
 
18
- **样式需显式引入**:vite lib build 把内部 CSS import 抽到 `dist/index.css`。
19
- 真实棋盘组件、`forwardRef` 句柄与 `useGame()` hook 随引擎后续提交补入。
25
+ **样式需显式引入**:vite lib build 把内部 CSS import 抽到 `dist/index.css`,入口加一次
26
+ `import '@my-games/sudoku-react/style.css'`(或 core 的 `@my-games/sudoku-core/style.css`)。
27
+
28
+ ## Props
29
+
30
+ 同 Vue 版:`difficulty` / `storage` / `saveKey` / `bestKey` / `freshStart` / `autofocus` / `showPad` /
31
+ `showHint`(默认 `false`,`hint()` 始终在句柄 / `useGame` 上)/ `title`,另有 `className`(追加到最外层
32
+ `.game-sudoku`)、`onDigit(digit, changed)`、`onSolved({ difficulty, seconds })`。
33
+
34
+ ## Ref 命令式方法(`GameSudokuHandle`)
35
+
36
+ ```tsx
37
+ import { useRef } from 'react';
38
+ import { GameSudoku, type GameSudokuHandle } from '@my-games/sudoku-react';
39
+
40
+ const g = useRef<GameSudokuHandle>(null);
41
+ // g.current?.select(0, 2); g.current?.input(4); g.current?.hint(); g.current?.undo();
42
+ ```
43
+
44
+ ## Hook(想自己画 UI)
45
+
46
+ ```tsx
47
+ import { useGame } from '@my-games/sudoku-react';
48
+ const { state, select, input, hint, undo, newGame } = useGame({ difficulty: 'hard' });
49
+ ```
20
50
 
21
51
  完整说明见仓库根 README:<https://github.com/my-gam/sudoku#readme>
22
52
 
@@ -1,5 +1,34 @@
1
+ import { Difficulty, Digit, SaveStorage } from '@my-games/sudoku-core';
2
+ /**
3
+ * GameSudoku —— React 18/19 组件,行为与 Vue 版对齐。
4
+ * 键盘:数字 1-9 填数 / N 切笔记 / Backspace·Delete 擦除 / 方向键移动选中;
5
+ * 严格判 `document.activeElement === board`,多实例并存只活动那个响应。
6
+ */
1
7
  export interface GameSudokuProps {
8
+ difficulty?: Difficulty;
9
+ storage?: SaveStorage | null;
10
+ saveKey?: string;
11
+ bestKey?: string;
12
+ freshStart?: boolean;
13
+ autofocus?: boolean;
14
+ showPad?: boolean;
2
15
  title?: string;
16
+ className?: string;
17
+ showHint?: boolean;
18
+ onDigit?: (digit: Digit, changed: boolean) => void;
19
+ onSolved?: (payload: {
20
+ difficulty: Difficulty;
21
+ seconds: number;
22
+ }) => void;
3
23
  }
4
- export declare function GameSudoku({ title }: GameSudokuProps): import('react').JSX.Element;
24
+ export interface GameSudokuHandle {
25
+ select(row: number, col: number): void;
26
+ input(digit: Digit): boolean;
27
+ erase(): boolean;
28
+ toggleNote(digit: Digit): boolean;
29
+ hint(): boolean;
30
+ newGame(difficulty?: Difficulty): void;
31
+ undo(): boolean;
32
+ }
33
+ export declare const GameSudoku: import('react').ForwardRefExoticComponent<GameSudokuProps & import('react').RefAttributes<GameSudokuHandle>>;
5
34
  //# sourceMappingURL=GameSudoku.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"GameSudoku.d.ts","sourceRoot":"","sources":["../src/GameSudoku.tsx"],"names":[],"mappings":"AAEA,OAAO,iCAAiC,CAAC;AAEzC,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,UAAU,CAAC,EAAE,KAAgB,EAAE,EAAE,eAAe,+BAM/D"}
1
+ {"version":3,"file":"GameSudoku.d.ts","sourceRoot":"","sources":["../src/GameSudoku.tsx"],"names":[],"mappings":"AAWA,OAAO,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,KAAK,EAAa,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAGvF;;;;GAIG;AAEH,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IACnD,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,UAAU,EAAE,UAAU,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CAC3E;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC;IAC7B,KAAK,IAAI,OAAO,CAAC;IACjB,UAAU,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC;IAClC,IAAI,IAAI,OAAO,CAAC;IAChB,OAAO,CAAC,UAAU,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IACvC,IAAI,IAAI,OAAO,CAAC;CACjB;AAiTD,eAAO,MAAM,UAAU,8GAA8B,CAAC"}
package/dist/index.css CHANGED
@@ -1,18 +1,376 @@
1
- /*
2
- * @my-games/sudoku-core —— 0.0.0 占位样式表(单数命名 ./style.css)。
1
+ /**
2
+ * game-sudoku 通用样式表(单数命名:`./style.css`)。
3
3
  *
4
- * 空骨架阶段只放最小基类,保证三包各自构建能产出非空 dist/index.css
5
- * (verify:dist 守卫要求 vue/react 的 index.css 存在且非 0 字节)。
6
- * 真实棋盘样式(3×3 宫格布局、题面/用户/冲突配色、铅笔笔记迷你格)随引擎补入。
4
+ * 由 `@my-games/sudoku-core/style.css` 暴露,Vue / React 适配器都直接 import 这一份,
5
+ * 保持视觉一致(避免两包各自漂移)。
6
+ *
7
+ * 关键布局:棋盘是 3×3 的「宫」网格(.game-sudoku-box 各含 3×3 共 9 格)。
8
+ * 宫之间的粗缝(box 背景透出 strong line 色)天然构成九宫分隔线,宫之间用 box-gap、
9
+ * 宫内用 cell-gap 两级间距即可,无需 nth-child 取模去判边界。
10
+ *
11
+ * 约定:
12
+ * - 数字配色区分「题面给定」(深色加粗) 与「玩家填入」(蓝色);冲突格红底。
13
+ * - 选中格高亮 + 同数/同行列宫淡色提示;空格用 .cell-notes 3×3 迷你格画铅笔标记。
14
+ * - 尺寸自适应:容器宽度决定一切,clamp() 缩放,320px~520px 视觉合理。
15
+ * - prefers-reduced-motion 下关过渡/动画。
7
16
  */
8
17
 
9
18
  .game-sudoku {
19
+ --board-size: min(92vw, 460px);
20
+ --box-gap: 3px;
21
+ --cell-gap: 1px;
22
+ --radius: clamp(4px, 1.2vw, 10px);
23
+ --line-strong: #3b4a5a;
24
+ --line-soft: #93a3b3;
25
+ --cell-bg: #ffffff;
26
+ --text-given: #1f2d3d;
27
+ --text-user: #1c63b5;
28
+ --text-muted: #8a99a8;
29
+ --hl-selected: #cfe3ff;
30
+ --hl-peer: #eef4fb;
31
+ --hl-same: #d7e9ff;
32
+ --bad: #ffd7d7;
33
+ --bad-text: #b4232a;
34
+ --accent: #1c63b5;
35
+ --accent-dark: #144d8f;
36
+
10
37
  display: inline-flex;
11
38
  flex-direction: column;
12
- gap: 8px;
13
- font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
14
- color: #1f2d3d;
39
+ gap: 10px;
40
+ font-family: 'Helvetica Neue', 'Segoe UI', system-ui, -apple-system, sans-serif;
41
+ color: var(--text-given);
15
42
  user-select: none;
16
43
  -webkit-tap-highlight-color: transparent;
17
44
  }
45
+
46
+ .game-sudoku-header {
47
+ display: flex;
48
+ align-items: flex-start;
49
+ justify-content: space-between;
50
+ gap: 10px;
51
+ width: var(--board-size);
52
+ max-width: 92vw;
53
+ }
54
+
55
+ .game-sudoku-title {
56
+ margin: 0;
57
+ font-size: clamp(24px, 7vw, 38px);
58
+ font-weight: 800;
59
+ letter-spacing: -0.5px;
60
+ }
61
+
62
+ .game-sudoku-title .subtitle {
63
+ display: block;
64
+ font-size: 12px;
65
+ font-weight: 600;
66
+ letter-spacing: 0;
67
+ color: var(--text-muted);
68
+ }
69
+
70
+ .game-sudoku-stats {
71
+ display: flex;
72
+ gap: 6px;
73
+ }
74
+
75
+ .game-sudoku-stat {
76
+ position: relative;
77
+ min-width: 66px;
78
+ padding: 5px 10px 7px;
79
+ border-radius: var(--radius);
80
+ background: var(--line-strong);
81
+ color: #fff;
82
+ text-align: center;
83
+ font-variant-numeric: tabular-nums;
84
+ }
85
+
86
+ .game-sudoku-stat .label {
87
+ display: block;
88
+ font-size: 10px;
89
+ letter-spacing: 1px;
90
+ text-transform: uppercase;
91
+ opacity: 0.85;
92
+ }
93
+
94
+ .game-sudoku-stat .value {
95
+ display: block;
96
+ font-size: 18px;
97
+ font-weight: 700;
98
+ line-height: 1.2;
99
+ }
100
+
101
+ .game-sudoku-stat.is-danger .value {
102
+ color: #ffb3b3;
103
+ }
104
+
105
+ .game-sudoku-toolbar {
106
+ display: flex;
107
+ align-items: center;
108
+ justify-content: space-between;
109
+ gap: 8px;
110
+ width: var(--board-size);
111
+ max-width: 92vw;
112
+ flex-wrap: wrap;
113
+ }
114
+
115
+ .game-sudoku-difficulty {
116
+ display: inline-flex;
117
+ gap: 4px;
118
+ }
119
+
120
+ .game-sudoku-chip {
121
+ padding: 6px 12px;
122
+ border: 1px solid var(--line-soft);
123
+ border-radius: 999px;
124
+ background: #fff;
125
+ color: var(--text-given);
126
+ font: inherit;
127
+ font-size: 13px;
128
+ font-weight: 600;
129
+ cursor: pointer;
130
+ }
131
+
132
+ .game-sudoku-chip.is-active {
133
+ background: var(--accent);
134
+ border-color: var(--accent);
135
+ color: #fff;
136
+ }
137
+
138
+ .game-sudoku-chip:disabled {
139
+ opacity: 0.5;
140
+ cursor: default;
141
+ }
142
+
143
+ .game-sudoku-actions {
144
+ display: flex;
145
+ gap: 6px;
146
+ flex-wrap: wrap;
147
+ }
148
+
149
+ .game-sudoku-button {
150
+ padding: 8px 12px;
151
+ border: 0;
152
+ border-radius: var(--radius);
153
+ background: #e7edf3;
154
+ color: var(--text-given);
155
+ font: inherit;
156
+ font-weight: 700;
157
+ font-size: 13px;
158
+ cursor: pointer;
159
+ transition: background 120ms, transform 80ms;
160
+ }
161
+
162
+ .game-sudoku-button:hover:not(:disabled) {
163
+ background: #d7e2ec;
164
+ }
165
+
166
+ .game-sudoku-button:active:not(:disabled) {
167
+ transform: translateY(1px);
168
+ }
169
+
170
+ .game-sudoku-button:disabled {
171
+ opacity: 0.4;
172
+ cursor: not-allowed;
173
+ }
174
+
175
+ .game-sudoku-button.is-primary {
176
+ background: var(--accent);
177
+ color: #fff;
178
+ }
179
+
180
+ .game-sudoku-button.is-primary:hover:not(:disabled) {
181
+ background: var(--accent-dark);
182
+ }
183
+
184
+ .game-sudoku-button.is-toggled {
185
+ background: var(--accent-dark);
186
+ color: #fff;
187
+ }
188
+
189
+ .game-sudoku-board {
190
+ position: relative;
191
+ width: var(--board-size);
192
+ max-width: 92vw;
193
+ padding: var(--box-gap);
194
+ border-radius: var(--radius);
195
+ background: var(--line-strong);
196
+ outline: none;
197
+ box-sizing: content-box;
198
+ display: grid;
199
+ grid-template-columns: repeat(3, 1fr);
200
+ grid-template-rows: repeat(3, 1fr);
201
+ gap: var(--box-gap);
202
+ aspect-ratio: 1;
203
+ }
204
+
205
+ .game-sudoku-board:focus-visible {
206
+ box-shadow: 0 0 0 3px rgba(28, 99, 181, 0.4);
207
+ }
208
+
209
+ .game-sudoku-box {
210
+ display: grid;
211
+ grid-template-columns: repeat(3, 1fr);
212
+ grid-template-rows: repeat(3, 1fr);
213
+ gap: var(--cell-gap);
214
+ background: var(--line-soft);
215
+ border-radius: 3px;
216
+ overflow: hidden;
217
+ }
218
+
219
+ .game-sudoku-cell {
220
+ position: relative;
221
+ display: flex;
222
+ align-items: center;
223
+ justify-content: center;
224
+ background: var(--cell-bg);
225
+ font-weight: 500;
226
+ font-size: clamp(15px, 4.6vw, 26px);
227
+ line-height: 1;
228
+ cursor: pointer;
229
+ aspect-ratio: 1;
230
+ }
231
+
232
+ .game-sudoku-cell.is-given {
233
+ color: var(--text-given);
234
+ font-weight: 800;
235
+ }
236
+
237
+ .game-sudoku-cell.is-user {
238
+ color: var(--text-user);
239
+ }
240
+
241
+ .game-sudoku-cell.is-peer {
242
+ background: var(--hl-peer);
243
+ }
244
+
245
+ .game-sudoku-cell.is-same {
246
+ background: var(--hl-same);
247
+ }
248
+
249
+ .game-sudoku-cell.is-selected {
250
+ background: var(--hl-selected);
251
+ }
252
+
253
+ .game-sudoku-cell.is-conflict {
254
+ background: var(--bad);
255
+ color: var(--bad-text);
256
+ }
257
+
258
+ .game-sudoku-cell.is-hint {
259
+ color: var(--accent-dark);
260
+ }
261
+
262
+ .cell-notes {
263
+ position: absolute;
264
+ inset: 2px;
265
+ display: grid;
266
+ grid-template-columns: repeat(3, 1fr);
267
+ grid-template-rows: repeat(3, 1fr);
268
+ align-items: center;
269
+ justify-items: center;
270
+ font-size: clamp(7px, 1.8vw, 11px);
271
+ font-weight: 600;
272
+ color: var(--text-muted);
273
+ pointer-events: none;
274
+ }
275
+
276
+ .game-sudoku-pad {
277
+ display: grid;
278
+ grid-template-columns: repeat(9, 1fr);
279
+ gap: 6px;
280
+ width: var(--board-size);
281
+ max-width: 92vw;
282
+ }
283
+
284
+ .game-sudoku-pad button {
285
+ padding: 10px 0;
286
+ border: 0;
287
+ border-radius: var(--radius);
288
+ background: #eef3f8;
289
+ color: var(--text-user);
290
+ font: inherit;
291
+ font-weight: 700;
292
+ font-size: clamp(16px, 4vw, 22px);
293
+ cursor: pointer;
294
+ }
295
+
296
+ .game-sudoku-pad button:hover:not(:disabled) {
297
+ background: #dfeaf5;
298
+ }
299
+
300
+ .game-sudoku-pad button:disabled {
301
+ opacity: 0.35;
302
+ cursor: not-allowed;
303
+ }
304
+
305
+ .game-sudoku-pad .is-active-mode {
306
+ background: var(--accent);
307
+ color: #fff;
308
+ }
309
+
310
+ .game-sudoku-overlay {
311
+ position: absolute;
312
+ inset: 0;
313
+ display: flex;
314
+ flex-direction: column;
315
+ align-items: center;
316
+ justify-content: center;
317
+ gap: 12px;
318
+ border-radius: var(--radius);
319
+ background: rgba(255, 255, 255, 0.86);
320
+ color: var(--text-given);
321
+ text-align: center;
322
+ opacity: 0;
323
+ pointer-events: none;
324
+ transition: opacity 220ms ease-in;
325
+ z-index: 10;
326
+ }
327
+
328
+ .game-sudoku-overlay.is-visible {
329
+ opacity: 1;
330
+ pointer-events: auto;
331
+ }
332
+
333
+ .game-sudoku-overlay h2 {
334
+ margin: 0;
335
+ font-size: clamp(26px, 7vw, 40px);
336
+ font-weight: 800;
337
+ color: var(--accent-dark);
338
+ }
339
+
340
+ .game-sudoku-overlay .meta {
341
+ font-size: 14px;
342
+ color: var(--text-muted);
343
+ }
344
+
345
+ .game-sudoku-overlay .row {
346
+ display: flex;
347
+ gap: 8px;
348
+ }
349
+
350
+ .game-sudoku-desc {
351
+ margin: 0;
352
+ font-size: 13px;
353
+ line-height: 1.5;
354
+ color: var(--text-muted);
355
+ }
356
+
357
+ @keyframes game-sudoku-pop {
358
+ 0% { transform: scale(1); }
359
+ 50% { transform: scale(1.18); }
360
+ 100% { transform: scale(1); }
361
+ }
362
+
363
+ .game-sudoku-cell.is-pop {
364
+ animation: game-sudoku-pop 200ms ease-out;
365
+ }
366
+
367
+ @media (prefers-reduced-motion: reduce) {
368
+ .game-sudoku-button,
369
+ .game-sudoku-overlay {
370
+ transition: none;
371
+ }
372
+ .game-sudoku-cell.is-pop {
373
+ animation: none;
374
+ }
375
+ }
18
376
  /*$vite$:1*/
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { GameSudoku, type GameSudokuProps } from './GameSudoku';
2
- export declare const SUDOKU_PACKAGE: "sudoku-react";
3
- export declare const SUDOKU_STAGE: "0.0.0";
1
+ export { GameSudoku, type GameSudokuProps, type GameSudokuHandle } from './GameSudoku';
2
+ export { useGame, type UseGameReturn } from './useGame';
3
+ export type { Cell, Difficulty, Digit, GameState, GameStatus, Grid, SaveStorage } from '@my-games/sudoku-core';
4
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAEhE,eAAO,MAAM,cAAc,EAAG,cAAuB,CAAC;AACtD,eAAO,MAAM,YAAY,EAAG,OAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACvF,OAAO,EAAE,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAExD,YAAY,EACV,IAAI,EACJ,UAAU,EACV,KAAK,EACL,SAAS,EACT,UAAU,EACV,IAAI,EACJ,WAAW,EACZ,MAAM,uBAAuB,CAAC"}