@scrabble-solver/scrabble-solver 2.11.6 → 2.11.8
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 +11 -11
- 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/routes-manifest.json +1 -1
- package/.next/server/chunks/277.js +653 -399
- package/.next/server/chunks/865.js +153 -115
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/404.js.nft.json +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/pages/_app.js +9 -2
- package/.next/server/pages/_app.js.nft.json +1 -1
- package/.next/server/pages/api/solve.js +30 -1
- package/.next/server/pages/index.html +1 -1
- package/.next/server/pages/index.js +35 -6
- package/.next/server/pages/index.js.nft.json +1 -1
- package/.next/server/pages/index.json +1 -1
- package/.next/server/pages-manifest.json +3 -3
- package/.next/static/5ttGCAW8jcIKxpR8om9fK/_buildManifest.js +1 -0
- package/.next/static/chunks/framework-2c5cac93e8c637b5.js +49 -0
- package/.next/static/chunks/pages/{404-8176f4acd0cfeb42.js → 404-ca203fa27afc37d8.js} +1 -1
- package/.next/static/chunks/pages/_app-76a8840b6244d5a2.js +28 -0
- package/.next/static/chunks/pages/index-6894f40e6cac9243.js +1 -0
- package/.next/static/css/af871fef886ef5b7.css +2 -0
- package/.next/static/css/c6e0e01f44fc0425.css +1 -0
- package/.next/trace +50 -50
- package/package.json +9 -9
- package/src/components/Board/Board.module.scss +2 -59
- package/src/components/Board/Board.tsx +22 -10
- package/src/components/Board/BoardPure.tsx +13 -8
- package/src/components/Board/components/Cell/Cell.module.scss +8 -140
- package/src/components/Board/components/Cell/Cell.tsx +18 -37
- package/src/components/Board/hooks/index.ts +1 -0
- package/src/components/Board/hooks/useBackgroundImage.tsx +174 -0
- package/src/components/Board/hooks/useGrid.ts +17 -1
- package/src/components/Board/lib/getBonusColor.ts +18 -0
- package/src/components/Board/lib/index.ts +1 -0
- package/src/components/DictionaryInput/DictionaryInput.tsx +5 -3
- package/src/components/Key/Key.module.scss +7 -11
- package/src/components/Rack/Rack.tsx +4 -1
- package/src/components/Rack/RackTile.tsx +12 -2
- package/src/components/Results/Cell.tsx +2 -2
- package/src/components/Results/Result.tsx +4 -8
- package/src/components/Results/Results.module.scss +5 -3
- package/src/components/Solver/Solver.tsx +2 -2
- package/src/components/Tile/Tile.module.scss +4 -0
- package/src/components/Tooltip/Tooltip.module.scss +2 -0
- package/src/hooks/useAppLayout.ts +1 -0
- package/src/i18n/constants.ts +25 -8
- package/src/i18n/de.json +2 -2
- package/src/i18n/en.json +2 -2
- package/src/i18n/es.json +2 -2
- package/src/i18n/fa.json +2 -2
- package/src/i18n/fr.json +2 -2
- package/src/i18n/pl.json +2 -2
- package/src/lib/dataUrlToBlob.ts +20 -0
- package/src/lib/index.ts +2 -0
- package/src/lib/isCtrl.ts +7 -0
- package/src/modals/KeyMapModal/KeyMapModal.tsx +20 -4
- package/src/modals/KeyMapModal/components/Mapping/Mapping.module.scss +10 -4
- package/src/pages/_app.tsx +1 -3
- package/src/parameters/index.ts +33 -2
- package/src/state/index.ts +1 -1
- package/src/state/sagas.ts +3 -4
- package/src/state/store.ts +34 -0
- package/src/styles/variables.scss +4 -0
- package/.next/static/Jmk00rVXCbdjFgP77tKXQ/_buildManifest.js +0 -1
- package/.next/static/chunks/framework-2c79e2a64abdb08b.js +0 -33
- package/.next/static/chunks/pages/_app-b4fa92112b8f0385.js +0 -28
- package/.next/static/chunks/pages/index-ccd762f8f5028729.js +0 -1
- package/.next/static/css/1cd302e7648d209c.css +0 -2
- package/.next/static/css/34adfcf12a7d9bb6.css +0 -1
- package/src/components/Board/components/Cell/CellPure.tsx +0 -93
- package/src/components/Board/components/Cell/lib.ts +0 -59
- package/src/state/createAppStore.ts +0 -38
- /package/.next/static/{Jmk00rVXCbdjFgP77tKXQ → 5ttGCAW8jcIKxpR8om9fK}/_ssgManifest.js +0 -0
|
@@ -17,7 +17,7 @@ import { useDispatch } from 'react-redux';
|
|
|
17
17
|
|
|
18
18
|
import { useLatest } from 'hooks';
|
|
19
19
|
import { LOCALE_FEATURES } from 'i18n';
|
|
20
|
-
import { createGridOf, createKeyboardNavigation, extractCharacters, extractInputValue } from 'lib';
|
|
20
|
+
import { createGridOf, createKeyboardNavigation, extractCharacters, extractInputValue, isCtrl } from 'lib';
|
|
21
21
|
import { boardSlice, selectConfig, selectLocale, useTypedSelector } from 'state';
|
|
22
22
|
import { Direction, Point } from 'types';
|
|
23
23
|
|
|
@@ -290,6 +290,22 @@ const useGrid = (rows: Cell[][]): [State, Actions] => {
|
|
|
290
290
|
|
|
291
291
|
const { x, y } = position;
|
|
292
292
|
const character = event.key.toLowerCase();
|
|
293
|
+
const isTogglingBlank = isCtrl(event) && character === 'b';
|
|
294
|
+
const twoCharacterTile = config.getTwoCharacterTileByPrefix(character);
|
|
295
|
+
|
|
296
|
+
if (isTogglingBlank) {
|
|
297
|
+
event.preventDefault();
|
|
298
|
+
dispatch(boardSlice.actions.toggleCellIsBlank(position));
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (isCtrl(event) && twoCharacterTile) {
|
|
303
|
+
event.preventDefault();
|
|
304
|
+
dispatch(boardSlice.actions.changeCellValue({ x, y, value: twoCharacterTile }));
|
|
305
|
+
moveFocus(1);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
293
309
|
const cell = rows[y][x];
|
|
294
310
|
const twoCharacterCandidate = cell.tile.character + character;
|
|
295
311
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { BONUS_WORD } from '@scrabble-solver/constants';
|
|
2
|
+
import { Bonus } from '@scrabble-solver/types';
|
|
3
|
+
|
|
4
|
+
import { COLOR_BONUS_CHARACTER, COLOR_BONUS_CHARACTER_MULTIPLIER, COLOR_BONUS_WORD } from 'parameters';
|
|
5
|
+
|
|
6
|
+
const getBonusColor = (bonus: Bonus): string => {
|
|
7
|
+
if (bonus.type === BONUS_WORD) {
|
|
8
|
+
return COLOR_BONUS_WORD[bonus.multiplier];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (bonus.score) {
|
|
12
|
+
return COLOR_BONUS_CHARACTER[bonus.score];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return COLOR_BONUS_CHARACTER_MULTIPLIER[bonus.multiplier];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default getBonusColor;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { COMMA_ARABIC, COMMA_LATIN } from '@scrabble-solver/constants';
|
|
2
1
|
import classNames from 'classnames';
|
|
3
2
|
import { ChangeEvent, FormEvent, FunctionComponent } from 'react';
|
|
4
3
|
import { useDispatch } from 'react-redux';
|
|
5
4
|
|
|
6
|
-
import {
|
|
5
|
+
import { LOCALE_FEATURES } from 'i18n';
|
|
6
|
+
import { dictionarySlice, selectDictionary, selectLocale, useTranslate, useTypedSelector } from 'state';
|
|
7
7
|
|
|
8
8
|
import styles from './DictionaryInput.module.scss';
|
|
9
9
|
|
|
@@ -14,7 +14,9 @@ interface Props {
|
|
|
14
14
|
const DictionaryInput: FunctionComponent<Props> = ({ className }) => {
|
|
15
15
|
const dispatch = useDispatch();
|
|
16
16
|
const translate = useTranslate();
|
|
17
|
+
const locale = useTypedSelector(selectLocale);
|
|
17
18
|
const { input } = useTypedSelector(selectDictionary);
|
|
19
|
+
const { comma } = LOCALE_FEATURES[locale];
|
|
18
20
|
|
|
19
21
|
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
20
22
|
dispatch(dictionarySlice.actions.changeInput(event.target.value));
|
|
@@ -29,7 +31,7 @@ const DictionaryInput: FunctionComponent<Props> = ({ className }) => {
|
|
|
29
31
|
<form className={classNames(styles.dictionaryInput, className)} onSubmit={handleSubmit}>
|
|
30
32
|
<input
|
|
31
33
|
className={styles.input}
|
|
32
|
-
pattern={`.*[^\\s${
|
|
34
|
+
pattern={`.*[^\\s${comma}].*`}
|
|
33
35
|
placeholder={translate('dictionary.input.placeholder')}
|
|
34
36
|
required
|
|
35
37
|
title={translate('dictionary.input.title')}
|
|
@@ -1,19 +1,15 @@
|
|
|
1
|
-
$key-size: 36px;
|
|
2
|
-
$icon-size: 15px;
|
|
3
|
-
|
|
4
1
|
.key {
|
|
5
2
|
position: relative;
|
|
6
|
-
top: calc(0px - 2 * var(--border--width));
|
|
7
3
|
display: inline-block;
|
|
8
|
-
min-width:
|
|
9
|
-
height:
|
|
4
|
+
min-width: var(--key--height);
|
|
5
|
+
height: var(--key--height);
|
|
10
6
|
padding: var(--spacing--s) var(--spacing--m);
|
|
11
7
|
background-color: var(--color--white);
|
|
12
8
|
border: var(--border);
|
|
13
9
|
border-radius: var(--border--radius);
|
|
14
10
|
box-shadow: var(--box-shadow);
|
|
15
11
|
font-family: var(--font--family--monospace);
|
|
16
|
-
line-height: calc(
|
|
12
|
+
line-height: calc(var(--key--height) - 2 * (var(--spacing--s) + var(--border--width)));
|
|
17
13
|
vertical-align: middle;
|
|
18
14
|
text-align: center;
|
|
19
15
|
white-space: nowrap;
|
|
@@ -24,9 +20,9 @@ $icon-size: 15px;
|
|
|
24
20
|
|
|
25
21
|
:global(svg) {
|
|
26
22
|
position: absolute;
|
|
27
|
-
top: calc(
|
|
28
|
-
left: calc(
|
|
29
|
-
width:
|
|
30
|
-
height:
|
|
23
|
+
top: calc((var(--key--height) - var(--key--icon--size)) / 2 - var(--border--width));
|
|
24
|
+
left: calc((var(--key--height) - var(--key--icon--size)) / 2 - var(--border--width));
|
|
25
|
+
width: var(--key--icon--size);
|
|
26
|
+
height: var(--key--icon--size);
|
|
31
27
|
}
|
|
32
28
|
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
extractCharacters,
|
|
10
10
|
extractInputValue,
|
|
11
11
|
getTileSizes,
|
|
12
|
+
isCtrl,
|
|
12
13
|
zipCharactersAndTiles,
|
|
13
14
|
} from 'lib';
|
|
14
15
|
import { rackSlice, selectConfig, selectLocale, selectRack, selectResultCandidateTiles, useTypedSelector } from 'state';
|
|
@@ -100,7 +101,9 @@ const Rack: FunctionComponent<Props> = ({ className, tileSize }) => {
|
|
|
100
101
|
changeActiveIndex(1);
|
|
101
102
|
},
|
|
102
103
|
onKeyDown: (event) => {
|
|
103
|
-
if (event
|
|
104
|
+
if (isCtrl(event) && config.isTwoCharacterTilePrefix(event.key)) {
|
|
105
|
+
changeActiveIndex(1);
|
|
106
|
+
} else if (event.currentTarget.value === event.key) {
|
|
104
107
|
// change event did not fire because the same character was typed over the current one
|
|
105
108
|
// but we still want to move the caret
|
|
106
109
|
event.preventDefault();
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from 'react';
|
|
14
14
|
import { useDispatch } from 'react-redux';
|
|
15
15
|
|
|
16
|
-
import { createKeyboardNavigation, extractCharacters, extractInputValue } from 'lib';
|
|
16
|
+
import { createKeyboardNavigation, extractCharacters, extractInputValue, isCtrl } from 'lib';
|
|
17
17
|
import {
|
|
18
18
|
rackSlice,
|
|
19
19
|
selectCharacterIsValid,
|
|
@@ -80,7 +80,17 @@ const RackTile: FunctionComponent<Props> = ({
|
|
|
80
80
|
event.preventDefault();
|
|
81
81
|
dispatch(rackSlice.actions.changeCharacter({ character: null, index }));
|
|
82
82
|
},
|
|
83
|
-
onKeyDown
|
|
83
|
+
onKeyDown: (event) => {
|
|
84
|
+
if (isCtrl(event) && config.isTwoCharacterTilePrefix(event.key)) {
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
event.stopPropagation();
|
|
87
|
+
const twoTilesCharacter = config.getTwoCharacterTileByPrefix(event.key);
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
89
|
+
dispatch(rackSlice.actions.changeCharacter({ character: twoTilesCharacter!, index }));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
onKeyDown(event);
|
|
93
|
+
},
|
|
84
94
|
});
|
|
85
95
|
}, [index, onKeyDown]);
|
|
86
96
|
|
|
@@ -22,9 +22,9 @@ const Cell: FunctionComponent<Props> = ({ className, translationKey, tooltip, va
|
|
|
22
22
|
const triggerProps = useTooltip(`${translate(translationKey)}: ${tooltip || formattedValue}`);
|
|
23
23
|
|
|
24
24
|
return (
|
|
25
|
-
<
|
|
25
|
+
<div className={classNames(styles.cell, className)} {...triggerProps}>
|
|
26
26
|
{formattedValue}
|
|
27
|
-
</
|
|
27
|
+
</div>
|
|
28
28
|
);
|
|
29
29
|
};
|
|
30
30
|
|
|
@@ -30,10 +30,10 @@ const Result = ({ data, index, style }: Props): ReactElement => {
|
|
|
30
30
|
const ref = useRef<HTMLButtonElement>(null);
|
|
31
31
|
const columns = useColumns();
|
|
32
32
|
const locale = useTypedSelector(selectLocale);
|
|
33
|
-
const { consonants, vowels } = LOCALE_FEATURES[locale];
|
|
33
|
+
const { consonants, direction, separator, vowels } = LOCALE_FEATURES[locale];
|
|
34
34
|
const result = results[index];
|
|
35
35
|
const isMatching = useTypedSelector((state) => selectIsResultMatching(state, index));
|
|
36
|
-
const
|
|
36
|
+
const words = direction === 'rtl' ? [...result.words].reverse() : result.words;
|
|
37
37
|
const enabledColumns = Object.fromEntries(columns.map((column) => [column.id, true]));
|
|
38
38
|
|
|
39
39
|
const handleClick: MouseEventHandler = (event) => onClick(result, event);
|
|
@@ -60,11 +60,7 @@ const Result = ({ data, index, style }: Props): ReactElement => {
|
|
|
60
60
|
>
|
|
61
61
|
<span className={styles.resultContent}>
|
|
62
62
|
{enabledColumns[ResultColumn.Word] && (
|
|
63
|
-
<Cell
|
|
64
|
-
className={styles.word}
|
|
65
|
-
translationKey="common.word"
|
|
66
|
-
value={`${result.word.toLocaleUpperCase()}${otherWords.length > 0 ? ` (${otherWords})` : ''}`}
|
|
67
|
-
/>
|
|
63
|
+
<Cell className={styles.word} translationKey="common.word" value={result.word} />
|
|
68
64
|
)}
|
|
69
65
|
|
|
70
66
|
{enabledColumns[ResultColumn.TilesCount] && (
|
|
@@ -87,7 +83,7 @@ const Result = ({ data, index, style }: Props): ReactElement => {
|
|
|
87
83
|
<Cell
|
|
88
84
|
className={styles.stat}
|
|
89
85
|
translationKey="common.words"
|
|
90
|
-
tooltip={`${result.wordsCount} (${
|
|
86
|
+
tooltip={`${result.wordsCount.toLocaleString(locale)} (${words.join(separator)})`}
|
|
91
87
|
value={result.wordsCount}
|
|
92
88
|
/>
|
|
93
89
|
)}
|
|
@@ -60,7 +60,6 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
|
|
|
60
60
|
cursor: pointer;
|
|
61
61
|
text-transform: uppercase;
|
|
62
62
|
transition: var(--transition);
|
|
63
|
-
padding: var(--spacing--s) 0;
|
|
64
63
|
|
|
65
64
|
&:focus,
|
|
66
65
|
&:hover {
|
|
@@ -135,6 +134,7 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
|
|
|
135
134
|
display: flex;
|
|
136
135
|
align-items: center;
|
|
137
136
|
justify-content: space-between;
|
|
137
|
+
height: 100%;
|
|
138
138
|
|
|
139
139
|
.word {
|
|
140
140
|
@include ellipsis;
|
|
@@ -145,8 +145,10 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
|
|
|
145
145
|
display: flex;
|
|
146
146
|
align-items: center;
|
|
147
147
|
justify-content: center;
|
|
148
|
+
height: 100%;
|
|
148
149
|
padding: 0 var(--spacing--s);
|
|
149
150
|
gap: var(--spacing--s);
|
|
151
|
+
line-height: var(--results--item--height);
|
|
150
152
|
|
|
151
153
|
.result &:first-child,
|
|
152
154
|
.headerButton:first-child & {
|
|
@@ -180,12 +182,12 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
|
|
|
180
182
|
}
|
|
181
183
|
|
|
182
184
|
.word {
|
|
183
|
-
flex: 1 0
|
|
185
|
+
flex: 1 0;
|
|
184
186
|
text-transform: uppercase;
|
|
185
187
|
}
|
|
186
188
|
|
|
187
189
|
.stat {
|
|
188
|
-
$width:
|
|
190
|
+
$width: 55px;
|
|
189
191
|
|
|
190
192
|
flex: 0 0 $width;
|
|
191
193
|
max-width: $width;
|
|
@@ -34,7 +34,7 @@ const Solver: FunctionComponent<Props> = ({ className, onShowResults }) => {
|
|
|
34
34
|
const dispatch = useDispatch();
|
|
35
35
|
const translate = useTranslate();
|
|
36
36
|
const isTouchDevice = useIsTouchDevice();
|
|
37
|
-
const {
|
|
37
|
+
const { maxControlsWidth, showCompactControls, showFloatingSolveButton, tileSize } = useAppLayout();
|
|
38
38
|
const error = useTypedSelector(selectSolveError);
|
|
39
39
|
const isOutdated = useTypedSelector(selectAreResultsOutdated);
|
|
40
40
|
const resultCandidate = useTypedSelector(selectResultCandidate);
|
|
@@ -93,7 +93,7 @@ const Solver: FunctionComponent<Props> = ({ className, onShowResults }) => {
|
|
|
93
93
|
<div className={styles.container}>
|
|
94
94
|
<div className={styles.content}>
|
|
95
95
|
<form className={styles.boardContainer} onSubmit={handleSubmit}>
|
|
96
|
-
<Board
|
|
96
|
+
<Board className={styles.board} />
|
|
97
97
|
<input className={styles.submitInput} tabIndex={-1} type="submit" />
|
|
98
98
|
</form>
|
|
99
99
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
.tooltip {
|
|
2
|
+
max-width: var(--tooltip--max-width);
|
|
2
3
|
padding: var(--spacing--s) var(--spacing--m);
|
|
3
4
|
box-shadow: var(--box-shadow);
|
|
4
5
|
border-radius: var(--border--radius);
|
|
5
6
|
background-color: var(--color--tooltip--background);
|
|
6
7
|
color: var(--color--tooltip--foreground);
|
|
7
8
|
z-index: var(--z-index--tooltip);
|
|
9
|
+
text-align: center;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
.arrow {
|
package/src/i18n/constants.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { COMMA_ARABIC, COMMA_LATIN } from '@scrabble-solver/constants';
|
|
1
2
|
import { Locale } from '@scrabble-solver/types';
|
|
2
3
|
import { FunctionComponent, SVGAttributes } from 'react';
|
|
3
4
|
|
|
@@ -6,45 +7,61 @@ import { FlagDe, FlagEs, FlagFa, FlagFr, FlagGb, FlagPl, FlagUs } from 'icons';
|
|
|
6
7
|
import styles from './i18n.module.scss';
|
|
7
8
|
|
|
8
9
|
interface LocaleFeatures {
|
|
9
|
-
|
|
10
|
+
comma: string;
|
|
10
11
|
consonants: boolean;
|
|
12
|
+
direction: 'ltr' | 'rtl';
|
|
13
|
+
separator: string;
|
|
11
14
|
vowels: boolean;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
export const LOCALE_FEATURES: Record<Locale, LocaleFeatures> = {
|
|
15
18
|
[Locale.DE_DE]: {
|
|
16
|
-
|
|
19
|
+
comma: COMMA_LATIN,
|
|
17
20
|
consonants: true,
|
|
21
|
+
direction: 'ltr',
|
|
22
|
+
separator: `${COMMA_LATIN} `,
|
|
18
23
|
vowels: true,
|
|
19
24
|
},
|
|
20
25
|
[Locale.EN_GB]: {
|
|
21
|
-
|
|
26
|
+
comma: COMMA_LATIN,
|
|
22
27
|
consonants: true,
|
|
28
|
+
direction: 'ltr',
|
|
29
|
+
separator: `${COMMA_LATIN} `,
|
|
23
30
|
vowels: true,
|
|
24
31
|
},
|
|
25
32
|
[Locale.EN_US]: {
|
|
26
|
-
|
|
33
|
+
comma: COMMA_LATIN,
|
|
27
34
|
consonants: true,
|
|
35
|
+
direction: 'ltr',
|
|
36
|
+
separator: `${COMMA_LATIN} `,
|
|
28
37
|
vowels: true,
|
|
29
38
|
},
|
|
30
39
|
[Locale.ES_ES]: {
|
|
31
|
-
|
|
40
|
+
comma: COMMA_LATIN,
|
|
32
41
|
consonants: true,
|
|
42
|
+
direction: 'ltr',
|
|
43
|
+
separator: `${COMMA_LATIN} `,
|
|
33
44
|
vowels: true,
|
|
34
45
|
},
|
|
35
46
|
[Locale.FA_IR]: {
|
|
36
|
-
|
|
47
|
+
comma: COMMA_ARABIC,
|
|
37
48
|
consonants: false,
|
|
49
|
+
direction: 'rtl',
|
|
50
|
+
separator: `${COMMA_ARABIC} `,
|
|
38
51
|
vowels: false,
|
|
39
52
|
},
|
|
40
53
|
[Locale.FR_FR]: {
|
|
41
|
-
|
|
54
|
+
comma: COMMA_LATIN,
|
|
42
55
|
consonants: true,
|
|
56
|
+
direction: 'ltr',
|
|
57
|
+
separator: `${COMMA_LATIN} `,
|
|
43
58
|
vowels: true,
|
|
44
59
|
},
|
|
45
60
|
[Locale.PL_PL]: {
|
|
46
|
-
|
|
61
|
+
comma: COMMA_LATIN,
|
|
47
62
|
consonants: true,
|
|
63
|
+
direction: 'ltr',
|
|
64
|
+
separator: `${COMMA_LATIN} `,
|
|
48
65
|
vowels: true,
|
|
49
66
|
},
|
|
50
67
|
};
|
package/src/i18n/de.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"cell.filter-cell": "Zielort
|
|
2
|
+
"cell.filter-cell": "Zielort",
|
|
3
3
|
"cell.set-blank": "Als Blanko markieren",
|
|
4
4
|
"cell.set-not-blank": "Nicht als Blanko markieren",
|
|
5
5
|
"cell.tile.location": "Brett: Stein ({{x}}, {{y}})",
|
|
6
|
-
"cell.toggle-direction": "Schreibrichtung
|
|
6
|
+
"cell.toggle-direction": "Schreibrichtung",
|
|
7
7
|
"common.blanks": "Blankos",
|
|
8
8
|
"common.clear": "Löschen",
|
|
9
9
|
"common.close": "Schließen",
|
package/src/i18n/en.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"cell.filter-cell": "Target destination
|
|
2
|
+
"cell.filter-cell": "Target destination",
|
|
3
3
|
"cell.set-blank": "Mark it a blank",
|
|
4
4
|
"cell.set-not-blank": "Mark it not a blank",
|
|
5
5
|
"cell.tile.location": "Board: tile ({{x}}, {{y}})",
|
|
6
|
-
"cell.toggle-direction": "Typing direction
|
|
6
|
+
"cell.toggle-direction": "Typing direction",
|
|
7
7
|
"common.blanks": "Blanks",
|
|
8
8
|
"common.clear": "Clear",
|
|
9
9
|
"common.close": "Close",
|
package/src/i18n/es.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"cell.filter-cell": "Destino objetivo
|
|
2
|
+
"cell.filter-cell": "Destino objetivo",
|
|
3
3
|
"cell.set-blank": "Marcar como en blanco",
|
|
4
4
|
"cell.set-not-blank": "Marcar como no en blanco",
|
|
5
5
|
"cell.tile.location": "Tablero: espacio ({{x}}, {{y}})",
|
|
6
|
-
"cell.toggle-direction": "Dirección de escritura
|
|
6
|
+
"cell.toggle-direction": "Dirección de escritura",
|
|
7
7
|
"common.blanks": "Blancos",
|
|
8
8
|
"common.clear": "Borrar",
|
|
9
9
|
"common.close": "Cerrar",
|
package/src/i18n/fa.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"cell.filter-cell": "مقصد
|
|
2
|
+
"cell.filter-cell": "مقصد",
|
|
3
3
|
"cell.set-blank": "علامت گذاری به عنوان خالی",
|
|
4
4
|
"cell.set-not-blank": "علامت گذاری به عنوان غیر خالی",
|
|
5
|
-
"cell.toggle-direction": "جهت تایپ - کلیک برای تغییر",
|
|
6
5
|
"cell.tile.location": "({{x}}، {{y}}) کاشی: صفحه",
|
|
6
|
+
"cell.toggle-direction": "جهت تایپ",
|
|
7
7
|
"common.blanks": "خالی",
|
|
8
8
|
"common.clear": "پاک کردن",
|
|
9
9
|
"common.close": "بستن",
|
package/src/i18n/fr.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"cell.filter-cell": "Destination cible
|
|
2
|
+
"cell.filter-cell": "Destination cible",
|
|
3
3
|
"cell.set-blank": "Marquer comme vide",
|
|
4
4
|
"cell.set-not-blank": "Marquer comme non vide",
|
|
5
5
|
"cell.tile.location": "Plateau: la case ({{x}}, {{y}})",
|
|
6
|
-
"cell.toggle-direction": "Direction d'écriture
|
|
6
|
+
"cell.toggle-direction": "Direction d'écriture",
|
|
7
7
|
"common.blanks": "Cases vides",
|
|
8
8
|
"common.clear": "Effacer",
|
|
9
9
|
"common.close": "Fermer",
|
package/src/i18n/pl.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"cell.filter-cell": "Miejsce docelowe
|
|
2
|
+
"cell.filter-cell": "Miejsce docelowe",
|
|
3
3
|
"cell.set-blank": "Oznacz jako blank",
|
|
4
4
|
"cell.set-not-blank": "Oznacz jako nie blank",
|
|
5
5
|
"cell.tile.location": "Plansza: płytka ({{x}}, {{y}})",
|
|
6
|
-
"cell.toggle-direction": "Kierunek wpisywania
|
|
6
|
+
"cell.toggle-direction": "Kierunek wpisywania",
|
|
7
7
|
"common.blanks": "Blanki",
|
|
8
8
|
"common.clear": "Wyczyść",
|
|
9
9
|
"common.close": "Zamknij",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const dataUrlToBlob = (dataUrl: string): Blob => {
|
|
2
|
+
const [mime = '', data] = dataUrl.split(',');
|
|
3
|
+
const [, type] = mime.match(/:(.*?);/) || [];
|
|
4
|
+
|
|
5
|
+
if (typeof type !== 'string') {
|
|
6
|
+
throw new Error('Unsupported data URL');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const byteString = globalThis.atob(data);
|
|
10
|
+
const u8arr = new Uint8Array(byteString.length);
|
|
11
|
+
let index = byteString.length;
|
|
12
|
+
|
|
13
|
+
while (index--) {
|
|
14
|
+
u8arr[index] = byteString.charCodeAt(index);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return new Blob([u8arr], { type });
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default dataUrlToBlob;
|
package/src/lib/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ export { default as createKeyComparator } from './createKeyComparator';
|
|
|
8
8
|
export { default as createNullMovingComparator } from './createNullMovingComparator';
|
|
9
9
|
export { default as createRegExp } from './createRegExp';
|
|
10
10
|
export { default as createStringComparator } from './createStringComparator';
|
|
11
|
+
export { default as dataUrlToBlob } from './dataUrlToBlob';
|
|
11
12
|
export { default as detectLocale } from './detectLocale';
|
|
12
13
|
export { default as extractCharacters } from './extractCharacters';
|
|
13
14
|
export { default as extractInputValue } from './extractInputValue';
|
|
@@ -21,6 +22,7 @@ export { default as getTotalRemainingTilesCount } from './getTotalRemainingTiles
|
|
|
21
22
|
export { default as groupResults } from './groupResults';
|
|
22
23
|
export { default as guessLocale } from './guessLocale';
|
|
23
24
|
export { default as inverseDirection } from './inverseDirection';
|
|
25
|
+
export { default as isCtrl } from './isCtrl';
|
|
24
26
|
export { default as isMac } from './isMac';
|
|
25
27
|
export { default as isRegExp } from './isRegExp';
|
|
26
28
|
export { default as isStringArray } from './isStringArray';
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { FunctionComponent, memo } from 'react';
|
|
2
2
|
|
|
3
|
-
import { Modal } from 'components';
|
|
4
|
-
import { useTranslate } from 'state';
|
|
3
|
+
import { Key, Modal } from 'components';
|
|
4
|
+
import { selectConfig, useTranslate, useTypedSelector } from 'state';
|
|
5
5
|
|
|
6
6
|
import { Mapping } from './components';
|
|
7
|
-
import { ARROWS, BACKSPACE, DEL, ENTER, SPACE } from './keys';
|
|
7
|
+
import { ARROWS, BACKSPACE, CTRL, DEL, ENTER, SPACE } from './keys';
|
|
8
8
|
|
|
9
9
|
interface Props {
|
|
10
10
|
className?: string;
|
|
@@ -14,6 +14,7 @@ interface Props {
|
|
|
14
14
|
|
|
15
15
|
const KeyMapModal: FunctionComponent<Props> = ({ className, isOpen, onClose }) => {
|
|
16
16
|
const translate = useTranslate();
|
|
17
|
+
const config = useTypedSelector(selectConfig);
|
|
17
18
|
|
|
18
19
|
return (
|
|
19
20
|
<Modal className={className} isOpen={isOpen} title={translate('keyMap')} onClose={onClose}>
|
|
@@ -21,10 +22,25 @@ const KeyMapModal: FunctionComponent<Props> = ({ className, isOpen, onClose }) =
|
|
|
21
22
|
<Mapping description={translate('keyMap.board-and-rack.navigate')} mapping={[ARROWS]} />
|
|
22
23
|
<Mapping description={translate('keyMap.board-and-rack.remove-tile')} mapping={[DEL, BACKSPACE]} />
|
|
23
24
|
<Mapping description={translate('keyMap.board-and-rack.submit')} mapping={[ENTER]} />
|
|
25
|
+
{config.twoCharacterTiles.length > 0 && (
|
|
26
|
+
<Mapping
|
|
27
|
+
description={translate('keyMap.board-and-rack.insert-two-letter-tile')}
|
|
28
|
+
mapping={[
|
|
29
|
+
[
|
|
30
|
+
CTRL,
|
|
31
|
+
<>
|
|
32
|
+
{config.twoCharacterTiles.map(([firstLetter]) => (
|
|
33
|
+
<Key key={firstLetter}>{firstLetter.toUpperCase()}</Key>
|
|
34
|
+
))}
|
|
35
|
+
</>,
|
|
36
|
+
],
|
|
37
|
+
]}
|
|
38
|
+
/>
|
|
39
|
+
)}
|
|
24
40
|
</Modal.Section>
|
|
25
41
|
|
|
26
42
|
<Modal.Section title={translate('keyMap.board')}>
|
|
27
|
-
<Mapping description={translate('keyMap.board.toggle-blank')} mapping={[SPACE]} />
|
|
43
|
+
<Mapping description={translate('keyMap.board.toggle-blank')} mapping={[SPACE, [CTRL, <Key key="b">B</Key>]]} />
|
|
28
44
|
<Mapping description={translate('keyMap.board.toggle-direction')} mapping={[ARROWS]} />
|
|
29
45
|
</Modal.Section>
|
|
30
46
|
|
|
@@ -18,13 +18,19 @@
|
|
|
18
18
|
flex-wrap: wrap;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
.group
|
|
22
|
-
.plus {
|
|
21
|
+
.group {
|
|
23
22
|
margin: 0 var(--spacing--s);
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
.slash
|
|
27
|
-
|
|
25
|
+
.slash,
|
|
26
|
+
.plus {
|
|
27
|
+
min-width: var(--key--height);
|
|
28
|
+
height: var(--key--height);
|
|
29
|
+
padding: var(--spacing--s) var(--spacing--m);
|
|
30
|
+
font-family: var(--font--family--monospace);
|
|
31
|
+
line-height: calc(var(--key--height) - 2 * (var(--spacing--s) + var(--border--width)));
|
|
32
|
+
vertical-align: middle;
|
|
33
|
+
text-align: center;
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
.group {
|
package/src/pages/_app.tsx
CHANGED
|
@@ -4,7 +4,7 @@ import { FunctionComponent } from 'react';
|
|
|
4
4
|
import { Provider } from 'react-redux';
|
|
5
5
|
|
|
6
6
|
import { SeoMessage } from 'components';
|
|
7
|
-
import {
|
|
7
|
+
import { store } from 'state';
|
|
8
8
|
|
|
9
9
|
import 'styles/global.scss';
|
|
10
10
|
|
|
@@ -40,8 +40,6 @@ const KEYWORDS = [
|
|
|
40
40
|
'Kamil Mielnik',
|
|
41
41
|
].join(',');
|
|
42
42
|
|
|
43
|
-
const store = createAppStore();
|
|
44
|
-
|
|
45
43
|
const App: FunctionComponent<AppProps> = ({ Component, pageProps }) => (
|
|
46
44
|
<>
|
|
47
45
|
<Head>
|