@scrabble-solver/scrabble-solver 2.16.7 → 2.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (370) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +22 -20
  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 +8 -1
  16. package/.next/next-minimal-server.js.nft.json +1 -1
  17. package/.next/next-server.js.nft.json +1 -1
  18. package/.next/prerender-manifest.json +4 -4
  19. package/.next/react-loadable-manifest.json +176 -1
  20. package/.next/required-server-files.js +3 -2
  21. package/.next/required-server-files.json +3 -2
  22. package/.next/routes-manifest.json +1 -1
  23. package/.next/server/chunks/30.js +1 -1
  24. package/.next/server/chunks/309.js +1 -0
  25. package/.next/server/chunks/316.js +1 -0
  26. package/.next/server/chunks/344.js +1 -0
  27. package/.next/server/chunks/371.js +1 -0
  28. package/.next/server/chunks/392.js +1 -1
  29. package/.next/server/chunks/465.js +1 -0
  30. package/.next/server/chunks/546.js +1 -0
  31. package/.next/server/chunks/623.js +1 -0
  32. package/.next/server/chunks/624.js +1 -0
  33. package/.next/server/chunks/641.js +1 -0
  34. package/.next/server/chunks/671.js +1 -1
  35. package/.next/server/chunks/692.js +1 -0
  36. package/.next/server/chunks/705.js +1 -0
  37. package/.next/server/chunks/712.js +1 -1
  38. package/.next/server/chunks/846.js +1 -0
  39. package/.next/server/chunks/853.js +15 -0
  40. package/.next/server/chunks/886.js +1 -0
  41. package/.next/server/chunks/916.js +1 -0
  42. package/.next/server/chunks/953.js +1 -0
  43. package/.next/server/chunks/961.js +1 -0
  44. package/.next/server/chunks/968.js +1 -0
  45. package/.next/server/dynamic-css-manifest.js +1 -1
  46. package/.next/server/middleware-build-manifest.js +1 -1
  47. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  48. package/.next/server/pages/404.html +28 -1
  49. package/.next/server/pages/500.html +28 -1
  50. package/.next/server/pages/_app.js +1 -1
  51. package/.next/server/pages/_app.js.nft.json +1 -1
  52. package/.next/server/pages/_document.js +28 -1
  53. package/.next/server/pages/_document.js.nft.json +1 -1
  54. package/.next/server/pages/_error.js +28 -1
  55. package/.next/server/pages/_error.js.nft.json +1 -1
  56. package/.next/server/pages/api/dictionary/[locale]/[word].js +1 -1
  57. package/.next/server/pages/api/dictionary/[locale]/[word].js.nft.json +1 -1
  58. package/.next/server/pages/api/dictionary/[locale].js +1 -1
  59. package/.next/server/pages/api/dictionary/[locale].js.nft.json +1 -1
  60. package/.next/server/pages/api/solve.js +1 -1
  61. package/.next/server/pages/api/solve.js.nft.json +1 -1
  62. package/.next/server/pages/api/verify.js +1 -1
  63. package/.next/server/pages/api/verify.js.nft.json +1 -1
  64. package/.next/server/pages/api/visit.js.nft.json +1 -1
  65. package/.next/server/pages/index.html +28 -1
  66. package/.next/server/pages/index.js +30 -1
  67. package/.next/server/pages/index.js.nft.json +1 -1
  68. package/.next/server/pages/index.json +1 -1
  69. package/.next/server/pages/not-found.html +28 -1
  70. package/.next/server/pages/not-found.js.nft.json +1 -1
  71. package/.next/server/pages-manifest.json +1 -1
  72. package/.next/server/webpack-api-runtime.js +1 -1
  73. package/.next/server/webpack-runtime.js +1 -1
  74. package/.next/static/FL82obwzFHRzvV6z76I-U/_buildManifest.js +1 -0
  75. package/.next/static/chunks/101-829ab8a8d310e7a0.js +3 -0
  76. package/.next/static/chunks/106.e414e88694add27f.js +1 -0
  77. package/.next/static/chunks/127.125d896a19061d71.js +1 -0
  78. package/.next/static/chunks/18.8b91e137d239e15c.js +1 -0
  79. package/.next/static/chunks/277.6785dfaf22fdb0b7.js +1 -0
  80. package/.next/static/chunks/307.67f52f13e3d60d4e.js +1 -0
  81. package/.next/static/chunks/341.79535ba85bf1986f.js +1 -0
  82. package/.next/static/chunks/363.01eed3233f92b6a4.js +1 -0
  83. package/.next/static/chunks/422.1559ae6756912ca5.js +1 -0
  84. package/.next/static/chunks/541.dcd9e8b804f88da5.js +1 -0
  85. package/.next/static/chunks/583.7a46807be14c377e.js +1 -0
  86. package/.next/static/chunks/631.98d42abad0a60af6.js +1 -0
  87. package/.next/static/chunks/663.d2c9ca36b26695b8.js +1 -0
  88. package/.next/static/chunks/711.ba7b8f900dfd40d0.js +1 -0
  89. package/.next/static/chunks/744.b06c9a3de4a1d877.js +1 -0
  90. package/.next/static/chunks/820.97b82f751ca49579.js +1 -0
  91. package/.next/static/chunks/865.a9b563a45848a83c.js +1 -0
  92. package/.next/static/chunks/912.ff8037ba9d4db5c8.js +1 -0
  93. package/.next/static/chunks/939.19b33a3d3c351d73.js +1 -0
  94. package/.next/static/chunks/990.61296e5870b91f11.js +1 -0
  95. package/.next/static/chunks/framework-626cbd48ac5aca57.js +1 -0
  96. package/.next/static/chunks/main-b92e08f3fc96dd8d.js +5 -0
  97. package/.next/static/chunks/pages/_app-ba5d628e81a15461.js +1 -0
  98. package/.next/static/chunks/pages/{_error-afeda7c5231b6c3c.js → _error-f0290a98eb3ed0ec.js} +1 -1
  99. package/.next/static/chunks/pages/index-5f9fa6ac3cd3d97b.js +1 -0
  100. package/.next/static/chunks/pages/not-found-23d61fd3ed791cad.js +1 -0
  101. package/.next/static/chunks/webpack-e34c3ae12275e978.js +1 -0
  102. package/.next/static/css/2d03f0da2c678dfc.css +1 -0
  103. package/.next/static/css/7b6724d2df70f48f.css +1 -0
  104. package/.next/static/css/abc2425d3ba607bc.css +1 -0
  105. package/.next/static/css/b736be3bd02b5d99.css +1 -0
  106. package/.next/static/css/b74cd7de82dabcdd.css +1 -0
  107. package/.next/static/css/c403ce739a9c0d83.css +1 -0
  108. package/.next/static/css/ca311e6c030663d4.css +1 -0
  109. package/.next/static/css/dee878eb64099d92.css +1 -0
  110. package/.next/static/css/fbfdce70731d0d2b.css +1 -0
  111. package/.next/trace +21 -21
  112. package/.next/trace-build +1 -1
  113. package/empty-module.js +2 -0
  114. package/next.config.js +13 -8
  115. package/package.json +35 -16
  116. package/public/llms.txt +11 -0
  117. package/public/robots.txt +4 -0
  118. package/scripts/generate-scss-types.ts +112 -0
  119. package/src/@types/scss.d.ts +6 -9
  120. package/src/@types/svg.d.ts +1 -1
  121. package/src/components/Alert/Alert.module.scss.d.ts +13 -0
  122. package/src/components/Alert/Alert.tsx +4 -1
  123. package/src/components/Badge/Badge.module.scss.d.ts +6 -0
  124. package/src/components/Board/Board.module.scss +10 -0
  125. package/src/components/Board/Board.module.scss.d.ts +17 -0
  126. package/src/components/Board/Board.tsx +14 -26
  127. package/src/components/Board/BoardPure.tsx +54 -49
  128. package/src/components/Board/components/Actions/Actions.module.scss +18 -18
  129. package/src/components/Board/components/Actions/Actions.module.scss.d.ts +7 -0
  130. package/src/components/Board/components/Actions/Actions.tsx +6 -3
  131. package/src/components/Board/components/Actions/lib.ts +3 -1
  132. package/src/components/Board/components/Cell/Cell.module.scss.d.ts +13 -0
  133. package/src/components/Board/components/Cell/Cell.tsx +32 -34
  134. package/src/components/Board/components/InputPrompt/InputPrompt.module.scss.d.ts +10 -0
  135. package/src/components/Board/components/InputPrompt/InputPrompt.tsx +1 -1
  136. package/src/components/Board/components/ToggleDirectionButton/ToggleDirectionButton.module.scss.d.ts +8 -0
  137. package/src/components/Board/components/ToggleDirectionButton/ToggleDirectionButton.tsx +2 -2
  138. package/src/components/Board/getBoardBackground.ts +175 -0
  139. package/src/components/Board/hooks/index.ts +0 -1
  140. package/src/components/Board/hooks/useBoardStyle.ts +10 -20
  141. package/src/components/Board/hooks/useFloatingActions.ts +3 -6
  142. package/src/components/Board/hooks/useGrid.ts +23 -9
  143. package/src/components/Board/selectors.ts +1 -1
  144. package/src/components/Button/Button.module.scss.d.ts +12 -0
  145. package/src/components/Dictionary/Dictionary.module.scss +8 -22
  146. package/src/components/Dictionary/Dictionary.module.scss.d.ts +14 -0
  147. package/src/components/Dictionary/Dictionary.tsx +1 -10
  148. package/src/components/DictionaryInput/DictionaryInput.module.scss.d.ts +7 -0
  149. package/src/components/DictionaryInput/DictionaryInput.tsx +1 -1
  150. package/src/components/EmptyState/EmptyState.module.scss.d.ts +7 -0
  151. package/src/components/EmptyState/EmptyState.tsx +1 -1
  152. package/src/components/IconButton/IconButton.module.scss.d.ts +8 -0
  153. package/src/components/Key/Key.module.scss.d.ts +6 -0
  154. package/src/components/Keys/Arrows/Arrows.module.scss.d.ts +11 -0
  155. package/src/components/Keys/Arrows/Arrows.tsx +4 -1
  156. package/src/components/Keys/index.tsx +1 -1
  157. package/src/components/Loading/Loading.module.scss.d.ts +9 -0
  158. package/src/components/Loading/Loading.tsx +1 -1
  159. package/src/components/Logo/Logo.tsx +4 -9
  160. package/src/components/Logo/LogoBlueprint.tsx +1 -0
  161. package/src/components/Modal/Modal.module.scss.d.ts +15 -0
  162. package/src/components/Modal/Modal.tsx +16 -3
  163. package/src/components/Modal/components/Section/Section.module.scss.d.ts +7 -0
  164. package/src/components/NavButtons/NavButtons.module.scss +20 -0
  165. package/src/components/NavButtons/NavButtons.module.scss.d.ts +13 -0
  166. package/src/components/NavButtons/NavButtons.tsx +29 -45
  167. package/src/components/NotFound/NotFound.module.scss.d.ts +9 -0
  168. package/src/components/NotFound/NotFound.tsx +1 -1
  169. package/src/components/PlainTiles/PlainTiles.module.scss.d.ts +11 -0
  170. package/src/components/PlainTiles/PlainTiles.tsx +0 -1
  171. package/src/components/PlainTiles/Tile.tsx +3 -4
  172. package/src/components/PlainTiles/lib.ts +4 -19
  173. package/src/components/PlainTiles/types.ts +0 -1
  174. package/src/components/Progress/Progress.module.scss +7 -0
  175. package/src/components/Progress/Progress.module.scss.d.ts +6 -0
  176. package/src/components/Progress/Progress.tsx +2 -11
  177. package/src/components/Rack/Rack.module.scss +3 -0
  178. package/src/components/Rack/Rack.module.scss.d.ts +10 -0
  179. package/src/components/Rack/Rack.tsx +8 -11
  180. package/src/components/Rack/components/InputPrompt/InputPrompt.module.scss +2 -0
  181. package/src/components/Rack/components/InputPrompt/InputPrompt.module.scss.d.ts +7 -0
  182. package/src/components/Rack/components/InputPrompt/InputPrompt.tsx +1 -13
  183. package/src/components/Rack/components/InputPrompt/lib.ts +1 -1
  184. package/src/components/Rack/components/RackTile/RackTile.module.scss.d.ts +6 -0
  185. package/src/components/Rack/components/RackTile/RackTile.tsx +5 -4
  186. package/src/components/Rack/selectors.ts +1 -1
  187. package/src/components/Radio/Radio.module.scss.d.ts +12 -0
  188. package/src/components/Results/Cell.tsx +3 -12
  189. package/src/components/Results/Header.tsx +9 -18
  190. package/src/components/Results/HeaderButton.tsx +4 -5
  191. package/src/components/Results/Result.tsx +4 -15
  192. package/src/components/Results/Results.module.scss +4 -7
  193. package/src/components/Results/Results.module.scss.d.ts +27 -0
  194. package/src/components/Results/Results.tsx +14 -5
  195. package/src/components/Results/SolveButton.tsx +1 -1
  196. package/src/components/Results/types.ts +3 -3
  197. package/src/components/ResultsInput/ResultsInput.module.scss.d.ts +7 -0
  198. package/src/components/ResultsInput/ResultsInput.tsx +1 -1
  199. package/src/components/Solver/Solver.module.scss +6 -5
  200. package/src/components/Solver/Solver.module.scss.d.ts +20 -0
  201. package/src/components/Solver/Solver.tsx +23 -24
  202. package/src/components/Solver/components/InsertButton/InsertButton.module.scss.d.ts +7 -0
  203. package/src/components/Solver/components/InsertButton/InsertButton.tsx +1 -1
  204. package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.module.scss.d.ts +18 -0
  205. package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.tsx +3 -1
  206. package/src/components/Spinner/Spinner.module.scss.d.ts +10 -0
  207. package/src/components/Tile/Tile.module.scss +3 -0
  208. package/src/components/Tile/Tile.module.scss.d.ts +19 -0
  209. package/src/components/Tile/Tile.tsx +5 -14
  210. package/src/components/Tile/TilePure.tsx +2 -12
  211. package/src/components/Tooltip/Tooltip.module.scss.d.ts +7 -0
  212. package/src/components/Tooltip/Tooltip.tsx +14 -3
  213. package/src/components/Tooltip/useTooltip.ts +2 -1
  214. package/src/hooks/useColumns.ts +4 -7
  215. package/src/hooks/useDeferredRender.ts +20 -0
  216. package/src/hooks/useDirection.ts +4 -3
  217. package/src/hooks/useLanguage.ts +2 -2
  218. package/src/hooks/useLocalStorage.ts +32 -8
  219. package/src/hooks/useMedia.ts +26 -26
  220. package/src/i18n/constants.ts +2 -15
  221. package/src/i18n/i18n.ts +18 -17
  222. package/src/i18n/languages/english.json +0 -1
  223. package/src/i18n/languages/french.json +0 -1
  224. package/src/i18n/languages/german.json +0 -1
  225. package/src/i18n/languages/persian.json +0 -1
  226. package/src/i18n/languages/polish.json +0 -1
  227. package/src/i18n/languages/romanian.json +0 -1
  228. package/src/i18n/languages/spanish.json +0 -1
  229. package/src/i18n/languages/turkish.json +0 -1
  230. package/src/i18n/localeIcons.ts +24 -0
  231. package/src/lib/localeTransliterate.test.ts +5 -0
  232. package/src/lib/localeTransliterate.ts +26 -6
  233. package/src/lib/waitForIdleOrFirstIntent.ts +33 -0
  234. package/src/modals/DictionaryModal/DictionaryModal.module.scss.d.ts +8 -0
  235. package/src/modals/DictionaryModal/DictionaryModal.tsx +3 -1
  236. package/src/modals/KeyMapModal/KeyMapModal.tsx +3 -1
  237. package/src/modals/KeyMapModal/components/Mapping/Mapping.module.scss.d.ts +11 -0
  238. package/src/modals/MenuModal/MenuModal.module.scss +5 -0
  239. package/src/modals/MenuModal/MenuModal.module.scss.d.ts +10 -0
  240. package/src/modals/MenuModal/MenuModal.tsx +16 -5
  241. package/src/modals/RemainingTilesModal/RemainingTilesModal.module.scss +4 -1
  242. package/src/modals/RemainingTilesModal/RemainingTilesModal.module.scss.d.ts +9 -0
  243. package/src/modals/RemainingTilesModal/RemainingTilesModal.tsx +4 -6
  244. package/src/modals/RemainingTilesModal/components/Character/Character.module.scss +1 -0
  245. package/src/modals/RemainingTilesModal/components/Character/Character.module.scss.d.ts +12 -0
  246. package/src/modals/RemainingTilesModal/components/Character/Character.tsx +5 -12
  247. package/src/modals/ResultsModal/ResultsModal.module.scss.d.ts +8 -0
  248. package/src/modals/ResultsModal/ResultsModal.tsx +8 -4
  249. package/src/modals/SettingsModal/SettingsModal.tsx +2 -2
  250. package/src/modals/SettingsModal/components/AutoGroupTilesSetting/AutoGroupTilesSetting.module.scss.d.ts +7 -0
  251. package/src/modals/SettingsModal/components/AutoGroupTilesSetting/AutoGroupTilesSetting.tsx +1 -1
  252. package/src/modals/SettingsModal/components/ConfigSetting/ConfigSetting.module.scss.d.ts +7 -0
  253. package/src/modals/SettingsModal/components/ConfigSetting/ConfigSetting.tsx +1 -1
  254. package/src/modals/SettingsModal/components/HighlightUnreachableCellsSetting/HighlightUnreachableCellsSetting.module.scss.d.ts +7 -0
  255. package/src/modals/SettingsModal/components/HighlightUnreachableCellsSetting/HighlightUnreachableCellsSetting.tsx +1 -1
  256. package/src/modals/SettingsModal/components/InputModeSetting/InputModeSetting.module.scss.d.ts +7 -0
  257. package/src/modals/SettingsModal/components/InputModeSetting/InputModeSetting.tsx +1 -1
  258. package/src/modals/SettingsModal/components/LocaleSetting/LocaleSetting.module.scss.d.ts +8 -0
  259. package/src/modals/SettingsModal/components/LocaleSetting/LocaleSetting.tsx +24 -21
  260. package/src/modals/SettingsModal/components/RemoveCellFiltersSetting/RemoveCellFiltersSetting.module.scss.d.ts +7 -0
  261. package/src/modals/SettingsModal/components/RemoveCellFiltersSetting/RemoveCellFiltersSetting.tsx +1 -1
  262. package/src/modals/SettingsModal/components/ShowCoordinatesSetting/ShowCoordinatesSetting.module.scss.d.ts +7 -0
  263. package/src/modals/SettingsModal/components/ShowCoordinatesSetting/ShowCoordinatesSetting.tsx +1 -5
  264. package/src/modals/WordsModal/WordsModal.module.scss.d.ts +11 -0
  265. package/src/modals/WordsModal/WordsModal.tsx +4 -2
  266. package/src/modals/preload.ts +24 -0
  267. package/src/pages/_app.tsx +5 -8
  268. package/src/pages/_document.tsx +91 -5
  269. package/src/pages/api/dictionary/[locale]/[word].ts +2 -2
  270. package/src/pages/api/dictionary/[locale]/index.ts +80 -3
  271. package/src/pages/api/solve.ts +3 -3
  272. package/src/pages/api/verify.ts +12 -3
  273. package/src/pages/index.module.scss +1 -0
  274. package/src/pages/index.module.scss.d.ts +11 -0
  275. package/src/pages/index.tsx +98 -38
  276. package/src/pages/not-found.tsx +1 -1
  277. package/src/parameters/index.ts +24 -93
  278. package/src/sdk/solve.ts +13 -7
  279. package/src/sdk/verify.ts +12 -6
  280. package/src/service-worker/appShell.test.ts +86 -0
  281. package/src/service-worker/appShell.ts +39 -0
  282. package/src/service-worker/index.ts +4 -5
  283. package/src/serviceWorkerManager.ts +5 -19
  284. package/src/solver-worker/client.test.ts +151 -0
  285. package/src/solver-worker/client.ts +106 -0
  286. package/src/solver-worker/dictionaries/constants.ts +9 -0
  287. package/src/solver-worker/dictionaries/getDictionary.ts +21 -0
  288. package/src/solver-worker/dictionaries/index.ts +2 -0
  289. package/src/solver-worker/dictionaries/revalidateDictionary.test.ts +199 -0
  290. package/src/solver-worker/dictionaries/revalidateDictionary.ts +86 -0
  291. package/src/solver-worker/dictionaries/revalidatedAt.ts +43 -0
  292. package/src/solver-worker/getGaddag.test.ts +109 -0
  293. package/src/solver-worker/getGaddag.ts +62 -0
  294. package/src/solver-worker/index.ts +1 -0
  295. package/src/solver-worker/index.worker.ts +124 -0
  296. package/src/solver-worker/messages.ts +20 -0
  297. package/src/state/actions.ts +1 -1
  298. package/src/state/app/index.ts +4 -0
  299. package/src/state/app/initialState.ts +6 -0
  300. package/src/state/app/selectors.ts +5 -0
  301. package/src/state/app/slice.ts +20 -0
  302. package/src/state/app/types.ts +4 -0
  303. package/src/state/board/initialState.ts +1 -2
  304. package/src/state/i18n/index.ts +4 -0
  305. package/src/state/i18n/initialState.ts +5 -0
  306. package/src/state/i18n/selectors.ts +3 -0
  307. package/src/state/i18n/slice.ts +16 -0
  308. package/src/state/i18n/types.ts +7 -0
  309. package/src/state/index.ts +3 -0
  310. package/src/state/lib.ts +1 -1
  311. package/src/state/localStorage.ts +86 -29
  312. package/src/state/rack/initialState.ts +1 -2
  313. package/src/state/rack/slice.ts +4 -1
  314. package/src/state/results/lib.ts +5 -5
  315. package/src/state/results/selectors.ts +1 -1
  316. package/src/state/sagas.ts +139 -13
  317. package/src/state/selectors.ts +1 -1
  318. package/src/state/settings/initialState.ts +7 -10
  319. package/src/state/settings/selectors.ts +7 -2
  320. package/src/state/settings/slice.ts +5 -2
  321. package/src/state/store.ts +4 -0
  322. package/src/state/types.ts +4 -0
  323. package/src/state/useTypedStore.ts +6 -0
  324. package/src/styles/_tokens.scss +1 -0
  325. package/src/styles/global.scss +9 -0
  326. package/src/styles/variables.module.scss +0 -21
  327. package/src/styles/variables.module.scss.d.ts +27 -0
  328. package/src/styles/variables.scss +46 -2
  329. package/src/types/api.ts +1 -2
  330. package/src/types/index.ts +1 -2
  331. package/tsconfig.json +4 -0
  332. package/tsconfig.tsbuildinfo +1 -1
  333. package/.next/server/chunks/318.js +0 -3
  334. package/.next/server/chunks/791.js +0 -15
  335. package/.next/static/chunks/framework-3cf15b8041c59dd3.js +0 -1
  336. package/.next/static/chunks/main-852475ca502b4e49.js +0 -5
  337. package/.next/static/chunks/pages/_app-a5bbe02710f13d05.js +0 -7
  338. package/.next/static/chunks/pages/index-ec0f6aeeaf7be227.js +0 -1
  339. package/.next/static/chunks/pages/not-found-9e076427bad69d90.js +0 -1
  340. package/.next/static/chunks/webpack-1a7a166b34ed8d24.js +0 -1
  341. package/.next/static/css/0af35adb4bab56f0.css +0 -2
  342. package/.next/static/css/a78e04fe95452380.css +0 -1
  343. package/.next/static/vYpMmuC_yEurZGcyGVnLf/_buildManifest.js +0 -1
  344. package/src/app-layout/AppLayoutContext.tsx +0 -25
  345. package/src/app-layout/index.ts +0 -1
  346. package/src/app-layout/useAppLayoutValue.ts +0 -112
  347. package/src/components/Board/hooks/useBackgroundImage.tsx +0 -279
  348. package/src/components/index.ts +0 -26
  349. package/src/hooks/index.ts +0 -12
  350. package/src/hooks/useOnWindowResize.ts +0 -11
  351. package/src/hooks/useViewportSize.ts +0 -17
  352. package/src/i18n/index.ts +0 -2
  353. package/src/icons/Flag.svg +0 -4
  354. package/src/icons/Star.svg +0 -4
  355. package/src/icons/index.ts +0 -49
  356. package/src/lib/getTileSizes.ts +0 -13
  357. package/src/lib/index.ts +0 -24
  358. package/src/modals/index.ts +0 -7
  359. package/src/sdk/getDictionary.ts +0 -9
  360. package/src/service-worker/average.ts +0 -7
  361. package/src/service-worker/dictionaries/constants.ts +0 -5
  362. package/src/service-worker/dictionaries/getDictionary.ts +0 -20
  363. package/src/service-worker/dictionaries/index.ts +0 -2
  364. package/src/service-worker/dictionaries/revalidateDictionary.ts +0 -33
  365. package/src/service-worker/getTrie.ts +0 -24
  366. package/src/service-worker/routeSolveRequests.ts +0 -91
  367. package/src/service-worker/routeVerifyRequests.ts +0 -37
  368. /package/.next/static/{vYpMmuC_yEurZGcyGVnLf → FL82obwzFHRzvV6z76I-U}/_ssgManifest.js +0 -0
  369. /package/src/{service-worker → solver-worker}/dictionaries/expirationManager.ts +0 -0
  370. /package/src/{service-worker → solver-worker}/dictionaries/getDictionaryUrl.ts +0 -0
@@ -0,0 +1,109 @@
1
+ import { Gaddag } from '@kamilmielnik/gaddag';
2
+ import { Locale } from '@scrabble-solver/types';
3
+ import { describe, expect, it, mock } from 'bun:test';
4
+
5
+ import type * as getGaddagModule from './getGaddag';
6
+
7
+ let dictionary: Response | undefined;
8
+ const deletedLocales: Locale[] = [];
9
+
10
+ await mock.module('./dictionaries', () => ({
11
+ deleteDictionary: (locale: Locale) => {
12
+ deletedLocales.push(locale);
13
+ return Promise.resolve();
14
+ },
15
+ getDictionary: () => Promise.resolve(dictionary),
16
+ }));
17
+
18
+ const { getGaddag }: typeof getGaddagModule = await import('./getGaddag');
19
+
20
+ describe('getGaddag', () => {
21
+ it('returns undefined when there is no cached dictionary', async () => {
22
+ dictionary = undefined;
23
+ expect(await getGaddag(Locale.EN_US)).toBeUndefined();
24
+ });
25
+
26
+ it('deserializes a cached dictionary', async () => {
27
+ dictionary = createDictionary(['scrabble', 'solver']);
28
+ const gaddag = await getGaddag(Locale.EN_US);
29
+
30
+ expect(gaddag?.has('scrabble')).toBe(true);
31
+ expect(gaddag?.has('trie')).toBe(false);
32
+ });
33
+
34
+ it('deletes a dictionary cached by an app version with an incompatible format', async () => {
35
+ dictionary = new Response('serialized trie from a previous app version');
36
+
37
+ expect(await getGaddag(Locale.PL_PL)).toBeUndefined();
38
+ expect(deletedLocales).toContain(Locale.PL_PL);
39
+ });
40
+
41
+ it('deletes a corrupted dictionary', async () => {
42
+ dictionary = new Response(Gaddag.fromArray(['scrabble']).serialize().subarray(0, 10));
43
+
44
+ expect(await getGaddag(Locale.FA_IR)).toBeUndefined();
45
+ expect(deletedLocales).toContain(Locale.FA_IR);
46
+ });
47
+
48
+ it('reuses the deserialized dictionary while the cached response is unchanged', async () => {
49
+ dictionary = createDictionary(['scrabble'], { etag: '"v1"' });
50
+ const first = await getGaddag(Locale.EN_GB);
51
+ dictionary = createDictionary(['scrabble'], { etag: '"v1"' });
52
+ const second = await getGaddag(Locale.EN_GB);
53
+
54
+ expect(first?.has('scrabble')).toBe(true);
55
+ expect(second).toBe(first);
56
+ });
57
+
58
+ it('reuses the deserialized dictionary when a revalidation only refreshes the Date header', async () => {
59
+ dictionary = createDictionary(['scrabble'], { date: 'Thu, 06 Aug 2026 00:00:00 GMT', etag: '"v1"' });
60
+ const first = await getGaddag(Locale.FR_FR);
61
+ dictionary = createDictionary(['scrabble'], { date: 'Fri, 07 Aug 2026 00:00:00 GMT', etag: '"v1"' });
62
+ const second = await getGaddag(Locale.FR_FR);
63
+
64
+ expect(first?.has('scrabble')).toBe(true);
65
+ expect(second).toBe(first);
66
+ });
67
+
68
+ it('picks up a dictionary another tab wrote into the shared cache', async () => {
69
+ dictionary = createDictionary(['scrabble'], { etag: '"v1"' });
70
+ const first = await getGaddag(Locale.TR_TR);
71
+ dictionary = createDictionary(['solver'], { etag: '"v2"' });
72
+ const second = await getGaddag(Locale.TR_TR);
73
+
74
+ expect(first?.has('scrabble')).toBe(true);
75
+ expect(second).not.toBe(first);
76
+ expect(second?.has('solver')).toBe(true);
77
+ });
78
+
79
+ it('falls back to the Date header for responses without an ETag', async () => {
80
+ dictionary = createDictionary(['scrabble'], { date: 'Thu, 06 Aug 2026 00:00:00 GMT' });
81
+ const first = await getGaddag(Locale.DE_DE);
82
+ dictionary = createDictionary(['scrabble'], { date: 'Thu, 06 Aug 2026 00:00:00 GMT' });
83
+ const second = await getGaddag(Locale.DE_DE);
84
+ dictionary = createDictionary(['scrabble'], { date: 'Fri, 07 Aug 2026 00:00:00 GMT' });
85
+ const third = await getGaddag(Locale.DE_DE);
86
+
87
+ expect(first?.has('scrabble')).toBe(true);
88
+ expect(second).toBe(first);
89
+ expect(third).not.toBe(first);
90
+ expect(third?.has('scrabble')).toBe(true);
91
+ });
92
+
93
+ it('memoizes each locale independently', async () => {
94
+ dictionary = createDictionary(['scrabble'], { etag: '"es"' });
95
+ const spanish = await getGaddag(Locale.ES_ES);
96
+ dictionary = createDictionary(['solver'], { etag: '"ro"' });
97
+ const romanian = await getGaddag(Locale.RO_RO);
98
+ dictionary = createDictionary(['scrabble'], { etag: '"es"' });
99
+ const spanishAgain = await getGaddag(Locale.ES_ES);
100
+
101
+ expect(spanish?.has('scrabble')).toBe(true);
102
+ expect(romanian?.has('solver')).toBe(true);
103
+ expect(spanishAgain).toBe(spanish);
104
+ });
105
+ });
106
+
107
+ function createDictionary(words: string[], headers: Record<string, string> = {}): Response {
108
+ return new Response(Gaddag.fromArray(words).serialize(), { headers });
109
+ }
@@ -0,0 +1,62 @@
1
+ import { Gaddag } from '@kamilmielnik/gaddag';
2
+ import { type Locale } from '@scrabble-solver/types';
3
+
4
+ import { deleteDictionary, getDictionary } from './dictionaries';
5
+
6
+ interface DeserializedDictionary {
7
+ gaddag: Gaddag;
8
+ version: string;
9
+ }
10
+
11
+ const deserializedDictionaries: Partial<Record<Locale, DeserializedDictionary>> = {};
12
+
13
+ /**
14
+ * Returns undefined when there is no cached dictionary or when the cached
15
+ * dictionary cannot be deserialized (it was downloaded by an app version with
16
+ * an incompatible format). An undeserializable entry is deleted before
17
+ * returning, so the revalidation the caller triggers next downloads a fresh
18
+ * dictionary instead of getting a 304 for the broken bytes.
19
+ */
20
+ export async function getGaddag(locale: Locale): Promise<Gaddag | undefined> {
21
+ const response = await getDictionary(locale);
22
+
23
+ if (typeof response === 'undefined') {
24
+ return undefined;
25
+ }
26
+
27
+ const version = readVersion(response);
28
+ const deserialized = deserializedDictionaries[locale];
29
+
30
+ if (deserialized && deserialized.version === version) {
31
+ return deserialized.gaddag;
32
+ }
33
+
34
+ try {
35
+ const gaddag = Gaddag.deserialize(new Uint8Array(await response.arrayBuffer()));
36
+
37
+ if (typeof version !== 'undefined') {
38
+ deserializedDictionaries[locale] = { gaddag, version };
39
+ }
40
+
41
+ return gaddag;
42
+ } catch {
43
+ delete deserializedDictionaries[locale];
44
+ await deleteDictionary(locale);
45
+ return undefined;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Read from the cached response rather than tracked in memory, so a dictionary
51
+ * another tab downloaded into the shared cache is picked up here too.
52
+ */
53
+ function readVersion(response: Response): string | undefined {
54
+ const etag = response.headers.get('etag');
55
+
56
+ if (etag !== null) {
57
+ return etag;
58
+ }
59
+
60
+ const parts = ['date', 'content-length'].map((name) => response.headers.get(name) ?? '');
61
+ return parts.every((part) => part === '') ? undefined : parts.join('|');
62
+ }
@@ -0,0 +1 @@
1
+ export { prefetchDictionary, solveLocally, verifyLocally } from './client';
@@ -0,0 +1,124 @@
1
+ /// <reference lib="WebWorker" />
2
+
3
+ import { getConfig } from '@scrabble-solver/configs';
4
+ import { BLANK } from '@scrabble-solver/constants';
5
+ import { solve } from '@scrabble-solver/solver';
6
+ import { Board, type Locale, Tile } from '@scrabble-solver/types';
7
+
8
+ import { type SolveRequestPayload, type VerifyRequestPayload } from '@/types';
9
+
10
+ import { revalidateDictionary } from './dictionaries';
11
+ import { getGaddag } from './getGaddag';
12
+ import { type SolverWorkerRequest, type SolverWorkerResponse } from './messages';
13
+
14
+ declare const self: DedicatedWorkerGlobalScope;
15
+
16
+ /**
17
+ * Solves run synchronously on the single worker thread, so a burst of requests
18
+ * would queue stale solves ahead of the latest one.
19
+ */
20
+ let latestSolveId = 0;
21
+
22
+ self.addEventListener('message', ({ data }: MessageEvent<SolverWorkerRequest>) => {
23
+ if (data.type === 'solve') {
24
+ latestSolveId = data.id;
25
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
26
+ respond(data.id, () => handleSolve(data.id, data.payload));
27
+ } else if (data.type === 'verify') {
28
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
29
+ respond(data.id, () => handleVerify(data.id, data.payload));
30
+ } else if (data.type === 'prefetch') {
31
+ prefetchDictionary(data.locale).catch(() => undefined);
32
+ }
33
+ });
34
+
35
+ async function handleSolve(id: number, payload: SolveRequestPayload): Promise<SolverWorkerResponse> {
36
+ const { board, characters, game, locale } = payload;
37
+ const gaddag = await getGaddag(locale);
38
+ revalidateInBackground(locale);
39
+
40
+ if (!gaddag) {
41
+ return { id, outcome: 'unavailable' };
42
+ }
43
+
44
+ await yieldToQueuedMessages();
45
+
46
+ if (latestSolveId !== id) {
47
+ return { id, outcome: 'superseded' };
48
+ }
49
+
50
+ const config = getConfig(game, locale);
51
+ const tiles = characters.map((character) => new Tile({ character, isBlank: character === BLANK }));
52
+ return { data: solve(gaddag, config, Board.fromJson(board), tiles), id, outcome: 'answered' };
53
+ }
54
+
55
+ async function handleVerify(
56
+ id: number,
57
+ { board: boardJson, locale }: VerifyRequestPayload,
58
+ ): Promise<SolverWorkerResponse> {
59
+ const gaddag = await getGaddag(locale);
60
+ revalidateInBackground(locale);
61
+
62
+ if (!gaddag) {
63
+ return { id, outcome: 'unavailable' };
64
+ }
65
+
66
+ const board = Board.fromJson(boardJson);
67
+ const words = board.getWords().sort((a, b) => a.localeCompare(b, locale));
68
+ const invalidWords: string[] = [];
69
+ const validWords: string[] = [];
70
+
71
+ for (const word of words) {
72
+ if (gaddag.has(word)) {
73
+ validWords.push(word);
74
+ } else {
75
+ invalidWords.push(word);
76
+ }
77
+ }
78
+
79
+ return { data: { invalidWords, validWords }, id, outcome: 'answered' };
80
+ }
81
+
82
+ /**
83
+ * Deserializes the dictionary ahead of the first solve, so it does not pay the
84
+ * cold-start cost. Warms even when revalidation fails (e.g. offline) - a
85
+ * previously cached dictionary can still be deserialized.
86
+ */
87
+ async function prefetchDictionary(locale: Locale): Promise<void> {
88
+ try {
89
+ await revalidateDictionary(locale);
90
+ } catch {
91
+ // Solving falls back to the server until a later revalidation succeeds.
92
+ }
93
+
94
+ await getGaddag(locale);
95
+ }
96
+
97
+ /**
98
+ * Revalidation failures are expected (offline, server down) and must not
99
+ * surface as unhandled rejections - solving keeps using the cached dictionary
100
+ * until a later revalidation succeeds.
101
+ */
102
+ function revalidateInBackground(locale: Locale): void {
103
+ revalidateDictionary(locale).catch(() => undefined);
104
+ }
105
+
106
+ async function respond(id: number, handle: () => Promise<SolverWorkerResponse>): Promise<void> {
107
+ try {
108
+ self.postMessage(await handle());
109
+ } catch {
110
+ self.postMessage({ id, outcome: 'unavailable' } satisfies SolverWorkerResponse);
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Messages queued behind this handler dispatch only between macrotasks - a
116
+ * MessageChannel round-trip lets them register before an expensive solve.
117
+ */
118
+ function yieldToQueuedMessages(): Promise<void> {
119
+ return new Promise((resolve) => {
120
+ const { port1, port2 } = new MessageChannel();
121
+ port1.onmessage = () => resolve();
122
+ port2.postMessage(null);
123
+ });
124
+ }
@@ -0,0 +1,20 @@
1
+ import { type Locale, type ResultJson } from '@scrabble-solver/types';
2
+
3
+ import { type SolveRequestPayload, type VerifyRequestPayload } from '@/types';
4
+
5
+ export interface VerifyResult {
6
+ invalidWords: string[];
7
+ validWords: string[];
8
+ }
9
+
10
+ export type SolverWorkerRequest =
11
+ | { id: number; type: 'prefetch'; locale: Locale }
12
+ | { id: number; type: 'solve'; payload: SolveRequestPayload }
13
+ | { id: number; type: 'verify'; payload: VerifyRequestPayload };
14
+
15
+ export type SolverWorkerResponse =
16
+ | { id: number; outcome: 'answered'; data: ResultJson[] | VerifyResult }
17
+ /** No cached dictionary, or an internal error - the caller falls back to the server. */
18
+ | { id: number; outcome: 'unavailable' }
19
+ /** A newer solve replaced this one - the newer answer is the one worth waiting for. */
20
+ | { id: number; outcome: 'superseded' };
@@ -1,5 +1,5 @@
1
1
  import { createAction } from '@reduxjs/toolkit';
2
2
 
3
- export const initialize = createAction('initialize');
3
+ export const initialize = createAction<{ version: string }>('initialize');
4
4
 
5
5
  export const reset = createAction('reset');
@@ -0,0 +1,4 @@
1
+ export * from './initialState';
2
+ export * from './selectors';
3
+ export * from './slice';
4
+ export * from './types';
@@ -0,0 +1,6 @@
1
+ import type { AppState } from './types';
2
+
3
+ export const appInitialState: AppState = {
4
+ isHydrated: false,
5
+ version: '',
6
+ };
@@ -0,0 +1,5 @@
1
+ import type { RootState } from '../types';
2
+
3
+ export const selectIsHydrated = (state: RootState): boolean => state.app.isHydrated;
4
+
5
+ export const selectVersion = (state: RootState): string => state.app.version;
@@ -0,0 +1,20 @@
1
+ import { createSlice } from '@reduxjs/toolkit';
2
+
3
+ import { initialize } from '../actions';
4
+
5
+ import { appInitialState } from './initialState';
6
+
7
+ export const appSlice = createSlice({
8
+ initialState: appInitialState,
9
+ name: 'app',
10
+ reducers: {
11
+ hydrated: (state) => {
12
+ state.isHydrated = true;
13
+ },
14
+ },
15
+ extraReducers: (builder) => {
16
+ builder.addCase(initialize, (state, action) => {
17
+ state.version = action.payload.version;
18
+ });
19
+ },
20
+ });
@@ -0,0 +1,4 @@
1
+ export interface AppState {
2
+ isHydrated: boolean;
3
+ version: string;
4
+ }
@@ -1,7 +1,6 @@
1
1
  import { getConfig } from '@scrabble-solver/configs';
2
2
  import { Board } from '@scrabble-solver/types';
3
3
 
4
- import { localStorage } from '../localStorage';
5
4
  import { settingsInitialState } from '../settings';
6
5
 
7
6
  import type { BoardState } from './types';
@@ -9,7 +8,7 @@ import type { BoardState } from './types';
9
8
  const { game, locale } = settingsInitialState;
10
9
  const { boardHeight, boardWidth } = getConfig(game, locale);
11
10
 
12
- export const boardInitialState: BoardState = localStorage.getBoard() ?? Board.create(boardWidth, boardHeight);
11
+ export const boardInitialState: BoardState = Board.create(boardWidth, boardHeight);
13
12
 
14
13
  // const createOxyphenbutazone = () => {
15
14
  // // Tiles: oypbaze
@@ -0,0 +1,4 @@
1
+ export * from './initialState';
2
+ export * from './selectors';
3
+ export * from './slice';
4
+ export * from './types';
@@ -0,0 +1,5 @@
1
+ import type { I18nState } from './types';
2
+
3
+ export const i18nInitialState: I18nState = {
4
+ translations: {},
5
+ };
@@ -0,0 +1,3 @@
1
+ import type { RootState } from '../types';
2
+
3
+ export const selectLoadedTranslations = (state: RootState) => state.i18n.translations;
@@ -0,0 +1,16 @@
1
+ import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
2
+ import { type Locale } from '@scrabble-solver/types';
3
+
4
+ import type { Translations } from '@/types';
5
+
6
+ import { i18nInitialState } from './initialState';
7
+
8
+ export const i18nSlice = createSlice({
9
+ initialState: i18nInitialState,
10
+ name: 'i18n',
11
+ reducers: {
12
+ loaded: (state, action: PayloadAction<{ locale: Locale; translations: Translations }>) => {
13
+ state.translations[action.payload.locale] = action.payload.translations;
14
+ },
15
+ },
16
+ });
@@ -0,0 +1,7 @@
1
+ import { type Locale } from '@scrabble-solver/types';
2
+
3
+ import type { Translations } from '@/types';
4
+
5
+ export interface I18nState {
6
+ translations: Partial<Record<Locale, Translations>>;
7
+ }
@@ -1,8 +1,10 @@
1
1
  export * from './actions';
2
+ export * from './app';
2
3
  export * from './board';
3
4
  export * from './cellFilters';
4
5
  export * from './dictionary';
5
6
  export * from './hoveredTile';
7
+ export * from './i18n';
6
8
  export * from './localStorage';
7
9
  export * from './rack';
8
10
  export * from './results';
@@ -12,4 +14,5 @@ export * from './solve';
12
14
  export * from './store';
13
15
  export * from './useTranslate';
14
16
  export * from './useTypedSelector';
17
+ export * from './useTypedStore';
15
18
  export * from './verify';
package/src/state/lib.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { BLANK } from '@scrabble-solver/constants';
2
2
  import { type Board, type Config } from '@scrabble-solver/types';
3
3
 
4
- import { createKeyComparator } from '@/lib';
4
+ import { createKeyComparator } from '@/lib/createKeyComparator';
5
5
  import { type RemainingTile } from '@/types';
6
6
 
7
7
  export const getRemainingTiles = (
@@ -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';