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