@scrabble-solver/scrabble-solver 2.10.11 → 2.11.1

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 (146) 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} +956 -930
  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 -28
  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 -11
  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 +256 -210
  43. package/.next/server/pages/index.js.nft.json +1 -1
  44. package/.next/server/pages/index.json +1 -1
  45. package/.next/static/chunks/main-0ecb9ccfcb6c9b24.js +1 -0
  46. package/.next/static/chunks/pages/404-448ba28510855455.js +1 -0
  47. package/.next/static/chunks/pages/_app-270526803bc274eb.js +28 -0
  48. package/.next/static/chunks/pages/{_error-8353112a01355ec2.js → _error-54de1933a164a1ff.js} +1 -1
  49. package/.next/static/chunks/pages/index-c6e7754ccf3532df.js +1 -0
  50. package/.next/static/css/ad39b36eab07e613.css +1 -0
  51. package/.next/static/css/e5803e581e4c0451.css +2 -0
  52. package/.next/static/esK8DG-6aS5V7QFRtR3YE/_buildManifest.js +1 -0
  53. package/.next/trace +53 -55
  54. package/package.json +12 -17
  55. package/src/components/{Solver/components/EmptyState/EmptyState.module.scss → Alert/Alert.module.scss} +11 -7
  56. package/src/components/{Solver/components/EmptyState/EmptyState.tsx → Alert/Alert.tsx} +8 -6
  57. package/src/components/Alert/index.ts +1 -0
  58. package/src/components/Board/Board.module.scss +55 -0
  59. package/src/components/Board/BoardPure.tsx +4 -0
  60. package/src/components/Board/components/Cell/Cell.module.scss +42 -0
  61. package/src/components/Board/components/Cell/Cell.tsx +12 -0
  62. package/src/components/Board/components/Cell/CellPure.tsx +12 -0
  63. package/src/components/Board/hooks/useGrid.ts +8 -24
  64. package/src/components/Dictionary/Dictionary.module.scss +17 -8
  65. package/src/components/Dictionary/Dictionary.tsx +5 -5
  66. package/src/components/DictionaryInput/DictionaryInput.module.scss +1 -0
  67. package/src/components/EmptyState/EmptyState.module.scss +2 -1
  68. package/src/components/EmptyState/EmptyState.tsx +1 -2
  69. package/src/components/Loading/Loading.module.scss +1 -1
  70. package/src/components/Loading/Loading.tsx +1 -1
  71. package/src/components/Logo/Logo.tsx +5 -0
  72. package/src/components/Modal/Modal.module.scss +2 -1
  73. package/src/components/NavButtons/NavButtons.tsx +4 -5
  74. package/src/components/PlainTiles/PlainTiles.module.scss +1 -1
  75. package/src/components/PlainTiles/Tile.tsx +3 -3
  76. package/src/components/Rack/Rack.module.scss +25 -0
  77. package/src/components/Rack/Rack.tsx +5 -4
  78. package/src/components/Rack/RackTile.tsx +6 -13
  79. package/src/components/Results/Results.module.scss +33 -2
  80. package/src/components/Results/Results.tsx +11 -11
  81. package/src/components/ResultsInput/ResultsInput.module.scss +1 -0
  82. package/src/components/Solver/Solver.module.scss +6 -4
  83. package/src/components/Solver/Solver.tsx +16 -28
  84. package/src/components/Solver/components/FloatingSolveButton/FloatingSolveButton.module.scss +7 -0
  85. package/src/components/Solver/components/{SolveButton/SolveButton.tsx → FloatingSolveButton/FloatingSolveButton.tsx} +6 -6
  86. package/src/components/Solver/components/FloatingSolveButton/index.ts +1 -0
  87. package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.module.scss +19 -4
  88. package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.tsx +13 -1
  89. package/src/components/Solver/components/index.ts +1 -2
  90. package/src/components/Spinner/Spinner.module.scss +11 -0
  91. package/src/components/Spinner/Spinner.tsx +19 -0
  92. package/src/components/Spinner/index.ts +1 -0
  93. package/src/components/Tile/Tile.module.scss +14 -2
  94. package/src/components/Tile/Tile.tsx +5 -5
  95. package/src/components/Tooltip/Tooltip.module.scss +1 -72
  96. package/src/components/Tooltip/useTooltip.tsx +25 -35
  97. package/src/components/index.ts +2 -0
  98. package/src/hooks/index.ts +1 -1
  99. package/src/hooks/useAppLayout.ts +29 -0
  100. package/src/i18n/de.json +1 -0
  101. package/src/i18n/en.json +1 -0
  102. package/src/i18n/es.json +1 -0
  103. package/src/i18n/fa.json +1 -0
  104. package/src/i18n/fr.json +1 -0
  105. package/src/i18n/index.ts +1 -1
  106. package/src/i18n/pl.json +1 -0
  107. package/src/lib/index.ts +0 -1
  108. package/src/modals/DictionaryModal/DictionaryModal.module.scss +23 -0
  109. package/src/modals/DictionaryModal/DictionaryModal.tsx +27 -0
  110. package/src/modals/DictionaryModal/index.ts +1 -0
  111. package/src/modals/KeyMapModal/KeyMapModal.tsx +5 -21
  112. package/src/modals/KeyMapModal/components/Mapping/Mapping.module.scss +4 -0
  113. package/src/modals/MenuModal/MenuModal.tsx +7 -1
  114. package/src/modals/RemainingTilesModal/components/Character/Character.module.scss +1 -0
  115. package/src/modals/ResultsModal/ResultsModal.module.scss +19 -0
  116. package/src/modals/ResultsModal/ResultsModal.tsx +8 -7
  117. package/src/modals/SettingsModal/components/LocaleSetting/LocaleSetting.module.scss +2 -0
  118. package/src/modals/WordsModal/WordsModal.module.scss +8 -1
  119. package/src/modals/index.ts +1 -0
  120. package/src/pages/api/solve.ts +9 -10
  121. package/src/pages/index.tsx +20 -15
  122. package/src/state/createAppStore.ts +26 -10
  123. package/src/state/localStorage.ts +0 -9
  124. package/src/state/types.ts +20 -2
  125. package/src/styles/animations.scss +10 -0
  126. package/src/styles/global.scss +1 -1
  127. package/src/styles/mixins.scss +22 -0
  128. package/src/styles/variables.scss +17 -2
  129. package/src/types/index.ts +1 -0
  130. package/.next/server/font-loader-manifest.js +0 -1
  131. package/.next/static/chunks/main-74c4d6b2b5c362f3.js +0 -1
  132. package/.next/static/chunks/pages/404-6c1a6e3251710371.js +0 -1
  133. package/.next/static/chunks/pages/_app-d98e480ff8c583de.js +0 -28
  134. package/.next/static/chunks/pages/index-bd1c7d3872c37456.js +0 -1
  135. package/.next/static/css/a9b55372a26cf77d.css +0 -1
  136. package/.next/static/css/b8954b85e2fa5b63.css +0 -2
  137. package/.next/static/msKI0ZURgJImoGBJvCBiF/_buildManifest.js +0 -1
  138. package/src/components/Solver/components/EmptyState/index.ts +0 -1
  139. package/src/components/Solver/components/SolveButton/SolveButton.module.scss +0 -4
  140. package/src/components/Solver/components/SolveButton/index.ts +0 -1
  141. package/src/components/Tooltip/constants.ts +0 -28
  142. package/src/hooks/useUniqueId.ts +0 -9
  143. package/src/lib/isCtrl.ts +0 -7
  144. package/src/state/rootReducer.ts +0 -25
  145. /package/.next/server/{font-loader-manifest.json → next-font-manifest.json} +0 -0
  146. /package/.next/static/{msKI0ZURgJImoGBJvCBiF → esK8DG-6aS5V7QFRtR3YE}/_ssgManifest.js +0 -0
@@ -3,6 +3,7 @@
3
3
  .rack {
4
4
  display: flex;
5
5
  box-shadow: var(--box-shadow);
6
+ border-radius: var(--border--radius);
6
7
  }
7
8
 
8
9
  .tile {
@@ -15,3 +16,27 @@
15
16
  z-index: 2;
16
17
  }
17
18
  }
19
+
20
+ .sharpLeft {
21
+ [dir='ltr'] & {
22
+ border-top-left-radius: 0;
23
+ border-bottom-left-radius: 0;
24
+ }
25
+
26
+ [dir='rtl'] & {
27
+ border-top-right-radius: 0;
28
+ border-bottom-right-radius: 0;
29
+ }
30
+ }
31
+
32
+ .sharpRight {
33
+ [dir='ltr'] & {
34
+ border-top-right-radius: 0;
35
+ border-bottom-right-radius: 0;
36
+ }
37
+
38
+ [dir='rtl'] & {
39
+ border-top-left-radius: 0;
40
+ border-bottom-left-radius: 0;
41
+ }
42
+ }
@@ -8,7 +8,6 @@ import {
8
8
  createKeyboardNavigation,
9
9
  extractCharacters,
10
10
  extractInputValue,
11
- isCtrl,
12
11
  zipCharactersAndTiles,
13
12
  } from 'lib';
14
13
  import { rackSlice, selectConfig, selectLocale, selectRack, selectResultCandidateTiles, useTypedSelector } from 'state';
@@ -99,9 +98,7 @@ const Rack: FunctionComponent<Props> = ({ className, tileSize }) => {
99
98
  changeActiveIndex(1);
100
99
  },
101
100
  onKeyDown: (event) => {
102
- if (isCtrl(event) && config.isTwoCharacterTilePrefix(event.key)) {
103
- changeActiveIndex(1);
104
- } else if (event.currentTarget.value === event.key) {
101
+ if (event.currentTarget.value === event.key) {
105
102
  // change event did not fire because the same character was typed over the current one
106
103
  // but we still want to move the caret
107
104
  event.preventDefault();
@@ -118,6 +115,10 @@ const Rack: FunctionComponent<Props> = ({ className, tileSize }) => {
118
115
  <RackTile
119
116
  activeIndexRef={activeIndexRef}
120
117
  character={character}
118
+ className={classNames({
119
+ [styles.sharpLeft]: index !== 0,
120
+ [styles.sharpRight]: index !== tiles.length - 1,
121
+ })}
121
122
  index={index}
122
123
  inputRef={tilesRefs[index]}
123
124
  key={index}
@@ -1,5 +1,6 @@
1
1
  import { BLANK } from '@scrabble-solver/constants';
2
2
  import { Tile as TileModel } from '@scrabble-solver/types';
3
+ import classNames from 'classnames';
3
4
  import {
4
5
  ChangeEvent,
5
6
  ChangeEventHandler,
@@ -12,7 +13,7 @@ import {
12
13
  } from 'react';
13
14
  import { useDispatch } from 'react-redux';
14
15
 
15
- import { createKeyboardNavigation, extractCharacters, extractInputValue, isCtrl } from 'lib';
16
+ import { createKeyboardNavigation, extractCharacters, extractInputValue } from 'lib';
16
17
  import {
17
18
  rackSlice,
18
19
  selectCharacterIsValid,
@@ -30,6 +31,7 @@ import styles from './Rack.module.scss';
30
31
  interface Props {
31
32
  activeIndexRef: MutableRefObject<number | undefined>;
32
33
  character: string | null;
34
+ className?: string;
33
35
  index: number;
34
36
  inputRef: RefObject<HTMLInputElement>;
35
37
  size: number;
@@ -41,6 +43,7 @@ interface Props {
41
43
  const RackTile: FunctionComponent<Props> = ({
42
44
  activeIndexRef,
43
45
  character,
46
+ className,
44
47
  index,
45
48
  inputRef,
46
49
  size,
@@ -77,17 +80,7 @@ const RackTile: FunctionComponent<Props> = ({
77
80
  event.preventDefault();
78
81
  dispatch(rackSlice.actions.changeCharacter({ character: null, index }));
79
82
  },
80
- onKeyDown: (event) => {
81
- if (isCtrl(event) && config.isTwoCharacterTilePrefix(event.key)) {
82
- event.preventDefault();
83
- event.stopPropagation();
84
- const twoTilesCharacter = config.getTwoCharacterTileByPrefix(event.key);
85
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
86
- dispatch(rackSlice.actions.changeCharacter({ character: twoTilesCharacter!, index }));
87
- }
88
-
89
- onKeyDown(event);
90
- },
83
+ onKeyDown,
91
84
  });
92
85
  }, [index, onKeyDown]);
93
86
 
@@ -97,7 +90,7 @@ const RackTile: FunctionComponent<Props> = ({
97
90
  index: (index + 1).toLocaleString(locale),
98
91
  })}
99
92
  autoFocus={index === 0}
100
- className={styles.tile}
93
+ className={classNames(styles.tile, className)}
101
94
  character={character === null ? undefined : character}
102
95
  highlighted={tile !== null}
103
96
  inputRef={inputRef}
@@ -9,6 +9,7 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
9
9
  height: 100%;
10
10
  background: var(--color--background--element);
11
11
  border: var(--border);
12
+ border-radius: var(--border--radius);
12
13
  box-shadow: var(--box-shadow);
13
14
  font-family: var(--font--family--title);
14
15
  }
@@ -21,8 +22,6 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
21
22
  flex: 1;
22
23
  position: relative;
23
24
  height: 100%;
24
- border-top: var(--border);
25
- border-bottom: var(--border);
26
25
  }
27
26
 
28
27
  .listContainer {
@@ -31,6 +30,9 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
31
30
  }
32
31
 
33
32
  .list {
33
+ @include scrollbars;
34
+
35
+ scrollbar-gutter: stable;
34
36
  transition: var(--transition);
35
37
 
36
38
  &.outdated {
@@ -45,6 +47,9 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
45
47
  align-items: center;
46
48
  justify-content: space-between;
47
49
  font-weight: 700;
50
+ border-bottom: var(--border);
51
+ border-top-left-radius: inherit;
52
+ border-top-right-radius: inherit;
48
53
  }
49
54
 
50
55
  .headerButton {
@@ -61,6 +66,26 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
61
66
  background-color: var(--color--primary);
62
67
  color: var(--color--primary--opposite);
63
68
  }
69
+
70
+ &:first-child {
71
+ [dir='ltr'] & {
72
+ border-top-left-radius: inherit;
73
+ }
74
+
75
+ [dir='rtl'] & {
76
+ border-top-right-radius: inherit;
77
+ }
78
+ }
79
+
80
+ &:last-child {
81
+ [dir='ltr'] & {
82
+ border-top-right-radius: inherit;
83
+ }
84
+
85
+ [dir='rtl'] & {
86
+ border-top-left-radius: inherit;
87
+ }
88
+ }
64
89
  }
65
90
 
66
91
  .headerButtonLabel {
@@ -173,3 +198,9 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
173
198
  width: $size;
174
199
  height: $size;
175
200
  }
201
+
202
+ .input {
203
+ border-top: var(--border);
204
+ border-bottom-left-radius: var(--border--radius);
205
+ border-bottom-right-radius: var(--border--radius);
206
+ }
@@ -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>
@@ -8,4 +8,5 @@
8
8
  @include text-input;
9
9
 
10
10
  border: none;
11
+ border-radius: inherit;
11
12
  }
@@ -70,16 +70,21 @@
70
70
  flex-direction: column;
71
71
  background-color: var(--color--background--element);
72
72
  border: var(--border);
73
+ border-radius: var(--border--radius);
73
74
  box-shadow: var(--box-shadow);
74
75
  }
75
76
 
76
77
  .dictionary {
77
78
  flex: 1;
78
79
  border-bottom: var(--border);
80
+ border-top-left-radius: var(--border--radius);
81
+ border-top-right-radius: var(--border--radius);
79
82
  }
80
83
 
81
84
  .dictionaryInput {
82
85
  flex: 0 0 auto;
86
+ border-bottom-left-radius: var(--border--radius);
87
+ border-bottom-right-radius: var(--border--radius);
83
88
  }
84
89
 
85
90
  .bottomContainer {
@@ -97,10 +102,6 @@
97
102
  min-width: 0;
98
103
  }
99
104
 
100
- .rack {
101
- border: var(--border);
102
- }
103
-
104
105
  .controls {
105
106
  width: 100%;
106
107
  }
@@ -114,6 +115,7 @@
114
115
 
115
116
  position: fixed;
116
117
  bottom: var(--spacing);
118
+ z-index: 1;
117
119
 
118
120
  @include media('<xs') {
119
121
  --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,
@@ -26,13 +19,14 @@ import {
26
19
  useTypedSelector,
27
20
  } from 'state';
28
21
 
22
+ import Alert from '../Alert';
29
23
  import Board from '../Board';
30
24
  import Dictionary from '../Dictionary';
31
25
  import DictionaryInput from '../DictionaryInput';
32
26
  import Rack from '../Rack';
33
27
  import Results from '../Results';
34
28
 
35
- import { EmptyState, ResultCandidatePicker, SolveButton } from './components';
29
+ import { FloatingSolveButton, ResultCandidatePicker } from './components';
36
30
  import styles from './Solver.module.scss';
37
31
 
38
32
  interface Props {
@@ -47,14 +41,12 @@ const Solver: FunctionComponent<Props> = ({ className, height, width, onShowResu
47
41
  const dispatch = useDispatch();
48
42
  const translate = useTranslate();
49
43
  const isTouchDevice = useIsTouchDevice();
44
+ const { componentsSpacing, isBoardFullWidth, showColumn, showCompactControls, showFloatingSolveButton } =
45
+ useAppLayout();
50
46
  const [bottomContainerRef, { height: bottomContainerHeight }] = useMeasure<HTMLDivElement>();
51
47
  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);
48
+ const maxBoardWidth = width - columnWidth - (showColumn ? componentsSpacing : 0) - 2 * componentsSpacing;
49
+ const maxBoardHeight = isBoardFullWidth ? Number.POSITIVE_INFINITY : Math.max(height - bottomContainerHeight, 0);
58
50
  const config = useTypedSelector(selectConfig);
59
51
  const error = useTypedSelector(selectSolveError);
60
52
  const isOutdated = useTypedSelector(selectAreResultsOutdated);
@@ -107,18 +99,14 @@ const Solver: FunctionComponent<Props> = ({ className, height, width, onShowResu
107
99
  const handleSubmit = (event: SyntheticEvent) => {
108
100
  event.preventDefault();
109
101
 
110
- if (isLessThanL) {
111
- onShowResults();
112
- }
113
-
114
102
  dispatch(solveSlice.actions.submit());
115
103
  };
116
104
 
117
105
  useEffect(() => {
118
- if (isLessThanL && bestResult && !isOutdated) {
106
+ if (showCompactControls && bestResult && !isOutdated) {
119
107
  dispatch(resultsSlice.actions.changeResultCandidate(bestResult));
120
108
  }
121
- }, [bestResult, dispatch, isLessThanL, isOutdated]);
109
+ }, [bestResult, dispatch, showCompactControls, isOutdated]);
122
110
 
123
111
  return (
124
112
  <div className={classNames(styles.solver, className)}>
@@ -147,27 +135,27 @@ const Solver: FunctionComponent<Props> = ({ className, height, width, onShowResu
147
135
  <input className={styles.submitInput} tabIndex={-1} type="submit" />
148
136
  </form>
149
137
 
150
- {isLessThanL && (
138
+ {showCompactControls && (
151
139
  <div className={styles.controls} style={{ maxWidth: maxControlsWidth }}>
152
140
  <ResultCandidatePicker onResultClick={onShowResults} />
153
141
 
154
142
  {error && (
155
- <EmptyState className={styles.emptyState} variant="error">
143
+ <Alert className={styles.emptyState} variant="error">
156
144
  {error.message}
157
- </EmptyState>
145
+ </Alert>
158
146
  )}
159
147
 
160
148
  {allResults && allResults.length === 0 && !isOutdated && (
161
- <EmptyState className={styles.emptyState} variant="warning">
149
+ <Alert className={styles.emptyState} variant="warning">
162
150
  {translate('results.empty-state.no-results')}
163
- </EmptyState>
151
+ </Alert>
164
152
  )}
165
153
  </div>
166
154
  )}
167
155
  </div>
168
156
  </div>
169
157
 
170
- {isTouchDevice && <SolveButton className={styles.solve} onClick={handleSubmit} />}
158
+ {showFloatingSolveButton && <FloatingSolveButton className={styles.solve} onClick={handleSubmit} />}
171
159
  </div>
172
160
  );
173
161
  };
@@ -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,3 @@
1
- export { default as EmptyState } from './EmptyState';
1
+ export { default as FloatingSolveButton } from './FloatingSolveButton';
2
2
  export { default as InsertButton } from './InsertButton';
3
3
  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';
@@ -5,6 +5,7 @@
5
5
 
6
6
  position: relative;
7
7
  transition: var(--transition);
8
+ border-radius: var(--border--radius);
8
9
 
9
10
  &.points1 {
10
11
  --background-color: var(--color--yellow);
@@ -85,6 +86,7 @@
85
86
  align-items: center;
86
87
  justify-content: center;
87
88
  background-color: var(--background-color);
89
+ border-radius: inherit;
88
90
  transition: var(--transition);
89
91
  pointer-events: none;
90
92
  user-select: none;
@@ -94,10 +96,18 @@
94
96
  }
95
97
 
96
98
  .raised & {
97
- box-shadow: inset -2px -2px 2px -1px rgba(34, 34, 34, 0.8);
99
+ --shadow--size: 2px;
100
+ --shadow--blur: 2px;
101
+ --shadow--spread: -1px;
102
+ --shadow--color: rgba(34, 34, 34, 0.8);
103
+
104
+ box-shadow: inset calc(-1 * var(--shadow--size)) calc(-1 * var(--shadow--size)) var(--shadow--blur)
105
+ var(--shadow--spread) var(--shadow--color);
98
106
 
99
107
  @include media('<xs') {
100
- box-shadow: inset -2px -2px 1px -1px rgba(34, 34, 34, 0.8);
108
+ --shadow--size: 1px;
109
+ --shadow--spread: 0;
110
+ --shadow--blur: 1px;
101
111
  }
102
112
  }
103
113
  }
@@ -146,10 +156,12 @@
146
156
  [dir='ltr'] & {
147
157
  top: 0;
148
158
  right: 0;
159
+ border-top-right-radius: inherit;
149
160
  }
150
161
 
151
162
  [dir='rtl'] & {
152
163
  bottom: 0;
153
164
  right: 0;
165
+ border-bottom-right-radius: inherit;
154
166
  }
155
167
  }