scrabble-solver 2.17.0 → 2.18.1

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 (422) hide show
  1. package/.github/workflows/bunx.yml +13 -10
  2. package/.github/workflows/e2e.yml +7 -9
  3. package/.github/workflows/npx.yml +13 -10
  4. package/.nx/workspace-data/d/server-process.json +2 -2
  5. package/.nx/workspace-data/file-map.json +1406 -1406
  6. package/.nx/workspace-data/lockfile-dependencies.hash +1 -1
  7. package/.nx/workspace-data/lockfile-nodes.hash +1 -1
  8. package/.nx/workspace-data/nx_files.nxt +0 -0
  9. package/.nx/workspace-data/parsed-lock-file.dependencies.json +212 -1317
  10. package/.nx/workspace-data/parsed-lock-file.nodes.json +758 -2693
  11. package/.nx/workspace-data/project-graph.json +1076 -4158
  12. package/.nx/workspace-data/source-maps.json +204 -0
  13. package/.oxlintrc.json +8 -11
  14. package/CLAUDE.md +18 -15
  15. package/README.md +8 -1
  16. package/bun.lock +230 -499
  17. package/e2e/app.spec.ts +124 -0
  18. package/e2e/bugs/112-character-bonus.spec.ts +25 -0
  19. package/e2e/bugs/129-esc-sidebar.spec.ts +17 -0
  20. package/e2e/bugs/2-q-tile-in-english.spec.ts +19 -0
  21. package/e2e/bugs/3-x-tile-in-polish.spec.ts +19 -0
  22. package/e2e/bugs/30-click-inside-radio-icon.spec.ts +25 -0
  23. package/e2e/bugs/333-disabled-radio-cursor.spec.ts +18 -0
  24. package/e2e/bugs/342-results-sorting.spec.ts +30 -0
  25. package/e2e/bugs/352-dictionary-race-condition.spec.ts +27 -0
  26. package/e2e/bugs/363-dictionary-input-unique-words.spec.ts +27 -0
  27. package/e2e/bugs/380-transliteration-when-pasting.spec.ts +55 -0
  28. package/e2e/bugs/398-digraphs-in-created-words.spec.ts +23 -0
  29. package/e2e/bugs/401-board-state-on-game-change.spec.ts +24 -0
  30. package/e2e/features/198-highlight-unreachable-cells.spec.ts +85 -0
  31. package/e2e/features/91-hover-remaining-tile-highlights.spec.ts +76 -0
  32. package/e2e/features/hidden-coordinates-migration.spec.ts +27 -0
  33. package/e2e/features/solver-paths.spec.ts +45 -0
  34. package/e2e/lib/actions.ts +119 -0
  35. package/e2e/lib/selectors.ts +71 -0
  36. package/e2e/tsconfig.json +10 -0
  37. package/lerna.json +1 -1
  38. package/package.json +30 -52
  39. package/packages/configs/package.json +13 -4
  40. package/packages/constants/package.json +6 -2
  41. package/packages/dictionaries/package.json +12 -5
  42. package/packages/logger/package.json +7 -2
  43. package/packages/scrabble-solver/.next/BUILD_ID +1 -1
  44. package/packages/scrabble-solver/.next/build-manifest.json +22 -20
  45. package/packages/scrabble-solver/.next/cache/.previewinfo +1 -1
  46. package/packages/scrabble-solver/.next/cache/.rscinfo +1 -1
  47. package/packages/scrabble-solver/.next/cache/.tsbuildinfo +1 -1
  48. package/packages/scrabble-solver/.next/cache/webpack/client-production/0.pack +0 -0
  49. package/packages/scrabble-solver/.next/cache/webpack/client-production/index.pack +0 -0
  50. package/packages/scrabble-solver/.next/cache/webpack/client-production/index.pack.old +0 -0
  51. package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/index.pack +0 -0
  52. package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/index.pack.old +0 -0
  53. package/packages/scrabble-solver/.next/cache/webpack/server-production/0.pack +0 -0
  54. package/packages/scrabble-solver/.next/cache/webpack/server-production/index.pack +0 -0
  55. package/packages/scrabble-solver/.next/cache/webpack/server-production/index.pack.old +0 -0
  56. package/packages/scrabble-solver/.next/diagnostics/framework.json +1 -1
  57. package/packages/scrabble-solver/.next/dynamic-css-manifest.json +8 -1
  58. package/packages/scrabble-solver/.next/next-minimal-server.js.nft.json +1 -1
  59. package/packages/scrabble-solver/.next/next-server.js.nft.json +1 -1
  60. package/packages/scrabble-solver/.next/prerender-manifest.json +4 -4
  61. package/packages/scrabble-solver/.next/react-loadable-manifest.json +176 -1
  62. package/packages/scrabble-solver/.next/routes-manifest.json +1 -1
  63. package/packages/scrabble-solver/.next/server/chunks/309.js +1 -0
  64. package/packages/scrabble-solver/.next/server/chunks/{785.js → 316.js} +1 -1
  65. package/packages/scrabble-solver/.next/server/chunks/344.js +1 -0
  66. package/packages/scrabble-solver/.next/server/chunks/371.js +1 -0
  67. package/packages/scrabble-solver/.next/server/chunks/465.js +1 -0
  68. package/packages/scrabble-solver/.next/server/chunks/546.js +1 -0
  69. package/packages/scrabble-solver/.next/server/chunks/623.js +1 -0
  70. package/packages/scrabble-solver/.next/server/chunks/624.js +1 -0
  71. package/packages/scrabble-solver/.next/server/chunks/641.js +1 -0
  72. package/packages/scrabble-solver/.next/server/chunks/671.js +1 -1
  73. package/packages/scrabble-solver/.next/server/chunks/692.js +1 -0
  74. package/packages/scrabble-solver/.next/server/chunks/705.js +1 -0
  75. package/packages/scrabble-solver/.next/server/chunks/712.js +1 -1
  76. package/packages/scrabble-solver/.next/server/chunks/846.js +1 -0
  77. package/packages/scrabble-solver/.next/server/chunks/853.js +15 -0
  78. package/packages/scrabble-solver/.next/server/chunks/886.js +1 -0
  79. package/packages/scrabble-solver/.next/server/chunks/916.js +1 -0
  80. package/packages/scrabble-solver/.next/server/chunks/953.js +1 -0
  81. package/packages/scrabble-solver/.next/server/chunks/961.js +1 -0
  82. package/packages/scrabble-solver/.next/server/chunks/968.js +1 -0
  83. package/packages/scrabble-solver/.next/server/dynamic-css-manifest.js +1 -1
  84. package/packages/scrabble-solver/.next/server/middleware-build-manifest.js +1 -1
  85. package/packages/scrabble-solver/.next/server/middleware-react-loadable-manifest.js +1 -1
  86. package/packages/scrabble-solver/.next/server/pages/404.html +29 -1
  87. package/packages/scrabble-solver/.next/server/pages/500.html +29 -1
  88. package/packages/scrabble-solver/.next/server/pages/_app.js +1 -1
  89. package/packages/scrabble-solver/.next/server/pages/_app.js.nft.json +1 -1
  90. package/packages/scrabble-solver/.next/server/pages/_document.js +28 -1
  91. package/packages/scrabble-solver/.next/server/pages/_document.js.nft.json +1 -1
  92. package/packages/scrabble-solver/.next/server/pages/_error.js +28 -1
  93. package/packages/scrabble-solver/.next/server/pages/_error.js.nft.json +1 -1
  94. package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale]/[word].js.nft.json +1 -1
  95. package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale].js.nft.json +1 -1
  96. package/packages/scrabble-solver/.next/server/pages/api/solve.js +1 -1
  97. package/packages/scrabble-solver/.next/server/pages/api/solve.js.nft.json +1 -1
  98. package/packages/scrabble-solver/.next/server/pages/api/verify.js.nft.json +1 -1
  99. package/packages/scrabble-solver/.next/server/pages/api/visit.js.nft.json +1 -1
  100. package/packages/scrabble-solver/.next/server/pages/index.html +29 -1
  101. package/packages/scrabble-solver/.next/server/pages/index.js +30 -1
  102. package/packages/scrabble-solver/.next/server/pages/index.js.nft.json +1 -1
  103. package/packages/scrabble-solver/.next/server/pages/index.json +1 -1
  104. package/packages/scrabble-solver/.next/server/pages/not-found.html +29 -1
  105. package/packages/scrabble-solver/.next/server/pages/not-found.js.nft.json +1 -1
  106. package/packages/scrabble-solver/.next/server/pages-manifest.json +1 -1
  107. package/packages/scrabble-solver/.next/server/webpack-api-runtime.js +1 -1
  108. package/packages/scrabble-solver/.next/server/webpack-runtime.js +1 -1
  109. package/packages/scrabble-solver/.next/static/5KOWzYqfY1Z2rjrzptr8D/_buildManifest.js +1 -0
  110. package/packages/scrabble-solver/.next/static/chunks/101-829ab8a8d310e7a0.js +3 -0
  111. package/packages/scrabble-solver/.next/static/chunks/106.e414e88694add27f.js +1 -0
  112. package/packages/scrabble-solver/.next/static/chunks/127.125d896a19061d71.js +1 -0
  113. package/packages/scrabble-solver/.next/static/chunks/18.8b91e137d239e15c.js +1 -0
  114. package/packages/scrabble-solver/.next/static/chunks/277.6785dfaf22fdb0b7.js +1 -0
  115. package/packages/scrabble-solver/.next/static/chunks/307.67f52f13e3d60d4e.js +1 -0
  116. package/packages/scrabble-solver/.next/static/chunks/341.79535ba85bf1986f.js +1 -0
  117. package/packages/scrabble-solver/.next/static/chunks/363.01eed3233f92b6a4.js +1 -0
  118. package/packages/scrabble-solver/.next/static/chunks/422.1559ae6756912ca5.js +1 -0
  119. package/packages/scrabble-solver/.next/static/chunks/541.dcd9e8b804f88da5.js +1 -0
  120. package/packages/scrabble-solver/.next/static/chunks/631.98d42abad0a60af6.js +1 -0
  121. package/packages/scrabble-solver/.next/static/chunks/663.d2c9ca36b26695b8.js +1 -0
  122. package/packages/scrabble-solver/.next/static/chunks/711.ba7b8f900dfd40d0.js +1 -0
  123. package/packages/scrabble-solver/.next/static/chunks/744.b06c9a3de4a1d877.js +1 -0
  124. package/packages/scrabble-solver/.next/static/chunks/820.97b82f751ca49579.js +1 -0
  125. package/packages/scrabble-solver/.next/static/chunks/865.a9b563a45848a83c.js +1 -0
  126. package/packages/scrabble-solver/.next/static/chunks/912.ff8037ba9d4db5c8.js +1 -0
  127. package/packages/scrabble-solver/.next/static/chunks/939.19b33a3d3c351d73.js +1 -0
  128. package/packages/scrabble-solver/.next/static/chunks/990.61296e5870b91f11.js +1 -0
  129. package/packages/scrabble-solver/.next/static/chunks/framework-626cbd48ac5aca57.js +1 -0
  130. package/packages/scrabble-solver/.next/static/chunks/main-b92e08f3fc96dd8d.js +5 -0
  131. package/packages/scrabble-solver/.next/static/chunks/pages/_app-2e607dcd4135f340.js +1 -0
  132. package/packages/scrabble-solver/.next/static/chunks/pages/{_error-afeda7c5231b6c3c.js → _error-f0290a98eb3ed0ec.js} +1 -1
  133. package/packages/scrabble-solver/.next/static/chunks/pages/index-e0087247c995328d.js +1 -0
  134. package/packages/scrabble-solver/.next/static/chunks/pages/not-found-a59f9deeeff63d17.js +1 -0
  135. package/packages/scrabble-solver/.next/static/chunks/webpack-cfb34d175e081981.js +1 -0
  136. package/packages/scrabble-solver/.next/static/css/2d03f0da2c678dfc.css +1 -0
  137. package/packages/scrabble-solver/.next/static/css/4dbcacc1122ac6b9.css +2 -0
  138. package/packages/scrabble-solver/.next/static/css/7b6724d2df70f48f.css +1 -0
  139. package/packages/scrabble-solver/.next/static/css/b736be3bd02b5d99.css +1 -0
  140. package/packages/scrabble-solver/.next/static/css/b74cd7de82dabcdd.css +1 -0
  141. package/packages/scrabble-solver/.next/static/css/c403ce739a9c0d83.css +1 -0
  142. package/packages/scrabble-solver/.next/static/css/ca311e6c030663d4.css +1 -0
  143. package/packages/scrabble-solver/.next/static/css/dee878eb64099d92.css +1 -0
  144. package/packages/scrabble-solver/.next/static/css/fbfdce70731d0d2b.css +1 -0
  145. package/packages/scrabble-solver/.next/trace +21 -21
  146. package/packages/scrabble-solver/.next/trace-build +1 -1
  147. package/packages/scrabble-solver/empty-module.js +2 -0
  148. package/packages/scrabble-solver/next.config.js +13 -2
  149. package/packages/scrabble-solver/package.json +42 -16
  150. package/packages/scrabble-solver/public/llms.txt +11 -0
  151. package/packages/scrabble-solver/public/robots.txt +5 -0
  152. package/packages/scrabble-solver/public/service-worker.js +1 -1
  153. package/packages/scrabble-solver/public/site.webmanifest +14 -0
  154. package/packages/scrabble-solver/scripts/generate-scss-types.ts +112 -0
  155. package/packages/scrabble-solver/src/@types/scss.d.ts +6 -9
  156. package/packages/scrabble-solver/src/@types/svg.d.ts +1 -1
  157. package/packages/scrabble-solver/src/components/Alert/Alert.module.scss.d.ts +13 -0
  158. package/packages/scrabble-solver/src/components/Alert/Alert.tsx +4 -1
  159. package/packages/scrabble-solver/src/components/Badge/Badge.module.scss.d.ts +6 -0
  160. package/packages/scrabble-solver/src/components/Board/Board.module.scss +10 -0
  161. package/packages/scrabble-solver/src/components/Board/Board.module.scss.d.ts +17 -0
  162. package/packages/scrabble-solver/src/components/Board/Board.tsx +14 -26
  163. package/packages/scrabble-solver/src/components/Board/BoardPure.tsx +54 -49
  164. package/packages/scrabble-solver/src/components/Board/components/Actions/Actions.module.scss +18 -18
  165. package/packages/scrabble-solver/src/components/Board/components/Actions/Actions.module.scss.d.ts +7 -0
  166. package/packages/scrabble-solver/src/components/Board/components/Actions/Actions.tsx +6 -3
  167. package/packages/scrabble-solver/src/components/Board/components/Actions/lib.ts +3 -1
  168. package/packages/scrabble-solver/src/components/Board/components/Cell/Cell.module.scss.d.ts +13 -0
  169. package/packages/scrabble-solver/src/components/Board/components/Cell/Cell.tsx +28 -33
  170. package/packages/scrabble-solver/src/components/Board/components/InputPrompt/InputPrompt.module.scss.d.ts +10 -0
  171. package/packages/scrabble-solver/src/components/Board/components/InputPrompt/InputPrompt.tsx +1 -1
  172. package/packages/scrabble-solver/src/components/Board/components/ToggleDirectionButton/ToggleDirectionButton.module.scss.d.ts +8 -0
  173. package/packages/scrabble-solver/src/components/Board/components/ToggleDirectionButton/ToggleDirectionButton.tsx +2 -2
  174. package/packages/scrabble-solver/src/components/Board/getBoardBackground.ts +175 -0
  175. package/packages/scrabble-solver/src/components/Board/hooks/index.ts +0 -1
  176. package/packages/scrabble-solver/src/components/Board/hooks/useBoardStyle.ts +10 -20
  177. package/packages/scrabble-solver/src/components/Board/hooks/useFloatingActions.ts +3 -6
  178. package/packages/scrabble-solver/src/components/Board/hooks/useGrid.ts +6 -3
  179. package/packages/scrabble-solver/src/components/Board/selectors.ts +1 -1
  180. package/packages/scrabble-solver/src/components/Button/Button.module.scss.d.ts +12 -0
  181. package/packages/scrabble-solver/src/components/Dictionary/Dictionary.module.scss +1 -0
  182. package/packages/scrabble-solver/src/components/Dictionary/Dictionary.module.scss.d.ts +14 -0
  183. package/packages/scrabble-solver/src/components/Dictionary/Dictionary.tsx +1 -3
  184. package/packages/scrabble-solver/src/components/DictionaryInput/DictionaryInput.module.scss.d.ts +7 -0
  185. package/packages/scrabble-solver/src/components/DictionaryInput/DictionaryInput.tsx +1 -1
  186. package/packages/scrabble-solver/src/components/EmptyState/EmptyState.module.scss.d.ts +7 -0
  187. package/packages/scrabble-solver/src/components/EmptyState/EmptyState.tsx +1 -1
  188. package/packages/scrabble-solver/src/components/IconButton/IconButton.module.scss.d.ts +8 -0
  189. package/packages/scrabble-solver/src/components/Key/Key.module.scss.d.ts +6 -0
  190. package/packages/scrabble-solver/src/components/Keys/Arrows/Arrows.module.scss.d.ts +11 -0
  191. package/packages/scrabble-solver/src/components/Keys/Arrows/Arrows.tsx +4 -1
  192. package/packages/scrabble-solver/src/components/Keys/index.tsx +1 -1
  193. package/packages/scrabble-solver/src/components/Loading/Loading.module.scss.d.ts +9 -0
  194. package/packages/scrabble-solver/src/components/Loading/Loading.tsx +1 -1
  195. package/packages/scrabble-solver/src/components/Logo/Logo.tsx +4 -9
  196. package/packages/scrabble-solver/src/components/Logo/LogoBlueprint.tsx +1 -0
  197. package/packages/scrabble-solver/src/components/Modal/Modal.module.scss.d.ts +15 -0
  198. package/packages/scrabble-solver/src/components/Modal/Modal.tsx +16 -3
  199. package/packages/scrabble-solver/src/components/Modal/components/Section/Section.module.scss.d.ts +7 -0
  200. package/packages/scrabble-solver/src/components/NavButtons/NavButtons.module.scss +20 -0
  201. package/packages/scrabble-solver/src/components/NavButtons/NavButtons.module.scss.d.ts +13 -0
  202. package/packages/scrabble-solver/src/components/NavButtons/NavButtons.tsx +29 -45
  203. package/packages/scrabble-solver/src/components/NotFound/NotFound.module.scss.d.ts +9 -0
  204. package/packages/scrabble-solver/src/components/NotFound/NotFound.tsx +1 -1
  205. package/packages/scrabble-solver/src/components/PlainTiles/PlainTiles.module.scss.d.ts +11 -0
  206. package/packages/scrabble-solver/src/components/PlainTiles/PlainTiles.tsx +0 -1
  207. package/packages/scrabble-solver/src/components/PlainTiles/Tile.tsx +3 -4
  208. package/packages/scrabble-solver/src/components/PlainTiles/lib.ts +4 -19
  209. package/packages/scrabble-solver/src/components/PlainTiles/types.ts +0 -1
  210. package/packages/scrabble-solver/src/components/Progress/Progress.module.scss +7 -0
  211. package/packages/scrabble-solver/src/components/Progress/Progress.module.scss.d.ts +6 -0
  212. package/packages/scrabble-solver/src/components/Progress/Progress.tsx +2 -11
  213. package/packages/scrabble-solver/src/components/Rack/Rack.module.scss +3 -0
  214. package/packages/scrabble-solver/src/components/Rack/Rack.module.scss.d.ts +10 -0
  215. package/packages/scrabble-solver/src/components/Rack/Rack.tsx +8 -11
  216. package/packages/scrabble-solver/src/components/Rack/components/InputPrompt/InputPrompt.module.scss +2 -0
  217. package/packages/scrabble-solver/src/components/Rack/components/InputPrompt/InputPrompt.module.scss.d.ts +7 -0
  218. package/packages/scrabble-solver/src/components/Rack/components/InputPrompt/InputPrompt.tsx +1 -13
  219. package/packages/scrabble-solver/src/components/Rack/components/InputPrompt/lib.ts +1 -1
  220. package/packages/scrabble-solver/src/components/Rack/components/RackTile/RackTile.module.scss.d.ts +6 -0
  221. package/packages/scrabble-solver/src/components/Rack/components/RackTile/RackTile.tsx +5 -4
  222. package/packages/scrabble-solver/src/components/Rack/selectors.ts +1 -1
  223. package/packages/scrabble-solver/src/components/Radio/Radio.module.scss.d.ts +12 -0
  224. package/packages/scrabble-solver/src/components/Results/Cell.tsx +3 -12
  225. package/packages/scrabble-solver/src/components/Results/Header.tsx +9 -18
  226. package/packages/scrabble-solver/src/components/Results/HeaderButton.tsx +4 -5
  227. package/packages/scrabble-solver/src/components/Results/Result.tsx +4 -12
  228. package/packages/scrabble-solver/src/components/Results/Results.module.scss +4 -6
  229. package/packages/scrabble-solver/src/components/Results/Results.module.scss.d.ts +27 -0
  230. package/packages/scrabble-solver/src/components/Results/Results.tsx +4 -3
  231. package/packages/scrabble-solver/src/components/Results/SolveButton.tsx +1 -1
  232. package/packages/scrabble-solver/src/components/ResultsInput/ResultsInput.module.scss.d.ts +7 -0
  233. package/packages/scrabble-solver/src/components/ResultsInput/ResultsInput.tsx +1 -1
  234. package/packages/scrabble-solver/src/components/Solver/Solver.module.scss +6 -5
  235. package/packages/scrabble-solver/src/components/Solver/Solver.module.scss.d.ts +20 -0
  236. package/packages/scrabble-solver/src/components/Solver/Solver.tsx +19 -21
  237. package/packages/scrabble-solver/src/components/Solver/components/InsertButton/InsertButton.module.scss.d.ts +7 -0
  238. package/packages/scrabble-solver/src/components/Solver/components/InsertButton/InsertButton.tsx +1 -1
  239. package/packages/scrabble-solver/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.module.scss.d.ts +18 -0
  240. package/packages/scrabble-solver/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.tsx +3 -1
  241. package/packages/scrabble-solver/src/components/Spinner/Spinner.module.scss.d.ts +10 -0
  242. package/packages/scrabble-solver/src/components/Tile/Tile.module.scss +3 -0
  243. package/packages/scrabble-solver/src/components/Tile/Tile.module.scss.d.ts +19 -0
  244. package/packages/scrabble-solver/src/components/Tile/Tile.tsx +5 -14
  245. package/packages/scrabble-solver/src/components/Tile/TilePure.tsx +2 -12
  246. package/packages/scrabble-solver/src/components/Tooltip/Tooltip.module.scss.d.ts +7 -0
  247. package/packages/scrabble-solver/src/components/Tooltip/Tooltip.tsx +14 -3
  248. package/packages/scrabble-solver/src/components/Tooltip/useTooltip.ts +1 -0
  249. package/packages/scrabble-solver/src/hooks/useColumns.ts +4 -7
  250. package/packages/scrabble-solver/src/hooks/useDeferredRender.ts +20 -0
  251. package/packages/scrabble-solver/src/hooks/useDirection.ts +4 -3
  252. package/packages/scrabble-solver/src/hooks/useLanguage.ts +2 -2
  253. package/packages/scrabble-solver/src/hooks/useLocalStorage.ts +32 -8
  254. package/packages/scrabble-solver/src/hooks/useMedia.ts +26 -26
  255. package/packages/scrabble-solver/src/i18n/constants.ts +2 -15
  256. package/packages/scrabble-solver/src/i18n/i18n.ts +18 -17
  257. package/packages/scrabble-solver/src/i18n/languages/english.json +0 -1
  258. package/packages/scrabble-solver/src/i18n/languages/french.json +0 -1
  259. package/packages/scrabble-solver/src/i18n/languages/german.json +0 -1
  260. package/packages/scrabble-solver/src/i18n/languages/persian.json +0 -1
  261. package/packages/scrabble-solver/src/i18n/languages/polish.json +0 -1
  262. package/packages/scrabble-solver/src/i18n/languages/romanian.json +0 -1
  263. package/packages/scrabble-solver/src/i18n/languages/spanish.json +0 -1
  264. package/packages/scrabble-solver/src/i18n/languages/turkish.json +0 -1
  265. package/packages/scrabble-solver/src/i18n/localeIcons.ts +24 -0
  266. package/packages/scrabble-solver/src/lib/localeTransliterate.test.ts +5 -0
  267. package/packages/scrabble-solver/src/lib/localeTransliterate.ts +26 -6
  268. package/packages/scrabble-solver/src/lib/waitForIdleOrFirstIntent.ts +44 -0
  269. package/packages/scrabble-solver/src/modals/DictionaryModal/DictionaryModal.module.scss.d.ts +8 -0
  270. package/packages/scrabble-solver/src/modals/DictionaryModal/DictionaryModal.tsx +3 -1
  271. package/packages/scrabble-solver/src/modals/KeyMapModal/KeyMapModal.tsx +3 -1
  272. package/packages/scrabble-solver/src/modals/KeyMapModal/components/Mapping/Mapping.module.scss.d.ts +11 -0
  273. package/packages/scrabble-solver/src/modals/MenuModal/MenuModal.module.scss +5 -0
  274. package/packages/scrabble-solver/src/modals/MenuModal/MenuModal.module.scss.d.ts +10 -0
  275. package/packages/scrabble-solver/src/modals/MenuModal/MenuModal.tsx +16 -5
  276. package/packages/scrabble-solver/src/modals/RemainingTilesModal/RemainingTilesModal.module.scss +4 -1
  277. package/packages/scrabble-solver/src/modals/RemainingTilesModal/RemainingTilesModal.module.scss.d.ts +9 -0
  278. package/packages/scrabble-solver/src/modals/RemainingTilesModal/RemainingTilesModal.tsx +4 -6
  279. package/packages/scrabble-solver/src/modals/RemainingTilesModal/components/Character/Character.module.scss +1 -0
  280. package/packages/scrabble-solver/src/modals/RemainingTilesModal/components/Character/Character.module.scss.d.ts +12 -0
  281. package/packages/scrabble-solver/src/modals/RemainingTilesModal/components/Character/Character.tsx +5 -12
  282. package/packages/scrabble-solver/src/modals/ResultsModal/ResultsModal.module.scss.d.ts +8 -0
  283. package/packages/scrabble-solver/src/modals/ResultsModal/ResultsModal.tsx +8 -4
  284. package/packages/scrabble-solver/src/modals/SettingsModal/SettingsModal.tsx +2 -2
  285. package/packages/scrabble-solver/src/modals/SettingsModal/components/AutoGroupTilesSetting/AutoGroupTilesSetting.module.scss.d.ts +7 -0
  286. package/packages/scrabble-solver/src/modals/SettingsModal/components/AutoGroupTilesSetting/AutoGroupTilesSetting.tsx +1 -1
  287. package/packages/scrabble-solver/src/modals/SettingsModal/components/ConfigSetting/ConfigSetting.module.scss.d.ts +7 -0
  288. package/packages/scrabble-solver/src/modals/SettingsModal/components/ConfigSetting/ConfigSetting.tsx +1 -1
  289. package/packages/scrabble-solver/src/modals/SettingsModal/components/HighlightUnreachableCellsSetting/HighlightUnreachableCellsSetting.module.scss.d.ts +7 -0
  290. package/packages/scrabble-solver/src/modals/SettingsModal/components/HighlightUnreachableCellsSetting/HighlightUnreachableCellsSetting.tsx +1 -1
  291. package/packages/scrabble-solver/src/modals/SettingsModal/components/InputModeSetting/InputModeSetting.module.scss.d.ts +7 -0
  292. package/packages/scrabble-solver/src/modals/SettingsModal/components/InputModeSetting/InputModeSetting.tsx +1 -1
  293. package/packages/scrabble-solver/src/modals/SettingsModal/components/LocaleSetting/LocaleSetting.module.scss.d.ts +8 -0
  294. package/packages/scrabble-solver/src/modals/SettingsModal/components/LocaleSetting/LocaleSetting.tsx +24 -21
  295. package/packages/scrabble-solver/src/modals/SettingsModal/components/RemoveCellFiltersSetting/RemoveCellFiltersSetting.module.scss.d.ts +7 -0
  296. package/packages/scrabble-solver/src/modals/SettingsModal/components/RemoveCellFiltersSetting/RemoveCellFiltersSetting.tsx +1 -1
  297. package/packages/scrabble-solver/src/modals/SettingsModal/components/ShowCoordinatesSetting/ShowCoordinatesSetting.module.scss.d.ts +7 -0
  298. package/packages/scrabble-solver/src/modals/SettingsModal/components/ShowCoordinatesSetting/ShowCoordinatesSetting.tsx +1 -5
  299. package/packages/scrabble-solver/src/modals/WordsModal/WordsModal.module.scss.d.ts +11 -0
  300. package/packages/scrabble-solver/src/modals/WordsModal/WordsModal.tsx +4 -2
  301. package/packages/scrabble-solver/src/modals/preload.ts +24 -0
  302. package/packages/scrabble-solver/src/pages/_app.tsx +37 -76
  303. package/packages/scrabble-solver/src/pages/_document.tsx +112 -24
  304. package/packages/scrabble-solver/src/pages/api/solve.ts +1 -1
  305. package/packages/scrabble-solver/src/pages/index.module.scss +1 -0
  306. package/packages/scrabble-solver/src/pages/index.module.scss.d.ts +11 -0
  307. package/packages/scrabble-solver/src/pages/index.tsx +127 -40
  308. package/packages/scrabble-solver/src/pages/not-found.tsx +14 -3
  309. package/packages/scrabble-solver/src/parameters/index.ts +31 -93
  310. package/packages/scrabble-solver/src/serviceWorkerManager.ts +5 -2
  311. package/packages/scrabble-solver/src/state/actions.ts +1 -1
  312. package/packages/scrabble-solver/src/state/app/index.ts +4 -0
  313. package/packages/scrabble-solver/src/state/app/initialState.ts +6 -0
  314. package/packages/scrabble-solver/src/state/app/selectors.ts +5 -0
  315. package/packages/scrabble-solver/src/state/app/slice.ts +20 -0
  316. package/packages/scrabble-solver/src/state/app/types.ts +4 -0
  317. package/packages/scrabble-solver/src/state/board/initialState.ts +1 -2
  318. package/packages/scrabble-solver/src/state/i18n/index.ts +4 -0
  319. package/packages/scrabble-solver/src/state/i18n/initialState.ts +5 -0
  320. package/packages/scrabble-solver/src/state/i18n/selectors.ts +3 -0
  321. package/packages/scrabble-solver/src/state/i18n/slice.ts +16 -0
  322. package/packages/scrabble-solver/src/state/i18n/types.ts +7 -0
  323. package/packages/scrabble-solver/src/state/index.ts +2 -0
  324. package/packages/scrabble-solver/src/state/lib.ts +1 -1
  325. package/packages/scrabble-solver/src/state/localStorage.ts +86 -29
  326. package/packages/scrabble-solver/src/state/rack/initialState.ts +1 -2
  327. package/packages/scrabble-solver/src/state/rack/slice.ts +4 -1
  328. package/packages/scrabble-solver/src/state/results/lib.ts +5 -5
  329. package/packages/scrabble-solver/src/state/results/selectors.ts +1 -1
  330. package/packages/scrabble-solver/src/state/sagas.ts +125 -9
  331. package/packages/scrabble-solver/src/state/selectors.ts +1 -1
  332. package/packages/scrabble-solver/src/state/settings/initialState.ts +7 -10
  333. package/packages/scrabble-solver/src/state/settings/selectors.ts +7 -2
  334. package/packages/scrabble-solver/src/state/settings/slice.ts +5 -2
  335. package/packages/scrabble-solver/src/state/store.ts +4 -0
  336. package/packages/scrabble-solver/src/state/types.ts +4 -0
  337. package/packages/scrabble-solver/src/styles/_tokens.scss +1 -0
  338. package/packages/scrabble-solver/src/styles/global.scss +9 -0
  339. package/packages/scrabble-solver/src/styles/variables.module.scss +0 -21
  340. package/packages/scrabble-solver/src/styles/variables.module.scss.d.ts +27 -0
  341. package/packages/scrabble-solver/src/styles/variables.scss +46 -2
  342. package/packages/scrabble-solver/src/types/index.ts +1 -2
  343. package/packages/scrabble-solver/tsconfig.tsbuildinfo +1 -1
  344. package/packages/solver/package.json +14 -6
  345. package/packages/types/build/Direction.d.ts +1 -0
  346. package/packages/types/build/Direction.js +2 -0
  347. package/packages/types/build/ShowCoordinates.d.ts +1 -2
  348. package/packages/types/build/TextDirection.d.ts +1 -0
  349. package/packages/types/build/TextDirection.js +2 -0
  350. package/packages/types/build/index.d.ts +2 -0
  351. package/packages/types/package.json +10 -3
  352. package/packages/types/src/Direction.ts +1 -0
  353. package/packages/types/src/ShowCoordinates.ts +1 -2
  354. package/packages/types/src/TextDirection.ts +1 -0
  355. package/packages/types/src/index.ts +2 -0
  356. package/packages/word-definitions/package.json +9 -3
  357. package/packages/word-lists/package.json +11 -3
  358. package/playwright-report/index.html +49 -0
  359. package/playwright.config.ts +30 -0
  360. package/screencast.gif +0 -0
  361. package/scripts/record-screencast.ts +347 -0
  362. package/test-results/.last-run.json +4 -0
  363. package/tsconfig.json +1 -1
  364. package/babel.config.js +0 -4
  365. package/cypress/e2e/app.spec.cy.ts +0 -123
  366. package/cypress/e2e/bugs/112-character-bonus.ts +0 -40
  367. package/cypress/e2e/bugs/129-esc-sidebar.ts +0 -23
  368. package/cypress/e2e/bugs/2-q-tile-in-english.ts +0 -33
  369. package/cypress/e2e/bugs/3-x-tile-in-polish.ts +0 -33
  370. package/cypress/e2e/bugs/30-click-inside-radio-icon.ts +0 -26
  371. package/cypress/e2e/bugs/333-disabled-radio-cursor.ts +0 -24
  372. package/cypress/e2e/bugs/342-results-sorting.ts +0 -48
  373. package/cypress/e2e/bugs/348-results-word-padding.ts +0 -74
  374. package/cypress/e2e/bugs/352-dictionary-race-condition.ts +0 -29
  375. package/cypress/e2e/bugs/363-dictionary-input-unique-words.ts +0 -41
  376. package/cypress/e2e/bugs/380-transliteration-when-pasting.ts +0 -75
  377. package/cypress/e2e/bugs/398-digraphs-in-created-words.ts +0 -38
  378. package/cypress/e2e/bugs/401-board-state-on-game-change.ts +0 -36
  379. package/cypress/e2e/bugs.spec.cy.ts +0 -13
  380. package/cypress/e2e/features/198-highlight-unreachable-cells.ts +0 -101
  381. package/cypress/e2e/features/91-hover-remaining-tile-highlights.ts +0 -90
  382. package/cypress/e2e/features/solver-paths.ts +0 -66
  383. package/cypress/e2e/features.spec.cy.ts +0 -3
  384. package/cypress/support/commands.ts +0 -4
  385. package/cypress/support/e2e.ts +0 -1
  386. package/cypress/support/index.ts +0 -1
  387. package/cypress/support/lib/actions.ts +0 -104
  388. package/cypress/support/lib/selectors.ts +0 -70
  389. package/cypress/tsconfig.json +0 -10
  390. package/cypress.config.ts +0 -17
  391. package/packages/scrabble-solver/.next/server/chunks/129.js +0 -1
  392. package/packages/scrabble-solver/.next/server/chunks/318.js +0 -3
  393. package/packages/scrabble-solver/.next/server/chunks/791.js +0 -15
  394. package/packages/scrabble-solver/.next/static/OLE8_ZInlGH7ENU0NIOpf/_buildManifest.js +0 -1
  395. package/packages/scrabble-solver/.next/static/chunks/830.661816bf0de8c2f1.js +0 -1
  396. package/packages/scrabble-solver/.next/static/chunks/framework-3cf15b8041c59dd3.js +0 -1
  397. package/packages/scrabble-solver/.next/static/chunks/main-5b999f3f24ebfa84.js +0 -5
  398. package/packages/scrabble-solver/.next/static/chunks/pages/_app-7024e7a27ffbb2fa.js +0 -7
  399. package/packages/scrabble-solver/.next/static/chunks/pages/index-8b398b0a5d870773.js +0 -1
  400. package/packages/scrabble-solver/.next/static/chunks/pages/not-found-9e076427bad69d90.js +0 -1
  401. package/packages/scrabble-solver/.next/static/chunks/webpack-3943d624bbb47a64.js +0 -1
  402. package/packages/scrabble-solver/.next/static/css/a78e04fe95452380.css +0 -1
  403. package/packages/scrabble-solver/.next/static/css/aedeaada218d7280.css +0 -2
  404. package/packages/scrabble-solver/src/app-layout/AppLayoutContext.tsx +0 -25
  405. package/packages/scrabble-solver/src/app-layout/index.ts +0 -1
  406. package/packages/scrabble-solver/src/app-layout/useAppLayoutValue.ts +0 -112
  407. package/packages/scrabble-solver/src/components/Board/hooks/useBackgroundImage.tsx +0 -279
  408. package/packages/scrabble-solver/src/components/SeoMessage/SeoMessage.tsx +0 -18
  409. package/packages/scrabble-solver/src/components/SeoMessage/index.ts +0 -1
  410. package/packages/scrabble-solver/src/components/index.ts +0 -26
  411. package/packages/scrabble-solver/src/hooks/index.ts +0 -12
  412. package/packages/scrabble-solver/src/hooks/useOnWindowResize.ts +0 -11
  413. package/packages/scrabble-solver/src/hooks/useViewportSize.ts +0 -17
  414. package/packages/scrabble-solver/src/i18n/index.ts +0 -2
  415. package/packages/scrabble-solver/src/icons/Flag.svg +0 -4
  416. package/packages/scrabble-solver/src/icons/Star.svg +0 -4
  417. package/packages/scrabble-solver/src/icons/index.ts +0 -49
  418. package/packages/scrabble-solver/src/lib/getTileSizes.ts +0 -13
  419. package/packages/scrabble-solver/src/lib/index.ts +0 -24
  420. package/packages/scrabble-solver/src/modals/index.ts +0 -7
  421. /package/{cypress/support → e2e}/lib/index.ts +0 -0
  422. /package/packages/scrabble-solver/.next/static/{OLE8_ZInlGH7ENU0NIOpf → 5KOWzYqfY1Z2rjrzptr8D}/_ssgManifest.js +0 -0
@@ -1,13 +1,20 @@
1
- import { Board, type BoardJson } from '@scrabble-solver/types';
1
+ import { Board, type BoardJson, isObject, type Locale } from '@scrabble-solver/types';
2
2
  import store2 from 'store2';
3
3
 
4
- import type { Rack } from '@/types';
4
+ import type { Rack, Translations } from '@/types';
5
5
 
6
6
  import type { SettingsState } from './settings/types';
7
7
 
8
8
  const BOARD = 'board';
9
9
  const RACK = 'rack';
10
10
  const SETTINGS = 'settings';
11
+ const TRANSLATIONS = 'translations';
12
+
13
+ interface PersistedTranslations {
14
+ locale: Locale;
15
+ translations: Translations;
16
+ version: string;
17
+ }
11
18
 
12
19
  const LEGACY_KEYS: Record<keyof SettingsState, string> = {
13
20
  autoGroupTiles: 'auto-group-tiles',
@@ -21,33 +28,15 @@ const LEGACY_KEYS: Record<keyof SettingsState, string> = {
21
28
 
22
29
  const store = store2.namespace('scrabble-solver');
23
30
 
24
- /**
25
- * Introduced in 2.15.26 on 2026/04/27.
26
- * Life expectancy: 1y.
27
- */
28
- const migrateLegacySettings = (): Partial<SettingsState> => {
29
- const settings: Partial<SettingsState> = {};
30
- let hasLegacy = false;
31
-
32
- for (const [setting, legacyKey] of Object.entries(LEGACY_KEYS) as [keyof SettingsState, string][]) {
33
- if (store.has(legacyKey)) {
34
- settings[setting] = store.get(legacyKey);
35
- store.remove(legacyKey);
36
- hasLegacy = true;
37
- }
38
- }
39
-
40
- if (hasLegacy) {
41
- store.set(SETTINGS, settings, true);
42
- }
43
-
44
- return settings;
45
- };
46
-
47
31
  export const localStorage = {
48
32
  getBoard(): Board | undefined {
49
- const serialized = store.get(BOARD) as string | undefined;
50
- return serialized ? Board.fromJson(JSON.parse(serialized) as BoardJson) : undefined;
33
+ try {
34
+ const serialized = store.get(BOARD) as string | undefined;
35
+ return serialized ? Board.fromJson(JSON.parse(serialized) as BoardJson) : undefined;
36
+ } catch {
37
+ store.remove(BOARD);
38
+ return undefined;
39
+ }
51
40
  },
52
41
 
53
42
  setBoard(board: Board | undefined): void {
@@ -56,7 +45,14 @@ export const localStorage = {
56
45
  },
57
46
 
58
47
  getRack(): Rack | undefined {
59
- return store.get(RACK) as Rack | undefined;
48
+ const rack = store.get(RACK) as Rack | undefined;
49
+
50
+ if (rack !== undefined && !Array.isArray(rack)) {
51
+ store.remove(RACK);
52
+ return undefined;
53
+ }
54
+
55
+ return rack;
60
56
  },
61
57
 
62
58
  setRack(rack: Rack | undefined): void {
@@ -65,10 +61,71 @@ export const localStorage = {
65
61
 
66
62
  getSettings(): Partial<SettingsState> {
67
63
  const stored = store.get(SETTINGS) as Partial<SettingsState> | undefined;
68
- return stored ?? migrateLegacySettings();
64
+
65
+ if (stored !== undefined && !isObject(stored)) {
66
+ store.remove(SETTINGS);
67
+ }
68
+
69
+ return migrateHiddenShowCoordinates(isObject(stored) ? stored : migrateLegacySettings());
69
70
  },
70
71
 
71
72
  setSettings(settings: SettingsState): void {
72
73
  store.set(SETTINGS, settings, true);
73
74
  },
75
+
76
+ getTranslations(locale: Locale, version: string): Translations | undefined {
77
+ const stored = store.get(TRANSLATIONS) as PersistedTranslations | undefined;
78
+
79
+ if (typeof stored === 'undefined') {
80
+ return undefined;
81
+ }
82
+
83
+ if (!isObject(stored) || stored.locale !== locale || stored.version !== version || !isObject(stored.translations)) {
84
+ store.remove(TRANSLATIONS);
85
+ return undefined;
86
+ }
87
+
88
+ return stored.translations;
89
+ },
90
+
91
+ setTranslations(locale: Locale, version: string, translations: Translations): void {
92
+ store.set(TRANSLATIONS, { locale, translations, version } satisfies PersistedTranslations, true);
93
+ },
74
94
  };
95
+
96
+ /**
97
+ * Introduced in 2.15.26 on 2026/04/27.
98
+ * Life expectancy: 1y.
99
+ */
100
+ function migrateLegacySettings(): Partial<SettingsState> {
101
+ const settings: Partial<SettingsState> = {};
102
+ let hasLegacy = false;
103
+
104
+ for (const [setting, legacyKey] of Object.entries(LEGACY_KEYS) as [keyof SettingsState, string][]) {
105
+ if (store.has(legacyKey)) {
106
+ settings[setting] = store.get(legacyKey);
107
+ store.remove(legacyKey);
108
+ hasLegacy = true;
109
+ }
110
+ }
111
+
112
+ if (hasLegacy) {
113
+ store.set(SETTINGS, settings, true);
114
+ }
115
+
116
+ return settings;
117
+ }
118
+
119
+ /**
120
+ * The 'hidden' showCoordinates option was removed on 2026/08/13.
121
+ * Life expectancy: 1y.
122
+ */
123
+ function migrateHiddenShowCoordinates(settings: Partial<SettingsState>): Partial<SettingsState> {
124
+ if (String(settings.showCoordinates) !== 'hidden') {
125
+ return settings;
126
+ }
127
+
128
+ const migrated: Partial<SettingsState> = { ...settings, showCoordinates: 'original' };
129
+ store.set(SETTINGS, migrated, true);
130
+ return migrated;
131
+ }
@@ -1,6 +1,5 @@
1
1
  import { getConfig } from '@scrabble-solver/configs';
2
2
 
3
- import { localStorage } from '../localStorage';
4
3
  import { settingsInitialState } from '../settings';
5
4
 
6
5
  import type { RackState } from './types';
@@ -9,4 +8,4 @@ const defaultConfig = getConfig(settingsInitialState.game, settingsInitialState.
9
8
 
10
9
  export const rackDefaultState: RackState = Array(defaultConfig.rackSize).fill(null);
11
10
 
12
- export const rackInitialState: RackState = localStorage.getRack() ?? rackDefaultState;
11
+ export const rackInitialState: RackState = rackDefaultState;
@@ -1,7 +1,10 @@
1
1
  import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
2
2
  import { type Tile } from '@scrabble-solver/types';
3
3
 
4
- import { arrayEquals, createNullMovingComparator, inverseDirection, zipCharactersAndTiles } from '@/lib';
4
+ import { arrayEquals } from '@/lib/arrayEquals';
5
+ import { createNullMovingComparator } from '@/lib/createNullMovingComparator';
6
+ import { inverseDirection } from '@/lib/inverseDirection';
7
+ import { zipCharactersAndTiles } from '@/lib/zipCharactersAndTiles';
5
8
  import { type AutoGroupTiles, type Rack } from '@/types';
6
9
 
7
10
  import { rackDefaultState, rackInitialState } from './initialState';
@@ -1,6 +1,10 @@
1
1
  import { type Result, type ShowCoordinates } from '@scrabble-solver/types';
2
2
 
3
- import { createKeyComparator, createRegExp, createStringComparator, getCoordinate, reverseComparator } from '@/lib';
3
+ import { createKeyComparator } from '@/lib/createKeyComparator';
4
+ import { createRegExp } from '@/lib/createRegExp';
5
+ import { createStringComparator } from '@/lib/createStringComparator';
6
+ import { getCoordinate } from '@/lib/getCoordinate';
7
+ import { reverseComparator } from '@/lib/reverseComparator';
4
8
  import {
5
9
  type CellFilter,
6
10
  type Comparator,
@@ -11,10 +15,6 @@ import {
11
15
  } from '@/types';
12
16
 
13
17
  export const getCoordinates = (result: Result, showCoordinates: ShowCoordinates): string => {
14
- if (showCoordinates === 'hidden') {
15
- return '';
16
- }
17
-
18
18
  const firstCell = result.cells[0];
19
19
  const x = getCoordinate(firstCell.x, showCoordinates === 'original' ? 'letter' : 'number');
20
20
  const y = getCoordinate(firstCell.y, showCoordinates === 'original' ? 'number' : 'letter');
@@ -1,6 +1,6 @@
1
1
  import { createSelector } from '@reduxjs/toolkit';
2
2
 
3
- import { createRegExp } from '@/lib';
3
+ import { createRegExp } from '@/lib/createRegExp';
4
4
 
5
5
  import { selectCellFilters } from '../cellFilters';
6
6
  import { selectLocale, selectShowCoordinates } from '../settings';
@@ -5,18 +5,23 @@
5
5
 
6
6
  import { type PayloadAction } from '@reduxjs/toolkit';
7
7
  import { hasConfig, languages } from '@scrabble-solver/configs';
8
- import { Board, type Locale, type Result } from '@scrabble-solver/types';
8
+ import { Board, Locale, type Result } from '@scrabble-solver/types';
9
9
  import { call, delay, put, select, spawn, takeEvery, takeLatest } from 'redux-saga/effects';
10
10
 
11
- import { LOCALE_FEATURES } from '@/i18n';
12
- import { memoize } from '@/lib';
11
+ import { LOCALE_FEATURES } from '@/i18n/constants';
12
+ import { loadTranslations } from '@/i18n/i18n';
13
+ import { memoize } from '@/lib/memoize';
14
+ import { waitForFirstIntent, waitForIdleOrFirstIntent } from '@/lib/waitForIdleOrFirstIntent';
13
15
  import { findWordDefinitions, solve, verify, visit } from '@/sdk';
14
16
  import { prefetchDictionary } from '@/solver-worker';
15
17
 
16
18
  import { initialize, reset } from './actions';
19
+ import { appSlice, selectVersion } from './app';
17
20
  import { boardSlice, selectBoard } from './board';
18
21
  import { cellFiltersSlice, selectCellFilter } from './cellFilters';
19
22
  import { dictionarySlice, selectDictionary } from './dictionary';
23
+ import { i18nSlice, selectLoadedTranslations } from './i18n';
24
+ import { localStorage } from './localStorage';
20
25
  import { rackSlice, selectCharacters, selectRack } from './rack';
21
26
  import { resultsSlice } from './results';
22
27
  import {
@@ -25,8 +30,11 @@ import {
25
30
  selectLocale,
26
31
  selectLocaleAutoGroupTiles,
27
32
  selectRemoveCellFilters,
33
+ settingsInitialState,
28
34
  settingsSlice,
35
+ type SettingsState,
29
36
  } from './settings';
37
+ import { guessLocale } from './settings/lib';
30
38
  import { solveSlice } from './solve';
31
39
  import { verifySlice } from './verify';
32
40
 
@@ -119,13 +127,22 @@ function* onDictionarySubmit(): AnyGenerator {
119
127
  }
120
128
  }
121
129
 
122
- function* onInitialize(): AnyGenerator {
130
+ function* onInitialize({ payload }: PayloadAction<{ version: string }>): AnyGenerator {
131
+ try {
132
+ yield* hydratePersistedState(payload.version);
133
+ // oxlint-disable-next-line no-empty
134
+ } catch {
135
+ } finally {
136
+ yield put(appSlice.actions.hydrated());
137
+ }
138
+
123
139
  const board = yield select(selectBoard);
124
140
  const locale = yield select(selectLocale);
125
141
 
126
- yield spawn(prefetchDictionary, locale);
127
- // Detached: offline, a failed visit would otherwise tear down the whole root saga.
128
- yield spawn(visit);
142
+ yield spawn(prefetchDictionaryOnFirstIntent);
143
+ yield spawn(loadLocaleTranslations, locale);
144
+ yield spawn(preloadTranslationsWhenIdle);
145
+ yield spawn(visitWhenIdle);
129
146
 
130
147
  if (!board.isEmpty()) {
131
148
  yield* resetRack();
@@ -133,6 +150,100 @@ function* onInitialize(): AnyGenerator {
133
150
  }
134
151
  }
135
152
 
153
+ function* hydratePersistedState(version: string): AnyGenerator {
154
+ const isTouchScreen = globalThis.matchMedia?.('(hover: none)').matches ?? false;
155
+ const settings: Pick<SettingsState, 'game' | 'inputMode' | 'locale'> & Partial<SettingsState> = {
156
+ game: settingsInitialState.game,
157
+ inputMode: isTouchScreen ? ('touchscreen' as const) : ('keyboard' as const),
158
+ locale: guessLocale(),
159
+ ...localStorage.getSettings(),
160
+ };
161
+
162
+ if (!hasConfig(settings.game, settings.locale)) {
163
+ const localeDefault = Object.values(languages).find((config) => config.locale === settings.locale);
164
+ settings.game = localeDefault?.game ?? settingsInitialState.game;
165
+ settings.locale = localeDefault?.locale ?? guessLocale();
166
+ }
167
+
168
+ yield put(settingsSlice.actions.init(settings));
169
+ yield* hydratePersistedTranslations(settings.locale, version);
170
+
171
+ const config = yield select(selectConfig);
172
+ const currentBoard: Board = yield select(selectBoard);
173
+ const board = localStorage.getBoard();
174
+
175
+ if (board) {
176
+ yield put(boardSlice.actions.init(board));
177
+ } else if (currentBoard.rows.length !== config.boardHeight || currentBoard.rows[0].length !== config.boardWidth) {
178
+ yield put(boardSlice.actions.init(Board.create(config.boardWidth, config.boardHeight)));
179
+ }
180
+
181
+ const currentRack = yield select(selectRack);
182
+ const rack = localStorage.getRack();
183
+
184
+ if (rack) {
185
+ yield put(rackSlice.actions.init(rack));
186
+ } else if (currentRack.length !== config.rackSize) {
187
+ yield put(rackSlice.actions.init(Array(config.rackSize).fill(null)));
188
+ }
189
+ }
190
+
191
+ // Applied synchronously so the first post-hydration paint is already translated
192
+ function* hydratePersistedTranslations(locale: Locale, version: string): AnyGenerator {
193
+ const translations = localStorage.getTranslations(locale, version);
194
+
195
+ if (translations) {
196
+ yield put(i18nSlice.actions.loaded({ locale, translations }));
197
+ }
198
+ }
199
+
200
+ function* visitWhenIdle(): AnyGenerator {
201
+ yield call(waitForIdleOrFirstIntent);
202
+ yield call(visit);
203
+ }
204
+
205
+ function* prefetchDictionaryOnFirstIntent(): AnyGenerator {
206
+ yield call(waitForFirstIntent);
207
+ const locale = yield select(selectLocale);
208
+ yield call(prefetchDictionary, locale);
209
+ }
210
+
211
+ function* loadLocaleTranslations(locale: Locale): AnyGenerator {
212
+ const loaded = yield select(selectLoadedTranslations);
213
+
214
+ if (loaded[locale]) {
215
+ return;
216
+ }
217
+
218
+ const translations = yield call(loadTranslations, locale);
219
+ yield put(i18nSlice.actions.loaded({ locale, translations }));
220
+
221
+ const activeLocale = yield select(selectLocale);
222
+
223
+ if (locale === activeLocale) {
224
+ const version = yield select(selectVersion);
225
+ localStorage.setTranslations(locale, version, translations);
226
+ }
227
+ }
228
+
229
+ // The cache makes the next boot's first paint translated without waiting for a chunk download
230
+ function* persistTranslationsCache(locale: Locale): AnyGenerator {
231
+ const loaded = yield select(selectLoadedTranslations);
232
+ const version = yield select(selectVersion);
233
+
234
+ if (loaded[locale]) {
235
+ localStorage.setTranslations(locale, version, loaded[locale]);
236
+ }
237
+ }
238
+
239
+ function* preloadTranslationsWhenIdle(): AnyGenerator {
240
+ yield call(waitForIdleOrFirstIntent);
241
+
242
+ for (const locale of Object.values(Locale)) {
243
+ yield spawn(loadLocaleTranslations, locale);
244
+ }
245
+ }
246
+
136
247
  function* onReset(): AnyGenerator {
137
248
  const config = yield select(selectConfig);
138
249
 
@@ -146,7 +257,9 @@ function* onReset(): AnyGenerator {
146
257
  }
147
258
 
148
259
  function* onLocaleChange({ payload: locale }: PayloadAction<Locale>): AnyGenerator {
260
+ yield spawn(loadLocaleTranslations, locale);
149
261
  yield spawn(prefetchDictionary, locale);
262
+ yield* persistTranslationsCache(locale);
150
263
 
151
264
  const game = yield select(selectGame);
152
265
 
@@ -176,11 +289,14 @@ function* onResultCandidateChange({ payload: result }: PayloadAction<Result | nu
176
289
  return;
177
290
  }
178
291
 
179
- yield delay(SUBMIT_DELAY);
180
-
181
292
  const locale: Locale = yield select(selectLocale);
182
293
  const uniqueWords = Array.from(new Set(result.words));
183
294
  const input = uniqueWords.join(LOCALE_FEATURES[locale].separator);
295
+
296
+ if (!memoizedFindWordDefinitions.hasCache(locale, input)) {
297
+ yield delay(SUBMIT_DELAY);
298
+ }
299
+
184
300
  yield put(dictionarySlice.actions.changeInput(input));
185
301
  yield put(dictionarySlice.actions.submit());
186
302
  }
@@ -1,6 +1,6 @@
1
1
  import { createSelector } from '@reduxjs/toolkit';
2
2
 
3
- import { unorderedArraysEqual } from '@/lib';
3
+ import { unorderedArraysEqual } from '@/lib/unorderedArraysEqual';
4
4
 
5
5
  import { selectBoard } from './board';
6
6
  import { getRemainingTiles } from './lib';
@@ -1,19 +1,16 @@
1
- import { Game } from '@scrabble-solver/types';
1
+ import { Game, Locale } from '@scrabble-solver/types';
2
2
 
3
- import { localStorage } from '../localStorage';
4
-
5
- import { guessLocale } from './lib';
6
3
  import type { SettingsState } from './types';
7
4
 
8
- const isTouchScreen = typeof globalThis.matchMedia !== 'undefined' && globalThis.matchMedia('(hover: none)').matches;
9
-
5
+ /**
6
+ * Deterministic defaults shared by the server render and the client's hydration render.
7
+ */
10
8
  export const settingsInitialState: SettingsState = {
11
9
  autoGroupTiles: null,
12
10
  game: Game.Scrabble,
13
11
  highlightUnreachableCells: false,
14
- inputMode: isTouchScreen ? 'touchscreen' : 'keyboard',
15
- locale: guessLocale(),
12
+ inputMode: 'keyboard',
13
+ locale: Locale.EN_US,
16
14
  removeCellFilters: 'always',
17
- showCoordinates: 'hidden',
18
- ...localStorage.getSettings(),
15
+ showCoordinates: 'original',
19
16
  };
@@ -3,8 +3,10 @@ import { getConfig } from '@scrabble-solver/configs';
3
3
  import { BLANK } from '@scrabble-solver/constants';
4
4
  import { type Cell, type Tile } from '@scrabble-solver/types';
5
5
 
6
- import { i18n, LOCALE_FEATURES } from '@/i18n';
6
+ import { LOCALE_FEATURES } from '@/i18n/constants';
7
+ import { englishTranslations } from '@/i18n/i18n';
7
8
 
9
+ import { selectLoadedTranslations } from '../i18n';
8
10
  import type { RootState } from '../types';
9
11
 
10
12
  const selectCell = (_: unknown, cell: Cell): Cell => cell;
@@ -68,6 +70,9 @@ export const selectCellIsValid = createSelector([selectConfig, selectCell], (con
68
70
  return config.tiles.some((tile) => tile.character === cell.tile.character);
69
71
  });
70
72
 
71
- export const selectTranslations = createSelector([selectLocale], (locale) => i18n[locale]);
73
+ export const selectTranslations = createSelector(
74
+ [selectLocale, selectLoadedTranslations],
75
+ (locale, translations) => translations[locale] ?? englishTranslations,
76
+ );
72
77
 
73
78
  export const selectRemoveCellFilters = createSelector([selectSettings], (settings) => settings.removeCellFilters);
@@ -44,8 +44,11 @@ export const settingsSlice = createSlice({
44
44
  return { ...state, removeCellFilters };
45
45
  },
46
46
 
47
- init: (state, action: PayloadAction<Partial<Pick<typeof settingsInitialState, 'game' | 'locale'>>>) => {
48
- return { ...state, ...action.payload };
47
+ init: (state, action: PayloadAction<Partial<typeof settingsInitialState>>) => {
48
+ const settings = { ...state, ...action.payload };
49
+ const keys = Object.keys(settings) as (keyof typeof settingsInitialState)[];
50
+ const hasDiff = keys.some((key) => settings[key] !== state[key]);
51
+ return hasDiff ? settings : state;
49
52
  },
50
53
  },
51
54
  });
@@ -1,10 +1,12 @@
1
1
  import { configureStore } from '@reduxjs/toolkit';
2
2
  import reduxSaga from 'redux-saga';
3
3
 
4
+ import { appSlice } from './app';
4
5
  import { boardSlice } from './board';
5
6
  import { cellFiltersSlice } from './cellFilters';
6
7
  import { dictionarySlice } from './dictionary';
7
8
  import { hoveredTileSlice } from './hoveredTile';
9
+ import { i18nSlice } from './i18n';
8
10
  import { rackSlice } from './rack';
9
11
  import { resultsSlice } from './results';
10
12
  import { rootSaga } from './sagas';
@@ -16,10 +18,12 @@ const sagaMiddleware = reduxSaga();
16
18
 
17
19
  export const store = configureStore({
18
20
  reducer: {
21
+ app: appSlice.reducer,
19
22
  board: boardSlice.reducer,
20
23
  cellFilters: cellFiltersSlice.reducer,
21
24
  dictionary: dictionarySlice.reducer,
22
25
  hoveredTile: hoveredTileSlice.reducer,
26
+ i18n: i18nSlice.reducer,
23
27
  rack: rackSlice.reducer,
24
28
  results: resultsSlice.reducer,
25
29
  settings: settingsSlice.reducer,
@@ -1,7 +1,9 @@
1
+ import type { AppState } from './app';
1
2
  import type { BoardState } from './board';
2
3
  import type { CellFiltersState } from './cellFilters';
3
4
  import type { DictionaryState } from './dictionary';
4
5
  import type { HoveredTileState } from './hoveredTile';
6
+ import type { I18nState } from './i18n';
5
7
  import type { RackState } from './rack';
6
8
  import type { ResultsState } from './results';
7
9
  import type { SettingsState } from './settings';
@@ -9,10 +11,12 @@ import type { SolveState } from './solve';
9
11
  import type { VerifyState } from './verify';
10
12
 
11
13
  export type RootState = {
14
+ app: AppState;
12
15
  board: BoardState;
13
16
  cellFilters: CellFiltersState;
14
17
  dictionary: DictionaryState;
15
18
  hoveredTile: HoveredTileState;
19
+ i18n: I18nState;
16
20
  rack: RackState;
17
21
  results: ResultsState;
18
22
  settings: SettingsState;
@@ -57,3 +57,4 @@ $modal-width: 370px;
57
57
  $results-item-height: 40px;
58
58
  $solver-column-width: 580px;
59
59
  $text-input-height: 40px;
60
+ $tile-size-max: 80px;
@@ -50,3 +50,12 @@ h4 {
50
50
  nextjs-portal {
51
51
  display: none;
52
52
  }
53
+
54
+ /**
55
+ * Added pre-paint by _document's inline script when the persisted game differs from the
56
+ * prerendered default board; removed after hydration applies the persisted config.
57
+ * The class name must match CONFIG_PENDING_CLASS in parameters/index.ts.
58
+ */
59
+ html.config-pending main {
60
+ visibility: hidden;
61
+ }
@@ -7,23 +7,16 @@
7
7
  breakpointL: strip-unit($breakpoint-l);
8
8
  breakpointXl: strip-unit($breakpoint-xl);
9
9
 
10
- borderColor: $border-color;
11
10
  borderColorLight: $border-color-light;
12
- borderRadius: strip-unit($border-radius);
13
11
  borderWidth: strip-unit($border-width);
14
12
 
15
- colorBlue: $color-blue;
16
13
  colorBlueLight: $color-blue-light;
17
- colorGreen: $color-green;
18
14
  colorGreenLight: $color-green-light;
19
- colorRed: $color-red;
20
15
  colorRedLight: $color-red-light;
21
16
  colorVioletLight: $color-violet-light;
22
- colorYellow: $color-yellow;
23
17
  colorYellowLight: $color-yellow-light;
24
18
 
25
19
  colorBackground: $color-background;
26
- colorForegroundSecondary: $color-foreground-secondary;
27
20
 
28
21
  colorBonusCharacterMultiplier2: $color-bonus-character-multiplier-2;
29
22
  colorBonusCharacterMultiplier3: $color-bonus-character-multiplier-3;
@@ -32,22 +25,8 @@
32
25
  colorBonusWordMultiplier3: $color-bonus-word-multiplier-3;
33
26
  colorBonusWordMultiplier4: $color-bonus-word-multiplier-4;
34
27
 
35
- spacingXs: strip-unit($spacing-xs);
36
- spacingS: strip-unit($spacing-s);
37
- spacingM: strip-unit($spacing-m);
38
- spacingL: strip-unit($spacing-l);
39
- spacingXl: strip-unit($spacing-xl);
40
-
41
28
  transitionDuration: strip-unit($transition-duration);
42
29
  transitionDurationLong: strip-unit($transition-duration-long);
43
30
 
44
- buttonIconSize: strip-unit($button-icon-size);
45
- dictionaryHeight: strip-unit($dictionary-height);
46
- dictionaryHeightMobile: strip-unit($dictionary-height-mobile);
47
- logoHeight: strip-unit($logo-height);
48
- logoHeightSmall: strip-unit($logo-height-small);
49
- modalWidth: strip-unit($modal-width);
50
31
  resultsItemHeight: strip-unit($results-item-height);
51
- solverColumnWidth: strip-unit($solver-column-width);
52
- textInputHeight: strip-unit($text-input-height);
53
32
  }
@@ -0,0 +1,27 @@
1
+ // Generated by scripts/generate-scss-types.ts - do not edit.
2
+ declare const styles: {
3
+ readonly borderColorLight: string;
4
+ readonly borderWidth: string;
5
+ readonly breakpointL: string;
6
+ readonly breakpointM: string;
7
+ readonly breakpointS: string;
8
+ readonly breakpointXl: string;
9
+ readonly breakpointXs: string;
10
+ readonly colorBackground: string;
11
+ readonly colorBlueLight: string;
12
+ readonly colorBonusCharacterMultiplier2: string;
13
+ readonly colorBonusCharacterMultiplier3: string;
14
+ readonly colorBonusCharacterMultiplier4: string;
15
+ readonly colorBonusWordMultiplier2: string;
16
+ readonly colorBonusWordMultiplier3: string;
17
+ readonly colorBonusWordMultiplier4: string;
18
+ readonly colorGreenLight: string;
19
+ readonly colorRedLight: string;
20
+ readonly colorVioletLight: string;
21
+ readonly colorYellowLight: string;
22
+ readonly resultsItemHeight: string;
23
+ readonly transitionDuration: string;
24
+ readonly transitionDurationLong: string;
25
+ };
26
+
27
+ export default styles;