@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,401 +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.usePuzzleSession = usePuzzleSession;
|
|
27
|
-
const react_1 = require("react");
|
|
28
|
-
const ts_sudoku_lib_1 = require("@fgv/ts-sudoku-lib");
|
|
29
|
-
const puzzleDefinitionHelper_1 = require("../utils/puzzleDefinitionHelper");
|
|
30
|
-
/**
|
|
31
|
-
* Hook for managing puzzle session state and operations
|
|
32
|
-
* @public
|
|
33
|
-
*/
|
|
34
|
-
function usePuzzleSession(initialPuzzleDescription) {
|
|
35
|
-
const [session, setSession] = (0, react_1.useState)(null);
|
|
36
|
-
const [puzzleDescription, setPuzzleDescription] = (0, react_1.useState)(null);
|
|
37
|
-
const [selectedCell, setSelectedCell] = (0, react_1.useState)(null);
|
|
38
|
-
const [selectedCells, setSelectedCells] = (0, react_1.useState)([]);
|
|
39
|
-
const [inputMode, setInputMode] = (0, react_1.useState)('notes'); // Notes-first paradigm
|
|
40
|
-
const [error, setError] = (0, react_1.useState)(null);
|
|
41
|
-
const [updateCounter, setUpdateCounter] = (0, react_1.useState)(0); // Force re-renders
|
|
42
|
-
// Initialize or update puzzle session
|
|
43
|
-
(0, react_1.useEffect)(() => {
|
|
44
|
-
// Use the provided description or create a default empty 9x9 puzzle
|
|
45
|
-
const puzzleDescriptor = initialPuzzleDescription || (0, puzzleDefinitionHelper_1.createEmptyPuzzleDefinition)();
|
|
46
|
-
const sessionResult = ts_sudoku_lib_1.Puzzles.Any.create(puzzleDescriptor).onSuccess((puzzle) => ts_sudoku_lib_1.PuzzleSession.create(puzzle));
|
|
47
|
-
if (sessionResult.isSuccess()) {
|
|
48
|
-
setSession(sessionResult.value);
|
|
49
|
-
setPuzzleDescription(puzzleDescriptor);
|
|
50
|
-
setError(null);
|
|
51
|
-
}
|
|
52
|
-
else {
|
|
53
|
-
setError(`Failed to create puzzle session: ${sessionResult.message}`);
|
|
54
|
-
setSession(null);
|
|
55
|
-
setPuzzleDescription(null);
|
|
56
|
-
}
|
|
57
|
-
}, [initialPuzzleDescription]);
|
|
58
|
-
// Get validation errors
|
|
59
|
-
const validationErrors = (0, react_1.useMemo)(() => {
|
|
60
|
-
if (!session)
|
|
61
|
-
return [];
|
|
62
|
-
const errors = [];
|
|
63
|
-
const invalidCells = session.getInvalidCells();
|
|
64
|
-
invalidCells.forEach((cell) => {
|
|
65
|
-
const contentsResult = session.state.getCellContents(cell.id);
|
|
66
|
-
if (contentsResult.isSuccess() && contentsResult.value.value) {
|
|
67
|
-
const contents = contentsResult.value;
|
|
68
|
-
// Check for duplicates in row, column, and section
|
|
69
|
-
const rowCells = session.rows[cell.row];
|
|
70
|
-
const colCells = session.cols[cell.col];
|
|
71
|
-
// Calculate section index based on puzzle dimensions
|
|
72
|
-
const cageHeight = session.puzzle.dimensions.cageHeightInCells;
|
|
73
|
-
const cageWidth = session.puzzle.dimensions.cageWidthInCells;
|
|
74
|
-
const sectionRow = Math.floor(cell.row / cageHeight);
|
|
75
|
-
const sectionCol = Math.floor(cell.col / cageWidth);
|
|
76
|
-
const sectionsPerRow = Math.ceil(session.numColumns / cageWidth);
|
|
77
|
-
const sectionIndex = sectionRow * sectionsPerRow + sectionCol;
|
|
78
|
-
const sectionCells = session.sections[sectionIndex];
|
|
79
|
-
const checkDuplicates = (cageCellIds, type) => {
|
|
80
|
-
const duplicates = cageCellIds.filter((cellId) => {
|
|
81
|
-
if (cellId === cell.id)
|
|
82
|
-
return false;
|
|
83
|
-
const cellContentsResult = session.state.getCellContents(cellId);
|
|
84
|
-
return cellContentsResult.isSuccess() && cellContentsResult.value.value === contents.value;
|
|
85
|
-
});
|
|
86
|
-
if (duplicates.length > 0) {
|
|
87
|
-
errors.push({
|
|
88
|
-
cellId: cell.id,
|
|
89
|
-
type,
|
|
90
|
-
conflictingCells: duplicates,
|
|
91
|
-
message: `Duplicate ${contents.value} in ${type.replace('duplicate-', '').replace('-', ' ')}`
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
checkDuplicates(rowCells.cellIds, 'duplicate-row');
|
|
96
|
-
checkDuplicates(colCells.cellIds, 'duplicate-column');
|
|
97
|
-
checkDuplicates(sectionCells.cellIds, 'duplicate-section');
|
|
98
|
-
// For Sudoku X puzzles, also check diagonal constraints
|
|
99
|
-
if (puzzleDescription?.type === 'sudoku-x') {
|
|
100
|
-
// Find diagonal cages in the session
|
|
101
|
-
const diagonalCages = session.cages.filter((cage) => cage.cageType === 'x');
|
|
102
|
-
// Check main diagonal (top-left to bottom-right)
|
|
103
|
-
if (cell.row === cell.col && diagonalCages.length > 0) {
|
|
104
|
-
const mainDiagonal = diagonalCages[0];
|
|
105
|
-
checkDuplicates(mainDiagonal.cellIds, 'duplicate-diagonal');
|
|
106
|
-
}
|
|
107
|
-
// Check anti-diagonal (top-right to bottom-left)
|
|
108
|
-
if (cell.row + cell.col === 8 && diagonalCages.length > 1) {
|
|
109
|
-
const antiDiagonal = diagonalCages[1];
|
|
110
|
-
checkDuplicates(antiDiagonal.cellIds, 'duplicate-diagonal');
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
return errors;
|
|
116
|
-
}, [session, puzzleDescription, updateCounter]);
|
|
117
|
-
// Get cell display information
|
|
118
|
-
const cellDisplayInfo = (0, react_1.useMemo)(() => {
|
|
119
|
-
if (!session)
|
|
120
|
-
return [];
|
|
121
|
-
return session.cells.map((cell) => {
|
|
122
|
-
const contentsResult = session.state.getCellContents(cell.id);
|
|
123
|
-
/* c8 ignore next 1 - defense in depth */
|
|
124
|
-
const contents = contentsResult.isSuccess() ? contentsResult.value : { value: undefined, notes: [] };
|
|
125
|
-
const hasValidationError = validationErrors.some((error) => error.cellId === cell.id);
|
|
126
|
-
return {
|
|
127
|
-
id: cell.id,
|
|
128
|
-
row: cell.row,
|
|
129
|
-
column: cell.col,
|
|
130
|
-
contents,
|
|
131
|
-
isImmutable: cell.immutable,
|
|
132
|
-
hasValidationError
|
|
133
|
-
};
|
|
134
|
-
});
|
|
135
|
-
}, [session, validationErrors, updateCounter]);
|
|
136
|
-
// Get cage display information for Killer Sudoku
|
|
137
|
-
const cageDisplayInfo = (0, react_1.useMemo)(() => {
|
|
138
|
-
if (!session || puzzleDescription?.type !== 'killer-sudoku')
|
|
139
|
-
return [];
|
|
140
|
-
return session.cages
|
|
141
|
-
.filter((cage) => cage.cageType === 'killer') // Only show killer cages, not diagonal/section cages
|
|
142
|
-
.map((cage) => {
|
|
143
|
-
// Calculate current sum
|
|
144
|
-
let currentSum = 0;
|
|
145
|
-
let isComplete = true;
|
|
146
|
-
let hasValues = false;
|
|
147
|
-
for (const cellId of cage.cellIds) {
|
|
148
|
-
const contentsResult = session.state.getCellContents(cellId);
|
|
149
|
-
if (contentsResult.isSuccess() && contentsResult.value.value) {
|
|
150
|
-
currentSum += contentsResult.value.value;
|
|
151
|
-
hasValues = true;
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
isComplete = false;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
// Check if the current sum is valid (not exceeding target when incomplete, or matching target when complete)
|
|
158
|
-
const isValid = isComplete ? currentSum === cage.total : currentSum <= cage.total;
|
|
159
|
-
// Check if cage should be highlighted (contains selected cell)
|
|
160
|
-
const isHighlighted = selectedCell ? cage.cellIds.includes(selectedCell) : false;
|
|
161
|
-
return {
|
|
162
|
-
cage,
|
|
163
|
-
isHighlighted,
|
|
164
|
-
currentSum: hasValues ? currentSum : undefined,
|
|
165
|
-
isComplete,
|
|
166
|
-
isValid
|
|
167
|
-
};
|
|
168
|
-
});
|
|
169
|
-
}, [session, puzzleDescription, selectedCell, updateCounter]);
|
|
170
|
-
// Helper function to remove notes that conflict with a placed value
|
|
171
|
-
const removeConflictingNotes = (0, react_1.useCallback)((placedCellId, placedValue) => {
|
|
172
|
-
/* c8 ignore next 1 - defense in depth should not happen */
|
|
173
|
-
if (!session)
|
|
174
|
-
return;
|
|
175
|
-
const placedCell = session.cells.find((cell) => cell.id === placedCellId);
|
|
176
|
-
/* c8 ignore next 1 - defense in depth should not happen */
|
|
177
|
-
if (!placedCell)
|
|
178
|
-
return;
|
|
179
|
-
// Get cells in the same row, column, and section
|
|
180
|
-
const rowCells = session.rows[placedCell.row].cellIds;
|
|
181
|
-
const colCells = session.cols[placedCell.col].cellIds;
|
|
182
|
-
// Calculate section index based on puzzle dimensions
|
|
183
|
-
const cageHeight = session.puzzle.dimensions.cageHeightInCells;
|
|
184
|
-
const cageWidth = session.puzzle.dimensions.cageWidthInCells;
|
|
185
|
-
const sectionRow = Math.floor(placedCell.row / cageHeight);
|
|
186
|
-
const sectionCol = Math.floor(placedCell.col / cageWidth);
|
|
187
|
-
const sectionsPerRow = Math.ceil(session.numColumns / cageWidth);
|
|
188
|
-
const sectionIndex = sectionRow * sectionsPerRow + sectionCol;
|
|
189
|
-
const sectionCells = session.sections[sectionIndex].cellIds;
|
|
190
|
-
// Start with row, column, and section cells
|
|
191
|
-
const affectedCells = new Set([...rowCells, ...colCells, ...sectionCells]);
|
|
192
|
-
// For Sudoku X puzzles, also include diagonal cells
|
|
193
|
-
if (puzzleDescription?.type === 'sudoku-x') {
|
|
194
|
-
// Find diagonal cages in the session
|
|
195
|
-
const diagonalCages = session.cages.filter((cage) => cage.cageType === 'x');
|
|
196
|
-
// Check main diagonal (top-left to bottom-right)
|
|
197
|
-
if (placedCell.row === placedCell.col && diagonalCages.length > 0) {
|
|
198
|
-
const mainDiagonal = diagonalCages[0];
|
|
199
|
-
mainDiagonal.cellIds.forEach((cellId) => affectedCells.add(cellId));
|
|
200
|
-
}
|
|
201
|
-
// Check anti-diagonal (top-right to bottom-left)
|
|
202
|
-
if (placedCell.row + placedCell.col === 8 && diagonalCages.length > 1) {
|
|
203
|
-
const antiDiagonal = diagonalCages[1];
|
|
204
|
-
antiDiagonal.cellIds.forEach((cellId) => affectedCells.add(cellId));
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
// Remove the placed cell itself from the list
|
|
208
|
-
affectedCells.delete(placedCellId);
|
|
209
|
-
// Update notes for each affected cell
|
|
210
|
-
for (const cellId of Array.from(affectedCells)) {
|
|
211
|
-
const contentsResult = session.state.getCellContents(cellId);
|
|
212
|
-
if (contentsResult.isSuccess()) {
|
|
213
|
-
const contents = contentsResult.value;
|
|
214
|
-
// Only update if the cell has notes and contains the placed value
|
|
215
|
-
if (contents.notes.length > 0 && contents.notes.includes(placedValue) && !contents.value) {
|
|
216
|
-
const newNotes = contents.notes.filter((note) => note !== placedValue);
|
|
217
|
-
session.updateCellNotes(cellId, newNotes);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}, [session, puzzleDescription]);
|
|
222
|
-
// Update cell value with smart note removal
|
|
223
|
-
const updateCellValue = (0, react_1.useCallback)((cellId, value) => {
|
|
224
|
-
/* c8 ignore next 1 - defense in depth should not happen */
|
|
225
|
-
if (!session)
|
|
226
|
-
return;
|
|
227
|
-
const result = session.updateCellValue(cellId, value);
|
|
228
|
-
if (result.isSuccess()) {
|
|
229
|
-
// If a value was placed (not removed), remove conflicting notes
|
|
230
|
-
if (value !== undefined) {
|
|
231
|
-
removeConflictingNotes(cellId, value);
|
|
232
|
-
}
|
|
233
|
-
// Force re-render by incrementing update counter
|
|
234
|
-
setUpdateCounter((prev) => prev + 1);
|
|
235
|
-
}
|
|
236
|
-
else {
|
|
237
|
-
setError(`Failed to update cell: ${result.message}`);
|
|
238
|
-
}
|
|
239
|
-
}, [session, removeConflictingNotes]);
|
|
240
|
-
// Toggle a note in a cell
|
|
241
|
-
const toggleCellNote = (0, react_1.useCallback)((cellId, note) => {
|
|
242
|
-
/* c8 ignore next 1 - defense in depth should not happen */
|
|
243
|
-
if (!session)
|
|
244
|
-
return;
|
|
245
|
-
// Get current cell contents
|
|
246
|
-
const contentsResult = session.state.getCellContents(cellId);
|
|
247
|
-
if (contentsResult.isFailure()) {
|
|
248
|
-
setError(`Failed to get cell contents: ${contentsResult.message}`);
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
const currentNotes = contentsResult.value.notes;
|
|
252
|
-
const newNotes = currentNotes.includes(note)
|
|
253
|
-
? currentNotes.filter((n) => n !== note) // Remove note if it exists
|
|
254
|
-
: [...currentNotes, note].sort(); // Add note if it doesn't exist
|
|
255
|
-
const result = session.updateCellNotes(cellId, newNotes);
|
|
256
|
-
if (result.isSuccess()) {
|
|
257
|
-
setUpdateCounter((prev) => prev + 1);
|
|
258
|
-
/* c8 ignore next 3 - defense in depth */
|
|
259
|
-
}
|
|
260
|
-
else {
|
|
261
|
-
setError(`Failed to toggle note: ${result.message}`);
|
|
262
|
-
}
|
|
263
|
-
}, [session]);
|
|
264
|
-
// Clear all notes from a cell
|
|
265
|
-
const clearCellNotes = (0, react_1.useCallback)((cellId) => {
|
|
266
|
-
/* c8 ignore next 1 - defense in depth should not happen */
|
|
267
|
-
if (!session)
|
|
268
|
-
return;
|
|
269
|
-
const result = session.updateCellNotes(cellId, []);
|
|
270
|
-
if (result.isSuccess()) {
|
|
271
|
-
setUpdateCounter((prev) => prev + 1);
|
|
272
|
-
}
|
|
273
|
-
else {
|
|
274
|
-
setError(`Failed to clear notes: ${result.message}`);
|
|
275
|
-
}
|
|
276
|
-
}, [session]);
|
|
277
|
-
// Navigate to adjacent cell
|
|
278
|
-
const navigateToCell = (0, react_1.useCallback)((direction, wrap = 'wrap-around') => {
|
|
279
|
-
if (!session || !selectedCell)
|
|
280
|
-
return;
|
|
281
|
-
const currentCell = session.cells.find((cell) => cell.id === selectedCell);
|
|
282
|
-
/* c8 ignore next 1 - defense in depth should not happen */
|
|
283
|
-
if (!currentCell)
|
|
284
|
-
return;
|
|
285
|
-
let newRow = currentCell.row;
|
|
286
|
-
let newCol = currentCell.col;
|
|
287
|
-
switch (direction) {
|
|
288
|
-
case 'up':
|
|
289
|
-
newRow = newRow > 0 ? newRow - 1 : wrap === 'wrap-around' ? session.numRows - 1 : newRow;
|
|
290
|
-
break;
|
|
291
|
-
case 'down':
|
|
292
|
-
newRow = newRow < session.numRows - 1 ? newRow + 1 : wrap === 'wrap-around' ? 0 : newRow;
|
|
293
|
-
break;
|
|
294
|
-
case 'left':
|
|
295
|
-
newCol = newCol > 0 ? newCol - 1 : wrap === 'wrap-around' ? session.numColumns - 1 : newCol;
|
|
296
|
-
break;
|
|
297
|
-
case 'right':
|
|
298
|
-
newCol = newCol < session.numColumns - 1 ? newCol + 1 : wrap === 'wrap-around' ? 0 : newCol;
|
|
299
|
-
break;
|
|
300
|
-
}
|
|
301
|
-
const targetCell = session.cells.find((cell) => cell.row === newRow && cell.col === newCol);
|
|
302
|
-
if (targetCell) {
|
|
303
|
-
setSelectedCell(targetCell.id);
|
|
304
|
-
}
|
|
305
|
-
}, [session, selectedCell]);
|
|
306
|
-
// Undo last move
|
|
307
|
-
const undo = (0, react_1.useCallback)(() => {
|
|
308
|
-
/* c8 ignore next 1 - defense in depth should not happen */
|
|
309
|
-
if (!session)
|
|
310
|
-
return;
|
|
311
|
-
const result = session.undo();
|
|
312
|
-
if (result.isSuccess()) {
|
|
313
|
-
setUpdateCounter((prev) => prev + 1);
|
|
314
|
-
}
|
|
315
|
-
else {
|
|
316
|
-
setError(`Failed to undo: ${result.message}`);
|
|
317
|
-
}
|
|
318
|
-
}, [session]);
|
|
319
|
-
// Redo last undone move
|
|
320
|
-
const redo = (0, react_1.useCallback)(() => {
|
|
321
|
-
/* c8 ignore next 1 - defense in depth should not happen */
|
|
322
|
-
if (!session)
|
|
323
|
-
return;
|
|
324
|
-
const result = session.redo();
|
|
325
|
-
if (result.isSuccess()) {
|
|
326
|
-
setUpdateCounter((prev) => prev + 1);
|
|
327
|
-
}
|
|
328
|
-
else {
|
|
329
|
-
setError(`Failed to redo: ${result.message}`);
|
|
330
|
-
}
|
|
331
|
-
}, [session]);
|
|
332
|
-
// Reset puzzle to initial state
|
|
333
|
-
const reset = (0, react_1.useCallback)(() => {
|
|
334
|
-
/* c8 ignore next 1 - defense in depth should not happen */
|
|
335
|
-
if (!session)
|
|
336
|
-
return;
|
|
337
|
-
const result = ts_sudoku_lib_1.PuzzleSession.create(session.puzzle);
|
|
338
|
-
if (result.isSuccess()) {
|
|
339
|
-
setSession(result.value);
|
|
340
|
-
setSelectedCell(null);
|
|
341
|
-
setUpdateCounter(0);
|
|
342
|
-
/* c8 ignore next 3 - defense in depth */
|
|
343
|
-
}
|
|
344
|
-
else {
|
|
345
|
-
setError(`Failed to reset: ${result.message}`);
|
|
346
|
-
}
|
|
347
|
-
}, [session]);
|
|
348
|
-
// Export puzzle state
|
|
349
|
-
const exportPuzzle = (0, react_1.useCallback)(() => {
|
|
350
|
-
/* c8 ignore next 1 - defense in depth should not happen */
|
|
351
|
-
if (!session)
|
|
352
|
-
return null;
|
|
353
|
-
const cellsArray = Array(session.numRows * session.numColumns).fill('.');
|
|
354
|
-
session.cells.forEach((cell) => {
|
|
355
|
-
const contentsResult = session.state.getCellContents(cell.id);
|
|
356
|
-
const index = cell.row * session.numColumns + cell.col;
|
|
357
|
-
if (contentsResult.isSuccess() && contentsResult.value.value) {
|
|
358
|
-
cellsArray[index] = contentsResult.value.value.toString();
|
|
359
|
-
}
|
|
360
|
-
});
|
|
361
|
-
/* c8 ignore next 4 - defense in depth */
|
|
362
|
-
const id = session.id && session.id !== '' ? session.id : 'exported-puzzle';
|
|
363
|
-
const description = session.description && session.description !== '' ? session.description : 'Exported Puzzle';
|
|
364
|
-
const type = puzzleDescription?.type ?? 'sudoku';
|
|
365
|
-
return (0, puzzleDefinitionHelper_1.createPuzzleDefinition)({
|
|
366
|
-
id,
|
|
367
|
-
description,
|
|
368
|
-
type,
|
|
369
|
-
level: 1,
|
|
370
|
-
totalRows: session.numRows,
|
|
371
|
-
totalColumns: session.numColumns,
|
|
372
|
-
cells: cellsArray.join('')
|
|
373
|
-
});
|
|
374
|
-
}, [session, puzzleDescription, updateCounter]);
|
|
375
|
-
return {
|
|
376
|
-
session,
|
|
377
|
-
selectedCell,
|
|
378
|
-
selectedCells,
|
|
379
|
-
inputMode,
|
|
380
|
-
setSelectedCell,
|
|
381
|
-
setSelectedCells,
|
|
382
|
-
setInputMode,
|
|
383
|
-
cellDisplayInfo,
|
|
384
|
-
cageDisplayInfo,
|
|
385
|
-
validationErrors,
|
|
386
|
-
isValid: session?.checkIsValid() ?? false,
|
|
387
|
-
isSolved: session?.checkIsSolved() ?? false,
|
|
388
|
-
canUndo: session?.canUndo ?? false,
|
|
389
|
-
canRedo: session?.canRedo ?? false,
|
|
390
|
-
error,
|
|
391
|
-
updateCellValue,
|
|
392
|
-
toggleCellNote,
|
|
393
|
-
clearCellNotes,
|
|
394
|
-
navigateToCell,
|
|
395
|
-
undo,
|
|
396
|
-
redo,
|
|
397
|
-
reset,
|
|
398
|
-
exportPuzzle
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
//# sourceMappingURL=usePuzzleSession.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"usePuzzleSession.js","sourceRoot":"","sources":["../../src/hooks/usePuzzleSession.ts"],"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 { useCallback, useEffect, useMemo, useState } from 'react';\nimport {\n PuzzleSession,\n CellId,\n NavigationDirection,\n NavigationWrap,\n Puzzles,\n IPuzzleDefinition\n} from '@fgv/ts-sudoku-lib';\nimport { IValidationError, ICellDisplayInfo, InputMode, ICageDisplayInfo } from '../types';\nimport { createEmptyPuzzleDefinition, createPuzzleDefinition } from '../utils/puzzleDefinitionHelper';\n\n/**\n * Hook for managing puzzle session state and operations\n * @public\n */\nexport function usePuzzleSession(initialPuzzleDescription?: IPuzzleDefinition): {\n session: PuzzleSession | null;\n selectedCell: CellId | null;\n selectedCells: CellId[];\n inputMode: InputMode;\n setSelectedCell: (cell: CellId | null) => void;\n setSelectedCells: (cells: CellId[]) => void;\n setInputMode: (mode: InputMode) => void;\n cellDisplayInfo: ICellDisplayInfo[];\n cageDisplayInfo: ICageDisplayInfo[];\n validationErrors: IValidationError[];\n isValid: boolean;\n isSolved: boolean;\n canUndo: boolean;\n canRedo: boolean;\n error: string | null;\n updateCellValue: (cellId: CellId, value: number | undefined) => void;\n toggleCellNote: (cellId: CellId, note: number) => void;\n clearCellNotes: (cellId: CellId) => void;\n navigateToCell: (direction: NavigationDirection, wrap?: NavigationWrap) => void;\n undo: () => void;\n redo: () => void;\n reset: () => void;\n exportPuzzle: () => IPuzzleDefinition | null;\n} {\n const [session, setSession] = useState<PuzzleSession | null>(null);\n const [puzzleDescription, setPuzzleDescription] = useState<IPuzzleDefinition | null>(null);\n const [selectedCell, setSelectedCell] = useState<CellId | null>(null);\n const [selectedCells, setSelectedCells] = useState<CellId[]>([]);\n const [inputMode, setInputMode] = useState<InputMode>('notes'); // Notes-first paradigm\n const [error, setError] = useState<string | null>(null);\n const [updateCounter, setUpdateCounter] = useState(0); // Force re-renders\n\n // Initialize or update puzzle session\n useEffect(() => {\n // Use the provided description or create a default empty 9x9 puzzle\n const puzzleDescriptor: IPuzzleDefinition = initialPuzzleDescription || createEmptyPuzzleDefinition();\n\n const sessionResult = Puzzles.Any.create(puzzleDescriptor).onSuccess((puzzle) =>\n PuzzleSession.create(puzzle)\n );\n\n if (sessionResult.isSuccess()) {\n setSession(sessionResult.value);\n setPuzzleDescription(puzzleDescriptor);\n setError(null);\n } else {\n setError(`Failed to create puzzle session: ${sessionResult.message}`);\n setSession(null);\n setPuzzleDescription(null);\n }\n }, [initialPuzzleDescription]);\n\n // Get validation errors\n const validationErrors = useMemo((): IValidationError[] => {\n if (!session) return [];\n\n const errors: IValidationError[] = [];\n const invalidCells = session.getInvalidCells();\n\n invalidCells.forEach((cell) => {\n const contentsResult = session.state.getCellContents(cell.id);\n if (contentsResult.isSuccess() && contentsResult.value.value) {\n const contents = contentsResult.value;\n\n // Check for duplicates in row, column, and section\n const rowCells = session.rows[cell.row];\n const colCells = session.cols[cell.col];\n\n // Calculate section index based on puzzle dimensions\n const cageHeight = session.puzzle.dimensions.cageHeightInCells;\n const cageWidth = session.puzzle.dimensions.cageWidthInCells;\n const sectionRow = Math.floor(cell.row / cageHeight);\n const sectionCol = Math.floor(cell.col / cageWidth);\n const sectionsPerRow = Math.ceil(session.numColumns / cageWidth);\n const sectionIndex = sectionRow * sectionsPerRow + sectionCol;\n const sectionCells = session.sections[sectionIndex];\n\n const checkDuplicates = (cageCellIds: CellId[], type: IValidationError['type']): void => {\n const duplicates = cageCellIds.filter((cellId) => {\n if (cellId === cell.id) return false;\n const cellContentsResult = session.state.getCellContents(cellId);\n return cellContentsResult.isSuccess() && cellContentsResult.value.value === contents.value;\n });\n\n if (duplicates.length > 0) {\n errors.push({\n cellId: cell.id,\n type,\n conflictingCells: duplicates,\n message: `Duplicate ${contents.value} in ${type.replace('duplicate-', '').replace('-', ' ')}`\n });\n }\n };\n\n checkDuplicates(rowCells.cellIds, 'duplicate-row');\n checkDuplicates(colCells.cellIds, 'duplicate-column');\n checkDuplicates(sectionCells.cellIds, 'duplicate-section');\n\n // For Sudoku X puzzles, also check diagonal constraints\n if (puzzleDescription?.type === 'sudoku-x') {\n // Find diagonal cages in the session\n const diagonalCages = session.cages.filter((cage) => cage.cageType === 'x');\n\n // Check main diagonal (top-left to bottom-right)\n if (cell.row === cell.col && diagonalCages.length > 0) {\n const mainDiagonal = diagonalCages[0];\n checkDuplicates(mainDiagonal.cellIds, 'duplicate-diagonal');\n }\n\n // Check anti-diagonal (top-right to bottom-left)\n if (cell.row + cell.col === 8 && diagonalCages.length > 1) {\n const antiDiagonal = diagonalCages[1];\n checkDuplicates(antiDiagonal.cellIds, 'duplicate-diagonal');\n }\n }\n }\n });\n\n return errors;\n }, [session, puzzleDescription, updateCounter]);\n\n // Get cell display information\n const cellDisplayInfo = useMemo((): ICellDisplayInfo[] => {\n if (!session) return [];\n\n return session.cells.map((cell) => {\n const contentsResult = session.state.getCellContents(cell.id);\n /* c8 ignore next 1 - defense in depth */\n const contents = contentsResult.isSuccess() ? contentsResult.value : { value: undefined, notes: [] };\n const hasValidationError = validationErrors.some((error) => error.cellId === cell.id);\n\n return {\n id: cell.id,\n row: cell.row,\n column: cell.col,\n contents,\n isImmutable: cell.immutable,\n hasValidationError\n };\n });\n }, [session, validationErrors, updateCounter]);\n\n // Get cage display information for Killer Sudoku\n const cageDisplayInfo = useMemo((): ICageDisplayInfo[] => {\n if (!session || puzzleDescription?.type !== 'killer-sudoku') return [];\n\n return session.cages\n .filter((cage) => cage.cageType === 'killer') // Only show killer cages, not diagonal/section cages\n .map((cage) => {\n // Calculate current sum\n let currentSum = 0;\n let isComplete = true;\n let hasValues = false;\n\n for (const cellId of cage.cellIds) {\n const contentsResult = session.state.getCellContents(cellId);\n if (contentsResult.isSuccess() && contentsResult.value.value) {\n currentSum += contentsResult.value.value;\n hasValues = true;\n } else {\n isComplete = false;\n }\n }\n\n // Check if the current sum is valid (not exceeding target when incomplete, or matching target when complete)\n const isValid = isComplete ? currentSum === cage.total : currentSum <= cage.total;\n\n // Check if cage should be highlighted (contains selected cell)\n const isHighlighted = selectedCell ? cage.cellIds.includes(selectedCell) : false;\n\n return {\n cage,\n isHighlighted,\n currentSum: hasValues ? currentSum : undefined,\n isComplete,\n isValid\n };\n });\n }, [session, puzzleDescription, selectedCell, updateCounter]);\n\n // Helper function to remove notes that conflict with a placed value\n const removeConflictingNotes = useCallback(\n (placedCellId: CellId, placedValue: number) => {\n /* c8 ignore next 1 - defense in depth should not happen */\n if (!session) return;\n\n const placedCell = session.cells.find((cell) => cell.id === placedCellId);\n /* c8 ignore next 1 - defense in depth should not happen */\n if (!placedCell) return;\n\n // Get cells in the same row, column, and section\n const rowCells = session.rows[placedCell.row].cellIds;\n const colCells = session.cols[placedCell.col].cellIds;\n\n // Calculate section index based on puzzle dimensions\n const cageHeight = session.puzzle.dimensions.cageHeightInCells;\n const cageWidth = session.puzzle.dimensions.cageWidthInCells;\n const sectionRow = Math.floor(placedCell.row / cageHeight);\n const sectionCol = Math.floor(placedCell.col / cageWidth);\n const sectionsPerRow = Math.ceil(session.numColumns / cageWidth);\n const sectionIndex = sectionRow * sectionsPerRow + sectionCol;\n const sectionCells = session.sections[sectionIndex].cellIds;\n\n // Start with row, column, and section cells\n const affectedCells = new Set([...rowCells, ...colCells, ...sectionCells]);\n\n // For Sudoku X puzzles, also include diagonal cells\n if (puzzleDescription?.type === 'sudoku-x') {\n // Find diagonal cages in the session\n const diagonalCages = session.cages.filter((cage) => cage.cageType === 'x');\n\n // Check main diagonal (top-left to bottom-right)\n if (placedCell.row === placedCell.col && diagonalCages.length > 0) {\n const mainDiagonal = diagonalCages[0];\n mainDiagonal.cellIds.forEach((cellId) => affectedCells.add(cellId));\n }\n\n // Check anti-diagonal (top-right to bottom-left)\n if (placedCell.row + placedCell.col === 8 && diagonalCages.length > 1) {\n const antiDiagonal = diagonalCages[1];\n antiDiagonal.cellIds.forEach((cellId) => affectedCells.add(cellId));\n }\n }\n\n // Remove the placed cell itself from the list\n affectedCells.delete(placedCellId);\n\n // Update notes for each affected cell\n for (const cellId of Array.from(affectedCells)) {\n const contentsResult = session.state.getCellContents(cellId);\n if (contentsResult.isSuccess()) {\n const contents = contentsResult.value;\n\n // Only update if the cell has notes and contains the placed value\n if (contents.notes.length > 0 && contents.notes.includes(placedValue) && !contents.value) {\n const newNotes = contents.notes.filter((note) => note !== placedValue);\n session.updateCellNotes(cellId, newNotes);\n }\n }\n }\n },\n [session, puzzleDescription]\n );\n\n // Update cell value with smart note removal\n const updateCellValue = useCallback(\n (cellId: CellId, value: number | undefined) => {\n /* c8 ignore next 1 - defense in depth should not happen */\n if (!session) return;\n\n const result = session.updateCellValue(cellId, value);\n if (result.isSuccess()) {\n // If a value was placed (not removed), remove conflicting notes\n if (value !== undefined) {\n removeConflictingNotes(cellId, value);\n }\n\n // Force re-render by incrementing update counter\n setUpdateCounter((prev) => prev + 1);\n } else {\n setError(`Failed to update cell: ${result.message}`);\n }\n },\n [session, removeConflictingNotes]\n );\n\n // Toggle a note in a cell\n const toggleCellNote = useCallback(\n (cellId: CellId, note: number) => {\n /* c8 ignore next 1 - defense in depth should not happen */\n if (!session) return;\n\n // Get current cell contents\n const contentsResult = session.state.getCellContents(cellId);\n if (contentsResult.isFailure()) {\n setError(`Failed to get cell contents: ${contentsResult.message}`);\n return;\n }\n\n const currentNotes = contentsResult.value.notes;\n const newNotes = currentNotes.includes(note)\n ? currentNotes.filter((n) => n !== note) // Remove note if it exists\n : [...currentNotes, note].sort(); // Add note if it doesn't exist\n\n const result = session.updateCellNotes(cellId, newNotes);\n if (result.isSuccess()) {\n setUpdateCounter((prev) => prev + 1);\n /* c8 ignore next 3 - defense in depth */\n } else {\n setError(`Failed to toggle note: ${result.message}`);\n }\n },\n [session]\n );\n\n // Clear all notes from a cell\n const clearCellNotes = useCallback(\n (cellId: CellId) => {\n /* c8 ignore next 1 - defense in depth should not happen */\n if (!session) return;\n\n const result = session.updateCellNotes(cellId, []);\n if (result.isSuccess()) {\n setUpdateCounter((prev) => prev + 1);\n } else {\n setError(`Failed to clear notes: ${result.message}`);\n }\n },\n [session]\n );\n\n // Navigate to adjacent cell\n const navigateToCell = useCallback(\n (direction: NavigationDirection, wrap: NavigationWrap = 'wrap-around') => {\n if (!session || !selectedCell) return;\n\n const currentCell = session.cells.find((cell) => cell.id === selectedCell);\n /* c8 ignore next 1 - defense in depth should not happen */\n if (!currentCell) return;\n\n let newRow = currentCell.row;\n let newCol = currentCell.col;\n\n switch (direction) {\n case 'up':\n newRow = newRow > 0 ? newRow - 1 : wrap === 'wrap-around' ? session.numRows - 1 : newRow;\n break;\n case 'down':\n newRow = newRow < session.numRows - 1 ? newRow + 1 : wrap === 'wrap-around' ? 0 : newRow;\n break;\n case 'left':\n newCol = newCol > 0 ? newCol - 1 : wrap === 'wrap-around' ? session.numColumns - 1 : newCol;\n break;\n case 'right':\n newCol = newCol < session.numColumns - 1 ? newCol + 1 : wrap === 'wrap-around' ? 0 : newCol;\n break;\n }\n\n const targetCell = session.cells.find((cell) => cell.row === newRow && cell.col === newCol);\n if (targetCell) {\n setSelectedCell(targetCell.id);\n }\n },\n [session, selectedCell]\n );\n\n // Undo last move\n const undo = useCallback(() => {\n /* c8 ignore next 1 - defense in depth should not happen */\n if (!session) return;\n\n const result = session.undo();\n if (result.isSuccess()) {\n setUpdateCounter((prev) => prev + 1);\n } else {\n setError(`Failed to undo: ${result.message}`);\n }\n }, [session]);\n\n // Redo last undone move\n const redo = useCallback(() => {\n /* c8 ignore next 1 - defense in depth should not happen */\n if (!session) return;\n\n const result = session.redo();\n if (result.isSuccess()) {\n setUpdateCounter((prev) => prev + 1);\n } else {\n setError(`Failed to redo: ${result.message}`);\n }\n }, [session]);\n\n // Reset puzzle to initial state\n const reset = useCallback(() => {\n /* c8 ignore next 1 - defense in depth should not happen */\n if (!session) return;\n\n const result = PuzzleSession.create(session.puzzle);\n if (result.isSuccess()) {\n setSession(result.value);\n setSelectedCell(null);\n setUpdateCounter(0);\n /* c8 ignore next 3 - defense in depth */\n } else {\n setError(`Failed to reset: ${result.message}`);\n }\n }, [session]);\n\n // Export puzzle state\n const exportPuzzle = useCallback((): IPuzzleDefinition | null => {\n /* c8 ignore next 1 - defense in depth should not happen */\n if (!session) return null;\n\n const cellsArray = Array(session.numRows * session.numColumns).fill('.');\n\n session.cells.forEach((cell) => {\n const contentsResult = session.state.getCellContents(cell.id);\n const index = cell.row * session.numColumns + cell.col;\n if (contentsResult.isSuccess() && contentsResult.value.value) {\n cellsArray[index] = contentsResult.value.value.toString();\n }\n });\n\n /* c8 ignore next 4 - defense in depth */\n const id = session.id && session.id !== '' ? session.id : 'exported-puzzle';\n const description =\n session.description && session.description !== '' ? session.description : 'Exported Puzzle';\n const type = puzzleDescription?.type ?? 'sudoku';\n\n return createPuzzleDefinition({\n id,\n description,\n type,\n level: 1,\n totalRows: session.numRows,\n totalColumns: session.numColumns,\n cells: cellsArray.join('')\n });\n }, [session, puzzleDescription, updateCounter]);\n\n return {\n session,\n selectedCell,\n selectedCells,\n inputMode,\n setSelectedCell,\n setSelectedCells,\n setInputMode,\n cellDisplayInfo,\n cageDisplayInfo,\n validationErrors,\n isValid: session?.checkIsValid() ?? false,\n isSolved: session?.checkIsSolved() ?? false,\n canUndo: session?.canUndo ?? false,\n canRedo: session?.canRedo ?? false,\n error,\n updateCellValue,\n toggleCellNote,\n clearCellNotes,\n navigateToCell,\n undo,\n redo,\n reset,\n exportPuzzle\n };\n}\n"]}
|
|
@@ -1,142 +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.useResponsiveLayout = useResponsiveLayout;
|
|
27
|
-
const react_1 = require("react");
|
|
28
|
-
/**
|
|
29
|
-
* Breakpoints for responsive design
|
|
30
|
-
*/
|
|
31
|
-
const BREAKPOINTS = {
|
|
32
|
-
mobile: 768,
|
|
33
|
-
tablet: 1024,
|
|
34
|
-
smallHeight: 600
|
|
35
|
-
};
|
|
36
|
-
/**
|
|
37
|
-
* Detect if device supports touch
|
|
38
|
-
*/
|
|
39
|
-
function detectTouchSupport() {
|
|
40
|
-
return ('ontouchstart' in window ||
|
|
41
|
-
navigator.maxTouchPoints > 0 ||
|
|
42
|
-
(navigator.msMaxTouchPoints ?? 0) > 0);
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Determine device type based on screen size and touch capability
|
|
46
|
-
*/
|
|
47
|
-
function determineDeviceType(width, height, isTouch) {
|
|
48
|
-
if (width <= BREAKPOINTS.mobile) {
|
|
49
|
-
return 'mobile';
|
|
50
|
-
}
|
|
51
|
-
if (width <= BREAKPOINTS.tablet && isTouch) {
|
|
52
|
-
return 'tablet';
|
|
53
|
-
}
|
|
54
|
-
return 'desktop';
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Determine optimal keypad layout mode
|
|
58
|
-
*/
|
|
59
|
-
function determineKeypadLayoutMode(deviceType, orientation, width, height, isTouch) {
|
|
60
|
-
// Desktop: show overlay on demand or hidden by default
|
|
61
|
-
if (deviceType === 'desktop') {
|
|
62
|
-
return isTouch ? 'overlay' : 'hidden';
|
|
63
|
-
}
|
|
64
|
-
// Mobile portrait: side-by-side if width allows
|
|
65
|
-
if (deviceType === 'mobile' && orientation === 'portrait') {
|
|
66
|
-
return width >= 360 ? 'side-by-side' : 'stacked';
|
|
67
|
-
}
|
|
68
|
-
// Mobile landscape: stacked beside grid if height allows
|
|
69
|
-
if (deviceType === 'mobile' && orientation === 'landscape') {
|
|
70
|
-
return height >= BREAKPOINTS.smallHeight ? 'stacked' : 'overlay';
|
|
71
|
-
}
|
|
72
|
-
// Tablet: side-by-side in portrait, stacked in landscape
|
|
73
|
-
if (deviceType === 'tablet') {
|
|
74
|
-
return orientation === 'portrait' ? 'side-by-side' : 'stacked';
|
|
75
|
-
}
|
|
76
|
-
/* c8 ignore next 2 - defense in depth: all device types are explicitly handled above */
|
|
77
|
-
return 'overlay';
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Hook to detect responsive layout information for optimal keypad display
|
|
81
|
-
* @param forceLayoutMode - Optional layout mode override for testing
|
|
82
|
-
* @public
|
|
83
|
-
*/
|
|
84
|
-
function useResponsiveLayout(forceLayoutMode) {
|
|
85
|
-
const [layoutInfo, setLayoutInfo] = (0, react_1.useState)(() => {
|
|
86
|
-
const width = window.innerWidth;
|
|
87
|
-
const height = window.innerHeight;
|
|
88
|
-
const isTouchDevice = detectTouchSupport();
|
|
89
|
-
const orientation = width > height ? 'landscape' : 'portrait';
|
|
90
|
-
const deviceType = determineDeviceType(width, height, isTouchDevice);
|
|
91
|
-
const keypadLayoutMode = forceLayoutMode ?? determineKeypadLayoutMode(deviceType, orientation, width, height, isTouchDevice);
|
|
92
|
-
return {
|
|
93
|
-
deviceType,
|
|
94
|
-
orientation,
|
|
95
|
-
keypadLayoutMode,
|
|
96
|
-
screenWidth: width,
|
|
97
|
-
screenHeight: height,
|
|
98
|
-
isTouchDevice,
|
|
99
|
-
isSmallScreen: width <= BREAKPOINTS.mobile,
|
|
100
|
-
hasSpaceForDualKeypads: width >= 480 || (orientation === 'landscape' && height >= BREAKPOINTS.smallHeight)
|
|
101
|
-
};
|
|
102
|
-
});
|
|
103
|
-
(0, react_1.useEffect)(() => {
|
|
104
|
-
// Don't set up listeners if layout mode is forced
|
|
105
|
-
if (forceLayoutMode) {
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
function updateLayoutInfo() {
|
|
109
|
-
const width = window.innerWidth;
|
|
110
|
-
const height = window.innerHeight;
|
|
111
|
-
const isTouchDevice = detectTouchSupport();
|
|
112
|
-
const orientation = width > height ? 'landscape' : 'portrait';
|
|
113
|
-
const deviceType = determineDeviceType(width, height, isTouchDevice);
|
|
114
|
-
const keypadLayoutMode = determineKeypadLayoutMode(deviceType, orientation, width, height, isTouchDevice);
|
|
115
|
-
setLayoutInfo({
|
|
116
|
-
deviceType,
|
|
117
|
-
orientation,
|
|
118
|
-
keypadLayoutMode,
|
|
119
|
-
screenWidth: width,
|
|
120
|
-
screenHeight: height,
|
|
121
|
-
isTouchDevice,
|
|
122
|
-
isSmallScreen: width <= BREAKPOINTS.mobile,
|
|
123
|
-
hasSpaceForDualKeypads: width >= 480 || (orientation === 'landscape' && height >= BREAKPOINTS.smallHeight)
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
// Update on resize
|
|
127
|
-
window.addEventListener('resize', updateLayoutInfo);
|
|
128
|
-
// Update on orientation change
|
|
129
|
-
window.addEventListener('orientationchange', () => {
|
|
130
|
-
// Small delay to ensure screen dimensions are updated
|
|
131
|
-
setTimeout(updateLayoutInfo, 100);
|
|
132
|
-
});
|
|
133
|
-
// Initial update
|
|
134
|
-
updateLayoutInfo();
|
|
135
|
-
return () => {
|
|
136
|
-
window.removeEventListener('resize', updateLayoutInfo);
|
|
137
|
-
window.removeEventListener('orientationchange', updateLayoutInfo);
|
|
138
|
-
};
|
|
139
|
-
}, [forceLayoutMode]);
|
|
140
|
-
return layoutInfo;
|
|
141
|
-
}
|
|
142
|
-
//# sourceMappingURL=useResponsiveLayout.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"useResponsiveLayout.js","sourceRoot":"","sources":["../../src/hooks/useResponsiveLayout.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;;AA6HH,kDA2EC;AAtMD,iCAA4C;AAkD5C;;GAEG;AACH,MAAM,WAAW,GAAG;IAClB,MAAM,EAAE,GAAG;IACX,MAAM,EAAE,IAAI;IACZ,WAAW,EAAE,GAAG;CACR,CAAC;AAEX;;GAEG;AACH,SAAS,kBAAkB;IACzB,OAAO,CACL,cAAc,IAAI,MAAM;QACxB,SAAS,CAAC,cAAc,GAAG,CAAC;QAC5B,CAAE,SAAsD,CAAC,gBAAgB,IAAI,CAAC,CAAC,GAAG,CAAC,CACpF,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,SAAS,mBAAmB,CAAC,KAAa,EAAE,MAAc,EAAE,OAAgB;IAC1E,IAAI,KAAK,IAAI,WAAW,CAAC,MAAM,EAAE,CAAC;QAChC,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,IAAI,KAAK,IAAI,WAAW,CAAC,MAAM,IAAI,OAAO,EAAE,CAAC;QAC3C,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;GAEG;AACH,SAAS,yBAAyB,CAChC,UAAsB,EACtB,WAA8B,EAC9B,KAAa,EACb,MAAc,EACd,OAAgB;IAEhB,uDAAuD;IACvD,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QAC7B,OAAO,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC;IACxC,CAAC;IAED,gDAAgD;IAChD,IAAI,UAAU,KAAK,QAAQ,IAAI,WAAW,KAAK,UAAU,EAAE,CAAC;QAC1D,OAAO,KAAK,IAAI,GAAG,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,SAAS,CAAC;IACnD,CAAC;IAED,yDAAyD;IACzD,IAAI,UAAU,KAAK,QAAQ,IAAI,WAAW,KAAK,WAAW,EAAE,CAAC;QAC3D,OAAO,MAAM,IAAI,WAAW,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;IACnE,CAAC;IAED,yDAAyD;IACzD,IAAI,UAAU,KAAK,QAAQ,EAAE,CAAC;QAC5B,OAAO,WAAW,KAAK,UAAU,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,SAAS,CAAC;IACjE,CAAC;IACD,wFAAwF;IACxF,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;GAIG;AACH,SAAgB,mBAAmB,CAAC,eAAkC;IACpE,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,GAAG,IAAA,gBAAQ,EAAwB,GAAG,EAAE;QACvE,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC;QAChC,MAAM,MAAM,GAAG,MAAM,CAAC,WAAW,CAAC;QAClC,MAAM,aAAa,GAAG,kBAAkB,EAAE,CAAC;QAC3C,MAAM,WAAW,GAAsB,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,UAAU,CAAC;QACjF,MAAM,UAAU,GAAG,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,CAAC,CAAC;QACrE,MAAM,gBAAgB,GACpB,eAAe,IAAI,yBAAyB,CAAC,UAAU,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,aAAa,CAAC,CAAC;QAEtG,OAAO;YACL,UAAU;YACV,WAAW;YACX,gBAAgB;YAChB,WAAW,EAAE,KAAK;YAClB,YAAY,EAAE,MAAM;YACpB,aAAa;YACb,aAAa,EAAE,KAAK,IAAI,WAAW,CAAC,MAAM;YAC1C,sBAAsB,EACpB,KAAK,IAAI,GAAG,IAAI,CAAC,WAAW,KAAK,WAAW,IAAI,MAAM,IAAI,WAAW,CAAC,WAAW,CAAC;SACrF,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,IAAA,iBAAS,EAAC,GAAG,EAAE;QACb,kDAAkD;QAClD,IAAI,eAAe,EAAE,CAAC;YACpB,OAAO;QACT,CAAC;QAED,SAAS,gBAAgB;YACvB,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC;YAChC,MAAM,MAAM,GAAG,MAAM,CAAC,WAAW,CAAC;YAClC,MAAM,aAAa,GAAG,kBAAkB,EAAE,CAAC;YAC3C,MAAM,WAAW,GAAsB,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,UAAU,CAAC;YACjF,MAAM,UAAU,GAAG,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,CAAC,CAAC;YACrE,MAAM,gBAAgB,GAAG,yBAAyB,CAChD,UAAU,EACV,WAAW,EACX,KAAK,EACL,MAAM,EACN,aAAa,CACd,CAAC;YAEF,aAAa,CAAC;gBACZ,UAAU;gBACV,WAAW;gBACX,gBAAgB;gBAChB,WAAW,EAAE,KAAK;gBAClB,YAAY,EAAE,MAAM;gBACpB,aAAa;gBACb,aAAa,EAAE,KAAK,IAAI,WAAW,CAAC,MAAM;gBAC1C,sBAAsB,EACpB,KAAK,IAAI,GAAG,IAAI,CAAC,WAAW,KAAK,WAAW,IAAI,MAAM,IAAI,WAAW,CAAC,WAAW,CAAC;aACrF,CAAC,CAAC;QACL,CAAC;QAED,mBAAmB;QACnB,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC;QAEpD,+BAA+B;QAC/B,MAAM,CAAC,gBAAgB,CAAC,mBAAmB,EAAE,GAAG,EAAE;YAChD,sDAAsD;YACtD,UAAU,CAAC,gBAAgB,EAAE,GAAG,CAAC,CAAC;QACpC,CAAC,CAAC,CAAC;QAEH,iBAAiB;QACjB,gBAAgB,EAAE,CAAC;QAEnB,OAAO,GAAG,EAAE;YACV,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC;YACvD,MAAM,CAAC,mBAAmB,CAAC,mBAAmB,EAAE,gBAAgB,CAAC,CAAC;QACpE,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,eAAe,CAAC,CAAC,CAAC;IAEtB,OAAO,UAAU,CAAC;AACpB,CAAC","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 { useState, useEffect } from 'react';\n\n/**\n * Device type detection result\n * @public\n */\nexport type DeviceType = 'mobile' | 'tablet' | 'desktop';\n\n/**\n * Screen orientation\n * @public\n */\nexport type ScreenOrientation = 'portrait' | 'landscape';\n\n/**\n * Layout mode for dual keypad\n * @public\n */\nexport type KeypadLayoutMode = 'side-by-side' | 'stacked' | 'overlay' | 'hidden';\n\n/**\n * Responsive layout information\n * @public\n */\nexport interface IResponsiveLayoutInfo {\n /** Device type based on screen size and touch capability */\n readonly deviceType: DeviceType;\n\n /** Current screen orientation */\n readonly orientation: ScreenOrientation;\n\n /** Recommended keypad layout mode */\n readonly keypadLayoutMode: KeypadLayoutMode;\n\n /** Screen width in pixels */\n readonly screenWidth: number;\n\n /** Screen height in pixels */\n readonly screenHeight: number;\n\n /** Whether touch is supported */\n readonly isTouchDevice: boolean;\n\n /** Whether the screen is considered small */\n readonly isSmallScreen: boolean;\n\n /** Whether the screen has enough space for dual keypads */\n readonly hasSpaceForDualKeypads: boolean;\n}\n\n/**\n * Breakpoints for responsive design\n */\nconst BREAKPOINTS = {\n mobile: 768,\n tablet: 1024,\n smallHeight: 600\n} as const;\n\n/**\n * Detect if device supports touch\n */\nfunction detectTouchSupport(): boolean {\n return (\n 'ontouchstart' in window ||\n navigator.maxTouchPoints > 0 ||\n ((navigator as unknown as { msMaxTouchPoints?: number }).msMaxTouchPoints ?? 0) > 0\n );\n}\n\n/**\n * Determine device type based on screen size and touch capability\n */\nfunction determineDeviceType(width: number, height: number, isTouch: boolean): DeviceType {\n if (width <= BREAKPOINTS.mobile) {\n return 'mobile';\n }\n\n if (width <= BREAKPOINTS.tablet && isTouch) {\n return 'tablet';\n }\n\n return 'desktop';\n}\n\n/**\n * Determine optimal keypad layout mode\n */\nfunction determineKeypadLayoutMode(\n deviceType: DeviceType,\n orientation: ScreenOrientation,\n width: number,\n height: number,\n isTouch: boolean\n): KeypadLayoutMode {\n // Desktop: show overlay on demand or hidden by default\n if (deviceType === 'desktop') {\n return isTouch ? 'overlay' : 'hidden';\n }\n\n // Mobile portrait: side-by-side if width allows\n if (deviceType === 'mobile' && orientation === 'portrait') {\n return width >= 360 ? 'side-by-side' : 'stacked';\n }\n\n // Mobile landscape: stacked beside grid if height allows\n if (deviceType === 'mobile' && orientation === 'landscape') {\n return height >= BREAKPOINTS.smallHeight ? 'stacked' : 'overlay';\n }\n\n // Tablet: side-by-side in portrait, stacked in landscape\n if (deviceType === 'tablet') {\n return orientation === 'portrait' ? 'side-by-side' : 'stacked';\n }\n /* c8 ignore next 2 - defense in depth: all device types are explicitly handled above */\n return 'overlay';\n}\n\n/**\n * Hook to detect responsive layout information for optimal keypad display\n * @param forceLayoutMode - Optional layout mode override for testing\n * @public\n */\nexport function useResponsiveLayout(forceLayoutMode?: KeypadLayoutMode): IResponsiveLayoutInfo {\n const [layoutInfo, setLayoutInfo] = useState<IResponsiveLayoutInfo>(() => {\n const width = window.innerWidth;\n const height = window.innerHeight;\n const isTouchDevice = detectTouchSupport();\n const orientation: ScreenOrientation = width > height ? 'landscape' : 'portrait';\n const deviceType = determineDeviceType(width, height, isTouchDevice);\n const keypadLayoutMode =\n forceLayoutMode ?? determineKeypadLayoutMode(deviceType, orientation, width, height, isTouchDevice);\n\n return {\n deviceType,\n orientation,\n keypadLayoutMode,\n screenWidth: width,\n screenHeight: height,\n isTouchDevice,\n isSmallScreen: width <= BREAKPOINTS.mobile,\n hasSpaceForDualKeypads:\n width >= 480 || (orientation === 'landscape' && height >= BREAKPOINTS.smallHeight)\n };\n });\n\n useEffect(() => {\n // Don't set up listeners if layout mode is forced\n if (forceLayoutMode) {\n return;\n }\n\n function updateLayoutInfo(): void {\n const width = window.innerWidth;\n const height = window.innerHeight;\n const isTouchDevice = detectTouchSupport();\n const orientation: ScreenOrientation = width > height ? 'landscape' : 'portrait';\n const deviceType = determineDeviceType(width, height, isTouchDevice);\n const keypadLayoutMode = determineKeypadLayoutMode(\n deviceType,\n orientation,\n width,\n height,\n isTouchDevice\n );\n\n setLayoutInfo({\n deviceType,\n orientation,\n keypadLayoutMode,\n screenWidth: width,\n screenHeight: height,\n isTouchDevice,\n isSmallScreen: width <= BREAKPOINTS.mobile,\n hasSpaceForDualKeypads:\n width >= 480 || (orientation === 'landscape' && height >= BREAKPOINTS.smallHeight)\n });\n }\n\n // Update on resize\n window.addEventListener('resize', updateLayoutInfo);\n\n // Update on orientation change\n window.addEventListener('orientationchange', () => {\n // Small delay to ensure screen dimensions are updated\n setTimeout(updateLayoutInfo, 100);\n });\n\n // Initial update\n updateLayoutInfo();\n\n return () => {\n window.removeEventListener('resize', updateLayoutInfo);\n window.removeEventListener('orientationchange', updateLayoutInfo);\n };\n }, [forceLayoutMode]);\n\n return layoutInfo;\n}\n"]}
|