scrabble-solver 2.16.1 → 2.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/.nx/workspace-data/d/server-process.json +2 -2
  2. package/.nx/workspace-data/file-map.json +437 -393
  3. package/.nx/workspace-data/lockfile-dependencies.hash +1 -1
  4. package/.nx/workspace-data/lockfile-nodes.hash +1 -1
  5. package/.nx/workspace-data/nx_files.nxt +0 -0
  6. package/.nx/workspace-data/parsed-lock-file.dependencies.json +110 -90
  7. package/.nx/workspace-data/parsed-lock-file.nodes.json +96 -60
  8. package/.nx/workspace-data/project-graph.json +220 -162
  9. package/README.md +1 -2
  10. package/bump-version.js +28 -7
  11. package/bun.lock +55 -41
  12. package/cypress/e2e/features/198-highlight-unreachable-cells.ts +101 -0
  13. package/cypress/e2e/features/91-hover-remaining-tile-highlights.ts +90 -0
  14. package/cypress/e2e/features.spec.cy.ts +2 -0
  15. package/lerna.json +1 -1
  16. package/package.json +4 -4
  17. package/packages/configs/package.json +3 -3
  18. package/packages/constants/package.json +1 -1
  19. package/packages/dictionaries/package.json +4 -4
  20. package/packages/logger/package.json +1 -1
  21. package/packages/scrabble-solver/.next/BUILD_ID +1 -1
  22. package/packages/scrabble-solver/.next/build-manifest.json +6 -6
  23. package/packages/scrabble-solver/.next/cache/.previewinfo +1 -1
  24. package/packages/scrabble-solver/.next/cache/.rscinfo +1 -1
  25. package/packages/scrabble-solver/.next/cache/.tsbuildinfo +1 -1
  26. package/packages/scrabble-solver/.next/cache/webpack/client-production/0.pack +0 -0
  27. package/packages/scrabble-solver/.next/cache/webpack/client-production/index.pack +0 -0
  28. package/packages/scrabble-solver/.next/cache/webpack/client-production/index.pack.old +0 -0
  29. package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/index.pack +0 -0
  30. package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/index.pack.old +0 -0
  31. package/packages/scrabble-solver/.next/cache/webpack/server-production/0.pack +0 -0
  32. package/packages/scrabble-solver/.next/cache/webpack/server-production/index.pack +0 -0
  33. package/packages/scrabble-solver/.next/cache/webpack/server-production/index.pack.old +0 -0
  34. package/packages/scrabble-solver/.next/dev/build-manifest.json +31 -0
  35. package/packages/scrabble-solver/.next/dev/cache/webpack/client-development/0.pack.gz +0 -0
  36. package/packages/scrabble-solver/.next/dev/cache/webpack/client-development/index.pack.gz +0 -0
  37. package/packages/scrabble-solver/.next/dev/react-loadable-manifest.json +1 -0
  38. package/packages/scrabble-solver/.next/dev/server/middleware-build-manifest.js +31 -0
  39. package/packages/scrabble-solver/.next/dev/server/middleware-react-loadable-manifest.js +1 -0
  40. package/packages/scrabble-solver/.next/dev/server/next-font-manifest.js +1 -0
  41. package/packages/scrabble-solver/.next/dev/server/next-font-manifest.json +1 -0
  42. package/packages/scrabble-solver/.next/dev/server/pages/_app.js +4082 -0
  43. package/packages/scrabble-solver/.next/dev/server/pages/_error.js +4126 -0
  44. package/packages/scrabble-solver/.next/dev/server/pages/index.js +4804 -0
  45. package/packages/scrabble-solver/.next/dev/server/pages-manifest.json +6 -0
  46. package/packages/scrabble-solver/.next/dev/server/webpack-runtime.js +231 -0
  47. package/packages/scrabble-solver/.next/dev/static/chunks/pages/_app.js +5131 -0
  48. package/packages/scrabble-solver/.next/dev/static/chunks/webpack.js +1238 -0
  49. package/packages/scrabble-solver/.next/dev/static/development/_buildManifest.js +1 -0
  50. package/packages/scrabble-solver/.next/dev/static/development/_ssgManifest.js +1 -0
  51. package/packages/scrabble-solver/.next/prerender-manifest.json +4 -4
  52. package/packages/scrabble-solver/.next/routes-manifest.json +1 -1
  53. package/packages/scrabble-solver/.next/server/chunks/712.js +1 -1
  54. package/packages/scrabble-solver/.next/server/middleware-build-manifest.js +1 -1
  55. package/packages/scrabble-solver/.next/server/pages/404.html +1 -1
  56. package/packages/scrabble-solver/.next/server/pages/500.html +1 -1
  57. package/packages/scrabble-solver/.next/server/pages/index.html +1 -1
  58. package/packages/scrabble-solver/.next/server/pages/index.js +1 -1
  59. package/packages/scrabble-solver/.next/server/pages/index.json +1 -1
  60. package/packages/scrabble-solver/.next/server/pages/not-found.html +1 -1
  61. package/packages/scrabble-solver/.next/static/{-2mw8IrOEpCcsQa6oPYhR → a98wTJsZR6CASwFn19QFv}/_buildManifest.js +1 -1
  62. package/packages/scrabble-solver/.next/static/chunks/pages/_app-31e560b7b14739d6.js +7 -0
  63. package/packages/scrabble-solver/.next/static/chunks/pages/index-35f70f1627836521.js +1 -0
  64. package/packages/scrabble-solver/.next/static/css/0af35adb4bab56f0.css +2 -0
  65. package/packages/scrabble-solver/.next/static/css/a78e04fe95452380.css +1 -0
  66. package/packages/scrabble-solver/.next/trace +21 -21
  67. package/packages/scrabble-solver/.next/trace-build +1 -1
  68. package/packages/scrabble-solver/package.json +8 -8
  69. package/packages/scrabble-solver/public/service-worker.js +1 -1
  70. package/packages/scrabble-solver/src/components/Board/Board.tsx +3 -1
  71. package/packages/scrabble-solver/src/components/Board/BoardPure.tsx +3 -0
  72. package/packages/scrabble-solver/src/components/Board/components/Cell/Cell.module.scss +6 -0
  73. package/packages/scrabble-solver/src/components/Board/components/Cell/Cell.tsx +11 -2
  74. package/packages/scrabble-solver/src/components/Board/lib.ts +83 -2
  75. package/packages/scrabble-solver/src/components/Board/selectors.ts +20 -1
  76. package/packages/scrabble-solver/src/components/Rack/components/RackTile/RackTile.tsx +4 -1
  77. package/packages/scrabble-solver/src/components/Tile/TilePure.tsx +20 -18
  78. package/packages/scrabble-solver/src/i18n/languages/english.json +3 -0
  79. package/packages/scrabble-solver/src/i18n/languages/french.json +3 -0
  80. package/packages/scrabble-solver/src/i18n/languages/german.json +3 -0
  81. package/packages/scrabble-solver/src/i18n/languages/persian.json +3 -0
  82. package/packages/scrabble-solver/src/i18n/languages/polish.json +3 -0
  83. package/packages/scrabble-solver/src/i18n/languages/romanian.json +3 -0
  84. package/packages/scrabble-solver/src/i18n/languages/spanish.json +3 -0
  85. package/packages/scrabble-solver/src/i18n/languages/turkish.json +3 -0
  86. package/packages/scrabble-solver/src/modals/RemainingTilesModal/components/Character/Character.module.scss +4 -0
  87. package/packages/scrabble-solver/src/modals/RemainingTilesModal/components/Character/Character.tsx +35 -2
  88. package/packages/scrabble-solver/src/modals/SettingsModal/SettingsModal.tsx +8 -0
  89. package/packages/scrabble-solver/src/modals/SettingsModal/components/AutoGroupTilesSetting/AutoGroupTilesSetting.tsx +4 -4
  90. package/packages/scrabble-solver/src/modals/SettingsModal/components/HighlightUnreachableCellsSetting/HighlightUnreachableCellsSetting.module.scss +12 -0
  91. package/packages/scrabble-solver/src/modals/SettingsModal/components/HighlightUnreachableCellsSetting/HighlightUnreachableCellsSetting.tsx +51 -0
  92. package/packages/scrabble-solver/src/modals/SettingsModal/components/HighlightUnreachableCellsSetting/index.ts +1 -0
  93. package/packages/scrabble-solver/src/modals/SettingsModal/components/index.ts +1 -0
  94. package/packages/scrabble-solver/src/state/hoveredTile/index.ts +4 -0
  95. package/packages/scrabble-solver/src/state/hoveredTile/initialState.ts +5 -0
  96. package/packages/scrabble-solver/src/state/hoveredTile/selectors.ts +3 -0
  97. package/packages/scrabble-solver/src/state/hoveredTile/slice.ts +16 -0
  98. package/packages/scrabble-solver/src/state/hoveredTile/types.ts +3 -0
  99. package/packages/scrabble-solver/src/state/index.ts +1 -0
  100. package/packages/scrabble-solver/src/state/localStorage.ts +1 -0
  101. package/packages/scrabble-solver/src/state/settings/initialState.ts +1 -0
  102. package/packages/scrabble-solver/src/state/settings/selectors.ts +5 -0
  103. package/packages/scrabble-solver/src/state/settings/slice.ts +5 -0
  104. package/packages/scrabble-solver/src/state/settings/types.ts +1 -0
  105. package/packages/scrabble-solver/src/state/store.ts +2 -0
  106. package/packages/scrabble-solver/src/state/types.ts +2 -0
  107. package/packages/scrabble-solver/src/types/index.ts +3 -0
  108. package/packages/scrabble-solver/tsconfig.tsbuildinfo +1 -1
  109. package/packages/solver/package.json +5 -5
  110. package/packages/types/package.json +2 -2
  111. package/packages/word-definitions/package.json +2 -2
  112. package/packages/word-lists/package.json +2 -2
  113. package/todo.txt +0 -4
  114. package/packages/scrabble-solver/.next/static/chunks/pages/_app-9c9aaea7b6881765.js +0 -7
  115. package/packages/scrabble-solver/.next/static/chunks/pages/index-e9ea63e97865edcf.js +0 -1
  116. package/packages/scrabble-solver/.next/static/css/78726ae7cf7a5497.css +0 -2
  117. package/packages/scrabble-solver/.next/static/css/addf6fd4f75742d0.css +0 -1
  118. /package/packages/scrabble-solver/.next/static/{-2mw8IrOEpCcsQa6oPYhR → a98wTJsZR6CASwFn19QFv}/_ssgManifest.js +0 -0
@@ -82,24 +82,26 @@ const TilePureBase: FunctionComponent<Props> = ({
82
82
  >
83
83
  {character || placeholder}
84
84
 
85
- <input
86
- aria-label={ariaLabel}
87
- autoCapitalize="none"
88
- autoComplete="off"
89
- autoCorrect="off"
90
- autoFocus={autoFocus}
91
- className={styles.input}
92
- disabled={disabled}
93
- ref={inputRef}
94
- spellCheck={false}
95
- tabIndex={tabIndex}
96
- value={character || ''}
97
- onChange={onChange}
98
- onFocus={onFocus}
99
- onKeyDown={onKeyDown}
100
- onMouseDown={onMouseDown}
101
- onTouchStart={onTouchStart}
102
- />
85
+ {!disabled && (
86
+ <input
87
+ aria-label={ariaLabel}
88
+ autoCapitalize="none"
89
+ autoComplete="off"
90
+ autoCorrect="off"
91
+ autoFocus={autoFocus}
92
+ className={styles.input}
93
+ disabled={disabled}
94
+ ref={inputRef}
95
+ spellCheck={false}
96
+ tabIndex={tabIndex}
97
+ value={character || ''}
98
+ onChange={onChange}
99
+ onFocus={onFocus}
100
+ onKeyDown={onKeyDown}
101
+ onMouseDown={onMouseDown}
102
+ onTouchStart={onTouchStart}
103
+ />
104
+ )}
103
105
 
104
106
  {canShowPoints && (
105
107
  <span className={styles.points} style={pointsStyle}>
@@ -13,6 +13,8 @@
13
13
  "common.consonants": "Consonants",
14
14
  "common.loading": "Loading",
15
15
  "common.next": "Next",
16
+ "common.off": "Off",
17
+ "common.on": "On",
16
18
  "common.points": "Points",
17
19
  "common.previous": "Previous",
18
20
  "common.space": "Spacebar",
@@ -64,6 +66,7 @@
64
66
  "settings.autoGroupTiles.right": "On the right",
65
67
  "settings.autoGroupTiles.null": "Do not group",
66
68
  "settings.game": "Game",
69
+ "settings.highlightUnreachableCells": "Highlight unreachable cells",
67
70
  "settings.inputMode": "Input mode",
68
71
  "settings.inputMode.keyboard": "Keyboard",
69
72
  "settings.inputMode.touchscreen": "Touchscreen",
@@ -13,6 +13,8 @@
13
13
  "common.consonants": "Consonnes",
14
14
  "common.loading": "Chargement",
15
15
  "common.next": "Suivant",
16
+ "common.off": "Désactivé",
17
+ "common.on": "Activé",
16
18
  "common.points": "Points",
17
19
  "common.previous": "Précédent",
18
20
  "common.space": "Barre d'espace",
@@ -64,6 +66,7 @@
64
66
  "settings.autoGroupTiles.right": "Vers la gauche",
65
67
  "settings.autoGroupTiles.null": "Ne pas grouper",
66
68
  "settings.game": "Jeu",
69
+ "settings.highlightUnreachableCells": "Mettre en évidence les cases inatteignables",
67
70
  "settings.inputMode": "Mode de saisie",
68
71
  "settings.inputMode.keyboard": "Clavier",
69
72
  "settings.inputMode.touchscreen": "Écran tactile",
@@ -13,6 +13,8 @@
13
13
  "common.consonants": "Konsonanten",
14
14
  "common.loading": "Laden",
15
15
  "common.next": "Weiter",
16
+ "common.off": "Aus",
17
+ "common.on": "Ein",
16
18
  "common.points": "Punkte",
17
19
  "common.previous": "Zurück",
18
20
  "common.space": "Leertaste",
@@ -64,6 +66,7 @@
64
66
  "settings.autoGroupTiles.right": "Rechte Seite",
65
67
  "settings.autoGroupTiles.null": "Nicht gruppieren",
66
68
  "settings.game": "Spiel",
69
+ "settings.highlightUnreachableCells": "Unerreichbare Felder hervorheben",
67
70
  "settings.inputMode": "Eingabemodus",
68
71
  "settings.inputMode.keyboard": "Tastatur",
69
72
  "settings.inputMode.touchscreen": "Touchscreen",
@@ -13,6 +13,8 @@
13
13
  "common.consonants": "حروف صامت",
14
14
  "common.loading": "در حال بارگزاری",
15
15
  "common.next": "بعدی",
16
+ "common.off": "خاموش",
17
+ "common.on": "روشن",
16
18
  "common.points": "امتیازات",
17
19
  "common.previous": "قبلی",
18
20
  "common.space": "دکمه اسپیس",
@@ -64,6 +66,7 @@
64
66
  "settings.autoGroupTiles.right": "در سمت راست",
65
67
  "settings.autoGroupTiles.null": "کنار هم قرار نده",
66
68
  "settings.game": "بازی",
69
+ "settings.highlightUnreachableCells": "خانه‌های غیرقابل دسترس را برجسته کن",
67
70
  "settings.inputMode": "حالت ورودی",
68
71
  "settings.inputMode.keyboard": "صفحه کلید",
69
72
  "settings.inputMode.touchscreen": "صفحه لمسی",
@@ -13,6 +13,8 @@
13
13
  "common.consonants": "Spółgłoski",
14
14
  "common.loading": "Ładowanie",
15
15
  "common.next": "Następne",
16
+ "common.off": "Wyłączone",
17
+ "common.on": "Włączone",
16
18
  "common.points": "Punkty",
17
19
  "common.previous": "Poprzednie",
18
20
  "common.space": "Spacja",
@@ -64,6 +66,7 @@
64
66
  "settings.autoGroupTiles.right": "Po prawej",
65
67
  "settings.autoGroupTiles.null": "Nie grupuj",
66
68
  "settings.game": "Gra",
69
+ "settings.highlightUnreachableCells": "Wyróżnij nieosiągalne pola",
67
70
  "settings.inputMode": "Tryb wpisywania",
68
71
  "settings.inputMode.keyboard": "Klawiatura",
69
72
  "settings.inputMode.touchscreen": "Ekran dotykowy",
@@ -13,6 +13,8 @@
13
13
  "common.consonants": "Consoane",
14
14
  "common.loading": "Incarcare",
15
15
  "common.next": "Pasul urmator",
16
+ "common.off": "Dezactivat",
17
+ "common.on": "Activat",
16
18
  "common.points": "Puncte",
17
19
  "common.previous": "Pasul anterior",
18
20
  "common.space": "Bara de spațiu",
@@ -64,6 +66,7 @@
64
66
  "settings.autoGroupTiles.right": "In partea dreapta",
65
67
  "settings.autoGroupTiles.null": "Nu se grupeaza",
66
68
  "settings.game": "Joc",
69
+ "settings.highlightUnreachableCells": "Evidențiază celulele inaccesibile",
67
70
  "settings.inputMode": "Mod introducere",
68
71
  "settings.inputMode.keyboard": "Tastatura",
69
72
  "settings.inputMode.touchscreen": "Ecran",
@@ -13,6 +13,8 @@
13
13
  "common.consonants": "Consonantes",
14
14
  "common.loading": "Cargando",
15
15
  "common.next": "Siguiente",
16
+ "common.off": "Desactivado",
17
+ "common.on": "Activado",
16
18
  "common.points": "Puntos",
17
19
  "common.previous": "Anterior",
18
20
  "common.space": "Barra espaciadora",
@@ -64,6 +66,7 @@
64
66
  "settings.autoGroupTiles.right": "A la derecha",
65
67
  "settings.autoGroupTiles.null": "No agrupar",
66
68
  "settings.game": "Juego",
69
+ "settings.highlightUnreachableCells": "Resaltar casillas inalcanzables",
67
70
  "settings.inputMode": "Modo de entrada",
68
71
  "settings.inputMode.keyboard": "Teclado",
69
72
  "settings.inputMode.touchscreen": "Pantalla táctil",
@@ -13,6 +13,8 @@
13
13
  "common.consonants": "Ünlü harfler",
14
14
  "common.loading": "Yükleniyor",
15
15
  "common.next": "Sonraki",
16
+ "common.off": "Kapalı",
17
+ "common.on": "Açık",
16
18
  "common.points": "Puanlar",
17
19
  "common.previous": "Öncesi",
18
20
  "common.space": "Boşluk çubuğu",
@@ -64,6 +66,7 @@
64
66
  "settings.autoGroupTiles.right": "Sağdaki",
65
67
  "settings.autoGroupTiles.null": "Gruplamayın",
66
68
  "settings.game": "Oyun",
69
+ "settings.highlightUnreachableCells": "Ulaşılamayan kareleri vurgula",
67
70
  "settings.inputMode": "Giriş Methodu",
68
71
  "settings.inputMode.keyboard": "Klavye",
69
72
  "settings.inputMode.touchscreen": "Dokunmatik ekran",
@@ -43,3 +43,7 @@
43
43
  font-size: var(--font--size--m);
44
44
  white-space: nowrap;
45
45
  }
46
+
47
+ .unused {
48
+ cursor: not-allowed;
49
+ }
@@ -1,11 +1,18 @@
1
1
  import { BLANK } from '@scrabble-solver/constants';
2
2
  import classNames from 'classnames';
3
- import { type FunctionComponent } from 'react';
3
+ import { type FunctionComponent, useCallback, useEffect } from 'react';
4
+ import { useDispatch } from 'react-redux';
4
5
 
5
6
  import { Progress, Tile } from '@/components';
6
7
  import { LOCALE_FEATURES } from '@/i18n';
7
8
  import { REMAINING_TILES_TILE_SIZE } from '@/parameters';
8
- import { selectCharacterPoints, selectLocale, useTypedSelector } from '@/state';
9
+ import {
10
+ hoveredTileSlice,
11
+ selectCharacterPoints,
12
+ selectHoveredCharacter,
13
+ selectLocale,
14
+ useTypedSelector,
15
+ } from '@/state';
9
16
  import { type RemainingTile } from '@/types';
10
17
 
11
18
  import styles from './Character.module.scss';
@@ -15,7 +22,9 @@ interface Props {
15
22
  }
16
23
 
17
24
  export const Character: FunctionComponent<Props> = ({ tile }) => {
25
+ const dispatch = useDispatch();
18
26
  const locale = useTypedSelector(selectLocale);
27
+ const hoveredCharacter = useTypedSelector(selectHoveredCharacter);
19
28
  const { direction } = LOCALE_FEATURES[locale];
20
29
  const { character, count, usedCount } = tile;
21
30
 
@@ -27,19 +36,43 @@ export const Character: FunctionComponent<Props> = ({ tile }) => {
27
36
  const points = useTypedSelector((state) => selectCharacterPoints(state, character));
28
37
  const current = direction === 'ltr' ? remainingCount : count;
29
38
  const total = direction === 'ltr' ? count : remainingCount;
39
+ const isUsed = usedCount > 0;
40
+ const isHovered = isUsed && hoveredCharacter === character;
41
+
42
+ const handleMouseEnter = useCallback(() => {
43
+ if (!isUsed) {
44
+ return;
45
+ }
46
+ dispatch(hoveredTileSlice.actions.set(character));
47
+ }, [character, dispatch, isUsed]);
48
+
49
+ const handleMouseLeave = useCallback(() => {
50
+ dispatch(hoveredTileSlice.actions.clear());
51
+ }, [dispatch]);
52
+
53
+ useEffect(() => {
54
+ return () => {
55
+ dispatch(hoveredTileSlice.actions.clear());
56
+ };
57
+ }, [dispatch]);
30
58
 
31
59
  return (
32
60
  <div
33
61
  className={classNames(styles.character, {
34
62
  [styles.finished]: remainingCount <= 0,
35
63
  [styles.overused]: remainingCount < 0,
64
+ [styles.unused]: !isUsed,
36
65
  })}
66
+ data-testid={character === BLANK ? 'remaining-tile-blank' : `remaining-tile-${character}`}
67
+ onMouseEnter={handleMouseEnter}
68
+ onMouseLeave={handleMouseLeave}
37
69
  >
38
70
  <Tile
39
71
  aria-label={character}
40
72
  character={character}
41
73
  className={styles.tile}
42
74
  disabled
75
+ highlighted={isHovered}
43
76
  isBlank={character === BLANK}
44
77
  isValid={remainingCount >= 0}
45
78
  points={points}
@@ -7,6 +7,7 @@ import { useTranslate } from '@/state';
7
7
  import {
8
8
  AutoGroupTilesSetting,
9
9
  ConfigSetting,
10
+ HighlightUnreachableCellsSetting,
10
11
  InputModeSetting,
11
12
  LocaleSetting,
12
13
  ShowCoordinatesSetting,
@@ -50,6 +51,13 @@ const SettingsModalBase: FunctionComponent<Props> = ({ className, isOpen, onClos
50
51
  <Modal.Section label={translate('settings.removeCellFilters')} title={translate('settings.removeCellFilters')}>
51
52
  <RemoveCellFiltersSetting disabled={!isOpen} />
52
53
  </Modal.Section>
54
+
55
+ <Modal.Section
56
+ label={translate('settings.highlightUnreachableCells')}
57
+ title={translate('settings.highlightUnreachableCells')}
58
+ >
59
+ <HighlightUnreachableCellsSetting disabled={!isOpen} />
60
+ </Modal.Section>
53
61
  </Modal>
54
62
  );
55
63
  };
@@ -19,6 +19,10 @@ export const AutoGroupTilesSetting: FunctionComponent<Props> = ({ className, dis
19
19
  const value = useTypedSelector(selectAutoGroupTiles);
20
20
 
21
21
  const options = [
22
+ {
23
+ label: translate('settings.autoGroupTiles.null'),
24
+ value: NULL_VALUE,
25
+ },
22
26
  {
23
27
  label: translate('settings.autoGroupTiles.left'),
24
28
  value: 'left',
@@ -27,10 +31,6 @@ export const AutoGroupTilesSetting: FunctionComponent<Props> = ({ className, dis
27
31
  label: translate('settings.autoGroupTiles.right'),
28
32
  value: 'right',
29
33
  },
30
- {
31
- label: translate('settings.autoGroupTiles.null'),
32
- value: NULL_VALUE,
33
- },
34
34
  ];
35
35
 
36
36
  const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
@@ -0,0 +1,12 @@
1
+ .option {
2
+ margin-bottom: var(--spacing--m);
3
+
4
+ &:last-child {
5
+ margin-bottom: 0;
6
+ }
7
+ }
8
+
9
+ .label {
10
+ display: flex;
11
+ align-items: center;
12
+ }
@@ -0,0 +1,51 @@
1
+ import { type ChangeEvent, type FunctionComponent } from 'react';
2
+ import { useDispatch } from 'react-redux';
3
+
4
+ import { Radio } from '@/components';
5
+ import { selectHighlightUnreachableCells, settingsSlice, useTranslate, useTypedSelector } from '@/state';
6
+
7
+ import styles from './HighlightUnreachableCellsSetting.module.scss';
8
+
9
+ interface Props {
10
+ className?: string;
11
+ disabled?: boolean;
12
+ }
13
+
14
+ export const HighlightUnreachableCellsSetting: FunctionComponent<Props> = ({ className, disabled }) => {
15
+ const dispatch = useDispatch();
16
+ const translate = useTranslate();
17
+ const value = useTypedSelector(selectHighlightUnreachableCells);
18
+
19
+ const options = [
20
+ {
21
+ label: translate('common.off'),
22
+ value: 'off',
23
+ },
24
+ {
25
+ label: translate('common.on'),
26
+ value: 'on',
27
+ },
28
+ ];
29
+
30
+ const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
31
+ dispatch(settingsSlice.actions.changeHighlightUnreachableCells(event.target.value === 'on'));
32
+ };
33
+
34
+ return (
35
+ <div className={className}>
36
+ {options.map((option) => (
37
+ <Radio
38
+ checked={(value ? 'on' : 'off') === option.value}
39
+ className={styles.option}
40
+ disabled={disabled}
41
+ key={option.value}
42
+ name="highlightUnreachableCells"
43
+ value={option.value}
44
+ onChange={handleChange}
45
+ >
46
+ <div className={styles.label}>{option.label}</div>
47
+ </Radio>
48
+ ))}
49
+ </div>
50
+ );
51
+ };
@@ -0,0 +1 @@
1
+ export { HighlightUnreachableCellsSetting } from './HighlightUnreachableCellsSetting';
@@ -1,5 +1,6 @@
1
1
  export { AutoGroupTilesSetting } from './AutoGroupTilesSetting';
2
2
  export { ConfigSetting } from './ConfigSetting';
3
+ export { HighlightUnreachableCellsSetting } from './HighlightUnreachableCellsSetting';
3
4
  export { InputModeSetting } from './InputModeSetting';
4
5
  export { LocaleSetting } from './LocaleSetting';
5
6
  export { ShowCoordinatesSetting } from './ShowCoordinatesSetting';
@@ -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 { HoveredTileState } from './types';
2
+
3
+ export const hoveredTileInitialState: HoveredTileState = {
4
+ character: null,
5
+ };
@@ -0,0 +1,3 @@
1
+ import type { RootState } from '../types';
2
+
3
+ export const selectHoveredCharacter = (state: RootState): string | null => state.hoveredTile.character;
@@ -0,0 +1,16 @@
1
+ import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
2
+
3
+ import { hoveredTileInitialState } from './initialState';
4
+
5
+ export const hoveredTileSlice = createSlice({
6
+ initialState: hoveredTileInitialState,
7
+ name: 'hoveredTile',
8
+ reducers: {
9
+ set: (state, action: PayloadAction<string>) => {
10
+ state.character = action.payload;
11
+ },
12
+ clear: (state) => {
13
+ state.character = null;
14
+ },
15
+ },
16
+ });
@@ -0,0 +1,3 @@
1
+ export interface HoveredTileState {
2
+ character: string | null;
3
+ }
@@ -2,6 +2,7 @@ export * from './actions';
2
2
  export * from './board';
3
3
  export * from './cellFilters';
4
4
  export * from './dictionary';
5
+ export * from './hoveredTile';
5
6
  export * from './localStorage';
6
7
  export * from './rack';
7
8
  export * from './results';
@@ -12,6 +12,7 @@ const SETTINGS = 'settings';
12
12
  const LEGACY_KEYS: Record<keyof SettingsState, string> = {
13
13
  autoGroupTiles: 'auto-group-tiles',
14
14
  game: 'config-id',
15
+ highlightUnreachableCells: 'highlight-unreachable-cells',
15
16
  inputMode: 'input-mode',
16
17
  locale: 'locale',
17
18
  removeCellFilters: 'remove-cell-filters',
@@ -10,6 +10,7 @@ const isTouchScreen = typeof globalThis.matchMedia !== 'undefined' && globalThis
10
10
  export const settingsInitialState: SettingsState = {
11
11
  autoGroupTiles: null,
12
12
  game: Game.Scrabble,
13
+ highlightUnreachableCells: false,
13
14
  inputMode: isTouchScreen ? 'touchscreen' : 'keyboard',
14
15
  locale: guessLocale(),
15
16
  removeCellFilters: 'always',
@@ -19,6 +19,11 @@ export const selectAutoGroupTiles = createSelector([selectSettings], (settings)
19
19
 
20
20
  export const selectGame = createSelector([selectSettings], (settings) => settings.game);
21
21
 
22
+ export const selectHighlightUnreachableCells = createSelector(
23
+ [selectSettings],
24
+ (settings) => settings.highlightUnreachableCells,
25
+ );
26
+
22
27
  export const selectInputMode = createSelector([selectSettings], (settings) => settings.inputMode);
23
28
 
24
29
  export const selectLocale = createSelector([selectSettings], (settings) => settings.locale);
@@ -19,6 +19,11 @@ export const settingsSlice = createSlice({
19
19
  return { ...state, game };
20
20
  },
21
21
 
22
+ changeHighlightUnreachableCells: (state, action: PayloadAction<boolean>) => {
23
+ const highlightUnreachableCells = action.payload;
24
+ return { ...state, highlightUnreachableCells };
25
+ },
26
+
22
27
  changeInputMode: (state, action: PayloadAction<InputMode>) => {
23
28
  const inputMode = action.payload;
24
29
  return { ...state, inputMode };
@@ -5,6 +5,7 @@ import type { AutoGroupTiles, InputMode, RemoveCellFilters } from '@/types';
5
5
  export interface SettingsState {
6
6
  autoGroupTiles: AutoGroupTiles;
7
7
  game: Game;
8
+ highlightUnreachableCells: boolean;
8
9
  inputMode: InputMode;
9
10
  locale: Locale;
10
11
  showCoordinates: ShowCoordinates;
@@ -4,6 +4,7 @@ import reduxSaga from 'redux-saga';
4
4
  import { boardSlice } from './board';
5
5
  import { cellFiltersSlice } from './cellFilters';
6
6
  import { dictionarySlice } from './dictionary';
7
+ import { hoveredTileSlice } from './hoveredTile';
7
8
  import { rackSlice } from './rack';
8
9
  import { resultsSlice } from './results';
9
10
  import { rootSaga } from './sagas';
@@ -18,6 +19,7 @@ export const store = configureStore({
18
19
  board: boardSlice.reducer,
19
20
  cellFilters: cellFiltersSlice.reducer,
20
21
  dictionary: dictionarySlice.reducer,
22
+ hoveredTile: hoveredTileSlice.reducer,
21
23
  rack: rackSlice.reducer,
22
24
  results: resultsSlice.reducer,
23
25
  settings: settingsSlice.reducer,
@@ -1,6 +1,7 @@
1
1
  import type { BoardState } from './board';
2
2
  import type { CellFiltersState } from './cellFilters';
3
3
  import type { DictionaryState } from './dictionary';
4
+ import type { HoveredTileState } from './hoveredTile';
4
5
  import type { RackState } from './rack';
5
6
  import type { ResultsState } from './results';
6
7
  import type { SettingsState } from './settings';
@@ -11,6 +12,7 @@ export type RootState = {
11
12
  board: BoardState;
12
13
  cellFilters: CellFiltersState;
13
14
  dictionary: DictionaryState;
15
+ hoveredTile: HoveredTileState;
14
16
  rack: RackState;
15
17
  results: ResultsState;
16
18
  settings: SettingsState;
@@ -89,6 +89,8 @@ export type TranslationKey =
89
89
  | 'common.blanks'
90
90
  | 'common.consonants'
91
91
  | 'common.next'
92
+ | 'common.off'
93
+ | 'common.on'
92
94
  | 'common.points'
93
95
  | 'common.previous'
94
96
  | 'common.space'
@@ -140,6 +142,7 @@ export type TranslationKey =
140
142
  | 'settings.autoGroupTiles.right'
141
143
  | 'settings.autoGroupTiles.null'
142
144
  | 'settings.game'
145
+ | 'settings.highlightUnreachableCells'
143
146
  | 'settings.inputMode'
144
147
  | 'settings.inputMode.keyboard'
145
148
  | 'settings.inputMode.touchscreen'