scrabble-solver 2.13.5 → 2.13.7

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 (94) hide show
  1. package/.nx/cache/file-map.json +423 -408
  2. package/.nx/cache/lockfile.hash +1 -1
  3. package/.nx/cache/nx_files.nxt +0 -0
  4. package/.nx/cache/parsed-lock-file.json +306 -361
  5. package/.nx/cache/project-graph.json +316 -382
  6. package/bump-version.js +15 -9
  7. package/lerna.json +1 -1
  8. package/package.json +8 -8
  9. package/packages/configs/package.json +3 -3
  10. package/packages/constants/package.json +1 -1
  11. package/packages/dictionaries/package-lock.json +2 -2
  12. package/packages/dictionaries/package.json +4 -4
  13. package/packages/logger/package-lock.json +2 -2
  14. package/packages/logger/package.json +1 -1
  15. package/packages/scrabble-solver/.next/BUILD_ID +1 -1
  16. package/packages/scrabble-solver/.next/build-manifest.json +6 -6
  17. package/packages/scrabble-solver/.next/cache/.tsbuildinfo +1 -1
  18. package/packages/scrabble-solver/.next/cache/eslint/.cache_8dgz12 +1 -1
  19. package/packages/scrabble-solver/.next/cache/webpack/client-production/0.pack +0 -0
  20. package/packages/scrabble-solver/.next/cache/webpack/client-production/index.pack +0 -0
  21. package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/0.pack +0 -0
  22. package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/index.pack +0 -0
  23. package/packages/scrabble-solver/.next/cache/webpack/server-production/0.pack +0 -0
  24. package/packages/scrabble-solver/.next/cache/webpack/server-production/index.pack +0 -0
  25. package/packages/scrabble-solver/.next/prerender-manifest.js +1 -1
  26. package/packages/scrabble-solver/.next/prerender-manifest.json +1 -1
  27. package/packages/scrabble-solver/.next/routes-manifest.json +1 -1
  28. package/packages/scrabble-solver/.next/server/chunks/807.js +1 -1
  29. package/packages/scrabble-solver/.next/server/chunks/977.js +1 -1
  30. package/packages/scrabble-solver/.next/server/middleware-build-manifest.js +1 -1
  31. package/packages/scrabble-solver/.next/server/pages/404.html +1 -1
  32. package/packages/scrabble-solver/.next/server/pages/500.html +1 -1
  33. package/packages/scrabble-solver/.next/server/pages/_app.js +1 -1
  34. package/packages/scrabble-solver/.next/server/pages/_error.js +1 -1
  35. package/packages/scrabble-solver/.next/server/pages/api/solve.js +1 -1
  36. package/packages/scrabble-solver/.next/server/pages/index.html +1 -1
  37. package/packages/scrabble-solver/.next/server/pages/index.js +1 -1
  38. package/packages/scrabble-solver/.next/server/pages/index.json +1 -1
  39. package/packages/scrabble-solver/.next/server/pages-manifest.json +1 -1
  40. package/packages/scrabble-solver/.next/static/{w6DXAmYMqW0zwT34pDH0d → QbBIK_mEs1LhYSpQwv6rZ}/_buildManifest.js +1 -1
  41. package/packages/scrabble-solver/.next/static/chunks/pages/{404-129e0943628b6fab.js → 404-9d5621b1ca024a45.js} +1 -1
  42. package/packages/scrabble-solver/.next/static/chunks/pages/_app-7bc3bf713c40526a.js +17 -0
  43. package/packages/scrabble-solver/.next/static/chunks/pages/index-710b5c27542027be.js +1 -0
  44. package/packages/scrabble-solver/.next/static/css/{f549d7823f599b8d.css → fe70298e6f597553.css} +1 -1
  45. package/packages/scrabble-solver/.next/trace +44 -44
  46. package/packages/scrabble-solver/package-lock.json +2 -3
  47. package/packages/scrabble-solver/package.json +8 -9
  48. package/packages/scrabble-solver/public/service-worker.js +1 -1
  49. package/packages/scrabble-solver/src/components/Board/BoardPure.tsx +1 -1
  50. package/packages/scrabble-solver/src/components/Board/hooks/useGrid.ts +13 -5
  51. package/packages/scrabble-solver/src/components/Board/lib/index.ts +0 -1
  52. package/packages/scrabble-solver/src/components/Button/Button.tsx +16 -17
  53. package/packages/scrabble-solver/src/components/Button/Link.tsx +15 -16
  54. package/packages/scrabble-solver/src/components/IconButton/IconButton.tsx +8 -8
  55. package/packages/scrabble-solver/src/components/IconButton/Link.tsx +8 -8
  56. package/packages/scrabble-solver/src/components/Results/Cell.tsx +4 -5
  57. package/packages/scrabble-solver/src/components/Results/HeaderButton.tsx +25 -22
  58. package/packages/scrabble-solver/src/components/Results/Result.tsx +15 -3
  59. package/packages/scrabble-solver/src/components/Results/Results.module.scss +25 -10
  60. package/packages/scrabble-solver/src/components/Results/Results.tsx +1 -1
  61. package/packages/scrabble-solver/src/components/Results/getCoordinatesColumn.ts +15 -0
  62. package/packages/scrabble-solver/src/components/Results/getLocaleColumns.ts +7 -0
  63. package/packages/scrabble-solver/src/components/Results/types.ts +1 -0
  64. package/packages/scrabble-solver/src/components/Results/useColumns.ts +9 -6
  65. package/packages/scrabble-solver/src/components/Tooltip/Tooltip.tsx +28 -0
  66. package/packages/scrabble-solver/src/components/Tooltip/TooltipContent.tsx +53 -0
  67. package/packages/scrabble-solver/src/components/Tooltip/TooltipTrigger.tsx +26 -0
  68. package/packages/scrabble-solver/src/components/Tooltip/context.ts +17 -0
  69. package/packages/scrabble-solver/src/components/Tooltip/index.ts +1 -1
  70. package/packages/scrabble-solver/src/components/Tooltip/useTooltip.ts +54 -0
  71. package/packages/scrabble-solver/src/components/index.ts +1 -1
  72. package/packages/scrabble-solver/src/hooks/index.ts +0 -1
  73. package/packages/scrabble-solver/src/hooks/useLocalStorage.ts +8 -0
  74. package/packages/scrabble-solver/src/lib/getCoordinates.ts +18 -0
  75. package/packages/scrabble-solver/src/lib/groupResults.ts +16 -11
  76. package/packages/scrabble-solver/src/lib/index.ts +2 -1
  77. package/packages/scrabble-solver/src/lib/sortResults.ts +1 -0
  78. package/packages/scrabble-solver/src/pages/_app.tsx +5 -1
  79. package/packages/scrabble-solver/src/parameters/index.ts +1 -0
  80. package/packages/scrabble-solver/src/types/index.ts +1 -0
  81. package/packages/solver/package-lock.json +2 -2
  82. package/packages/solver/package.json +5 -5
  83. package/packages/types/package.json +2 -2
  84. package/packages/word-definitions/package-lock.json +2 -2
  85. package/packages/word-definitions/package.json +2 -2
  86. package/packages/word-lists/package-lock.json +3 -3
  87. package/packages/word-lists/package.json +3 -3
  88. package/packages/scrabble-solver/.next/static/chunks/pages/_app-8788856de4b1b755.js +0 -17
  89. package/packages/scrabble-solver/.next/static/chunks/pages/index-caaf20b2488cb10e.js +0 -1
  90. package/packages/scrabble-solver/src/components/Tooltip/useTooltip.tsx +0 -134
  91. package/packages/scrabble-solver/src/hooks/usePortal.tsx +0 -47
  92. package/packages/scrabble-solver/src/lib/canUseDom.ts +0 -3
  93. /package/packages/scrabble-solver/.next/static/{w6DXAmYMqW0zwT34pDH0d → QbBIK_mEs1LhYSpQwv6rZ}/_ssgManifest.js +0 -0
  94. /package/packages/scrabble-solver/src/{components/Board/lib → lib}/getCoordinate.ts +0 -0
@@ -6,45 +6,48 @@ import { SortDown, SortUp } from 'icons';
6
6
  import { resultsSlice, selectResultsSort, useTranslate, useTypedSelector } from 'state';
7
7
  import { SortDirection } from 'types';
8
8
 
9
- import { useTooltip } from '../Tooltip';
9
+ import { Tooltip } from '../Tooltip';
10
10
 
11
11
  import styles from './Results.module.scss';
12
12
  import { Column } from './types';
13
13
 
14
14
  interface Props {
15
15
  column: Column;
16
+ sortable?: boolean;
16
17
  }
17
18
 
18
- const HeaderButton = ({ column }: Props): ReactElement => {
19
+ const HeaderButton = ({ column, sortable }: Props): ReactElement => {
19
20
  const dispatch = useDispatch();
20
21
  const translate = useTranslate();
21
22
  const sort = useTypedSelector(selectResultsSort);
22
- const triggerProps = useTooltip(translate(column.translationKey));
23
23
 
24
24
  const handleClick = useCallback(() => {
25
25
  dispatch(resultsSlice.actions.sort(column.id));
26
26
  }, [column.id, dispatch]);
27
27
 
28
28
  return (
29
- <button
30
- aria-label={translate(column.translationKey)}
31
- className={classNames(styles.headerButton, column.className)}
32
- key={column.id}
33
- type="button"
34
- onClick={handleClick}
35
- {...triggerProps}
36
- >
37
- <span className={styles.cell}>
38
- <span className={styles.headerButtonLabel}>{translate(column.translationKey)}</span>
39
-
40
- {sort.column === column.id && (
41
- <>
42
- {sort.direction === SortDirection.Ascending && <SortUp className={styles.sortIcon} />}
43
- {sort.direction === SortDirection.Descending && <SortDown className={styles.sortIcon} />}
44
- </>
45
- )}
46
- </span>
47
- </button>
29
+ <Tooltip tooltip={translate(column.translationKey)}>
30
+ <button
31
+ aria-label={translate(column.translationKey)}
32
+ className={classNames(styles.headerButton, column.className, {
33
+ [styles.sortable]: sortable,
34
+ })}
35
+ key={column.id}
36
+ type="button"
37
+ onClick={sortable ? handleClick : undefined}
38
+ >
39
+ <span className={styles.cell}>
40
+ <span className={styles.headerButtonLabel}>{translate(column.translationKey)}</span>
41
+
42
+ {sort.column === column.id && (
43
+ <>
44
+ {sort.direction === SortDirection.Ascending && <SortUp className={styles.sortIcon} />}
45
+ {sort.direction === SortDirection.Descending && <SortDown className={styles.sortIcon} />}
46
+ </>
47
+ )}
48
+ </span>
49
+ </button>
50
+ </Tooltip>
48
51
  );
49
52
  };
50
53
 
@@ -1,10 +1,16 @@
1
1
  import classNames from 'classnames';
2
- import { CSSProperties, FocusEventHandler, MouseEventHandler, ReactElement, useRef } from 'react';
2
+ import { CSSProperties, FocusEventHandler, MouseEventHandler, ReactElement, useMemo, useRef } from 'react';
3
3
  import Highlighter from 'react-highlight-words';
4
4
 
5
5
  import { LOCALE_FEATURES } from 'i18n';
6
- import { noop } from 'lib';
7
- import { selectIsResultMatching, selectLocale, selectResultsQuery, useTypedSelector } from 'state';
6
+ import { getCoordinates, noop } from 'lib';
7
+ import {
8
+ selectIsResultMatching,
9
+ selectLocale,
10
+ selectResultsQuery,
11
+ selectShowCoordinates,
12
+ useTypedSelector,
13
+ } from 'state';
8
14
  import { ResultColumn } from 'types';
9
15
 
10
16
  import Cell from './Cell';
@@ -31,12 +37,14 @@ const Result = ({ data, index, style }: Props): ReactElement => {
31
37
  const ref = useRef<HTMLButtonElement>(null);
32
38
  const columns = useColumns();
33
39
  const locale = useTypedSelector(selectLocale);
40
+ const showCoordinates = useTypedSelector(selectShowCoordinates);
34
41
  const query = useTypedSelector(selectResultsQuery);
35
42
  const { consonants, direction, separator, vowels } = LOCALE_FEATURES[locale];
36
43
  const result = results[index];
37
44
  const isMatching = useTypedSelector((state) => selectIsResultMatching(state, index));
38
45
  const words = direction === 'rtl' ? [...result.words].reverse() : result.words;
39
46
  const enabledColumns = Object.fromEntries(columns.map((column) => [column.id, true]));
47
+ const coordinates = useMemo(() => getCoordinates(result, showCoordinates), [result, showCoordinates]);
40
48
 
41
49
  const handleClick: MouseEventHandler = (event) => onClick(result, event);
42
50
  const handleMouseEnter: MouseEventHandler = (event) => onMouseEnter(result, event);
@@ -61,6 +69,10 @@ const Result = ({ data, index, style }: Props): ReactElement => {
61
69
  onMouseLeave={handleMouseLeave}
62
70
  >
63
71
  <span className={styles.resultContent}>
72
+ {enabledColumns[ResultColumn.Coordinates] && (
73
+ <Cell className={styles.coordinates} translationKey="settings.showCoordinates" value={coordinates} />
74
+ )}
75
+
64
76
  {enabledColumns[ResultColumn.Word] && (
65
77
  <Cell className={styles.word} translationKey="common.word" value={result.word}>
66
78
  <Highlighter highlightClassName={styles.highlight} searchWords={[query]} textToHighlight={result.word} />
@@ -55,19 +55,11 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
55
55
 
56
56
  .headerButton {
57
57
  @include button-reset;
58
- @include focus-effect;
59
58
 
60
- cursor: pointer;
61
59
  text-transform: uppercase;
62
60
  transition: var(--transition);
63
61
  background-color: var(--color--background);
64
62
 
65
- &:focus,
66
- &:hover {
67
- background-color: var(--color--primary);
68
- color: var(--color--primary--opposite);
69
- }
70
-
71
63
  &:first-child {
72
64
  [dir='ltr'] & {
73
65
  border-top-left-radius: inherit;
@@ -89,6 +81,18 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
89
81
  }
90
82
  }
91
83
 
84
+ .sortable {
85
+ @include focus-effect;
86
+
87
+ cursor: pointer;
88
+
89
+ &:focus,
90
+ &:hover {
91
+ background-color: var(--color--primary);
92
+ color: var(--color--primary--opposite);
93
+ }
94
+ }
95
+
92
96
  .headerButtonLabel {
93
97
  @include ellipsis;
94
98
 
@@ -139,6 +143,7 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
139
143
 
140
144
  .word {
141
145
  @include ellipsis;
146
+ display: flex;
142
147
  }
143
148
  }
144
149
 
@@ -151,10 +156,13 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
151
156
  gap: var(--spacing--s);
152
157
  line-height: var(--results--item--height);
153
158
 
154
- .result &:first-child,
155
- .headerButton:first-child & {
159
+ &.word,
160
+ .headerButton.word & {
156
161
  justify-content: flex-start;
162
+ }
157
163
 
164
+ .result &:first-child,
165
+ .headerButton:first-child & {
158
166
  [dir='ltr'] & {
159
167
  padding-left: $row-padding-horizontal;
160
168
  text-align: left;
@@ -203,6 +211,13 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
203
211
  font-weight: bold;
204
212
  }
205
213
 
214
+ .coordinates {
215
+ $width: 50px;
216
+
217
+ flex: 0 0 $width;
218
+ max-width: $width;
219
+ }
220
+
206
221
  .solveButton {
207
222
  display: block;
208
223
  margin: var(--spacing--xl) auto 0;
@@ -69,7 +69,7 @@ const Results: FunctionComponent<Props> = ({ callbacks, className, highlightedIn
69
69
  <div className={classNames(styles.results, className)}>
70
70
  <div className={styles.header}>
71
71
  {columns.map((column) => (
72
- <HeaderButton column={column} key={column.id} />
72
+ <HeaderButton column={column} key={column.id} sortable={column.sortable} />
73
73
  ))}
74
74
  </div>
75
75
 
@@ -0,0 +1,15 @@
1
+ import { ResultColumn } from 'types';
2
+
3
+ import styles from './Results.module.scss';
4
+ import { Column } from './types';
5
+
6
+ const getCoordinatesColumn = (): Column => {
7
+ return {
8
+ className: styles.coordinates,
9
+ id: ResultColumn.Coordinates,
10
+ translationKey: 'settings.showCoordinates',
11
+ sortable: false,
12
+ };
13
+ };
14
+
15
+ export default getCoordinatesColumn;
@@ -10,11 +10,13 @@ const getLocaleColumns = (options: { consonants: boolean; vowels: boolean }): Co
10
10
  className: styles.word,
11
11
  id: ResultColumn.Word,
12
12
  translationKey: 'common.word',
13
+ sortable: true,
13
14
  },
14
15
  {
15
16
  className: styles.stat,
16
17
  id: ResultColumn.TilesCount,
17
18
  translationKey: 'common.tiles',
19
+ sortable: true,
18
20
  },
19
21
  ];
20
22
 
@@ -23,6 +25,7 @@ const getLocaleColumns = (options: { consonants: boolean; vowels: boolean }): Co
23
25
  className: styles.stat,
24
26
  id: ResultColumn.ConsonantsCount,
25
27
  translationKey: 'common.consonants',
28
+ sortable: true,
26
29
  });
27
30
  }
28
31
 
@@ -31,6 +34,7 @@ const getLocaleColumns = (options: { consonants: boolean; vowels: boolean }): Co
31
34
  className: styles.stat,
32
35
  id: ResultColumn.VowelsCount,
33
36
  translationKey: 'common.vowels',
37
+ sortable: true,
34
38
  });
35
39
  }
36
40
 
@@ -39,16 +43,19 @@ const getLocaleColumns = (options: { consonants: boolean; vowels: boolean }): Co
39
43
  className: styles.stat,
40
44
  id: ResultColumn.BlanksCount,
41
45
  translationKey: 'common.blanks',
46
+ sortable: true,
42
47
  },
43
48
  {
44
49
  className: styles.stat,
45
50
  id: ResultColumn.WordsCount,
46
51
  translationKey: 'common.words',
52
+ sortable: true,
47
53
  },
48
54
  {
49
55
  className: styles.points,
50
56
  id: ResultColumn.Points,
51
57
  translationKey: 'common.points',
58
+ sortable: true,
52
59
  },
53
60
  );
54
61
 
@@ -7,6 +7,7 @@ export interface Column {
7
7
  className: string;
8
8
  id: ResultColumn;
9
9
  translationKey: TranslationKey;
10
+ sortable?: boolean;
10
11
  }
11
12
 
12
13
  export interface ResultCallbacks {
@@ -1,8 +1,9 @@
1
1
  import { useMediaQueries } from 'hooks';
2
2
  import { LOCALE_FEATURES } from 'i18n';
3
- import { selectLocale, useTypedSelector } from 'state';
3
+ import { selectLocale, selectShowCoordinates, useTypedSelector } from 'state';
4
4
  import { ResultColumn } from 'types';
5
5
 
6
+ import getCoordinatesColumn from './getCoordinatesColumn';
6
7
  import getLocaleColumns from './getLocaleColumns';
7
8
  import { Column } from './types';
8
9
 
@@ -17,25 +18,27 @@ const COLUMNS_L = [...COLUMNS_XS];
17
18
  const useColumns = (): Column[] => {
18
19
  const locale = useTypedSelector(selectLocale);
19
20
  const localeColumns = getLocaleColumns(LOCALE_FEATURES[locale]);
21
+ const showCoordinates = useTypedSelector(selectShowCoordinates);
22
+ const columns = showCoordinates === 'hidden' ? localeColumns : [getCoordinatesColumn(), ...localeColumns];
20
23
  const { isLessThanXs, isLessThanS, isLessThanM, isLessThanL } = useMediaQueries();
21
24
 
22
25
  if (isLessThanXs) {
23
- return localeColumns.filter((column) => COLUMNS_XS.includes(column.id));
26
+ return columns.filter((column) => COLUMNS_XS.includes(column.id));
24
27
  }
25
28
 
26
29
  if (isLessThanS) {
27
- return localeColumns.filter((column) => COLUMNS_S.includes(column.id));
30
+ return columns.filter((column) => COLUMNS_S.includes(column.id));
28
31
  }
29
32
 
30
33
  if (isLessThanM) {
31
- return localeColumns.filter((column) => COLUMNS_M.includes(column.id));
34
+ return columns.filter((column) => COLUMNS_M.includes(column.id));
32
35
  }
33
36
 
34
37
  if (isLessThanL) {
35
- return localeColumns.filter((column) => COLUMNS_L.includes(column.id));
38
+ return columns.filter((column) => COLUMNS_L.includes(column.id));
36
39
  }
37
40
 
38
- return localeColumns;
41
+ return columns;
39
42
  };
40
43
 
41
44
  export default useColumns;
@@ -0,0 +1,28 @@
1
+ import type { Placement } from '@floating-ui/react';
2
+ import { FunctionComponent, ReactNode } from 'react';
3
+
4
+ import { TooltipContext } from './context';
5
+ import { TooltipContent } from './TooltipContent';
6
+ import { TooltipTrigger } from './TooltipTrigger';
7
+ import { useTooltip } from './useTooltip';
8
+
9
+ interface Props {
10
+ children: ReactNode;
11
+ placement?: Placement;
12
+ tooltip?: ReactNode;
13
+ }
14
+
15
+ export const Tooltip: FunctionComponent<Props> = ({ children, placement, tooltip }) => {
16
+ const state = useTooltip({ placement });
17
+
18
+ if (!tooltip) {
19
+ return children;
20
+ }
21
+
22
+ return (
23
+ <TooltipContext.Provider value={state}>
24
+ <TooltipTrigger>{children}</TooltipTrigger>
25
+ <TooltipContent>{tooltip}</TooltipContent>
26
+ </TooltipContext.Provider>
27
+ );
28
+ };
@@ -0,0 +1,53 @@
1
+ import { FloatingArrow, FloatingPortal, useDelayGroup, useMergeRefs, useTransitionStyles } from '@floating-ui/react';
2
+ import { forwardRef, HTMLProps } from 'react';
3
+
4
+ import { TOOLTIP_DURATION } from 'parameters';
5
+
6
+ import { useTooltipContext } from './context';
7
+ import styles from './Tooltip.module.scss';
8
+
9
+ type Props = HTMLProps<HTMLDivElement>;
10
+
11
+ const INSTANT_DURATION = 0;
12
+
13
+ export const TooltipContent = forwardRef<HTMLDivElement, Props>((props, ref) => {
14
+ const state = useTooltipContext();
15
+ const { context } = state;
16
+ const { isInstantPhase, currentId } = useDelayGroup(context, { id: context.floatingId });
17
+ const finalRef = useMergeRefs([state.refs.setFloating, ref]);
18
+
19
+ const instantPhaseDuration = {
20
+ open: INSTANT_DURATION,
21
+ close: currentId === context.floatingId ? TOOLTIP_DURATION : INSTANT_DURATION,
22
+ };
23
+
24
+ const { isMounted, styles: transitionStyles } = useTransitionStyles(context, {
25
+ duration: isInstantPhase ? instantPhaseDuration : TOOLTIP_DURATION,
26
+ initial: {
27
+ opacity: 0,
28
+ },
29
+ });
30
+
31
+ if (!isMounted) {
32
+ return null;
33
+ }
34
+
35
+ return (
36
+ <FloatingPortal>
37
+ <div
38
+ className={styles.tooltip}
39
+ ref={finalRef}
40
+ style={{
41
+ ...state.floatingStyles,
42
+ ...props.style,
43
+ ...transitionStyles,
44
+ }}
45
+ {...state.getFloatingProps(props)}
46
+ >
47
+ <div>{props.children}</div>
48
+
49
+ <FloatingArrow className={styles.arrow} context={context} ref={state.arrowRef} />
50
+ </div>
51
+ </FloatingPortal>
52
+ );
53
+ });
@@ -0,0 +1,26 @@
1
+ import { useMergeRefs } from '@floating-ui/react';
2
+ import { HTMLProps, cloneElement, forwardRef, isValidElement } from 'react';
3
+
4
+ import { useTooltipContext } from './context';
5
+
6
+ type Props = HTMLProps<HTMLElement>;
7
+
8
+ export const TooltipTrigger = forwardRef<HTMLElement, Props>(({ children, ...props }, ref) => {
9
+ const state = useTooltipContext();
10
+ const childrenElement = children as any; // eslint-disable-line @typescript-eslint/no-explicit-any
11
+ const finalRef = useMergeRefs([state.refs.setReference, ref, childrenElement.ref]);
12
+
13
+ if (!isValidElement(children)) {
14
+ throw new Error("TooltipTrigger's children are not a valid element");
15
+ }
16
+
17
+ return cloneElement(
18
+ children,
19
+ state.getReferenceProps({
20
+ ref: finalRef,
21
+ ...props,
22
+ ...children.props,
23
+ 'data-state': state.open ? 'open' : 'closed',
24
+ }),
25
+ );
26
+ });
@@ -0,0 +1,17 @@
1
+ import { createContext, useContext } from 'react';
2
+
3
+ import { useTooltip } from './useTooltip';
4
+
5
+ type TooltipContextType = ReturnType<typeof useTooltip> | null;
6
+
7
+ export const TooltipContext = createContext<TooltipContextType>(null);
8
+
9
+ export const useTooltipContext = () => {
10
+ const context = useContext(TooltipContext);
11
+
12
+ if (context === null) {
13
+ throw new Error('Tooltip components must be wrapped in <Tooltip />');
14
+ }
15
+
16
+ return context;
17
+ };
@@ -1 +1 @@
1
- export { default as useTooltip } from './useTooltip';
1
+ export { Tooltip } from './Tooltip';
@@ -0,0 +1,54 @@
1
+ import {
2
+ Placement,
3
+ arrow,
4
+ autoUpdate,
5
+ flip,
6
+ offset,
7
+ shift,
8
+ useDelayGroup,
9
+ useDismiss,
10
+ useFloating,
11
+ useFocus,
12
+ useHover,
13
+ useInteractions,
14
+ useRole,
15
+ } from '@floating-ui/react';
16
+ import { useMemo, useRef, useState } from 'react';
17
+
18
+ const ARROW_SIZE = 7;
19
+ const ARROW_GAP = 3;
20
+ const PADDING = 10;
21
+
22
+ interface TooltipOptions {
23
+ placement?: Placement;
24
+ }
25
+
26
+ export const useTooltip = ({ placement = 'top' }: TooltipOptions = {}) => {
27
+ const [isOpen, setIsOpen] = useState(false);
28
+ const arrowRef = useRef(null);
29
+ const data = useFloating({
30
+ placement,
31
+ open: isOpen,
32
+ onOpenChange: setIsOpen,
33
+ whileElementsMounted: autoUpdate,
34
+ middleware: [flip(), shift({ padding: PADDING }), offset(ARROW_SIZE + ARROW_GAP), arrow({ element: arrowRef })],
35
+ });
36
+ const { context } = data;
37
+ const { delay } = useDelayGroup(context);
38
+ const hover = useHover(context, { move: false, delay });
39
+ const focus = useFocus(context);
40
+ const dismiss = useDismiss(context);
41
+ const role = useRole(context, { role: 'tooltip' });
42
+ const interactions = useInteractions([hover, focus, dismiss, role]);
43
+
44
+ return useMemo(
45
+ () => ({
46
+ arrowRef,
47
+ open: isOpen,
48
+ setOpen: setIsOpen,
49
+ ...interactions,
50
+ ...data,
51
+ }),
52
+ [arrowRef, isOpen, setIsOpen, interactions, data],
53
+ );
54
+ };
@@ -23,4 +23,4 @@ export { default as SeoMessage } from './SeoMessage';
23
23
  export { default as Solver } from './Solver';
24
24
  export { default as Spinner } from './Spinner';
25
25
  export { default as Tile } from './Tile';
26
- export { useTooltip } from './Tooltip';
26
+ export * from './Tooltip';
@@ -9,5 +9,4 @@ export { default as useMedia } from './useMedia';
9
9
  export { default as useMediaQueries } from './useMediaQueries';
10
10
  export { default as useMediaQuery } from './useMediaQuery';
11
11
  export { default as useOnWindowResize } from './useOnWindowResize';
12
- export { default as usePortal } from './usePortal';
13
12
  export { default as useViewportSize } from './useViewportSize';
@@ -8,6 +8,7 @@ import {
8
8
  selectInputMode,
9
9
  selectLocale,
10
10
  selectRack,
11
+ selectShowCoordinates,
11
12
  useTypedSelector,
12
13
  } from 'state';
13
14
 
@@ -18,6 +19,7 @@ const useLocalStorage = () => {
18
19
  const inputMode = useTypedSelector(selectInputMode);
19
20
  const locale = useTypedSelector(selectLocale);
20
21
  const rack = useTypedSelector(selectRack);
22
+ const showCoordinates = useTypedSelector(selectShowCoordinates);
21
23
 
22
24
  useEffect(() => {
23
25
  if (autoGroupTiles) {
@@ -54,6 +56,12 @@ const useLocalStorage = () => {
54
56
  localStorage.setRack(rack);
55
57
  }
56
58
  }, [rack]);
59
+
60
+ useEffect(() => {
61
+ if (showCoordinates) {
62
+ localStorage.setShowCoordinates(showCoordinates);
63
+ }
64
+ }, [showCoordinates]);
57
65
  };
58
66
 
59
67
  export default useLocalStorage;
@@ -0,0 +1,18 @@
1
+ import { Result, ShowCoordinates } from '@scrabble-solver/types';
2
+
3
+ import getCoordinate from './getCoordinate';
4
+
5
+ const getCoordinates = (result: Result, showCoordinates: ShowCoordinates): string => {
6
+ if (showCoordinates === 'hidden') {
7
+ return '';
8
+ }
9
+
10
+ const firstCell = result.cells[0];
11
+ const isHorizontal = firstCell.x !== result.cells[1].x;
12
+ const x = getCoordinate(firstCell.x, showCoordinates === 'original' ? 'letter' : 'number');
13
+ const y = getCoordinate(firstCell.y, showCoordinates === 'original' ? 'number' : 'letter');
14
+
15
+ return isHorizontal ? `${y}${x}` : `${x}${y}`;
16
+ };
17
+
18
+ export default getCoordinates;
@@ -18,18 +18,23 @@ const groupResults = (
18
18
  return results;
19
19
  }
20
20
 
21
+ const regExp = createRegExp(query);
22
+
21
23
  return results.reduce<GroupedResults>(
22
- ({ matching, other }, result) => {
23
- const matchesQuery = createRegExp(query).test(result.word);
24
- const matchesCellFilter = cellFilter.every(({ x, y }) => {
25
- return result.cells.some((cell) => cell.x === x && cell.y === y);
26
- });
27
- const isMatching = matchesQuery && matchesCellFilter;
28
-
29
- return {
30
- matching: isMatching ? [...matching, result] : matching,
31
- other: isMatching ? other : [...other, result],
32
- };
24
+ (groupedResults, result) => {
25
+ 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
+
31
+ if (matchesCellFilter() && matchesQuery()) {
32
+ groupedResults.matching.push(result);
33
+ } else {
34
+ groupedResults.other.push(result);
35
+ }
36
+
37
+ return groupedResults;
33
38
  },
34
39
  { matching: [], other: [] },
35
40
  );
@@ -1,5 +1,4 @@
1
1
  export { default as arrayEquals } from './arrayEquals';
2
- export { default as canUseDom } from './canUseDom';
3
2
  export { default as createArray } from './createArray';
4
3
  export { default as createComparator } from './createComparator';
5
4
  export { default as createGridOf } from './createGridOf';
@@ -15,6 +14,8 @@ export { default as extractCharactersByCase } from './extractCharactersByCase';
15
14
  export { default as extractInputValue } from './extractInputValue';
16
15
  export { default as findCell } from './findCell';
17
16
  export { default as getCellSize } from './getCellSize';
17
+ export { default as getCoordinate } from './getCoordinate';
18
+ export { default as getCoordinates } from './getCoordinates';
18
19
  export { default as getRemainingTiles } from './getRemainingTiles';
19
20
  export { default as getRemainingTilesCount } from './getRemainingTilesCount';
20
21
  export { default as getRemainingTilesGroups } from './getRemainingTilesGroups';
@@ -8,6 +8,7 @@ import reverseComparator from './reverseComparator';
8
8
  const comparators: Record<ResultColumn, (locale: string) => Comparator<Result>> = {
9
9
  [ResultColumn.BlanksCount]: (locale: string) => createKeyComparator('blanksCount', locale),
10
10
  [ResultColumn.ConsonantsCount]: (locale: string) => createKeyComparator('consonantsCount', locale),
11
+ [ResultColumn.Coordinates]: () => () => 0,
11
12
  [ResultColumn.Points]: (locale: string) => createKeyComparator('points', locale),
12
13
  [ResultColumn.TilesCount]: (locale: string) => createKeyComparator('tilesCount', locale),
13
14
  [ResultColumn.VowelsCount]: (locale: string) => createKeyComparator('vowelsCount', locale),
@@ -1,3 +1,4 @@
1
+ import { FloatingDelayGroup } from '@floating-ui/react';
1
2
  import { AppProps } from 'next/app';
2
3
  import Head from 'next/head';
3
4
  import { FunctionComponent } from 'react';
@@ -64,7 +65,10 @@ const App: FunctionComponent<AppProps> = ({ Component, pageProps }) => (
64
65
 
65
66
  <Provider store={store}>
66
67
  <SeoMessage />
67
- <Component {...pageProps} />
68
+
69
+ <FloatingDelayGroup delay={0}>
70
+ <Component {...pageProps} />
71
+ </FloatingDelayGroup>
68
72
  </Provider>
69
73
  </>
70
74
  );
@@ -90,6 +90,7 @@ export const MODAL_HEADER_HEIGHT = 45;
90
90
  export const NAV_PADDING = SPACING_L;
91
91
 
92
92
  export const TILE_SIZE = 80;
93
+ export const TOOLTIP_DURATION = TRANSITION_DURATION;
93
94
 
94
95
  export const PLAIN_TILES_COLOR_DEFAULT = COLOR_GREEN;
95
96
  export const PLAIN_TILES_PADDING_HORIZONTAL = 0;
@@ -40,6 +40,7 @@ export interface RemainingTilesGroup {
40
40
  export enum ResultColumn {
41
41
  BlanksCount = 'blanks-count',
42
42
  ConsonantsCount = 'consonants-count',
43
+ Coordinates = 'coordinates',
43
44
  Points = 'points',
44
45
  TilesCount = 'tiles-count',
45
46
  VowelsCount = 'vowels-count',