@scrabble-solver/scrabble-solver 2.18.1 → 2.18.3

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 (196) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +14 -14
  3. package/.next/cache/.previewinfo +1 -1
  4. package/.next/cache/.rscinfo +1 -1
  5. package/.next/cache/.tsbuildinfo +1 -1
  6. package/.next/cache/webpack/client-production/0.pack +0 -0
  7. package/.next/cache/webpack/client-production/index.pack +0 -0
  8. package/.next/cache/webpack/client-production/index.pack.old +0 -0
  9. package/.next/cache/webpack/edge-server-production/index.pack +0 -0
  10. package/.next/cache/webpack/edge-server-production/index.pack.old +0 -0
  11. package/.next/cache/webpack/server-production/0.pack +0 -0
  12. package/.next/cache/webpack/server-production/index.pack +0 -0
  13. package/.next/cache/webpack/server-production/index.pack.old +0 -0
  14. package/.next/diagnostics/framework.json +1 -1
  15. package/.next/dynamic-css-manifest.json +2 -2
  16. package/.next/prerender-manifest.json +4 -4
  17. package/.next/react-loadable-manifest.json +37 -37
  18. package/.next/routes-manifest.json +1 -1
  19. package/.next/server/chunks/309.js +1 -1
  20. package/.next/server/chunks/344.js +1 -1
  21. package/.next/server/chunks/371.js +1 -1
  22. package/.next/server/chunks/392.js +1 -1
  23. package/.next/server/chunks/465.js +1 -1
  24. package/.next/server/chunks/546.js +1 -1
  25. package/.next/server/chunks/641.js +1 -1
  26. package/.next/server/chunks/67.js +1 -0
  27. package/.next/server/chunks/712.js +1 -1
  28. package/.next/server/chunks/812.js +1 -1
  29. package/.next/server/chunks/846.js +1 -1
  30. package/.next/server/chunks/886.js +1 -1
  31. package/.next/server/chunks/942.js +1 -0
  32. package/.next/server/chunks/953.js +1 -1
  33. package/.next/server/chunks/961.js +1 -1
  34. package/.next/server/dynamic-css-manifest.js +1 -1
  35. package/.next/server/middleware-build-manifest.js +1 -1
  36. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  37. package/.next/server/pages/404.html +3 -3
  38. package/.next/server/pages/500.html +3 -3
  39. package/.next/server/pages/_app.js +1 -1
  40. package/.next/server/pages/_app.js.nft.json +1 -1
  41. package/.next/server/pages/_document.js +2 -2
  42. package/.next/server/pages/_document.js.nft.json +1 -1
  43. package/.next/server/pages/_error.js +1 -1
  44. package/.next/server/pages/_error.js.nft.json +1 -1
  45. package/.next/server/pages/api/dictionary/[locale]/[word].js +1 -1
  46. package/.next/server/pages/api/dictionary/[locale]/[word].js.nft.json +1 -1
  47. package/.next/server/pages/api/dictionary/[locale].js +1 -1
  48. package/.next/server/pages/api/dictionary/[locale].js.nft.json +1 -1
  49. package/.next/server/pages/api/solve.js +1 -1
  50. package/.next/server/pages/api/solve.js.nft.json +1 -1
  51. package/.next/server/pages/api/verify.js +1 -1
  52. package/.next/server/pages/api/verify.js.nft.json +1 -1
  53. package/.next/server/pages/index.html +3 -3
  54. package/.next/server/pages/index.js +4 -4
  55. package/.next/server/pages/index.js.nft.json +1 -1
  56. package/.next/server/pages/index.json +1 -1
  57. package/.next/server/pages/not-found.html +3 -3
  58. package/.next/server/pages/not-found.js.nft.json +1 -1
  59. package/.next/server/pages-manifest.json +1 -1
  60. package/.next/static/{5KOWzYqfY1Z2rjrzptr8D → 8WnIUTe-yJvPD6stltvYI}/_buildManifest.js +1 -1
  61. package/.next/static/chunks/100.305bdf9c75b1758f.js +1 -0
  62. package/.next/static/chunks/256.6a20b23a766cfebd.js +1 -0
  63. package/.next/static/chunks/{307.67f52f13e3d60d4e.js → 307.89e6100f58443679.js} +1 -1
  64. package/.next/static/chunks/{363.01eed3233f92b6a4.js → 363.cec65b1f9668a75e.js} +1 -1
  65. package/.next/static/chunks/{422.1559ae6756912ca5.js → 422.3e55b4c94e328701.js} +1 -1
  66. package/.next/static/chunks/{663.d2c9ca36b26695b8.js → 663.bd2c45413fbca518.js} +1 -1
  67. package/.next/static/chunks/693.677c88e0eb1340b5.js +1 -0
  68. package/.next/static/chunks/{820.97b82f751ca49579.js → 820.823e9e885ebb66af.js} +1 -1
  69. package/.next/static/chunks/827.c81b92b383a748c9.js +1 -0
  70. package/.next/static/chunks/{865.a9b563a45848a83c.js → 865.59baba8296c9b116.js} +1 -1
  71. package/.next/static/chunks/912.077603e4f0bc6cca.js +1 -0
  72. package/.next/static/chunks/{939.19b33a3d3c351d73.js → 939.2ad86b50405c7897.js} +1 -1
  73. package/.next/static/chunks/980.7ce17464fc56648a.js +1 -0
  74. package/.next/static/chunks/{main-b92e08f3fc96dd8d.js → main-7e52da35a4beea0b.js} +1 -1
  75. package/.next/static/chunks/pages/_app-e3fe880e24c024ab.js +1 -0
  76. package/.next/static/chunks/pages/index-b073a1686818dd26.js +1 -0
  77. package/.next/static/chunks/webpack-d7138f1ef6b8c9d6.js +1 -0
  78. package/.next/static/css/65bcc9440e067113.css +1 -0
  79. package/.next/static/css/{4dbcacc1122ac6b9.css → 92f85ce358a0a468.css} +1 -1
  80. package/.next/static/css/a691348b788f8bc0.css +1 -0
  81. package/.next/static/css/cd4d7f2458487399.css +1 -0
  82. package/.next/trace +22 -21
  83. package/.next/trace-build +1 -1
  84. package/AGENTS.md +9 -0
  85. package/CLAUDE.md +1 -0
  86. package/package.json +14 -12
  87. package/public/llms.txt +1 -1
  88. package/src/components/Board/Board.tsx +3 -1
  89. package/src/components/Board/BoardPure.tsx +26 -20
  90. package/src/components/Board/components/Cell/Cell.tsx +4 -4
  91. package/src/components/Board/lib.test.ts +106 -0
  92. package/src/components/Board/lib.ts +34 -3
  93. package/src/components/Board/selectors.ts +17 -5
  94. package/src/components/Button/Button.module.scss +1 -1
  95. package/src/components/Modal/Modal.module.scss +0 -4
  96. package/src/components/Results/Header.tsx +30 -5
  97. package/src/components/Results/Result.tsx +45 -55
  98. package/src/components/Results/Results.module.scss +0 -126
  99. package/src/components/Results/Results.module.scss.d.ts +0 -10
  100. package/src/components/Results/Results.tsx +25 -5
  101. package/src/components/Solver/Solver.module.scss +4 -4
  102. package/src/components/Solver/Solver.tsx +5 -3
  103. package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.module.scss +5 -1
  104. package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.module.scss.d.ts +1 -0
  105. package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.tsx +5 -2
  106. package/src/components/Table/Cell.tsx +42 -0
  107. package/src/components/Table/Header.tsx +11 -0
  108. package/src/components/{Results → Table}/HeaderButton.tsx +22 -14
  109. package/src/components/Table/Row.tsx +59 -0
  110. package/src/components/{ResultsInput/ResultsInput.tsx → Table/Search.tsx} +9 -11
  111. package/src/components/Table/Table.module.scss +147 -0
  112. package/src/components/Table/Table.module.scss.d.ts +19 -0
  113. package/src/components/Table/index.ts +5 -0
  114. package/src/components/Tile/Tile.module.scss +2 -1
  115. package/src/hooks/useColumns.ts +7 -5
  116. package/src/hooks/useIsCompactLayout.ts +14 -0
  117. package/src/i18n/languages/english.json +6 -1
  118. package/src/i18n/languages/french.json +6 -1
  119. package/src/i18n/languages/german.json +6 -1
  120. package/src/i18n/languages/persian.json +6 -1
  121. package/src/i18n/languages/polish.json +6 -1
  122. package/src/i18n/languages/romanian.json +6 -1
  123. package/src/i18n/languages/spanish.json +6 -1
  124. package/src/i18n/languages/turkish.json +6 -1
  125. package/src/icons/QuestionSquare.svg +5 -0
  126. package/src/lib/createCoordinatesComparator.ts +23 -0
  127. package/src/lib/createKeyComparator.ts +1 -4
  128. package/src/lib/createSortComparator.ts +22 -0
  129. package/src/lib/getCoordinates.test.ts +21 -0
  130. package/src/lib/getCoordinates.ts +16 -0
  131. package/src/lib/getNextSort.ts +12 -0
  132. package/src/modals/RemainingTilesModal/RemainingTilesModal.tsx +3 -1
  133. package/src/modals/RemainingTilesModal/components/Character/Character.tsx +8 -9
  134. package/src/modals/ResultsModal/ResultsModal.tsx +14 -10
  135. package/src/modals/WordsModal/WordsModal.module.scss +16 -27
  136. package/src/modals/WordsModal/WordsModal.module.scss.d.ts +3 -6
  137. package/src/modals/WordsModal/WordsModal.tsx +56 -42
  138. package/src/modals/WordsModal/components/WordsTable/WordRow.tsx +92 -0
  139. package/src/modals/WordsModal/components/WordsTable/WordsTable.module.scss +69 -0
  140. package/src/modals/WordsModal/components/WordsTable/WordsTable.module.scss.d.ts +16 -0
  141. package/src/modals/WordsModal/components/WordsTable/WordsTable.tsx +137 -0
  142. package/src/modals/WordsModal/components/WordsTable/index.ts +1 -0
  143. package/src/modals/WordsModal/components/index.ts +1 -0
  144. package/src/pages/api/verify.ts +7 -6
  145. package/src/pages/index.tsx +29 -4
  146. package/src/parameters/index.ts +2 -0
  147. package/src/sdk/verify.ts +3 -2
  148. package/src/solver-worker/client.test.ts +10 -3
  149. package/src/solver-worker/index.worker.ts +7 -7
  150. package/src/solver-worker/messages.ts +3 -3
  151. package/src/state/hoveredWord/index.ts +4 -0
  152. package/src/state/hoveredWord/initialState.ts +5 -0
  153. package/src/state/hoveredWord/selectors.ts +5 -0
  154. package/src/state/hoveredWord/slice.ts +18 -0
  155. package/src/state/hoveredWord/types.ts +5 -0
  156. package/src/state/index.ts +1 -0
  157. package/src/state/localStorage.ts +13 -2
  158. package/src/state/results/lib.ts +9 -26
  159. package/src/state/results/selectors.ts +9 -1
  160. package/src/state/results/slice.ts +4 -16
  161. package/src/state/sagas.ts +26 -1
  162. package/src/state/selectors.ts +18 -1
  163. package/src/state/store.ts +2 -0
  164. package/src/state/types.ts +2 -0
  165. package/src/state/verify/initialState.ts +7 -0
  166. package/src/state/verify/lib.test.ts +55 -0
  167. package/src/state/verify/lib.ts +49 -0
  168. package/src/state/verify/selectors.ts +37 -0
  169. package/src/state/verify/slice.ts +13 -2
  170. package/src/state/verify/types.ts +6 -2
  171. package/src/styles/_tokens.scss +2 -0
  172. package/src/styles/mixins.scss +5 -0
  173. package/src/styles/variables.module.scss +1 -0
  174. package/src/styles/variables.module.scss.d.ts +1 -0
  175. package/src/styles/variables.scss +1 -1
  176. package/src/types/index.ts +24 -11
  177. package/tsconfig.tsbuildinfo +1 -1
  178. package/.next/server/chunks/316.js +0 -1
  179. package/.next/server/chunks/916.js +0 -1
  180. package/.next/static/chunks/106.e414e88694add27f.js +0 -1
  181. package/.next/static/chunks/127.125d896a19061d71.js +0 -1
  182. package/.next/static/chunks/18.8b91e137d239e15c.js +0 -1
  183. package/.next/static/chunks/583.7a46807be14c377e.js +0 -1
  184. package/.next/static/chunks/711.ba7b8f900dfd40d0.js +0 -1
  185. package/.next/static/chunks/912.ff8037ba9d4db5c8.js +0 -1
  186. package/.next/static/chunks/pages/_app-2e607dcd4135f340.js +0 -1
  187. package/.next/static/chunks/pages/index-e0087247c995328d.js +0 -1
  188. package/.next/static/chunks/webpack-cfb34d175e081981.js +0 -1
  189. package/.next/static/css/b74cd7de82dabcdd.css +0 -1
  190. package/.next/static/css/c403ce739a9c0d83.css +0 -1
  191. package/.next/static/css/fbfdce70731d0d2b.css +0 -1
  192. package/src/components/Results/Cell.tsx +0 -32
  193. package/src/components/ResultsInput/ResultsInput.module.scss +0 -12
  194. package/src/components/ResultsInput/ResultsInput.module.scss.d.ts +0 -7
  195. package/src/components/ResultsInput/index.ts +0 -1
  196. /package/.next/static/{5KOWzYqfY1Z2rjrzptr8D → 8WnIUTe-yJvPD6stltvYI}/_ssgManifest.js +0 -0
package/.next/trace-build CHANGED
@@ -1 +1 @@
1
- [{"name":"run-typescript","duration":167919,"timestamp":15741743485,"id":13,"parentId":1,"tags":{},"startTime":1786879882182,"traceId":"09ff88835bed4614"},{"name":"run-webpack","duration":6553601,"timestamp":15741913668,"id":15,"parentId":1,"tags":{},"startTime":1786879882352,"traceId":"09ff88835bed4614"},{"name":"static-check","duration":184131,"timestamp":15748541724,"id":2019,"parentId":1,"tags":{},"startTime":1786879888980,"traceId":"09ff88835bed4614"},{"name":"static-generation","duration":249410,"timestamp":15748736408,"id":2037,"parentId":1,"tags":{},"startTime":1786879889175,"traceId":"09ff88835bed4614"},{"name":"collect-build-traces","duration":4410264,"timestamp":15748726538,"id":2034,"parentId":1,"tags":{},"startTime":1786879889165,"traceId":"09ff88835bed4614"},{"name":"telemetry-flush","duration":21,"timestamp":15753138117,"id":2044,"parentId":1,"tags":{},"startTime":1786879893576,"traceId":"09ff88835bed4614"},{"name":"next-build","duration":11637473,"timestamp":15741500668,"id":1,"tags":{"buildMode":"default","version":"16.3.1","bundler":"webpack","has-custom-webpack-config":"true","use-build-worker":"false"},"startTime":1786879881939,"traceId":"09ff88835bed4614"}]
1
+ [{"name":"run-typescript","duration":160243,"timestamp":6454978890,"id":13,"parentId":1,"tags":{},"startTime":1787605800486,"traceId":"f92ac34b43b8b857"},{"name":"run-webpack","duration":5760453,"timestamp":6455141586,"id":15,"parentId":1,"tags":{},"startTime":1787605800649,"traceId":"f92ac34b43b8b857"},{"name":"static-check","duration":171530,"timestamp":6460973542,"id":2106,"parentId":1,"tags":{},"startTime":1787605806481,"traceId":"f92ac34b43b8b857"},{"name":"static-generation","duration":249530,"timestamp":6461155014,"id":2124,"parentId":1,"tags":{},"startTime":1787605806662,"traceId":"f92ac34b43b8b857"},{"name":"collect-build-traces","duration":4270405,"timestamp":6461145430,"id":2121,"parentId":1,"tags":{},"startTime":1787605806652,"traceId":"f92ac34b43b8b857"},{"name":"telemetry-flush","duration":19,"timestamp":6465417121,"id":2131,"parentId":1,"tags":{},"startTime":1787605810924,"traceId":"f92ac34b43b8b857"},{"name":"next-build","duration":10674125,"timestamp":6454743019,"id":1,"tags":{"buildMode":"default","version":"16.3.2","bundler":"webpack","has-custom-webpack-config":"true","use-build-worker":"false"},"startTime":1787605800250,"traceId":"f92ac34b43b8b857"}]
package/AGENTS.md ADDED
@@ -0,0 +1,9 @@
1
+ <!-- BEGIN:nextjs-agent-rules -->
2
+
3
+ # This is NOT the Next.js you know
4
+
5
+ This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
6
+
7
+ This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
8
+
9
+ <!-- END:nextjs-agent-rules -->
package/CLAUDE.md ADDED
@@ -0,0 +1 @@
1
+ @AGENTS.md
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scrabble-solver/scrabble-solver",
3
- "version": "2.18.1",
3
+ "version": "2.18.3",
4
4
  "description": "Scrabble Solver web app: free word finder and board solver for Scrabble, Super Scrabble, Literaki, and more.",
5
5
  "keywords": [
6
6
  "scrabble",
@@ -45,7 +45,9 @@
45
45
  "build": "bun run generate-scss-types && env-cmd next build --webpack",
46
46
  "clean": "rimraf --glob .next/ 'src/**/*.module.scss.d.ts'",
47
47
  "debug": "NODE_OPTIONS='--inspect' next dev --webpack",
48
- "dev": "concurrently -n scss,next 'bun scripts/generate-scss-types.ts --watch' 'env-cmd next dev --webpack'",
48
+ "dev": "bun run --parallel --no-orphans 'dev:*'",
49
+ "dev:next": "env-cmd next dev --webpack",
50
+ "dev:scss": "bun run generate-scss-types --watch",
49
51
  "generate-scss-types": "bun scripts/generate-scss-types.ts",
50
52
  "start": "env-cmd next start -p 3333",
51
53
  "test": "bun test --timeout 180000 src",
@@ -55,18 +57,18 @@
55
57
  "@floating-ui/react": "^0.27.20",
56
58
  "@kamilmielnik/gaddag": "^2.0.0",
57
59
  "@reduxjs/toolkit": "^2.12.0",
58
- "@scrabble-solver/configs": "^2.18.1",
59
- "@scrabble-solver/constants": "^2.18.1",
60
- "@scrabble-solver/dictionaries": "^2.18.1",
61
- "@scrabble-solver/logger": "^2.18.1",
62
- "@scrabble-solver/solver": "^2.18.1",
63
- "@scrabble-solver/types": "^2.18.1",
64
- "@scrabble-solver/word-definitions": "^2.18.1",
60
+ "@scrabble-solver/configs": "^2.18.3",
61
+ "@scrabble-solver/constants": "^2.18.3",
62
+ "@scrabble-solver/dictionaries": "^2.18.3",
63
+ "@scrabble-solver/logger": "^2.18.3",
64
+ "@scrabble-solver/solver": "^2.18.3",
65
+ "@scrabble-solver/types": "^2.18.3",
66
+ "@scrabble-solver/word-definitions": "^2.18.3",
65
67
  "classnames": "^2.5.1",
66
68
  "env-cmd": "^11.0.0",
67
69
  "include-media": "^2.0.0",
68
70
  "include-media-query-builder": "^1.1.0",
69
- "next": "^16.3.1",
71
+ "next": "^16.3.2",
70
72
  "normalize.css": "^8.0.1",
71
73
  "react": "^19.2.8",
72
74
  "react-cool-onclickoutside": "^1.7.0",
@@ -92,7 +94,7 @@
92
94
  "@types/react-modal": "^3.16.3",
93
95
  "@types/react-portal": "^4.0.7",
94
96
  "postcss": "^8.5.26",
95
- "sass": "^1.102.0"
97
+ "sass": "^1.103.1"
96
98
  },
97
- "gitHead": "abd1636f7e2235d0ee06dbf6197de46b703feb44"
99
+ "gitHead": "a1b4a5735a1a48fbb5dfe7f214a7df2158309202"
98
100
  }
package/public/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # Scrabble Solver
2
2
 
3
- > Free and open-source analysis tool for Scrabble, Super Scrabble, Scrabble Duel, Letter League, Crossplay, Literaki, and Kelimelik. Quickly finds top-scoring words for a given board and rack in English, French, German, Persian, Polish, Romanian, Spanish, and Turkish.
3
+ > Free and open-source analysis tool for Scrabble, Super Scrabble, Scrabble Duel, Letter League, Crossplay, Literaki, Kelimelik, and Wyrazy. Quickly finds top-scoring words for a given board and rack in English, French, German, Persian, Polish, Romanian, Spanish, and Turkish.
4
4
 
5
5
  Scrabble Solver is a single-page web app. Enter the board state and your tiles to get every legal move ranked by score, check whether a word is allowed in the selected language's dictionary, and look up word definitions. Once a dictionary is cached, solving also works offline.
6
6
 
@@ -26,7 +26,7 @@ import styles from './Board.module.scss';
26
26
  import { BoardPure } from './BoardPure';
27
27
  import { Actions, InputPrompt } from './components';
28
28
  import { useBoardStyle, useFloatingActions, useFloatingFocus, useFloatingInputPrompt, useGrid } from './hooks';
29
- import { selectReachableCells, selectRowsWithCandidate } from './selectors';
29
+ import { selectHoveredWordCells, selectReachableCells, selectRowsWithCandidate } from './selectors';
30
30
 
31
31
  interface Props {
32
32
  className?: string;
@@ -39,6 +39,7 @@ export const Board: FunctionComponent<Props> = ({ className }) => {
39
39
  const config = useTypedSelector(selectConfig);
40
40
  const rows = useTypedSelector(selectRowsWithCandidate);
41
41
  const reachableCells = useTypedSelector(selectReachableCells);
42
+ const hoveredWordCells = useTypedSelector(selectHoveredWordCells);
42
43
  const inputMode = useTypedSelector(selectInputMode);
43
44
  const cellFilters = useTypedSelector(selectCellFilters);
44
45
  const showCoordinates = useTypedSelector(selectShowCoordinates);
@@ -148,6 +149,7 @@ export const Board: FunctionComponent<Props> = ({ className }) => {
148
149
  config={config}
149
150
  direction={LOCALE_FEATURES[locale].direction}
150
151
  hoveredCharacter={hoveredCharacter}
152
+ hoveredWordCells={hoveredWordCells}
151
153
  inputRefs={inputRefs}
152
154
  locale={locale}
153
155
  reachableCells={reachableCells}
@@ -33,6 +33,7 @@ interface Props {
33
33
  config: Config;
34
34
  direction: TextDirection;
35
35
  hoveredCharacter: string | null;
36
+ hoveredWordCells: boolean[][] | null;
36
37
  inputRefs: RefObject<HTMLInputElement | null>[][];
37
38
  locale: Locale;
38
39
  reachableCells: boolean[][] | null;
@@ -55,6 +56,7 @@ const BoardPureBase = forwardRef<HTMLDivElement, Props>(
55
56
  direction,
56
57
  cellFilters,
57
58
  hoveredCharacter,
59
+ hoveredWordCells,
58
60
  inputRefs,
59
61
  locale,
60
62
  reachableCells,
@@ -118,25 +120,29 @@ const BoardPureBase = forwardRef<HTMLDivElement, Props>(
118
120
  {getCoordinate(y, showCoordinates === 'original' ? 'number' : 'letter')}
119
121
  </div>
120
122
 
121
- {cells.map((cell, x) => (
122
- <Cell
123
- cell={cell}
124
- cellBottom={y < rows.length - 1 ? rows[y + 1][x] : undefined}
125
- cellLeft={x > 0 ? rows[y][x - 1] : undefined}
126
- cellRight={x < rows[y].length - 1 ? rows[y][x + 1] : undefined}
127
- cellTop={y > 0 ? rows[y - 1][x] : undefined}
128
- config={config}
129
- inputRef={inputRefs[y][x]}
130
- isHoverMatch={isHoverMatch(cell, hoveredCharacter)}
131
- isReachable={reachableCells ? reachableCells[y][x] : true}
132
- key={x}
133
- locale={locale}
134
- showCoordinates={showCoordinates}
135
- translate={translate}
136
- onChange={onChange}
137
- onFocus={onFocus}
138
- />
139
- ))}
123
+ {cells.map((cell, x) => {
124
+ const isHoveredWord = hoveredWordCells ? hoveredWordCells[y][x] : false;
125
+
126
+ return (
127
+ <Cell
128
+ cell={cell}
129
+ cellBottom={y < rows.length - 1 ? rows[y + 1][x] : undefined}
130
+ cellLeft={x > 0 ? rows[y][x - 1] : undefined}
131
+ cellRight={x < rows[y].length - 1 ? rows[y][x + 1] : undefined}
132
+ cellTop={y > 0 ? rows[y - 1][x] : undefined}
133
+ config={config}
134
+ highlighted={cell.isCandidate() || isHoverCharacter(cell, hoveredCharacter) || isHoveredWord}
135
+ inputRef={inputRefs[y][x]}
136
+ isReachable={reachableCells ? reachableCells[y][x] : true}
137
+ key={x}
138
+ locale={locale}
139
+ showCoordinates={showCoordinates}
140
+ translate={translate}
141
+ onChange={onChange}
142
+ onFocus={onFocus}
143
+ />
144
+ );
145
+ })}
140
146
  </Fragment>
141
147
  ))}
142
148
  </div>
@@ -145,7 +151,7 @@ const BoardPureBase = forwardRef<HTMLDivElement, Props>(
145
151
 
146
152
  export const BoardPure = memo(BoardPureBase);
147
153
 
148
- function isHoverMatch(cell: CellModel, hoveredCharacter: string | null): boolean {
154
+ function isHoverCharacter(cell: CellModel, hoveredCharacter: string | null): boolean {
149
155
  if (cell.tile.character === EMPTY_CELL || hoveredCharacter === null) {
150
156
  return false;
151
157
  }
@@ -28,8 +28,8 @@ interface Props {
28
28
  cellTop?: CellModel;
29
29
  className?: string;
30
30
  config: Config;
31
+ highlighted?: boolean;
31
32
  inputRef: RefObject<HTMLInputElement | null>;
32
- isHoverMatch: boolean;
33
33
  isReachable?: boolean;
34
34
  locale: Locale;
35
35
  showCoordinates: ShowCoordinates;
@@ -39,7 +39,7 @@ interface Props {
39
39
  }
40
40
 
41
41
  /**
42
- * Must not subscribe to the store - it can read the story only in event handlers.
42
+ * Must not subscribe to the store - it can read the store only in event handlers
43
43
  */
44
44
  const CellBase: FunctionComponent<Props> = ({
45
45
  cell,
@@ -49,8 +49,8 @@ const CellBase: FunctionComponent<Props> = ({
49
49
  cellTop,
50
50
  className,
51
51
  config,
52
+ highlighted,
52
53
  inputRef,
53
- isHoverMatch,
54
54
  isReachable = true,
55
55
  locale,
56
56
  showCoordinates,
@@ -116,7 +116,7 @@ const CellBase: FunctionComponent<Props> = ({
116
116
  [styles.unreachable]: !isReachable,
117
117
  })}
118
118
  character={isEmpty ? undefined : tile.character}
119
- highlighted={cell.isCandidate() || isHoverMatch}
119
+ highlighted={highlighted}
120
120
  inputRef={inputRef}
121
121
  isBlank={tile.isBlank}
122
122
  isValid={isValid}
@@ -0,0 +1,106 @@
1
+ import { Board, Cell, Result, Tile } from '@scrabble-solver/types';
2
+
3
+ import { getReachableCellsFromResults, getWordCells } from './lib';
4
+
5
+ describe('getReachableCellsFromResults', () => {
6
+ const board = Board.fromStringArray([' ', ' ', ' i ', ' ', ' ']);
7
+
8
+ it('marks only cells with tiles as reachable when there are no results', () => {
9
+ expect(getReachableCellsFromResults(board, [])).toEqual([
10
+ [false, false, false, false, false],
11
+ [false, false, false, false, false],
12
+ [false, false, true, false, false],
13
+ [false, false, false, false, false],
14
+ [false, false, false, false, false],
15
+ ]);
16
+ });
17
+
18
+ it('marks cells covered by any result as reachable and all other empty cells as unreachable', () => {
19
+ const results = [
20
+ createResult([createPlacedCell('q', 1, 2), board.rows[2][2]]),
21
+ createResult([createPlacedCell('q', 2, 1), board.rows[2][2]]),
22
+ ];
23
+
24
+ expect(getReachableCellsFromResults(board, results)).toEqual([
25
+ [false, false, false, false, false],
26
+ [false, false, true, false, false],
27
+ [false, true, true, false, false],
28
+ [false, false, false, false, false],
29
+ [false, false, false, false, false],
30
+ ]);
31
+ });
32
+ });
33
+
34
+ describe('getWordCells', () => {
35
+ const board = Board.fromStringArray([' ', ' cat ', ' o ', ' d x', ' ']);
36
+
37
+ it('marks the cells of a horizontal word', () => {
38
+ expect(getWordCells(board, { direction: 'horizontal', word: 'cat', x: 1, y: 1 })).toEqual([
39
+ [false, false, false, false, false],
40
+ [false, true, true, true, false],
41
+ [false, false, false, false, false],
42
+ [false, false, false, false, false],
43
+ [false, false, false, false, false],
44
+ ]);
45
+ });
46
+
47
+ it('marks the cells of a vertical word', () => {
48
+ expect(getWordCells(board, { direction: 'vertical', word: 'cod', x: 1, y: 1 })).toEqual([
49
+ [false, false, false, false, false],
50
+ [false, true, false, false, false],
51
+ [false, true, false, false, false],
52
+ [false, true, false, false, false],
53
+ [false, false, false, false, false],
54
+ ]);
55
+ });
56
+
57
+ it('stops at the board edge', () => {
58
+ expect(getWordCells(board, { direction: 'horizontal', word: 'x', x: 4, y: 3 })).toEqual([
59
+ [false, false, false, false, false],
60
+ [false, false, false, false, false],
61
+ [false, false, false, false, false],
62
+ [false, false, false, false, true],
63
+ [false, false, false, false, false],
64
+ ]);
65
+ });
66
+
67
+ it('marks the run of cells regardless of the word length in characters', () => {
68
+ const digraphBoard = new Board({
69
+ rows: [
70
+ [createFilledCell('ch', 0, 0), createFilledCell('e', 1, 0), createEmptyCell(2, 0)],
71
+ [createEmptyCell(0, 1), createEmptyCell(1, 1), createEmptyCell(2, 1)],
72
+ ],
73
+ });
74
+
75
+ expect(getWordCells(digraphBoard, { direction: 'horizontal', word: 'che', x: 0, y: 0 })).toEqual([
76
+ [true, true, false],
77
+ [false, false, false],
78
+ ]);
79
+ });
80
+
81
+ it('marks nothing when the starting cell is empty', () => {
82
+ expect(getWordCells(board, { direction: 'horizontal', word: 'gone', x: 0, y: 4 })).toEqual([
83
+ [false, false, false, false, false],
84
+ [false, false, false, false, false],
85
+ [false, false, false, false, false],
86
+ [false, false, false, false, false],
87
+ [false, false, false, false, false],
88
+ ]);
89
+ });
90
+ });
91
+
92
+ function createResult(cells: Cell[]): Result {
93
+ return new Result({ cells, collisions: [], id: 0, points: 0 });
94
+ }
95
+
96
+ function createPlacedCell(character: string, x: number, y: number): Cell {
97
+ return new Cell({ isEmpty: true, tile: new Tile({ character }), x, y });
98
+ }
99
+
100
+ function createFilledCell(character: string, x: number, y: number): Cell {
101
+ return new Cell({ isEmpty: false, tile: new Tile({ character }), x, y });
102
+ }
103
+
104
+ function createEmptyCell(x: number, y: number): Cell {
105
+ return new Cell({ isEmpty: true, tile: Tile.Null, x, y });
106
+ }
@@ -1,5 +1,5 @@
1
1
  import { BONUS_WORD } from '@scrabble-solver/constants';
2
- import { type Board, type Bonus, type Config } from '@scrabble-solver/types';
2
+ import { type Board, type BoardWord, type Bonus, type Result } from '@scrabble-solver/types';
3
3
 
4
4
  import { COLOR_BONUS_CHARACTER, COLOR_BONUS_CHARACTER_MULTIPLIER, COLOR_BONUS_WORD } from '@/parameters';
5
5
  import { type Direction, type Point } from '@/types';
@@ -36,8 +36,9 @@ export const getPositionInGrid = <T>(grid: T[][], constraint: (value: T) => bool
36
36
  return undefined;
37
37
  };
38
38
 
39
- export const getReachableCells = (config: Config, board: Board, charactersCount: number): boolean[][] => {
40
- const { boardHeight, boardWidth } = config;
39
+ export const getReachableCells = (board: Board, charactersCount: number): boolean[][] => {
40
+ const boardHeight = board.rowsCount;
41
+ const boardWidth = board.columnsCount;
41
42
  const reachable = createGrid(boardWidth, boardHeight, () => false);
42
43
 
43
44
  if (charactersCount === 0) {
@@ -116,3 +117,33 @@ export const getReachableCells = (config: Config, board: Board, charactersCount:
116
117
 
117
118
  return reachable;
118
119
  };
120
+
121
+ export const getReachableCellsFromResults = (board: Board, results: Result[]): boolean[][] => {
122
+ const reachable = createGrid(board.columnsCount, board.rowsCount, (x, y) => board.rows[y][x].hasTile());
123
+
124
+ for (const result of results) {
125
+ for (const cell of result.cells) {
126
+ reachable[cell.y][cell.x] = true;
127
+ }
128
+ }
129
+
130
+ return reachable;
131
+ };
132
+
133
+ export const getWordCells = (board: Board, word: BoardWord): boolean[][] => {
134
+ const wordCells = createGrid(board.columnsCount, board.rowsCount, () => false);
135
+ const isHorizontal = word.direction === 'horizontal';
136
+ let { x, y } = word;
137
+
138
+ while (x < board.columnsCount && y < board.rowsCount && board.rows[y][x].hasTile()) {
139
+ wordCells[y][x] = true;
140
+
141
+ if (isHorizontal) {
142
+ x += 1;
143
+ } else {
144
+ y += 1;
145
+ }
146
+ }
147
+
148
+ return wordCells;
149
+ };
@@ -4,24 +4,36 @@ import { findCell } from '@/lib/findCell';
4
4
  import {
5
5
  selectBoard,
6
6
  selectCharacters,
7
- selectConfig,
8
7
  selectHighlightUnreachableCells,
9
8
  selectResultCandidateCells,
9
+ selectUpToDateHoveredWord,
10
+ selectUpToDateResults,
10
11
  } from '@/state';
11
12
 
12
- import { getReachableCells } from './lib';
13
+ import { getReachableCells, getReachableCellsFromResults, getWordCells } from './lib';
13
14
 
14
15
  export const selectRowsWithCandidate = createSelector([selectBoard, selectResultCandidateCells], (board, cells) => {
15
16
  return board.rows.map((row, y) => row.map((cell, x) => findCell(cells, x, y) || cell));
16
17
  });
17
18
 
19
+ export const selectHoveredWordCells = createSelector(
20
+ [selectBoard, selectUpToDateHoveredWord],
21
+ (board, hoveredWord): boolean[][] | null => {
22
+ return hoveredWord ? getWordCells(board, hoveredWord) : null;
23
+ },
24
+ );
25
+
18
26
  export const selectReachableCells = createSelector(
19
- [selectConfig, selectBoard, selectCharacters, selectHighlightUnreachableCells],
20
- (config, board, characters, highlightUnreachableCells): boolean[][] | null => {
27
+ [selectBoard, selectCharacters, selectHighlightUnreachableCells, selectUpToDateResults],
28
+ (board, characters, highlightUnreachableCells, upToDateResults): boolean[][] | null => {
21
29
  if (!highlightUnreachableCells || characters.length === 0) {
22
30
  return null;
23
31
  }
24
32
 
25
- return getReachableCells(config, board, characters.length);
33
+ if (upToDateResults) {
34
+ return getReachableCellsFromResults(board, upToDateResults);
35
+ }
36
+
37
+ return getReachableCells(board, characters.length);
26
38
  },
27
39
  );
@@ -64,7 +64,7 @@
64
64
  gap: var(--spacing--l);
65
65
  line-height: var(--button--icon--size);
66
66
 
67
- @include media('<l') {
67
+ @include media('compact') {
68
68
  gap: var(--spacing--m);
69
69
  }
70
70
  }
@@ -112,10 +112,6 @@
112
112
  padding: var(--spacing--l);
113
113
  padding-top: 0;
114
114
 
115
- @include media('>l') {
116
- display: none;
117
- }
118
-
119
115
  > * {
120
116
  min-width: 0;
121
117
  flex: 1;
@@ -1,5 +1,7 @@
1
- import { type FunctionComponent } from 'react';
1
+ import { type FunctionComponent, useCallback } from 'react';
2
+ import { useDispatch } from 'react-redux';
2
3
 
4
+ import { Header as TableHeader, HeaderButton } from '@/components/Table';
3
5
  import { useColumns } from '@/hooks/useColumns';
4
6
  import GeoAlt from '@/icons/GeoAlt.svg';
5
7
  import OneTwoThree from '@/icons/OneTwoThree.svg';
@@ -8,27 +10,38 @@ import SquareA from '@/icons/SquareA.svg';
8
10
  import SquareB from '@/icons/SquareB.svg';
9
11
  import Squares from '@/icons/Squares.svg';
10
12
  import Words from '@/icons/Words.svg';
13
+ import { resultsSlice, selectResultsSort, useTypedSelector } from '@/state';
11
14
  import { ResultColumnId } from '@/types';
12
15
 
13
- import { HeaderButton } from './HeaderButton';
14
16
  import styles from './Results.module.scss';
15
17
 
16
18
  export const Header: FunctionComponent = () => {
19
+ const dispatch = useDispatch();
17
20
  const columns = useColumns();
21
+ const sort = useTypedSelector(selectResultsSort);
22
+
23
+ const handleSort = useCallback(
24
+ (columnId: ResultColumnId) => {
25
+ dispatch(resultsSlice.actions.sort(columnId));
26
+ },
27
+ [dispatch],
28
+ );
18
29
 
19
30
  return (
20
- <div className={styles.header}>
31
+ <TableHeader>
21
32
  {columns[ResultColumnId.Coordinates] && (
22
33
  <HeaderButton
23
34
  className={styles.coordinates}
24
35
  Icon={GeoAlt}
25
36
  id={ResultColumnId.Coordinates}
37
+ sort={sort}
26
38
  translationKey="settings.showCoordinates"
39
+ onSort={handleSort}
27
40
  />
28
41
  )}
29
42
 
30
43
  {columns[ResultColumnId.Word] && (
31
- <HeaderButton className={styles.word} id={ResultColumnId.Word} translationKey="common.word" />
44
+ <HeaderButton id={ResultColumnId.Word} primary sort={sort} translationKey="common.word" onSort={handleSort} />
32
45
  )}
33
46
 
34
47
  {columns[ResultColumnId.TilesCount] && (
@@ -36,7 +49,9 @@ export const Header: FunctionComponent = () => {
36
49
  className={styles.stat}
37
50
  Icon={Squares}
38
51
  id={ResultColumnId.TilesCount}
52
+ sort={sort}
39
53
  translationKey="common.tiles"
54
+ onSort={handleSort}
40
55
  />
41
56
  )}
42
57
 
@@ -45,7 +60,9 @@ export const Header: FunctionComponent = () => {
45
60
  className={styles.stat}
46
61
  Icon={SquareA}
47
62
  id={ResultColumnId.VowelsCount}
63
+ sort={sort}
48
64
  translationKey="common.vowels"
65
+ onSort={handleSort}
49
66
  />
50
67
  )}
51
68
 
@@ -54,7 +71,9 @@ export const Header: FunctionComponent = () => {
54
71
  className={styles.stat}
55
72
  Icon={SquareB}
56
73
  id={ResultColumnId.ConsonantsCount}
74
+ sort={sort}
57
75
  translationKey="common.consonants"
76
+ onSort={handleSort}
58
77
  />
59
78
  )}
60
79
 
@@ -63,7 +82,9 @@ export const Header: FunctionComponent = () => {
63
82
  className={styles.stat}
64
83
  Icon={Square}
65
84
  id={ResultColumnId.BlanksCount}
85
+ sort={sort}
66
86
  translationKey="common.blanks"
87
+ onSort={handleSort}
67
88
  />
68
89
  )}
69
90
 
@@ -72,7 +93,9 @@ export const Header: FunctionComponent = () => {
72
93
  className={styles.stat}
73
94
  Icon={Words}
74
95
  id={ResultColumnId.WordsCount}
96
+ sort={sort}
75
97
  translationKey="common.words"
98
+ onSort={handleSort}
76
99
  />
77
100
  )}
78
101
 
@@ -81,9 +104,11 @@ export const Header: FunctionComponent = () => {
81
104
  className={styles.points}
82
105
  Icon={OneTwoThree}
83
106
  id={ResultColumnId.Points}
107
+ sort={sort}
84
108
  translationKey="common.points"
109
+ onSort={handleSort}
85
110
  />
86
111
  )}
87
- </div>
112
+ </TableHeader>
88
113
  );
89
114
  };