@fgv/ts-sudoku-ui 5.1.0-2 → 5.1.0-21

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 (78) hide show
  1. package/eslint.config.js +18 -0
  2. package/lib/tsdoc-metadata.json +1 -1
  3. package/package.json +22 -21
  4. package/.test-failures-catalog.md +0 -470
  5. package/CHANGELOG.md +0 -11
  6. package/CSS_MODULES_MIGRATION_SUMMARY.md +0 -189
  7. package/dist/ts-sudoku-ui.d.ts +0 -668
  8. package/etc/ts-sudoku-ui.api.md +0 -481
  9. package/lib/test/fixtures/killerSudokuPuzzles.d.ts +0 -36
  10. package/lib/test/fixtures/killerSudokuPuzzles.d.ts.map +0 -1
  11. package/lib/test/fixtures/killerSudokuPuzzles.js +0 -57
  12. package/lib/test/fixtures/killerSudokuPuzzles.js.map +0 -1
  13. package/lib/test/unit/components/SudokuGridEntry.testHelpers.d.ts +0 -22
  14. package/lib/test/unit/components/SudokuGridEntry.testHelpers.d.ts.map +0 -1
  15. package/lib/test/unit/components/SudokuGridEntry.testHelpers.js +0 -116
  16. package/lib/test/unit/components/SudokuGridEntry.testHelpers.js.map +0 -1
  17. package/lib-commonjs/components/CageOverlay.js +0 -138
  18. package/lib-commonjs/components/CageOverlay.js.map +0 -1
  19. package/lib-commonjs/components/CageSumIndicator.js +0 -71
  20. package/lib-commonjs/components/CageSumIndicator.js.map +0 -1
  21. package/lib-commonjs/components/CombinationCard.js +0 -68
  22. package/lib-commonjs/components/CombinationCard.js.map +0 -1
  23. package/lib-commonjs/components/CombinationGrid.js +0 -56
  24. package/lib-commonjs/components/CombinationGrid.js.map +0 -1
  25. package/lib-commonjs/components/CompactControlRibbon.js +0 -146
  26. package/lib-commonjs/components/CompactControlRibbon.js.map +0 -1
  27. package/lib-commonjs/components/DualKeypad.js +0 -142
  28. package/lib-commonjs/components/DualKeypad.js.map +0 -1
  29. package/lib-commonjs/components/KillerCombinationsExplorer.js +0 -74
  30. package/lib-commonjs/components/KillerCombinationsExplorer.js.map +0 -1
  31. package/lib-commonjs/components/KillerCombinationsModal.js +0 -140
  32. package/lib-commonjs/components/KillerCombinationsModal.js.map +0 -1
  33. package/lib-commonjs/components/KillerCombinationsPanel.js +0 -84
  34. package/lib-commonjs/components/KillerCombinationsPanel.js.map +0 -1
  35. package/lib-commonjs/components/NumberKeypad.js +0 -186
  36. package/lib-commonjs/components/NumberKeypad.js.map +0 -1
  37. package/lib-commonjs/components/SudokuCell.js +0 -245
  38. package/lib-commonjs/components/SudokuCell.js.map +0 -1
  39. package/lib-commonjs/components/SudokuControls.js +0 -145
  40. package/lib-commonjs/components/SudokuControls.js.map +0 -1
  41. package/lib-commonjs/components/SudokuGrid.js +0 -182
  42. package/lib-commonjs/components/SudokuGrid.js.map +0 -1
  43. package/lib-commonjs/components/SudokuGridEntry.js +0 -378
  44. package/lib-commonjs/components/SudokuGridEntry.js.map +0 -1
  45. package/lib-commonjs/components/ValidationDisplay.js +0 -100
  46. package/lib-commonjs/components/ValidationDisplay.js.map +0 -1
  47. package/lib-commonjs/contexts/DiagnosticLoggerContext.js +0 -104
  48. package/lib-commonjs/contexts/DiagnosticLoggerContext.js.map +0 -1
  49. package/lib-commonjs/contexts/index.js +0 -32
  50. package/lib-commonjs/contexts/index.js.map +0 -1
  51. package/lib-commonjs/hooks/useCombinationElimination.js +0 -126
  52. package/lib-commonjs/hooks/useCombinationElimination.js.map +0 -1
  53. package/lib-commonjs/hooks/useKeyboardShortcut.js +0 -58
  54. package/lib-commonjs/hooks/useKeyboardShortcut.js.map +0 -1
  55. package/lib-commonjs/hooks/useKillerCombinations.js +0 -68
  56. package/lib-commonjs/hooks/useKillerCombinations.js.map +0 -1
  57. package/lib-commonjs/hooks/useLongPress.js +0 -72
  58. package/lib-commonjs/hooks/useLongPress.js.map +0 -1
  59. package/lib-commonjs/hooks/usePuzzleSession.js +0 -401
  60. package/lib-commonjs/hooks/usePuzzleSession.js.map +0 -1
  61. package/lib-commonjs/hooks/useResponsiveLayout.js +0 -142
  62. package/lib-commonjs/hooks/useResponsiveLayout.js.map +0 -1
  63. package/lib-commonjs/index.browser.js +0 -29
  64. package/lib-commonjs/index.browser.js.map +0 -1
  65. package/lib-commonjs/index.js +0 -73
  66. package/lib-commonjs/index.js.map +0 -1
  67. package/lib-commonjs/test/fixtures/killerSudokuPuzzles.js +0 -60
  68. package/lib-commonjs/test/fixtures/killerSudokuPuzzles.js.map +0 -1
  69. package/lib-commonjs/test/unit/components/SudokuGridEntry.testHelpers.js +0 -125
  70. package/lib-commonjs/test/unit/components/SudokuGridEntry.testHelpers.js.map +0 -1
  71. package/lib-commonjs/types/index.js +0 -26
  72. package/lib-commonjs/types/index.js.map +0 -1
  73. package/lib-commonjs/utils/CageLookupManager.js +0 -287
  74. package/lib-commonjs/utils/CageLookupManager.js.map +0 -1
  75. package/lib-commonjs/utils/CagePatternManager.js +0 -143
  76. package/lib-commonjs/utils/CagePatternManager.js.map +0 -1
  77. package/lib-commonjs/utils/puzzleDefinitionHelper.js +0 -103
  78. package/lib-commonjs/utils/puzzleDefinitionHelper.js.map +0 -1
@@ -1,378 +0,0 @@
1
- "use strict";
2
- /*
3
- * MIT License
4
- *
5
- * Copyright (c) 2023 Erik Fortune
6
- *
7
- * Permission is hereby granted, free of charge, to any person obtaining a copy
8
- * of this software and associated documentation files (the "Software"), to deal
9
- * in the Software without restriction, including without limitation the rights
10
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
- * copies of the Software, and to permit persons to whom the Software is
12
- * furnished to do so, subject to the following conditions:
13
- *
14
- * The above copyright notice and this permission notice shall be included in all
15
- * copies or substantial portions of the Software.
16
- *
17
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
- * SOFTWARE.
24
- */
25
- Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.SudokuGridEntry = void 0;
27
- const tslib_1 = require("tslib");
28
- const react_1 = tslib_1.__importStar(require("react"));
29
- const usePuzzleSession_1 = require("../hooks/usePuzzleSession");
30
- const SudokuGrid_1 = require("./SudokuGrid");
31
- const CompactControlRibbon_1 = require("./CompactControlRibbon");
32
- const DualKeypad_1 = require("./DualKeypad");
33
- const useResponsiveLayout_1 = require("../hooks/useResponsiveLayout");
34
- const KillerCombinationsExplorer_1 = require("./KillerCombinationsExplorer");
35
- const useKeyboardShortcut_1 = require("../hooks/useKeyboardShortcut");
36
- /**
37
- * Main container component that orchestrates puzzle entry
38
- * @public
39
- */
40
- const SudokuGridEntry = ({ initialPuzzleDescription, onStateChange, onValidationErrors, className, forceLayoutMode }) => {
41
- const { session, selectedCell, selectedCells, inputMode, setSelectedCell, setSelectedCells, setInputMode, cellDisplayInfo, cageDisplayInfo, validationErrors, isValid, isSolved, canUndo, canRedo, error, updateCellValue, toggleCellNote, clearCellNotes, navigateToCell, undo, redo, reset, exportPuzzle } = (0, usePuzzleSession_1.usePuzzleSession)(initialPuzzleDescription);
42
- // Drag-to-add multiselect state
43
- const [isDragging, setIsDragging] = (0, react_1.useState)(false);
44
- const dragStartCellRef = (0, react_1.useRef)(null);
45
- // Killer Combinations Explorer state
46
- const [isCombinationsExplorerOpen, setIsCombinationsExplorerOpen] = (0, react_1.useState)(false);
47
- // Get responsive layout information
48
- const responsiveLayout = (0, useResponsiveLayout_1.useResponsiveLayout)(forceLayoutMode);
49
- // Determine selected cage for killer sudoku
50
- const selectedCage = (0, react_1.useMemo)(() => {
51
- if (initialPuzzleDescription?.type === 'killer-sudoku' &&
52
- selectedCells.length > 0 &&
53
- cageDisplayInfo &&
54
- cageDisplayInfo.length > 0) {
55
- // Find all cages that contain selected cells
56
- const cagesContainingSelection = cageDisplayInfo.filter((cageInfo) => selectedCells.some((cellId) => cageInfo.cage.containsCell(cellId)));
57
- // Only return a cage if all selected cells are in the same cage
58
- if (cagesContainingSelection.length === 1) {
59
- // Verify all selected cells are in this cage
60
- const cage = cagesContainingSelection[0].cage;
61
- const allInSameCage = selectedCells.every((cellId) => cage.containsCell(cellId));
62
- if (allInSameCage) {
63
- return cage;
64
- }
65
- }
66
- }
67
- return null;
68
- }, [initialPuzzleDescription?.type, selectedCells, cageDisplayInfo]);
69
- // Keyboard shortcut for combinations explorer
70
- (0, useKeyboardShortcut_1.useKeyboardShortcut)('k', () => {
71
- if (selectedCage) {
72
- setIsCombinationsExplorerOpen((prev) => !prev);
73
- }
74
- }, { ctrl: true, meta: true, preventDefault: true });
75
- // Handle cell selection with multi-select support
76
- const handleCellSelect = (0, react_1.useCallback)((cellId, event) => {
77
- if (event && (event.ctrlKey || event.metaKey)) {
78
- // Multi-select mode: add/remove from selection
79
- const currentIndex = selectedCells.indexOf(cellId);
80
- if (currentIndex >= 0) {
81
- // Remove from selection
82
- const newSelection = selectedCells.filter((id) => id !== cellId);
83
- setSelectedCells(newSelection);
84
- // If we removed the primary selected cell, set new primary
85
- if (selectedCell === cellId && newSelection.length > 0) {
86
- setSelectedCell(newSelection[0]);
87
- }
88
- else if (newSelection.length === 0) {
89
- setSelectedCell(null);
90
- }
91
- }
92
- else {
93
- // Add to selection
94
- setSelectedCells([...selectedCells, cellId]);
95
- setSelectedCell(cellId); // Make this the primary selection
96
- }
97
- }
98
- else {
99
- // Single select mode
100
- setSelectedCell(cellId);
101
- setSelectedCells([cellId]);
102
- }
103
- }, [selectedCell, selectedCells, setSelectedCell, setSelectedCells]);
104
- // Handle cell value changes
105
- const handleCellValueChange = (0, react_1.useCallback)((cellId, value) => {
106
- updateCellValue(cellId, value);
107
- }, [updateCellValue]);
108
- // Handle note toggle (supports multi-select with normalize-to-same-state logic)
109
- const handleNoteToggle = (0, react_1.useCallback)((cellId, note) => {
110
- // If multiple cells are selected and this is one of them, apply normalize logic to all
111
- if (selectedCells.length > 1 && selectedCells.includes(cellId)) {
112
- // Get all selected cell display info to check current note states
113
- const selectedCellsInfo = cellDisplayInfo.filter((cell) => selectedCells.includes(cell.id));
114
- // Count how many selected cells currently have this note
115
- const cellsWithNote = selectedCellsInfo.filter((cell) => cell.contents.notes.includes(note));
116
- const allSelected = selectedCellsInfo.length;
117
- const withNote = cellsWithNote.length;
118
- // Normalize logic:
119
- // - If ALL have note: Remove from all
120
- // - If SOME have note: Add to all (normalize to "all have")
121
- // - If NONE have note: Add to all
122
- if (withNote === allSelected) {
123
- // ALL have the note → remove from all
124
- selectedCells.forEach((id) => {
125
- // Only remove if the cell actually has the note (safety check)
126
- const cellInfo = cellDisplayInfo.find((c) => c.id === id);
127
- if (cellInfo && cellInfo.contents.notes.includes(note)) {
128
- toggleCellNote(id, note);
129
- }
130
- });
131
- }
132
- else {
133
- // SOME or NONE have the note → add to all
134
- selectedCells.forEach((id) => {
135
- // Only add if the cell doesn't already have the note
136
- const cellInfo = cellDisplayInfo.find((c) => c.id === id);
137
- if (cellInfo && !cellInfo.contents.notes.includes(note)) {
138
- toggleCellNote(id, note);
139
- }
140
- });
141
- }
142
- }
143
- else {
144
- // Single cell selection - normal toggle behavior
145
- toggleCellNote(cellId, note);
146
- }
147
- }, [toggleCellNote, selectedCells, cellDisplayInfo]);
148
- // Handle clear all notes (supports multi-select)
149
- const handleClearAllNotes = (0, react_1.useCallback)((cellId) => {
150
- // If multiple cells are selected and this is one of them, apply to all
151
- if (selectedCells.length > 1 && selectedCells.includes(cellId)) {
152
- selectedCells.forEach((id) => {
153
- clearCellNotes(id);
154
- });
155
- }
156
- else {
157
- clearCellNotes(cellId);
158
- }
159
- }, [clearCellNotes, selectedCells]);
160
- // Handle long press toggle for multiselect
161
- const handleLongPressToggle = (0, react_1.useCallback)((cellId, event) => {
162
- event.preventDefault();
163
- // Toggle cell in selection: if not in selection, add it; if in selection, remove it
164
- const currentIndex = selectedCells.indexOf(cellId);
165
- if (currentIndex >= 0) {
166
- // Remove from selection
167
- const newSelection = selectedCells.filter((id) => id !== cellId);
168
- setSelectedCells(newSelection);
169
- // If we removed the primary selected cell, set new primary or clear
170
- if (selectedCell === cellId) {
171
- if (newSelection.length > 0) {
172
- setSelectedCell(newSelection[0]);
173
- }
174
- else {
175
- setSelectedCell(null);
176
- }
177
- }
178
- }
179
- else {
180
- // Add to selection
181
- const newSelection = [...selectedCells, cellId];
182
- setSelectedCells(newSelection);
183
- setSelectedCell(cellId); // Make this the primary selection
184
- }
185
- // Enter drag mode for potential drag-to-add functionality
186
- setIsDragging(true);
187
- dragStartCellRef.current = cellId;
188
- }, [selectedCell, selectedCells, setSelectedCell, setSelectedCells]);
189
- // Handle drag over cell during drag-to-add
190
- const handleDragOver = (0, react_1.useCallback)((cellId) => {
191
- if (!isDragging || dragStartCellRef.current === null)
192
- return;
193
- // Only add cells to selection during drag (don't toggle/remove)
194
- if (!selectedCells.includes(cellId)) {
195
- const newSelection = [...selectedCells, cellId];
196
- setSelectedCells(newSelection);
197
- // Don't change primary selection during drag
198
- }
199
- }, [isDragging, selectedCells, setSelectedCells]);
200
- // Handle end of drag operation
201
- const handleDragEnd = (0, react_1.useCallback)(() => {
202
- setIsDragging(false);
203
- dragStartCellRef.current = null;
204
- }, []);
205
- // Handle navigation
206
- const handleNavigate = (0, react_1.useCallback)((direction) => {
207
- navigateToCell(direction);
208
- }, [navigateToCell]);
209
- // Handle export
210
- const handleExport = (0, react_1.useCallback)(() => {
211
- const exported = exportPuzzle();
212
- if (exported) {
213
- // Export puzzle to file - exported object contains the puzzle data
214
- // Create a downloadable file
215
- const dataStr = JSON.stringify(exported, null, 2);
216
- const dataBlob = new Blob([dataStr], { type: 'application/json' });
217
- const url = URL.createObjectURL(dataBlob);
218
- const link = document.createElement('a');
219
- link.href = url;
220
- /* c8 ignore next 1 - defensive in depth */
221
- link.download = `${exported.id ?? 'sudoku-puzzle'}.json`;
222
- document.body.appendChild(link);
223
- link.click();
224
- document.body.removeChild(link);
225
- URL.revokeObjectURL(url);
226
- }
227
- }, [exportPuzzle]);
228
- // Handle dual keypad note press (applies to all selected cells)
229
- const handleKeypadNotePress = (0, react_1.useCallback)((number) => {
230
- if (selectedCells.length > 0) {
231
- selectedCells.forEach((cellId) => {
232
- toggleCellNote(cellId, number);
233
- });
234
- }
235
- }, [selectedCells, toggleCellNote]);
236
- // Handle dual keypad value press (applies to all selected cells)
237
- const handleKeypadValuePress = (0, react_1.useCallback)((number) => {
238
- if (selectedCells.length > 0) {
239
- selectedCells.forEach((cellId) => {
240
- updateCellValue(cellId, number);
241
- });
242
- }
243
- }, [selectedCells, updateCellValue]);
244
- // Handle dual keypad clear notes (applies to all selected cells)
245
- const handleKeypadClearNotes = (0, react_1.useCallback)(() => {
246
- if (selectedCells.length > 0) {
247
- selectedCells.forEach((cellId) => {
248
- clearCellNotes(cellId);
249
- });
250
- }
251
- }, [selectedCells, clearCellNotes]);
252
- // Handle dual keypad clear values (applies to all selected cells)
253
- const handleKeypadClearValues = (0, react_1.useCallback)(() => {
254
- if (selectedCells.length > 0) {
255
- selectedCells.forEach((cellId) => {
256
- updateCellValue(cellId, undefined);
257
- });
258
- }
259
- }, [selectedCells, updateCellValue]);
260
- // Notify parent of state changes
261
- (0, react_1.useEffect)(() => {
262
- if (onStateChange) {
263
- onStateChange(isValid, isSolved);
264
- }
265
- }, [isValid, isSolved, onStateChange]);
266
- // Notify parent of validation errors
267
- (0, react_1.useEffect)(() => {
268
- if (onValidationErrors) {
269
- onValidationErrors(validationErrors);
270
- }
271
- }, [validationErrors, onValidationErrors]);
272
- // Calculate container classes
273
- const containerClasses = (0, react_1.useMemo)(() => {
274
- const classes = ['w-full min-h-screen font-sans'];
275
- if (className)
276
- classes.push(className);
277
- return classes.join(' ');
278
- }, [className]);
279
- // Check if we're in mobile mode with visible keypads (side-by-side or stacked)
280
- const isMobileWithVisibleKeypads = (0, react_1.useMemo)(() =>
281
- /* c8 ignore next 3 - defense in depth */
282
- responsiveLayout.deviceType === 'mobile' &&
283
- (responsiveLayout.keypadLayoutMode === 'side-by-side' ||
284
- responsiveLayout.keypadLayoutMode === 'stacked'), [responsiveLayout.deviceType, responsiveLayout.keypadLayoutMode]);
285
- // Determine if we should show traditional input mode toggle - only when keypads are not visible
286
- const showTraditionalModeToggle = responsiveLayout.keypadLayoutMode === 'hidden' || responsiveLayout.keypadLayoutMode === 'overlay';
287
- // Calculate main container classes - mobile-optimized spacing
288
- const mainContainerClasses = (0, react_1.useMemo)(() => {
289
- const classes = ['flex items-start mx-auto'];
290
- // Optimize spacing for mobile with visible keypads
291
- if (isMobileWithVisibleKeypads) {
292
- classes.push('gap-2 p-2');
293
- }
294
- else {
295
- classes.push('gap-5 p-5');
296
- }
297
- // Layout based on device and orientation
298
- if (responsiveLayout.keypadLayoutMode === 'stacked' && responsiveLayout.orientation === 'landscape') {
299
- // Landscape: keypads beside grid
300
- classes.push('flex-row max-w-screen-lg justify-center');
301
- }
302
- else if (responsiveLayout.keypadLayoutMode === 'side-by-side') {
303
- // Portrait mobile: keypads BELOW grid
304
- classes.push('flex-col items-center max-w-2xl');
305
- }
306
- else {
307
- // Default vertical layout
308
- classes.push('flex-col items-center max-w-2xl');
309
- }
310
- return classes.join(' ');
311
- }, [responsiveLayout.keypadLayoutMode, responsiveLayout.orientation, isMobileWithVisibleKeypads]);
312
- // Calculate game area classes
313
- const gameAreaClasses = (0, react_1.useMemo)(() => {
314
- const classes = ['flex flex-col items-center'];
315
- // Optimize spacing for mobile with visible keypads
316
- if (isMobileWithVisibleKeypads) {
317
- classes.push('gap-2');
318
- }
319
- else {
320
- classes.push('gap-5');
321
- }
322
- if (responsiveLayout.keypadLayoutMode === 'stacked' && responsiveLayout.orientation === 'landscape') {
323
- classes.push('flex-none');
324
- }
325
- else {
326
- classes.push('flex-1');
327
- }
328
- return classes.join(' ');
329
- }, [responsiveLayout.keypadLayoutMode, responsiveLayout.orientation, isMobileWithVisibleKeypads]);
330
- // Show error state if session failed to initialize
331
- if (error) {
332
- return (react_1.default.createElement("div", { className: containerClasses, "data-testid": "sudoku-grid-entry-error" },
333
- react_1.default.createElement("div", { className: "p-5 border border-red-500 rounded-lg bg-red-50 text-red-800 text-center mx-5 my-5 max-w-md" },
334
- react_1.default.createElement("div", { className: "font-semibold mb-2 text-lg" }, "Error"),
335
- react_1.default.createElement("div", { className: "text-sm leading-relaxed" }, error))));
336
- }
337
- // Show loading state while session is initializing
338
- if (!session) {
339
- return (react_1.default.createElement("div", { className: containerClasses, "data-testid": "sudoku-grid-entry-loading" },
340
- react_1.default.createElement("div", { className: "p-5 text-center text-gray-600 mx-5 my-5 max-w-md" },
341
- react_1.default.createElement("div", { className: "text-lg" }, "Loading puzzle..."))));
342
- }
343
- return (react_1.default.createElement("div", { className: containerClasses, "data-testid": "sudoku-grid-entry" },
344
- react_1.default.createElement("div", { className: mainContainerClasses },
345
- react_1.default.createElement("div", { className: gameAreaClasses },
346
- react_1.default.createElement("div", { className: `text-center ${isMobileWithVisibleKeypads ? 'mb-1' : 'mb-3'}` },
347
- react_1.default.createElement("h2", { className: `m-0 font-semibold text-gray-900 ${isMobileWithVisibleKeypads ? 'mb-1 text-lg' : 'mb-2 text-2xl'}` }, session.description),
348
- session.id && (react_1.default.createElement("div", { className: `text-gray-600 m-0 ${isMobileWithVisibleKeypads ? 'text-xs' : 'text-sm'}` },
349
- "ID: ",
350
- session.id))),
351
- showTraditionalModeToggle && (react_1.default.createElement("div", { className: "flex items-center gap-3 p-2 px-4 bg-gray-100 rounded-lg border border-gray-300 sm:flex-row flex-col sm:gap-3 gap-2 sm:p-2 sm:px-4 p-2" },
352
- react_1.default.createElement("span", { className: "text-sm font-semibold text-gray-900 sm:text-sm text-xs" }, "Input Mode:"),
353
- react_1.default.createElement("button", { type: "button", onClick: () => setInputMode('notes'), className: `px-3 py-1 border border-gray-300 rounded text-gray-900 text-xs cursor-pointer font-semibold transition-all duration-150 min-h-11 flex items-center justify-center ${inputMode === 'notes'
354
- ? 'bg-blue-500 text-white border-blue-600'
355
- : 'bg-gray-50 hover:bg-blue-100 hover:border-blue-500'} sm:min-w-40 min-w-30 sm:px-4 px-2` }, "\uD83D\uDCDD Notes (Default)"),
356
- react_1.default.createElement("button", { type: "button", onClick: () => setInputMode('value'), className: `px-3 py-1 border border-gray-300 rounded text-gray-900 text-xs cursor-pointer font-semibold transition-all duration-150 min-h-11 flex items-center justify-center ${inputMode === 'value'
357
- ? 'bg-blue-500 text-white border-blue-600'
358
- : 'bg-gray-50 hover:bg-blue-100 hover:border-blue-500'} sm:min-w-40 min-w-30 sm:px-4 px-2` }, "\u270F\uFE0F Values (Hold Shift/Ctrl/Cmd)"))),
359
- react_1.default.createElement("div", { className: "h-6 flex items-center justify-center" }, selectedCells.length > 1 && (react_1.default.createElement("div", { className: "px-3 py-1 bg-green-50 rounded border border-green-500 text-xs text-green-800 text-center font-normal" },
360
- selectedCells.length,
361
- " cells selected - Operations will apply to all selected cells"))),
362
- react_1.default.createElement(SudokuGrid_1.SudokuGrid, { numRows: session.numRows, numColumns: session.numColumns, cells: cellDisplayInfo, cages: cageDisplayInfo, selectedCell: selectedCell, selectedCells: selectedCells, inputMode: inputMode, puzzleType: initialPuzzleDescription?.type, puzzleDimensions: session.puzzle.dimensions, onCellSelect: handleCellSelect, onLongPressToggle: handleLongPressToggle, onCellValueChange: handleCellValueChange, onNoteToggle: handleNoteToggle, onClearAllNotes: handleClearAllNotes, onNavigate: handleNavigate, onDragOver: handleDragOver, onDragEnd: handleDragEnd, isDragging: isDragging }),
363
- react_1.default.createElement(CompactControlRibbon_1.CompactControlRibbon, { canUndo: canUndo, canRedo: canRedo, canReset: true, isValid: isValid, isSolved: isSolved, validationErrors: validationErrors, onUndo: undo, onRedo: redo, onReset: reset, onExport: handleExport, showCombinations: initialPuzzleDescription?.type === 'killer-sudoku', canShowCombinations: selectedCage !== null, onCombinations: () => setIsCombinationsExplorerOpen(true), className: "mt-3 mb-2" }),
364
- showTraditionalModeToggle && responsiveLayout.deviceType !== 'mobile' && (react_1.default.createElement("div", { className: "text-xs text-gray-600 text-center leading-normal max-w-md mx-auto sm:text-xs text-2xs sm:max-w-lg max-w-80" },
365
- react_1.default.createElement("div", { className: "font-semibold mb-1 text-gray-900" }, "Notes-First Input System:"),
366
- react_1.default.createElement("div", { className: "mb-0.5" }, "\u2022 Numbers add/remove notes by default"),
367
- react_1.default.createElement("div", { className: "mb-0.5" }, "\u2022 Hold Shift/Ctrl/Cmd + number to place value"),
368
- react_1.default.createElement("div", { className: "mb-0.5" }, "\u2022 Double-tap cell to clear all notes"),
369
- react_1.default.createElement("div", { className: "mb-0.5" }, "\u2022 Ctrl/Cmd + click to select multiple cells"),
370
- react_1.default.createElement("div", { className: "mb-0" }, "\u2022 Smart notes: placing values removes conflicting notes")))),
371
- responsiveLayout.keypadLayoutMode === 'side-by-side' && (react_1.default.createElement(DualKeypad_1.DualKeypad, { inputMode: inputMode, onInputModeChange: setInputMode, onNotePress: handleKeypadNotePress, onValuePress: handleKeypadValuePress, onClearNotes: handleKeypadClearNotes, onClearValues: handleKeypadClearValues, hasCellSelection: selectedCells.length > 0, selectedCellCount: selectedCells.length, showOverlayToggle: false })),
372
- responsiveLayout.keypadLayoutMode === 'stacked' && (react_1.default.createElement(DualKeypad_1.DualKeypad, { inputMode: inputMode, onInputModeChange: setInputMode, onNotePress: handleKeypadNotePress, onValuePress: handleKeypadValuePress, onClearNotes: handleKeypadClearNotes, onClearValues: handleKeypadClearValues, hasCellSelection: selectedCells.length > 0, selectedCellCount: selectedCells.length, showOverlayToggle: false, forceLayoutMode: forceLayoutMode }))),
373
- (responsiveLayout.keypadLayoutMode === 'overlay' ||
374
- responsiveLayout.keypadLayoutMode === 'hidden') && (react_1.default.createElement(DualKeypad_1.DualKeypad, { inputMode: inputMode, onInputModeChange: setInputMode, onNotePress: handleKeypadNotePress, onValuePress: handleKeypadValuePress, onClearNotes: handleKeypadClearNotes, onClearValues: handleKeypadClearValues, hasCellSelection: selectedCells.length > 0, selectedCellCount: selectedCells.length, showOverlayToggle: true })),
375
- initialPuzzleDescription?.type === 'killer-sudoku' && selectedCage && (react_1.default.createElement(KillerCombinationsExplorer_1.KillerCombinationsExplorer, { selectedCage: selectedCage, puzzleId: /* c8 ignore next 1 - defense in depth */ initialPuzzleDescription.id ?? 'default', isOpen: isCombinationsExplorerOpen, onClose: /* c8 ignore next 1 - defense in depth */ () => setIsCombinationsExplorerOpen(false) }))));
376
- };
377
- exports.SudokuGridEntry = SudokuGridEntry;
378
- //# sourceMappingURL=SudokuGridEntry.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"SudokuGridEntry.js","sourceRoot":"","sources":["../../src/components/SudokuGridEntry.tsx"],"names":[],"mappings":"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","sourcesContent":["/*\n * MIT License\n *\n * Copyright (c) 2023 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport React, { useCallback, useEffect, useMemo, useState, useRef } from 'react';\nimport { CellId, NavigationDirection } from '@fgv/ts-sudoku-lib';\nimport { ISudokuGridEntryProps } from '../types';\nimport { usePuzzleSession } from '../hooks/usePuzzleSession';\nimport { SudokuGrid } from './SudokuGrid';\nimport { CompactControlRibbon } from './CompactControlRibbon';\nimport { DualKeypad } from './DualKeypad';\nimport { useResponsiveLayout } from '../hooks/useResponsiveLayout';\nimport { KillerCombinationsExplorer } from './KillerCombinationsExplorer';\nimport { useKeyboardShortcut } from '../hooks/useKeyboardShortcut';\n\n/**\n * Main container component that orchestrates puzzle entry\n * @public\n */\nexport const SudokuGridEntry: React.FC<ISudokuGridEntryProps> = ({\n initialPuzzleDescription,\n onStateChange,\n onValidationErrors,\n className,\n forceLayoutMode\n}) => {\n const {\n session,\n selectedCell,\n selectedCells,\n inputMode,\n setSelectedCell,\n setSelectedCells,\n setInputMode,\n cellDisplayInfo,\n cageDisplayInfo,\n validationErrors,\n isValid,\n isSolved,\n canUndo,\n canRedo,\n error,\n updateCellValue,\n toggleCellNote,\n clearCellNotes,\n navigateToCell,\n undo,\n redo,\n reset,\n exportPuzzle\n } = usePuzzleSession(initialPuzzleDescription);\n\n // Drag-to-add multiselect state\n const [isDragging, setIsDragging] = useState(false);\n const dragStartCellRef = useRef<CellId | null>(null);\n\n // Killer Combinations Explorer state\n const [isCombinationsExplorerOpen, setIsCombinationsExplorerOpen] = useState(false);\n\n // Get responsive layout information\n const responsiveLayout = useResponsiveLayout(forceLayoutMode);\n\n // Determine selected cage for killer sudoku\n const selectedCage = useMemo(() => {\n if (\n initialPuzzleDescription?.type === 'killer-sudoku' &&\n selectedCells.length > 0 &&\n cageDisplayInfo &&\n cageDisplayInfo.length > 0\n ) {\n // Find all cages that contain selected cells\n const cagesContainingSelection = cageDisplayInfo.filter((cageInfo) =>\n selectedCells.some((cellId) => cageInfo.cage.containsCell(cellId))\n );\n\n // Only return a cage if all selected cells are in the same cage\n if (cagesContainingSelection.length === 1) {\n // Verify all selected cells are in this cage\n const cage = cagesContainingSelection[0].cage;\n const allInSameCage = selectedCells.every((cellId) => cage.containsCell(cellId));\n if (allInSameCage) {\n return cage;\n }\n }\n }\n return null;\n }, [initialPuzzleDescription?.type, selectedCells, cageDisplayInfo]);\n\n // Keyboard shortcut for combinations explorer\n useKeyboardShortcut(\n 'k',\n () => {\n if (selectedCage) {\n setIsCombinationsExplorerOpen((prev) => !prev);\n }\n },\n { ctrl: true, meta: true, preventDefault: true }\n );\n\n // Handle cell selection with multi-select support\n const handleCellSelect = useCallback(\n (cellId: CellId, event?: React.MouseEvent) => {\n if (event && (event.ctrlKey || event.metaKey)) {\n // Multi-select mode: add/remove from selection\n const currentIndex = selectedCells.indexOf(cellId);\n if (currentIndex >= 0) {\n // Remove from selection\n const newSelection = selectedCells.filter((id) => id !== cellId);\n setSelectedCells(newSelection);\n // If we removed the primary selected cell, set new primary\n if (selectedCell === cellId && newSelection.length > 0) {\n setSelectedCell(newSelection[0]);\n } else if (newSelection.length === 0) {\n setSelectedCell(null);\n }\n } else {\n // Add to selection\n setSelectedCells([...selectedCells, cellId]);\n setSelectedCell(cellId); // Make this the primary selection\n }\n } else {\n // Single select mode\n setSelectedCell(cellId);\n setSelectedCells([cellId]);\n }\n },\n [selectedCell, selectedCells, setSelectedCell, setSelectedCells]\n );\n\n // Handle cell value changes\n const handleCellValueChange = useCallback(\n (cellId: CellId, value: number | undefined) => {\n updateCellValue(cellId, value);\n },\n [updateCellValue]\n );\n\n // Handle note toggle (supports multi-select with normalize-to-same-state logic)\n const handleNoteToggle = useCallback(\n (cellId: CellId, note: number) => {\n // If multiple cells are selected and this is one of them, apply normalize logic to all\n if (selectedCells.length > 1 && selectedCells.includes(cellId)) {\n // Get all selected cell display info to check current note states\n const selectedCellsInfo = cellDisplayInfo.filter((cell) => selectedCells.includes(cell.id));\n\n // Count how many selected cells currently have this note\n const cellsWithNote = selectedCellsInfo.filter((cell) => cell.contents.notes.includes(note));\n const allSelected = selectedCellsInfo.length;\n const withNote = cellsWithNote.length;\n\n // Normalize logic:\n // - If ALL have note: Remove from all\n // - If SOME have note: Add to all (normalize to \"all have\")\n // - If NONE have note: Add to all\n if (withNote === allSelected) {\n // ALL have the note → remove from all\n selectedCells.forEach((id) => {\n // Only remove if the cell actually has the note (safety check)\n const cellInfo = cellDisplayInfo.find((c) => c.id === id);\n if (cellInfo && cellInfo.contents.notes.includes(note)) {\n toggleCellNote(id, note);\n }\n });\n } else {\n // SOME or NONE have the note → add to all\n selectedCells.forEach((id) => {\n // Only add if the cell doesn't already have the note\n const cellInfo = cellDisplayInfo.find((c) => c.id === id);\n if (cellInfo && !cellInfo.contents.notes.includes(note)) {\n toggleCellNote(id, note);\n }\n });\n }\n } else {\n // Single cell selection - normal toggle behavior\n toggleCellNote(cellId, note);\n }\n },\n [toggleCellNote, selectedCells, cellDisplayInfo]\n );\n\n // Handle clear all notes (supports multi-select)\n const handleClearAllNotes = useCallback(\n (cellId: CellId) => {\n // If multiple cells are selected and this is one of them, apply to all\n if (selectedCells.length > 1 && selectedCells.includes(cellId)) {\n selectedCells.forEach((id) => {\n clearCellNotes(id);\n });\n } else {\n clearCellNotes(cellId);\n }\n },\n [clearCellNotes, selectedCells]\n );\n\n // Handle long press toggle for multiselect\n const handleLongPressToggle = useCallback(\n (cellId: CellId, event: React.TouchEvent | React.MouseEvent) => {\n event.preventDefault();\n\n // Toggle cell in selection: if not in selection, add it; if in selection, remove it\n const currentIndex = selectedCells.indexOf(cellId);\n\n if (currentIndex >= 0) {\n // Remove from selection\n const newSelection = selectedCells.filter((id) => id !== cellId);\n setSelectedCells(newSelection);\n // If we removed the primary selected cell, set new primary or clear\n if (selectedCell === cellId) {\n if (newSelection.length > 0) {\n setSelectedCell(newSelection[0]);\n } else {\n setSelectedCell(null);\n }\n }\n } else {\n // Add to selection\n const newSelection = [...selectedCells, cellId];\n setSelectedCells(newSelection);\n setSelectedCell(cellId); // Make this the primary selection\n }\n\n // Enter drag mode for potential drag-to-add functionality\n setIsDragging(true);\n dragStartCellRef.current = cellId;\n },\n [selectedCell, selectedCells, setSelectedCell, setSelectedCells]\n );\n\n // Handle drag over cell during drag-to-add\n const handleDragOver = useCallback(\n (cellId: CellId) => {\n if (!isDragging || dragStartCellRef.current === null) return;\n\n // Only add cells to selection during drag (don't toggle/remove)\n if (!selectedCells.includes(cellId)) {\n const newSelection = [...selectedCells, cellId];\n setSelectedCells(newSelection);\n // Don't change primary selection during drag\n }\n },\n [isDragging, selectedCells, setSelectedCells]\n );\n\n // Handle end of drag operation\n const handleDragEnd = useCallback(() => {\n setIsDragging(false);\n dragStartCellRef.current = null;\n }, []);\n\n // Handle navigation\n const handleNavigate = useCallback(\n (direction: NavigationDirection) => {\n navigateToCell(direction);\n },\n [navigateToCell]\n );\n\n // Handle export\n const handleExport = useCallback(() => {\n const exported = exportPuzzle();\n if (exported) {\n // Export puzzle to file - exported object contains the puzzle data\n\n // Create a downloadable file\n const dataStr = JSON.stringify(exported, null, 2);\n const dataBlob = new Blob([dataStr], { type: 'application/json' });\n const url = URL.createObjectURL(dataBlob);\n const link = document.createElement('a');\n link.href = url;\n /* c8 ignore next 1 - defensive in depth */\n link.download = `${exported.id ?? 'sudoku-puzzle'}.json`;\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n URL.revokeObjectURL(url);\n }\n }, [exportPuzzle]);\n\n // Handle dual keypad note press (applies to all selected cells)\n const handleKeypadNotePress = useCallback(\n (number: number) => {\n if (selectedCells.length > 0) {\n selectedCells.forEach((cellId) => {\n toggleCellNote(cellId, number);\n });\n }\n },\n [selectedCells, toggleCellNote]\n );\n\n // Handle dual keypad value press (applies to all selected cells)\n const handleKeypadValuePress = useCallback(\n (number: number) => {\n if (selectedCells.length > 0) {\n selectedCells.forEach((cellId) => {\n updateCellValue(cellId, number);\n });\n }\n },\n [selectedCells, updateCellValue]\n );\n\n // Handle dual keypad clear notes (applies to all selected cells)\n const handleKeypadClearNotes = useCallback(() => {\n if (selectedCells.length > 0) {\n selectedCells.forEach((cellId) => {\n clearCellNotes(cellId);\n });\n }\n }, [selectedCells, clearCellNotes]);\n\n // Handle dual keypad clear values (applies to all selected cells)\n const handleKeypadClearValues = useCallback(() => {\n if (selectedCells.length > 0) {\n selectedCells.forEach((cellId) => {\n updateCellValue(cellId, undefined);\n });\n }\n }, [selectedCells, updateCellValue]);\n\n // Notify parent of state changes\n useEffect(() => {\n if (onStateChange) {\n onStateChange(isValid, isSolved);\n }\n }, [isValid, isSolved, onStateChange]);\n\n // Notify parent of validation errors\n useEffect(() => {\n if (onValidationErrors) {\n onValidationErrors(validationErrors);\n }\n }, [validationErrors, onValidationErrors]);\n\n // Calculate container classes\n const containerClasses = useMemo(() => {\n const classes = ['w-full min-h-screen font-sans'];\n if (className) classes.push(className);\n return classes.join(' ');\n }, [className]);\n\n // Check if we're in mobile mode with visible keypads (side-by-side or stacked)\n const isMobileWithVisibleKeypads = useMemo(\n () =>\n /* c8 ignore next 3 - defense in depth */\n responsiveLayout.deviceType === 'mobile' &&\n (responsiveLayout.keypadLayoutMode === 'side-by-side' ||\n responsiveLayout.keypadLayoutMode === 'stacked'),\n [responsiveLayout.deviceType, responsiveLayout.keypadLayoutMode]\n );\n\n // Determine if we should show traditional input mode toggle - only when keypads are not visible\n const showTraditionalModeToggle =\n responsiveLayout.keypadLayoutMode === 'hidden' || responsiveLayout.keypadLayoutMode === 'overlay';\n\n // Calculate main container classes - mobile-optimized spacing\n const mainContainerClasses = useMemo(() => {\n const classes = ['flex items-start mx-auto'];\n\n // Optimize spacing for mobile with visible keypads\n if (isMobileWithVisibleKeypads) {\n classes.push('gap-2 p-2');\n } else {\n classes.push('gap-5 p-5');\n }\n\n // Layout based on device and orientation\n if (responsiveLayout.keypadLayoutMode === 'stacked' && responsiveLayout.orientation === 'landscape') {\n // Landscape: keypads beside grid\n classes.push('flex-row max-w-screen-lg justify-center');\n } else if (responsiveLayout.keypadLayoutMode === 'side-by-side') {\n // Portrait mobile: keypads BELOW grid\n classes.push('flex-col items-center max-w-2xl');\n } else {\n // Default vertical layout\n classes.push('flex-col items-center max-w-2xl');\n }\n\n return classes.join(' ');\n }, [responsiveLayout.keypadLayoutMode, responsiveLayout.orientation, isMobileWithVisibleKeypads]);\n\n // Calculate game area classes\n const gameAreaClasses = useMemo(() => {\n const classes = ['flex flex-col items-center'];\n\n // Optimize spacing for mobile with visible keypads\n if (isMobileWithVisibleKeypads) {\n classes.push('gap-2');\n } else {\n classes.push('gap-5');\n }\n\n if (responsiveLayout.keypadLayoutMode === 'stacked' && responsiveLayout.orientation === 'landscape') {\n classes.push('flex-none');\n } else {\n classes.push('flex-1');\n }\n\n return classes.join(' ');\n }, [responsiveLayout.keypadLayoutMode, responsiveLayout.orientation, isMobileWithVisibleKeypads]);\n\n // Show error state if session failed to initialize\n if (error) {\n return (\n <div className={containerClasses} data-testid=\"sudoku-grid-entry-error\">\n <div className=\"p-5 border border-red-500 rounded-lg bg-red-50 text-red-800 text-center mx-5 my-5 max-w-md\">\n <div className=\"font-semibold mb-2 text-lg\">Error</div>\n <div className=\"text-sm leading-relaxed\">{error}</div>\n </div>\n </div>\n );\n }\n\n // Show loading state while session is initializing\n if (!session) {\n return (\n <div className={containerClasses} data-testid=\"sudoku-grid-entry-loading\">\n <div className=\"p-5 text-center text-gray-600 mx-5 my-5 max-w-md\">\n <div className=\"text-lg\">Loading puzzle...</div>\n </div>\n </div>\n );\n }\n\n return (\n <div className={containerClasses} data-testid=\"sudoku-grid-entry\">\n <div className={mainContainerClasses}>\n {/* Game area container */}\n <div className={gameAreaClasses}>\n {/* Puzzle title - compact on mobile */}\n <div className={`text-center ${isMobileWithVisibleKeypads ? 'mb-1' : 'mb-3'}`}>\n <h2\n className={`m-0 font-semibold text-gray-900 ${\n isMobileWithVisibleKeypads ? 'mb-1 text-lg' : 'mb-2 text-2xl'\n }`}\n >\n {session.description}\n </h2>\n {session.id && (\n <div className={`text-gray-600 m-0 ${isMobileWithVisibleKeypads ? 'text-xs' : 'text-sm'}`}>\n ID: {session.id}\n </div>\n )}\n </div>\n\n {/* Traditional input mode toggle - only show if keypads are hidden/overlay */}\n {showTraditionalModeToggle && (\n <div className=\"flex items-center gap-3 p-2 px-4 bg-gray-100 rounded-lg border border-gray-300 sm:flex-row flex-col sm:gap-3 gap-2 sm:p-2 sm:px-4 p-2\">\n <span className=\"text-sm font-semibold text-gray-900 sm:text-sm text-xs\">Input Mode:</span>\n <button\n type=\"button\"\n onClick={() => setInputMode('notes')}\n className={`px-3 py-1 border border-gray-300 rounded text-gray-900 text-xs cursor-pointer font-semibold transition-all duration-150 min-h-11 flex items-center justify-center ${\n inputMode === 'notes'\n ? 'bg-blue-500 text-white border-blue-600'\n : 'bg-gray-50 hover:bg-blue-100 hover:border-blue-500'\n } sm:min-w-40 min-w-30 sm:px-4 px-2`}\n >\n 📝 Notes (Default)\n </button>\n <button\n type=\"button\"\n onClick={() => setInputMode('value')}\n className={`px-3 py-1 border border-gray-300 rounded text-gray-900 text-xs cursor-pointer font-semibold transition-all duration-150 min-h-11 flex items-center justify-center ${\n inputMode === 'value'\n ? 'bg-blue-500 text-white border-blue-600'\n : 'bg-gray-50 hover:bg-blue-100 hover:border-blue-500'\n } sm:min-w-40 min-w-30 sm:px-4 px-2`}\n >\n ✏️ Values (Hold Shift/Ctrl/Cmd)\n </button>\n </div>\n )}\n\n {/* Multi-select hint - reserve space to prevent layout shift */}\n <div className=\"h-6 flex items-center justify-center\">\n {selectedCells.length > 1 && (\n <div className=\"px-3 py-1 bg-green-50 rounded border border-green-500 text-xs text-green-800 text-center font-normal\">\n {selectedCells.length} cells selected - Operations will apply to all selected cells\n </div>\n )}\n </div>\n\n {/* Main grid */}\n <SudokuGrid\n numRows={session.numRows}\n numColumns={session.numColumns}\n cells={cellDisplayInfo}\n cages={cageDisplayInfo}\n selectedCell={selectedCell}\n selectedCells={selectedCells}\n inputMode={inputMode}\n puzzleType={initialPuzzleDescription?.type}\n puzzleDimensions={session.puzzle.dimensions}\n onCellSelect={handleCellSelect}\n onLongPressToggle={handleLongPressToggle}\n onCellValueChange={handleCellValueChange}\n onNoteToggle={handleNoteToggle}\n onClearAllNotes={handleClearAllNotes}\n onNavigate={handleNavigate}\n onDragOver={handleDragOver}\n onDragEnd={handleDragEnd}\n isDragging={isDragging}\n />\n\n {/* Compact Control Ribbon - positioned between puzzle and keypads */}\n <CompactControlRibbon\n canUndo={canUndo}\n canRedo={canRedo}\n canReset={true}\n isValid={isValid}\n isSolved={isSolved}\n validationErrors={validationErrors}\n onUndo={undo}\n onRedo={redo}\n onReset={reset}\n onExport={handleExport}\n showCombinations={initialPuzzleDescription?.type === 'killer-sudoku'}\n canShowCombinations={selectedCage !== null}\n onCombinations={() => setIsCombinationsExplorerOpen(true)}\n className=\"mt-3 mb-2\"\n />\n\n {/* Help text - only show if using traditional input and not on mobile with visible keypads */}\n {showTraditionalModeToggle && responsiveLayout.deviceType !== 'mobile' && (\n <div className=\"text-xs text-gray-600 text-center leading-normal max-w-md mx-auto sm:text-xs text-2xs sm:max-w-lg max-w-80\">\n <div className=\"font-semibold mb-1 text-gray-900\">Notes-First Input System:</div>\n <div className=\"mb-0.5\">• Numbers add/remove notes by default</div>\n <div className=\"mb-0.5\">• Hold Shift/Ctrl/Cmd + number to place value</div>\n <div className=\"mb-0.5\">• Double-tap cell to clear all notes</div>\n <div className=\"mb-0.5\">• Ctrl/Cmd + click to select multiple cells</div>\n <div className=\"mb-0\">• Smart notes: placing values removes conflicting notes</div>\n </div>\n )}\n </div>\n\n {/* Dual Keypad - positioned based on layout mode */}\n {responsiveLayout.keypadLayoutMode === 'side-by-side' && (\n <DualKeypad\n inputMode={inputMode}\n onInputModeChange={setInputMode}\n onNotePress={handleKeypadNotePress}\n onValuePress={handleKeypadValuePress}\n onClearNotes={handleKeypadClearNotes}\n onClearValues={handleKeypadClearValues}\n hasCellSelection={selectedCells.length > 0}\n selectedCellCount={selectedCells.length}\n showOverlayToggle={false}\n />\n )}\n\n {/* Stacked keypad for landscape layout */}\n {responsiveLayout.keypadLayoutMode === 'stacked' && (\n <DualKeypad\n inputMode={inputMode}\n onInputModeChange={setInputMode}\n onNotePress={handleKeypadNotePress}\n onValuePress={handleKeypadValuePress}\n onClearNotes={handleKeypadClearNotes}\n onClearValues={handleKeypadClearValues}\n hasCellSelection={selectedCells.length > 0}\n selectedCellCount={selectedCells.length}\n showOverlayToggle={false}\n forceLayoutMode={forceLayoutMode}\n />\n )}\n </div>\n\n {/* Overlay keypad - rendered outside main container */}\n {(responsiveLayout.keypadLayoutMode === 'overlay' ||\n responsiveLayout.keypadLayoutMode === 'hidden') && (\n <DualKeypad\n inputMode={inputMode}\n onInputModeChange={setInputMode}\n onNotePress={handleKeypadNotePress}\n onValuePress={handleKeypadValuePress}\n onClearNotes={handleKeypadClearNotes}\n onClearValues={handleKeypadClearValues}\n hasCellSelection={selectedCells.length > 0}\n selectedCellCount={selectedCells.length}\n showOverlayToggle={true}\n />\n )}\n\n {/* Killer Combinations Explorer - only render for killer sudoku */}\n {initialPuzzleDescription?.type === 'killer-sudoku' && selectedCage && (\n <KillerCombinationsExplorer\n selectedCage={selectedCage}\n puzzleId={/* c8 ignore next 1 - defense in depth */ initialPuzzleDescription.id ?? 'default'}\n isOpen={isCombinationsExplorerOpen}\n onClose={/* c8 ignore next 1 - defense in depth */ () => setIsCombinationsExplorerOpen(false)}\n />\n )}\n </div>\n );\n};\n"]}
@@ -1,100 +0,0 @@
1
- "use strict";
2
- /*
3
- * MIT License
4
- *
5
- * Copyright (c) 2023 Erik Fortune
6
- *
7
- * Permission is hereby granted, free of charge, to any person obtaining a copy
8
- * of this software and associated documentation files (the "Software"), to deal
9
- * in the Software without restriction, including without limitation the rights
10
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
- * copies of the Software, and to permit persons to whom the Software is
12
- * furnished to do so, subject to the following conditions:
13
- *
14
- * The above copyright notice and this permission notice shall be included in all
15
- * copies or substantial portions of the Software.
16
- *
17
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
- * SOFTWARE.
24
- */
25
- Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.ValidationDisplay = void 0;
27
- const tslib_1 = require("tslib");
28
- const react_1 = tslib_1.__importStar(require("react"));
29
- /**
30
- * Component to display validation errors and puzzle status
31
- * @public
32
- */
33
- const ValidationDisplay = ({ errors, isValid, isSolved, className }) => {
34
- // Calculate display classes
35
- const displayClasses = (0, react_1.useMemo)(() => {
36
- const classes = ['w-full max-w-lg mx-auto mt-4 p-3 text-center text-sm'];
37
- if (className)
38
- classes.push(className);
39
- return classes.join(' ');
40
- }, [className]);
41
- // Group errors by type for better display
42
- const errorsByType = (0, react_1.useMemo)(() => {
43
- const grouped = {
44
- 'duplicate-row': [],
45
- 'duplicate-column': [],
46
- 'duplicate-section': [],
47
- 'duplicate-diagonal': [],
48
- 'invalid-value': []
49
- };
50
- errors.forEach((error) => {
51
- if (error.type in grouped) {
52
- grouped[error.type].push(error);
53
- }
54
- });
55
- return grouped;
56
- }, [errors]);
57
- // Get status message
58
- const statusMessage = (0, react_1.useMemo)(() => {
59
- if (isSolved)
60
- return 'Puzzle solved! 🎉';
61
- if (isValid)
62
- return 'Puzzle is valid';
63
- return `${errors.length} validation error${errors.length === 1 ? '' : 's'}`;
64
- }, [isValid, isSolved, errors.length]);
65
- // Calculate status container classes
66
- const statusContainerClasses = (0, react_1.useMemo)(() => {
67
- const classes = ['p-2 mb-2 rounded-lg border'];
68
- if (isSolved)
69
- classes.push('bg-green-50 border-green-500 text-green-800');
70
- else if (isValid)
71
- classes.push('bg-blue-50 border-blue-500 text-blue-800');
72
- else
73
- classes.push('bg-red-50 border-red-500 text-red-800');
74
- return classes.join(' ');
75
- }, [isSolved, isValid, errors.length]);
76
- if (errors.length === 0 && !isSolved) {
77
- return (react_1.default.createElement("div", { className: displayClasses, "data-testid": "validation-display" },
78
- react_1.default.createElement("div", { className: statusContainerClasses },
79
- react_1.default.createElement("div", { className: "font-semibold" }, statusMessage))));
80
- }
81
- return (react_1.default.createElement("div", { className: displayClasses, "data-testid": "validation-display" },
82
- react_1.default.createElement("div", { className: statusContainerClasses },
83
- react_1.default.createElement("div", { className: "font-semibold" }, statusMessage)),
84
- errors.length > 0 && (react_1.default.createElement("div", { className: "mt-3 p-3 bg-gray-50 rounded border border-gray-200 text-left" },
85
- react_1.default.createElement("div", { className: "font-bold mb-2 text-gray-900" }, "Validation Errors:"),
86
- Object.entries(errorsByType).map(([type, typeErrors]) => {
87
- if (typeErrors.length === 0)
88
- return null;
89
- const typeLabel = type.replace('duplicate-', '').replace('-', ' ');
90
- return (react_1.default.createElement("div", { key: type, className: "mb-3 last:mb-0" },
91
- react_1.default.createElement("div", { className: "font-semibold text-gray-700 mb-1 capitalize" },
92
- typeLabel,
93
- " (",
94
- typeErrors.length,
95
- ")"),
96
- react_1.default.createElement("ul", { className: "list-disc list-inside space-y-1 text-xs text-gray-600" }, typeErrors.map((error, index) => (react_1.default.createElement("li", { key: `${error.cellId}-${index}`, className: "leading-relaxed" }, error.message))))));
97
- })))));
98
- };
99
- exports.ValidationDisplay = ValidationDisplay;
100
- //# sourceMappingURL=ValidationDisplay.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ValidationDisplay.js","sourceRoot":"","sources":["../../src/components/ValidationDisplay.tsx"],"names":[],"mappings":"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","sourcesContent":["/*\n * MIT License\n *\n * Copyright (c) 2023 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport React, { useMemo } from 'react';\nimport { IValidationDisplayProps } from '../types';\n\n/**\n * Component to display validation errors and puzzle status\n * @public\n */\nexport const ValidationDisplay: React.FC<IValidationDisplayProps> = ({\n errors,\n isValid,\n isSolved,\n className\n}) => {\n // Calculate display classes\n const displayClasses = useMemo(() => {\n const classes = ['w-full max-w-lg mx-auto mt-4 p-3 text-center text-sm'];\n if (className) classes.push(className);\n return classes.join(' ');\n }, [className]);\n\n // Group errors by type for better display\n const errorsByType = useMemo(() => {\n const grouped = {\n 'duplicate-row': [] as typeof errors,\n 'duplicate-column': [] as typeof errors,\n 'duplicate-section': [] as typeof errors,\n 'duplicate-diagonal': [] as typeof errors,\n 'invalid-value': [] as typeof errors\n };\n\n errors.forEach((error) => {\n if (error.type in grouped) {\n grouped[error.type].push(error);\n }\n });\n\n return grouped;\n }, [errors]);\n\n // Get status message\n const statusMessage = useMemo(() => {\n if (isSolved) return 'Puzzle solved! 🎉';\n if (isValid) return 'Puzzle is valid';\n return `${errors.length} validation error${errors.length === 1 ? '' : 's'}`;\n }, [isValid, isSolved, errors.length]);\n\n // Calculate status container classes\n const statusContainerClasses = useMemo(() => {\n const classes = ['p-2 mb-2 rounded-lg border'];\n if (isSolved) classes.push('bg-green-50 border-green-500 text-green-800');\n else if (isValid) classes.push('bg-blue-50 border-blue-500 text-blue-800');\n else classes.push('bg-red-50 border-red-500 text-red-800');\n return classes.join(' ');\n }, [isSolved, isValid, errors.length]);\n\n if (errors.length === 0 && !isSolved) {\n return (\n <div className={displayClasses} data-testid=\"validation-display\">\n <div className={statusContainerClasses}>\n <div className=\"font-semibold\">{statusMessage}</div>\n </div>\n </div>\n );\n }\n\n return (\n <div className={displayClasses} data-testid=\"validation-display\">\n <div className={statusContainerClasses}>\n <div className=\"font-semibold\">{statusMessage}</div>\n </div>\n\n {errors.length > 0 && (\n <div className=\"mt-3 p-3 bg-gray-50 rounded border border-gray-200 text-left\">\n <div className=\"font-bold mb-2 text-gray-900\">Validation Errors:</div>\n\n {Object.entries(errorsByType).map(([type, typeErrors]) => {\n if (typeErrors.length === 0) return null;\n\n const typeLabel = type.replace('duplicate-', '').replace('-', ' ');\n\n return (\n <div key={type} className=\"mb-3 last:mb-0\">\n <div className=\"font-semibold text-gray-700 mb-1 capitalize\">\n {typeLabel} ({typeErrors.length})\n </div>\n <ul className=\"list-disc list-inside space-y-1 text-xs text-gray-600\">\n {typeErrors.map((error, index) => (\n <li key={`${error.cellId}-${index}`} className=\"leading-relaxed\">\n {error.message}\n </li>\n ))}\n </ul>\n </div>\n );\n })}\n </div>\n )}\n </div>\n );\n};\n"]}