@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.
- package/eslint.config.js +18 -0
- package/lib/tsdoc-metadata.json +1 -1
- package/package.json +22 -21
- package/.test-failures-catalog.md +0 -470
- package/CHANGELOG.md +0 -11
- package/CSS_MODULES_MIGRATION_SUMMARY.md +0 -189
- package/dist/ts-sudoku-ui.d.ts +0 -668
- package/etc/ts-sudoku-ui.api.md +0 -481
- package/lib/test/fixtures/killerSudokuPuzzles.d.ts +0 -36
- package/lib/test/fixtures/killerSudokuPuzzles.d.ts.map +0 -1
- package/lib/test/fixtures/killerSudokuPuzzles.js +0 -57
- package/lib/test/fixtures/killerSudokuPuzzles.js.map +0 -1
- package/lib/test/unit/components/SudokuGridEntry.testHelpers.d.ts +0 -22
- package/lib/test/unit/components/SudokuGridEntry.testHelpers.d.ts.map +0 -1
- package/lib/test/unit/components/SudokuGridEntry.testHelpers.js +0 -116
- package/lib/test/unit/components/SudokuGridEntry.testHelpers.js.map +0 -1
- package/lib-commonjs/components/CageOverlay.js +0 -138
- package/lib-commonjs/components/CageOverlay.js.map +0 -1
- package/lib-commonjs/components/CageSumIndicator.js +0 -71
- package/lib-commonjs/components/CageSumIndicator.js.map +0 -1
- package/lib-commonjs/components/CombinationCard.js +0 -68
- package/lib-commonjs/components/CombinationCard.js.map +0 -1
- package/lib-commonjs/components/CombinationGrid.js +0 -56
- package/lib-commonjs/components/CombinationGrid.js.map +0 -1
- package/lib-commonjs/components/CompactControlRibbon.js +0 -146
- package/lib-commonjs/components/CompactControlRibbon.js.map +0 -1
- package/lib-commonjs/components/DualKeypad.js +0 -142
- package/lib-commonjs/components/DualKeypad.js.map +0 -1
- package/lib-commonjs/components/KillerCombinationsExplorer.js +0 -74
- package/lib-commonjs/components/KillerCombinationsExplorer.js.map +0 -1
- package/lib-commonjs/components/KillerCombinationsModal.js +0 -140
- package/lib-commonjs/components/KillerCombinationsModal.js.map +0 -1
- package/lib-commonjs/components/KillerCombinationsPanel.js +0 -84
- package/lib-commonjs/components/KillerCombinationsPanel.js.map +0 -1
- package/lib-commonjs/components/NumberKeypad.js +0 -186
- package/lib-commonjs/components/NumberKeypad.js.map +0 -1
- package/lib-commonjs/components/SudokuCell.js +0 -245
- package/lib-commonjs/components/SudokuCell.js.map +0 -1
- package/lib-commonjs/components/SudokuControls.js +0 -145
- package/lib-commonjs/components/SudokuControls.js.map +0 -1
- package/lib-commonjs/components/SudokuGrid.js +0 -182
- package/lib-commonjs/components/SudokuGrid.js.map +0 -1
- package/lib-commonjs/components/SudokuGridEntry.js +0 -378
- package/lib-commonjs/components/SudokuGridEntry.js.map +0 -1
- package/lib-commonjs/components/ValidationDisplay.js +0 -100
- package/lib-commonjs/components/ValidationDisplay.js.map +0 -1
- package/lib-commonjs/contexts/DiagnosticLoggerContext.js +0 -104
- package/lib-commonjs/contexts/DiagnosticLoggerContext.js.map +0 -1
- package/lib-commonjs/contexts/index.js +0 -32
- package/lib-commonjs/contexts/index.js.map +0 -1
- package/lib-commonjs/hooks/useCombinationElimination.js +0 -126
- package/lib-commonjs/hooks/useCombinationElimination.js.map +0 -1
- package/lib-commonjs/hooks/useKeyboardShortcut.js +0 -58
- package/lib-commonjs/hooks/useKeyboardShortcut.js.map +0 -1
- package/lib-commonjs/hooks/useKillerCombinations.js +0 -68
- package/lib-commonjs/hooks/useKillerCombinations.js.map +0 -1
- package/lib-commonjs/hooks/useLongPress.js +0 -72
- package/lib-commonjs/hooks/useLongPress.js.map +0 -1
- package/lib-commonjs/hooks/usePuzzleSession.js +0 -401
- package/lib-commonjs/hooks/usePuzzleSession.js.map +0 -1
- package/lib-commonjs/hooks/useResponsiveLayout.js +0 -142
- package/lib-commonjs/hooks/useResponsiveLayout.js.map +0 -1
- package/lib-commonjs/index.browser.js +0 -29
- package/lib-commonjs/index.browser.js.map +0 -1
- package/lib-commonjs/index.js +0 -73
- package/lib-commonjs/index.js.map +0 -1
- package/lib-commonjs/test/fixtures/killerSudokuPuzzles.js +0 -60
- package/lib-commonjs/test/fixtures/killerSudokuPuzzles.js.map +0 -1
- package/lib-commonjs/test/unit/components/SudokuGridEntry.testHelpers.js +0 -125
- package/lib-commonjs/test/unit/components/SudokuGridEntry.testHelpers.js.map +0 -1
- package/lib-commonjs/types/index.js +0 -26
- package/lib-commonjs/types/index.js.map +0 -1
- package/lib-commonjs/utils/CageLookupManager.js +0 -287
- package/lib-commonjs/utils/CageLookupManager.js.map +0 -1
- package/lib-commonjs/utils/CagePatternManager.js +0 -143
- package/lib-commonjs/utils/CagePatternManager.js.map +0 -1
- package/lib-commonjs/utils/puzzleDefinitionHelper.js +0 -103
- 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"]}
|