@scrabble-solver/scrabble-solver 2.13.8 → 2.13.9

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 (59) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +6 -6
  3. package/.next/cache/.tsbuildinfo +1 -1
  4. package/.next/cache/eslint/.cache_8dgz12 +1 -1
  5. package/.next/cache/webpack/client-production/0.pack +0 -0
  6. package/.next/cache/webpack/client-production/index.pack +0 -0
  7. package/.next/cache/webpack/edge-server-production/0.pack +0 -0
  8. package/.next/cache/webpack/edge-server-production/index.pack +0 -0
  9. package/.next/cache/webpack/server-production/0.pack +0 -0
  10. package/.next/cache/webpack/server-production/index.pack +0 -0
  11. package/.next/prerender-manifest.js +1 -1
  12. package/.next/prerender-manifest.json +1 -1
  13. package/.next/routes-manifest.json +1 -1
  14. package/.next/server/chunks/807.js +1 -1
  15. package/.next/server/middleware-build-manifest.js +1 -1
  16. package/.next/server/pages/404.html +1 -1
  17. package/.next/server/pages/500.html +1 -1
  18. package/.next/server/pages/index.html +1 -1
  19. package/.next/server/pages/index.js +1 -1
  20. package/.next/server/pages/index.json +1 -1
  21. package/.next/static/chunks/pages/{404-63b972b24be99c62.js → 404-01653a877b233143.js} +1 -1
  22. package/.next/static/chunks/pages/_app-735b5863675c1b5d.js +17 -0
  23. package/.next/static/chunks/pages/{index-7b73be2915cc7099.js → index-36c448d585a58425.js} +1 -1
  24. package/.next/static/css/841a5b5f0b2fb131.css +2 -0
  25. package/.next/static/{7zESQYo9UAqNh9LV0b7Sd → eLvYNd4B2hzSgBZ_PuZcQ}/_buildManifest.js +1 -1
  26. package/.next/trace +44 -44
  27. package/LICENSE +1 -1
  28. package/package.json +9 -9
  29. package/src/components/Board/Board.tsx +3 -3
  30. package/src/components/Board/BoardPure.tsx +27 -19
  31. package/src/components/Board/components/Actions/Actions.tsx +8 -12
  32. package/src/components/Board/components/Actions/lib.ts +30 -0
  33. package/src/components/Board/hooks/useBackgroundImage.tsx +3 -24
  34. package/src/components/Radio/Radio.module.scss +2 -1
  35. package/src/components/Tile/Tile.module.scss +0 -2
  36. package/src/components/Tile/Tile.tsx +1 -15
  37. package/src/components/Tooltip/Tooltip.module.scss +1 -0
  38. package/src/hooks/useAppLayout.ts +0 -1
  39. package/src/i18n/languages/english.json +2 -1
  40. package/src/i18n/languages/french.json +2 -1
  41. package/src/i18n/languages/german.json +2 -1
  42. package/src/i18n/languages/persian.json +2 -1
  43. package/src/i18n/languages/polish.json +2 -1
  44. package/src/i18n/languages/romanian.json +2 -1
  45. package/src/i18n/languages/spanish.json +2 -1
  46. package/src/icons/Ban.svg +4 -0
  47. package/src/icons/index.ts +1 -0
  48. package/src/lib/groupResults.ts +4 -7
  49. package/src/lib/index.ts +1 -0
  50. package/src/lib/resultMatchesCellFilter.ts +23 -0
  51. package/src/parameters/index.ts +0 -9
  52. package/src/state/sagas.ts +4 -4
  53. package/src/state/selectors.ts +5 -8
  54. package/src/state/slices/cellFilterInitialState.ts +2 -2
  55. package/src/state/slices/cellFilterSlice.ts +29 -4
  56. package/src/types/index.ts +10 -1
  57. package/.next/static/chunks/pages/_app-a2848b7efa6bb6b0.js +0 -17
  58. package/.next/static/css/b37850c8d5270d91.css +0 -2
  59. /package/.next/static/{7zESQYo9UAqNh9LV0b7Sd → eLvYNd4B2hzSgBZ_PuZcQ}/_ssgManifest.js +0 -0
package/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2022 Kamil Mielnik <kamil@kamilmielnik.com>
1
+ Copyright (c) 2024 Kamil Mielnik <kamil@kamilmielnik.com>
2
2
 
3
3
  Attribution-NonCommercial-NoDerivatives 4.0 International
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scrabble-solver/scrabble-solver",
3
- "version": "2.13.8",
3
+ "version": "2.13.9",
4
4
  "description": "Scrabble Solver 2 - App",
5
5
  "engines": {
6
6
  "node": ">=16"
@@ -30,13 +30,13 @@
30
30
  "@floating-ui/react": "^0.26.17",
31
31
  "@kamilmielnik/trie": "^3.0.0",
32
32
  "@reduxjs/toolkit": "^2.2.5",
33
- "@scrabble-solver/configs": "^2.13.8",
34
- "@scrabble-solver/constants": "^2.13.8",
35
- "@scrabble-solver/dictionaries": "^2.13.8",
36
- "@scrabble-solver/logger": "^2.13.8",
37
- "@scrabble-solver/solver": "^2.13.8",
38
- "@scrabble-solver/types": "^2.13.8",
39
- "@scrabble-solver/word-definitions": "^2.13.8",
33
+ "@scrabble-solver/configs": "^2.13.9",
34
+ "@scrabble-solver/constants": "^2.13.9",
35
+ "@scrabble-solver/dictionaries": "^2.13.9",
36
+ "@scrabble-solver/logger": "^2.13.9",
37
+ "@scrabble-solver/solver": "^2.13.9",
38
+ "@scrabble-solver/types": "^2.13.9",
39
+ "@scrabble-solver/word-definitions": "^2.13.9",
40
40
  "classnames": "^2.5.1",
41
41
  "env-cmd": "^10.1.0",
42
42
  "include-media": "^2.0.0",
@@ -72,5 +72,5 @@
72
72
  "@types/redux-saga": "^0.10.5",
73
73
  "sass": "^1.77.5"
74
74
  },
75
- "gitHead": "294a7a790843ef9d260be52a3144e323c8ac0641"
75
+ "gitHead": "d09676296f8fafadb7da0cf1e11fb240aaf309ee"
76
76
  }
@@ -9,7 +9,7 @@ import { useDispatch } from 'react-redux';
9
9
 
10
10
  import { useAppLayout } from 'hooks';
11
11
  import { LOCALE_FEATURES } from 'i18n';
12
- import { TRANSITION } from 'parameters';
12
+ import { BORDER_WIDTH, TRANSITION } from 'parameters';
13
13
  import {
14
14
  boardSlice,
15
15
  cellFilterSlice,
@@ -176,8 +176,8 @@ const Board: FunctionComponent<Props> = ({ className }) => {
176
176
  ref={floatingFocus.refs.setFloating}
177
177
  style={{
178
178
  position: floatingFocus.strategy,
179
- top: floatingFocus.y ? floatingFocus.y + cellSize : 0,
180
- left: floatingFocus.x ?? 0,
179
+ top: (floatingFocus.y ? floatingFocus.y + cellSize : 0) - (showCoordinates === 'hidden' ? 0 : BORDER_WIDTH),
180
+ left: (floatingFocus.x ?? 0) - (showCoordinates === 'hidden' ? 0 : BORDER_WIDTH),
181
181
  width: cellSize,
182
182
  height: cellSize,
183
183
  opacity: hasFocus ? 1 : 0,
@@ -12,10 +12,10 @@ import {
12
12
  memo,
13
13
  } from 'react';
14
14
 
15
- import { FlagFill } from 'icons';
15
+ import { Ban, FlagFill } from 'icons';
16
16
  import { getCoordinate } from 'lib';
17
17
  import { BORDER_WIDTH } from 'parameters';
18
- import { Point } from 'types';
18
+ import { CellFilterEntry } from 'types';
19
19
 
20
20
  import styles from './Board.module.scss';
21
21
  import { Cell } from './components';
@@ -26,7 +26,7 @@ interface Props {
26
26
  coordinatesFontSize: number;
27
27
  coordinatesSize: number;
28
28
  direction: 'ltr' | 'rtl';
29
- filteredCells: Point[];
29
+ filteredCells: CellFilterEntry[];
30
30
  inputRefs: RefObject<HTMLInputElement>[][];
31
31
  rows: CellModel[][];
32
32
  showCoordinates: ShowCoordinates;
@@ -87,22 +87,30 @@ const BoardPure = forwardRef<HTMLDivElement, Props>(
87
87
  </>
88
88
  )}
89
89
 
90
- {filteredCells.map(({ x, y }) => (
91
- <div
92
- className={styles.iconContainer}
93
- key={[x, y].join('-')}
94
- style={{
95
- height: cellSize,
96
- width: cellSize,
97
- left: direction === 'ltr' ? coordinatesSize + x * (cellSize + BORDER_WIDTH) : undefined,
98
- right: direction === 'rtl' ? coordinatesSize + x * (cellSize + BORDER_WIDTH) : undefined,
99
- top: coordinatesSize + y * (cellSize + BORDER_WIDTH),
100
- }}
101
- >
102
- <div className={styles.iconBackground} />
103
- <FlagFill className={styles.icon} />
104
- </div>
105
- ))}
90
+ {/* The dynamic changes to the board presentation need to be outside of useBackgroundImage
91
+ to prevent flickering on blob URL change (i.e. when flagging a field,
92
+ but not when changing game type since user's attention is not on the board
93
+ when that happens)*/}
94
+ {filteredCells.map(({ x, y, type }) => {
95
+ const Icon = type === 'exclude' ? Ban : FlagFill;
96
+
97
+ return (
98
+ <div
99
+ className={styles.iconContainer}
100
+ key={[x, y].join('-')}
101
+ style={{
102
+ height: cellSize,
103
+ width: cellSize,
104
+ left: direction === 'ltr' ? coordinatesSize + BORDER_WIDTH + x * (cellSize + BORDER_WIDTH) : undefined,
105
+ right: direction === 'rtl' ? coordinatesSize + BORDER_WIDTH + x * (cellSize + BORDER_WIDTH) : undefined,
106
+ top: coordinatesSize + BORDER_WIDTH + y * (cellSize + BORDER_WIDTH),
107
+ }}
108
+ >
109
+ <div className={styles.iconBackground} />
110
+ <Icon className={styles.icon} />
111
+ </div>
112
+ );
113
+ })}
106
114
 
107
115
  {rows.map((cells, y) => (
108
116
  <Fragment key={y}>
@@ -3,21 +3,16 @@ import { Cell } from '@scrabble-solver/types';
3
3
  import classNames from 'classnames';
4
4
  import { forwardRef, HTMLProps, MouseEventHandler } from 'react';
5
5
 
6
- import { Flag, FlagFill, Keyboard, Square, SquareFill } from 'icons';
6
+ import { Keyboard, Square, SquareFill } from 'icons';
7
7
  import { findCell } from 'lib';
8
- import {
9
- selectCellIsFiltered,
10
- selectInputMode,
11
- selectResultCandidateCells,
12
- useTranslate,
13
- useTypedSelector,
14
- } from 'state';
8
+ import { selectCellFilter, selectInputMode, selectResultCandidateCells, useTranslate, useTypedSelector } from 'state';
15
9
  import { Direction } from 'types';
16
10
 
17
11
  import Button from '../../../Button';
18
12
  import ToggleDirectionButton from '../ToggleDirectionButton';
19
13
 
20
14
  import styles from './Actions.module.scss';
15
+ import { getNextCellFilter } from './lib';
21
16
 
22
17
  interface Props extends HTMLProps<HTMLDivElement> {
23
18
  cell: Cell;
@@ -35,10 +30,11 @@ const Actions = forwardRef<HTMLDivElement, Props>(
35
30
  ) => {
36
31
  const translate = useTranslate();
37
32
  const inputMode = useTypedSelector(selectInputMode);
38
- const isFiltered = useTypedSelector((state) => selectCellIsFiltered(state, cell));
33
+ const filter = useTypedSelector((state) => selectCellFilter(state, cell));
39
34
  const resultCandidateCells = useTypedSelector(selectResultCandidateCells);
40
35
  const isBlank = cell.tile.isBlank;
41
36
  const isEmpty = cell.tile.character === EMPTY_CELL || Boolean(findCell(resultCandidateCells, cell.x, cell.y));
37
+ const { Icon, labelTranslationKey } = getNextCellFilter(filter);
42
38
 
43
39
  // On iOS it helps with losing focus too early which makes Actions disappear
44
40
  const handleMouseDown: MouseEventHandler = (event) => event.preventDefault();
@@ -67,10 +63,10 @@ const Actions = forwardRef<HTMLDivElement, Props>(
67
63
 
68
64
  {isEmpty && (
69
65
  <Button
70
- aria-label={translate('cell.filter-cell')}
66
+ aria-label={translate(labelTranslationKey)}
71
67
  className={classNames(styles.action)}
72
- Icon={isFiltered ? Flag : FlagFill}
73
- tooltip={translate('cell.filter-cell')}
68
+ Icon={Icon}
69
+ tooltip={translate(labelTranslationKey)}
74
70
  onClick={onToggleFilterCell}
75
71
  onMouseDown={handleMouseDown}
76
72
  />
@@ -0,0 +1,30 @@
1
+ import { FunctionComponent, SVGAttributes } from 'react';
2
+
3
+ import { Ban, Eraser, FlagFill } from 'icons';
4
+ import { CellFilterEntry, TranslationKey } from 'types';
5
+
6
+ export const getNextCellFilter = (
7
+ filter: CellFilterEntry | undefined,
8
+ ): {
9
+ Icon: FunctionComponent<SVGAttributes<SVGElement>>;
10
+ labelTranslationKey: TranslationKey;
11
+ } => {
12
+ if (filter?.type === 'exclude') {
13
+ return {
14
+ Icon: Eraser,
15
+ labelTranslationKey: 'common.clear',
16
+ };
17
+ }
18
+
19
+ if (filter?.type === 'include') {
20
+ return {
21
+ Icon: Ban,
22
+ labelTranslationKey: 'cell.filter-cell.exclude',
23
+ };
24
+ }
25
+
26
+ return {
27
+ Icon: FlagFill,
28
+ labelTranslationKey: 'cell.filter-cell.include',
29
+ };
30
+ };
@@ -7,16 +7,9 @@ import { Provider } from 'react-redux';
7
7
 
8
8
  import { useAppLayout, useMediaQueries } from 'hooks';
9
9
  import { LOCALE_FEATURES } from 'i18n';
10
- import { FlagFill, Star } from 'icons';
10
+ import { Star } from 'icons';
11
11
  import { dataUrlToBlob, getTileSizes } from 'lib';
12
- import {
13
- BORDER_COLOR_LIGHT,
14
- BORDER_RADIUS,
15
- BORDER_WIDTH,
16
- COLOR_BACKGROUND,
17
- COLOR_BONUS_START,
18
- COLOR_FILTERED,
19
- } from 'parameters';
12
+ import { BORDER_COLOR_LIGHT, BORDER_RADIUS, BORDER_WIDTH, COLOR_BACKGROUND, COLOR_BONUS_START } from 'parameters';
20
13
  import { selectConfig, selectLocale, selectShowCoordinates, store, useTypedSelector } from 'state';
21
14
  import { Point } from 'types';
22
15
 
@@ -30,7 +23,6 @@ const BONUS = 'b';
30
23
  const BONUS_WORD_2 = 'b2';
31
24
  const BONUS_WORD_3 = 'b3';
32
25
  const BONUS_WORD_4 = 'b4';
33
- const CELL_FILTER = 'c';
34
26
 
35
27
  const useBackgroundImage = () => {
36
28
  const { boardSize, cellSize, coordinatesSize } = useAppLayout();
@@ -131,19 +123,6 @@ const useBackgroundImage = () => {
131
123
  x4
132
124
  </text>
133
125
  </symbol>
134
-
135
- <symbol id={CELL_FILTER}>
136
- <rect
137
- fill={COLOR_FILTERED}
138
- height={bonusSize}
139
- rx={borderRadius}
140
- width={bonusSize}
141
- x={bonusOffset}
142
- y={bonusOffset}
143
- />
144
-
145
- <FlagFill color="white" height={iconSize} width={iconSize} x={iconOffset} y={iconOffset} />
146
- </symbol>
147
126
  </defs>
148
127
 
149
128
  {showCoordinates === 'hidden' && (
@@ -230,7 +209,7 @@ const useBackgroundImage = () => {
230
209
  </Provider>,
231
210
  );
232
211
 
233
- const encodedSvg = globalThis.btoa(backgroundSvg);
212
+ const encodedSvg = useMemo(() => globalThis.btoa(backgroundSvg), [backgroundSvg]);
234
213
  const dataUrl = `data:image/svg+xml;base64,${encodedSvg}`;
235
214
  const blob = useMemo(() => dataUrlToBlob(dataUrl), [dataUrl]);
236
215
  const blobUrl = useMemo(() => URL.createObjectURL(blob), [blob]);
@@ -25,6 +25,7 @@ $radio-box-size: $radio-size + 2 * $radio-inner-border;
25
25
 
26
26
  &.disabled {
27
27
  opacity: var(--opacity--disabled);
28
+ cursor: not-allowed;
28
29
  }
29
30
  }
30
31
 
@@ -35,7 +36,7 @@ $radio-box-size: $radio-size + 2 * $radio-inner-border;
35
36
  width: 100%;
36
37
  height: 100%;
37
38
  opacity: 0;
38
- cursor: pointer;
39
+ cursor: inherit;
39
40
  }
40
41
 
41
42
  .icon {
@@ -13,8 +13,6 @@
13
13
  font-weight: bold;
14
14
  text-transform: uppercase;
15
15
  text-align: center;
16
- transition: var(--transition);
17
- transition-property: background-color, color, box-shadow;
18
16
  user-select: none;
19
17
 
20
18
  @include media('<xs') {
@@ -9,14 +9,12 @@ import {
9
9
  Ref,
10
10
  TouchEventHandler,
11
11
  useCallback,
12
- useEffect,
13
12
  useMemo,
14
13
  useRef,
15
14
  } from 'react';
16
15
 
17
16
  import { useAppLayout } from 'hooks';
18
17
  import { getTileSizes, noop } from 'lib';
19
- import { EASE_OUT_CUBIC, TILE_APPEAR_DURATION, TILE_APPEAR_KEYFRAMES } from 'parameters';
20
18
  import { selectLocale, useTypedSelector } from 'state';
21
19
 
22
20
  import TilePure from './TilePure';
@@ -65,7 +63,7 @@ const Tile: FunctionComponent<Props> = ({
65
63
  onTouchStart = noop,
66
64
  }) => {
67
65
  const locale = useTypedSelector(selectLocale);
68
- const { animateTile, showTilePoints } = useAppLayout();
66
+ const { showTilePoints } = useAppLayout();
69
67
  const { pointsFontSize, tileSize } = getTileSizes(size);
70
68
  const style = useMemo(() => ({ height: tileSize, width: tileSize }), [tileSize]);
71
69
  const pointsStyle = useMemo(() => ({ fontSize: pointsFontSize }), [pointsFontSize]);
@@ -83,18 +81,6 @@ const Tile: FunctionComponent<Props> = ({
83
81
  [onKeyDown],
84
82
  );
85
83
 
86
- useEffect(() => {
87
- if (!ref.current?.parentElement || !character || !animateTile) {
88
- return;
89
- }
90
-
91
- ref.current.parentElement.animate(TILE_APPEAR_KEYFRAMES, {
92
- duration: TILE_APPEAR_DURATION,
93
- easing: EASE_OUT_CUBIC,
94
- fill: 'forwards',
95
- });
96
- }, [character, animateTile]);
97
-
98
84
  return (
99
85
  <TilePure
100
86
  aria-label={ariaLabel}
@@ -7,6 +7,7 @@
7
7
  color: var(--color--tooltip--foreground);
8
8
  z-index: var(--z-index--tooltip);
9
9
  text-align: center;
10
+ pointer-events: none;
10
11
  }
11
12
 
12
13
  .arrow {
@@ -71,7 +71,6 @@ const useAppLayout = () => {
71
71
 
72
72
  return {
73
73
  actionsWidth: 2 * BUTTON_HEIGHT - BORDER_WIDTH,
74
- animateTile: !isLessThanXs,
75
74
  boardSize,
76
75
  cellSize,
77
76
  coordinatesFontSize: coordinatesSize * 0.6,
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "cell.enter-word": "Enter word",
3
- "cell.filter-cell": "Target destination",
3
+ "cell.filter-cell.exclude": "Exclude destination",
4
+ "cell.filter-cell.include": "Target destination",
4
5
  "cell.set-blank": "Mark it a blank",
5
6
  "cell.set-not-blank": "Mark it not a blank",
6
7
  "cell.tile.location": "Board: tile ({{x}}, {{y}})",
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "cell.enter-word": "Entrez un mot",
3
- "cell.filter-cell": "Destination cible",
3
+ "cell.filter-cell.exclude": "Exclure la destination",
4
+ "cell.filter-cell.include": "Destination cible",
4
5
  "cell.set-blank": "Marquer comme vide",
5
6
  "cell.set-not-blank": "Marquer comme non vide",
6
7
  "cell.tile.location": "Plateau: la case ({{x}}, {{y}})",
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "cell.enter-word": "Wort eingeben",
3
- "cell.filter-cell": "Zielort",
3
+ "cell.filter-cell.exclude": "Ziel ausschließen",
4
+ "cell.filter-cell.include": "Ziel anvisieren",
4
5
  "cell.set-blank": "Als Blanko markieren",
5
6
  "cell.set-not-blank": "Nicht als Blanko markieren",
6
7
  "cell.tile.location": "Brett: Stein ({{x}}, {{y}})",
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "cell.enter-word": "کلمه را وارد کنید",
3
- "cell.filter-cell": "مقصد",
3
+ "cell.filter-cell.exclude": "مقصد را حذف کنید",
4
+ "cell.filter-cell.include": "مقصد مورد نظر",
4
5
  "cell.set-blank": "علامت گذاری به عنوان خالی",
5
6
  "cell.set-not-blank": "علامت گذاری به عنوان غیر خالی",
6
7
  "cell.tile.location": "({{x}}، {{y}}) کاشی: صفحه",
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "cell.enter-word": "Wprowadź słowo",
3
- "cell.filter-cell": "Miejsce docelowe",
3
+ "cell.filter-cell.exclude": "Wyklucz miejsce",
4
+ "cell.filter-cell.include": "Miejsce docelowe",
4
5
  "cell.set-blank": "Oznacz jako blank",
5
6
  "cell.set-not-blank": "Oznacz jako nie blank",
6
7
  "cell.tile.location": "Plansza: płytka ({{x}}, {{y}})",
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "cell.enter-word": "Introducere cuvant",
3
- "cell.filter-cell": "Destinatia",
3
+ "cell.filter-cell.exclude": "Excludeți destinația",
4
+ "cell.filter-cell.include": "Destinație țintă",
4
5
  "cell.set-blank": "Marcare camp liber",
5
6
  "cell.set-not-blank": "Marcare camp ocupat ",
6
7
  "cell.tile.location": "Tabla: camp ({{x}}, {{y}})",
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "cell.enter-word": "Ingresar palabra",
3
- "cell.filter-cell": "Destino objetivo",
3
+ "cell.filter-cell.exclude": "Excluir destino",
4
+ "cell.filter-cell.include": "Destino objetivo",
4
5
  "cell.set-blank": "Marcar como en blanco",
5
6
  "cell.set-not-blank": "Marcar como no en blanco",
6
7
  "cell.tile.location": "Tablero: espacio ({{x}}, {{y}})",
@@ -0,0 +1,4 @@
1
+ <!-- https://icons.getbootstrap.com/icons/ban/ -->
2
+ <svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
3
+ <path d="M15 8a6.97 6.97 0 0 0-1.71-4.584l-9.874 9.875A7 7 0 0 0 15 8M2.71 12.584l9.874-9.875a7 7 0 0 0-9.874 9.874ZM16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0" fill="currentColor" />
4
+ </svg>
@@ -2,6 +2,7 @@ export { default as ArrowDown } from './ArrowDown.svg';
2
2
  export { default as ArrowLeft } from './ArrowLeft.svg';
3
3
  export { default as ArrowRight } from './ArrowRight.svg';
4
4
  export { default as ArrowUp } from './ArrowUp.svg';
5
+ export { default as Ban } from './Ban.svg';
5
6
  export { default as BookHalf } from './BookHalf.svg';
6
7
  export { default as CardChecklist } from './CardChecklist.svg';
7
8
  export { default as Check } from './Check.svg';
@@ -1,8 +1,9 @@
1
1
  import { Result } from '@scrabble-solver/types';
2
2
 
3
- import { Point } from 'types';
3
+ import { CellFilterEntry } from 'types';
4
4
 
5
5
  import createRegExp from './createRegExp';
6
+ import resultMatchesCellFilter from './resultMatchesCellFilter';
6
7
 
7
8
  interface GroupedResults {
8
9
  matching: Result[];
@@ -12,7 +13,7 @@ interface GroupedResults {
12
13
  const groupResults = (
13
14
  results: Result[] | undefined,
14
15
  query: string,
15
- cellFilter: Point[],
16
+ cellFilter: CellFilterEntry[],
16
17
  ): GroupedResults | undefined => {
17
18
  if (typeof results === 'undefined') {
18
19
  return results;
@@ -23,12 +24,8 @@ const groupResults = (
23
24
  return results.reduce<GroupedResults>(
24
25
  (groupedResults, result) => {
25
26
  const matchesQuery = () => regExp.test(result.word);
26
- const matchesCellFilter = () =>
27
- cellFilter.every(({ x, y }) => {
28
- return result.cells.some((cell) => cell.x === x && cell.y === y);
29
- });
30
27
 
31
- if (matchesCellFilter() && matchesQuery()) {
28
+ if (resultMatchesCellFilter(result, cellFilter) && matchesQuery()) {
32
29
  groupedResults.matching.push(result);
33
30
  } else {
34
31
  groupedResults.other.push(result);
package/src/lib/index.ts CHANGED
@@ -32,6 +32,7 @@ export { default as isUpperCase } from './isUpperCase';
32
32
  export { default as memoize } from './memoize';
33
33
  export { default as noop } from './noop';
34
34
  export { default as numberComparator } from './numberComparator';
35
+ export { default as resultMatchesCellFilter } from './resultMatchesCellFilter';
35
36
  export { default as reverseComparator } from './reverseComparator';
36
37
  export { default as sortResults } from './sortResults';
37
38
  export { default as unorderedArraysEqual } from './unorderedArraysEqual';
@@ -0,0 +1,23 @@
1
+ import { Result } from '@scrabble-solver/types';
2
+
3
+ import { CellFilterEntry } from 'types';
4
+
5
+ const resultMatchesCellFilter = (result: Result, cellFilter: CellFilterEntry[]) => {
6
+ const excludeFilters = cellFilter.filter((filter) => filter.type === 'exclude');
7
+ const matchesExcludeFilters = excludeFilters.every(({ x, y }) => {
8
+ return result.cells.every((cell) => cell.x !== x || cell.y !== y);
9
+ });
10
+
11
+ if (!matchesExcludeFilters) {
12
+ return false;
13
+ }
14
+
15
+ const includeFilter = cellFilter.filter((filter) => filter.type === 'include');
16
+ const matchesIncludeFilters = includeFilter.every(({ x, y }) => {
17
+ return result.cells.some((cell) => cell.x === x && cell.y === y);
18
+ });
19
+
20
+ return matchesExcludeFilters && matchesIncludeFilters;
21
+ };
22
+
23
+ export default resultMatchesCellFilter;
@@ -6,7 +6,6 @@ export const BREAKPOINTS = {
6
6
  xl: 1400,
7
7
  };
8
8
 
9
- export const EASE_OUT_CUBIC = 'cubic-bezier(0.33, 1, 0.68, 1)'; // https://easings.net/#easeOutCubic
10
9
  export const TRANSITION = 'var(--transition)';
11
10
 
12
11
  export const GITHUB_PROJECT_URL = 'https://github.com/kamilmielnik/scrabble-solver';
@@ -128,11 +127,3 @@ export const RESULTS_HEADER_HEIGHT = RESULTS_ITEM_HEIGHT;
128
127
  export const SOLVER_COLUMN_WIDTH = 580;
129
128
 
130
129
  export const TEXT_INPUT_HEIGHT = 40;
131
-
132
- export const TILE_APPEAR_DURATION = 200;
133
-
134
- export const TILE_APPEAR_KEYFRAMES = [
135
- { transform: 'translateY(0)' },
136
- { transform: 'translateY(10%)', offset: 0.5 },
137
- { transform: 'translateY(0)' },
138
- ];
@@ -12,7 +12,7 @@ import { findWordDefinitions, solve, verify, visit } from 'sdk';
12
12
  import { initialize, reset } from './actions';
13
13
  import {
14
14
  selectBoard,
15
- selectCellIsFiltered,
15
+ selectCellFilter,
16
16
  selectCharacters,
17
17
  selectConfig,
18
18
  selectDictionary,
@@ -56,10 +56,10 @@ export function* rootSaga(): AnyGenerator {
56
56
  }
57
57
 
58
58
  function* onCellValueChange({ payload }: PayloadAction<{ value: string; x: number; y: number }>): AnyGenerator {
59
- const isFiltered = yield select((state) => selectCellIsFiltered(state, payload));
59
+ const filter = yield select((state) => selectCellFilter(state, payload));
60
60
 
61
- if (isFiltered) {
62
- yield put(cellFilterSlice.actions.toggle(payload));
61
+ if (filter) {
62
+ yield put(cellFilterSlice.actions.cancel(payload));
63
63
  }
64
64
 
65
65
  yield put(resultsSlice.actions.changeResultCandidate(null));
@@ -12,6 +12,7 @@ import {
12
12
  getRemainingTiles,
13
13
  getRemainingTilesGroups,
14
14
  groupResults,
15
+ resultMatchesCellFilter,
15
16
  sortResults,
16
17
  unorderedArraysEqual,
17
18
  } from 'lib';
@@ -75,8 +76,8 @@ export const selectConfig = createSelector([selectGame, selectLocale], getConfig
75
76
 
76
77
  export const selectFilteredCells = selectCellFilterRoot;
77
78
 
78
- export const selectCellIsFiltered = createSelector([selectFilteredCells, selectPoint], (cellFilter, { x, y }) => {
79
- return cellFilter.some((cell) => cell.x === x && cell.y === y);
79
+ export const selectCellFilter = createSelector([selectFilteredCells, selectPoint], (cellFilter, { x, y }) => {
80
+ return cellFilter.find((cell) => cell.x === x && cell.y === y);
80
81
  });
81
82
 
82
83
  export const selectCellIsValid = createSelector([selectConfig, selectCell], (config, cell) => {
@@ -106,7 +107,7 @@ export const selectResults = createSelector([selectGroupedResults], (groupedResu
106
107
 
107
108
  export const selectIsResultMatching = createSelector(
108
109
  [selectResults, selectResultsQuery, selectFilteredCells, selectResultIndex],
109
- (results, query, filteredCells, index) => {
110
+ (results, query, cellFilter, index) => {
110
111
  if (!results) {
111
112
  return false;
112
113
  }
@@ -118,11 +119,7 @@ export const selectIsResultMatching = createSelector(
118
119
  return false;
119
120
  }
120
121
 
121
- if (filteredCells) {
122
- return filteredCells.every(({ x, y }) => result.cells.some((cell) => cell.x === x && cell.y === y));
123
- }
124
-
125
- return true;
122
+ return resultMatchesCellFilter(result, cellFilter);
126
123
  },
127
124
  );
128
125
 
@@ -1,6 +1,6 @@
1
- import { Point } from 'types';
1
+ import { CellFilterEntry } from 'types';
2
2
 
3
- export type CellFilterState = Point[];
3
+ export type CellFilterState = CellFilterEntry[];
4
4
 
5
5
  const cellFilterInitialState: CellFilterState = [];
6
6