@scrabble-solver/scrabble-solver 2.10.10 → 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 (118) 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} +860 -767
  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 -12
  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 -14
  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 +250 -175
  43. package/.next/server/pages/index.js.nft.json +1 -1
  44. package/.next/server/pages/index.json +1 -1
  45. package/.next/server/pages-manifest.json +1 -1
  46. package/.next/static/45ye7793DY705HOcuK9lJ/_buildManifest.js +1 -0
  47. package/.next/static/chunks/main-0ecb9ccfcb6c9b24.js +1 -0
  48. package/.next/static/chunks/pages/404-e0f30450e9920dc3.js +1 -0
  49. package/.next/static/chunks/pages/_app-d7acee5e526752d9.js +28 -0
  50. package/.next/static/chunks/pages/{_error-8353112a01355ec2.js → _error-54de1933a164a1ff.js} +1 -1
  51. package/.next/static/chunks/pages/index-35d2c1c79a201ae2.js +1 -0
  52. package/.next/static/css/a48caa6f57de6e98.css +1 -0
  53. package/.next/static/css/c49bbe944ddd1b39.css +2 -0
  54. package/.next/trace +55 -55
  55. package/package.json +12 -13
  56. package/src/components/Board/components/Cell/Cell.module.scss +1 -0
  57. package/src/components/Board/hooks/useGrid.ts +1 -1
  58. package/src/components/Dictionary/Dictionary.module.scss +13 -8
  59. package/src/components/Dictionary/Dictionary.tsx +5 -5
  60. package/src/components/Loading/Loading.tsx +1 -1
  61. package/src/components/Modal/Modal.module.scss +0 -1
  62. package/src/components/NavButtons/NavButtons.tsx +4 -5
  63. package/src/components/Results/Results.module.scss +15 -2
  64. package/src/components/Results/Results.tsx +78 -67
  65. package/src/components/ResultsInput/ResultsInput.tsx +6 -2
  66. package/src/components/Solver/Solver.module.scss +9 -7
  67. package/src/components/Solver/Solver.tsx +21 -49
  68. package/src/components/Solver/components/FloatingSolveButton/FloatingSolveButton.module.scss +7 -0
  69. package/src/components/Solver/components/{SolveButton/SolveButton.tsx → FloatingSolveButton/FloatingSolveButton.tsx} +6 -6
  70. package/src/components/Solver/components/FloatingSolveButton/index.ts +1 -0
  71. package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.module.scss +19 -4
  72. package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.tsx +13 -1
  73. package/src/components/Solver/components/index.ts +1 -1
  74. package/src/components/Spinner/Spinner.module.scss +11 -0
  75. package/src/components/Spinner/Spinner.tsx +19 -0
  76. package/src/components/Spinner/index.ts +1 -0
  77. package/src/components/Tile/Tile.tsx +9 -9
  78. package/src/components/index.ts +1 -1
  79. package/src/hooks/index.ts +1 -0
  80. package/src/hooks/useAppLayout.ts +29 -0
  81. package/src/i18n/de.json +1 -0
  82. package/src/i18n/en.json +1 -0
  83. package/src/i18n/es.json +1 -0
  84. package/src/i18n/fa.json +1 -0
  85. package/src/i18n/fr.json +1 -0
  86. package/src/i18n/index.ts +1 -1
  87. package/src/i18n/pl.json +1 -0
  88. package/src/modals/DictionaryModal/DictionaryModal.module.scss +18 -0
  89. package/src/modals/DictionaryModal/DictionaryModal.tsx +27 -0
  90. package/src/modals/DictionaryModal/index.ts +1 -0
  91. package/src/modals/MenuModal/MenuModal.tsx +7 -1
  92. package/src/modals/ResultsModal/ResultsModal.module.scss +18 -1
  93. package/src/modals/ResultsModal/ResultsModal.tsx +10 -13
  94. package/src/modals/index.ts +1 -0
  95. package/src/pages/api/solve.ts +9 -10
  96. package/src/pages/index.tsx +20 -15
  97. package/src/parameters/index.ts +0 -6
  98. package/src/service-worker/routeSolveRequests.ts +4 -1
  99. package/src/state/localStorage.ts +0 -9
  100. package/src/styles/animations.scss +10 -0
  101. package/src/styles/global.scss +1 -1
  102. package/src/styles/variables.scss +1 -1
  103. package/src/types/index.ts +1 -0
  104. package/.next/server/font-loader-manifest.js +0 -1
  105. package/.next/static/chunks/main-74c4d6b2b5c362f3.js +0 -1
  106. package/.next/static/chunks/pages/404-d5ff00df1c687977.js +0 -1
  107. package/.next/static/chunks/pages/_app-3272e798504c40d8.js +0 -28
  108. package/.next/static/chunks/pages/index-5c2544930e46c5ce.js +0 -1
  109. package/.next/static/css/336e75db2b74b157.css +0 -2
  110. package/.next/static/css/ec4e47a6b1866fe5.css +0 -1
  111. package/.next/static/warzWo25tDxo_Eiv9T6f2/_buildManifest.js +0 -1
  112. package/src/components/Solver/components/SolveButton/SolveButton.module.scss +0 -4
  113. package/src/components/Solver/components/SolveButton/index.ts +0 -1
  114. package/src/components/Well/Well.module.scss +0 -6
  115. package/src/components/Well/Well.tsx +0 -17
  116. package/src/components/Well/index.ts +0 -1
  117. /package/.next/server/{font-loader-manifest.json → next-font-manifest.json} +0 -0
  118. /package/.next/static/{warzWo25tDxo_Eiv9T6f2 → 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.10",
3
+ "version": "2.11.0",
4
4
  "description": "Scrabble Solver 2 - App",
5
5
  "engines": {
6
6
  "node": ">=16"
@@ -28,22 +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.10",
36
- "@scrabble-solver/constants": "^2.10.10",
37
- "@scrabble-solver/dictionaries": "^2.10.10",
38
- "@scrabble-solver/logger": "^2.10.10",
39
- "@scrabble-solver/solver": "^2.10.10",
40
- "@scrabble-solver/types": "^2.10.10",
41
- "@scrabble-solver/word-definitions": "^2.10.10",
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
- "merge-refs": "^1.1.2",
46
- "next": "^13.2.1",
45
+ "next": "^13.2.4",
47
46
  "normalize.css": "^8.0.1",
48
47
  "react": "^18.2.0",
49
48
  "react-dom": "^18.2.0",
@@ -75,8 +74,8 @@
75
74
  "@types/redux-saga": "^0.10.5",
76
75
  "@types/uuid": "^9.0.1",
77
76
  "env-cmd": "^10.1.0",
78
- "sass": "^1.58.3",
77
+ "sass": "^1.59.2",
79
78
  "workbox-webpack-plugin": "^6.5.4"
80
79
  },
81
- "gitHead": "42551ac84b78ed24543230c19b861e1543f7a50a"
80
+ "gitHead": "493dd931e7f16c34c425295a0f048756d6c192c8"
82
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
- {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}
@@ -7,6 +7,9 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
7
7
  display: flex;
8
8
  flex-direction: column;
9
9
  height: 100%;
10
+ background: var(--color--background--element);
11
+ border: var(--border);
12
+ box-shadow: var(--box-shadow);
10
13
  font-family: var(--font--family--title);
11
14
  }
12
15
 
@@ -14,8 +17,18 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
14
17
  flex: 1;
15
18
  }
16
19
 
20
+ .content {
21
+ flex: 1;
22
+ position: relative;
23
+ height: 100%;
24
+ }
25
+
26
+ .listContainer {
27
+ position: absolute;
28
+ top: 0;
29
+ }
30
+
17
31
  .list {
18
- flex: 1 1 auto;
19
32
  transition: var(--transition);
20
33
 
21
34
  &.outdated {
@@ -29,8 +42,8 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
29
42
  display: flex;
30
43
  align-items: center;
31
44
  justify-content: space-between;
32
- border-bottom: var(--border);
33
45
  font-weight: 700;
46
+ border-bottom: var(--border);
34
47
  }
35
48
 
36
49
  .headerButton {
@@ -1,9 +1,10 @@
1
1
  import classNames from 'classnames';
2
2
  import { FunctionComponent, useEffect, useMemo, useState } from 'react';
3
+ import { useLatest, useMeasure } from 'react-use';
3
4
  import { FixedSizeList } from 'react-window';
4
5
 
5
6
  import { LOCALE_FEATURES } from 'i18n';
6
- import { BORDER_WIDTH, RESULTS_HEADER_HEIGHT, RESULTS_INPUT_HEIGHT, RESULTS_ITEM_HEIGHT } from 'parameters';
7
+ import { RESULTS_ITEM_HEIGHT } from 'parameters';
7
8
  import {
8
9
  selectAreResultsOutdated,
9
10
  selectIsLoading,
@@ -18,6 +19,7 @@ import {
18
19
  import EmptyState from '../EmptyState';
19
20
  import Loading from '../Loading';
20
21
  import ResultsInput from '../ResultsInput';
22
+ import Sizer from '../Sizer';
21
23
 
22
24
  import HeaderButton from './HeaderButton';
23
25
  import Result from './Result';
@@ -28,12 +30,11 @@ import useColumns from './useColumns';
28
30
 
29
31
  interface Props {
30
32
  callbacks: ResultCallbacks;
31
- height: number;
33
+ className?: string;
32
34
  highlightedIndex?: number;
33
- width: number;
34
35
  }
35
36
 
36
- const Results: FunctionComponent<Props> = ({ callbacks, height, highlightedIndex, width }) => {
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];
@@ -44,92 +45,102 @@ const Results: FunctionComponent<Props> = ({ callbacks, height, highlightedIndex
44
45
  const isOutdated = useTypedSelector(selectAreResultsOutdated);
45
46
  const error = useTypedSelector(selectSolveError);
46
47
  const itemData = useMemo(() => ({ ...callbacks, highlightedIndex, results }), [callbacks, highlightedIndex, results]);
48
+ const [sizerRef, { height, width }] = useMeasure<HTMLDivElement>();
47
49
  const [listRef, setListRef] = useState<FixedSizeList<ResultData> | null>(null);
48
50
  const columns = useColumns();
49
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';
50
55
 
51
56
  useEffect(() => {
52
57
  // without setTimeout, the initial scrolling offset is calculated
53
58
  // incorrectly, as the list is not fully rendered by the browser yet
54
59
  const timeout = globalThis.setTimeout(() => {
55
60
  if (listRef) {
56
- listRef.scrollToItem(scrollToIndex, 'center');
61
+ listRef.scrollToItem(scrollToIndexRef.current, 'center');
57
62
  }
58
63
  }, 0);
59
64
 
60
65
  return () => {
61
66
  globalThis.clearTimeout(timeout);
62
67
  };
63
- }, [listRef, scrollToIndex]);
68
+ }, [allResults, listRef, scrollToIndexRef]);
64
69
 
65
70
  return (
66
- <div className={styles.results}>
71
+ <div className={classNames(styles.results, className)}>
67
72
  <div className={styles.header}>
68
73
  {columns.map((column) => (
69
74
  <HeaderButton column={column} key={column.id} />
70
75
  ))}
71
76
  </div>
72
77
 
73
- {typeof error !== 'undefined' && (
74
- <EmptyState className={styles.emptyState} variant="error">
75
- {error.message}
76
- </EmptyState>
77
- )}
78
-
79
- {typeof error === 'undefined' && typeof filteredResults === 'undefined' && (
80
- <EmptyState className={styles.emptyState} variant="info">
81
- {translate('results.empty-state.uninitialized')}
82
-
83
- <SolveButton className={styles.solveButton} />
84
- </EmptyState>
85
- )}
86
-
87
- {typeof error === 'undefined' && typeof filteredResults !== 'undefined' && typeof allResults !== 'undefined' && (
88
- <>
89
- {isOutdated && (
90
- <EmptyState className={styles.emptyState} variant="info">
91
- {translate('results.empty-state.outdated')}
92
-
93
- <SolveButton className={styles.solveButton} />
94
- </EmptyState>
95
- )}
96
-
97
- {!isOutdated && (
98
- <>
99
- {allResults.length === 0 && (
100
- <EmptyState className={styles.emptyState} variant="warning">
101
- {translate('results.empty-state.no-results')}
102
- </EmptyState>
103
- )}
104
-
105
- {allResults.length > 0 && filteredResults.length === 0 && (
106
- <EmptyState className={styles.emptyState} variant="info">
107
- {translate('results.empty-state.no-filtered-results')}
108
- </EmptyState>
109
- )}
110
-
111
- {allResults.length > 0 && filteredResults.length > 0 && (
112
- <FixedSizeList
113
- className={classNames(styles.list, {
114
- [styles.outdated]: isOutdated,
115
- })}
116
- direction={direction}
117
- height={height - RESULTS_HEADER_HEIGHT - RESULTS_INPUT_HEIGHT - 2 * BORDER_WIDTH}
118
- itemCount={filteredResults.length}
119
- itemData={itemData}
120
- itemSize={RESULTS_ITEM_HEIGHT}
121
- ref={setListRef}
122
- width={width}
123
- >
124
- {Result}
125
- </FixedSizeList>
126
- )}
127
-
128
- {allResults.length > 0 && <ResultsInput className={styles.input} />}
129
- </>
130
- )}
131
- </>
132
- )}
78
+ <div className={styles.content}>
79
+ <Sizer ref={sizerRef} />
80
+
81
+ {typeof error !== 'undefined' && (
82
+ <EmptyState className={styles.emptyState} variant="error">
83
+ {error.message}
84
+ </EmptyState>
85
+ )}
86
+
87
+ {typeof error === 'undefined' && typeof filteredResults === 'undefined' && (
88
+ <EmptyState className={styles.emptyState} variant="info">
89
+ {translate('results.empty-state.uninitialized')}
90
+
91
+ <SolveButton className={styles.solveButton} />
92
+ </EmptyState>
93
+ )}
94
+
95
+ {hasResults && (
96
+ <>
97
+ {isOutdated && (
98
+ <EmptyState className={styles.emptyState} variant="info">
99
+ {translate('results.empty-state.outdated')}
100
+
101
+ <SolveButton className={styles.solveButton} />
102
+ </EmptyState>
103
+ )}
104
+
105
+ {!isOutdated && (
106
+ <>
107
+ {allResults.length === 0 && (
108
+ <EmptyState className={styles.emptyState} variant="warning">
109
+ {translate('results.empty-state.no-results')}
110
+ </EmptyState>
111
+ )}
112
+
113
+ {allResults.length > 0 && filteredResults.length === 0 && (
114
+ <EmptyState className={styles.emptyState} variant="info">
115
+ {translate('results.empty-state.no-filtered-results')}
116
+ </EmptyState>
117
+ )}
118
+
119
+ {allResults.length > 0 && filteredResults.length > 0 && (
120
+ <div className={styles.listContainer}>
121
+ <FixedSizeList
122
+ className={classNames(styles.list, {
123
+ [styles.outdated]: isOutdated,
124
+ })}
125
+ direction={direction}
126
+ height={height}
127
+ itemCount={filteredResults.length}
128
+ itemData={itemData}
129
+ itemSize={RESULTS_ITEM_HEIGHT}
130
+ ref={setListRef}
131
+ width={width}
132
+ >
133
+ {Result}
134
+ </FixedSizeList>
135
+ </div>
136
+ )}
137
+ </>
138
+ )}
139
+ </>
140
+ )}
141
+ </div>
142
+
143
+ {hasResults && allResults.length > 0 && !isOutdated && <ResultsInput className={styles.input} />}
133
144
 
134
145
  {isLoading && <Loading />}
135
146
  </div>
@@ -1,5 +1,5 @@
1
1
  import classNames from 'classnames';
2
- import { ChangeEvent, FunctionComponent, useState } from 'react';
2
+ import { ChangeEvent, FormEventHandler, FunctionComponent, useState } from 'react';
3
3
  import { useDispatch } from 'react-redux';
4
4
 
5
5
  import { isRegExp } from 'lib';
@@ -27,8 +27,12 @@ const ResultsInput: FunctionComponent<Props> = ({ className }) => {
27
27
  }
28
28
  };
29
29
 
30
+ const handleSubmit: FormEventHandler = (event) => {
31
+ event.preventDefault();
32
+ };
33
+
30
34
  return (
31
- <form className={classNames(styles.resultsInput, className)}>
35
+ <form className={classNames(styles.resultsInput, className)} onSubmit={handleSubmit}>
32
36
  <input
33
37
  className={styles.input}
34
38
  placeholder={translate('results.input.placeholder')}
@@ -32,6 +32,7 @@
32
32
  display: grid;
33
33
  grid-template-columns: minmax(0, 1fr) var(--solver-column--width);
34
34
  gap: var(--spacing);
35
+ height: 100%;
35
36
  margin: 0 auto;
36
37
 
37
38
  @include media('<l') {
@@ -62,17 +63,17 @@
62
63
  }
63
64
  }
64
65
 
65
- .resultsContainer {
66
- flex: 1;
67
- position: relative;
68
- }
69
-
70
- .dictionary {
66
+ .dictionaryContainer {
67
+ flex: 0 0 auto;
68
+ height: var(--dictionary--height);
71
69
  display: flex;
72
70
  flex-direction: column;
71
+ background-color: var(--color--background--element);
72
+ border: var(--border);
73
+ box-shadow: var(--box-shadow);
73
74
  }
74
75
 
75
- .dictionaryOutput {
76
+ .dictionary {
76
77
  flex: 1;
77
78
  border-bottom: var(--border);
78
79
  }
@@ -113,6 +114,7 @@
113
114
 
114
115
  position: fixed;
115
116
  bottom: var(--spacing);
117
+ z-index: 1;
116
118
 
117
119
  @include media('<xs') {
118
120
  --spacing: var(--spacing--m);
@@ -4,17 +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
- COLUMN_MIN_HEIGHT,
13
- COMPONENTS_SPACING,
14
- COMPONENTS_SPACING_SMALL,
15
- DICTIONARY_HEIGHT,
16
- RACK_TILE_SIZE_MAX,
17
- } 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';
18
9
  import {
19
10
  resultsSlice,
20
11
  selectAreResultsOutdated,
@@ -33,9 +24,8 @@ import Dictionary from '../Dictionary';
33
24
  import DictionaryInput from '../DictionaryInput';
34
25
  import Rack from '../Rack';
35
26
  import Results from '../Results';
36
- import Well from '../Well';
37
27
 
38
- import { EmptyState, ResultCandidatePicker, SolveButton } from './components';
28
+ import { EmptyState, FloatingSolveButton, ResultCandidatePicker } from './components';
39
29
  import styles from './Solver.module.scss';
40
30
 
41
31
  interface Props {
@@ -50,18 +40,12 @@ const Solver: FunctionComponent<Props> = ({ className, height, width, onShowResu
50
40
  const dispatch = useDispatch();
51
41
  const translate = useTranslate();
52
42
  const isTouchDevice = useIsTouchDevice();
43
+ const { componentsSpacing, isBoardFullWidth, showColumn, showCompactControls, showFloatingSolveButton } =
44
+ useAppLayout();
53
45
  const [bottomContainerRef, { height: bottomContainerHeight }] = useMeasure<HTMLDivElement>();
54
- const [resultsContainerRef, { width: resultsContainerWidth }] = useMeasure<HTMLDivElement>();
55
- const isLessThanXl = useMediaQuery('<xl');
56
- const isLessThanL = useMediaQuery('<l');
57
- const isLessThanM = useMediaQuery('<m');
58
- const componentsSpacing = isLessThanXl ? COMPONENTS_SPACING_SMALL : COMPONENTS_SPACING;
59
- const maxBoardWidth = width - resultsContainerWidth - (isLessThanL ? 0 : componentsSpacing) - 2 * componentsSpacing;
60
- const maxBoardHeight = Math.max(
61
- height - bottomContainerHeight,
62
- isLessThanL ? 0 : COLUMN_MIN_HEIGHT,
63
- isLessThanM ? Number.POSITIVE_INFINITY : 0,
64
- );
46
+ const [columnRef, { width: columnWidth }] = useMeasure<HTMLDivElement>();
47
+ const maxBoardWidth = width - columnWidth - (showColumn ? componentsSpacing : 0) - 2 * componentsSpacing;
48
+ const maxBoardHeight = isBoardFullWidth ? Number.POSITIVE_INFINITY : Math.max(height - bottomContainerHeight, 0);
65
49
  const config = useTypedSelector(selectConfig);
66
50
  const error = useTypedSelector(selectSolveError);
67
51
  const isOutdated = useTypedSelector(selectAreResultsOutdated);
@@ -74,8 +58,6 @@ const Solver: FunctionComponent<Props> = ({ className, height, width, onShowResu
74
58
  const cellSize = Math.min(cellWidth, cellHeight);
75
59
  const cellSizeSafe = Math.min(Math.max(cellSize, BOARD_TILE_SIZE_MIN), BOARD_TILE_SIZE_MAX);
76
60
  const tileSize = Math.min((maxBoardWidth - 2 * BORDER_WIDTH) / config.maximumCharactersCount, RACK_TILE_SIZE_MAX);
77
- const boardSize = (cellSizeSafe + BORDER_WIDTH) * Math.max(config.boardWidth, config.boardHeight) + BORDER_WIDTH;
78
- const resultsHeight = boardSize - DICTIONARY_HEIGHT - componentsSpacing - 4 * BORDER_WIDTH;
79
61
  const maxControlsWidth = tileSize * config.maximumCharactersCount + 2 * BORDER_WIDTH;
80
62
  const touchCallbacks = useMemo(
81
63
  () => ({
@@ -116,53 +98,43 @@ const Solver: FunctionComponent<Props> = ({ className, height, width, onShowResu
116
98
  const handleSubmit = (event: SyntheticEvent) => {
117
99
  event.preventDefault();
118
100
 
119
- if (isLessThanL) {
120
- onShowResults();
121
- }
122
-
123
101
  dispatch(solveSlice.actions.submit());
124
102
  };
125
103
 
126
104
  useEffect(() => {
127
- if (isLessThanL && bestResult && !isOutdated) {
105
+ if (showCompactControls && bestResult && !isOutdated) {
128
106
  dispatch(resultsSlice.actions.changeResultCandidate(bestResult));
129
107
  }
130
- }, [bestResult, dispatch, isLessThanL, isOutdated]);
108
+ }, [bestResult, dispatch, showCompactControls, isOutdated]);
131
109
 
132
110
  return (
133
111
  <div className={classNames(styles.solver, className)}>
134
112
  <div className={styles.container}>
135
113
  <div className={styles.content}>
136
- <form className={styles.boardContainer} onSubmit={handleSubmit}>
114
+ <form id="a" className={styles.boardContainer} onSubmit={handleSubmit}>
137
115
  <Board cellSize={cellSizeSafe} className={styles.board} />
138
116
  <input className={styles.submitInput} tabIndex={-1} type="submit" />
139
117
  </form>
140
118
 
141
- <div className={styles.column}>
142
- <Well className={styles.resultsContainer} ref={resultsContainerRef}>
143
- {resultsContainerWidth > 0 && resultsHeight > 0 && (
144
- <Results callbacks={callbacks} height={resultsHeight} width={resultsContainerWidth} />
145
- )}
146
- </Well>
147
-
148
- <Well>
149
- <div className={styles.dictionary} style={{ height: DICTIONARY_HEIGHT }}>
150
- <Dictionary className={styles.dictionaryOutput} />
151
- <DictionaryInput className={styles.dictionaryInput} />
152
- </div>
153
- </Well>
119
+ <div className={styles.column} ref={columnRef}>
120
+ <Results callbacks={callbacks} />
121
+
122
+ <div className={styles.dictionaryContainer}>
123
+ <Dictionary className={styles.dictionary} />
124
+ <DictionaryInput className={styles.dictionaryInput} />
125
+ </div>
154
126
  </div>
155
127
  </div>
156
128
  </div>
157
129
 
158
130
  <div className={styles.bottomContainer} ref={bottomContainerRef}>
159
131
  <div className={styles.bottomContent}>
160
- <form onSubmit={handleSubmit}>
132
+ <form id="b" onSubmit={handleSubmit}>
161
133
  <Rack className={styles.rack} tileSize={tileSize} />
162
134
  <input className={styles.submitInput} tabIndex={-1} type="submit" />
163
135
  </form>
164
136
 
165
- {isLessThanL && (
137
+ {showCompactControls && (
166
138
  <div className={styles.controls} style={{ maxWidth: maxControlsWidth }}>
167
139
  <ResultCandidatePicker onResultClick={onShowResults} />
168
140
 
@@ -182,7 +154,7 @@ const Solver: FunctionComponent<Props> = ({ className, height, width, onShowResu
182
154
  </div>
183
155
  </div>
184
156
 
185
- {isTouchDevice && <SolveButton className={styles.solve} onClick={handleSubmit} />}
157
+ {showFloatingSolveButton && <FloatingSolveButton className={styles.solve} onClick={handleSubmit} />}
186
158
  </div>
187
159
  );
188
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
+ }