@scrabble-solver/scrabble-solver 2.10.3 → 2.10.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 (194) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +13 -13
  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/routes-manifest.json +1 -1
  15. package/.next/server/chunks/176.js +1439 -1658
  16. package/.next/server/chunks/579.js +50 -26
  17. package/.next/server/middleware-build-manifest.js +1 -1
  18. package/.next/server/pages/404.html +2 -2
  19. package/.next/server/pages/404.js.nft.json +1 -1
  20. package/.next/server/pages/500.html +2 -2
  21. package/.next/server/pages/_app.js.nft.json +1 -1
  22. package/.next/server/pages/api/solve.js +22 -6
  23. package/.next/server/pages/index.html +2 -2
  24. package/.next/server/pages/index.js +1509 -52
  25. package/.next/server/pages/index.js.nft.json +1 -1
  26. package/.next/server/pages/index.json +1 -1
  27. package/.next/server/pages-manifest.json +2 -2
  28. package/.next/static/WILX-RgqlLTd4ZoPs8C_E/_buildManifest.js +1 -0
  29. package/.next/static/{FJkPF91uL-OCAJKTTpVSP → WILX-RgqlLTd4ZoPs8C_E}/_ssgManifest.js +0 -0
  30. package/.next/static/chunks/144-6768fe9a92865ec8.js +1 -0
  31. package/.next/static/chunks/490-d29992f1c264d70e.js +5 -0
  32. package/.next/static/chunks/pages/{404-24f9617eeb8d6dc1.js → 404-7619583a9e7188b1.js} +1 -1
  33. package/.next/static/chunks/pages/_app-fa0661b072fc6af9.js +24 -0
  34. package/.next/static/chunks/pages/index-1a6bbb880f28606a.js +1 -0
  35. package/.next/static/css/78e42ad01f580f64.css +1 -0
  36. package/.next/static/css/d80ffccf2315791a.css +1 -0
  37. package/.next/static/css/e737d5d7fbed2434.css +1 -0
  38. package/.next/trace +55 -52
  39. package/package.json +11 -9
  40. package/src/components/Badge/Badge.module.scss +4 -5
  41. package/src/components/Board/BoardPure.tsx +5 -5
  42. package/src/components/Board/components/Cell/Cell.module.scss +11 -11
  43. package/src/components/Button/Button.module.scss +34 -40
  44. package/src/components/Button/Button.tsx +28 -8
  45. package/src/components/Button/Link.tsx +44 -0
  46. package/src/components/Dictionary/Dictionary.module.scss +1 -2
  47. package/src/components/Dictionary/Dictionary.tsx +2 -6
  48. package/src/components/DictionaryInput/DictionaryInput.module.scss +1 -3
  49. package/src/components/EmptyState/EmptyState.module.scss +6 -7
  50. package/src/components/EmptyState/EmptyState.tsx +6 -6
  51. package/src/components/Key/Key.module.scss +0 -1
  52. package/src/components/{Splash/Splash.module.scss → LogoSplashScreen/LogoSplashScreen.module.scss} +1 -1
  53. package/src/components/{Splash/Splash.tsx → LogoSplashScreen/LogoSplashScreen.tsx} +7 -7
  54. package/src/components/LogoSplashScreen/index.ts +1 -0
  55. package/src/components/{Sidebar/Sidebar.module.scss → Modal/Modal.module.scss} +25 -20
  56. package/src/components/{Sidebar/Sidebar.tsx → Modal/Modal.tsx} +9 -9
  57. package/src/components/{Sidebar → Modal}/components/Section/Section.module.scss +0 -0
  58. package/src/components/{Sidebar → Modal}/components/Section/Section.tsx +0 -0
  59. package/src/components/{Sidebar → Modal}/components/Section/index.ts +0 -0
  60. package/src/components/{Sidebar → Modal}/components/index.ts +0 -0
  61. package/src/components/Modal/index.ts +1 -0
  62. package/src/components/NavButtons/NavButtons.tsx +22 -3
  63. package/src/components/Progress/Progress.module.scss +9 -0
  64. package/src/components/Progress/Progress.tsx +38 -0
  65. package/src/components/Progress/index.ts +1 -0
  66. package/src/components/Rack/Rack.module.scss +2 -0
  67. package/src/components/Rack/Rack.tsx +3 -1
  68. package/src/components/Rack/RackTile.tsx +3 -2
  69. package/src/components/Radio/Radio.module.scss +2 -2
  70. package/src/components/ResultCandidatePicker/ResultCandidatePicker.module.scss +76 -0
  71. package/src/components/ResultCandidatePicker/ResultCandidatePicker.tsx +38 -0
  72. package/src/components/ResultCandidatePicker/index.ts +1 -0
  73. package/src/components/Results/Result.tsx +64 -43
  74. package/src/components/Results/Results.module.scss +9 -16
  75. package/src/components/Results/Results.tsx +45 -28
  76. package/src/components/Results/SolveButton.tsx +10 -7
  77. package/src/components/Results/{getColumns.ts → getLocaleColumns.ts} +2 -2
  78. package/src/components/Results/types.ts +16 -0
  79. package/src/components/Results/useColumns.ts +44 -0
  80. package/src/components/ResultsInput/ResultsInput.module.scss +1 -3
  81. package/src/components/Sizer/Sizer.module.scss +10 -0
  82. package/src/components/Sizer/Sizer.tsx +10 -0
  83. package/src/components/Sizer/index.ts +1 -0
  84. package/src/components/Solver/Solver.module.scss +83 -27
  85. package/src/components/Solver/Solver.tsx +158 -25
  86. package/src/components/Solver/components/ApplyButton/ApplyButton.module.scss +5 -0
  87. package/src/components/Solver/components/ApplyButton/ApplyButton.tsx +38 -0
  88. package/src/components/Solver/components/ApplyButton/index.ts +1 -0
  89. package/src/components/Solver/components/EmptyState/EmptyState.module.scss +59 -0
  90. package/src/components/Solver/components/EmptyState/EmptyState.tsx +41 -0
  91. package/src/components/Solver/components/EmptyState/index.ts +1 -0
  92. package/src/components/Solver/components/SolveButton/SolveButton.module.scss +4 -0
  93. package/src/components/Solver/components/SolveButton/SolveButton.tsx +43 -0
  94. package/src/components/Solver/components/SolveButton/index.ts +1 -0
  95. package/src/components/Solver/components/index.ts +3 -0
  96. package/src/components/{Screen/Screen.module.scss → SplashScreen/SplashScreen.module.scss} +2 -12
  97. package/src/components/{Screen/Screen.tsx → SplashScreen/SplashScreen.tsx} +4 -4
  98. package/src/components/SplashScreen/index.ts +1 -0
  99. package/src/components/SquareButton/SquareButton.module.scss +3 -3
  100. package/src/components/SquareButton/SquareButton.tsx +2 -2
  101. package/src/components/Tile/Tile.module.scss +39 -30
  102. package/src/components/Tile/Tile.tsx +2 -2
  103. package/src/components/Tile/TilePure.tsx +9 -5
  104. package/src/components/Tooltip/Tooltip.module.scss +5 -6
  105. package/src/components/index.ts +6 -7
  106. package/src/hooks/index.ts +1 -1
  107. package/src/hooks/useMediaQuery.ts +11 -0
  108. package/src/hooks/usePortal.tsx +1 -1
  109. package/src/i18n/de.json +2 -0
  110. package/src/i18n/en.json +2 -0
  111. package/src/i18n/es.json +2 -0
  112. package/src/i18n/fa.json +2 -0
  113. package/src/i18n/fr.json +2 -0
  114. package/src/i18n/pl.json +2 -0
  115. package/src/icons/CardChecklist.svg +5 -0
  116. package/src/icons/Check.svg +2 -2
  117. package/src/icons/ChevronDown.svg +4 -0
  118. package/src/icons/CrossCircleFill.svg +4 -0
  119. package/src/icons/{CrossFill.svg → CrossSquareFill.svg} +0 -0
  120. package/src/icons/ExclamationTriangleFill.svg +4 -0
  121. package/src/icons/InfoCircleFill.svg +4 -0
  122. package/src/icons/List.svg +4 -0
  123. package/src/icons/Search.svg +4 -0
  124. package/src/icons/index.ts +8 -2
  125. package/src/{components/KeyMap/KeyMap.tsx → modals/KeyMapModal/KeyMapModal.tsx} +11 -13
  126. package/src/{components/KeyMap → modals/KeyMapModal}/components/Mapping/Mapping.module.scss +0 -0
  127. package/src/{components/KeyMap → modals/KeyMapModal}/components/Mapping/Mapping.tsx +0 -0
  128. package/src/{components/KeyMap → modals/KeyMapModal}/components/Mapping/index.ts +0 -0
  129. package/src/{components/KeyMap → modals/KeyMapModal}/components/index.ts +0 -0
  130. package/src/modals/KeyMapModal/index.ts +1 -0
  131. package/src/{components/KeyMap → modals/KeyMapModal}/keys.tsx +1 -2
  132. package/src/modals/MenuModal/MenuModal.module.scss +11 -0
  133. package/src/modals/MenuModal/MenuModal.tsx +56 -0
  134. package/src/modals/MenuModal/index.ts +1 -0
  135. package/src/modals/RemainingTilesModal/RemainingTilesModal.module.scss +28 -0
  136. package/src/{components/RemainingTiles/RemainingTiles.tsx → modals/RemainingTilesModal/RemainingTilesModal.tsx} +14 -12
  137. package/src/{components/RemainingTiles → modals/RemainingTilesModal/components/Character}/Character.module.scss +7 -3
  138. package/src/{components/RemainingTiles → modals/RemainingTilesModal/components/Character}/Character.tsx +11 -3
  139. package/src/modals/RemainingTilesModal/components/Character/index.ts +1 -0
  140. package/src/modals/RemainingTilesModal/components/index.ts +1 -0
  141. package/src/modals/RemainingTilesModal/index.ts +1 -0
  142. package/src/modals/ResultsModal/ResultsModal.module.scss +7 -0
  143. package/src/modals/ResultsModal/ResultsModal.tsx +58 -0
  144. package/src/modals/ResultsModal/index.ts +1 -0
  145. package/src/modals/SettingsModal/SettingsModal.tsx +34 -0
  146. package/src/{components/Settings → modals/SettingsModal}/components/AutoGroupTilesSetting/AutoGroupTilesSetting.module.scss +0 -0
  147. package/src/{components/Settings → modals/SettingsModal}/components/AutoGroupTilesSetting/AutoGroupTilesSetting.tsx +1 -2
  148. package/src/{components/Settings → modals/SettingsModal}/components/AutoGroupTilesSetting/constants.ts +0 -0
  149. package/src/{components/Settings → modals/SettingsModal}/components/AutoGroupTilesSetting/index.ts +0 -0
  150. package/src/{components/Settings → modals/SettingsModal}/components/AutoGroupTilesSetting/lib.ts +0 -0
  151. package/src/{components/Settings → modals/SettingsModal}/components/ConfigSetting/ConfigSetting.module.scss +0 -0
  152. package/src/{components/Settings → modals/SettingsModal}/components/ConfigSetting/ConfigSetting.tsx +1 -2
  153. package/src/{components/Settings → modals/SettingsModal}/components/ConfigSetting/index.ts +0 -0
  154. package/src/{components/Settings → modals/SettingsModal}/components/ConfigSetting/options.ts +0 -0
  155. package/src/{components/Settings → modals/SettingsModal}/components/ConfigSetting/types.ts +0 -0
  156. package/src/{components/Settings → modals/SettingsModal}/components/LocaleSetting/LocaleSetting.module.scss +0 -0
  157. package/src/{components/Settings → modals/SettingsModal}/components/LocaleSetting/LocaleSetting.tsx +1 -2
  158. package/src/{components/Settings → modals/SettingsModal}/components/LocaleSetting/index.ts +0 -0
  159. package/src/{components/Settings → modals/SettingsModal}/components/LocaleSetting/options.ts +0 -0
  160. package/src/{components/Settings → modals/SettingsModal}/components/LocaleSetting/types.ts +0 -0
  161. package/src/{components/Settings → modals/SettingsModal}/components/index.ts +0 -0
  162. package/src/modals/SettingsModal/index.ts +1 -0
  163. package/src/{components/Words/Words.module.scss → modals/WordsModal/WordsModal.module.scss} +7 -1
  164. package/src/{components/Words/Words.tsx → modals/WordsModal/WordsModal.tsx} +10 -12
  165. package/src/modals/WordsModal/index.ts +1 -0
  166. package/src/modals/index.ts +6 -0
  167. package/src/pages/index.module.scss +6 -4
  168. package/src/pages/index.tsx +63 -26
  169. package/src/parameters/index.ts +28 -6
  170. package/src/state/createAppStore.ts +7 -4
  171. package/src/styles/mixins.scss +17 -15
  172. package/src/styles/variables.scss +15 -17
  173. package/src/types/index.ts +2 -0
  174. package/tsconfig.json +1 -0
  175. package/.next/cache/webpack/client-development/7.pack_ +0 -0
  176. package/.next/static/FJkPF91uL-OCAJKTTpVSP/_buildManifest.js +0 -1
  177. package/.next/static/chunks/361-d16f336a9752a55a.js +0 -1
  178. package/.next/static/chunks/724-eb48df4d1ba3df8b.js +0 -5
  179. package/.next/static/chunks/pages/_app-959e495f0f221247.js +0 -24
  180. package/.next/static/chunks/pages/index-1e30dafa41bddb80.js +0 -1
  181. package/.next/static/css/aafd07997120f1e4.css +0 -1
  182. package/.next/static/css/cb5b206454513f3c.css +0 -1
  183. package/.next/static/css/eb9d57f7103525ab.css +0 -1
  184. package/src/components/KeyMap/index.ts +0 -1
  185. package/src/components/RemainingTiles/RemainingTiles.module.scss +0 -16
  186. package/src/components/RemainingTiles/index.ts +0 -1
  187. package/src/components/Screen/index.ts +0 -1
  188. package/src/components/Settings/Settings.tsx +0 -35
  189. package/src/components/Settings/index.ts +0 -1
  190. package/src/components/Sidebar/index.ts +0 -1
  191. package/src/components/Splash/index.ts +0 -1
  192. package/src/components/Words/index.ts +0 -1
  193. package/src/hooks/useIsTablet.ts +0 -10
  194. package/src/icons/Play.svg +0 -4
@@ -1,56 +1,143 @@
1
+ import { Result } from '@scrabble-solver/types';
1
2
  import classNames from 'classnames';
2
- import { FormEvent, FunctionComponent } from 'react';
3
+ import { FunctionComponent, SyntheticEvent, useEffect, useMemo } from 'react';
3
4
  import { useDispatch } from 'react-redux';
4
5
  import { useMeasure } from 'react-use';
5
6
 
6
- import { useIsTablet } from 'hooks';
7
- import { getCellSize } from 'lib';
8
- import { COMPONENTS_SPACING, COMPONENTS_SPACING_MOBILE, DICTIONARY_HEIGHT } from 'parameters';
9
- import { selectConfig, solveSlice, useTypedSelector } from 'state';
7
+ import { useIsTouchDevice, useMediaQuery } from 'hooks';
8
+ import {
9
+ BOARD_TILE_SIZE_MAX,
10
+ BOARD_TILE_SIZE_MIN,
11
+ BORDER_WIDTH,
12
+ COLUMN_MIN_HEIGHT,
13
+ COMPONENTS_SPACING,
14
+ COMPONENTS_SPACING_SMALL,
15
+ DICTIONARY_HEIGHT,
16
+ RACK_TILE_SIZE_MAX,
17
+ } from 'parameters';
18
+ import {
19
+ resultsSlice,
20
+ selectAreResultsOutdated,
21
+ selectConfig,
22
+ selectResultCandidate,
23
+ selectSolveError,
24
+ selectSortedFilteredResults,
25
+ selectSortedResults,
26
+ solveSlice,
27
+ useTranslate,
28
+ useTypedSelector,
29
+ } from 'state';
10
30
 
11
31
  import Board from '../Board';
12
32
  import Dictionary from '../Dictionary';
13
33
  import DictionaryInput from '../DictionaryInput';
14
34
  import Rack from '../Rack';
35
+ import ResultCandidatePicker from '../ResultCandidatePicker';
15
36
  import Results from '../Results';
16
37
  import Well from '../Well';
17
38
 
39
+ import { ApplyButton, EmptyState, SolveButton } from './components';
18
40
  import styles from './Solver.module.scss';
19
41
 
20
42
  interface Props {
21
43
  className?: string;
44
+ height: number;
45
+ width: number;
46
+ onShowResults: () => void;
22
47
  }
23
48
 
24
- const Solver: FunctionComponent<Props> = ({ className }) => {
49
+ // eslint-disable-next-line max-statements
50
+ const Solver: FunctionComponent<Props> = ({ className, height, width, onShowResults }) => {
25
51
  const dispatch = useDispatch();
26
- const isTablet = useIsTablet();
27
- const [boardRef, { height: boardHeight }] = useMeasure<HTMLDivElement>();
28
- const [contentRef, { height: contentHeight, width: contentWidth }] = useMeasure<HTMLDivElement>();
29
- const [resultsContainerRef, { height: resultsContainerHeight, width: resultsContainerWidth }] =
30
- useMeasure<HTMLDivElement>();
52
+ const translate = useTranslate();
53
+ const isTouchDevice = useIsTouchDevice();
54
+ const [bottomContainerRef, { height: bottomContainerHeight }] = useMeasure<HTMLDivElement>();
55
+ const [resultsContainerRef, { width: resultsContainerWidth }] = useMeasure<HTMLDivElement>();
56
+ const isLessThanXl = useMediaQuery('<xl');
57
+ const isLessThanL = useMediaQuery('<l');
58
+ const componentsSpacing = isLessThanXl ? COMPONENTS_SPACING_SMALL : COMPONENTS_SPACING;
59
+ const maxBoardWidth = width - resultsContainerWidth - (isLessThanL ? 0 : componentsSpacing) - 2 * componentsSpacing;
60
+ const maxBoardHeight = Math.max(height - bottomContainerHeight, isLessThanL ? 0 : COLUMN_MIN_HEIGHT);
31
61
  const config = useTypedSelector(selectConfig);
32
- const cellSize = getCellSize(config, contentWidth - resultsContainerWidth, contentHeight);
33
- const componentsSpacing = isTablet ? COMPONENTS_SPACING_MOBILE : COMPONENTS_SPACING;
34
- const resultsHeight = boardHeight - DICTIONARY_HEIGHT - componentsSpacing;
62
+ const resultCandidate = useTypedSelector(selectResultCandidate);
63
+ const isOutdated = useTypedSelector(selectAreResultsOutdated);
64
+ const allResults = useTypedSelector(selectSortedResults);
65
+ const error = useTypedSelector(selectSolveError);
66
+ const results = useTypedSelector(selectSortedFilteredResults);
67
+ const [bestResult] = results || [];
68
+ const cellWidth = (maxBoardWidth - (config.boardWidth + 1) * BORDER_WIDTH) / config.boardWidth;
69
+ const cellHeight = (maxBoardHeight - (config.boardHeight + 1) * BORDER_WIDTH) / config.boardHeight;
70
+ const cellSize = Math.min(cellWidth, cellHeight);
71
+ const cellSizeSafe = Math.min(Math.max(cellSize, BOARD_TILE_SIZE_MIN), BOARD_TILE_SIZE_MAX);
72
+ const tileSize = Math.min((maxBoardWidth - 2 * BORDER_WIDTH) / config.maximumCharactersCount, RACK_TILE_SIZE_MAX);
73
+ const boardSize = (cellSizeSafe + BORDER_WIDTH) * Math.max(config.boardWidth, config.boardHeight) + BORDER_WIDTH;
74
+ const resultsHeight = boardSize - DICTIONARY_HEIGHT - componentsSpacing - 4 * BORDER_WIDTH;
75
+ const maxControlsWidth = tileSize * config.maximumCharactersCount + 2 * BORDER_WIDTH;
76
+ const touchCallbacks = useMemo(
77
+ () => ({
78
+ onClick: (result: Result) => {
79
+ const isSelected = result === resultCandidate;
35
80
 
36
- const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
81
+ if (isSelected) {
82
+ dispatch(resultsSlice.actions.applyResult(result));
83
+ } else {
84
+ dispatch(resultsSlice.actions.changeResultCandidate(result));
85
+ }
86
+ },
87
+ }),
88
+ [dispatch, resultCandidate],
89
+ );
90
+ const mouseCallbacks = useMemo(
91
+ () => ({
92
+ onBlur: () => {
93
+ dispatch(resultsSlice.actions.changeResultCandidate(null));
94
+ },
95
+ onClick: (result: Result) => {
96
+ dispatch(resultsSlice.actions.applyResult(result));
97
+ },
98
+ onFocus: (result: Result) => {
99
+ dispatch(resultsSlice.actions.changeResultCandidate(result));
100
+ },
101
+ onMouseEnter: (result: Result) => {
102
+ dispatch(resultsSlice.actions.changeResultCandidate(result));
103
+ },
104
+ onMouseLeave: () => {
105
+ dispatch(resultsSlice.actions.changeResultCandidate(null));
106
+ },
107
+ }),
108
+ [dispatch],
109
+ );
110
+ const callbacks = isTouchDevice ? touchCallbacks : mouseCallbacks;
111
+
112
+ const handleSubmit = (event: SyntheticEvent) => {
37
113
  event.preventDefault();
114
+
115
+ if (isLessThanL) {
116
+ onShowResults();
117
+ }
118
+
38
119
  dispatch(solveSlice.actions.submit());
39
120
  };
40
121
 
122
+ useEffect(() => {
123
+ if (bestResult) {
124
+ dispatch(resultsSlice.actions.changeResultCandidate(bestResult));
125
+ }
126
+ }, [bestResult, dispatch]);
127
+
41
128
  return (
42
129
  <div className={classNames(styles.solver, className)}>
43
- <div className={styles.contentWrapper}>
44
- <div className={styles.content} ref={contentRef}>
130
+ <div className={styles.container}>
131
+ <div className={styles.content}>
45
132
  <form className={styles.boardContainer} onSubmit={handleSubmit}>
46
- {contentWidth > 0 && <Board cellSize={cellSize} innerRef={boardRef} />}
133
+ <Board cellSize={cellSizeSafe} className={styles.board} />
47
134
  <input className={styles.submitInput} tabIndex={-1} type="submit" />
48
135
  </form>
49
136
 
50
- <div className={styles.sidebar} style={{ height: boardHeight + 1 }}>
137
+ <div className={styles.column}>
51
138
  <Well className={styles.resultsContainer} ref={resultsContainerRef}>
52
- {resultsContainerWidth > 0 && resultsContainerHeight > 0 && (
53
- <Results height={resultsHeight} width={resultsContainerWidth} />
139
+ {resultsContainerWidth > 0 && resultsHeight > 0 && (
140
+ <Results callbacks={callbacks} height={resultsHeight} width={resultsContainerWidth} />
54
141
  )}
55
142
  </Well>
56
143
 
@@ -64,10 +151,56 @@ const Solver: FunctionComponent<Props> = ({ className }) => {
64
151
  </div>
65
152
  </div>
66
153
 
67
- <form className={styles.rackContainer} onSubmit={handleSubmit}>
68
- <Rack className={styles.rack} />
69
- <input className={styles.submitInput} tabIndex={-1} type="submit" />
70
- </form>
154
+ <div className={styles.bottomContainer} ref={bottomContainerRef}>
155
+ <div className={styles.bottomContent}>
156
+ <form className={styles.rackContainer} onSubmit={handleSubmit}>
157
+ <Rack className={styles.rack} tileSize={tileSize} />
158
+ <input className={styles.submitInput} tabIndex={-1} type="submit" />
159
+ </form>
160
+
161
+ {isLessThanL && (
162
+ <div className={styles.controls} style={{ maxWidth: maxControlsWidth }}>
163
+ {typeof error !== 'undefined' && (
164
+ <EmptyState variant="error" onClick={onShowResults}>
165
+ {error.message}
166
+ </EmptyState>
167
+ )}
168
+
169
+ {typeof error === 'undefined' && typeof results === 'undefined' && (
170
+ <EmptyState variant="info" onClick={onShowResults}>
171
+ {translate('results.empty-state.uninitialized')}
172
+ </EmptyState>
173
+ )}
174
+
175
+ {typeof error === 'undefined' && typeof results !== 'undefined' && typeof allResults !== 'undefined' && (
176
+ <>
177
+ {(isOutdated || !resultCandidate) && (
178
+ <EmptyState variant="info" onClick={onShowResults}>
179
+ {translate('results.empty-state.outdated')}
180
+ </EmptyState>
181
+ )}
182
+
183
+ {!isOutdated && allResults.length === 0 && (
184
+ <EmptyState variant="warning" onClick={onShowResults}>
185
+ {translate('results.empty-state.no-results')}
186
+ </EmptyState>
187
+ )}
188
+
189
+ {!isOutdated && allResults.length > 0 && resultCandidate && (
190
+ <ResultCandidatePicker className={styles.resultCandidatePicker} onClick={onShowResults} />
191
+ )}
192
+ </>
193
+ )}
194
+
195
+ {allResults && allResults.length > 0 && !isOutdated && resultCandidate && (
196
+ <ApplyButton className={classNames(styles.submit, styles.apply)} />
197
+ )}
198
+ </div>
199
+ )}
200
+ </div>
201
+ </div>
202
+
203
+ {isTouchDevice && <SolveButton className={styles.solve} onClick={handleSubmit} />}
71
204
  </div>
72
205
  );
73
206
  };
@@ -0,0 +1,5 @@
1
+ @import 'styles/mixins';
2
+
3
+ .icon {
4
+ transform: scale(1.5);
5
+ }
@@ -0,0 +1,38 @@
1
+ import { FunctionComponent } from 'react';
2
+ import { useDispatch } from 'react-redux';
3
+
4
+ import { Check } from 'icons';
5
+ import { resultsSlice, selectResultCandidate, useTypedSelector } from 'state';
6
+
7
+ import Button from '../../../Button';
8
+
9
+ import styles from './ApplyButton.module.scss';
10
+
11
+ interface Props {
12
+ className?: string;
13
+ }
14
+
15
+ const ApplyButton: FunctionComponent<Props> = ({ className }) => {
16
+ const dispatch = useDispatch();
17
+ const resultCandidate = useTypedSelector(selectResultCandidate);
18
+
19
+ const handleClick = () => {
20
+ if (resultCandidate) {
21
+ dispatch(resultsSlice.actions.applyResult(resultCandidate));
22
+ }
23
+ };
24
+
25
+ return (
26
+ <Button
27
+ className={className}
28
+ disabled={!resultCandidate}
29
+ Icon={Check}
30
+ iconClassName={styles.icon}
31
+ type="submit"
32
+ variant="primary"
33
+ onClick={handleClick}
34
+ />
35
+ );
36
+ };
37
+
38
+ export default ApplyButton;
@@ -0,0 +1 @@
1
+ export { default } from './ApplyButton';
@@ -0,0 +1,59 @@
1
+ .emptyState {
2
+ display: flex;
3
+ min-width: 0;
4
+ width: 100%;
5
+ border: var(--border);
6
+ border-radius: var(--border--radius);
7
+ background-color: white;
8
+ box-shadow: var(--box-shadow);
9
+ cursor: pointer;
10
+ text-align: center;
11
+ }
12
+
13
+ .iconContainer {
14
+ flex: 0 0 auto;
15
+ display: flex;
16
+ align-items: center;
17
+ justify-content: center;
18
+ padding: var(--spacing--m);
19
+ color: var(--color--foreground--secondary);
20
+
21
+ [dir='ltr'] & {
22
+ border-top-left-radius: var(--border--radius);
23
+ border-bottom-left-radius: var(--border--radius);
24
+ border-right: var(--border);
25
+ }
26
+
27
+ [dir='rtl'] & {
28
+ border-top-right-radius: var(--border--radius);
29
+ border-bottom-right-radius: var(--border--radius);
30
+ border-left: var(--border);
31
+ }
32
+
33
+ .error & {
34
+ background-color: var(--color--red--light);
35
+ color: var(--color--error);
36
+ }
37
+
38
+ .warning & {
39
+ background-color: var(--color--yellow--light);
40
+ color: var(--color--warning);
41
+ }
42
+
43
+ .info & {
44
+ background-color: var(--color--blue--light);
45
+ color: var(--color--info);
46
+ }
47
+ }
48
+
49
+ .icon {
50
+ $size: 24px;
51
+
52
+ width: $size;
53
+ height: $size;
54
+ }
55
+
56
+ .content {
57
+ flex: 1;
58
+ padding: var(--spacing--m) var(--spacing--l);
59
+ }
@@ -0,0 +1,41 @@
1
+ import classNames from 'classnames';
2
+ import { FunctionComponent, HTMLProps, ReactNode, SVGAttributes } from 'react';
3
+
4
+ import { CrossCircleFill, ExclamationTriangleFill, InfoCircleFill } from 'icons';
5
+
6
+ import styles from './EmptyState.module.scss';
7
+
8
+ interface Props extends HTMLProps<HTMLDivElement> {
9
+ children: ReactNode;
10
+ className?: string;
11
+ variant: 'error' | 'info' | 'warning';
12
+ }
13
+
14
+ const ICON_PER_TYPE: Record<Props['variant'], FunctionComponent<SVGAttributes<SVGElement>>> = {
15
+ error: CrossCircleFill,
16
+ info: InfoCircleFill,
17
+ warning: ExclamationTriangleFill,
18
+ };
19
+
20
+ const EmptyState: FunctionComponent<Props> = ({ children, className, variant, ...props }) => {
21
+ const Icon = ICON_PER_TYPE[variant];
22
+
23
+ return (
24
+ <div
25
+ className={classNames(styles.emptyState, className, {
26
+ [styles.error]: variant === 'error',
27
+ [styles.info]: variant === 'info',
28
+ [styles.warning]: variant === 'warning',
29
+ })}
30
+ {...props}
31
+ >
32
+ <div className={styles.iconContainer}>
33
+ <Icon className={styles.icon} />
34
+ </div>
35
+
36
+ <div className={styles.content}>{children}</div>
37
+ </div>
38
+ );
39
+ };
40
+
41
+ export default EmptyState;
@@ -0,0 +1 @@
1
+ export { default } from './EmptyState';
@@ -0,0 +1,4 @@
1
+ .solveButton {
2
+ padding: var(--spacing--l);
3
+ border-radius: 50%;
4
+ }
@@ -0,0 +1,43 @@
1
+ import classNames from 'classnames';
2
+ import { FunctionComponent, MouseEventHandler } from 'react';
3
+ import { useDispatch } from 'react-redux';
4
+
5
+ import { Search } from 'icons';
6
+ import { noop } from 'lib';
7
+ import { selectAreResultsOutdated, selectIsLoading, selectRack, solveSlice, useTypedSelector } from 'state';
8
+
9
+ import Button from '../../../Button';
10
+
11
+ import styles from './SolveButton.module.scss';
12
+
13
+ interface Props {
14
+ className?: string;
15
+ onClick?: MouseEventHandler;
16
+ }
17
+
18
+ const SolveButton: FunctionComponent<Props> = ({ className, onClick = noop }) => {
19
+ const dispatch = useDispatch();
20
+ const isLoading = useTypedSelector(selectIsLoading);
21
+ const isOutdated = useTypedSelector(selectAreResultsOutdated);
22
+ const rack = useTypedSelector(selectRack);
23
+ const hasTiles = rack.some((tile) => tile !== null);
24
+
25
+ const handleClick: MouseEventHandler = (event) => {
26
+ dispatch(solveSlice.actions.submit());
27
+ onClick(event);
28
+ };
29
+
30
+ return (
31
+ <Button
32
+ className={classNames(styles.solveButton, className)}
33
+ disabled={isLoading || !isOutdated || !hasTiles}
34
+ Icon={Search}
35
+ iconClassName={styles.icon}
36
+ type="submit"
37
+ variant="primary"
38
+ onClick={handleClick}
39
+ />
40
+ );
41
+ };
42
+
43
+ export default SolveButton;
@@ -0,0 +1 @@
1
+ export { default } from './SolveButton';
@@ -0,0 +1,3 @@
1
+ export { default as ApplyButton } from './ApplyButton';
2
+ export { default as EmptyState } from './EmptyState';
3
+ export { default as SolveButton } from './SolveButton';
@@ -1,6 +1,4 @@
1
- $z-index: 100;
2
-
3
- .screen {
1
+ .splashScreen {
4
2
  position: fixed;
5
3
  top: 0;
6
4
  bottom: 0;
@@ -8,15 +6,7 @@ $z-index: 100;
8
6
  right: 0;
9
7
  background-color: var(--color--background);
10
8
  opacity: 1;
11
- z-index: $z-index;
12
- }
13
-
14
- .closeButton {
15
- position: fixed;
16
- top: 0;
17
- right: 0;
18
- z-index: $z-index + 1;
19
- margin: var(--spacing--l);
9
+ z-index: var(--z-index--modal);
20
10
  }
21
11
 
22
12
  .content {
@@ -1,7 +1,7 @@
1
1
  import classNames from 'classnames';
2
2
  import { AnimationEventHandler, FunctionComponent, ReactNode } from 'react';
3
3
 
4
- import styles from './Screen.module.scss';
4
+ import styles from './SplashScreen.module.scss';
5
5
 
6
6
  interface Props {
7
7
  children?: ReactNode;
@@ -10,10 +10,10 @@ interface Props {
10
10
  onAnimationEnd?: AnimationEventHandler<HTMLDivElement>;
11
11
  }
12
12
 
13
- const Screen: FunctionComponent<Props> = ({ children, className, contentClassName, onAnimationEnd }) => (
14
- <div className={classNames(styles.screen, className)} onAnimationEnd={onAnimationEnd}>
13
+ const SplashScreen: FunctionComponent<Props> = ({ children, className, contentClassName, onAnimationEnd }) => (
14
+ <div className={classNames(styles.splashScreen, className)} onAnimationEnd={onAnimationEnd}>
15
15
  <div className={classNames(styles.content, contentClassName)}>{children}</div>
16
16
  </div>
17
17
  );
18
18
 
19
- export default Screen;
19
+ export default SplashScreen;
@@ -0,0 +1 @@
1
+ export { default } from './SplashScreen';
@@ -21,7 +21,7 @@
21
21
 
22
22
  &[disabled] {
23
23
  pointer-events: none;
24
- opacity: 0.25;
24
+ opacity: var(--opacity--disabled);
25
25
  }
26
26
  }
27
27
 
@@ -32,8 +32,8 @@
32
32
  }
33
33
 
34
34
  .icon {
35
- width: var(--square-button-size);
36
- height: var(--square-button-size);
35
+ width: var(--square-button--size);
36
+ height: var(--square-button--size);
37
37
  pointer-events: none;
38
38
  color: inherit;
39
39
  transition: var(--transition);
@@ -1,5 +1,5 @@
1
1
  import classNames from 'classnames';
2
- import { ButtonHTMLAttributes, FunctionComponent, MouseEventHandler, SVGAttributes } from 'react';
2
+ import { ButtonHTMLAttributes, FunctionComponent, MouseEventHandler, ReactNode, SVGAttributes } from 'react';
3
3
 
4
4
  import { useTooltip } from '../Tooltip';
5
5
 
@@ -9,7 +9,7 @@ import styles from './SquareButton.module.scss';
9
9
  interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
10
10
  children?: never;
11
11
  Icon: FunctionComponent<SVGAttributes<SVGElement>>;
12
- tooltip: string;
12
+ tooltip: ReactNode;
13
13
  onClick: MouseEventHandler<HTMLButtonElement>;
14
14
  }
15
15
 
@@ -4,43 +4,28 @@
4
4
  --background-color: transparent;
5
5
 
6
6
  position: relative;
7
- background-color: var(--background-color);
8
7
  transition: var(--transition);
9
8
 
10
- &.raised {
11
- box-shadow: inset -2px -2px 2px -1px rgba(34, 34, 34, 0.8);
12
- }
13
-
14
9
  &.points1 {
15
10
  --background-color: var(--color--yellow);
16
-
17
- background-color: var(--background-color);
18
11
  }
19
12
 
20
13
  &.points2 {
21
14
  --background-color: var(--color--green);
22
-
23
- background-color: var(--background-color);
24
15
  }
25
16
 
26
17
  &.points3,
27
18
  &.points4 {
28
19
  --background-color: var(--color--blue);
29
-
30
- background-color: var(--background-color);
31
20
  }
32
21
 
33
22
  &.points5 {
34
23
  --background-color: var(--color--red);
35
-
36
- background-color: var(--background-color);
37
24
  }
38
25
 
39
26
  &.blank {
40
27
  --background-color: white;
41
28
 
42
- background-color: var(--background-color);
43
-
44
29
  .character {
45
30
  color: black;
46
31
  }
@@ -49,7 +34,6 @@
49
34
  &.highlighted {
50
35
  --background-color: var(--color--primary);
51
36
 
52
- background-color: var(--background-color);
53
37
  color: white;
54
38
 
55
39
  .character {
@@ -64,34 +48,55 @@
64
48
  &.invalid {
65
49
  --background-color: var(--color--red--light);
66
50
 
67
- background-color: var(--background-color);
68
51
  color: var(--color--error);
69
52
  }
53
+
54
+ &:not(.disabled) {
55
+ .input::selection {
56
+ --background--color: transparent;
57
+ }
58
+ }
70
59
  }
71
60
 
61
+ .input,
72
62
  .character {
73
- width: 100%;
74
- height: 100%;
75
63
  padding: 0;
76
- box-sizing: border-box;
77
- background-color: transparent;
78
- color: inherit;
79
- border: none;
80
64
  font-weight: bold;
81
65
  text-transform: uppercase;
82
66
  text-align: center;
83
67
  caret-color: transparent;
68
+ box-sizing: border-box;
69
+ }
84
70
 
85
- &::placeholder {
86
- color: var(--color--inactive);
87
- }
71
+ .input {
72
+ width: 100%;
73
+ height: 100%;
74
+ background-color: transparent;
75
+ color: transparent;
76
+ border: none;
77
+ font-size: 16px; // prevent iOS from automatically zooming in on focus
78
+ }
79
+
80
+ .character {
81
+ position: absolute;
82
+ top: 0;
83
+ right: 0;
84
+ bottom: 0;
85
+ left: 0;
86
+ display: flex;
87
+ align-items: center;
88
+ justify-content: center;
89
+ background-color: var(--background-color);
90
+ transition: var(--transition);
91
+ pointer-events: none;
92
+ user-select: none;
88
93
 
89
- &::selection {
90
- background: transparent;
94
+ .empty & {
95
+ color: var(--color--inactive);
91
96
  }
92
97
 
93
- &:disabled {
94
- color: inherit;
98
+ .raised & {
99
+ box-shadow: inset -2px -2px 2px -1px rgba(34, 34, 34, 0.8);
95
100
  }
96
101
  }
97
102
 
@@ -104,6 +109,10 @@
104
109
  pointer-events: none;
105
110
  letter-spacing: -1px;
106
111
 
112
+ @include media('<xs') {
113
+ display: none;
114
+ }
115
+
107
116
  [dir='ltr'] & {
108
117
  bottom: 1%;
109
118
  right: 9%;
@@ -55,7 +55,7 @@ const Tile: FunctionComponent<Props> = ({
55
55
  const locale = useTypedSelector(selectLocale);
56
56
  const { pointsFontSize, tileFontSize, tileSize } = getTileSizes(size);
57
57
  const style = useMemo(() => ({ height: tileSize, width: tileSize }), [tileSize]);
58
- const inputStyle = useMemo(() => ({ fontSize: tileFontSize }), [tileFontSize]);
58
+ const characterStyle = useMemo(() => ({ fontSize: tileFontSize }), [tileFontSize]);
59
59
  const pointsStyle = useMemo(() => ({ fontSize: pointsFontSize }), [pointsFontSize]);
60
60
  const inputRef = useMemo<RefObject<HTMLInputElement>>(() => ref || createRef(), [ref]);
61
61
  const isEmpty = !character || character === EMPTY_CELL;
@@ -78,11 +78,11 @@ const Tile: FunctionComponent<Props> = ({
78
78
  autoFocus={autoFocus}
79
79
  canShowPoints={canShowPoints}
80
80
  character={character}
81
+ characterStyle={characterStyle}
81
82
  className={className}
82
83
  disabled={disabled}
83
84
  highlighted={highlighted}
84
85
  inputRef={inputRef}
85
- inputStyle={inputStyle}
86
86
  isBlank={isBlank}
87
87
  isValid={isValid}
88
88
  placeholder={placeholder}