@fgv/ts-sudoku-ui 5.0.1-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/.rush/temp/chunked-rush-logs/ts-sudoku-ui.build.chunks.jsonl +12 -0
  2. package/.rush/temp/operation/build/log-chunks.jsonl +12 -0
  3. package/.rush/temp/operation/build/state.json +3 -0
  4. package/.rush/temp/shrinkwrap-deps.json +1087 -0
  5. package/.test-failures-catalog.md +470 -0
  6. package/CSS_MODULES_MIGRATION_SUMMARY.md +189 -0
  7. package/dist/ts-sudoku-ui.d.ts +668 -0
  8. package/etc/ts-sudoku-ui.api.md +481 -0
  9. package/lib/components/CageOverlay.d.ts +8 -0
  10. package/lib/components/CageOverlay.d.ts.map +1 -0
  11. package/lib/components/CageOverlay.js +133 -0
  12. package/lib/components/CageOverlay.js.map +1 -0
  13. package/lib/components/CageSumIndicator.d.ts +8 -0
  14. package/lib/components/CageSumIndicator.d.ts.map +1 -0
  15. package/lib/components/CageSumIndicator.js +66 -0
  16. package/lib/components/CageSumIndicator.js.map +1 -0
  17. package/lib/components/CombinationCard.d.ts +8 -0
  18. package/lib/components/CombinationCard.d.ts.map +1 -0
  19. package/lib/components/CombinationCard.js +64 -0
  20. package/lib/components/CombinationCard.js.map +1 -0
  21. package/lib/components/CombinationGrid.d.ts +8 -0
  22. package/lib/components/CombinationGrid.d.ts.map +1 -0
  23. package/lib/components/CombinationGrid.js +51 -0
  24. package/lib/components/CombinationGrid.js.map +1 -0
  25. package/lib/components/CompactControlRibbon.d.ts +45 -0
  26. package/lib/components/CompactControlRibbon.d.ts.map +1 -0
  27. package/lib/components/CompactControlRibbon.js +141 -0
  28. package/lib/components/CompactControlRibbon.js.map +1 -0
  29. package/lib/components/DualKeypad.d.ts +39 -0
  30. package/lib/components/DualKeypad.d.ts.map +1 -0
  31. package/lib/components/DualKeypad.js +137 -0
  32. package/lib/components/DualKeypad.js.map +1 -0
  33. package/lib/components/KillerCombinationsExplorer.d.ts +9 -0
  34. package/lib/components/KillerCombinationsExplorer.d.ts.map +1 -0
  35. package/lib/components/KillerCombinationsExplorer.js +69 -0
  36. package/lib/components/KillerCombinationsExplorer.js.map +1 -0
  37. package/lib/components/KillerCombinationsModal.d.ts +20 -0
  38. package/lib/components/KillerCombinationsModal.d.ts.map +1 -0
  39. package/lib/components/KillerCombinationsModal.js +135 -0
  40. package/lib/components/KillerCombinationsModal.js.map +1 -0
  41. package/lib/components/KillerCombinationsPanel.d.ts +20 -0
  42. package/lib/components/KillerCombinationsPanel.d.ts.map +1 -0
  43. package/lib/components/KillerCombinationsPanel.js +79 -0
  44. package/lib/components/KillerCombinationsPanel.js.map +1 -0
  45. package/lib/components/NumberKeypad.d.ts +30 -0
  46. package/lib/components/NumberKeypad.d.ts.map +1 -0
  47. package/lib/components/NumberKeypad.js +181 -0
  48. package/lib/components/NumberKeypad.js.map +1 -0
  49. package/lib/components/SudokuCell.d.ts +8 -0
  50. package/lib/components/SudokuCell.d.ts.map +1 -0
  51. package/lib/components/SudokuCell.js +240 -0
  52. package/lib/components/SudokuCell.js.map +1 -0
  53. package/lib/components/SudokuControls.d.ts +8 -0
  54. package/lib/components/SudokuControls.d.ts.map +1 -0
  55. package/lib/components/SudokuControls.js +140 -0
  56. package/lib/components/SudokuControls.js.map +1 -0
  57. package/lib/components/SudokuGrid.d.ts +8 -0
  58. package/lib/components/SudokuGrid.d.ts.map +1 -0
  59. package/lib/components/SudokuGrid.js +177 -0
  60. package/lib/components/SudokuGrid.js.map +1 -0
  61. package/lib/components/SudokuGridEntry.d.ts +8 -0
  62. package/lib/components/SudokuGridEntry.d.ts.map +1 -0
  63. package/lib/components/SudokuGridEntry.js +373 -0
  64. package/lib/components/SudokuGridEntry.js.map +1 -0
  65. package/lib/components/ValidationDisplay.d.ts +8 -0
  66. package/lib/components/ValidationDisplay.d.ts.map +1 -0
  67. package/lib/components/ValidationDisplay.js +95 -0
  68. package/lib/components/ValidationDisplay.js.map +1 -0
  69. package/lib/contexts/DiagnosticLoggerContext.d.ts +81 -0
  70. package/lib/contexts/DiagnosticLoggerContext.d.ts.map +1 -0
  71. package/lib/contexts/DiagnosticLoggerContext.js +98 -0
  72. package/lib/contexts/DiagnosticLoggerContext.js.map +1 -0
  73. package/lib/contexts/index.d.ts +3 -0
  74. package/lib/contexts/index.d.ts.map +1 -0
  75. package/lib/contexts/index.js +25 -0
  76. package/lib/contexts/index.js.map +1 -0
  77. package/lib/hooks/useCombinationElimination.d.ts +10 -0
  78. package/lib/hooks/useCombinationElimination.d.ts.map +1 -0
  79. package/lib/hooks/useCombinationElimination.js +123 -0
  80. package/lib/hooks/useCombinationElimination.js.map +1 -0
  81. package/lib/hooks/useKeyboardShortcut.d.ts +19 -0
  82. package/lib/hooks/useKeyboardShortcut.d.ts.map +1 -0
  83. package/lib/hooks/useKeyboardShortcut.js +55 -0
  84. package/lib/hooks/useKeyboardShortcut.js.map +1 -0
  85. package/lib/hooks/useKillerCombinations.d.ts +11 -0
  86. package/lib/hooks/useKillerCombinations.d.ts.map +1 -0
  87. package/lib/hooks/useKillerCombinations.js +65 -0
  88. package/lib/hooks/useKillerCombinations.js.map +1 -0
  89. package/lib/hooks/useLongPress.d.ts +31 -0
  90. package/lib/hooks/useLongPress.d.ts.map +1 -0
  91. package/lib/hooks/useLongPress.js +69 -0
  92. package/lib/hooks/useLongPress.js.map +1 -0
  93. package/lib/hooks/usePuzzleSession.d.ts +32 -0
  94. package/lib/hooks/usePuzzleSession.d.ts.map +1 -0
  95. package/lib/hooks/usePuzzleSession.js +398 -0
  96. package/lib/hooks/usePuzzleSession.js.map +1 -0
  97. package/lib/hooks/useResponsiveLayout.d.ts +44 -0
  98. package/lib/hooks/useResponsiveLayout.d.ts.map +1 -0
  99. package/lib/hooks/useResponsiveLayout.js +139 -0
  100. package/lib/hooks/useResponsiveLayout.js.map +1 -0
  101. package/lib/index.d.ts +28 -0
  102. package/lib/index.d.ts.map +1 -0
  103. package/lib/index.js +49 -0
  104. package/lib/index.js.map +1 -0
  105. package/lib/test/fixtures/killerSudokuPuzzles.d.ts +36 -0
  106. package/lib/test/fixtures/killerSudokuPuzzles.d.ts.map +1 -0
  107. package/lib/test/fixtures/killerSudokuPuzzles.js +57 -0
  108. package/lib/test/fixtures/killerSudokuPuzzles.js.map +1 -0
  109. package/lib/test/unit/components/SudokuGridEntry.testHelpers.d.ts +22 -0
  110. package/lib/test/unit/components/SudokuGridEntry.testHelpers.d.ts.map +1 -0
  111. package/lib/test/unit/components/SudokuGridEntry.testHelpers.js +116 -0
  112. package/lib/test/unit/components/SudokuGridEntry.testHelpers.js.map +1 -0
  113. package/lib/tsdoc-metadata.json +11 -0
  114. package/lib/types/index.d.ts +234 -0
  115. package/lib/types/index.d.ts.map +1 -0
  116. package/lib/types/index.js +25 -0
  117. package/lib/types/index.js.map +1 -0
  118. package/lib/utils/CageLookupManager.d.ts +74 -0
  119. package/lib/utils/CageLookupManager.d.ts.map +1 -0
  120. package/lib/utils/CageLookupManager.js +283 -0
  121. package/lib/utils/CageLookupManager.js.map +1 -0
  122. package/lib/utils/CagePatternManager.d.ts +82 -0
  123. package/lib/utils/CagePatternManager.d.ts.map +1 -0
  124. package/lib/utils/CagePatternManager.js +139 -0
  125. package/lib/utils/CagePatternManager.js.map +1 -0
  126. package/lib/utils/puzzleDefinitionHelper.d.ts +25 -0
  127. package/lib/utils/puzzleDefinitionHelper.d.ts.map +1 -0
  128. package/lib/utils/puzzleDefinitionHelper.js +99 -0
  129. package/lib/utils/puzzleDefinitionHelper.js.map +1 -0
  130. package/lib-commonjs/components/CageOverlay.js +138 -0
  131. package/lib-commonjs/components/CageOverlay.js.map +1 -0
  132. package/lib-commonjs/components/CageSumIndicator.js +71 -0
  133. package/lib-commonjs/components/CageSumIndicator.js.map +1 -0
  134. package/lib-commonjs/components/CombinationCard.js +68 -0
  135. package/lib-commonjs/components/CombinationCard.js.map +1 -0
  136. package/lib-commonjs/components/CombinationGrid.js +56 -0
  137. package/lib-commonjs/components/CombinationGrid.js.map +1 -0
  138. package/lib-commonjs/components/CompactControlRibbon.js +146 -0
  139. package/lib-commonjs/components/CompactControlRibbon.js.map +1 -0
  140. package/lib-commonjs/components/DualKeypad.js +142 -0
  141. package/lib-commonjs/components/DualKeypad.js.map +1 -0
  142. package/lib-commonjs/components/KillerCombinationsExplorer.js +74 -0
  143. package/lib-commonjs/components/KillerCombinationsExplorer.js.map +1 -0
  144. package/lib-commonjs/components/KillerCombinationsModal.js +140 -0
  145. package/lib-commonjs/components/KillerCombinationsModal.js.map +1 -0
  146. package/lib-commonjs/components/KillerCombinationsPanel.js +84 -0
  147. package/lib-commonjs/components/KillerCombinationsPanel.js.map +1 -0
  148. package/lib-commonjs/components/NumberKeypad.js +186 -0
  149. package/lib-commonjs/components/NumberKeypad.js.map +1 -0
  150. package/lib-commonjs/components/SudokuCell.js +245 -0
  151. package/lib-commonjs/components/SudokuCell.js.map +1 -0
  152. package/lib-commonjs/components/SudokuControls.js +145 -0
  153. package/lib-commonjs/components/SudokuControls.js.map +1 -0
  154. package/lib-commonjs/components/SudokuGrid.js +182 -0
  155. package/lib-commonjs/components/SudokuGrid.js.map +1 -0
  156. package/lib-commonjs/components/SudokuGridEntry.js +378 -0
  157. package/lib-commonjs/components/SudokuGridEntry.js.map +1 -0
  158. package/lib-commonjs/components/ValidationDisplay.js +100 -0
  159. package/lib-commonjs/components/ValidationDisplay.js.map +1 -0
  160. package/lib-commonjs/contexts/DiagnosticLoggerContext.js +104 -0
  161. package/lib-commonjs/contexts/DiagnosticLoggerContext.js.map +1 -0
  162. package/lib-commonjs/contexts/index.js +32 -0
  163. package/lib-commonjs/contexts/index.js.map +1 -0
  164. package/lib-commonjs/hooks/useCombinationElimination.js +126 -0
  165. package/lib-commonjs/hooks/useCombinationElimination.js.map +1 -0
  166. package/lib-commonjs/hooks/useKeyboardShortcut.js +58 -0
  167. package/lib-commonjs/hooks/useKeyboardShortcut.js.map +1 -0
  168. package/lib-commonjs/hooks/useKillerCombinations.js +68 -0
  169. package/lib-commonjs/hooks/useKillerCombinations.js.map +1 -0
  170. package/lib-commonjs/hooks/useLongPress.js +72 -0
  171. package/lib-commonjs/hooks/useLongPress.js.map +1 -0
  172. package/lib-commonjs/hooks/usePuzzleSession.js +401 -0
  173. package/lib-commonjs/hooks/usePuzzleSession.js.map +1 -0
  174. package/lib-commonjs/hooks/useResponsiveLayout.js +142 -0
  175. package/lib-commonjs/hooks/useResponsiveLayout.js.map +1 -0
  176. package/lib-commonjs/index.js +73 -0
  177. package/lib-commonjs/index.js.map +1 -0
  178. package/lib-commonjs/test/fixtures/killerSudokuPuzzles.js +60 -0
  179. package/lib-commonjs/test/fixtures/killerSudokuPuzzles.js.map +1 -0
  180. package/lib-commonjs/test/unit/components/SudokuGridEntry.testHelpers.js +125 -0
  181. package/lib-commonjs/test/unit/components/SudokuGridEntry.testHelpers.js.map +1 -0
  182. package/lib-commonjs/types/index.js +26 -0
  183. package/lib-commonjs/types/index.js.map +1 -0
  184. package/lib-commonjs/utils/CageLookupManager.js +287 -0
  185. package/lib-commonjs/utils/CageLookupManager.js.map +1 -0
  186. package/lib-commonjs/utils/CagePatternManager.js +143 -0
  187. package/lib-commonjs/utils/CagePatternManager.js.map +1 -0
  188. package/lib-commonjs/utils/puzzleDefinitionHelper.js +103 -0
  189. package/lib-commonjs/utils/puzzleDefinitionHelper.js.map +1 -0
  190. package/package.json +77 -0
@@ -0,0 +1,245 @@
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.SudokuCell = void 0;
27
+ const tslib_1 = require("tslib");
28
+ const react_1 = tslib_1.__importStar(require("react"));
29
+ const useLongPress_1 = require("../hooks/useLongPress");
30
+ /**
31
+ * Individual Sudoku cell component with input handling and validation display
32
+ * @public
33
+ */
34
+ const SudokuCell = ({ cellInfo, isSelected, inputMode, puzzleDimensions, onSelect, onLongPressToggle, onValueChange, onNoteToggle, onClearAllNotes, onDragOver, className }) => {
35
+ const { id, row, column, contents, isImmutable, hasValidationError } = cellInfo;
36
+ // Double-tap detection for clearing all notes
37
+ const lastClickTimeRef = (0, react_1.useRef)(0);
38
+ const DOUBLE_TAP_DELAY = 300; // milliseconds
39
+ // Track if a long press just occurred to prevent click interference
40
+ const longPressOccurredRef = (0, react_1.useRef)(false);
41
+ // Handle keyboard input with notes-first paradigm
42
+ const handleKeyDown = (0, react_1.useCallback)((event) => {
43
+ if (isImmutable)
44
+ return;
45
+ const key = event.key;
46
+ if (key >= '1' && key <= '9') {
47
+ const value = parseInt(key, 10);
48
+ // Check if modifier keys are pressed to override current input mode
49
+ const hasModifierKey = event.shiftKey || event.ctrlKey || event.metaKey;
50
+ if (hasModifierKey) {
51
+ // Modifier key held - always place value (override current mode)
52
+ onValueChange(value);
53
+ }
54
+ else if (inputMode === 'value') {
55
+ // Value mode - place value
56
+ onValueChange(value);
57
+ }
58
+ else {
59
+ // Notes mode (default) - toggle note
60
+ onNoteToggle(value);
61
+ }
62
+ event.preventDefault();
63
+ }
64
+ else if (key === 'Delete' || key === 'Backspace') {
65
+ if (contents.value) {
66
+ // Clear cell value
67
+ onValueChange(undefined);
68
+ }
69
+ else if (contents.notes.length > 0) {
70
+ // Clear all notes
71
+ onClearAllNotes();
72
+ }
73
+ event.preventDefault();
74
+ }
75
+ else if (key === '0') {
76
+ // Clear everything
77
+ if (contents.value) {
78
+ onValueChange(undefined);
79
+ }
80
+ if (contents.notes.length > 0) {
81
+ onClearAllNotes();
82
+ }
83
+ event.preventDefault();
84
+ }
85
+ // Arrow key navigation is handled by the parent component
86
+ }, [
87
+ isImmutable,
88
+ inputMode,
89
+ onValueChange,
90
+ onNoteToggle,
91
+ onClearAllNotes,
92
+ contents.value,
93
+ contents.notes.length
94
+ ]);
95
+ // Handle long press for multiselect toggle
96
+ const handleLongPress = (0, react_1.useCallback)((event) => {
97
+ if (onLongPressToggle && !isImmutable) {
98
+ longPressOccurredRef.current = true;
99
+ onLongPressToggle(event);
100
+ }
101
+ }, [onLongPressToggle, isImmutable]);
102
+ // Set up long press detection
103
+ const longPressProps = (0, useLongPress_1.useLongPress)({
104
+ onLongPress: handleLongPress,
105
+ delay: 500,
106
+ shouldPreventDefault: false
107
+ });
108
+ // Handle mouse click with double-tap detection
109
+ const handleClick = (0, react_1.useCallback)((event) => {
110
+ // If a long press just occurred, ignore this click event
111
+ if (longPressOccurredRef.current) {
112
+ longPressOccurredRef.current = false; // Reset for next interaction
113
+ event.preventDefault();
114
+ return;
115
+ }
116
+ // Prevent default context menu on Ctrl+click
117
+ if (event.ctrlKey || event.metaKey) {
118
+ event.preventDefault();
119
+ }
120
+ const currentTime = Date.now();
121
+ const timeSinceLastClick = currentTime - lastClickTimeRef.current;
122
+ if (timeSinceLastClick < DOUBLE_TAP_DELAY && !isImmutable && contents.notes.length > 0) {
123
+ // Double-tap detected - clear all notes
124
+ onClearAllNotes();
125
+ }
126
+ else {
127
+ // Single click - select cell (pass event for multi-select)
128
+ onSelect(event);
129
+ }
130
+ lastClickTimeRef.current = currentTime;
131
+ }, [onSelect, onClearAllNotes, isImmutable, contents.notes.length]);
132
+ // Handle mouse up - need to check if long press just occurred
133
+ const handleMouseUp = (0, react_1.useCallback)((event) => {
134
+ longPressProps.onMouseUp(event);
135
+ // Don't reset the flag here - let the click handler handle it
136
+ // The flag will be reset in handleClick or handleMouseLeave
137
+ }, [longPressProps]);
138
+ // Handle touch end - need to check if long press just occurred
139
+ const handleTouchEnd = (0, react_1.useCallback)((event) => {
140
+ longPressProps.onTouchEnd(event);
141
+ // Don't reset the flag here - let the click handler handle it
142
+ // The flag will be reset in handleClick or handleMouseLeave
143
+ }, [longPressProps]);
144
+ // Handle mouse leave - reset long press flag
145
+ const handleMouseLeave = (0, react_1.useCallback)((event) => {
146
+ longPressOccurredRef.current = false;
147
+ longPressProps.onMouseLeave(event);
148
+ }, [longPressProps]);
149
+ // Calculate CSS classes
150
+ const cellClasses = (0, react_1.useMemo)(() => {
151
+ const classes = [
152
+ // Base cell styles - fill grid cell completely
153
+ 'w-full',
154
+ 'h-full',
155
+ 'border',
156
+ 'border-gray-300',
157
+ 'bg-white',
158
+ 'text-blue-800',
159
+ 'text-base',
160
+ 'md:text-lg',
161
+ 'lg:text-xl',
162
+ 'font-bold',
163
+ 'flex',
164
+ 'items-center',
165
+ 'justify-center',
166
+ 'cursor-pointer',
167
+ 'outline-none',
168
+ 'relative',
169
+ 'select-none',
170
+ 'touch-manipulation',
171
+ 'transition-all',
172
+ 'duration-150',
173
+ 'overflow-hidden',
174
+ 'p-0'
175
+ ];
176
+ // Hover effects - apply before selection so selection can override
177
+ if (!isImmutable && !isSelected) {
178
+ classes.push('hover:bg-gray-50', 'hover:border-blue-500');
179
+ }
180
+ if (hasValidationError && !isSelected) {
181
+ classes.push('hover:bg-red-50', 'hover:border-red-600');
182
+ }
183
+ // State-based styles - Applied after hover so they take precedence
184
+ if (isSelected) {
185
+ if (className?.includes('multi-selected')) {
186
+ // Multi-Selected: More distinct blue background with darker border
187
+ classes.push('bg-blue-200', 'border-blue-500', '!bg-blue-200');
188
+ }
189
+ else {
190
+ // Single Selected: Darker blue background for clear distinction
191
+ classes.push('bg-blue-300', 'border-blue-600', '!bg-blue-300');
192
+ }
193
+ }
194
+ if (hasValidationError) {
195
+ classes.push('bg-red-50', 'text-red-800', 'border-red-500');
196
+ }
197
+ if (isImmutable) {
198
+ classes.push('bg-gray-100', 'cursor-default', 'text-gray-900');
199
+ }
200
+ if (contents.value) {
201
+ classes.push('font-black');
202
+ }
203
+ // Enhanced section boundaries - dynamic based on cage dimensions
204
+ const cageWidth = puzzleDimensions?.cageWidth ?? 3;
205
+ const cageHeight = puzzleDimensions?.cageHeight ?? 3;
206
+ const numRows = puzzleDimensions?.numRows ?? 9;
207
+ const numColumns = puzzleDimensions?.numColumns ?? 9;
208
+ if ((column + 1) % cageWidth === 0 && column < numColumns - 1) {
209
+ classes.push('border-r-2', 'border-r-black');
210
+ }
211
+ if ((row + 1) % cageHeight === 0 && row < numRows - 1) {
212
+ classes.push('border-b-2', 'border-b-black');
213
+ }
214
+ // Focus styles
215
+ classes.push('focus-visible:ring-2', 'focus-visible:ring-blue-500', 'focus-visible:ring-offset-1');
216
+ if (className)
217
+ classes.push(className);
218
+ return classes.join(' ');
219
+ }, [isSelected, hasValidationError, isImmutable, contents.value, row, column, className, puzzleDimensions]);
220
+ // Format display value
221
+ const displayValue = contents.value?.toString() || '';
222
+ // Render notes in fixed 3x3 grid layout - each number has fixed position
223
+ const renderNotesGrid = (0, react_1.useCallback)(() => {
224
+ /* c8 ignore next 1 - functional code tested but coverage intermittently missed */
225
+ if (contents.value)
226
+ return null; // Only show notes when no value is present
227
+ // Create a 3x3 grid for notes (positions 1-9) with fixed layout
228
+ // Only render notes that are actually selected (completely invisible otherwise)
229
+ const notesGrid = Array(9)
230
+ .fill(null)
231
+ .map((__value, index) => {
232
+ const noteValue = index + 1;
233
+ const hasNote = contents.notes.includes(noteValue);
234
+ return (react_1.default.createElement("div", { key: noteValue, className: "flex items-center justify-center" }, hasNote && (react_1.default.createElement("span", { className: "text-[0.5rem] md:text-[0.55rem] lg:text-[0.6rem] font-normal leading-none text-gray-600" }, noteValue))));
235
+ });
236
+ return (react_1.default.createElement("div", { className: "absolute inset-[12.5%] grid grid-cols-3 grid-rows-3 gap-0 pointer-events-none", style: { gridTemplateColumns: 'repeat(3, 1fr)', gridTemplateRows: 'repeat(3, 1fr)' }, "aria-hidden": "true" }, notesGrid));
237
+ }, [contents.value, contents.notes]);
238
+ return (react_1.default.createElement("button", { type: "button", className: cellClasses, onClick: handleClick, onContextMenu: (e) => e.preventDefault(), onKeyDown: handleKeyDown, onMouseDown: longPressProps.onMouseDown, onMouseUp: handleMouseUp, onMouseMove: onDragOver, onMouseLeave: handleMouseLeave, onTouchStart: longPressProps.onTouchStart, onTouchMove: onDragOver, onTouchEnd: handleTouchEnd, tabIndex: isSelected ? 0 : -1, "data-testid": `sudoku-cell-${id}`, "data-row": row, "data-column": column, "data-selected": isSelected, "data-error": hasValidationError, "data-immutable": isImmutable, "data-filled": !!contents.value, "aria-label": `Row ${row + 1}, Column ${column + 1}, ${contents.value
239
+ ? contents.value.toString()
240
+ : contents.notes.length > 0
241
+ ? `notes: ${contents.notes.join(', ')}`
242
+ : 'empty'}`, "aria-selected": isSelected }, contents.value ? react_1.default.createElement("span", { className: "text-inherit font-inherit" }, displayValue) : renderNotesGrid()));
243
+ };
244
+ exports.SudokuCell = SudokuCell;
245
+ //# sourceMappingURL=SudokuCell.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SudokuCell.js","sourceRoot":"","sources":["../../src/components/SudokuCell.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, useMemo, useRef } from 'react';\nimport { ISudokuCellProps } from '../types';\nimport { useLongPress } from '../hooks/useLongPress';\n\n/**\n * Individual Sudoku cell component with input handling and validation display\n * @public\n */\nexport const SudokuCell: React.FC<ISudokuCellProps> = ({\n cellInfo,\n isSelected,\n inputMode,\n puzzleDimensions,\n onSelect,\n onLongPressToggle,\n onValueChange,\n onNoteToggle,\n onClearAllNotes,\n onDragOver,\n className\n}) => {\n const { id, row, column, contents, isImmutable, hasValidationError } = cellInfo;\n\n // Double-tap detection for clearing all notes\n const lastClickTimeRef = useRef<number>(0);\n const DOUBLE_TAP_DELAY = 300; // milliseconds\n\n // Track if a long press just occurred to prevent click interference\n const longPressOccurredRef = useRef<boolean>(false);\n\n // Handle keyboard input with notes-first paradigm\n const handleKeyDown = useCallback(\n (event: React.KeyboardEvent) => {\n if (isImmutable) return;\n\n const key = event.key;\n\n if (key >= '1' && key <= '9') {\n const value = parseInt(key, 10);\n\n // Check if modifier keys are pressed to override current input mode\n const hasModifierKey = event.shiftKey || event.ctrlKey || event.metaKey;\n\n if (hasModifierKey) {\n // Modifier key held - always place value (override current mode)\n onValueChange(value);\n } else if (inputMode === 'value') {\n // Value mode - place value\n onValueChange(value);\n } else {\n // Notes mode (default) - toggle note\n onNoteToggle(value);\n }\n event.preventDefault();\n } else if (key === 'Delete' || key === 'Backspace') {\n if (contents.value) {\n // Clear cell value\n onValueChange(undefined);\n } else if (contents.notes.length > 0) {\n // Clear all notes\n onClearAllNotes();\n }\n event.preventDefault();\n } else if (key === '0') {\n // Clear everything\n if (contents.value) {\n onValueChange(undefined);\n }\n if (contents.notes.length > 0) {\n onClearAllNotes();\n }\n event.preventDefault();\n }\n // Arrow key navigation is handled by the parent component\n },\n [\n isImmutable,\n inputMode,\n onValueChange,\n onNoteToggle,\n onClearAllNotes,\n contents.value,\n contents.notes.length\n ]\n );\n\n // Handle long press for multiselect toggle\n const handleLongPress = useCallback(\n (event: React.TouchEvent | React.MouseEvent) => {\n if (onLongPressToggle && !isImmutable) {\n longPressOccurredRef.current = true;\n onLongPressToggle(event);\n }\n },\n [onLongPressToggle, isImmutable]\n );\n\n // Set up long press detection\n const longPressProps = useLongPress({\n onLongPress: handleLongPress,\n delay: 500,\n shouldPreventDefault: false\n });\n\n // Handle mouse click with double-tap detection\n const handleClick = useCallback(\n (event: React.MouseEvent) => {\n // If a long press just occurred, ignore this click event\n if (longPressOccurredRef.current) {\n longPressOccurredRef.current = false; // Reset for next interaction\n event.preventDefault();\n return;\n }\n\n // Prevent default context menu on Ctrl+click\n if (event.ctrlKey || event.metaKey) {\n event.preventDefault();\n }\n\n const currentTime = Date.now();\n const timeSinceLastClick = currentTime - lastClickTimeRef.current;\n\n if (timeSinceLastClick < DOUBLE_TAP_DELAY && !isImmutable && contents.notes.length > 0) {\n // Double-tap detected - clear all notes\n onClearAllNotes();\n } else {\n // Single click - select cell (pass event for multi-select)\n onSelect(event);\n }\n\n lastClickTimeRef.current = currentTime;\n },\n [onSelect, onClearAllNotes, isImmutable, contents.notes.length]\n );\n\n // Handle mouse up - need to check if long press just occurred\n const handleMouseUp = useCallback(\n (event: React.MouseEvent) => {\n longPressProps.onMouseUp(event);\n\n // Don't reset the flag here - let the click handler handle it\n // The flag will be reset in handleClick or handleMouseLeave\n },\n [longPressProps]\n );\n\n // Handle touch end - need to check if long press just occurred\n const handleTouchEnd = useCallback(\n (event: React.TouchEvent) => {\n longPressProps.onTouchEnd(event);\n\n // Don't reset the flag here - let the click handler handle it\n // The flag will be reset in handleClick or handleMouseLeave\n },\n [longPressProps]\n );\n\n // Handle mouse leave - reset long press flag\n const handleMouseLeave = useCallback(\n (event: React.MouseEvent) => {\n longPressOccurredRef.current = false;\n longPressProps.onMouseLeave(event);\n },\n [longPressProps]\n );\n\n // Calculate CSS classes\n const cellClasses = useMemo(() => {\n const classes = [\n // Base cell styles - fill grid cell completely\n 'w-full',\n 'h-full',\n 'border',\n 'border-gray-300',\n 'bg-white',\n 'text-blue-800',\n 'text-base',\n 'md:text-lg',\n 'lg:text-xl',\n 'font-bold',\n 'flex',\n 'items-center',\n 'justify-center',\n 'cursor-pointer',\n 'outline-none',\n 'relative',\n 'select-none',\n 'touch-manipulation',\n 'transition-all',\n 'duration-150',\n 'overflow-hidden',\n 'p-0'\n ];\n\n // Hover effects - apply before selection so selection can override\n if (!isImmutable && !isSelected) {\n classes.push('hover:bg-gray-50', 'hover:border-blue-500');\n }\n if (hasValidationError && !isSelected) {\n classes.push('hover:bg-red-50', 'hover:border-red-600');\n }\n\n // State-based styles - Applied after hover so they take precedence\n if (isSelected) {\n if (className?.includes('multi-selected')) {\n // Multi-Selected: More distinct blue background with darker border\n classes.push('bg-blue-200', 'border-blue-500', '!bg-blue-200');\n } else {\n // Single Selected: Darker blue background for clear distinction\n classes.push('bg-blue-300', 'border-blue-600', '!bg-blue-300');\n }\n }\n if (hasValidationError) {\n classes.push('bg-red-50', 'text-red-800', 'border-red-500');\n }\n if (isImmutable) {\n classes.push('bg-gray-100', 'cursor-default', 'text-gray-900');\n }\n if (contents.value) {\n classes.push('font-black');\n }\n\n // Enhanced section boundaries - dynamic based on cage dimensions\n const cageWidth = puzzleDimensions?.cageWidth ?? 3;\n const cageHeight = puzzleDimensions?.cageHeight ?? 3;\n const numRows = puzzleDimensions?.numRows ?? 9;\n const numColumns = puzzleDimensions?.numColumns ?? 9;\n\n if ((column + 1) % cageWidth === 0 && column < numColumns - 1) {\n classes.push('border-r-2', 'border-r-black');\n }\n if ((row + 1) % cageHeight === 0 && row < numRows - 1) {\n classes.push('border-b-2', 'border-b-black');\n }\n\n // Focus styles\n classes.push('focus-visible:ring-2', 'focus-visible:ring-blue-500', 'focus-visible:ring-offset-1');\n\n if (className) classes.push(className);\n\n return classes.join(' ');\n }, [isSelected, hasValidationError, isImmutable, contents.value, row, column, className, puzzleDimensions]);\n\n // Format display value\n const displayValue = contents.value?.toString() || '';\n\n // Render notes in fixed 3x3 grid layout - each number has fixed position\n const renderNotesGrid = useCallback(() => {\n /* c8 ignore next 1 - functional code tested but coverage intermittently missed */\n if (contents.value) return null; // Only show notes when no value is present\n\n // Create a 3x3 grid for notes (positions 1-9) with fixed layout\n // Only render notes that are actually selected (completely invisible otherwise)\n const notesGrid = Array(9)\n .fill(null)\n .map((__value, index) => {\n const noteValue = index + 1;\n const hasNote = contents.notes.includes(noteValue);\n\n return (\n <div key={noteValue} className=\"flex items-center justify-center\">\n {hasNote && (\n <span className=\"text-[0.5rem] md:text-[0.55rem] lg:text-[0.6rem] font-normal leading-none text-gray-600\">\n {noteValue}\n </span>\n )}\n </div>\n );\n });\n\n return (\n <div\n className=\"absolute inset-[12.5%] grid grid-cols-3 grid-rows-3 gap-0 pointer-events-none\"\n style={{ gridTemplateColumns: 'repeat(3, 1fr)', gridTemplateRows: 'repeat(3, 1fr)' }}\n aria-hidden=\"true\"\n >\n {notesGrid}\n </div>\n );\n }, [contents.value, contents.notes]);\n\n return (\n <button\n type=\"button\"\n className={cellClasses}\n onClick={handleClick}\n onContextMenu={(e) => e.preventDefault()}\n onKeyDown={handleKeyDown}\n onMouseDown={longPressProps.onMouseDown}\n onMouseUp={handleMouseUp}\n onMouseMove={onDragOver}\n onMouseLeave={handleMouseLeave}\n onTouchStart={longPressProps.onTouchStart}\n onTouchMove={onDragOver}\n onTouchEnd={handleTouchEnd}\n tabIndex={isSelected ? 0 : -1}\n data-testid={`sudoku-cell-${id}`}\n data-row={row}\n data-column={column}\n data-selected={isSelected}\n data-error={hasValidationError}\n data-immutable={isImmutable}\n data-filled={!!contents.value}\n aria-label={`Row ${row + 1}, Column ${column + 1}, ${\n contents.value\n ? contents.value.toString()\n : contents.notes.length > 0\n ? `notes: ${contents.notes.join(', ')}`\n : 'empty'\n }`}\n aria-selected={isSelected}\n >\n {contents.value ? <span className=\"text-inherit font-inherit\">{displayValue}</span> : renderNotesGrid()}\n </button>\n );\n};\n"]}
@@ -0,0 +1,145 @@
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.SudokuControls = void 0;
27
+ const tslib_1 = require("tslib");
28
+ const react_1 = tslib_1.__importStar(require("react"));
29
+ const useResponsiveLayout_1 = require("../hooks/useResponsiveLayout");
30
+ /**
31
+ * Control panel for grid-wide operations
32
+ * @public
33
+ */
34
+ const SudokuControls = ({ canUndo, canRedo, canReset, isValid, isSolved, onUndo, onRedo, onReset, onExport, className }) => {
35
+ const [showResetConfirm, setShowResetConfirm] = (0, react_1.useState)(false);
36
+ const responsiveLayout = (0, useResponsiveLayout_1.useResponsiveLayout)();
37
+ // Handle reset with confirmation
38
+ const handleResetClick = (0, react_1.useCallback)(() => {
39
+ if (showResetConfirm) {
40
+ onReset();
41
+ setShowResetConfirm(false);
42
+ }
43
+ else {
44
+ setShowResetConfirm(true);
45
+ // Auto-hide confirmation after 3 seconds
46
+ setTimeout(() => setShowResetConfirm(false), 3000);
47
+ }
48
+ }, [showResetConfirm, onReset]);
49
+ // Cancel reset confirmation
50
+ const handleCancelReset = (0, react_1.useCallback)(() => {
51
+ setShowResetConfirm(false);
52
+ }, []);
53
+ // Calculate control classes
54
+ const controlClasses = (0, react_1.useMemo)(() => {
55
+ const classes = ['sudoku-controls'];
56
+ if (className)
57
+ classes.push(className);
58
+ return classes.join(' ');
59
+ }, [className]);
60
+ // Button base styles
61
+ const buttonStyle = {
62
+ padding: '8px 16px',
63
+ margin: '4px',
64
+ border: '1px solid #ddd',
65
+ borderRadius: '4px',
66
+ backgroundColor: '#fff',
67
+ cursor: 'pointer',
68
+ fontSize: '14px',
69
+ fontWeight: 'bold'
70
+ };
71
+ const disabledButtonStyle = {
72
+ ...buttonStyle,
73
+ backgroundColor: '#f5f5f5',
74
+ color: '#999',
75
+ cursor: 'not-allowed'
76
+ };
77
+ // Removed unused primaryButtonStyle variable
78
+ const dangerButtonStyle = {
79
+ ...buttonStyle,
80
+ backgroundColor: '#f44336',
81
+ color: '#fff',
82
+ border: '1px solid #d32f2f'
83
+ };
84
+ const successButtonStyle = {
85
+ ...buttonStyle,
86
+ backgroundColor: '#4caf50',
87
+ color: '#fff',
88
+ border: '1px solid #388e3c'
89
+ };
90
+ // Calculate if we should use compact layout
91
+ const isCompactMode = responsiveLayout.deviceType === 'mobile' &&
92
+ (responsiveLayout.keypadLayoutMode === 'side-by-side' || responsiveLayout.keypadLayoutMode === 'stacked');
93
+ return (react_1.default.createElement("div", { className: controlClasses, "data-testid": "sudoku-controls" },
94
+ react_1.default.createElement("div", { style: {
95
+ display: 'flex',
96
+ flexDirection: isCompactMode ? 'row' : 'column',
97
+ alignItems: 'center',
98
+ justifyContent: 'center',
99
+ gap: isCompactMode ? '8px' : '16px',
100
+ padding: isCompactMode ? '8px' : '16px',
101
+ border: '1px solid #ddd',
102
+ borderRadius: '8px',
103
+ backgroundColor: '#fafafa',
104
+ flexWrap: isCompactMode ? 'wrap' : 'nowrap'
105
+ } },
106
+ react_1.default.createElement("div", { style: { display: 'flex', gap: '8px' } },
107
+ react_1.default.createElement("button", { type: "button", onClick: onUndo, disabled: !canUndo, style: canUndo ? buttonStyle : disabledButtonStyle, "data-testid": "undo-button", title: "Undo last move" }, "\u21B6 Undo"),
108
+ react_1.default.createElement("button", { type: "button", onClick: onRedo, disabled: !canRedo, style: canRedo ? buttonStyle : disabledButtonStyle, "data-testid": "redo-button", title: "Redo last undone move" }, "\u21B7 Redo")),
109
+ !showResetConfirm ? (react_1.default.createElement("button", { type: "button", onClick: handleResetClick, disabled: !canReset, style: canReset ? dangerButtonStyle : disabledButtonStyle, "data-testid": "reset-button", title: "Clear all entries" }, "\uD83D\uDDD1\uFE0F Reset Puzzle")) : (react_1.default.createElement("div", { style: {
110
+ display: 'flex',
111
+ flexDirection: isCompactMode ? 'row' : 'column',
112
+ alignItems: 'center',
113
+ gap: '8px'
114
+ } },
115
+ !isCompactMode && (react_1.default.createElement("div", { style: { fontSize: '12px', color: '#666', textAlign: 'center' } }, "Are you sure? This will clear all entries.")),
116
+ react_1.default.createElement("div", { style: { display: 'flex', gap: '8px' } },
117
+ react_1.default.createElement("button", { type: "button", onClick: handleResetClick, style: dangerButtonStyle, "data-testid": "confirm-reset-button", title: isCompactMode ? 'Are you sure? This will clear all entries.' : undefined }, "Yes, Reset"),
118
+ react_1.default.createElement("button", { type: "button", onClick: handleCancelReset, style: buttonStyle, "data-testid": "cancel-reset-button" }, "Cancel")))),
119
+ react_1.default.createElement("button", { type: "button", onClick: onExport, style: successButtonStyle, "data-testid": "export-button", title: "Export puzzle in compatible format" }, "\uD83D\uDCE4 Export Puzzle"),
120
+ !(responsiveLayout.deviceType === 'mobile' &&
121
+ (responsiveLayout.keypadLayoutMode === 'side-by-side' ||
122
+ responsiveLayout.keypadLayoutMode === 'stacked')) && (react_1.default.createElement("div", { style: {
123
+ fontSize: '12px',
124
+ color: '#666',
125
+ textAlign: 'center',
126
+ maxWidth: '300px',
127
+ lineHeight: '1.4'
128
+ } },
129
+ react_1.default.createElement("div", { style: { fontWeight: 'bold', marginBottom: '4px' } }, "Instructions:"),
130
+ react_1.default.createElement("div", null, "\u2022 Click a cell to select it"),
131
+ react_1.default.createElement("div", null, "\u2022 Type 1-9 to enter numbers"),
132
+ react_1.default.createElement("div", null, "\u2022 Press Delete/Backspace to clear"),
133
+ react_1.default.createElement("div", null, "\u2022 Use arrow keys to navigate"))),
134
+ react_1.default.createElement("div", { style: {
135
+ padding: '8px 12px',
136
+ borderRadius: '4px',
137
+ fontSize: '12px',
138
+ fontWeight: 'bold',
139
+ backgroundColor: isSolved ? '#e8f5e8' : isValid ? '#e3f2fd' : '#ffebee',
140
+ color: isSolved ? '#2e7d32' : isValid ? '#1565c0' : '#c62828',
141
+ border: `1px solid ${isSolved ? '#4caf50' : isValid ? '#2196f3' : '#f44336'}`
142
+ } }, isSolved ? '✅ Solved!' : isValid ? '✓ Valid' : '⚠ Has Errors'))));
143
+ };
144
+ exports.SudokuControls = SudokuControls;
145
+ //# sourceMappingURL=SudokuControls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SudokuControls.js","sourceRoot":"","sources":["../../src/components/SudokuControls.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, useMemo, useState } from 'react';\nimport { ISudokuControlsProps } from '../types';\nimport { useResponsiveLayout } from '../hooks/useResponsiveLayout';\n\n/**\n * Control panel for grid-wide operations\n * @public\n */\nexport const SudokuControls: React.FC<ISudokuControlsProps> = ({\n canUndo,\n canRedo,\n canReset,\n isValid,\n isSolved,\n onUndo,\n onRedo,\n onReset,\n onExport,\n className\n}) => {\n const [showResetConfirm, setShowResetConfirm] = useState(false);\n const responsiveLayout = useResponsiveLayout();\n\n // Handle reset with confirmation\n const handleResetClick = useCallback(() => {\n if (showResetConfirm) {\n onReset();\n setShowResetConfirm(false);\n } else {\n setShowResetConfirm(true);\n // Auto-hide confirmation after 3 seconds\n setTimeout(() => setShowResetConfirm(false), 3000);\n }\n }, [showResetConfirm, onReset]);\n\n // Cancel reset confirmation\n const handleCancelReset = useCallback(() => {\n setShowResetConfirm(false);\n }, []);\n\n // Calculate control classes\n const controlClasses = useMemo(() => {\n const classes = ['sudoku-controls'];\n if (className) classes.push(className);\n return classes.join(' ');\n }, [className]);\n\n // Button base styles\n const buttonStyle = {\n padding: '8px 16px',\n margin: '4px',\n border: '1px solid #ddd',\n borderRadius: '4px',\n backgroundColor: '#fff',\n cursor: 'pointer',\n fontSize: '14px',\n fontWeight: 'bold'\n };\n\n const disabledButtonStyle = {\n ...buttonStyle,\n backgroundColor: '#f5f5f5',\n color: '#999',\n cursor: 'not-allowed'\n };\n\n // Removed unused primaryButtonStyle variable\n\n const dangerButtonStyle = {\n ...buttonStyle,\n backgroundColor: '#f44336',\n color: '#fff',\n border: '1px solid #d32f2f'\n };\n\n const successButtonStyle = {\n ...buttonStyle,\n backgroundColor: '#4caf50',\n color: '#fff',\n border: '1px solid #388e3c'\n };\n\n // Calculate if we should use compact layout\n const isCompactMode =\n responsiveLayout.deviceType === 'mobile' &&\n (responsiveLayout.keypadLayoutMode === 'side-by-side' || responsiveLayout.keypadLayoutMode === 'stacked');\n\n return (\n <div className={controlClasses} data-testid=\"sudoku-controls\">\n <div\n style={{\n display: 'flex',\n flexDirection: isCompactMode ? 'row' : 'column',\n alignItems: 'center',\n justifyContent: 'center',\n gap: isCompactMode ? '8px' : '16px',\n padding: isCompactMode ? '8px' : '16px',\n border: '1px solid #ddd',\n borderRadius: '8px',\n backgroundColor: '#fafafa',\n flexWrap: isCompactMode ? 'wrap' : 'nowrap'\n }}\n >\n {/* Undo/Redo controls */}\n <div style={{ display: 'flex', gap: '8px' }}>\n <button\n type=\"button\"\n onClick={onUndo}\n disabled={!canUndo}\n style={canUndo ? buttonStyle : disabledButtonStyle}\n data-testid=\"undo-button\"\n title=\"Undo last move\"\n >\n ↶ Undo\n </button>\n <button\n type=\"button\"\n onClick={onRedo}\n disabled={!canRedo}\n style={canRedo ? buttonStyle : disabledButtonStyle}\n data-testid=\"redo-button\"\n title=\"Redo last undone move\"\n >\n ↷ Redo\n </button>\n </div>\n\n {/* Reset control with confirmation */}\n {!showResetConfirm ? (\n <button\n type=\"button\"\n onClick={handleResetClick}\n disabled={!canReset}\n style={canReset ? dangerButtonStyle : disabledButtonStyle}\n data-testid=\"reset-button\"\n title=\"Clear all entries\"\n >\n 🗑️ Reset Puzzle\n </button>\n ) : (\n <div\n style={{\n display: 'flex',\n flexDirection: isCompactMode ? 'row' : 'column',\n alignItems: 'center',\n gap: '8px'\n }}\n >\n {!isCompactMode && (\n <div style={{ fontSize: '12px', color: '#666', textAlign: 'center' }}>\n Are you sure? This will clear all entries.\n </div>\n )}\n <div style={{ display: 'flex', gap: '8px' }}>\n <button\n type=\"button\"\n onClick={handleResetClick}\n style={dangerButtonStyle}\n data-testid=\"confirm-reset-button\"\n title={isCompactMode ? 'Are you sure? This will clear all entries.' : undefined}\n >\n Yes, Reset\n </button>\n <button\n type=\"button\"\n onClick={handleCancelReset}\n style={buttonStyle}\n data-testid=\"cancel-reset-button\"\n >\n Cancel\n </button>\n </div>\n </div>\n )}\n\n {/* Export control */}\n <button\n type=\"button\"\n onClick={onExport}\n style={successButtonStyle}\n data-testid=\"export-button\"\n title=\"Export puzzle in compatible format\"\n >\n 📤 Export Puzzle\n </button>\n\n {/* Instructions - hide on mobile with visible keypads */}\n {!(\n responsiveLayout.deviceType === 'mobile' &&\n (responsiveLayout.keypadLayoutMode === 'side-by-side' ||\n responsiveLayout.keypadLayoutMode === 'stacked')\n ) && (\n <div\n style={{\n fontSize: '12px',\n color: '#666',\n textAlign: 'center',\n maxWidth: '300px',\n lineHeight: '1.4'\n }}\n >\n <div style={{ fontWeight: 'bold', marginBottom: '4px' }}>Instructions:</div>\n <div>• Click a cell to select it</div>\n <div>• Type 1-9 to enter numbers</div>\n <div>• Press Delete/Backspace to clear</div>\n <div>• Use arrow keys to navigate</div>\n </div>\n )}\n\n {/* Status indicator */}\n <div\n style={{\n padding: '8px 12px',\n borderRadius: '4px',\n fontSize: '12px',\n fontWeight: 'bold',\n backgroundColor: isSolved ? '#e8f5e8' : isValid ? '#e3f2fd' : '#ffebee',\n color: isSolved ? '#2e7d32' : isValid ? '#1565c0' : '#c62828',\n border: `1px solid ${isSolved ? '#4caf50' : isValid ? '#2196f3' : '#f44336'}`\n }}\n >\n {isSolved ? '✅ Solved!' : isValid ? '✓ Valid' : '⚠ Has Errors'}\n </div>\n </div>\n </div>\n );\n};\n"]}
@@ -0,0 +1,182 @@
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.SudokuGrid = void 0;
27
+ const tslib_1 = require("tslib");
28
+ const react_1 = tslib_1.__importStar(require("react"));
29
+ const SudokuCell_1 = require("./SudokuCell");
30
+ const CageOverlay_1 = require("./CageOverlay");
31
+ /**
32
+ * Grid component that renders the 9x9 Sudoku grid with proper visual structure
33
+ * @public
34
+ */
35
+ const SudokuGrid = ({ numRows, numColumns, cells, cages, selectedCell, selectedCells, inputMode, puzzleType, puzzleDimensions, onCellSelect, onLongPressToggle, onCellValueChange, onNoteToggle, onClearAllNotes, onNavigate, onDragOver, onDragEnd, isDragging, className }) => {
36
+ // Handle keyboard navigation
37
+ const handleKeyDown = (0, react_1.useCallback)((event) => {
38
+ if (!selectedCell)
39
+ return;
40
+ const { key } = event;
41
+ let direction = null;
42
+ switch (key) {
43
+ case 'ArrowUp':
44
+ direction = 'up';
45
+ break;
46
+ case 'ArrowDown':
47
+ direction = 'down';
48
+ break;
49
+ case 'ArrowLeft':
50
+ direction = 'left';
51
+ break;
52
+ case 'ArrowRight':
53
+ direction = 'right';
54
+ break;
55
+ default:
56
+ return; // Let the cell handle number/delete keys
57
+ }
58
+ if (direction) {
59
+ onNavigate(direction);
60
+ event.preventDefault();
61
+ }
62
+ }, [selectedCell, onNavigate]);
63
+ // Create cell select handler
64
+ const createCellSelectHandler = (0, react_1.useCallback)((cellId) => {
65
+ return (event) => onCellSelect(cellId, event);
66
+ }, [onCellSelect]);
67
+ // Create cell value change handler
68
+ const createCellValueChangeHandler = (0, react_1.useCallback)((cellId) => {
69
+ return (value) => onCellValueChange(cellId, value);
70
+ }, [onCellValueChange]);
71
+ // Create note toggle handler
72
+ const createNoteToggleHandler = (0, react_1.useCallback)((cellId) => {
73
+ return (note) => onNoteToggle(cellId, note);
74
+ }, [onNoteToggle]);
75
+ // Create clear all notes handler
76
+ const createClearAllNotesHandler = (0, react_1.useCallback)((cellId) => {
77
+ return () => onClearAllNotes(cellId);
78
+ }, [onClearAllNotes]);
79
+ // Create long press toggle handler
80
+ const createLongPressToggleHandler = (0, react_1.useCallback)((cellId) => {
81
+ return onLongPressToggle
82
+ ? (event) => onLongPressToggle(cellId, event)
83
+ : undefined;
84
+ }, [onLongPressToggle]);
85
+ // Create drag over handler
86
+ const createDragOverHandler = (0, react_1.useCallback)((cellId) => {
87
+ return onDragOver ? () => onDragOver(cellId) : undefined;
88
+ }, [onDragOver]);
89
+ // Calculate grid classes
90
+ const gridClasses = (0, react_1.useMemo)(() => {
91
+ const classes = ['sudoku-grid'];
92
+ if (className)
93
+ classes.push(className);
94
+ return classes.join(' ');
95
+ }, [className]);
96
+ // Calculate grid template styles for dynamic grid sizes
97
+ const gridStyles = (0, react_1.useMemo)(() => {
98
+ // Base cell size in pixels (for 9x9 grid, this would be 50px, 60px, 70px at different breakpoints)
99
+ const baseCellSizePx = 50; // Base size for small screens
100
+ // Calculate width based on number of columns
101
+ // We use the max of rows/columns to ensure square cells
102
+ const maxDimension = Math.max(numRows, numColumns);
103
+ const width = maxDimension * baseCellSizePx;
104
+ return {
105
+ gridTemplateColumns: `repeat(${numColumns}, 1fr)`,
106
+ gridTemplateRows: `repeat(${numRows}, 1fr)`,
107
+ width: `min(${width}px, 100%)`,
108
+ maxWidth: `${width}px`
109
+ };
110
+ }, [numRows, numColumns]);
111
+ // Calculate border classes based on puzzle type
112
+ const borderClasses = (0, react_1.useMemo)(() => {
113
+ //if (puzzleType === 'killer') {
114
+ // Minimal borders for Killer Sudoku to let cage borders stand out
115
+ //return 'gap-px border border-2 border-gray-300 bg-gray-300';
116
+ //}
117
+ // Standard thick borders for other puzzle types
118
+ return 'border-2 border-black bg-black';
119
+ }, [puzzleType]);
120
+ // Sort cells by row and column for proper display order
121
+ const sortedCells = (0, react_1.useMemo)(() => {
122
+ return [...cells].sort((a, b) => {
123
+ if (a.row !== b.row)
124
+ return a.row - b.row;
125
+ return a.column - b.column;
126
+ });
127
+ }, [cells]);
128
+ // Calculate grid dimensions for overlays
129
+ const gridDimensions = (0, react_1.useMemo)(() => {
130
+ // Base cell size in grid units - we'll scale this with CSS
131
+ const baseCellSize = 1;
132
+ return {
133
+ cellSize: baseCellSize,
134
+ gridSize: {
135
+ width: numColumns * baseCellSize,
136
+ height: numRows * baseCellSize
137
+ }
138
+ };
139
+ }, [numRows, numColumns]);
140
+ // Render diagonal lines for Sudoku X puzzles
141
+ const renderDiagonalLines = () => {
142
+ if (puzzleType !== 'sudoku-x')
143
+ return null;
144
+ return (react_1.default.createElement("div", { className: "absolute inset-0 w-full h-full pointer-events-none", style: {
145
+ background: `
146
+ linear-gradient(45deg, transparent 49%, rgba(156, 163, 175, 0.15) 49.5%, rgba(156, 163, 175, 0.15) 50.5%, transparent 51%),
147
+ linear-gradient(-45deg, transparent 49%, rgba(156, 163, 175, 0.15) 49.5%, rgba(156, 163, 175, 0.15) 50.5%, transparent 51%)
148
+ `,
149
+ zIndex: 1
150
+ } }));
151
+ };
152
+ // Render cage overlay for Killer Sudoku
153
+ const renderCageOverlay = () => {
154
+ if (puzzleType !== 'killer-sudoku' || !cages || cages.length === 0)
155
+ return null;
156
+ return (react_1.default.createElement(CageOverlay_1.CageOverlay, { cages: cages, gridSize: gridDimensions.gridSize, cellSize: gridDimensions.cellSize, numRows: numRows, numColumns: numColumns }));
157
+ };
158
+ return (react_1.default.createElement("div", { className: `
159
+ ${gridClasses}
160
+ relative
161
+ grid ${puzzleType === 'killer-sudoku' ? 'gap-0' : 'gap-0'}
162
+ ${borderClasses}
163
+ mx-auto outline-none
164
+ aspect-square
165
+ `, style: gridStyles, onKeyDown: handleKeyDown, onMouseUp: onDragEnd, onTouchEnd: onDragEnd, role: "grid", "aria-label": "Sudoku puzzle entry grid", tabIndex: 0, "data-testid": "sudoku-grid" },
166
+ renderDiagonalLines(),
167
+ renderCageOverlay(),
168
+ sortedCells.map((cellInfo) => {
169
+ const isPrimarySelected = selectedCell === cellInfo.id;
170
+ const isInMultiSelection = selectedCells.includes(cellInfo.id);
171
+ return (react_1.default.createElement(SudokuCell_1.SudokuCell, { key: cellInfo.id, cellInfo: cellInfo, isSelected: isPrimarySelected || isInMultiSelection, inputMode: inputMode, puzzleDimensions: puzzleDimensions
172
+ ? {
173
+ numRows,
174
+ numColumns,
175
+ cageHeight: puzzleDimensions.cageHeightInCells,
176
+ cageWidth: puzzleDimensions.cageWidthInCells
177
+ }
178
+ : undefined, onSelect: createCellSelectHandler(cellInfo.id), onLongPressToggle: createLongPressToggleHandler(cellInfo.id), onValueChange: createCellValueChangeHandler(cellInfo.id), onNoteToggle: createNoteToggleHandler(cellInfo.id), onClearAllNotes: createClearAllNotesHandler(cellInfo.id), onDragOver: createDragOverHandler(cellInfo.id), className: isInMultiSelection && !isPrimarySelected ? 'sudoku-cell--multi-selected' : undefined }));
179
+ })));
180
+ };
181
+ exports.SudokuGrid = SudokuGrid;
182
+ //# sourceMappingURL=SudokuGrid.js.map