@my-games/sudoku-vue 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-vue
2
2
 
3
- > 数独的 Vue 3 组件适配器。**当前为 `0.0.0` 占位版本**,`GameSudoku` 仅渲染占位文案。
3
+ > 数独的 Vue 3 组件适配器(Composition API + SFC),基于 `@my-games/sudoku-core`。
4
4
 
5
5
  ```bash
6
6
  pnpm add @my-games/sudoku-vue vue
@@ -10,15 +10,51 @@ pnpm add @my-games/sudoku-vue vue
10
10
  <script setup lang="ts">
11
11
  import { GameSudoku } from '@my-games/sudoku-vue';
12
12
  import '@my-games/sudoku-vue/style.css';
13
+
14
+ function onSolved(p: { difficulty: string; seconds: number }) {
15
+ console.log('通关', p.difficulty, p.seconds + 's');
16
+ }
13
17
  </script>
14
18
 
15
19
  <template>
16
- <GameSudoku title="Sudoku" />
20
+ <GameSudoku difficulty="easy" save-key="my-app:sudoku:save" @solved="onSolved" />
17
21
  </template>
18
22
  ```
19
23
 
20
- **样式需显式引入**:vite lib build 把内部 CSS import 抽到 `dist/index.css`。
21
- 真实棋盘组件与 `useGame()` composable 随引擎后续提交补入。
24
+ **样式需显式引入**:vite lib build 把内部 CSS import 抽到 `dist/index.css`,消费者入口加一次
25
+ `import '@my-games/sudoku-vue/style.css'`(或 `import '@my-games/sudoku-core/style.css'`,同源)。
26
+
27
+ ## Props
28
+
29
+ | Prop | 类型 | 默认 | 说明 |
30
+ | --- | --- | --- | --- |
31
+ | `difficulty` | `'easy'\|'medium'\|'hard'` | `'easy'` | 初始难度(组件内难度条可即时切换重开) |
32
+ | `storage` | `SaveStorage \| null` | `localStorage` | 传 `null` 关闭持久化(SSR) |
33
+ | `saveKey` | `string` | `game-sudoku:save` | 存档键,多实例分开 |
34
+ | `bestKey` | `string` | `game-sudoku:best` | 分难度最佳用时键 |
35
+ | `freshStart` | `boolean` | `false` | 忽略已有存档开新局 |
36
+ | `autofocus` | `boolean` | `true` | mount 后 focus 棋盘让键盘立即可用 |
37
+ | `showPad` | `boolean` | `true` | 是否显示数字键盘 |
38
+ | `showHint` | `boolean` | `false` | 是否显示「提示」按钮(`hint()` 始终在 ref / `useGame` 上) |
39
+ | `title` | `string` | `'数独'` | 头部标题 |
40
+
41
+ ## 事件
42
+
43
+ - `input(digit, changed)` — 填数后触发。
44
+ - `solved({ difficulty, seconds })` — 通关瞬间触发一次。
45
+
46
+ ## Ref 命令式方法(`GameSudokuInstance`)
47
+
48
+ `select / input / erase / toggleNote / hint / newGame / undo`。
49
+
50
+ ## Composable(想自己画 UI)
51
+
52
+ ```ts
53
+ import { useGame } from '@my-games/sudoku-vue';
54
+ const { state, select, input, hint, undo, newGame } = useGame({ difficulty: 'medium' });
55
+ ```
56
+
57
+ `state` 是 `ShallowRef<GameState>`,模板里 auto-unwrap。
22
58
 
23
59
  完整说明见仓库根 README:<https://github.com/my-gam/sudoku#readme>
24
60
 
@@ -1,8 +1,53 @@
1
+ import { Digit, Difficulty, SaveStorage } from '@my-games/sudoku-core';
2
+ /**
3
+ * GameSudoku —— Vue 3 官方组件,桥接 @my-games/sudoku-core 引擎。
4
+ *
5
+ * 输入模型:
6
+ * - 点击格子选中;数字面板 / 键盘 1-9 填数;N 切笔记模式;Backspace/Delete 擦除;方向键移动选中。
7
+ * - 键盘走 window keydown,但严格判 `document.activeElement === boardEl`,多实例并存只活动那个响应。
8
+ * - 视觉/类名全部复用 core 的 style.css,与 React 版行为对齐。
9
+ */
1
10
  interface Props {
11
+ difficulty?: Difficulty;
12
+ storage?: SaveStorage | null;
13
+ saveKey?: string;
14
+ bestKey?: string;
15
+ freshStart?: boolean;
16
+ autofocus?: boolean;
2
17
  title?: string;
18
+ showPad?: boolean;
19
+ showHint?: boolean;
3
20
  }
4
- declare const __VLS_export: import('vue').DefineComponent<Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<Props> & Readonly<{}>, {
21
+ declare const __VLS_export: import('vue').DefineComponent<Props, {
22
+ select: (row: number, col: number) => void;
23
+ input: (digit: Digit) => boolean;
24
+ erase: () => boolean;
25
+ toggleNote: (digit: Digit) => boolean;
26
+ hint: () => boolean;
27
+ newGame: (difficulty?: Difficulty) => void;
28
+ undo: () => boolean;
29
+ }, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {} & {
30
+ input: (digit: Digit, changed: boolean) => any;
31
+ solved: (payload: {
32
+ difficulty: Difficulty;
33
+ seconds: number;
34
+ }) => any;
35
+ }, string, import('vue').PublicProps, Readonly<Props> & Readonly<{
36
+ onInput?: ((digit: Digit, changed: boolean) => any) | undefined;
37
+ onSolved?: ((payload: {
38
+ difficulty: Difficulty;
39
+ seconds: number;
40
+ }) => any) | undefined;
41
+ }>, {
42
+ difficulty: Difficulty;
43
+ storage: SaveStorage | null;
44
+ saveKey: string;
45
+ bestKey: string;
46
+ freshStart: boolean;
47
+ autofocus: boolean;
5
48
  title: string;
49
+ showPad: boolean;
50
+ showHint: boolean;
6
51
  }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
7
52
  declare const _default: typeof __VLS_export;
8
53
  export default _default;
@@ -1 +1 @@
1
- {"version":3,"file":"GameSudoku.vue.d.ts","sourceRoot":"","sources":["../src/GameSudoku.vue"],"names":[],"mappings":"AAqBA,OAAO,iCAAiC,CAAC;AAGzC,UAAU,KAAK;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA2BD,QAAA,MAAM,YAAY;;6EAGhB,CAAC;wBACkB,OAAO,YAAY;AAAxC,wBAAyC"}
1
+ {"version":3,"file":"GameSudoku.vue.d.ts","sourceRoot":"","sources":["../src/GameSudoku.vue"],"names":[],"mappings":"AA0VA,OAAO,iCAAiC,CAAC;AAEzC,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAG5E;;;;;;;GAOG;AAEH,UAAU,KAAK;IACb,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,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAmfD,QAAA,MAAM,YAAY;;;;;;;;;;;oBAleqB,UAAU;iBAAW,MAAM;;;;;oBAA3B,UAAU;iBAAW,MAAM;;;;;;;;;;;;6EAuehE,CAAC;wBACkB,OAAO,YAAY;AAAxC,wBAAyC"}
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,5 @@
1
1
  export { default as GameSudoku } from './GameSudoku.vue';
2
- export declare const SUDOKU_PACKAGE: "sudoku-vue";
3
- export declare const SUDOKU_STAGE: "0.0.0";
2
+ export { useGame, type UseGameReturn } from './useGame';
3
+ export type { GameSudokuInstance } from './instance';
4
+ export type { Cell, Difficulty, Digit, GameState, GameStatus, Grid, SaveStorage } from '@my-games/sudoku-core';
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEzD,eAAO,MAAM,cAAc,EAAG,YAAqB,CAAC;AACpD,eAAO,MAAM,YAAY,EAAG,OAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AACxD,YAAY,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAGrD,YAAY,EACV,IAAI,EACJ,UAAU,EACV,KAAK,EACL,SAAS,EACT,UAAU,EACV,IAAI,EACJ,WAAW,EACZ,MAAM,uBAAuB,CAAC"}