scrabble-solver 2.16.0 → 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 (125) hide show
  1. package/.nx/workspace-data/d/server-process.json +2 -2
  2. package/.nx/workspace-data/file-map.json +303 -259
  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 +194 -209
  7. package/.nx/workspace-data/parsed-lock-file.nodes.json +271 -208
  8. package/.nx/workspace-data/project-graph.json +500 -433
  9. package/README.md +3 -4
  10. package/bump-version.js +28 -7
  11. package/bun.lock +115 -75
  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 +6 -6
  17. package/packages/configs/package.json +3 -3
  18. package/packages/constants/package.json +1 -1
  19. package/packages/dictionaries/package.json +5 -5
  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/api/dictionary/[locale]/[word].js.nft.json +1 -1
  58. package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale].js.nft.json +1 -1
  59. package/packages/scrabble-solver/.next/server/pages/api/solve.js +1 -1
  60. package/packages/scrabble-solver/.next/server/pages/api/solve.js.nft.json +1 -1
  61. package/packages/scrabble-solver/.next/server/pages/api/verify.js +1 -1
  62. package/packages/scrabble-solver/.next/server/pages/api/verify.js.nft.json +1 -1
  63. package/packages/scrabble-solver/.next/server/pages/index.html +1 -1
  64. package/packages/scrabble-solver/.next/server/pages/index.js +1 -1
  65. package/packages/scrabble-solver/.next/server/pages/index.json +1 -1
  66. package/packages/scrabble-solver/.next/server/pages/not-found.html +1 -1
  67. package/packages/scrabble-solver/.next/static/{pw-RjyxoPdU0jV0ImxbXX → a98wTJsZR6CASwFn19QFv}/_buildManifest.js +1 -1
  68. package/packages/scrabble-solver/.next/static/chunks/pages/_app-31e560b7b14739d6.js +7 -0
  69. package/packages/scrabble-solver/.next/static/chunks/pages/index-35f70f1627836521.js +1 -0
  70. package/packages/scrabble-solver/.next/static/css/0af35adb4bab56f0.css +2 -0
  71. package/packages/scrabble-solver/.next/static/css/a78e04fe95452380.css +1 -0
  72. package/packages/scrabble-solver/.next/trace +21 -21
  73. package/packages/scrabble-solver/.next/trace-build +1 -1
  74. package/packages/scrabble-solver/package.json +9 -9
  75. package/packages/scrabble-solver/public/service-worker.js +2 -2
  76. package/packages/scrabble-solver/src/api/isBoardValid.ts +4 -4
  77. package/packages/scrabble-solver/src/components/Board/Board.tsx +3 -1
  78. package/packages/scrabble-solver/src/components/Board/BoardPure.tsx +3 -0
  79. package/packages/scrabble-solver/src/components/Board/components/Cell/Cell.module.scss +6 -0
  80. package/packages/scrabble-solver/src/components/Board/components/Cell/Cell.tsx +11 -2
  81. package/packages/scrabble-solver/src/components/Board/lib.ts +83 -2
  82. package/packages/scrabble-solver/src/components/Board/selectors.ts +20 -1
  83. package/packages/scrabble-solver/src/components/Rack/components/RackTile/RackTile.tsx +4 -1
  84. package/packages/scrabble-solver/src/components/Tile/TilePure.tsx +20 -18
  85. package/packages/scrabble-solver/src/i18n/languages/english.json +3 -0
  86. package/packages/scrabble-solver/src/i18n/languages/french.json +3 -0
  87. package/packages/scrabble-solver/src/i18n/languages/german.json +3 -0
  88. package/packages/scrabble-solver/src/i18n/languages/persian.json +3 -0
  89. package/packages/scrabble-solver/src/i18n/languages/polish.json +3 -0
  90. package/packages/scrabble-solver/src/i18n/languages/romanian.json +3 -0
  91. package/packages/scrabble-solver/src/i18n/languages/spanish.json +3 -0
  92. package/packages/scrabble-solver/src/i18n/languages/turkish.json +3 -0
  93. package/packages/scrabble-solver/src/modals/RemainingTilesModal/components/Character/Character.module.scss +4 -0
  94. package/packages/scrabble-solver/src/modals/RemainingTilesModal/components/Character/Character.tsx +35 -2
  95. package/packages/scrabble-solver/src/modals/SettingsModal/SettingsModal.tsx +8 -0
  96. package/packages/scrabble-solver/src/modals/SettingsModal/components/AutoGroupTilesSetting/AutoGroupTilesSetting.tsx +4 -4
  97. package/packages/scrabble-solver/src/modals/SettingsModal/components/HighlightUnreachableCellsSetting/HighlightUnreachableCellsSetting.module.scss +12 -0
  98. package/packages/scrabble-solver/src/modals/SettingsModal/components/HighlightUnreachableCellsSetting/HighlightUnreachableCellsSetting.tsx +51 -0
  99. package/packages/scrabble-solver/src/modals/SettingsModal/components/HighlightUnreachableCellsSetting/index.ts +1 -0
  100. package/packages/scrabble-solver/src/modals/SettingsModal/components/index.ts +1 -0
  101. package/packages/scrabble-solver/src/state/hoveredTile/index.ts +4 -0
  102. package/packages/scrabble-solver/src/state/hoveredTile/initialState.ts +5 -0
  103. package/packages/scrabble-solver/src/state/hoveredTile/selectors.ts +3 -0
  104. package/packages/scrabble-solver/src/state/hoveredTile/slice.ts +16 -0
  105. package/packages/scrabble-solver/src/state/hoveredTile/types.ts +3 -0
  106. package/packages/scrabble-solver/src/state/index.ts +1 -0
  107. package/packages/scrabble-solver/src/state/localStorage.ts +1 -0
  108. package/packages/scrabble-solver/src/state/settings/initialState.ts +1 -0
  109. package/packages/scrabble-solver/src/state/settings/selectors.ts +5 -0
  110. package/packages/scrabble-solver/src/state/settings/slice.ts +5 -0
  111. package/packages/scrabble-solver/src/state/settings/types.ts +1 -0
  112. package/packages/scrabble-solver/src/state/store.ts +2 -0
  113. package/packages/scrabble-solver/src/state/types.ts +2 -0
  114. package/packages/scrabble-solver/src/types/index.ts +3 -0
  115. package/packages/scrabble-solver/tsconfig.tsbuildinfo +1 -1
  116. package/packages/solver/package.json +6 -6
  117. package/packages/types/package.json +2 -2
  118. package/packages/word-definitions/package.json +2 -2
  119. package/packages/word-lists/package.json +2 -2
  120. package/todo.txt +0 -2
  121. package/packages/scrabble-solver/.next/static/chunks/pages/_app-9c9aaea7b6881765.js +0 -7
  122. package/packages/scrabble-solver/.next/static/chunks/pages/index-e9ea63e97865edcf.js +0 -1
  123. package/packages/scrabble-solver/.next/static/css/78726ae7cf7a5497.css +0 -2
  124. package/packages/scrabble-solver/.next/static/css/addf6fd4f75742d0.css +0 -1
  125. /package/packages/scrabble-solver/.next/static/{pw-RjyxoPdU0jV0ImxbXX → a98wTJsZR6CASwFn19QFv}/_ssgManifest.js +0 -0
@@ -25,7 +25,7 @@ import styles from './Board.module.scss';
25
25
  import { BoardPure } from './BoardPure';
26
26
  import { Actions, InputPrompt } from './components';
27
27
  import { useBoardStyle, useFloatingActions, useFloatingFocus, useFloatingInputPrompt, useGrid } from './hooks';
28
- import { selectRowsWithCandidate } from './selectors';
28
+ import { selectReachableCells, selectRowsWithCandidate } from './selectors';
29
29
 
30
30
  interface Props {
31
31
  className?: string;
@@ -35,6 +35,7 @@ export const Board: FunctionComponent<Props> = ({ className }) => {
35
35
  const dispatch = useDispatch();
36
36
  const locale = useTypedSelector(selectLocale);
37
37
  const rows = useTypedSelector(selectRowsWithCandidate);
38
+ const reachableCells = useTypedSelector(selectReachableCells);
38
39
  const inputMode = useTypedSelector(selectInputMode);
39
40
  const cellFilters = useTypedSelector(selectCellFilters);
40
41
  const showCoordinates = useTypedSelector(selectShowCoordinates);
@@ -157,6 +158,7 @@ export const Board: FunctionComponent<Props> = ({ className }) => {
157
158
  coordinatesSize={coordinatesSize}
158
159
  direction={LOCALE_FEATURES[locale].direction}
159
160
  inputRefs={inputRefs}
161
+ reachableCells={reachableCells}
160
162
  ref={ref}
161
163
  rows={rows}
162
164
  showCoordinates={showCoordinates}
@@ -28,6 +28,7 @@ interface Props {
28
28
  coordinatesSize: number;
29
29
  direction: 'ltr' | 'rtl';
30
30
  inputRefs: RefObject<HTMLInputElement | null>[][];
31
+ reachableCells: boolean[][] | null;
31
32
  rows: CellModel[][];
32
33
  showCoordinates: ShowCoordinates;
33
34
  style?: CSSProperties;
@@ -48,6 +49,7 @@ const BoardPureBase = forwardRef<HTMLDivElement, Props>(
48
49
  direction,
49
50
  cellFilters,
50
51
  inputRefs,
52
+ reachableCells,
51
53
  rows,
52
54
  showCoordinates,
53
55
  style,
@@ -137,6 +139,7 @@ const BoardPureBase = forwardRef<HTMLDivElement, Props>(
137
139
  cellRight={x < rows[y].length - 1 ? rows[y][x + 1] : undefined}
138
140
  cellTop={y > 0 ? rows[y - 1][x] : undefined}
139
141
  inputRef={inputRefs[y][x]}
142
+ isReachable={reachableCells ? reachableCells[y][x] : true}
140
143
  key={x}
141
144
  size={cellSize}
142
145
  onChange={onChange}
@@ -87,6 +87,12 @@
87
87
  border-end-end-radius: 0;
88
88
  }
89
89
 
90
+ &.unreachable {
91
+ background-color: var(--border--color);
92
+ opacity: 0.5;
93
+ border-radius: 0;
94
+ }
95
+
90
96
  &:focus-within {
91
97
  z-index: 2;
92
98
  }
@@ -1,4 +1,4 @@
1
- import { EMPTY_CELL } from '@scrabble-solver/constants';
1
+ import { BLANK, EMPTY_CELL } from '@scrabble-solver/constants';
2
2
  import { type Cell as CellModel } from '@scrabble-solver/types';
3
3
  import classNames from 'classnames';
4
4
  import {
@@ -14,6 +14,7 @@ import {
14
14
  import {
15
15
  selectCellIsValid,
16
16
  selectConfig,
17
+ selectHoveredCharacter,
17
18
  selectInputMode,
18
19
  selectLocale,
19
20
  selectTilePoints,
@@ -33,6 +34,7 @@ interface Props {
33
34
  cellTop?: CellModel;
34
35
  className?: string;
35
36
  inputRef: RefObject<HTMLInputElement | null>;
37
+ isReachable?: boolean;
36
38
  size: number;
37
39
  onChange: ChangeEventHandler<HTMLInputElement>;
38
40
  onFocus: (x: number, y: number) => void;
@@ -46,6 +48,7 @@ export const Cell: FunctionComponent<Props> = ({
46
48
  cellTop,
47
49
  className,
48
50
  inputRef,
51
+ isReachable = true,
49
52
  size,
50
53
  onChange,
51
54
  onFocus,
@@ -57,7 +60,12 @@ export const Cell: FunctionComponent<Props> = ({
57
60
  const inputMode = useTypedSelector(selectInputMode);
58
61
  const points = useTypedSelector((state) => selectTilePoints(state, cell.tile));
59
62
  const isValid = useTypedSelector((state) => selectCellIsValid(state, cell));
63
+ const hoveredCharacter = useTypedSelector(selectHoveredCharacter);
60
64
  const isEmpty = tile.character === EMPTY_CELL;
65
+ const isHoverMatch =
66
+ !isEmpty &&
67
+ hoveredCharacter !== null &&
68
+ (hoveredCharacter === BLANK ? tile.isBlank : !tile.isBlank && tile.character === hoveredCharacter);
61
69
 
62
70
  const handleFocus: FocusEventHandler<HTMLInputElement> = useCallback(
63
71
  (event) => {
@@ -106,9 +114,10 @@ export const Cell: FunctionComponent<Props> = ({
106
114
  [styles.sharpTopRight]: cellTop?.hasTile() || cellRight?.hasTile(),
107
115
  [styles.sharpBottomLeft]: cellBottom?.hasTile() || cellLeft?.hasTile(),
108
116
  [styles.sharpBottomRight]: cellBottom?.hasTile() || cellRight?.hasTile(),
117
+ [styles.unreachable]: !isReachable,
109
118
  })}
110
119
  character={isEmpty ? undefined : tile.character}
111
- highlighted={cell.isCandidate()}
120
+ highlighted={cell.isCandidate() || isHoverMatch}
112
121
  inputRef={inputRef}
113
122
  isBlank={tile.isBlank}
114
123
  isValid={isValid}
@@ -1,8 +1,8 @@
1
1
  import { BONUS_WORD } from '@scrabble-solver/constants';
2
- import { type Bonus } from '@scrabble-solver/types';
2
+ import { type Board, type Bonus, type Config } from '@scrabble-solver/types';
3
3
 
4
4
  import { COLOR_BONUS_CHARACTER, COLOR_BONUS_CHARACTER_MULTIPLIER, COLOR_BONUS_WORD } from '@/parameters';
5
- import { type Point } from '@/types';
5
+ import { type Direction, type Point } from '@/types';
6
6
 
7
7
  export const createGrid = <T>(width: number, height: number, getInitialValue: (x: number, y: number) => T): T[][] => {
8
8
  return Array.from({ length: height }).map((_row, y) => {
@@ -35,3 +35,84 @@ export const getPositionInGrid = <T>(grid: T[][], constraint: (value: T) => bool
35
35
 
36
36
  return undefined;
37
37
  };
38
+
39
+ export const getReachableCells = (config: Config, board: Board, charactersCount: number): boolean[][] => {
40
+ const { boardHeight, boardWidth } = config;
41
+ const reachable = createGrid(boardWidth, boardHeight, () => false);
42
+
43
+ if (charactersCount === 0) {
44
+ return reachable;
45
+ }
46
+
47
+ const isBoardEmpty = board.isEmpty();
48
+ const center = board.center;
49
+
50
+ const isAnchor = (x: number, y: number, playDirection: Direction): boolean => {
51
+ if (isBoardEmpty) {
52
+ return x === center.x && y === center.y;
53
+ }
54
+
55
+ if (playDirection === 'horizontal') {
56
+ const above = y > 0 && !board.rows[y - 1][x].isEmpty;
57
+ const below = y < boardHeight - 1 && !board.rows[y + 1][x].isEmpty;
58
+ return above || below;
59
+ }
60
+
61
+ const left = x > 0 && !board.rows[y][x - 1].isEmpty;
62
+ const right = x < boardWidth - 1 && !board.rows[y][x + 1].isEmpty;
63
+ return left || right;
64
+ };
65
+
66
+ const isReachableInDirection = (x: number, y: number, direction: Direction): boolean => {
67
+ const isHorizontal = direction === 'horizontal';
68
+ const length = isHorizontal ? boardWidth : boardHeight;
69
+ const startOffset = isHorizontal ? x : y;
70
+ const cellAt = (offset: number) => (isHorizontal ? board.rows[y][offset] : board.rows[offset][x]);
71
+
72
+ if (isAnchor(x, y, direction)) {
73
+ return true;
74
+ }
75
+
76
+ for (const step of [-1, 1] as const) {
77
+ let newTiles = 1;
78
+
79
+ for (let offset = startOffset + step; offset >= 0 && offset < length; offset += step) {
80
+ const cell = cellAt(offset);
81
+
82
+ if (cell.hasTile()) {
83
+ if (newTiles <= charactersCount) {
84
+ return true;
85
+ }
86
+ break;
87
+ }
88
+
89
+ newTiles += 1;
90
+
91
+ if (newTiles > charactersCount) {
92
+ break;
93
+ }
94
+
95
+ if (isAnchor(cell.x, cell.y, direction)) {
96
+ return true;
97
+ }
98
+ }
99
+ }
100
+
101
+ return false;
102
+ };
103
+
104
+ for (let y = 0; y < boardHeight; ++y) {
105
+ for (let x = 0; x < boardWidth; ++x) {
106
+ if (board.rows[y][x].hasTile()) {
107
+ reachable[y][x] = true;
108
+ continue;
109
+ }
110
+
111
+ if (isReachableInDirection(x, y, 'horizontal') || isReachableInDirection(x, y, 'vertical')) {
112
+ reachable[y][x] = true;
113
+ }
114
+ }
115
+ }
116
+
117
+ return reachable;
118
+ };
@@ -1,8 +1,27 @@
1
1
  import { createSelector } from '@reduxjs/toolkit';
2
2
 
3
3
  import { findCell } from '@/lib';
4
- import { selectBoard, selectResultCandidateCells } from '@/state';
4
+ import {
5
+ selectBoard,
6
+ selectCharacters,
7
+ selectConfig,
8
+ selectHighlightUnreachableCells,
9
+ selectResultCandidateCells,
10
+ } from '@/state';
11
+
12
+ import { getReachableCells } from './lib';
5
13
 
6
14
  export const selectRowsWithCandidate = createSelector([selectBoard, selectResultCandidateCells], (board, cells) => {
7
15
  return board.rows.map((row, y) => row.map((cell, x) => findCell(cells, x, y) || cell));
8
16
  });
17
+
18
+ export const selectReachableCells = createSelector(
19
+ [selectConfig, selectBoard, selectCharacters, selectHighlightUnreachableCells],
20
+ (config, board, characters, highlightUnreachableCells): boolean[][] | null => {
21
+ if (!highlightUnreachableCells || characters.length === 0) {
22
+ return null;
23
+ }
24
+
25
+ return getReachableCells(config, board, characters.length);
26
+ },
27
+ );
@@ -21,6 +21,7 @@ import {
21
21
  selectCharacterIsValid,
22
22
  selectCharacterPoints,
23
23
  selectConfig,
24
+ selectHoveredCharacter,
24
25
  selectInputMode,
25
26
  selectLocale,
26
27
  useTranslate,
@@ -63,6 +64,8 @@ export const RackTile: FunctionComponent<Props> = ({
63
64
  const inputMode = useTypedSelector(selectInputMode);
64
65
  const points = useTypedSelector((state) => selectCharacterPoints(state, character));
65
66
  const isValid = useTypedSelector((state) => selectCharacterIsValid(state, character));
67
+ const hoveredCharacter = useTypedSelector(selectHoveredCharacter);
68
+ const isHoverMatch = character !== null && hoveredCharacter !== null && hoveredCharacter === character;
66
69
 
67
70
  const handleFocus: FocusEventHandler<HTMLInputElement> = useCallback(
68
71
  (event) => {
@@ -141,7 +144,7 @@ export const RackTile: FunctionComponent<Props> = ({
141
144
  autoFocus={inputMode === 'keyboard' && index === 0}
142
145
  className={classNames(styles.rackTile, className)}
143
146
  character={character === null ? undefined : character}
144
- highlighted={tile !== null}
147
+ highlighted={tile !== null || isHoverMatch}
145
148
  inputRef={inputRef}
146
149
  isBlank={character === BLANK}
147
150
  isValid={isValid}
@@ -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';