@scrabble-solver/scrabble-solver 2.10.11 → 2.11.0

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 (111) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +12 -12
  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/next-server.js.nft.json +1 -1
  13. package/.next/prerender-manifest.json +1 -1
  14. package/.next/required-server-files.json +1 -1
  15. package/.next/routes-manifest.json +1 -1
  16. package/.next/server/chunks/{176.js → 277.js} +734 -590
  17. package/.next/server/chunks/{290.js → 417.js} +3 -3
  18. package/.next/server/chunks/50.js +371 -343
  19. package/.next/server/chunks/664.js +15 -15
  20. package/.next/server/chunks/859.js +17 -10
  21. package/.next/server/middleware-build-manifest.js +1 -1
  22. package/.next/server/next-font-manifest.js +1 -0
  23. package/.next/server/pages/404.html +2 -2
  24. package/.next/server/pages/404.js.nft.json +1 -1
  25. package/.next/server/pages/500.html +1 -1
  26. package/.next/server/pages/_app.js +4 -4
  27. package/.next/server/pages/_app.js.nft.json +1 -1
  28. package/.next/server/pages/_document.js +2 -2
  29. package/.next/server/pages/_document.js.nft.json +1 -1
  30. package/.next/server/pages/_error.js +4 -4
  31. package/.next/server/pages/api/dictionary/[locale]/[word].js +4 -4
  32. package/.next/server/pages/api/dictionary/[locale]/[word].js.nft.json +1 -1
  33. package/.next/server/pages/api/dictionary/[locale].js +3 -3
  34. package/.next/server/pages/api/dictionary/[locale].js.nft.json +1 -1
  35. package/.next/server/pages/api/solve.js +9 -10
  36. package/.next/server/pages/api/solve.js.nft.json +1 -1
  37. package/.next/server/pages/api/verify.js +3 -3
  38. package/.next/server/pages/api/verify.js.nft.json +1 -1
  39. package/.next/server/pages/api/visit.js +3 -3
  40. package/.next/server/pages/api/visit.js.nft.json +1 -1
  41. package/.next/server/pages/index.html +1 -1
  42. package/.next/server/pages/index.js +253 -159
  43. package/.next/server/pages/index.js.nft.json +1 -1
  44. package/.next/server/pages/index.json +1 -1
  45. package/.next/static/45ye7793DY705HOcuK9lJ/_buildManifest.js +1 -0
  46. package/.next/static/chunks/main-0ecb9ccfcb6c9b24.js +1 -0
  47. package/.next/static/chunks/pages/404-e0f30450e9920dc3.js +1 -0
  48. package/.next/static/chunks/pages/_app-d7acee5e526752d9.js +28 -0
  49. package/.next/static/chunks/pages/{_error-8353112a01355ec2.js → _error-54de1933a164a1ff.js} +1 -1
  50. package/.next/static/chunks/pages/index-35d2c1c79a201ae2.js +1 -0
  51. package/.next/static/css/a48caa6f57de6e98.css +1 -0
  52. package/.next/static/css/c49bbe944ddd1b39.css +2 -0
  53. package/.next/trace +55 -55
  54. package/package.json +12 -12
  55. package/src/components/Board/components/Cell/Cell.module.scss +1 -0
  56. package/src/components/Board/hooks/useGrid.ts +1 -1
  57. package/src/components/Dictionary/Dictionary.module.scss +13 -8
  58. package/src/components/Dictionary/Dictionary.tsx +5 -5
  59. package/src/components/Loading/Loading.tsx +1 -1
  60. package/src/components/Modal/Modal.module.scss +0 -1
  61. package/src/components/NavButtons/NavButtons.tsx +4 -5
  62. package/src/components/Results/Results.module.scss +5 -2
  63. package/src/components/Results/Results.tsx +11 -11
  64. package/src/components/Solver/Solver.module.scss +1 -0
  65. package/src/components/Solver/Solver.tsx +13 -26
  66. package/src/components/Solver/components/FloatingSolveButton/FloatingSolveButton.module.scss +7 -0
  67. package/src/components/Solver/components/{SolveButton/SolveButton.tsx → FloatingSolveButton/FloatingSolveButton.tsx} +6 -6
  68. package/src/components/Solver/components/FloatingSolveButton/index.ts +1 -0
  69. package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.module.scss +19 -4
  70. package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.tsx +13 -1
  71. package/src/components/Solver/components/index.ts +1 -1
  72. package/src/components/Spinner/Spinner.module.scss +11 -0
  73. package/src/components/Spinner/Spinner.tsx +19 -0
  74. package/src/components/Spinner/index.ts +1 -0
  75. package/src/components/Tile/Tile.tsx +5 -5
  76. package/src/components/index.ts +1 -0
  77. package/src/hooks/index.ts +1 -0
  78. package/src/hooks/useAppLayout.ts +29 -0
  79. package/src/i18n/de.json +1 -0
  80. package/src/i18n/en.json +1 -0
  81. package/src/i18n/es.json +1 -0
  82. package/src/i18n/fa.json +1 -0
  83. package/src/i18n/fr.json +1 -0
  84. package/src/i18n/index.ts +1 -1
  85. package/src/i18n/pl.json +1 -0
  86. package/src/modals/DictionaryModal/DictionaryModal.module.scss +18 -0
  87. package/src/modals/DictionaryModal/DictionaryModal.tsx +27 -0
  88. package/src/modals/DictionaryModal/index.ts +1 -0
  89. package/src/modals/MenuModal/MenuModal.tsx +7 -1
  90. package/src/modals/ResultsModal/ResultsModal.module.scss +18 -0
  91. package/src/modals/ResultsModal/ResultsModal.tsx +8 -7
  92. package/src/modals/index.ts +1 -0
  93. package/src/pages/api/solve.ts +9 -10
  94. package/src/pages/index.tsx +20 -15
  95. package/src/state/localStorage.ts +0 -9
  96. package/src/styles/animations.scss +10 -0
  97. package/src/styles/global.scss +1 -1
  98. package/src/styles/variables.scss +1 -1
  99. package/src/types/index.ts +1 -0
  100. package/.next/server/font-loader-manifest.js +0 -1
  101. package/.next/static/chunks/main-74c4d6b2b5c362f3.js +0 -1
  102. package/.next/static/chunks/pages/404-6c1a6e3251710371.js +0 -1
  103. package/.next/static/chunks/pages/_app-d98e480ff8c583de.js +0 -28
  104. package/.next/static/chunks/pages/index-bd1c7d3872c37456.js +0 -1
  105. package/.next/static/css/a9b55372a26cf77d.css +0 -1
  106. package/.next/static/css/b8954b85e2fa5b63.css +0 -2
  107. package/.next/static/msKI0ZURgJImoGBJvCBiF/_buildManifest.js +0 -1
  108. package/src/components/Solver/components/SolveButton/SolveButton.module.scss +0 -4
  109. package/src/components/Solver/components/SolveButton/index.ts +0 -1
  110. /package/.next/server/{font-loader-manifest.json → next-font-manifest.json} +0 -0
  111. /package/.next/static/{msKI0ZURgJImoGBJvCBiF → 45ye7793DY705HOcuK9lJ}/_ssgManifest.js +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scrabble-solver/scrabble-solver",
3
- "version": "2.10.11",
3
+ "version": "2.11.0",
4
4
  "description": "Scrabble Solver 2 - App",
5
5
  "engines": {
6
6
  "node": ">=16"
@@ -28,21 +28,21 @@
28
28
  "start": "env-cmd next start -p 3333"
29
29
  },
30
30
  "dependencies": {
31
- "@floating-ui/react": "^0.19.2",
31
+ "@floating-ui/react": "^0.20.1",
32
32
  "@kamilmielnik/trie": "^2.0.1",
33
33
  "@popperjs/core": "^2.11.6",
34
34
  "@reduxjs/toolkit": "^1.9.3",
35
- "@scrabble-solver/configs": "^2.10.11",
36
- "@scrabble-solver/constants": "^2.10.11",
37
- "@scrabble-solver/dictionaries": "^2.10.11",
38
- "@scrabble-solver/logger": "^2.10.11",
39
- "@scrabble-solver/solver": "^2.10.11",
40
- "@scrabble-solver/types": "^2.10.11",
41
- "@scrabble-solver/word-definitions": "^2.10.11",
35
+ "@scrabble-solver/configs": "^2.11.0",
36
+ "@scrabble-solver/constants": "^2.11.0",
37
+ "@scrabble-solver/dictionaries": "^2.11.0",
38
+ "@scrabble-solver/logger": "^2.11.0",
39
+ "@scrabble-solver/solver": "^2.11.0",
40
+ "@scrabble-solver/types": "^2.11.0",
41
+ "@scrabble-solver/word-definitions": "^2.11.0",
42
42
  "classnames": "^2.3.2",
43
43
  "include-media": "^2.0.0",
44
44
  "include-media-query-builder": "^1.1.0",
45
- "next": "^13.2.1",
45
+ "next": "^13.2.4",
46
46
  "normalize.css": "^8.0.1",
47
47
  "react": "^18.2.0",
48
48
  "react-dom": "^18.2.0",
@@ -74,8 +74,8 @@
74
74
  "@types/redux-saga": "^0.10.5",
75
75
  "@types/uuid": "^9.0.1",
76
76
  "env-cmd": "^10.1.0",
77
- "sass": "^1.58.3",
77
+ "sass": "^1.59.2",
78
78
  "workbox-webpack-plugin": "^6.5.4"
79
79
  },
80
- "gitHead": "b070bc7baa92d64a480fd457ad8cc757af51efc0"
80
+ "gitHead": "493dd931e7f16c34c425295a0f048756d6c192c8"
81
81
  }
@@ -112,6 +112,7 @@
112
112
  font-size: 60%;
113
113
  font-weight: bold;
114
114
  color: var(--color--white);
115
+ transition: var(--transition);
115
116
  content: ' ';
116
117
 
117
118
  [lang='fa-IR'] & {
@@ -333,7 +333,7 @@ const useGrid = (rows: Cell[][]): [State, Actions] => {
333
333
  dispatch(boardSlice.actions.toggleCellIsBlank(position));
334
334
  },
335
335
  });
336
- }, [changeActiveIndex, config, dispatch, locale, onDirectionToggle, rows]);
336
+ }, [changeActiveIndex, config, dispatch, locale, moveFocus, onDirectionToggle, rows]);
337
337
 
338
338
  const onPaste = useCallback<ClipboardEventHandler>(
339
339
  (event) => {
@@ -1,15 +1,26 @@
1
1
  .dictionary {
2
2
  position: relative;
3
+ max-height: var(--dictionary--height);
3
4
  height: var(--dictionary--height);
4
5
  overflow-y: auto;
5
6
  word-break: break-word;
6
7
  transition: var(--transition);
8
+
9
+ &.isAllowed {
10
+ background-color: var(--color--green--light);
11
+ }
12
+
13
+ &.isNotAllowed {
14
+ background-color: var(--color--red--light);
15
+ }
7
16
  }
8
17
 
9
18
  .result {
10
19
  transition: var(--transition);
11
20
 
12
21
  &.isAllowed {
22
+ background-color: var(--color--green--light);
23
+
13
24
  & + & {
14
25
  .content {
15
26
  padding-top: 0;
@@ -18,6 +29,8 @@
18
29
  }
19
30
 
20
31
  &.isNotAllowed {
32
+ background-color: var(--color--red--light);
33
+
21
34
  & + & {
22
35
  .content {
23
36
  padding-top: 0;
@@ -37,14 +50,6 @@
37
50
  text-transform: uppercase;
38
51
  }
39
52
 
40
- .isAllowed {
41
- background-color: var(--color--green--light);
42
- }
43
-
44
- .isNotAllowed {
45
- background-color: var(--color--red--light);
46
- }
47
-
48
53
  .definitions {
49
54
  margin: 0;
50
55
 
@@ -16,18 +16,18 @@ const Dictionary: FunctionComponent<Props> = ({ className }) => {
16
16
  const translate = useTranslate();
17
17
  const { results, isLoading } = useTypedSelector(selectDictionary);
18
18
  const error = useTypedSelector(selectDictionaryError);
19
- const isFirstAllowed = results.length > 0 ? results[0].isAllowed : undefined;
19
+ const isLastAllowed = results.length > 0 ? results[results.length - 1].isAllowed : undefined;
20
20
 
21
21
  return (
22
22
  <div
23
23
  className={classNames(styles.dictionary, className, {
24
- [styles.isAllowed]: isFirstAllowed === true,
25
- [styles.isNotAllowed]: isFirstAllowed === false,
24
+ [styles.isAllowed]: isLastAllowed === true,
25
+ [styles.isNotAllowed]: isLastAllowed === false,
26
26
  })}
27
27
  >
28
- {typeof error !== 'undefined' && <EmptyState variant="error">{error.message}</EmptyState>}
28
+ {typeof error !== 'undefined' && !isLoading && <EmptyState variant="error">{error.message}</EmptyState>}
29
29
 
30
- {typeof error === 'undefined' && results.length === 0 && (
30
+ {typeof error === 'undefined' && !isLoading && results.length === 0 && (
31
31
  <EmptyState variant="info">{translate('dictionary.empty-state.uninitialized')}</EmptyState>
32
32
  )}
33
33
 
@@ -29,7 +29,7 @@ const Loading: FunctionComponent<Props> = ({ className, wave = true }) => {
29
29
  const content = useMemo(() => prepareContent(message), [message]);
30
30
 
31
31
  return (
32
- <div className={classNames(styles.loading, className)}>
32
+ <div aria-label={translation} className={classNames(styles.loading, className)} role="status">
33
33
  <div className={styles.dim} />
34
34
  <div className={styles.logo}>
35
35
  <PlainTiles className={classNames(styles.tiles)} content={content} dropShadow wave={wave} />
@@ -115,7 +115,6 @@
115
115
  position: relative;
116
116
  flex: 1;
117
117
  min-height: 0;
118
- margin-top: calc(-1 * var(--spacing--l));
119
118
  padding: var(--spacing--l);
120
119
  overflow: auto;
121
120
  }
@@ -1,7 +1,7 @@
1
1
  import classNames from 'classnames';
2
2
  import { FunctionComponent } from 'react';
3
3
 
4
- import { useIsTouchDevice, useMediaQuery } from 'hooks';
4
+ import { useAppLayout } from 'hooks';
5
5
  import { CardChecklist, Cog, Eraser, Github, Keyboard, List, Sack } from 'icons';
6
6
  import { GITHUB_PROJECT_URL } from 'parameters';
7
7
  import { selectHasInvalidWords, selectHasOverusedTiles, useTranslate, useTypedSelector } from 'state';
@@ -30,10 +30,9 @@ const NavButtons: FunctionComponent<Props> = ({
30
30
  const translate = useTranslate();
31
31
  const hasOverusedTiles = useTypedSelector(selectHasOverusedTiles);
32
32
  const hasInvalidWords = useTypedSelector(selectHasInvalidWords);
33
- const isTouchDevice = useIsTouchDevice();
34
- const isLessThanS = useMediaQuery('<s');
33
+ const { showKeyMap, showShortNav } = useAppLayout();
35
34
 
36
- if (isLessThanS) {
35
+ if (showShortNav) {
37
36
  return (
38
37
  <div className={styles.navButtons}>
39
38
  <div className={styles.group}>
@@ -114,7 +113,7 @@ const NavButtons: FunctionComponent<Props> = ({
114
113
  <div className={styles.separator} />
115
114
 
116
115
  <div className={styles.group}>
117
- {!isTouchDevice && (
116
+ {showKeyMap && (
118
117
  <IconButton
119
118
  aria-label={translate('keyMap')}
120
119
  className={styles.button}
@@ -21,8 +21,6 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
21
21
  flex: 1;
22
22
  position: relative;
23
23
  height: 100%;
24
- border-top: var(--border);
25
- border-bottom: var(--border);
26
24
  }
27
25
 
28
26
  .listContainer {
@@ -45,6 +43,7 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
45
43
  align-items: center;
46
44
  justify-content: space-between;
47
45
  font-weight: 700;
46
+ border-bottom: var(--border);
48
47
  }
49
48
 
50
49
  .headerButton {
@@ -173,3 +172,7 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
173
172
  width: $size;
174
173
  height: $size;
175
174
  }
175
+
176
+ .input {
177
+ border-top: var(--border);
178
+ }
@@ -1,6 +1,6 @@
1
1
  import classNames from 'classnames';
2
2
  import { FunctionComponent, useEffect, useMemo, useState } from 'react';
3
- import { useMeasure } from 'react-use';
3
+ import { useLatest, useMeasure } from 'react-use';
4
4
  import { FixedSizeList } from 'react-window';
5
5
 
6
6
  import { LOCALE_FEATURES } from 'i18n';
@@ -30,10 +30,11 @@ import useColumns from './useColumns';
30
30
 
31
31
  interface Props {
32
32
  callbacks: ResultCallbacks;
33
+ className?: string;
33
34
  highlightedIndex?: number;
34
35
  }
35
36
 
36
- const Results: FunctionComponent<Props> = ({ callbacks, highlightedIndex }) => {
37
+ const Results: FunctionComponent<Props> = ({ callbacks, className, highlightedIndex }) => {
37
38
  const translate = useTranslate();
38
39
  const locale = useTypedSelector(selectLocale);
39
40
  const { direction } = LOCALE_FEATURES[locale];
@@ -48,23 +49,26 @@ const Results: FunctionComponent<Props> = ({ callbacks, highlightedIndex }) => {
48
49
  const [listRef, setListRef] = useState<FixedSizeList<ResultData> | null>(null);
49
50
  const columns = useColumns();
50
51
  const scrollToIndex = typeof highlightedIndex === 'number' ? highlightedIndex : 0;
52
+ const scrollToIndexRef = useLatest(scrollToIndex);
53
+ const hasResults =
54
+ typeof error === 'undefined' && typeof filteredResults !== 'undefined' && typeof allResults !== 'undefined';
51
55
 
52
56
  useEffect(() => {
53
57
  // without setTimeout, the initial scrolling offset is calculated
54
58
  // incorrectly, as the list is not fully rendered by the browser yet
55
59
  const timeout = globalThis.setTimeout(() => {
56
60
  if (listRef) {
57
- listRef.scrollToItem(scrollToIndex, 'center');
61
+ listRef.scrollToItem(scrollToIndexRef.current, 'center');
58
62
  }
59
63
  }, 0);
60
64
 
61
65
  return () => {
62
66
  globalThis.clearTimeout(timeout);
63
67
  };
64
- }, [listRef, scrollToIndex]);
68
+ }, [allResults, listRef, scrollToIndexRef]);
65
69
 
66
70
  return (
67
- <div className={styles.results}>
71
+ <div className={classNames(styles.results, className)}>
68
72
  <div className={styles.header}>
69
73
  {columns.map((column) => (
70
74
  <HeaderButton column={column} key={column.id} />
@@ -88,9 +92,7 @@ const Results: FunctionComponent<Props> = ({ callbacks, highlightedIndex }) => {
88
92
  </EmptyState>
89
93
  )}
90
94
 
91
- {typeof error === 'undefined' &&
92
- typeof filteredResults !== 'undefined' &&
93
- typeof allResults !== 'undefined' && (
95
+ {hasResults && (
94
96
  <>
95
97
  {isOutdated && (
96
98
  <EmptyState className={styles.emptyState} variant="info">
@@ -138,9 +140,7 @@ const Results: FunctionComponent<Props> = ({ callbacks, highlightedIndex }) => {
138
140
  )}
139
141
  </div>
140
142
 
141
- {typeof error === 'undefined' && typeof filteredResults !== 'undefined' && typeof allResults !== 'undefined' && (
142
- <>{allResults.length > 0 && !isOutdated && <ResultsInput />}</>
143
- )}
143
+ {hasResults && allResults.length > 0 && !isOutdated && <ResultsInput className={styles.input} />}
144
144
 
145
145
  {isLoading && <Loading />}
146
146
  </div>
@@ -114,6 +114,7 @@
114
114
 
115
115
  position: fixed;
116
116
  bottom: var(--spacing);
117
+ z-index: 1;
117
118
 
118
119
  @include media('<xs') {
119
120
  --spacing: var(--spacing--m);
@@ -4,15 +4,8 @@ import { FunctionComponent, SyntheticEvent, useEffect, useMemo } from 'react';
4
4
  import { useDispatch } from 'react-redux';
5
5
  import { useMeasure } from 'react-use';
6
6
 
7
- import { useIsTouchDevice, useMediaQuery } from 'hooks';
8
- import {
9
- BOARD_TILE_SIZE_MAX,
10
- BOARD_TILE_SIZE_MIN,
11
- BORDER_WIDTH,
12
- COMPONENTS_SPACING,
13
- COMPONENTS_SPACING_SMALL,
14
- RACK_TILE_SIZE_MAX,
15
- } from 'parameters';
7
+ import { useAppLayout, useIsTouchDevice } from 'hooks';
8
+ import { BOARD_TILE_SIZE_MAX, BOARD_TILE_SIZE_MIN, BORDER_WIDTH, RACK_TILE_SIZE_MAX } from 'parameters';
16
9
  import {
17
10
  resultsSlice,
18
11
  selectAreResultsOutdated,
@@ -32,7 +25,7 @@ import DictionaryInput from '../DictionaryInput';
32
25
  import Rack from '../Rack';
33
26
  import Results from '../Results';
34
27
 
35
- import { EmptyState, ResultCandidatePicker, SolveButton } from './components';
28
+ import { EmptyState, FloatingSolveButton, ResultCandidatePicker } from './components';
36
29
  import styles from './Solver.module.scss';
37
30
 
38
31
  interface Props {
@@ -47,14 +40,12 @@ const Solver: FunctionComponent<Props> = ({ className, height, width, onShowResu
47
40
  const dispatch = useDispatch();
48
41
  const translate = useTranslate();
49
42
  const isTouchDevice = useIsTouchDevice();
43
+ const { componentsSpacing, isBoardFullWidth, showColumn, showCompactControls, showFloatingSolveButton } =
44
+ useAppLayout();
50
45
  const [bottomContainerRef, { height: bottomContainerHeight }] = useMeasure<HTMLDivElement>();
51
46
  const [columnRef, { width: columnWidth }] = useMeasure<HTMLDivElement>();
52
- const isLessThanXl = useMediaQuery('<xl');
53
- const isLessThanL = useMediaQuery('<l');
54
- const isLessThanM = useMediaQuery('<m');
55
- const componentsSpacing = isLessThanXl ? COMPONENTS_SPACING_SMALL : COMPONENTS_SPACING;
56
- const maxBoardWidth = width - columnWidth - (isLessThanL ? 0 : componentsSpacing) - 2 * componentsSpacing;
57
- const maxBoardHeight = Math.max(height - bottomContainerHeight, isLessThanM ? Number.POSITIVE_INFINITY : 0);
47
+ const maxBoardWidth = width - columnWidth - (showColumn ? componentsSpacing : 0) - 2 * componentsSpacing;
48
+ const maxBoardHeight = isBoardFullWidth ? Number.POSITIVE_INFINITY : Math.max(height - bottomContainerHeight, 0);
58
49
  const config = useTypedSelector(selectConfig);
59
50
  const error = useTypedSelector(selectSolveError);
60
51
  const isOutdated = useTypedSelector(selectAreResultsOutdated);
@@ -107,24 +98,20 @@ const Solver: FunctionComponent<Props> = ({ className, height, width, onShowResu
107
98
  const handleSubmit = (event: SyntheticEvent) => {
108
99
  event.preventDefault();
109
100
 
110
- if (isLessThanL) {
111
- onShowResults();
112
- }
113
-
114
101
  dispatch(solveSlice.actions.submit());
115
102
  };
116
103
 
117
104
  useEffect(() => {
118
- if (isLessThanL && bestResult && !isOutdated) {
105
+ if (showCompactControls && bestResult && !isOutdated) {
119
106
  dispatch(resultsSlice.actions.changeResultCandidate(bestResult));
120
107
  }
121
- }, [bestResult, dispatch, isLessThanL, isOutdated]);
108
+ }, [bestResult, dispatch, showCompactControls, isOutdated]);
122
109
 
123
110
  return (
124
111
  <div className={classNames(styles.solver, className)}>
125
112
  <div className={styles.container}>
126
113
  <div className={styles.content}>
127
- <form className={styles.boardContainer} onSubmit={handleSubmit}>
114
+ <form id="a" className={styles.boardContainer} onSubmit={handleSubmit}>
128
115
  <Board cellSize={cellSizeSafe} className={styles.board} />
129
116
  <input className={styles.submitInput} tabIndex={-1} type="submit" />
130
117
  </form>
@@ -142,12 +129,12 @@ const Solver: FunctionComponent<Props> = ({ className, height, width, onShowResu
142
129
 
143
130
  <div className={styles.bottomContainer} ref={bottomContainerRef}>
144
131
  <div className={styles.bottomContent}>
145
- <form onSubmit={handleSubmit}>
132
+ <form id="b" onSubmit={handleSubmit}>
146
133
  <Rack className={styles.rack} tileSize={tileSize} />
147
134
  <input className={styles.submitInput} tabIndex={-1} type="submit" />
148
135
  </form>
149
136
 
150
- {isLessThanL && (
137
+ {showCompactControls && (
151
138
  <div className={styles.controls} style={{ maxWidth: maxControlsWidth }}>
152
139
  <ResultCandidatePicker onResultClick={onShowResults} />
153
140
 
@@ -167,7 +154,7 @@ const Solver: FunctionComponent<Props> = ({ className, height, width, onShowResu
167
154
  </div>
168
155
  </div>
169
156
 
170
- {isTouchDevice && <SolveButton className={styles.solve} onClick={handleSubmit} />}
157
+ {showFloatingSolveButton && <FloatingSolveButton className={styles.solve} onClick={handleSubmit} />}
171
158
  </div>
172
159
  );
173
160
  };
@@ -0,0 +1,7 @@
1
+ @import 'styles/animations';
2
+
3
+ .floatingSolveButton {
4
+ padding: var(--spacing--l);
5
+ border-radius: 50%;
6
+ box-shadow: var(--box-shadow) !important;
7
+ }
@@ -14,15 +14,16 @@ import {
14
14
  } from 'state';
15
15
 
16
16
  import Button from '../../../Button';
17
+ import Spinner from '../../../Spinner';
17
18
 
18
- import styles from './SolveButton.module.scss';
19
+ import styles from './FloatingSolveButton.module.scss';
19
20
 
20
21
  interface Props {
21
22
  className?: string;
22
23
  onClick?: MouseEventHandler;
23
24
  }
24
25
 
25
- const SolveButton: FunctionComponent<Props> = ({ className, onClick = noop }) => {
26
+ const FloatingSolveButton: FunctionComponent<Props> = ({ className, onClick = noop }) => {
26
27
  const dispatch = useDispatch();
27
28
  const translate = useTranslate();
28
29
  const isLoading = useTypedSelector(selectIsLoading);
@@ -38,10 +39,9 @@ const SolveButton: FunctionComponent<Props> = ({ className, onClick = noop }) =>
38
39
  return (
39
40
  <Button
40
41
  aria-label={translate('results.solve')}
41
- className={classNames(styles.solveButton, className)}
42
+ className={classNames(styles.floatingSolveButton, className)}
42
43
  disabled={isLoading || !isOutdated || !hasTiles}
43
- Icon={Search}
44
- iconClassName={styles.icon}
44
+ Icon={isLoading ? Spinner : Search}
45
45
  tooltip={translate('results.solve')}
46
46
  type="submit"
47
47
  variant="primary"
@@ -50,4 +50,4 @@ const SolveButton: FunctionComponent<Props> = ({ className, onClick = noop }) =>
50
50
  );
51
51
  };
52
52
 
53
- export default SolveButton;
53
+ export default FloatingSolveButton;
@@ -0,0 +1 @@
1
+ export { default } from './FloatingSolveButton';
@@ -1,6 +1,7 @@
1
1
  @import 'styles/mixins';
2
2
 
3
3
  .resultCandidatePicker {
4
+ position: relative;
4
5
  display: flex;
5
6
  gap: var(--spacing--l);
6
7
 
@@ -137,14 +138,28 @@
137
138
  }
138
139
  }
139
140
 
140
- .icon {
141
- $size: 20px;
141
+ .spinnerContainer {
142
+ position: absolute;
143
+ top: 0;
144
+ right: 0;
145
+ bottom: 0;
146
+ left: 0;
147
+ display: flex;
148
+ align-items: center;
149
+ justify-content: center;
150
+ }
142
151
 
143
- width: $size;
144
- height: $size;
152
+ .loading,
153
+ .icon {
154
+ width: var(--button--icon--size);
155
+ height: var(--button--icon--size);
145
156
  color: var(--color--inactive);
146
157
  }
147
158
 
159
+ .loading {
160
+ border-color: var(--color--inactive);
161
+ }
162
+
148
163
  .insert {
149
164
  flex: 0 0 auto;
150
165
  }
@@ -2,10 +2,12 @@ import classNames from 'classnames';
2
2
  import { FunctionComponent, HTMLProps, MouseEventHandler } from 'react';
3
3
  import { useDispatch } from 'react-redux';
4
4
 
5
+ import { useAppLayout } from 'hooks';
5
6
  import { ChevronDown, ChevronLeft, ChevronRight } from 'icons';
6
7
  import {
7
8
  resultsSlice,
8
9
  selectAreResultsOutdated,
10
+ selectIsLoading,
9
11
  selectLocale,
10
12
  selectResultCandidate,
11
13
  selectSortedResults,
@@ -14,6 +16,7 @@ import {
14
16
  } from 'state';
15
17
 
16
18
  import Button from '../../../Button';
19
+ import Spinner from '../../../Spinner';
17
20
  import InsertButton from '../InsertButton';
18
21
 
19
22
  import styles from './ResultCandidatePicker.module.scss';
@@ -26,6 +29,7 @@ const ResultCandidatePicker: FunctionComponent<Props> = ({ className, onResultCl
26
29
  const dispatch = useDispatch();
27
30
  const translate = useTranslate();
28
31
  const locale = useTypedSelector(selectLocale);
32
+ const isLoading = useTypedSelector(selectIsLoading);
29
33
  const isOutdated = useTypedSelector(selectAreResultsOutdated);
30
34
  const sortedResults = useTypedSelector(selectSortedResults);
31
35
  const results = sortedResults || [];
@@ -35,6 +39,7 @@ const ResultCandidatePicker: FunctionComponent<Props> = ({ className, onResultCl
35
39
  const isPreviousDisabled = index <= 0 || disabled;
36
40
  const isNextDisabled = index >= results.length - 1 || disabled;
37
41
  const bothEnabled = !isPreviousDisabled && !isNextDisabled;
42
+ const { showFloatingSolveButton } = useAppLayout();
38
43
 
39
44
  const handleNextClick = () => {
40
45
  if (!isNextDisabled) {
@@ -87,7 +92,14 @@ const ResultCandidatePicker: FunctionComponent<Props> = ({ className, onResultCl
87
92
  {!resultCandidate && <div className={styles.word}> </div>}
88
93
 
89
94
  <div className={styles.iconContainer}>
90
- <ChevronDown className={styles.icon} />
95
+ {showFloatingSolveButton && <ChevronDown className={styles.icon} />}
96
+
97
+ {!showFloatingSolveButton && (
98
+ <>
99
+ {isLoading && <Spinner className={styles.loading} />}
100
+ {!isLoading && <ChevronDown className={styles.icon} />}
101
+ </>
102
+ )}
91
103
  </div>
92
104
  </button>
93
105
 
@@ -1,4 +1,4 @@
1
1
  export { default as EmptyState } from './EmptyState';
2
+ export { default as FloatingSolveButton } from './FloatingSolveButton';
2
3
  export { default as InsertButton } from './InsertButton';
3
4
  export { default as ResultCandidatePicker } from './ResultCandidatePicker';
4
- export { default as SolveButton } from './SolveButton';
@@ -0,0 +1,11 @@
1
+ @import 'styles/animations';
2
+
3
+ .spinner {
4
+ width: var(--button--icon--size);
5
+ height: var(--button--icon--size);
6
+ border: var(--border);
7
+ border-width: 2px;
8
+ border-radius: 50%;
9
+ border-right-color: transparent !important;
10
+ animation: 750ms linear infinite rotate;
11
+ }
@@ -0,0 +1,19 @@
1
+ import classNames from 'classnames';
2
+ import { FunctionComponent } from 'react';
3
+
4
+ import { useTranslate } from 'state';
5
+
6
+ import styles from './Spinner.module.scss';
7
+
8
+ interface Props {
9
+ className?: string;
10
+ }
11
+
12
+ const Spinner: FunctionComponent<Props> = ({ className }) => {
13
+ const translate = useTranslate();
14
+ const translation = translate('common.loading');
15
+
16
+ return <div aria-label={translation} className={classNames(styles.spinner, className)} role="status" />;
17
+ };
18
+
19
+ export default Spinner;
@@ -0,0 +1 @@
1
+ export { default } from './Spinner';
@@ -11,7 +11,7 @@ import {
11
11
  useRef,
12
12
  } from 'react';
13
13
 
14
- import { useMediaQuery } from 'hooks';
14
+ import { useAppLayout } from 'hooks';
15
15
  import { getTileSizes, noop } from 'lib';
16
16
  import { EASE_OUT_CUBIC, TILE_APPEAR_DURATION, TILE_APPEAR_KEYFRAMES } from 'parameters';
17
17
  import { selectLocale, useTypedSelector } from 'state';
@@ -58,6 +58,7 @@ const Tile: FunctionComponent<Props> = ({
58
58
  onKeyDown = noop,
59
59
  }) => {
60
60
  const locale = useTypedSelector(selectLocale);
61
+ const { animateTile, showTilePoints } = useAppLayout();
61
62
  const { pointsFontSize, tileFontSize, tileSize } = getTileSizes(size);
62
63
  const style = useMemo(() => ({ height: tileSize, width: tileSize }), [tileSize]);
63
64
  const characterStyle = useMemo(() => ({ fontSize: tileFontSize }), [tileFontSize]);
@@ -65,8 +66,7 @@ const Tile: FunctionComponent<Props> = ({
65
66
  const ref = useRef<HTMLInputElement>(null);
66
67
  const mergedRef = useMergeRefs(inputRef ? [ref, inputRef] : [ref]);
67
68
  const isEmpty = !character || character === EMPTY_CELL;
68
- const isLessThanXs = useMediaQuery('<xs');
69
- const canShowPoints = (isBlank || !isEmpty) && typeof points !== 'undefined' && !isLessThanXs;
69
+ const canShowPoints = showTilePoints && (!isEmpty || isBlank) && typeof points !== 'undefined';
70
70
  const pointsFormatted = typeof points === 'number' ? points.toLocaleString(locale) : '';
71
71
 
72
72
  const handleKeyDown: KeyboardEventHandler<HTMLInputElement> = (event) => {
@@ -81,7 +81,7 @@ const Tile: FunctionComponent<Props> = ({
81
81
  }, [autoFocus, ref]);
82
82
 
83
83
  useEffect(() => {
84
- if (!ref.current?.parentElement || !character || isLessThanXs) {
84
+ if (!ref.current?.parentElement || !character || !animateTile) {
85
85
  return;
86
86
  }
87
87
 
@@ -90,7 +90,7 @@ const Tile: FunctionComponent<Props> = ({
90
90
  easing: EASE_OUT_CUBIC,
91
91
  fill: 'forwards',
92
92
  });
93
- }, [character, isLessThanXs]);
93
+ }, [character, animateTile]);
94
94
 
95
95
  return (
96
96
  <TilePure
@@ -22,6 +22,7 @@ export { default as ResultsInput } from './ResultsInput';
22
22
  export { default as SeoMessage } from './SeoMessage';
23
23
  export { default as Sizer } from './Sizer';
24
24
  export { default as Solver } from './Solver';
25
+ export { default as Spinner } from './Spinner';
25
26
  export { default as SplashScreen } from './SplashScreen';
26
27
  export { default as SvgFontCss } from './SvgFontCss';
27
28
  export { default as SvgFontFix } from './SvgFontFix';
@@ -1,3 +1,4 @@
1
+ export { default as useAppLayout } from './useAppLayout';
1
2
  export { default as useDirection } from './useDirection';
2
3
  export { default as useIsTouchDevice } from './useIsTouchDevice';
3
4
  export { default as useLanguage } from './useLanguage';