scrabble-solver 2.16.1 → 2.16.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/.github/workflows/bunx.yml +11 -3
  2. package/.github/workflows/npx.yml +11 -3
  3. package/.nx/workspace-data/d/server-process.json +2 -2
  4. package/.nx/workspace-data/file-map.json +548 -504
  5. package/.nx/workspace-data/lockfile-dependencies.hash +1 -1
  6. package/.nx/workspace-data/lockfile-nodes.hash +1 -1
  7. package/.nx/workspace-data/nx_files.nxt +0 -0
  8. package/.nx/workspace-data/parsed-lock-file.dependencies.json +110 -90
  9. package/.nx/workspace-data/parsed-lock-file.nodes.json +96 -60
  10. package/.nx/workspace-data/project-graph.json +220 -162
  11. package/CLAUDE.md +1 -1
  12. package/README.md +1 -2
  13. package/bump-version.js +19 -7
  14. package/bun.lock +55 -41
  15. package/cypress/e2e/features/198-highlight-unreachable-cells.ts +101 -0
  16. package/cypress/e2e/features/91-hover-remaining-tile-highlights.ts +90 -0
  17. package/cypress/e2e/features.spec.cy.ts +2 -0
  18. package/lerna.json +1 -1
  19. package/package.json +4 -4
  20. package/packages/configs/package.json +3 -3
  21. package/packages/constants/package.json +1 -1
  22. package/packages/dictionaries/package.json +4 -4
  23. package/packages/logger/package.json +1 -1
  24. package/packages/scrabble-solver/.next/BUILD_ID +1 -1
  25. package/packages/scrabble-solver/.next/build-manifest.json +6 -6
  26. package/packages/scrabble-solver/.next/cache/.previewinfo +1 -1
  27. package/packages/scrabble-solver/.next/cache/.rscinfo +1 -1
  28. package/packages/scrabble-solver/.next/cache/.tsbuildinfo +1 -1
  29. package/packages/scrabble-solver/.next/cache/webpack/client-production/0.pack +0 -0
  30. package/packages/scrabble-solver/.next/cache/webpack/client-production/index.pack +0 -0
  31. package/packages/scrabble-solver/.next/cache/webpack/client-production/index.pack.old +0 -0
  32. package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/index.pack +0 -0
  33. package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/index.pack.old +0 -0
  34. package/packages/scrabble-solver/.next/cache/webpack/server-production/0.pack +0 -0
  35. package/packages/scrabble-solver/.next/cache/webpack/server-production/index.pack +0 -0
  36. package/packages/scrabble-solver/.next/cache/webpack/server-production/index.pack.old +0 -0
  37. package/packages/scrabble-solver/.next/prerender-manifest.json +4 -4
  38. package/packages/scrabble-solver/.next/routes-manifest.json +1 -1
  39. package/packages/scrabble-solver/.next/server/chunks/712.js +1 -1
  40. package/packages/scrabble-solver/.next/server/middleware-build-manifest.js +1 -1
  41. package/packages/scrabble-solver/.next/server/pages/404.html +1 -1
  42. package/packages/scrabble-solver/.next/server/pages/500.html +1 -1
  43. package/packages/scrabble-solver/.next/server/pages/index.html +1 -1
  44. package/packages/scrabble-solver/.next/server/pages/index.js +1 -1
  45. package/packages/scrabble-solver/.next/server/pages/index.json +1 -1
  46. package/packages/scrabble-solver/.next/server/pages/not-found.html +1 -1
  47. package/packages/scrabble-solver/.next/static/{-2mw8IrOEpCcsQa6oPYhR → 2uHbiXWVmb-guNjYk2sfF}/_buildManifest.js +1 -1
  48. package/packages/scrabble-solver/.next/static/chunks/pages/_app-b74c31c912e414e4.js +7 -0
  49. package/packages/scrabble-solver/.next/static/chunks/pages/index-35f70f1627836521.js +1 -0
  50. package/packages/scrabble-solver/.next/static/css/0af35adb4bab56f0.css +2 -0
  51. package/packages/scrabble-solver/.next/static/css/a78e04fe95452380.css +1 -0
  52. package/packages/scrabble-solver/.next/trace +21 -21
  53. package/packages/scrabble-solver/.next/trace-build +1 -1
  54. package/packages/scrabble-solver/package.json +8 -8
  55. package/packages/scrabble-solver/public/service-worker.js +1 -1
  56. package/packages/scrabble-solver/src/components/Board/Board.tsx +3 -1
  57. package/packages/scrabble-solver/src/components/Board/BoardPure.tsx +3 -0
  58. package/packages/scrabble-solver/src/components/Board/components/Cell/Cell.module.scss +6 -0
  59. package/packages/scrabble-solver/src/components/Board/components/Cell/Cell.tsx +18 -6
  60. package/packages/scrabble-solver/src/components/Board/lib.ts +83 -2
  61. package/packages/scrabble-solver/src/components/Board/selectors.ts +20 -1
  62. package/packages/scrabble-solver/src/components/DictionaryInput/DictionaryInput.tsx +1 -0
  63. package/packages/scrabble-solver/src/components/Modal/Modal.tsx +2 -0
  64. package/packages/scrabble-solver/src/components/Progress/Progress.tsx +5 -0
  65. package/packages/scrabble-solver/src/components/Rack/components/RackTile/RackTile.tsx +4 -1
  66. package/packages/scrabble-solver/src/components/Radio/Radio.tsx +1 -1
  67. package/packages/scrabble-solver/src/components/Results/Result.tsx +1 -0
  68. package/packages/scrabble-solver/src/components/Results/Results.tsx +8 -2
  69. package/packages/scrabble-solver/src/components/Tile/TilePure.tsx +20 -18
  70. package/packages/scrabble-solver/src/i18n/languages/english.json +4 -1
  71. package/packages/scrabble-solver/src/i18n/languages/french.json +4 -1
  72. package/packages/scrabble-solver/src/i18n/languages/german.json +4 -1
  73. package/packages/scrabble-solver/src/i18n/languages/persian.json +4 -1
  74. package/packages/scrabble-solver/src/i18n/languages/polish.json +4 -1
  75. package/packages/scrabble-solver/src/i18n/languages/romanian.json +4 -1
  76. package/packages/scrabble-solver/src/i18n/languages/spanish.json +4 -1
  77. package/packages/scrabble-solver/src/i18n/languages/turkish.json +4 -1
  78. package/packages/scrabble-solver/src/modals/RemainingTilesModal/components/Character/Character.module.scss +4 -0
  79. package/packages/scrabble-solver/src/modals/RemainingTilesModal/components/Character/Character.tsx +35 -2
  80. package/packages/scrabble-solver/src/modals/SettingsModal/SettingsModal.tsx +8 -0
  81. package/packages/scrabble-solver/src/modals/SettingsModal/components/AutoGroupTilesSetting/AutoGroupTilesSetting.tsx +4 -4
  82. package/packages/scrabble-solver/src/modals/SettingsModal/components/HighlightUnreachableCellsSetting/HighlightUnreachableCellsSetting.module.scss +12 -0
  83. package/packages/scrabble-solver/src/modals/SettingsModal/components/HighlightUnreachableCellsSetting/HighlightUnreachableCellsSetting.tsx +51 -0
  84. package/packages/scrabble-solver/src/modals/SettingsModal/components/HighlightUnreachableCellsSetting/index.ts +1 -0
  85. package/packages/scrabble-solver/src/modals/SettingsModal/components/index.ts +1 -0
  86. package/packages/scrabble-solver/src/state/hoveredTile/index.ts +4 -0
  87. package/packages/scrabble-solver/src/state/hoveredTile/initialState.ts +5 -0
  88. package/packages/scrabble-solver/src/state/hoveredTile/selectors.ts +3 -0
  89. package/packages/scrabble-solver/src/state/hoveredTile/slice.ts +16 -0
  90. package/packages/scrabble-solver/src/state/hoveredTile/types.ts +3 -0
  91. package/packages/scrabble-solver/src/state/index.ts +1 -0
  92. package/packages/scrabble-solver/src/state/localStorage.ts +1 -0
  93. package/packages/scrabble-solver/src/state/settings/initialState.ts +1 -0
  94. package/packages/scrabble-solver/src/state/settings/selectors.ts +5 -0
  95. package/packages/scrabble-solver/src/state/settings/slice.ts +5 -0
  96. package/packages/scrabble-solver/src/state/settings/types.ts +1 -0
  97. package/packages/scrabble-solver/src/state/store.ts +2 -0
  98. package/packages/scrabble-solver/src/state/types.ts +2 -0
  99. package/packages/scrabble-solver/src/types/index.ts +3 -0
  100. package/packages/scrabble-solver/tsconfig.tsbuildinfo +1 -1
  101. package/packages/solver/package.json +5 -5
  102. package/packages/types/package.json +2 -2
  103. package/packages/word-definitions/package.json +2 -2
  104. package/packages/word-lists/package.json +2 -2
  105. package/todo.txt +0 -4
  106. package/packages/scrabble-solver/.next/static/chunks/pages/_app-9c9aaea7b6881765.js +0 -7
  107. package/packages/scrabble-solver/.next/static/chunks/pages/index-e9ea63e97865edcf.js +0 -1
  108. package/packages/scrabble-solver/.next/static/css/78726ae7cf7a5497.css +0 -2
  109. package/packages/scrabble-solver/.next/static/css/addf6fd4f75742d0.css +0 -1
  110. /package/packages/scrabble-solver/.next/static/{-2mw8IrOEpCcsQa6oPYhR → 2uHbiXWVmb-guNjYk2sfF}/_ssgManifest.js +0 -0
@@ -52,6 +52,7 @@ const ModalBase: FunctionComponent<Props> = ({ children, className, footer, isOp
52
52
 
53
53
  return (
54
54
  <ReactModal
55
+ aria={{ modal: true }}
55
56
  className={{
56
57
  afterOpen: styles.afterOpen,
57
58
  base: styles.modal,
@@ -61,6 +62,7 @@ const ModalBase: FunctionComponent<Props> = ({ children, className, footer, isOp
61
62
  contentLabel={title}
62
63
  isOpen={isOpen}
63
64
  overlayClassName={styles.overlay}
65
+ role="dialog"
64
66
  shouldReturnFocusAfterClose={shouldReturnFocusAfterClose}
65
67
  onRequestClose={onClose}
66
68
  >
@@ -25,7 +25,12 @@ export const Progress: FunctionComponent<Props> = ({ className, max, min = 0, st
25
25
  return (
26
26
  <div
27
27
  {...props}
28
+ aria-valuemax={max}
29
+ aria-valuemin={min}
30
+ aria-valuenow={value}
31
+ aria-valuetext={`${percent.toLocaleString(locale)}%`}
28
32
  className={classNames(styles.progress, className)}
33
+ role="progressbar"
29
34
  style={{
30
35
  ...style,
31
36
  backgroundImage: getGradient(progress, PROGRESS_COLOR_VALUE, PROGRESS_COLOR_BACKGROUND),
@@ -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}
@@ -30,7 +30,7 @@ export const Radio: FunctionComponent<Props> = ({ checked, children, className,
30
30
  onChange={onChange}
31
31
  />
32
32
 
33
- <div aria-hidden="true" className={styles.icon} role="img">
33
+ <div aria-hidden="true" className={styles.icon}>
34
34
  <div className={styles.iconContent} />
35
35
  </div>
36
36
 
@@ -50,6 +50,7 @@ export const Result = ({
50
50
 
51
51
  return (
52
52
  <button
53
+ aria-current={index === highlightedIndex ? 'true' : undefined}
53
54
  aria-hidden={isMatching ? undefined : 'true'}
54
55
  aria-label={result.word}
55
56
  className={classNames(styles.result, {
@@ -70,10 +70,16 @@ export const Results: FunctionComponent<Props> = ({ callbacks, className, highli
70
70
  }, [results, listRef, scrollToIndexRef]);
71
71
 
72
72
  return (
73
- <div className={classNames(styles.results, className)} data-testid="results">
73
+ <div
74
+ aria-busy={isLoadingDebounced}
75
+ aria-label={translate('results')}
76
+ className={classNames(styles.results, className)}
77
+ data-testid="results"
78
+ role="region"
79
+ >
74
80
  <Header />
75
81
 
76
- <div className={styles.content}>
82
+ <div aria-live="polite" className={styles.content}>
77
83
  {typeof error !== 'undefined' && (
78
84
  <EmptyState className={styles.emptyState} variant="error">
79
85
  {error.message}
@@ -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}>
@@ -4,7 +4,7 @@
4
4
  "cell.filter-cell.include": "Target destination",
5
5
  "cell.set-blank": "Mark it a blank",
6
6
  "cell.set-not-blank": "Mark it not a blank",
7
- "cell.tile.location": "Board: tile ({{x}}, {{y}})",
7
+ "cell.tile.location": "Board: tile {{coordinates}}",
8
8
  "cell.toggle-direction": "Typing direction",
9
9
  "common.arrows": "Arrow keys",
10
10
  "common.blanks": "Blanks",
@@ -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",
@@ -4,7 +4,7 @@
4
4
  "cell.filter-cell.include": "Destination cible",
5
5
  "cell.set-blank": "Marquer comme vide",
6
6
  "cell.set-not-blank": "Marquer comme non vide",
7
- "cell.tile.location": "Plateau: la case ({{x}}, {{y}})",
7
+ "cell.tile.location": "Plateau: la case {{coordinates}}",
8
8
  "cell.toggle-direction": "Direction d'écriture",
9
9
  "common.arrows": "Touches fléchées",
10
10
  "common.blanks": "Cases vides",
@@ -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",
@@ -4,7 +4,7 @@
4
4
  "cell.filter-cell.include": "Ziel anvisieren",
5
5
  "cell.set-blank": "Als Blanko markieren",
6
6
  "cell.set-not-blank": "Nicht als Blanko markieren",
7
- "cell.tile.location": "Brett: Stein ({{x}}, {{y}})",
7
+ "cell.tile.location": "Brett: Stein {{coordinates}}",
8
8
  "cell.toggle-direction": "Schreibrichtung",
9
9
  "common.arrows": "Pfeiltasten",
10
10
  "common.blanks": "Blankos",
@@ -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",
@@ -4,7 +4,7 @@
4
4
  "cell.filter-cell.include": "مقصد مورد نظر",
5
5
  "cell.set-blank": "علامت گذاری به عنوان خالی",
6
6
  "cell.set-not-blank": "علامت گذاری به عنوان غیر خالی",
7
- "cell.tile.location": "({{x}}، {{y}}) کاشی: صفحه",
7
+ "cell.tile.location": "{{coordinates}} کاشی: صفحه",
8
8
  "cell.toggle-direction": "جهت تایپ",
9
9
  "common.arrows": "کلیدهای جهت دار",
10
10
  "common.blanks": "خالی",
@@ -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": "صفحه لمسی",
@@ -4,7 +4,7 @@
4
4
  "cell.filter-cell.include": "Miejsce docelowe",
5
5
  "cell.set-blank": "Oznacz jako blank",
6
6
  "cell.set-not-blank": "Oznacz jako nie blank",
7
- "cell.tile.location": "Plansza: płytka ({{x}}, {{y}})",
7
+ "cell.tile.location": "Plansza: płytka {{coordinates}}",
8
8
  "cell.toggle-direction": "Kierunek wpisywania",
9
9
  "common.arrows": "Klawisze strzałek",
10
10
  "common.blanks": "Blanki",
@@ -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",
@@ -4,7 +4,7 @@
4
4
  "cell.filter-cell.include": "Destinație țintă",
5
5
  "cell.set-blank": "Marcare camp liber",
6
6
  "cell.set-not-blank": "Marcare camp ocupat ",
7
- "cell.tile.location": "Tabla: camp ({{x}}, {{y}})",
7
+ "cell.tile.location": "Tabla: camp {{coordinates}}",
8
8
  "cell.toggle-direction": "Directie scriere",
9
9
  "common.arrows": "Sagetile",
10
10
  "common.blanks": "Jokeri",
@@ -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",
@@ -4,7 +4,7 @@
4
4
  "cell.filter-cell.include": "Destino objetivo",
5
5
  "cell.set-blank": "Marcar como en blanco",
6
6
  "cell.set-not-blank": "Marcar como no en blanco",
7
- "cell.tile.location": "Tablero: espacio ({{x}}, {{y}})",
7
+ "cell.tile.location": "Tablero: espacio {{coordinates}}",
8
8
  "cell.toggle-direction": "Dirección de escritura",
9
9
  "common.arrows": "Teclas de flecha",
10
10
  "common.blanks": "Blancos",
@@ -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",
@@ -4,7 +4,7 @@
4
4
  "cell.filter-cell.include": "Hedef varış noktası",
5
5
  "cell.set-blank": "Boş olarak işaretle",
6
6
  "cell.set-not-blank": "Boş değil olarak işaretle",
7
- "cell.tile.location": "Tahta: harfler ({{x}}, {{y}})",
7
+ "cell.tile.location": "Tahta: harfler {{coordinates}}",
8
8
  "cell.toggle-direction": "Yazma yönü - geçiş yapmak için tıklayın",
9
9
  "common.arrows": "Ok tuşları",
10
10
  "common.blanks": "Jokerler",
@@ -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 };