@fgv/ts-sudoku-ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.rush/temp/chunked-rush-logs/ts-sudoku-ui.build.chunks.jsonl +14 -0
- package/.rush/temp/operation/build/log-chunks.jsonl +14 -0
- package/.rush/temp/operation/build/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +1086 -0
- package/.test-failures-catalog.md +470 -0
- package/CSS_MODULES_MIGRATION_SUMMARY.md +189 -0
- package/dist/ts-sudoku-ui.d.ts +668 -0
- package/etc/ts-sudoku-ui.api.md +481 -0
- package/lib/components/CageOverlay.d.ts +8 -0
- package/lib/components/CageOverlay.d.ts.map +1 -0
- package/lib/components/CageOverlay.js +133 -0
- package/lib/components/CageOverlay.js.map +1 -0
- package/lib/components/CageSumIndicator.d.ts +8 -0
- package/lib/components/CageSumIndicator.d.ts.map +1 -0
- package/lib/components/CageSumIndicator.js +66 -0
- package/lib/components/CageSumIndicator.js.map +1 -0
- package/lib/components/CombinationCard.d.ts +8 -0
- package/lib/components/CombinationCard.d.ts.map +1 -0
- package/lib/components/CombinationCard.js +64 -0
- package/lib/components/CombinationCard.js.map +1 -0
- package/lib/components/CombinationGrid.d.ts +8 -0
- package/lib/components/CombinationGrid.d.ts.map +1 -0
- package/lib/components/CombinationGrid.js +51 -0
- package/lib/components/CombinationGrid.js.map +1 -0
- package/lib/components/CompactControlRibbon.d.ts +45 -0
- package/lib/components/CompactControlRibbon.d.ts.map +1 -0
- package/lib/components/CompactControlRibbon.js +141 -0
- package/lib/components/CompactControlRibbon.js.map +1 -0
- package/lib/components/DualKeypad.d.ts +39 -0
- package/lib/components/DualKeypad.d.ts.map +1 -0
- package/lib/components/DualKeypad.js +137 -0
- package/lib/components/DualKeypad.js.map +1 -0
- package/lib/components/KillerCombinationsExplorer.d.ts +9 -0
- package/lib/components/KillerCombinationsExplorer.d.ts.map +1 -0
- package/lib/components/KillerCombinationsExplorer.js +69 -0
- package/lib/components/KillerCombinationsExplorer.js.map +1 -0
- package/lib/components/KillerCombinationsModal.d.ts +20 -0
- package/lib/components/KillerCombinationsModal.d.ts.map +1 -0
- package/lib/components/KillerCombinationsModal.js +135 -0
- package/lib/components/KillerCombinationsModal.js.map +1 -0
- package/lib/components/KillerCombinationsPanel.d.ts +20 -0
- package/lib/components/KillerCombinationsPanel.d.ts.map +1 -0
- package/lib/components/KillerCombinationsPanel.js +79 -0
- package/lib/components/KillerCombinationsPanel.js.map +1 -0
- package/lib/components/NumberKeypad.d.ts +30 -0
- package/lib/components/NumberKeypad.d.ts.map +1 -0
- package/lib/components/NumberKeypad.js +181 -0
- package/lib/components/NumberKeypad.js.map +1 -0
- package/lib/components/SudokuCell.d.ts +8 -0
- package/lib/components/SudokuCell.d.ts.map +1 -0
- package/lib/components/SudokuCell.js +240 -0
- package/lib/components/SudokuCell.js.map +1 -0
- package/lib/components/SudokuControls.d.ts +8 -0
- package/lib/components/SudokuControls.d.ts.map +1 -0
- package/lib/components/SudokuControls.js +140 -0
- package/lib/components/SudokuControls.js.map +1 -0
- package/lib/components/SudokuGrid.d.ts +8 -0
- package/lib/components/SudokuGrid.d.ts.map +1 -0
- package/lib/components/SudokuGrid.js +177 -0
- package/lib/components/SudokuGrid.js.map +1 -0
- package/lib/components/SudokuGridEntry.d.ts +8 -0
- package/lib/components/SudokuGridEntry.d.ts.map +1 -0
- package/lib/components/SudokuGridEntry.js +373 -0
- package/lib/components/SudokuGridEntry.js.map +1 -0
- package/lib/components/ValidationDisplay.d.ts +8 -0
- package/lib/components/ValidationDisplay.d.ts.map +1 -0
- package/lib/components/ValidationDisplay.js +95 -0
- package/lib/components/ValidationDisplay.js.map +1 -0
- package/lib/contexts/DiagnosticLoggerContext.d.ts +81 -0
- package/lib/contexts/DiagnosticLoggerContext.d.ts.map +1 -0
- package/lib/contexts/DiagnosticLoggerContext.js +98 -0
- package/lib/contexts/DiagnosticLoggerContext.js.map +1 -0
- package/lib/contexts/index.d.ts +3 -0
- package/lib/contexts/index.d.ts.map +1 -0
- package/lib/contexts/index.js +25 -0
- package/lib/contexts/index.js.map +1 -0
- package/lib/hooks/useCombinationElimination.d.ts +10 -0
- package/lib/hooks/useCombinationElimination.d.ts.map +1 -0
- package/lib/hooks/useCombinationElimination.js +123 -0
- package/lib/hooks/useCombinationElimination.js.map +1 -0
- package/lib/hooks/useKeyboardShortcut.d.ts +19 -0
- package/lib/hooks/useKeyboardShortcut.d.ts.map +1 -0
- package/lib/hooks/useKeyboardShortcut.js +55 -0
- package/lib/hooks/useKeyboardShortcut.js.map +1 -0
- package/lib/hooks/useKillerCombinations.d.ts +11 -0
- package/lib/hooks/useKillerCombinations.d.ts.map +1 -0
- package/lib/hooks/useKillerCombinations.js +65 -0
- package/lib/hooks/useKillerCombinations.js.map +1 -0
- package/lib/hooks/useLongPress.d.ts +31 -0
- package/lib/hooks/useLongPress.d.ts.map +1 -0
- package/lib/hooks/useLongPress.js +69 -0
- package/lib/hooks/useLongPress.js.map +1 -0
- package/lib/hooks/usePuzzleSession.d.ts +32 -0
- package/lib/hooks/usePuzzleSession.d.ts.map +1 -0
- package/lib/hooks/usePuzzleSession.js +398 -0
- package/lib/hooks/usePuzzleSession.js.map +1 -0
- package/lib/hooks/useResponsiveLayout.d.ts +44 -0
- package/lib/hooks/useResponsiveLayout.d.ts.map +1 -0
- package/lib/hooks/useResponsiveLayout.js +139 -0
- package/lib/hooks/useResponsiveLayout.js.map +1 -0
- package/lib/index.browser.d.ts +2 -0
- package/lib/index.browser.d.ts.map +1 -0
- package/lib/index.browser.js +26 -0
- package/lib/index.browser.js.map +1 -0
- package/lib/index.d.ts +28 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +49 -0
- package/lib/index.js.map +1 -0
- package/lib/test/fixtures/killerSudokuPuzzles.d.ts +36 -0
- package/lib/test/fixtures/killerSudokuPuzzles.d.ts.map +1 -0
- package/lib/test/fixtures/killerSudokuPuzzles.js +57 -0
- package/lib/test/fixtures/killerSudokuPuzzles.js.map +1 -0
- package/lib/test/unit/components/SudokuGridEntry.testHelpers.d.ts +22 -0
- package/lib/test/unit/components/SudokuGridEntry.testHelpers.d.ts.map +1 -0
- package/lib/test/unit/components/SudokuGridEntry.testHelpers.js +116 -0
- package/lib/test/unit/components/SudokuGridEntry.testHelpers.js.map +1 -0
- package/lib/tsdoc-metadata.json +11 -0
- package/lib/types/index.d.ts +234 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +25 -0
- package/lib/types/index.js.map +1 -0
- package/lib/utils/CageLookupManager.d.ts +74 -0
- package/lib/utils/CageLookupManager.d.ts.map +1 -0
- package/lib/utils/CageLookupManager.js +283 -0
- package/lib/utils/CageLookupManager.js.map +1 -0
- package/lib/utils/CagePatternManager.d.ts +82 -0
- package/lib/utils/CagePatternManager.d.ts.map +1 -0
- package/lib/utils/CagePatternManager.js +139 -0
- package/lib/utils/CagePatternManager.js.map +1 -0
- package/lib/utils/puzzleDefinitionHelper.d.ts +25 -0
- package/lib/utils/puzzleDefinitionHelper.d.ts.map +1 -0
- package/lib/utils/puzzleDefinitionHelper.js +99 -0
- package/lib/utils/puzzleDefinitionHelper.js.map +1 -0
- package/lib-commonjs/components/CageOverlay.js +138 -0
- package/lib-commonjs/components/CageOverlay.js.map +1 -0
- package/lib-commonjs/components/CageSumIndicator.js +71 -0
- package/lib-commonjs/components/CageSumIndicator.js.map +1 -0
- package/lib-commonjs/components/CombinationCard.js +68 -0
- package/lib-commonjs/components/CombinationCard.js.map +1 -0
- package/lib-commonjs/components/CombinationGrid.js +56 -0
- package/lib-commonjs/components/CombinationGrid.js.map +1 -0
- package/lib-commonjs/components/CompactControlRibbon.js +146 -0
- package/lib-commonjs/components/CompactControlRibbon.js.map +1 -0
- package/lib-commonjs/components/DualKeypad.js +142 -0
- package/lib-commonjs/components/DualKeypad.js.map +1 -0
- package/lib-commonjs/components/KillerCombinationsExplorer.js +74 -0
- package/lib-commonjs/components/KillerCombinationsExplorer.js.map +1 -0
- package/lib-commonjs/components/KillerCombinationsModal.js +140 -0
- package/lib-commonjs/components/KillerCombinationsModal.js.map +1 -0
- package/lib-commonjs/components/KillerCombinationsPanel.js +84 -0
- package/lib-commonjs/components/KillerCombinationsPanel.js.map +1 -0
- package/lib-commonjs/components/NumberKeypad.js +186 -0
- package/lib-commonjs/components/NumberKeypad.js.map +1 -0
- package/lib-commonjs/components/SudokuCell.js +245 -0
- package/lib-commonjs/components/SudokuCell.js.map +1 -0
- package/lib-commonjs/components/SudokuControls.js +145 -0
- package/lib-commonjs/components/SudokuControls.js.map +1 -0
- package/lib-commonjs/components/SudokuGrid.js +182 -0
- package/lib-commonjs/components/SudokuGrid.js.map +1 -0
- package/lib-commonjs/components/SudokuGridEntry.js +378 -0
- package/lib-commonjs/components/SudokuGridEntry.js.map +1 -0
- package/lib-commonjs/components/ValidationDisplay.js +100 -0
- package/lib-commonjs/components/ValidationDisplay.js.map +1 -0
- package/lib-commonjs/contexts/DiagnosticLoggerContext.js +104 -0
- package/lib-commonjs/contexts/DiagnosticLoggerContext.js.map +1 -0
- package/lib-commonjs/contexts/index.js +32 -0
- package/lib-commonjs/contexts/index.js.map +1 -0
- package/lib-commonjs/hooks/useCombinationElimination.js +126 -0
- package/lib-commonjs/hooks/useCombinationElimination.js.map +1 -0
- package/lib-commonjs/hooks/useKeyboardShortcut.js +58 -0
- package/lib-commonjs/hooks/useKeyboardShortcut.js.map +1 -0
- package/lib-commonjs/hooks/useKillerCombinations.js +68 -0
- package/lib-commonjs/hooks/useKillerCombinations.js.map +1 -0
- package/lib-commonjs/hooks/useLongPress.js +72 -0
- package/lib-commonjs/hooks/useLongPress.js.map +1 -0
- package/lib-commonjs/hooks/usePuzzleSession.js +401 -0
- package/lib-commonjs/hooks/usePuzzleSession.js.map +1 -0
- package/lib-commonjs/hooks/useResponsiveLayout.js +142 -0
- package/lib-commonjs/hooks/useResponsiveLayout.js.map +1 -0
- package/lib-commonjs/index.browser.js +29 -0
- package/lib-commonjs/index.browser.js.map +1 -0
- package/lib-commonjs/index.js +73 -0
- package/lib-commonjs/index.js.map +1 -0
- package/lib-commonjs/test/fixtures/killerSudokuPuzzles.js +60 -0
- package/lib-commonjs/test/fixtures/killerSudokuPuzzles.js.map +1 -0
- package/lib-commonjs/test/unit/components/SudokuGridEntry.testHelpers.js +125 -0
- package/lib-commonjs/test/unit/components/SudokuGridEntry.testHelpers.js.map +1 -0
- package/lib-commonjs/types/index.js +26 -0
- package/lib-commonjs/types/index.js.map +1 -0
- package/lib-commonjs/utils/CageLookupManager.js +287 -0
- package/lib-commonjs/utils/CageLookupManager.js.map +1 -0
- package/lib-commonjs/utils/CagePatternManager.js +143 -0
- package/lib-commonjs/utils/CagePatternManager.js.map +1 -0
- package/lib-commonjs/utils/puzzleDefinitionHelper.js +103 -0
- package/lib-commonjs/utils/puzzleDefinitionHelper.js.map +1 -0
- package/package.json +91 -0
|
@@ -0,0 +1,69 @@
|
|
|
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 { useRef, useCallback } from 'react';
|
|
25
|
+
/**
|
|
26
|
+
* Hook to detect long press gestures on touch and mouse devices
|
|
27
|
+
* @param options - Configuration options for long press detection
|
|
28
|
+
* @returns Event handlers to spread on target element
|
|
29
|
+
* @public
|
|
30
|
+
*/
|
|
31
|
+
export function useLongPress(options) {
|
|
32
|
+
const { onLongPress, delay = 500, shouldPreventDefault = true } = options;
|
|
33
|
+
const timeoutRef = useRef(undefined);
|
|
34
|
+
const isLongPressRef = useRef(false);
|
|
35
|
+
const eventRef = useRef(undefined);
|
|
36
|
+
const start = useCallback((event) => {
|
|
37
|
+
eventRef.current = event;
|
|
38
|
+
isLongPressRef.current = false;
|
|
39
|
+
if (shouldPreventDefault) {
|
|
40
|
+
event.preventDefault();
|
|
41
|
+
}
|
|
42
|
+
timeoutRef.current = setTimeout(() => {
|
|
43
|
+
isLongPressRef.current = true;
|
|
44
|
+
if (eventRef.current) {
|
|
45
|
+
onLongPress(eventRef.current);
|
|
46
|
+
}
|
|
47
|
+
}, delay);
|
|
48
|
+
}, [onLongPress, delay, shouldPreventDefault]);
|
|
49
|
+
const cancel = useCallback(() => {
|
|
50
|
+
if (timeoutRef.current) {
|
|
51
|
+
clearTimeout(timeoutRef.current);
|
|
52
|
+
timeoutRef.current = undefined;
|
|
53
|
+
}
|
|
54
|
+
isLongPressRef.current = false;
|
|
55
|
+
}, []);
|
|
56
|
+
const end = useCallback(() => {
|
|
57
|
+
const wasLongPress = isLongPressRef.current;
|
|
58
|
+
cancel();
|
|
59
|
+
return wasLongPress;
|
|
60
|
+
}, [cancel]);
|
|
61
|
+
return {
|
|
62
|
+
onMouseDown: start,
|
|
63
|
+
onMouseUp: end,
|
|
64
|
+
onMouseLeave: cancel,
|
|
65
|
+
onTouchStart: start,
|
|
66
|
+
onTouchEnd: end
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=useLongPress.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useLongPress.js","sourceRoot":"","sources":["../../src/hooks/useLongPress.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AA6B5C;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,OAA6B;IACxD,MAAM,EAAE,WAAW,EAAE,KAAK,GAAG,GAAG,EAAE,oBAAoB,GAAG,IAAI,EAAE,GAAG,OAAO,CAAC;IAE1E,MAAM,UAAU,GAAG,MAAM,CAA6B,SAAS,CAAC,CAAC;IACjE,MAAM,cAAc,GAAG,MAAM,CAAU,KAAK,CAAC,CAAC;IAC9C,MAAM,QAAQ,GAAG,MAAM,CAAkD,SAAS,CAAC,CAAC;IAEpF,MAAM,KAAK,GAAG,WAAW,CACvB,CAAC,KAA0C,EAAE,EAAE;QAC7C,QAAQ,CAAC,OAAO,GAAG,KAAK,CAAC;QACzB,cAAc,CAAC,OAAO,GAAG,KAAK,CAAC;QAE/B,IAAI,oBAAoB,EAAE,CAAC;YACzB,KAAK,CAAC,cAAc,EAAE,CAAC;QACzB,CAAC;QAED,UAAU,CAAC,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE;YACnC,cAAc,CAAC,OAAO,GAAG,IAAI,CAAC;YAC9B,IAAI,QAAQ,CAAC,OAAO,EAAE,CAAC;gBACrB,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;YAChC,CAAC;QACH,CAAC,EAAE,KAAK,CAAC,CAAC;IACZ,CAAC,EACD,CAAC,WAAW,EAAE,KAAK,EAAE,oBAAoB,CAAC,CAC3C,CAAC;IAEF,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,EAAE;QAC9B,IAAI,UAAU,CAAC,OAAO,EAAE,CAAC;YACvB,YAAY,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;YACjC,UAAU,CAAC,OAAO,GAAG,SAAS,CAAC;QACjC,CAAC;QACD,cAAc,CAAC,OAAO,GAAG,KAAK,CAAC;IACjC,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,GAAG,GAAG,WAAW,CAAC,GAAG,EAAE;QAC3B,MAAM,YAAY,GAAG,cAAc,CAAC,OAAO,CAAC;QAC5C,MAAM,EAAE,CAAC;QACT,OAAO,YAAY,CAAC;IACtB,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAEb,OAAO;QACL,WAAW,EAAE,KAAK;QAClB,SAAS,EAAE,GAAG;QACd,YAAY,EAAE,MAAM;QACpB,YAAY,EAAE,KAAK;QACnB,UAAU,EAAE,GAAG;KAChB,CAAC;AACJ,CAAC","sourcesContent":["/*\n * MIT License\n *\n * Copyright (c) 2023 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport { useRef, useCallback } from 'react';\n\n/**\n * Options for the useLongPress hook\n * @public\n */\nexport interface IUseLongPressOptions {\n /** Callback to execute on long press */\n readonly onLongPress: (event: React.TouchEvent | React.MouseEvent) => void;\n\n /** Delay before long press is triggered (ms) */\n readonly delay?: number;\n\n /** Whether to prevent default behavior during long press */\n readonly shouldPreventDefault?: boolean;\n}\n\n/**\n * Result of the useLongPress hook\n * @public\n */\nexport interface ILongPressHandlers {\n readonly onMouseDown: (event: React.MouseEvent) => void;\n readonly onMouseUp: (event: React.MouseEvent) => boolean;\n readonly onMouseLeave: (event: React.MouseEvent) => void;\n readonly onTouchStart: (event: React.TouchEvent) => void;\n readonly onTouchEnd: (event: React.TouchEvent) => boolean;\n}\n\n/**\n * Hook to detect long press gestures on touch and mouse devices\n * @param options - Configuration options for long press detection\n * @returns Event handlers to spread on target element\n * @public\n */\nexport function useLongPress(options: IUseLongPressOptions): ILongPressHandlers {\n const { onLongPress, delay = 500, shouldPreventDefault = true } = options;\n\n const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);\n const isLongPressRef = useRef<boolean>(false);\n const eventRef = useRef<React.TouchEvent | React.MouseEvent | undefined>(undefined);\n\n const start = useCallback(\n (event: React.TouchEvent | React.MouseEvent) => {\n eventRef.current = event;\n isLongPressRef.current = false;\n\n if (shouldPreventDefault) {\n event.preventDefault();\n }\n\n timeoutRef.current = setTimeout(() => {\n isLongPressRef.current = true;\n if (eventRef.current) {\n onLongPress(eventRef.current);\n }\n }, delay);\n },\n [onLongPress, delay, shouldPreventDefault]\n );\n\n const cancel = useCallback(() => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n timeoutRef.current = undefined;\n }\n isLongPressRef.current = false;\n }, []);\n\n const end = useCallback(() => {\n const wasLongPress = isLongPressRef.current;\n cancel();\n return wasLongPress;\n }, [cancel]);\n\n return {\n onMouseDown: start,\n onMouseUp: end,\n onMouseLeave: cancel,\n onTouchStart: start,\n onTouchEnd: end\n };\n}\n"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { PuzzleSession, CellId, NavigationDirection, NavigationWrap, IPuzzleDefinition } from '@fgv/ts-sudoku-lib';
|
|
2
|
+
import { IValidationError, ICellDisplayInfo, InputMode, ICageDisplayInfo } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* Hook for managing puzzle session state and operations
|
|
5
|
+
* @public
|
|
6
|
+
*/
|
|
7
|
+
export declare function usePuzzleSession(initialPuzzleDescription?: IPuzzleDefinition): {
|
|
8
|
+
session: PuzzleSession | null;
|
|
9
|
+
selectedCell: CellId | null;
|
|
10
|
+
selectedCells: CellId[];
|
|
11
|
+
inputMode: InputMode;
|
|
12
|
+
setSelectedCell: (cell: CellId | null) => void;
|
|
13
|
+
setSelectedCells: (cells: CellId[]) => void;
|
|
14
|
+
setInputMode: (mode: InputMode) => void;
|
|
15
|
+
cellDisplayInfo: ICellDisplayInfo[];
|
|
16
|
+
cageDisplayInfo: ICageDisplayInfo[];
|
|
17
|
+
validationErrors: IValidationError[];
|
|
18
|
+
isValid: boolean;
|
|
19
|
+
isSolved: boolean;
|
|
20
|
+
canUndo: boolean;
|
|
21
|
+
canRedo: boolean;
|
|
22
|
+
error: string | null;
|
|
23
|
+
updateCellValue: (cellId: CellId, value: number | undefined) => void;
|
|
24
|
+
toggleCellNote: (cellId: CellId, note: number) => void;
|
|
25
|
+
clearCellNotes: (cellId: CellId) => void;
|
|
26
|
+
navigateToCell: (direction: NavigationDirection, wrap?: NavigationWrap) => void;
|
|
27
|
+
undo: () => void;
|
|
28
|
+
redo: () => void;
|
|
29
|
+
reset: () => void;
|
|
30
|
+
exportPuzzle: () => IPuzzleDefinition | null;
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=usePuzzleSession.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usePuzzleSession.d.ts","sourceRoot":"","sources":["../../src/hooks/usePuzzleSession.ts"],"names":[],"mappings":"AAyBA,OAAO,EACL,aAAa,EACb,MAAM,EACN,mBAAmB,EACnB,cAAc,EAEd,iBAAiB,EAClB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAG3F;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,wBAAwB,CAAC,EAAE,iBAAiB,GAAG;IAC9E,OAAO,EAAE,aAAa,GAAG,IAAI,CAAC;IAC9B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,SAAS,EAAE,SAAS,CAAC;IACrB,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC/C,gBAAgB,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IAC5C,YAAY,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC;IACxC,eAAe,EAAE,gBAAgB,EAAE,CAAC;IACpC,eAAe,EAAE,gBAAgB,EAAE,CAAC;IACpC,gBAAgB,EAAE,gBAAgB,EAAE,CAAC;IACrC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,eAAe,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC;IACrE,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACvD,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,cAAc,EAAE,CAAC,SAAS,EAAE,mBAAmB,EAAE,IAAI,CAAC,EAAE,cAAc,KAAK,IAAI,CAAC;IAChF,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,YAAY,EAAE,MAAM,iBAAiB,GAAG,IAAI,CAAC;CAC9C,CAsaA"}
|
|
@@ -0,0 +1,398 @@
|
|
|
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 { useCallback, useEffect, useMemo, useState } from 'react';
|
|
25
|
+
import { PuzzleSession, Puzzles } from '@fgv/ts-sudoku-lib';
|
|
26
|
+
import { createEmptyPuzzleDefinition, createPuzzleDefinition } from '../utils/puzzleDefinitionHelper';
|
|
27
|
+
/**
|
|
28
|
+
* Hook for managing puzzle session state and operations
|
|
29
|
+
* @public
|
|
30
|
+
*/
|
|
31
|
+
export function usePuzzleSession(initialPuzzleDescription) {
|
|
32
|
+
const [session, setSession] = useState(null);
|
|
33
|
+
const [puzzleDescription, setPuzzleDescription] = useState(null);
|
|
34
|
+
const [selectedCell, setSelectedCell] = useState(null);
|
|
35
|
+
const [selectedCells, setSelectedCells] = useState([]);
|
|
36
|
+
const [inputMode, setInputMode] = useState('notes'); // Notes-first paradigm
|
|
37
|
+
const [error, setError] = useState(null);
|
|
38
|
+
const [updateCounter, setUpdateCounter] = useState(0); // Force re-renders
|
|
39
|
+
// Initialize or update puzzle session
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
// Use the provided description or create a default empty 9x9 puzzle
|
|
42
|
+
const puzzleDescriptor = initialPuzzleDescription || createEmptyPuzzleDefinition();
|
|
43
|
+
const sessionResult = Puzzles.Any.create(puzzleDescriptor).onSuccess((puzzle) => PuzzleSession.create(puzzle));
|
|
44
|
+
if (sessionResult.isSuccess()) {
|
|
45
|
+
setSession(sessionResult.value);
|
|
46
|
+
setPuzzleDescription(puzzleDescriptor);
|
|
47
|
+
setError(null);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
setError(`Failed to create puzzle session: ${sessionResult.message}`);
|
|
51
|
+
setSession(null);
|
|
52
|
+
setPuzzleDescription(null);
|
|
53
|
+
}
|
|
54
|
+
}, [initialPuzzleDescription]);
|
|
55
|
+
// Get validation errors
|
|
56
|
+
const validationErrors = useMemo(() => {
|
|
57
|
+
if (!session)
|
|
58
|
+
return [];
|
|
59
|
+
const errors = [];
|
|
60
|
+
const invalidCells = session.getInvalidCells();
|
|
61
|
+
invalidCells.forEach((cell) => {
|
|
62
|
+
const contentsResult = session.state.getCellContents(cell.id);
|
|
63
|
+
if (contentsResult.isSuccess() && contentsResult.value.value) {
|
|
64
|
+
const contents = contentsResult.value;
|
|
65
|
+
// Check for duplicates in row, column, and section
|
|
66
|
+
const rowCells = session.rows[cell.row];
|
|
67
|
+
const colCells = session.cols[cell.col];
|
|
68
|
+
// Calculate section index based on puzzle dimensions
|
|
69
|
+
const cageHeight = session.puzzle.dimensions.cageHeightInCells;
|
|
70
|
+
const cageWidth = session.puzzle.dimensions.cageWidthInCells;
|
|
71
|
+
const sectionRow = Math.floor(cell.row / cageHeight);
|
|
72
|
+
const sectionCol = Math.floor(cell.col / cageWidth);
|
|
73
|
+
const sectionsPerRow = Math.ceil(session.numColumns / cageWidth);
|
|
74
|
+
const sectionIndex = sectionRow * sectionsPerRow + sectionCol;
|
|
75
|
+
const sectionCells = session.sections[sectionIndex];
|
|
76
|
+
const checkDuplicates = (cageCellIds, type) => {
|
|
77
|
+
const duplicates = cageCellIds.filter((cellId) => {
|
|
78
|
+
if (cellId === cell.id)
|
|
79
|
+
return false;
|
|
80
|
+
const cellContentsResult = session.state.getCellContents(cellId);
|
|
81
|
+
return cellContentsResult.isSuccess() && cellContentsResult.value.value === contents.value;
|
|
82
|
+
});
|
|
83
|
+
if (duplicates.length > 0) {
|
|
84
|
+
errors.push({
|
|
85
|
+
cellId: cell.id,
|
|
86
|
+
type,
|
|
87
|
+
conflictingCells: duplicates,
|
|
88
|
+
message: `Duplicate ${contents.value} in ${type.replace('duplicate-', '').replace('-', ' ')}`
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
checkDuplicates(rowCells.cellIds, 'duplicate-row');
|
|
93
|
+
checkDuplicates(colCells.cellIds, 'duplicate-column');
|
|
94
|
+
checkDuplicates(sectionCells.cellIds, 'duplicate-section');
|
|
95
|
+
// For Sudoku X puzzles, also check diagonal constraints
|
|
96
|
+
if (puzzleDescription?.type === 'sudoku-x') {
|
|
97
|
+
// Find diagonal cages in the session
|
|
98
|
+
const diagonalCages = session.cages.filter((cage) => cage.cageType === 'x');
|
|
99
|
+
// Check main diagonal (top-left to bottom-right)
|
|
100
|
+
if (cell.row === cell.col && diagonalCages.length > 0) {
|
|
101
|
+
const mainDiagonal = diagonalCages[0];
|
|
102
|
+
checkDuplicates(mainDiagonal.cellIds, 'duplicate-diagonal');
|
|
103
|
+
}
|
|
104
|
+
// Check anti-diagonal (top-right to bottom-left)
|
|
105
|
+
if (cell.row + cell.col === 8 && diagonalCages.length > 1) {
|
|
106
|
+
const antiDiagonal = diagonalCages[1];
|
|
107
|
+
checkDuplicates(antiDiagonal.cellIds, 'duplicate-diagonal');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
return errors;
|
|
113
|
+
}, [session, puzzleDescription, updateCounter]);
|
|
114
|
+
// Get cell display information
|
|
115
|
+
const cellDisplayInfo = useMemo(() => {
|
|
116
|
+
if (!session)
|
|
117
|
+
return [];
|
|
118
|
+
return session.cells.map((cell) => {
|
|
119
|
+
const contentsResult = session.state.getCellContents(cell.id);
|
|
120
|
+
/* c8 ignore next 1 - defense in depth */
|
|
121
|
+
const contents = contentsResult.isSuccess() ? contentsResult.value : { value: undefined, notes: [] };
|
|
122
|
+
const hasValidationError = validationErrors.some((error) => error.cellId === cell.id);
|
|
123
|
+
return {
|
|
124
|
+
id: cell.id,
|
|
125
|
+
row: cell.row,
|
|
126
|
+
column: cell.col,
|
|
127
|
+
contents,
|
|
128
|
+
isImmutable: cell.immutable,
|
|
129
|
+
hasValidationError
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
}, [session, validationErrors, updateCounter]);
|
|
133
|
+
// Get cage display information for Killer Sudoku
|
|
134
|
+
const cageDisplayInfo = useMemo(() => {
|
|
135
|
+
if (!session || puzzleDescription?.type !== 'killer-sudoku')
|
|
136
|
+
return [];
|
|
137
|
+
return session.cages
|
|
138
|
+
.filter((cage) => cage.cageType === 'killer') // Only show killer cages, not diagonal/section cages
|
|
139
|
+
.map((cage) => {
|
|
140
|
+
// Calculate current sum
|
|
141
|
+
let currentSum = 0;
|
|
142
|
+
let isComplete = true;
|
|
143
|
+
let hasValues = false;
|
|
144
|
+
for (const cellId of cage.cellIds) {
|
|
145
|
+
const contentsResult = session.state.getCellContents(cellId);
|
|
146
|
+
if (contentsResult.isSuccess() && contentsResult.value.value) {
|
|
147
|
+
currentSum += contentsResult.value.value;
|
|
148
|
+
hasValues = true;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
isComplete = false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Check if the current sum is valid (not exceeding target when incomplete, or matching target when complete)
|
|
155
|
+
const isValid = isComplete ? currentSum === cage.total : currentSum <= cage.total;
|
|
156
|
+
// Check if cage should be highlighted (contains selected cell)
|
|
157
|
+
const isHighlighted = selectedCell ? cage.cellIds.includes(selectedCell) : false;
|
|
158
|
+
return {
|
|
159
|
+
cage,
|
|
160
|
+
isHighlighted,
|
|
161
|
+
currentSum: hasValues ? currentSum : undefined,
|
|
162
|
+
isComplete,
|
|
163
|
+
isValid
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
}, [session, puzzleDescription, selectedCell, updateCounter]);
|
|
167
|
+
// Helper function to remove notes that conflict with a placed value
|
|
168
|
+
const removeConflictingNotes = useCallback((placedCellId, placedValue) => {
|
|
169
|
+
/* c8 ignore next 1 - defense in depth should not happen */
|
|
170
|
+
if (!session)
|
|
171
|
+
return;
|
|
172
|
+
const placedCell = session.cells.find((cell) => cell.id === placedCellId);
|
|
173
|
+
/* c8 ignore next 1 - defense in depth should not happen */
|
|
174
|
+
if (!placedCell)
|
|
175
|
+
return;
|
|
176
|
+
// Get cells in the same row, column, and section
|
|
177
|
+
const rowCells = session.rows[placedCell.row].cellIds;
|
|
178
|
+
const colCells = session.cols[placedCell.col].cellIds;
|
|
179
|
+
// Calculate section index based on puzzle dimensions
|
|
180
|
+
const cageHeight = session.puzzle.dimensions.cageHeightInCells;
|
|
181
|
+
const cageWidth = session.puzzle.dimensions.cageWidthInCells;
|
|
182
|
+
const sectionRow = Math.floor(placedCell.row / cageHeight);
|
|
183
|
+
const sectionCol = Math.floor(placedCell.col / cageWidth);
|
|
184
|
+
const sectionsPerRow = Math.ceil(session.numColumns / cageWidth);
|
|
185
|
+
const sectionIndex = sectionRow * sectionsPerRow + sectionCol;
|
|
186
|
+
const sectionCells = session.sections[sectionIndex].cellIds;
|
|
187
|
+
// Start with row, column, and section cells
|
|
188
|
+
const affectedCells = new Set([...rowCells, ...colCells, ...sectionCells]);
|
|
189
|
+
// For Sudoku X puzzles, also include diagonal cells
|
|
190
|
+
if (puzzleDescription?.type === 'sudoku-x') {
|
|
191
|
+
// Find diagonal cages in the session
|
|
192
|
+
const diagonalCages = session.cages.filter((cage) => cage.cageType === 'x');
|
|
193
|
+
// Check main diagonal (top-left to bottom-right)
|
|
194
|
+
if (placedCell.row === placedCell.col && diagonalCages.length > 0) {
|
|
195
|
+
const mainDiagonal = diagonalCages[0];
|
|
196
|
+
mainDiagonal.cellIds.forEach((cellId) => affectedCells.add(cellId));
|
|
197
|
+
}
|
|
198
|
+
// Check anti-diagonal (top-right to bottom-left)
|
|
199
|
+
if (placedCell.row + placedCell.col === 8 && diagonalCages.length > 1) {
|
|
200
|
+
const antiDiagonal = diagonalCages[1];
|
|
201
|
+
antiDiagonal.cellIds.forEach((cellId) => affectedCells.add(cellId));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Remove the placed cell itself from the list
|
|
205
|
+
affectedCells.delete(placedCellId);
|
|
206
|
+
// Update notes for each affected cell
|
|
207
|
+
for (const cellId of Array.from(affectedCells)) {
|
|
208
|
+
const contentsResult = session.state.getCellContents(cellId);
|
|
209
|
+
if (contentsResult.isSuccess()) {
|
|
210
|
+
const contents = contentsResult.value;
|
|
211
|
+
// Only update if the cell has notes and contains the placed value
|
|
212
|
+
if (contents.notes.length > 0 && contents.notes.includes(placedValue) && !contents.value) {
|
|
213
|
+
const newNotes = contents.notes.filter((note) => note !== placedValue);
|
|
214
|
+
session.updateCellNotes(cellId, newNotes);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}, [session, puzzleDescription]);
|
|
219
|
+
// Update cell value with smart note removal
|
|
220
|
+
const updateCellValue = useCallback((cellId, value) => {
|
|
221
|
+
/* c8 ignore next 1 - defense in depth should not happen */
|
|
222
|
+
if (!session)
|
|
223
|
+
return;
|
|
224
|
+
const result = session.updateCellValue(cellId, value);
|
|
225
|
+
if (result.isSuccess()) {
|
|
226
|
+
// If a value was placed (not removed), remove conflicting notes
|
|
227
|
+
if (value !== undefined) {
|
|
228
|
+
removeConflictingNotes(cellId, value);
|
|
229
|
+
}
|
|
230
|
+
// Force re-render by incrementing update counter
|
|
231
|
+
setUpdateCounter((prev) => prev + 1);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
setError(`Failed to update cell: ${result.message}`);
|
|
235
|
+
}
|
|
236
|
+
}, [session, removeConflictingNotes]);
|
|
237
|
+
// Toggle a note in a cell
|
|
238
|
+
const toggleCellNote = useCallback((cellId, note) => {
|
|
239
|
+
/* c8 ignore next 1 - defense in depth should not happen */
|
|
240
|
+
if (!session)
|
|
241
|
+
return;
|
|
242
|
+
// Get current cell contents
|
|
243
|
+
const contentsResult = session.state.getCellContents(cellId);
|
|
244
|
+
if (contentsResult.isFailure()) {
|
|
245
|
+
setError(`Failed to get cell contents: ${contentsResult.message}`);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const currentNotes = contentsResult.value.notes;
|
|
249
|
+
const newNotes = currentNotes.includes(note)
|
|
250
|
+
? currentNotes.filter((n) => n !== note) // Remove note if it exists
|
|
251
|
+
: [...currentNotes, note].sort(); // Add note if it doesn't exist
|
|
252
|
+
const result = session.updateCellNotes(cellId, newNotes);
|
|
253
|
+
if (result.isSuccess()) {
|
|
254
|
+
setUpdateCounter((prev) => prev + 1);
|
|
255
|
+
/* c8 ignore next 3 - defense in depth */
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
setError(`Failed to toggle note: ${result.message}`);
|
|
259
|
+
}
|
|
260
|
+
}, [session]);
|
|
261
|
+
// Clear all notes from a cell
|
|
262
|
+
const clearCellNotes = useCallback((cellId) => {
|
|
263
|
+
/* c8 ignore next 1 - defense in depth should not happen */
|
|
264
|
+
if (!session)
|
|
265
|
+
return;
|
|
266
|
+
const result = session.updateCellNotes(cellId, []);
|
|
267
|
+
if (result.isSuccess()) {
|
|
268
|
+
setUpdateCounter((prev) => prev + 1);
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
setError(`Failed to clear notes: ${result.message}`);
|
|
272
|
+
}
|
|
273
|
+
}, [session]);
|
|
274
|
+
// Navigate to adjacent cell
|
|
275
|
+
const navigateToCell = useCallback((direction, wrap = 'wrap-around') => {
|
|
276
|
+
if (!session || !selectedCell)
|
|
277
|
+
return;
|
|
278
|
+
const currentCell = session.cells.find((cell) => cell.id === selectedCell);
|
|
279
|
+
/* c8 ignore next 1 - defense in depth should not happen */
|
|
280
|
+
if (!currentCell)
|
|
281
|
+
return;
|
|
282
|
+
let newRow = currentCell.row;
|
|
283
|
+
let newCol = currentCell.col;
|
|
284
|
+
switch (direction) {
|
|
285
|
+
case 'up':
|
|
286
|
+
newRow = newRow > 0 ? newRow - 1 : wrap === 'wrap-around' ? session.numRows - 1 : newRow;
|
|
287
|
+
break;
|
|
288
|
+
case 'down':
|
|
289
|
+
newRow = newRow < session.numRows - 1 ? newRow + 1 : wrap === 'wrap-around' ? 0 : newRow;
|
|
290
|
+
break;
|
|
291
|
+
case 'left':
|
|
292
|
+
newCol = newCol > 0 ? newCol - 1 : wrap === 'wrap-around' ? session.numColumns - 1 : newCol;
|
|
293
|
+
break;
|
|
294
|
+
case 'right':
|
|
295
|
+
newCol = newCol < session.numColumns - 1 ? newCol + 1 : wrap === 'wrap-around' ? 0 : newCol;
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
const targetCell = session.cells.find((cell) => cell.row === newRow && cell.col === newCol);
|
|
299
|
+
if (targetCell) {
|
|
300
|
+
setSelectedCell(targetCell.id);
|
|
301
|
+
}
|
|
302
|
+
}, [session, selectedCell]);
|
|
303
|
+
// Undo last move
|
|
304
|
+
const undo = useCallback(() => {
|
|
305
|
+
/* c8 ignore next 1 - defense in depth should not happen */
|
|
306
|
+
if (!session)
|
|
307
|
+
return;
|
|
308
|
+
const result = session.undo();
|
|
309
|
+
if (result.isSuccess()) {
|
|
310
|
+
setUpdateCounter((prev) => prev + 1);
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
setError(`Failed to undo: ${result.message}`);
|
|
314
|
+
}
|
|
315
|
+
}, [session]);
|
|
316
|
+
// Redo last undone move
|
|
317
|
+
const redo = useCallback(() => {
|
|
318
|
+
/* c8 ignore next 1 - defense in depth should not happen */
|
|
319
|
+
if (!session)
|
|
320
|
+
return;
|
|
321
|
+
const result = session.redo();
|
|
322
|
+
if (result.isSuccess()) {
|
|
323
|
+
setUpdateCounter((prev) => prev + 1);
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
setError(`Failed to redo: ${result.message}`);
|
|
327
|
+
}
|
|
328
|
+
}, [session]);
|
|
329
|
+
// Reset puzzle to initial state
|
|
330
|
+
const reset = useCallback(() => {
|
|
331
|
+
/* c8 ignore next 1 - defense in depth should not happen */
|
|
332
|
+
if (!session)
|
|
333
|
+
return;
|
|
334
|
+
const result = PuzzleSession.create(session.puzzle);
|
|
335
|
+
if (result.isSuccess()) {
|
|
336
|
+
setSession(result.value);
|
|
337
|
+
setSelectedCell(null);
|
|
338
|
+
setUpdateCounter(0);
|
|
339
|
+
/* c8 ignore next 3 - defense in depth */
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
setError(`Failed to reset: ${result.message}`);
|
|
343
|
+
}
|
|
344
|
+
}, [session]);
|
|
345
|
+
// Export puzzle state
|
|
346
|
+
const exportPuzzle = useCallback(() => {
|
|
347
|
+
/* c8 ignore next 1 - defense in depth should not happen */
|
|
348
|
+
if (!session)
|
|
349
|
+
return null;
|
|
350
|
+
const cellsArray = Array(session.numRows * session.numColumns).fill('.');
|
|
351
|
+
session.cells.forEach((cell) => {
|
|
352
|
+
const contentsResult = session.state.getCellContents(cell.id);
|
|
353
|
+
const index = cell.row * session.numColumns + cell.col;
|
|
354
|
+
if (contentsResult.isSuccess() && contentsResult.value.value) {
|
|
355
|
+
cellsArray[index] = contentsResult.value.value.toString();
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
/* c8 ignore next 4 - defense in depth */
|
|
359
|
+
const id = session.id && session.id !== '' ? session.id : 'exported-puzzle';
|
|
360
|
+
const description = session.description && session.description !== '' ? session.description : 'Exported Puzzle';
|
|
361
|
+
const type = puzzleDescription?.type ?? 'sudoku';
|
|
362
|
+
return createPuzzleDefinition({
|
|
363
|
+
id,
|
|
364
|
+
description,
|
|
365
|
+
type,
|
|
366
|
+
level: 1,
|
|
367
|
+
totalRows: session.numRows,
|
|
368
|
+
totalColumns: session.numColumns,
|
|
369
|
+
cells: cellsArray.join('')
|
|
370
|
+
});
|
|
371
|
+
}, [session, puzzleDescription, updateCounter]);
|
|
372
|
+
return {
|
|
373
|
+
session,
|
|
374
|
+
selectedCell,
|
|
375
|
+
selectedCells,
|
|
376
|
+
inputMode,
|
|
377
|
+
setSelectedCell,
|
|
378
|
+
setSelectedCells,
|
|
379
|
+
setInputMode,
|
|
380
|
+
cellDisplayInfo,
|
|
381
|
+
cageDisplayInfo,
|
|
382
|
+
validationErrors,
|
|
383
|
+
isValid: session?.checkIsValid() ?? false,
|
|
384
|
+
isSolved: session?.checkIsSolved() ?? false,
|
|
385
|
+
canUndo: session?.canUndo ?? false,
|
|
386
|
+
canRedo: session?.canRedo ?? false,
|
|
387
|
+
error,
|
|
388
|
+
updateCellValue,
|
|
389
|
+
toggleCellNote,
|
|
390
|
+
clearCellNotes,
|
|
391
|
+
navigateToCell,
|
|
392
|
+
undo,
|
|
393
|
+
redo,
|
|
394
|
+
reset,
|
|
395
|
+
exportPuzzle
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
//# sourceMappingURL=usePuzzleSession.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usePuzzleSession.js","sourceRoot":"","sources":["../../src/hooks/usePuzzleSession.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * MIT License\n *\n * Copyright (c) 2023 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport { useCallback, useEffect, useMemo, useState } from 'react';\nimport {\n PuzzleSession,\n CellId,\n NavigationDirection,\n NavigationWrap,\n Puzzles,\n IPuzzleDefinition\n} from '@fgv/ts-sudoku-lib';\nimport { IValidationError, ICellDisplayInfo, InputMode, ICageDisplayInfo } from '../types';\nimport { createEmptyPuzzleDefinition, createPuzzleDefinition } from '../utils/puzzleDefinitionHelper';\n\n/**\n * Hook for managing puzzle session state and operations\n * @public\n */\nexport function usePuzzleSession(initialPuzzleDescription?: IPuzzleDefinition): {\n session: PuzzleSession | null;\n selectedCell: CellId | null;\n selectedCells: CellId[];\n inputMode: InputMode;\n setSelectedCell: (cell: CellId | null) => void;\n setSelectedCells: (cells: CellId[]) => void;\n setInputMode: (mode: InputMode) => void;\n cellDisplayInfo: ICellDisplayInfo[];\n cageDisplayInfo: ICageDisplayInfo[];\n validationErrors: IValidationError[];\n isValid: boolean;\n isSolved: boolean;\n canUndo: boolean;\n canRedo: boolean;\n error: string | null;\n updateCellValue: (cellId: CellId, value: number | undefined) => void;\n toggleCellNote: (cellId: CellId, note: number) => void;\n clearCellNotes: (cellId: CellId) => void;\n navigateToCell: (direction: NavigationDirection, wrap?: NavigationWrap) => void;\n undo: () => void;\n redo: () => void;\n reset: () => void;\n exportPuzzle: () => IPuzzleDefinition | null;\n} {\n const [session, setSession] = useState<PuzzleSession | null>(null);\n const [puzzleDescription, setPuzzleDescription] = useState<IPuzzleDefinition | null>(null);\n const [selectedCell, setSelectedCell] = useState<CellId | null>(null);\n const [selectedCells, setSelectedCells] = useState<CellId[]>([]);\n const [inputMode, setInputMode] = useState<InputMode>('notes'); // Notes-first paradigm\n const [error, setError] = useState<string | null>(null);\n const [updateCounter, setUpdateCounter] = useState(0); // Force re-renders\n\n // Initialize or update puzzle session\n useEffect(() => {\n // Use the provided description or create a default empty 9x9 puzzle\n const puzzleDescriptor: IPuzzleDefinition = initialPuzzleDescription || createEmptyPuzzleDefinition();\n\n const sessionResult = Puzzles.Any.create(puzzleDescriptor).onSuccess((puzzle) =>\n PuzzleSession.create(puzzle)\n );\n\n if (sessionResult.isSuccess()) {\n setSession(sessionResult.value);\n setPuzzleDescription(puzzleDescriptor);\n setError(null);\n } else {\n setError(`Failed to create puzzle session: ${sessionResult.message}`);\n setSession(null);\n setPuzzleDescription(null);\n }\n }, [initialPuzzleDescription]);\n\n // Get validation errors\n const validationErrors = useMemo((): IValidationError[] => {\n if (!session) return [];\n\n const errors: IValidationError[] = [];\n const invalidCells = session.getInvalidCells();\n\n invalidCells.forEach((cell) => {\n const contentsResult = session.state.getCellContents(cell.id);\n if (contentsResult.isSuccess() && contentsResult.value.value) {\n const contents = contentsResult.value;\n\n // Check for duplicates in row, column, and section\n const rowCells = session.rows[cell.row];\n const colCells = session.cols[cell.col];\n\n // Calculate section index based on puzzle dimensions\n const cageHeight = session.puzzle.dimensions.cageHeightInCells;\n const cageWidth = session.puzzle.dimensions.cageWidthInCells;\n const sectionRow = Math.floor(cell.row / cageHeight);\n const sectionCol = Math.floor(cell.col / cageWidth);\n const sectionsPerRow = Math.ceil(session.numColumns / cageWidth);\n const sectionIndex = sectionRow * sectionsPerRow + sectionCol;\n const sectionCells = session.sections[sectionIndex];\n\n const checkDuplicates = (cageCellIds: CellId[], type: IValidationError['type']): void => {\n const duplicates = cageCellIds.filter((cellId) => {\n if (cellId === cell.id) return false;\n const cellContentsResult = session.state.getCellContents(cellId);\n return cellContentsResult.isSuccess() && cellContentsResult.value.value === contents.value;\n });\n\n if (duplicates.length > 0) {\n errors.push({\n cellId: cell.id,\n type,\n conflictingCells: duplicates,\n message: `Duplicate ${contents.value} in ${type.replace('duplicate-', '').replace('-', ' ')}`\n });\n }\n };\n\n checkDuplicates(rowCells.cellIds, 'duplicate-row');\n checkDuplicates(colCells.cellIds, 'duplicate-column');\n checkDuplicates(sectionCells.cellIds, 'duplicate-section');\n\n // For Sudoku X puzzles, also check diagonal constraints\n if (puzzleDescription?.type === 'sudoku-x') {\n // Find diagonal cages in the session\n const diagonalCages = session.cages.filter((cage) => cage.cageType === 'x');\n\n // Check main diagonal (top-left to bottom-right)\n if (cell.row === cell.col && diagonalCages.length > 0) {\n const mainDiagonal = diagonalCages[0];\n checkDuplicates(mainDiagonal.cellIds, 'duplicate-diagonal');\n }\n\n // Check anti-diagonal (top-right to bottom-left)\n if (cell.row + cell.col === 8 && diagonalCages.length > 1) {\n const antiDiagonal = diagonalCages[1];\n checkDuplicates(antiDiagonal.cellIds, 'duplicate-diagonal');\n }\n }\n }\n });\n\n return errors;\n }, [session, puzzleDescription, updateCounter]);\n\n // Get cell display information\n const cellDisplayInfo = useMemo((): ICellDisplayInfo[] => {\n if (!session) return [];\n\n return session.cells.map((cell) => {\n const contentsResult = session.state.getCellContents(cell.id);\n /* c8 ignore next 1 - defense in depth */\n const contents = contentsResult.isSuccess() ? contentsResult.value : { value: undefined, notes: [] };\n const hasValidationError = validationErrors.some((error) => error.cellId === cell.id);\n\n return {\n id: cell.id,\n row: cell.row,\n column: cell.col,\n contents,\n isImmutable: cell.immutable,\n hasValidationError\n };\n });\n }, [session, validationErrors, updateCounter]);\n\n // Get cage display information for Killer Sudoku\n const cageDisplayInfo = useMemo((): ICageDisplayInfo[] => {\n if (!session || puzzleDescription?.type !== 'killer-sudoku') return [];\n\n return session.cages\n .filter((cage) => cage.cageType === 'killer') // Only show killer cages, not diagonal/section cages\n .map((cage) => {\n // Calculate current sum\n let currentSum = 0;\n let isComplete = true;\n let hasValues = false;\n\n for (const cellId of cage.cellIds) {\n const contentsResult = session.state.getCellContents(cellId);\n if (contentsResult.isSuccess() && contentsResult.value.value) {\n currentSum += contentsResult.value.value;\n hasValues = true;\n } else {\n isComplete = false;\n }\n }\n\n // Check if the current sum is valid (not exceeding target when incomplete, or matching target when complete)\n const isValid = isComplete ? currentSum === cage.total : currentSum <= cage.total;\n\n // Check if cage should be highlighted (contains selected cell)\n const isHighlighted = selectedCell ? cage.cellIds.includes(selectedCell) : false;\n\n return {\n cage,\n isHighlighted,\n currentSum: hasValues ? currentSum : undefined,\n isComplete,\n isValid\n };\n });\n }, [session, puzzleDescription, selectedCell, updateCounter]);\n\n // Helper function to remove notes that conflict with a placed value\n const removeConflictingNotes = useCallback(\n (placedCellId: CellId, placedValue: number) => {\n /* c8 ignore next 1 - defense in depth should not happen */\n if (!session) return;\n\n const placedCell = session.cells.find((cell) => cell.id === placedCellId);\n /* c8 ignore next 1 - defense in depth should not happen */\n if (!placedCell) return;\n\n // Get cells in the same row, column, and section\n const rowCells = session.rows[placedCell.row].cellIds;\n const colCells = session.cols[placedCell.col].cellIds;\n\n // Calculate section index based on puzzle dimensions\n const cageHeight = session.puzzle.dimensions.cageHeightInCells;\n const cageWidth = session.puzzle.dimensions.cageWidthInCells;\n const sectionRow = Math.floor(placedCell.row / cageHeight);\n const sectionCol = Math.floor(placedCell.col / cageWidth);\n const sectionsPerRow = Math.ceil(session.numColumns / cageWidth);\n const sectionIndex = sectionRow * sectionsPerRow + sectionCol;\n const sectionCells = session.sections[sectionIndex].cellIds;\n\n // Start with row, column, and section cells\n const affectedCells = new Set([...rowCells, ...colCells, ...sectionCells]);\n\n // For Sudoku X puzzles, also include diagonal cells\n if (puzzleDescription?.type === 'sudoku-x') {\n // Find diagonal cages in the session\n const diagonalCages = session.cages.filter((cage) => cage.cageType === 'x');\n\n // Check main diagonal (top-left to bottom-right)\n if (placedCell.row === placedCell.col && diagonalCages.length > 0) {\n const mainDiagonal = diagonalCages[0];\n mainDiagonal.cellIds.forEach((cellId) => affectedCells.add(cellId));\n }\n\n // Check anti-diagonal (top-right to bottom-left)\n if (placedCell.row + placedCell.col === 8 && diagonalCages.length > 1) {\n const antiDiagonal = diagonalCages[1];\n antiDiagonal.cellIds.forEach((cellId) => affectedCells.add(cellId));\n }\n }\n\n // Remove the placed cell itself from the list\n affectedCells.delete(placedCellId);\n\n // Update notes for each affected cell\n for (const cellId of Array.from(affectedCells)) {\n const contentsResult = session.state.getCellContents(cellId);\n if (contentsResult.isSuccess()) {\n const contents = contentsResult.value;\n\n // Only update if the cell has notes and contains the placed value\n if (contents.notes.length > 0 && contents.notes.includes(placedValue) && !contents.value) {\n const newNotes = contents.notes.filter((note) => note !== placedValue);\n session.updateCellNotes(cellId, newNotes);\n }\n }\n }\n },\n [session, puzzleDescription]\n );\n\n // Update cell value with smart note removal\n const updateCellValue = useCallback(\n (cellId: CellId, value: number | undefined) => {\n /* c8 ignore next 1 - defense in depth should not happen */\n if (!session) return;\n\n const result = session.updateCellValue(cellId, value);\n if (result.isSuccess()) {\n // If a value was placed (not removed), remove conflicting notes\n if (value !== undefined) {\n removeConflictingNotes(cellId, value);\n }\n\n // Force re-render by incrementing update counter\n setUpdateCounter((prev) => prev + 1);\n } else {\n setError(`Failed to update cell: ${result.message}`);\n }\n },\n [session, removeConflictingNotes]\n );\n\n // Toggle a note in a cell\n const toggleCellNote = useCallback(\n (cellId: CellId, note: number) => {\n /* c8 ignore next 1 - defense in depth should not happen */\n if (!session) return;\n\n // Get current cell contents\n const contentsResult = session.state.getCellContents(cellId);\n if (contentsResult.isFailure()) {\n setError(`Failed to get cell contents: ${contentsResult.message}`);\n return;\n }\n\n const currentNotes = contentsResult.value.notes;\n const newNotes = currentNotes.includes(note)\n ? currentNotes.filter((n) => n !== note) // Remove note if it exists\n : [...currentNotes, note].sort(); // Add note if it doesn't exist\n\n const result = session.updateCellNotes(cellId, newNotes);\n if (result.isSuccess()) {\n setUpdateCounter((prev) => prev + 1);\n /* c8 ignore next 3 - defense in depth */\n } else {\n setError(`Failed to toggle note: ${result.message}`);\n }\n },\n [session]\n );\n\n // Clear all notes from a cell\n const clearCellNotes = useCallback(\n (cellId: CellId) => {\n /* c8 ignore next 1 - defense in depth should not happen */\n if (!session) return;\n\n const result = session.updateCellNotes(cellId, []);\n if (result.isSuccess()) {\n setUpdateCounter((prev) => prev + 1);\n } else {\n setError(`Failed to clear notes: ${result.message}`);\n }\n },\n [session]\n );\n\n // Navigate to adjacent cell\n const navigateToCell = useCallback(\n (direction: NavigationDirection, wrap: NavigationWrap = 'wrap-around') => {\n if (!session || !selectedCell) return;\n\n const currentCell = session.cells.find((cell) => cell.id === selectedCell);\n /* c8 ignore next 1 - defense in depth should not happen */\n if (!currentCell) return;\n\n let newRow = currentCell.row;\n let newCol = currentCell.col;\n\n switch (direction) {\n case 'up':\n newRow = newRow > 0 ? newRow - 1 : wrap === 'wrap-around' ? session.numRows - 1 : newRow;\n break;\n case 'down':\n newRow = newRow < session.numRows - 1 ? newRow + 1 : wrap === 'wrap-around' ? 0 : newRow;\n break;\n case 'left':\n newCol = newCol > 0 ? newCol - 1 : wrap === 'wrap-around' ? session.numColumns - 1 : newCol;\n break;\n case 'right':\n newCol = newCol < session.numColumns - 1 ? newCol + 1 : wrap === 'wrap-around' ? 0 : newCol;\n break;\n }\n\n const targetCell = session.cells.find((cell) => cell.row === newRow && cell.col === newCol);\n if (targetCell) {\n setSelectedCell(targetCell.id);\n }\n },\n [session, selectedCell]\n );\n\n // Undo last move\n const undo = useCallback(() => {\n /* c8 ignore next 1 - defense in depth should not happen */\n if (!session) return;\n\n const result = session.undo();\n if (result.isSuccess()) {\n setUpdateCounter((prev) => prev + 1);\n } else {\n setError(`Failed to undo: ${result.message}`);\n }\n }, [session]);\n\n // Redo last undone move\n const redo = useCallback(() => {\n /* c8 ignore next 1 - defense in depth should not happen */\n if (!session) return;\n\n const result = session.redo();\n if (result.isSuccess()) {\n setUpdateCounter((prev) => prev + 1);\n } else {\n setError(`Failed to redo: ${result.message}`);\n }\n }, [session]);\n\n // Reset puzzle to initial state\n const reset = useCallback(() => {\n /* c8 ignore next 1 - defense in depth should not happen */\n if (!session) return;\n\n const result = PuzzleSession.create(session.puzzle);\n if (result.isSuccess()) {\n setSession(result.value);\n setSelectedCell(null);\n setUpdateCounter(0);\n /* c8 ignore next 3 - defense in depth */\n } else {\n setError(`Failed to reset: ${result.message}`);\n }\n }, [session]);\n\n // Export puzzle state\n const exportPuzzle = useCallback((): IPuzzleDefinition | null => {\n /* c8 ignore next 1 - defense in depth should not happen */\n if (!session) return null;\n\n const cellsArray = Array(session.numRows * session.numColumns).fill('.');\n\n session.cells.forEach((cell) => {\n const contentsResult = session.state.getCellContents(cell.id);\n const index = cell.row * session.numColumns + cell.col;\n if (contentsResult.isSuccess() && contentsResult.value.value) {\n cellsArray[index] = contentsResult.value.value.toString();\n }\n });\n\n /* c8 ignore next 4 - defense in depth */\n const id = session.id && session.id !== '' ? session.id : 'exported-puzzle';\n const description =\n session.description && session.description !== '' ? session.description : 'Exported Puzzle';\n const type = puzzleDescription?.type ?? 'sudoku';\n\n return createPuzzleDefinition({\n id,\n description,\n type,\n level: 1,\n totalRows: session.numRows,\n totalColumns: session.numColumns,\n cells: cellsArray.join('')\n });\n }, [session, puzzleDescription, updateCounter]);\n\n return {\n session,\n selectedCell,\n selectedCells,\n inputMode,\n setSelectedCell,\n setSelectedCells,\n setInputMode,\n cellDisplayInfo,\n cageDisplayInfo,\n validationErrors,\n isValid: session?.checkIsValid() ?? false,\n isSolved: session?.checkIsSolved() ?? false,\n canUndo: session?.canUndo ?? false,\n canRedo: session?.canRedo ?? false,\n error,\n updateCellValue,\n toggleCellNote,\n clearCellNotes,\n navigateToCell,\n undo,\n redo,\n reset,\n exportPuzzle\n };\n}\n"]}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device type detection result
|
|
3
|
+
* @public
|
|
4
|
+
*/
|
|
5
|
+
export type DeviceType = 'mobile' | 'tablet' | 'desktop';
|
|
6
|
+
/**
|
|
7
|
+
* Screen orientation
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
export type ScreenOrientation = 'portrait' | 'landscape';
|
|
11
|
+
/**
|
|
12
|
+
* Layout mode for dual keypad
|
|
13
|
+
* @public
|
|
14
|
+
*/
|
|
15
|
+
export type KeypadLayoutMode = 'side-by-side' | 'stacked' | 'overlay' | 'hidden';
|
|
16
|
+
/**
|
|
17
|
+
* Responsive layout information
|
|
18
|
+
* @public
|
|
19
|
+
*/
|
|
20
|
+
export interface IResponsiveLayoutInfo {
|
|
21
|
+
/** Device type based on screen size and touch capability */
|
|
22
|
+
readonly deviceType: DeviceType;
|
|
23
|
+
/** Current screen orientation */
|
|
24
|
+
readonly orientation: ScreenOrientation;
|
|
25
|
+
/** Recommended keypad layout mode */
|
|
26
|
+
readonly keypadLayoutMode: KeypadLayoutMode;
|
|
27
|
+
/** Screen width in pixels */
|
|
28
|
+
readonly screenWidth: number;
|
|
29
|
+
/** Screen height in pixels */
|
|
30
|
+
readonly screenHeight: number;
|
|
31
|
+
/** Whether touch is supported */
|
|
32
|
+
readonly isTouchDevice: boolean;
|
|
33
|
+
/** Whether the screen is considered small */
|
|
34
|
+
readonly isSmallScreen: boolean;
|
|
35
|
+
/** Whether the screen has enough space for dual keypads */
|
|
36
|
+
readonly hasSpaceForDualKeypads: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Hook to detect responsive layout information for optimal keypad display
|
|
40
|
+
* @param forceLayoutMode - Optional layout mode override for testing
|
|
41
|
+
* @public
|
|
42
|
+
*/
|
|
43
|
+
export declare function useResponsiveLayout(forceLayoutMode?: KeypadLayoutMode): IResponsiveLayoutInfo;
|
|
44
|
+
//# sourceMappingURL=useResponsiveLayout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useResponsiveLayout.d.ts","sourceRoot":"","sources":["../../src/hooks/useResponsiveLayout.ts"],"names":[],"mappings":"AA0BA;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEzD;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG,UAAU,GAAG,WAAW,CAAC;AAEzD;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,cAAc,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEjF;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,4DAA4D;IAC5D,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAEhC,iCAAiC;IACjC,QAAQ,CAAC,WAAW,EAAE,iBAAiB,CAAC;IAExC,qCAAqC;IACrC,QAAQ,CAAC,gBAAgB,EAAE,gBAAgB,CAAC;IAE5C,6BAA6B;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAE7B,8BAA8B;IAC9B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAE9B,iCAAiC;IACjC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAEhC,6CAA6C;IAC7C,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAEhC,2DAA2D;IAC3D,QAAQ,CAAC,sBAAsB,EAAE,OAAO,CAAC;CAC1C;AAsED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,eAAe,CAAC,EAAE,gBAAgB,GAAG,qBAAqB,CA2E7F"}
|