@scrabble-solver/scrabble-solver 2.11.3 → 2.11.5

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 (137) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +7 -7
  3. package/.next/cache/.tsbuildinfo +1 -1
  4. package/.next/cache/eslint/.cache_8dgz12 +1 -1
  5. package/.next/cache/next-server.js.nft.json +1 -1
  6. package/.next/cache/webpack/client-production/0.pack +0 -0
  7. package/.next/cache/webpack/client-production/index.pack +0 -0
  8. package/.next/cache/webpack/edge-server-production/0.pack +0 -0
  9. package/.next/cache/webpack/edge-server-production/index.pack +0 -0
  10. package/.next/cache/webpack/server-production/0.pack +0 -0
  11. package/.next/cache/webpack/server-production/index.pack +0 -0
  12. package/.next/export-marker.json +1 -1
  13. package/.next/next-server.js.nft.json +1 -1
  14. package/.next/prerender-manifest.json +1 -1
  15. package/.next/routes-manifest.json +1 -1
  16. package/.next/server/chunks/131.js +1 -1
  17. package/.next/server/chunks/277.js +736 -913
  18. package/.next/server/chunks/44.js +2 -30
  19. package/.next/server/chunks/636.js +286 -0
  20. package/.next/server/chunks/675.js +550 -0
  21. package/.next/server/middleware-build-manifest.js +1 -1
  22. package/.next/server/pages/404.html +2 -2
  23. package/.next/server/pages/404.js.nft.json +1 -1
  24. package/.next/server/pages/500.html +1 -1
  25. package/.next/server/pages/_app.js +73 -9
  26. package/.next/server/pages/_app.js.nft.json +1 -1
  27. package/.next/server/pages/_document.js.nft.json +1 -1
  28. package/.next/server/pages/_error.js +1 -280
  29. package/.next/server/pages/_error.js.nft.json +1 -1
  30. package/.next/server/pages/api/solve.js +22 -2
  31. package/.next/server/pages/index.html +1 -1
  32. package/.next/server/pages/index.js +382 -313
  33. package/.next/server/pages/index.js.nft.json +1 -1
  34. package/.next/server/pages/index.json +1 -1
  35. package/.next/static/UzQCOB6CHhyOupkEq8oZM/_buildManifest.js +1 -0
  36. package/.next/static/chunks/pages/{404-448ba28510855455.js → 404-d30fe85d005ce32b.js} +1 -1
  37. package/.next/static/chunks/pages/_app-e27464a187a58684.js +28 -0
  38. package/.next/static/chunks/pages/index-3fd280f406cc00fd.js +1 -0
  39. package/.next/static/css/4bd04cebe207859c.css +1 -0
  40. package/.next/static/css/5b3b78170f4c5875.css +2 -0
  41. package/.next/trace +50 -53
  42. package/next.config.js +1 -0
  43. package/package.json +12 -13
  44. package/src/@types/svg.d.ts +1 -1
  45. package/src/components/Board/Board.tsx +48 -44
  46. package/src/components/Board/components/Actions/Actions.tsx +4 -2
  47. package/src/components/Board/components/Cell/Cell.module.scss +59 -1
  48. package/src/components/Board/hooks/useGrid.ts +5 -3
  49. package/src/components/Button/Button.module.scss +1 -1
  50. package/src/components/Loading/Loading.module.scss +1 -1
  51. package/src/components/Loading/Loading.tsx +1 -1
  52. package/src/components/Logo/Logo.tsx +10 -12
  53. package/src/components/Logo/LogoBlueprint.tsx +21 -0
  54. package/src/components/Logo/index.ts +1 -1
  55. package/src/components/Modal/Modal.module.scss +1 -6
  56. package/src/components/Modal/Modal.tsx +15 -8
  57. package/src/components/NavButtons/NavButtons.tsx +2 -2
  58. package/src/components/Rack/Rack.module.scss +59 -0
  59. package/src/components/Results/HeaderButton.tsx +6 -6
  60. package/src/components/Results/Results.module.scss +3 -0
  61. package/src/components/Results/Results.tsx +7 -7
  62. package/src/components/Results/useColumns.ts +2 -5
  63. package/src/components/Solver/Solver.tsx +6 -23
  64. package/src/components/Tile/Tile.module.scss +2 -1
  65. package/src/components/Tile/Tile.tsx +8 -4
  66. package/src/components/index.ts +0 -3
  67. package/src/hooks/index.ts +6 -0
  68. package/src/hooks/useAppLayout.ts +62 -12
  69. package/src/hooks/useEffectOnce.ts +5 -0
  70. package/src/hooks/useIsTouchDevice.ts +1 -1
  71. package/src/hooks/useLatest.ts +13 -0
  72. package/src/hooks/useLocalStorage.ts +51 -0
  73. package/src/hooks/useMedia.ts +36 -0
  74. package/src/hooks/useMediaQueries.ts +13 -0
  75. package/src/hooks/useMediaQuery.ts +2 -1
  76. package/src/hooks/useOnWindowResize.ts +13 -0
  77. package/src/hooks/useViewportSize.ts +19 -0
  78. package/src/i18n/constants.ts +14 -14
  79. package/src/i18n/de.json +2 -2
  80. package/src/i18n/en.json +2 -2
  81. package/src/i18n/es.json +2 -2
  82. package/src/i18n/fa.json +1 -1
  83. package/src/i18n/fr.json +2 -2
  84. package/src/i18n/pl.json +2 -2
  85. package/src/lib/arrayEquals.ts +5 -0
  86. package/src/lib/index.ts +1 -0
  87. package/src/lib/zipCharactersAndTiles.ts +3 -1
  88. package/src/modals/DictionaryModal/DictionaryModal.tsx +2 -2
  89. package/src/modals/KeyMapModal/KeyMapModal.tsx +2 -2
  90. package/src/modals/KeyMapModal/keys.tsx +0 -2
  91. package/src/modals/MenuModal/MenuModal.module.scss +28 -4
  92. package/src/modals/MenuModal/MenuModal.tsx +4 -4
  93. package/src/modals/RemainingTilesModal/RemainingTilesModal.tsx +2 -2
  94. package/src/modals/ResultsModal/ResultsModal.module.scss +1 -5
  95. package/src/modals/ResultsModal/ResultsModal.tsx +10 -2
  96. package/src/modals/SettingsModal/SettingsModal.tsx +2 -2
  97. package/src/modals/SettingsModal/components/AutoGroupTilesSetting/lib.ts +3 -1
  98. package/src/modals/SettingsModal/components/LocaleSetting/LocaleSetting.module.scss +1 -1
  99. package/src/modals/WordsModal/WordsModal.tsx +2 -2
  100. package/src/pages/index.module.scss +3 -21
  101. package/src/pages/index.tsx +51 -67
  102. package/src/parameters/index.ts +29 -2
  103. package/src/state/localStorage.ts +13 -2
  104. package/src/state/sagas.ts +16 -8
  105. package/src/state/slices/boardInitialState.ts +5 -1
  106. package/src/state/slices/boardSlice.ts +2 -2
  107. package/src/state/slices/rackInitialState.ts +8 -2
  108. package/src/state/slices/rackSlice.ts +16 -13
  109. package/src/state/slices/settingsInitialState.ts +9 -4
  110. package/src/state/slices/settingsSlice.ts +3 -1
  111. package/src/styles/animations.scss +0 -20
  112. package/src/styles/global.scss +0 -7
  113. package/src/styles/mixins.scss +0 -59
  114. package/src/styles/variables.scss +11 -0
  115. package/src/types/index.ts +4 -0
  116. package/.next/static/USLkKOoHbITebIEHkMGX_/_buildManifest.js +0 -1
  117. package/.next/static/chunks/pages/_app-21c83ddb81fc09d0.js +0 -28
  118. package/.next/static/chunks/pages/index-0858deea02b2a417.js +0 -1
  119. package/.next/static/css/885da289cec275b3.css +0 -1
  120. package/.next/static/css/ea1c8134fe9a143e.css +0 -2
  121. package/src/components/LogoSplashScreen/LogoSplashScreen.module.scss +0 -65
  122. package/src/components/LogoSplashScreen/LogoSplashScreen.tsx +0 -31
  123. package/src/components/LogoSplashScreen/index.ts +0 -1
  124. package/src/components/Sizer/Sizer.module.scss +0 -10
  125. package/src/components/Sizer/Sizer.tsx +0 -10
  126. package/src/components/Sizer/index.ts +0 -1
  127. package/src/components/SplashScreen/SplashScreen.module.scss +0 -14
  128. package/src/components/SplashScreen/SplashScreen.tsx +0 -19
  129. package/src/components/SplashScreen/index.ts +0 -1
  130. package/src/hooks/useLocalStorage/index.ts +0 -1
  131. package/src/hooks/useLocalStorage/useLocalStorage.ts +0 -13
  132. package/src/hooks/useLocalStorage/useLocalStorageBoard.ts +0 -29
  133. package/src/hooks/useLocalStorage/useLocalStorageConfigId.ts +0 -29
  134. package/src/hooks/useLocalStorage/useLocalStorageLocale.ts +0 -32
  135. package/src/hooks/useLocalStorage/useLocalStorageRack.ts +0 -29
  136. /package/.next/static/{USLkKOoHbITebIEHkMGX_ → UzQCOB6CHhyOupkEq8oZM}/_ssgManifest.js +0 -0
  137. /package/{src/components/Logo/Logo.svg → public/logo.svg} +0 -0
package/next.config.js CHANGED
@@ -35,6 +35,7 @@ module.exports = {
35
35
  ...config.module.rules,
36
36
  {
37
37
  test: /\.svg$/,
38
+ include: [path.resolve(__dirname, 'src/icons')],
38
39
  issuer: /\.tsx?$/,
39
40
  use: ['@svgr/webpack'],
40
41
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scrabble-solver/scrabble-solver",
3
- "version": "2.11.3",
3
+ "version": "2.11.5",
4
4
  "description": "Scrabble Solver 2 - App",
5
5
  "engines": {
6
6
  "node": ">=16"
@@ -28,16 +28,16 @@
28
28
  "start": "env-cmd next start -p 3333"
29
29
  },
30
30
  "dependencies": {
31
- "@floating-ui/react": "^0.20.1",
31
+ "@floating-ui/react": "^0.21.1",
32
32
  "@kamilmielnik/trie": "^2.0.1",
33
33
  "@reduxjs/toolkit": "^1.9.3",
34
- "@scrabble-solver/configs": "^2.11.3",
35
- "@scrabble-solver/constants": "^2.11.3",
36
- "@scrabble-solver/dictionaries": "^2.11.3",
37
- "@scrabble-solver/logger": "^2.11.3",
38
- "@scrabble-solver/solver": "^2.11.3",
39
- "@scrabble-solver/types": "^2.11.3",
40
- "@scrabble-solver/word-definitions": "^2.11.3",
34
+ "@scrabble-solver/configs": "^2.11.5",
35
+ "@scrabble-solver/constants": "^2.11.5",
36
+ "@scrabble-solver/dictionaries": "^2.11.5",
37
+ "@scrabble-solver/logger": "^2.11.5",
38
+ "@scrabble-solver/solver": "^2.11.5",
39
+ "@scrabble-solver/types": "^2.11.5",
40
+ "@scrabble-solver/word-definitions": "^2.11.5",
41
41
  "classnames": "^2.3.2",
42
42
  "include-media": "^2.0.0",
43
43
  "include-media-query-builder": "^1.1.0",
@@ -48,9 +48,8 @@
48
48
  "react-modal": "^3.16.1",
49
49
  "react-portal": "^4.2.2",
50
50
  "react-redux": "^8.0.5",
51
- "react-use": "^17.4.0",
52
51
  "react-window": "^1.8.8",
53
- "redux-saga": "^1.2.2",
52
+ "redux-saga": "^1.2.3",
54
53
  "store2": "^2.14.2",
55
54
  "workbox-expiration": "^6.5.4",
56
55
  "workbox-precaching": "^6.5.4",
@@ -69,8 +68,8 @@
69
68
  "@types/redux": "^3.6.31",
70
69
  "@types/redux-saga": "^0.10.5",
71
70
  "env-cmd": "^10.1.0",
72
- "sass": "^1.59.2",
71
+ "sass": "^1.59.3",
73
72
  "workbox-webpack-plugin": "^6.5.4"
74
73
  },
75
- "gitHead": "693e06a0f1375268d31d12c1a24924bf5ca3c267"
74
+ "gitHead": "6b881cf1208c9d8ea93d61086179bd813f4d2788"
76
75
  }
@@ -1,4 +1,4 @@
1
- declare module '*.svg' {
1
+ declare module 'icons/*.svg' {
2
2
  import { FunctionComponent, SVGAttributes } from 'react';
3
3
 
4
4
  const component: FunctionComponent<SVGAttributes<SVGElement>>;
@@ -1,9 +1,9 @@
1
- import { autoUpdate, FloatingPortal, offset, shift, useFloating, useMergeRefs } from '@floating-ui/react';
1
+ import { autoUpdate, FloatingPortal, offset, shift, useFloating } from '@floating-ui/react';
2
2
  import classNames from 'classnames';
3
- import { CSSProperties, FocusEventHandler, FunctionComponent, useState } from 'react';
3
+ import { CSSProperties, FocusEventHandler, FunctionComponent, useCallback, useState } from 'react';
4
4
  import { useDispatch } from 'react-redux';
5
- import { useMeasure } from 'react-use';
6
5
 
6
+ import { useAppLayout } from 'hooks';
7
7
  import { BOARD_CELL_ACTIONS_OFFSET, TRANSITION } from 'parameters';
8
8
  import { boardSlice, cellFilterSlice, selectBoard, selectRowsWithCandidate, useTypedSelector } from 'state';
9
9
 
@@ -21,7 +21,7 @@ const Board: FunctionComponent<Props> = ({ cellSize, className }) => {
21
21
  const dispatch = useDispatch();
22
22
  const rows = useTypedSelector(selectRowsWithCandidate);
23
23
  const board = useTypedSelector(selectBoard);
24
- const [actionsMeasureRef, { width: actionsWidth }] = useMeasure<HTMLDivElement>();
24
+ const { actionsWidth } = useAppLayout();
25
25
  const [{ activeIndex, direction, inputRefs }, { onChange, onDirectionToggle, onFocus, onKeyDown, onPaste }] =
26
26
  useGrid(rows);
27
27
  const inputRef = inputRefs[activeIndex.y][activeIndex.x];
@@ -41,53 +41,57 @@ const Board: FunctionComponent<Props> = ({ cellSize, className }) => {
41
41
  whileElementsMounted: autoUpdate,
42
42
  });
43
43
 
44
- const actionsRef = useMergeRefs([actionsMeasureRef, refs.setFloating]);
45
-
46
- const handleBlur: FocusEventHandler = (event) => {
47
- const eventComesFromActions = refs.floating.current?.contains(event.relatedTarget);
48
- const eventComesFromBoard = event.currentTarget.contains(event.relatedTarget);
49
- const isLocalEvent = eventComesFromActions || eventComesFromBoard;
50
-
51
- if (!isLocalEvent) {
52
- setShowActions(false);
53
- }
54
- };
44
+ const handleBlur: FocusEventHandler = useCallback(
45
+ (event) => {
46
+ const eventComesFromActions = refs.floating.current?.contains(event.relatedTarget);
47
+ const eventComesFromBoard = event.currentTarget.contains(event.relatedTarget);
48
+ const isLocalEvent = eventComesFromActions || eventComesFromBoard;
49
+
50
+ if (!isLocalEvent) {
51
+ setShowActions(false);
52
+ }
53
+ },
54
+ [refs.floating],
55
+ );
55
56
 
56
- const handleDirectionToggle = () => {
57
+ const handleDirectionToggle = useCallback(() => {
57
58
  inputRef.current?.focus();
58
59
  onDirectionToggle();
59
- };
60
-
61
- const handleFocus: typeof onFocus = (newX, newY) => {
62
- const isFirstFocus = !showActions;
63
- const originalTransition = refs.floating.current?.style.transition || '';
64
- const newInputRef = inputRefs[newY][newX].current;
65
- const newTileElement = newInputRef?.parentElement || null;
66
-
67
- if (isFirstFocus) {
68
- setTransition('none');
69
- }
70
-
71
- refs.setReference(newTileElement);
72
- onFocus(newX, newY);
73
- setShowActions(true);
74
-
75
- if (isFirstFocus) {
76
- setTimeout(() => {
77
- setTransition(originalTransition);
78
- }, 0);
79
- }
80
- };
81
-
82
- const handleToggleBlank = () => {
60
+ }, [inputRef, onDirectionToggle]);
61
+
62
+ const handleFocus: typeof onFocus = useCallback(
63
+ (newX, newY) => {
64
+ const isFirstFocus = !showActions;
65
+ const originalTransition = refs.floating.current?.style.transition || '';
66
+ const newInputRef = inputRefs[newY][newX].current;
67
+ const newTileElement = newInputRef?.parentElement || null;
68
+
69
+ if (isFirstFocus) {
70
+ setTransition('none');
71
+ }
72
+
73
+ refs.setReference(newTileElement);
74
+ onFocus(newX, newY);
75
+ setShowActions(true);
76
+
77
+ if (isFirstFocus) {
78
+ setTimeout(() => {
79
+ setTransition(originalTransition);
80
+ }, 0);
81
+ }
82
+ },
83
+ [inputRefs, onFocus, refs.floating, refs.setReference, showActions],
84
+ );
85
+
86
+ const handleToggleBlank = useCallback(() => {
83
87
  inputRef.current?.focus();
84
88
  dispatch(boardSlice.actions.toggleCellIsBlank(cell));
85
- };
89
+ }, [cell, dispatch, inputRef]);
86
90
 
87
- const handleToggleFilterCell = () => {
91
+ const handleToggleFilterCell = useCallback(() => {
88
92
  inputRef.current?.focus();
89
93
  dispatch(cellFilterSlice.actions.toggle(cell));
90
- };
94
+ }, [cell, dispatch, inputRef]);
91
95
 
92
96
  return (
93
97
  <>
@@ -112,7 +116,7 @@ const Board: FunctionComponent<Props> = ({ cellSize, className }) => {
112
116
  })}
113
117
  disabled={!showActions}
114
118
  direction={direction}
115
- ref={actionsRef}
119
+ ref={refs.setFloating}
116
120
  style={{
117
121
  position: strategy,
118
122
  top: y ?? 0,
@@ -4,7 +4,8 @@ import classNames from 'classnames';
4
4
  import { forwardRef, HTMLProps, MouseEventHandler } from 'react';
5
5
 
6
6
  import { ArrowDown, Flag, FlagFill, Square, SquareFill } from 'icons';
7
- import { selectCellIsFiltered, useTranslate, useTypedSelector } from 'state';
7
+ import { findCell } from 'lib';
8
+ import { selectCellIsFiltered, selectResultCandidateCells, useTranslate, useTypedSelector } from 'state';
8
9
 
9
10
  import Button from '../../../Button';
10
11
 
@@ -22,8 +23,9 @@ const Actions = forwardRef<HTMLDivElement, Props>(
22
23
  ({ cell, className, direction, disabled, onDirectionToggle, onToggleBlank, onToggleFilterCell, ...props }, ref) => {
23
24
  const translate = useTranslate();
24
25
  const isFiltered = useTypedSelector((state) => selectCellIsFiltered(state, cell));
26
+ const resultCandidateCells = useTypedSelector(selectResultCandidateCells);
25
27
  const isBlank = cell.tile.isBlank;
26
- const isEmpty = cell.tile.character === EMPTY_CELL;
28
+ const isEmpty = cell.tile.character === EMPTY_CELL || Boolean(findCell(resultCandidateCells, cell.x, cell.y));
27
29
 
28
30
  // On iOS it helps with losing focus too early which makes Actions disappear
29
31
  const handleMouseDown: MouseEventHandler = (event) => event.preventDefault();
@@ -1,5 +1,64 @@
1
1
  @import 'styles/mixins';
2
2
 
3
+ @mixin lighthouse-input-size-hack {
4
+ // Hack for this Lighthouse warning:
5
+ // > Interactive elements like buttons and links should be large enough (48x48px), and have
6
+ // > enough space around them, to be easy enough to tap without overlapping onto other elements.
7
+
8
+ input {
9
+ position: absolute;
10
+ top: -100%;
11
+ right: -100%;
12
+ left: -100%;
13
+ bottom: -100%;
14
+ width: 300%;
15
+ height: 300%;
16
+ clip-path: inset((100% / 3));
17
+ }
18
+
19
+ [dir='ltr'] & {
20
+ &:nth-child(1),
21
+ &:nth-child(2),
22
+ &:nth-child(3) {
23
+ input {
24
+ left: 0;
25
+ clip-path: polygon(0 (100% / 3), (100% / 3) (100% / 3), (100% / 3) (200% / 3), 0 (200% / 3));
26
+ }
27
+ }
28
+
29
+ &:nth-last-child(1),
30
+ &:nth-last-child(2),
31
+ &:nth-last-child(3) {
32
+ input {
33
+ left: -200%;
34
+ clip-path: polygon((200% / 3) (100% / 3), 100% (100% / 3), 100% (200% / 3), (200% / 3) (200% / 3));
35
+ }
36
+ }
37
+ }
38
+
39
+ [dir='rtl'] & {
40
+ &:nth-child(1),
41
+ &:nth-child(2),
42
+ &:nth-child(3) {
43
+ input {
44
+ left: -200%;
45
+ right: 0;
46
+ clip-path: polygon((200% / 3) (100% / 3), 100% (100% / 3), 100% (200% / 3), (200% / 3) (200% / 3));
47
+ }
48
+ }
49
+
50
+ &:nth-last-child(1),
51
+ &:nth-last-child(2),
52
+ &:nth-last-child(3) {
53
+ input {
54
+ left: 0;
55
+ right: -200%;
56
+ clip-path: polygon(0 (100% / 3), (100% / 3) (100% / 3), (100% / 3) (200% / 3), 0 (200% / 3));
57
+ }
58
+ }
59
+ }
60
+ }
61
+
3
62
  .cell {
4
63
  @include focus-effect;
5
64
  @include lighthouse-input-size-hack;
@@ -9,7 +68,6 @@
9
68
  vertical-align: middle;
10
69
  background-color: var(--color--white);
11
70
  border-bottom: var(--border--width) dotted var(--border--color--light);
12
- transition: var(--transition);
13
71
  background-clip: padding-box;
14
72
 
15
73
  [dir='ltr'] & {
@@ -14,8 +14,8 @@ import {
14
14
  useState,
15
15
  } from 'react';
16
16
  import { useDispatch } from 'react-redux';
17
- import { useLatest } from 'react-use';
18
17
 
18
+ import { useLatest } from 'hooks';
19
19
  import { LOCALE_FEATURES } from 'i18n';
20
20
  import { createGridOf, createKeyboardNavigation, extractCharacters, extractInputValue } from 'lib';
21
21
  import { boardSlice, selectConfig, selectLocale, useTypedSelector } from 'state';
@@ -235,6 +235,7 @@ const useGrid = (rows: Cell[][]): [State, Actions] => {
235
235
 
236
236
  if (direction === 'vertical') {
237
237
  onDirectionToggle();
238
+ changeActiveIndex(LOCALE_FEATURES[locale].direction === 'ltr' ? -1 : 0, 0);
238
239
  } else {
239
240
  changeActiveIndex(LOCALE_FEATURES[locale].direction === 'ltr' ? -1 : 1, 0);
240
241
  }
@@ -244,6 +245,7 @@ const useGrid = (rows: Cell[][]): [State, Actions] => {
244
245
 
245
246
  if (direction === 'vertical') {
246
247
  onDirectionToggle();
248
+ changeActiveIndex(LOCALE_FEATURES[locale].direction === 'ltr' ? 0 : -1, 0);
247
249
  } else {
248
250
  changeActiveIndex(LOCALE_FEATURES[locale].direction === 'ltr' ? 1 : -1, 0);
249
251
  }
@@ -253,9 +255,9 @@ const useGrid = (rows: Cell[][]): [State, Actions] => {
253
255
 
254
256
  if (direction === 'horizontal') {
255
257
  onDirectionToggle();
256
- } else {
257
- changeActiveIndex(0, -1);
258
258
  }
259
+
260
+ changeActiveIndex(0, -1);
259
261
  },
260
262
  onBackspace: (event) => {
261
263
  const position = getInputRefPosition(event.target as HTMLInputElement);
@@ -61,7 +61,7 @@
61
61
  display: flex;
62
62
  align-items: center;
63
63
  gap: var(--spacing--l);
64
-
64
+ line-height: var(--button--icon--size);
65
65
  }
66
66
 
67
67
  .icon {
@@ -19,7 +19,7 @@
19
19
  backdrop-filter: blur(2px);
20
20
  }
21
21
 
22
- .logo {
22
+ .text {
23
23
  position: relative;
24
24
  z-index: 2;
25
25
  width: 100%;
@@ -31,7 +31,7 @@ const Loading: FunctionComponent<Props> = ({ className, wave = true }) => {
31
31
  return (
32
32
  <div aria-label={translation} className={classNames(styles.loading, className)} role="status">
33
33
  <div className={styles.dim} />
34
- <div className={styles.logo}>
34
+ <div className={styles.text}>
35
35
  <PlainTiles className={classNames(styles.tiles)} content={content} dropShadow wave={wave} />
36
36
  </div>
37
37
  </div>
@@ -1,21 +1,19 @@
1
- /**
2
- * This component is unused, but it serves as a blueprint for the Logo.svg.
3
- * Logo.svg is what this component generates with all the text nodes transformed
4
- * into paths (manually with Inkscape), and corner radius removed from tiles.
5
- */
6
- import { CSSProperties, FunctionComponent } from 'react';
1
+ import Image from 'next/image';
2
+ import { forwardRef } from 'react';
7
3
 
8
- import PlainTiles from '../PlainTiles';
4
+ import { useAppLayout } from 'hooks';
5
+ import { LOGO_SRC } from 'parameters';
9
6
 
10
7
  interface Props {
11
8
  className?: string;
12
- style?: CSSProperties;
13
9
  }
14
10
 
15
- const CONTENT = [['SCRABBLE'], ['SOLVER', '2']];
11
+ const Logo = forwardRef<HTMLImageElement, Props>((props, ref) => {
12
+ const { logoHeight, logoWidth } = useAppLayout();
16
13
 
17
- const Logo: FunctionComponent<Props> = ({ className, style }) => (
18
- <PlainTiles className={className} content={CONTENT} style={style} />
19
- );
14
+ return (
15
+ <Image {...props} alt="Scrabble Solver 2" height={logoHeight} priority ref={ref} src={LOGO_SRC} width={logoWidth} />
16
+ );
17
+ });
20
18
 
21
19
  export default Logo;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * This component is unused, but it serves as a blueprint for the Logo.svg.
3
+ * Logo.svg is what this component generates with all the text nodes transformed
4
+ * into paths (manually with Inkscape), and corner radius removed from tiles.
5
+ */
6
+ import { CSSProperties, FunctionComponent } from 'react';
7
+
8
+ import PlainTiles from '../PlainTiles';
9
+
10
+ interface Props {
11
+ className?: string;
12
+ style?: CSSProperties;
13
+ }
14
+
15
+ const CONTENT = [['SCRABBLE'], ['SOLVER', '2']];
16
+
17
+ const LogoBlueprint: FunctionComponent<Props> = ({ className, style }) => (
18
+ <PlainTiles className={className} content={CONTENT} style={style} />
19
+ );
20
+
21
+ export default LogoBlueprint;
@@ -1 +1 @@
1
- export { default } from './Logo.svg';
1
+ export { default } from './Logo';
@@ -25,10 +25,6 @@
25
25
  &:focus {
26
26
  outline: none;
27
27
  }
28
-
29
- @include media('<s') {
30
- --modal--width: 100%;
31
- }
32
28
  }
33
29
 
34
30
  .afterOpen {
@@ -126,12 +122,11 @@
126
122
  flex: 0 0 auto;
127
123
  display: flex;
128
124
  flex-direction: row-reverse;
129
- justify-content: space-between;
130
125
  gap: var(--spacing--l);
131
126
  padding: var(--spacing--l);
132
127
  padding-top: 0;
133
128
 
134
- @include media('>s') {
129
+ @include media('>l') {
135
130
  display: none;
136
131
  }
137
132
 
@@ -1,7 +1,6 @@
1
1
  import classNames from 'classnames';
2
- import { FunctionComponent, ReactNode, useEffect, useState } from 'react';
2
+ import { FunctionComponent, ReactNode, useCallback, useEffect, useState } from 'react';
3
3
  import ReactModal from 'react-modal';
4
- import { useKey } from 'react-use';
5
4
 
6
5
  import { CrossSquareFill } from 'icons';
7
6
  import { TRANSITION_DURATION_LONG } from 'parameters';
@@ -25,16 +24,24 @@ const Modal: FunctionComponent<Props> = ({ children, className, footer, isOpen,
25
24
  const translate = useTranslate();
26
25
  const [shouldReturnFocusAfterClose, setShouldReturnFocusAfterClose] = useState(true);
27
26
 
28
- useKey(
29
- 'Escape',
30
- () => {
31
- setShouldReturnFocusAfterClose(false);
32
- onClose();
27
+ const handleEscape = useCallback(
28
+ (event: KeyboardEvent) => {
29
+ if (event.key === 'Escape') {
30
+ setShouldReturnFocusAfterClose(false);
31
+ onClose();
32
+ }
33
33
  },
34
- undefined,
35
34
  [onClose],
36
35
  );
37
36
 
37
+ useEffect(() => {
38
+ document.addEventListener('keydown', handleEscape);
39
+
40
+ return () => {
41
+ document.removeEventListener('keydown', handleEscape);
42
+ };
43
+ }, [handleEscape]);
44
+
38
45
  useEffect(() => {
39
46
  if (isOpen) {
40
47
  setShouldReturnFocusAfterClose(true);
@@ -1,5 +1,5 @@
1
1
  import classNames from 'classnames';
2
- import { FunctionComponent } from 'react';
2
+ import { FunctionComponent, memo } from 'react';
3
3
 
4
4
  import { useAppLayout } from 'hooks';
5
5
  import { CardChecklist, Cog, Eraser, Github, Keyboard, List, Sack } from 'icons';
@@ -135,4 +135,4 @@ const NavButtons: FunctionComponent<Props> = ({
135
135
  );
136
136
  };
137
137
 
138
- export default NavButtons;
138
+ export default memo(NavButtons);
@@ -1,5 +1,64 @@
1
1
  @import 'styles/mixins';
2
2
 
3
+ @mixin lighthouse-input-size-hack {
4
+ // Hack for this Lighthouse warning:
5
+ // > Interactive elements like buttons and links should be large enough (48x48px), and have
6
+ // > enough space around them, to be easy enough to tap without overlapping onto other elements.
7
+
8
+ input {
9
+ position: absolute;
10
+ top: -200%;
11
+ right: -100%;
12
+ left: -100%;
13
+ bottom: 0;
14
+ width: 300%;
15
+ height: 300%;
16
+ clip-path: polygon((100% / 3) (200% / 3), (200% / 3) (200% / 3), (200% / 3) 100%, (100% / 3) 100%);
17
+ }
18
+
19
+ [dir='ltr'] & {
20
+ &:nth-child(1),
21
+ &:nth-child(2),
22
+ &:nth-child(3) {
23
+ input {
24
+ left: 0;
25
+ clip-path: polygon(0 (200% / 3), (100% / 3) (200% / 3), (100% / 3) 100%, 0 100%);
26
+ }
27
+ }
28
+
29
+ &:nth-last-child(1),
30
+ &:nth-last-child(2),
31
+ &:nth-last-child(3) {
32
+ input {
33
+ left: -200%;
34
+ clip-path: polygon((200% / 3) (200% / 3), 100% (200% / 3), 100% 100%, (200% / 3) 100%);
35
+ }
36
+ }
37
+ }
38
+
39
+ [dir='rtl'] & {
40
+ &:nth-child(1),
41
+ &:nth-child(2),
42
+ &:nth-child(3) {
43
+ input {
44
+ left: -200%;
45
+ right: 0;
46
+ clip-path: polygon((200% / 3) (200% / 3), 100% (200% / 3), 100% 100%, (200% / 3) 100%);
47
+ }
48
+ }
49
+
50
+ &:nth-last-child(1),
51
+ &:nth-last-child(2),
52
+ &:nth-last-child(3) {
53
+ input {
54
+ left: 0;
55
+ right: -200%;
56
+ clip-path: polygon(0 (200% / 3), (100% / 3) (200% / 3), (100% / 3) 100%, 0 100%);
57
+ }
58
+ }
59
+ }
60
+ }
61
+
3
62
  .rack {
4
63
  display: flex;
5
64
  box-shadow: var(--box-shadow);
@@ -1,10 +1,10 @@
1
1
  import classNames from 'classnames';
2
- import { ReactElement } from 'react';
2
+ import { ReactElement, useCallback } from 'react';
3
3
  import { useDispatch } from 'react-redux';
4
4
 
5
5
  import { SortDown, SortUp } from 'icons';
6
6
  import { resultsSlice, selectResultsSort, useTranslate, useTypedSelector } from 'state';
7
- import { ResultColumn, SortDirection } from 'types';
7
+ import { SortDirection } from 'types';
8
8
 
9
9
  import { useTooltip } from '../Tooltip';
10
10
 
@@ -21,9 +21,9 @@ const HeaderButton = ({ column }: Props): ReactElement => {
21
21
  const sort = useTypedSelector(selectResultsSort);
22
22
  const triggerProps = useTooltip(translate(column.translationKey));
23
23
 
24
- const handleOrderChange = (columnId: ResultColumn) => {
25
- dispatch(resultsSlice.actions.sort(columnId));
26
- };
24
+ const handleClick = useCallback(() => {
25
+ dispatch(resultsSlice.actions.sort(column.id));
26
+ }, [column.id, dispatch]);
27
27
 
28
28
  return (
29
29
  <button
@@ -31,7 +31,7 @@ const HeaderButton = ({ column }: Props): ReactElement => {
31
31
  className={classNames(styles.headerButton, column.className)}
32
32
  key={column.id}
33
33
  type="button"
34
- onClick={() => handleOrderChange(column.id)}
34
+ onClick={handleClick}
35
35
  {...triggerProps}
36
36
  >
37
37
  <span className={styles.cell}>
@@ -26,6 +26,9 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
26
26
  .listContainer {
27
27
  position: absolute;
28
28
  top: 0;
29
+ left: 0;
30
+ right: 0;
31
+ bottom: 0;
29
32
  }
30
33
 
31
34
  .list {