@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,181 @@
1
+ /*
2
+ * MIT License
3
+ *
4
+ * Copyright (c) 2023 Erik Fortune
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in all
14
+ * copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ * SOFTWARE.
23
+ */
24
+ import React, { useCallback, useMemo } from 'react';
25
+ /**
26
+ * Reusable number keypad component for sudoku input
27
+ * @public
28
+ */
29
+ export const NumberKeypad = ({ keypadType, inputMode, isActive, onNumberPress, onClear, disabled = false, className, compact = false }) => {
30
+ // Handle number button press with haptic feedback if available
31
+ const handleNumberPress = useCallback((number) => {
32
+ /* c8 ignore next 1 - not reachable in practice */
33
+ if (disabled)
34
+ return;
35
+ // Trigger haptic feedback on touch devices
36
+ if ('vibrate' in navigator) {
37
+ navigator.vibrate(10); // 10ms vibration
38
+ }
39
+ onNumberPress(number);
40
+ }, [disabled, onNumberPress]);
41
+ // Handle clear button press
42
+ const handleClear = useCallback(() => {
43
+ /* c8 ignore next 1 - not reachable in practice */
44
+ if (disabled)
45
+ return;
46
+ // Trigger haptic feedback on touch devices
47
+ if ('vibrate' in navigator) {
48
+ navigator.vibrate(15); // 15ms vibration for clear
49
+ }
50
+ onClear();
51
+ }, [disabled, onClear]);
52
+ // Calculate keypad classes
53
+ const keypadClasses = useMemo(() => {
54
+ const classes = ['bg-white border border-gray-300 rounded-lg p-3 flex flex-col gap-3'];
55
+ // Keypad type specific styles
56
+ if (keypadType === 'notes') {
57
+ classes.push('border-blue-500');
58
+ }
59
+ else {
60
+ classes.push('border-green-500');
61
+ }
62
+ // Active/inactive styles
63
+ if (isActive) {
64
+ classes.push('shadow-md');
65
+ }
66
+ else {
67
+ classes.push('opacity-75');
68
+ }
69
+ // Compact mode
70
+ if (compact) {
71
+ classes.push('p-2 gap-2');
72
+ }
73
+ // Disabled state
74
+ if (disabled) {
75
+ classes.push('opacity-50 pointer-events-none');
76
+ }
77
+ if (className)
78
+ classes.push(className);
79
+ return classes.join(' ');
80
+ }, [keypadType, isActive, disabled, className, compact]);
81
+ // Calculate title and button classes
82
+ const titleClasses = useMemo(() => {
83
+ const classes = ['text-center font-semibold text-gray-800'];
84
+ if (isActive) {
85
+ classes.push('text-gray-900');
86
+ }
87
+ else {
88
+ classes.push('text-gray-600');
89
+ }
90
+ if (compact) {
91
+ classes.push('text-sm');
92
+ }
93
+ else {
94
+ classes.push('text-base');
95
+ }
96
+ return classes.join(' ');
97
+ }, [isActive, compact]);
98
+ const buttonClasses = useMemo(() => {
99
+ const classes = [
100
+ 'border-2',
101
+ 'rounded-lg',
102
+ 'font-bold',
103
+ 'transition-all',
104
+ 'duration-150',
105
+ 'flex',
106
+ 'items-center',
107
+ 'justify-center',
108
+ 'touch-manipulation'
109
+ ];
110
+ if (compact) {
111
+ classes.push('w-8', 'h-8', 'text-sm');
112
+ }
113
+ else {
114
+ classes.push('w-12', 'h-12', 'text-lg');
115
+ }
116
+ if (isActive) {
117
+ classes.push('bg-blue-500', 'text-white', 'border-blue-600', 'hover:bg-blue-600', 'active:bg-blue-700');
118
+ }
119
+ else {
120
+ classes.push('bg-gray-100', 'text-gray-700', 'border-gray-300', 'hover:bg-gray-200');
121
+ }
122
+ if (disabled) {
123
+ classes.push('opacity-50', 'cursor-not-allowed');
124
+ }
125
+ else {
126
+ classes.push('cursor-pointer');
127
+ }
128
+ return classes.join(' ');
129
+ }, [compact, isActive, disabled]);
130
+ const clearButtonClasses = useMemo(() => {
131
+ const classes = [
132
+ 'border-2',
133
+ 'rounded-lg',
134
+ 'font-bold',
135
+ 'transition-all',
136
+ 'duration-150',
137
+ 'flex',
138
+ 'items-center',
139
+ 'justify-center',
140
+ 'touch-manipulation',
141
+ 'col-span-2'
142
+ ];
143
+ if (compact) {
144
+ classes.push('h-8', 'text-sm');
145
+ }
146
+ else {
147
+ classes.push('h-12', 'text-lg');
148
+ }
149
+ if (isActive) {
150
+ classes.push('bg-red-500', 'text-white', 'border-red-600', 'hover:bg-red-600', 'active:bg-red-700');
151
+ }
152
+ else {
153
+ classes.push('bg-gray-100', 'text-gray-700', 'border-gray-300', 'hover:bg-gray-200');
154
+ }
155
+ if (disabled) {
156
+ classes.push('opacity-50', 'cursor-not-allowed');
157
+ }
158
+ else {
159
+ classes.push('cursor-pointer');
160
+ }
161
+ return classes.join(' ');
162
+ }, [compact, isActive, disabled]);
163
+ const gridClasses = useMemo(() => {
164
+ const classes = ['grid grid-cols-3 auto-rows-fr'];
165
+ if (compact) {
166
+ classes.push('gap-1');
167
+ }
168
+ else {
169
+ classes.push('gap-2');
170
+ }
171
+ return classes.join(' ');
172
+ }, [compact]);
173
+ // Keypad title
174
+ const title = keypadType === 'notes' ? '📝 Notes' : '✏️ Values';
175
+ return (React.createElement("div", { className: keypadClasses, role: "grid", "aria-label": `${keypadType} number keypad`, "data-testid": `number-keypad-${keypadType}` },
176
+ React.createElement("div", { className: titleClasses }, title),
177
+ React.createElement("div", { className: gridClasses, role: "grid" },
178
+ [1, 2, 3, 4, 5, 6, 7, 8, 9].map((number) => (React.createElement("button", { key: number, type: "button", onClick: () => handleNumberPress(number), disabled: disabled, className: buttonClasses, "aria-label": `Enter ${number} ${keypadType === 'notes' ? 'as note' : 'as value'}`, "data-testid": `keypad-${keypadType}-${number}`, onMouseDown: (e) => e.preventDefault() }, number))),
179
+ React.createElement("button", { type: "button", onClick: handleClear, disabled: disabled, className: clearButtonClasses, "aria-label": `Clear ${keypadType}`, "data-testid": `keypad-${keypadType}-clear`, onMouseDown: (e) => e.preventDefault() }, "Clear"))));
180
+ };
181
+ //# sourceMappingURL=NumberKeypad.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NumberKeypad.js","sourceRoot":"","sources":["../../src/components/NumberKeypad.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 } from 'react';\nimport { InputMode } from '../types';\n\n/**\n * Props for the NumberKeypad component\n * @public\n */\nexport interface INumberKeypadProps {\n /** Type of keypad - affects styling and behavior */\n readonly keypadType: 'notes' | 'values';\n\n /** Current input mode */\n readonly inputMode: InputMode;\n\n /** Whether this keypad is currently active */\n readonly isActive: boolean;\n\n /** Callback when a number is pressed */\n readonly onNumberPress: (number: number) => void;\n\n /** Callback when clear is pressed */\n readonly onClear: () => void;\n\n /** Whether the keypad is disabled */\n readonly disabled?: boolean;\n\n /** Additional CSS class name */\n readonly className?: string;\n\n /** Compact mode for smaller screens */\n readonly compact?: boolean;\n}\n\n/**\n * Reusable number keypad component for sudoku input\n * @public\n */\nexport const NumberKeypad: React.FC<INumberKeypadProps> = ({\n keypadType,\n inputMode,\n isActive,\n onNumberPress,\n onClear,\n disabled = false,\n className,\n compact = false\n}) => {\n // Handle number button press with haptic feedback if available\n const handleNumberPress = useCallback(\n (number: number) => {\n /* c8 ignore next 1 - not reachable in practice */\n if (disabled) return;\n\n // Trigger haptic feedback on touch devices\n if ('vibrate' in navigator) {\n navigator.vibrate(10); // 10ms vibration\n }\n\n onNumberPress(number);\n },\n [disabled, onNumberPress]\n );\n\n // Handle clear button press\n const handleClear = useCallback(() => {\n /* c8 ignore next 1 - not reachable in practice */\n if (disabled) return;\n\n // Trigger haptic feedback on touch devices\n if ('vibrate' in navigator) {\n navigator.vibrate(15); // 15ms vibration for clear\n }\n\n onClear();\n }, [disabled, onClear]);\n\n // Calculate keypad classes\n const keypadClasses = useMemo(() => {\n const classes = ['bg-white border border-gray-300 rounded-lg p-3 flex flex-col gap-3'];\n\n // Keypad type specific styles\n if (keypadType === 'notes') {\n classes.push('border-blue-500');\n } else {\n classes.push('border-green-500');\n }\n\n // Active/inactive styles\n if (isActive) {\n classes.push('shadow-md');\n } else {\n classes.push('opacity-75');\n }\n\n // Compact mode\n if (compact) {\n classes.push('p-2 gap-2');\n }\n\n // Disabled state\n if (disabled) {\n classes.push('opacity-50 pointer-events-none');\n }\n\n if (className) classes.push(className);\n\n return classes.join(' ');\n }, [keypadType, isActive, disabled, className, compact]);\n\n // Calculate title and button classes\n const titleClasses = useMemo(() => {\n const classes = ['text-center font-semibold text-gray-800'];\n\n if (isActive) {\n classes.push('text-gray-900');\n } else {\n classes.push('text-gray-600');\n }\n\n if (compact) {\n classes.push('text-sm');\n } else {\n classes.push('text-base');\n }\n\n return classes.join(' ');\n }, [isActive, compact]);\n\n const buttonClasses = useMemo(() => {\n const classes = [\n 'border-2',\n 'rounded-lg',\n 'font-bold',\n 'transition-all',\n 'duration-150',\n 'flex',\n 'items-center',\n 'justify-center',\n 'touch-manipulation'\n ];\n\n if (compact) {\n classes.push('w-8', 'h-8', 'text-sm');\n } else {\n classes.push('w-12', 'h-12', 'text-lg');\n }\n\n if (isActive) {\n classes.push('bg-blue-500', 'text-white', 'border-blue-600', 'hover:bg-blue-600', 'active:bg-blue-700');\n } else {\n classes.push('bg-gray-100', 'text-gray-700', 'border-gray-300', 'hover:bg-gray-200');\n }\n\n if (disabled) {\n classes.push('opacity-50', 'cursor-not-allowed');\n } else {\n classes.push('cursor-pointer');\n }\n\n return classes.join(' ');\n }, [compact, isActive, disabled]);\n\n const clearButtonClasses = useMemo(() => {\n const classes = [\n 'border-2',\n 'rounded-lg',\n 'font-bold',\n 'transition-all',\n 'duration-150',\n 'flex',\n 'items-center',\n 'justify-center',\n 'touch-manipulation',\n 'col-span-2'\n ];\n\n if (compact) {\n classes.push('h-8', 'text-sm');\n } else {\n classes.push('h-12', 'text-lg');\n }\n\n if (isActive) {\n classes.push('bg-red-500', 'text-white', 'border-red-600', 'hover:bg-red-600', 'active:bg-red-700');\n } else {\n classes.push('bg-gray-100', 'text-gray-700', 'border-gray-300', 'hover:bg-gray-200');\n }\n\n if (disabled) {\n classes.push('opacity-50', 'cursor-not-allowed');\n } else {\n classes.push('cursor-pointer');\n }\n\n return classes.join(' ');\n }, [compact, isActive, disabled]);\n\n const gridClasses = useMemo(() => {\n const classes = ['grid grid-cols-3 auto-rows-fr'];\n\n if (compact) {\n classes.push('gap-1');\n } else {\n classes.push('gap-2');\n }\n\n return classes.join(' ');\n }, [compact]);\n\n // Keypad title\n const title = keypadType === 'notes' ? '📝 Notes' : '✏️ Values';\n\n return (\n <div\n className={keypadClasses}\n role=\"grid\"\n aria-label={`${keypadType} number keypad`}\n data-testid={`number-keypad-${keypadType}`}\n >\n {/* Keypad title */}\n <div className={titleClasses}>{title}</div>\n\n {/* Number grid */}\n <div className={gridClasses} role=\"grid\">\n {/* Numbers 1-9 */}\n {[1, 2, 3, 4, 5, 6, 7, 8, 9].map((number) => (\n <button\n key={number}\n type=\"button\"\n onClick={() => handleNumberPress(number)}\n disabled={disabled}\n className={buttonClasses}\n aria-label={`Enter ${number} ${keypadType === 'notes' ? 'as note' : 'as value'}`}\n data-testid={`keypad-${keypadType}-${number}`}\n onMouseDown={(e) => e.preventDefault()} // Prevent focus on click\n >\n {number}\n </button>\n ))}\n\n {/* Clear button spanning 2 columns */}\n <button\n type=\"button\"\n onClick={handleClear}\n disabled={disabled}\n className={clearButtonClasses}\n aria-label={`Clear ${keypadType}`}\n data-testid={`keypad-${keypadType}-clear`}\n onMouseDown={(e) => e.preventDefault()} // Prevent focus on click\n >\n Clear\n </button>\n </div>\n </div>\n );\n};\n"]}
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import { ISudokuCellProps } from '../types';
3
+ /**
4
+ * Individual Sudoku cell component with input handling and validation display
5
+ * @public
6
+ */
7
+ export declare const SudokuCell: React.FC<ISudokuCellProps>;
8
+ //# sourceMappingURL=SudokuCell.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SudokuCell.d.ts","sourceRoot":"","sources":["../../src/components/SudokuCell.tsx"],"names":[],"mappings":"AAwBA,OAAO,KAAuC,MAAM,OAAO,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAG5C;;;GAGG;AACH,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC,EAAE,CAAC,gBAAgB,CAmTjD,CAAC"}
@@ -0,0 +1,240 @@
1
+ /*
2
+ * MIT License
3
+ *
4
+ * Copyright (c) 2023 Erik Fortune
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in all
14
+ * copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ * SOFTWARE.
23
+ */
24
+ import React, { useCallback, useMemo, useRef } from 'react';
25
+ import { useLongPress } from '../hooks/useLongPress';
26
+ /**
27
+ * Individual Sudoku cell component with input handling and validation display
28
+ * @public
29
+ */
30
+ export const SudokuCell = ({ cellInfo, isSelected, inputMode, puzzleDimensions, onSelect, onLongPressToggle, onValueChange, onNoteToggle, onClearAllNotes, onDragOver, className }) => {
31
+ const { id, row, column, contents, isImmutable, hasValidationError } = cellInfo;
32
+ // Double-tap detection for clearing all notes
33
+ const lastClickTimeRef = useRef(0);
34
+ const DOUBLE_TAP_DELAY = 300; // milliseconds
35
+ // Track if a long press just occurred to prevent click interference
36
+ const longPressOccurredRef = useRef(false);
37
+ // Handle keyboard input with notes-first paradigm
38
+ const handleKeyDown = useCallback((event) => {
39
+ if (isImmutable)
40
+ return;
41
+ const key = event.key;
42
+ if (key >= '1' && key <= '9') {
43
+ const value = parseInt(key, 10);
44
+ // Check if modifier keys are pressed to override current input mode
45
+ const hasModifierKey = event.shiftKey || event.ctrlKey || event.metaKey;
46
+ if (hasModifierKey) {
47
+ // Modifier key held - always place value (override current mode)
48
+ onValueChange(value);
49
+ }
50
+ else if (inputMode === 'value') {
51
+ // Value mode - place value
52
+ onValueChange(value);
53
+ }
54
+ else {
55
+ // Notes mode (default) - toggle note
56
+ onNoteToggle(value);
57
+ }
58
+ event.preventDefault();
59
+ }
60
+ else if (key === 'Delete' || key === 'Backspace') {
61
+ if (contents.value) {
62
+ // Clear cell value
63
+ onValueChange(undefined);
64
+ }
65
+ else if (contents.notes.length > 0) {
66
+ // Clear all notes
67
+ onClearAllNotes();
68
+ }
69
+ event.preventDefault();
70
+ }
71
+ else if (key === '0') {
72
+ // Clear everything
73
+ if (contents.value) {
74
+ onValueChange(undefined);
75
+ }
76
+ if (contents.notes.length > 0) {
77
+ onClearAllNotes();
78
+ }
79
+ event.preventDefault();
80
+ }
81
+ // Arrow key navigation is handled by the parent component
82
+ }, [
83
+ isImmutable,
84
+ inputMode,
85
+ onValueChange,
86
+ onNoteToggle,
87
+ onClearAllNotes,
88
+ contents.value,
89
+ contents.notes.length
90
+ ]);
91
+ // Handle long press for multiselect toggle
92
+ const handleLongPress = useCallback((event) => {
93
+ if (onLongPressToggle && !isImmutable) {
94
+ longPressOccurredRef.current = true;
95
+ onLongPressToggle(event);
96
+ }
97
+ }, [onLongPressToggle, isImmutable]);
98
+ // Set up long press detection
99
+ const longPressProps = useLongPress({
100
+ onLongPress: handleLongPress,
101
+ delay: 500,
102
+ shouldPreventDefault: false
103
+ });
104
+ // Handle mouse click with double-tap detection
105
+ const handleClick = useCallback((event) => {
106
+ // If a long press just occurred, ignore this click event
107
+ if (longPressOccurredRef.current) {
108
+ longPressOccurredRef.current = false; // Reset for next interaction
109
+ event.preventDefault();
110
+ return;
111
+ }
112
+ // Prevent default context menu on Ctrl+click
113
+ if (event.ctrlKey || event.metaKey) {
114
+ event.preventDefault();
115
+ }
116
+ const currentTime = Date.now();
117
+ const timeSinceLastClick = currentTime - lastClickTimeRef.current;
118
+ if (timeSinceLastClick < DOUBLE_TAP_DELAY && !isImmutable && contents.notes.length > 0) {
119
+ // Double-tap detected - clear all notes
120
+ onClearAllNotes();
121
+ }
122
+ else {
123
+ // Single click - select cell (pass event for multi-select)
124
+ onSelect(event);
125
+ }
126
+ lastClickTimeRef.current = currentTime;
127
+ }, [onSelect, onClearAllNotes, isImmutable, contents.notes.length]);
128
+ // Handle mouse up - need to check if long press just occurred
129
+ const handleMouseUp = useCallback((event) => {
130
+ longPressProps.onMouseUp(event);
131
+ // Don't reset the flag here - let the click handler handle it
132
+ // The flag will be reset in handleClick or handleMouseLeave
133
+ }, [longPressProps]);
134
+ // Handle touch end - need to check if long press just occurred
135
+ const handleTouchEnd = useCallback((event) => {
136
+ longPressProps.onTouchEnd(event);
137
+ // Don't reset the flag here - let the click handler handle it
138
+ // The flag will be reset in handleClick or handleMouseLeave
139
+ }, [longPressProps]);
140
+ // Handle mouse leave - reset long press flag
141
+ const handleMouseLeave = useCallback((event) => {
142
+ longPressOccurredRef.current = false;
143
+ longPressProps.onMouseLeave(event);
144
+ }, [longPressProps]);
145
+ // Calculate CSS classes
146
+ const cellClasses = useMemo(() => {
147
+ const classes = [
148
+ // Base cell styles - fill grid cell completely
149
+ 'w-full',
150
+ 'h-full',
151
+ 'border',
152
+ 'border-gray-300',
153
+ 'bg-white',
154
+ 'text-blue-800',
155
+ 'text-base',
156
+ 'md:text-lg',
157
+ 'lg:text-xl',
158
+ 'font-bold',
159
+ 'flex',
160
+ 'items-center',
161
+ 'justify-center',
162
+ 'cursor-pointer',
163
+ 'outline-none',
164
+ 'relative',
165
+ 'select-none',
166
+ 'touch-manipulation',
167
+ 'transition-all',
168
+ 'duration-150',
169
+ 'overflow-hidden',
170
+ 'p-0'
171
+ ];
172
+ // Hover effects - apply before selection so selection can override
173
+ if (!isImmutable && !isSelected) {
174
+ classes.push('hover:bg-gray-50', 'hover:border-blue-500');
175
+ }
176
+ if (hasValidationError && !isSelected) {
177
+ classes.push('hover:bg-red-50', 'hover:border-red-600');
178
+ }
179
+ // State-based styles - Applied after hover so they take precedence
180
+ if (isSelected) {
181
+ if (className?.includes('multi-selected')) {
182
+ // Multi-Selected: More distinct blue background with darker border
183
+ classes.push('bg-blue-200', 'border-blue-500', '!bg-blue-200');
184
+ }
185
+ else {
186
+ // Single Selected: Darker blue background for clear distinction
187
+ classes.push('bg-blue-300', 'border-blue-600', '!bg-blue-300');
188
+ }
189
+ }
190
+ if (hasValidationError) {
191
+ classes.push('bg-red-50', 'text-red-800', 'border-red-500');
192
+ }
193
+ if (isImmutable) {
194
+ classes.push('bg-gray-100', 'cursor-default', 'text-gray-900');
195
+ }
196
+ if (contents.value) {
197
+ classes.push('font-black');
198
+ }
199
+ // Enhanced section boundaries - dynamic based on cage dimensions
200
+ const cageWidth = puzzleDimensions?.cageWidth ?? 3;
201
+ const cageHeight = puzzleDimensions?.cageHeight ?? 3;
202
+ const numRows = puzzleDimensions?.numRows ?? 9;
203
+ const numColumns = puzzleDimensions?.numColumns ?? 9;
204
+ if ((column + 1) % cageWidth === 0 && column < numColumns - 1) {
205
+ classes.push('border-r-2', 'border-r-black');
206
+ }
207
+ if ((row + 1) % cageHeight === 0 && row < numRows - 1) {
208
+ classes.push('border-b-2', 'border-b-black');
209
+ }
210
+ // Focus styles
211
+ classes.push('focus-visible:ring-2', 'focus-visible:ring-blue-500', 'focus-visible:ring-offset-1');
212
+ if (className)
213
+ classes.push(className);
214
+ return classes.join(' ');
215
+ }, [isSelected, hasValidationError, isImmutable, contents.value, row, column, className, puzzleDimensions]);
216
+ // Format display value
217
+ const displayValue = contents.value?.toString() || '';
218
+ // Render notes in fixed 3x3 grid layout - each number has fixed position
219
+ const renderNotesGrid = useCallback(() => {
220
+ /* c8 ignore next 1 - functional code tested but coverage intermittently missed */
221
+ if (contents.value)
222
+ return null; // Only show notes when no value is present
223
+ // Create a 3x3 grid for notes (positions 1-9) with fixed layout
224
+ // Only render notes that are actually selected (completely invisible otherwise)
225
+ const notesGrid = Array(9)
226
+ .fill(null)
227
+ .map((__value, index) => {
228
+ const noteValue = index + 1;
229
+ const hasNote = contents.notes.includes(noteValue);
230
+ return (React.createElement("div", { key: noteValue, className: "flex items-center justify-center" }, hasNote && (React.createElement("span", { className: "text-[0.5rem] md:text-[0.55rem] lg:text-[0.6rem] font-normal leading-none text-gray-600" }, noteValue))));
231
+ });
232
+ return (React.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));
233
+ }, [contents.value, contents.notes]);
234
+ return (React.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
235
+ ? contents.value.toString()
236
+ : contents.notes.length > 0
237
+ ? `notes: ${contents.notes.join(', ')}`
238
+ : 'empty'}`, "aria-selected": isSelected }, contents.value ? React.createElement("span", { className: "text-inherit font-inherit" }, displayValue) : renderNotesGrid()));
239
+ };
240
+ //# 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,8 @@
1
+ import React from 'react';
2
+ import { ISudokuControlsProps } from '../types';
3
+ /**
4
+ * Control panel for grid-wide operations
5
+ * @public
6
+ */
7
+ export declare const SudokuControls: React.FC<ISudokuControlsProps>;
8
+ //# sourceMappingURL=SudokuControls.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SudokuControls.d.ts","sourceRoot":"","sources":["../../src/components/SudokuControls.tsx"],"names":[],"mappings":"AAwBA,OAAO,KAAyC,MAAM,OAAO,CAAC;AAC9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAGhD;;;GAGG;AACH,eAAO,MAAM,cAAc,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CA0NzD,CAAC"}