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/README.md +1 -1
- package/lerna.json +1 -1
- package/package.json +1 -1
- package/packages/configs/package.json +3 -3
- package/packages/constants/package.json +1 -1
- package/packages/dictionaries/package-lock.json +2 -2
- package/packages/dictionaries/package.json +4 -4
- package/packages/logger/package-lock.json +2 -2
- package/packages/logger/package.json +1 -1
- package/packages/scrabble-solver/.next/BUILD_ID +1 -1
- package/packages/scrabble-solver/.next/build-manifest.json +11 -11
- package/packages/scrabble-solver/.next/cache/.tsbuildinfo +1 -1
- package/packages/scrabble-solver/.next/cache/eslint/.cache_8dgz12 +1 -1
- package/packages/scrabble-solver/.next/cache/next-server.js.nft.json +1 -1
- package/packages/scrabble-solver/.next/cache/webpack/client-production/0.pack +0 -0
- package/packages/scrabble-solver/.next/cache/webpack/client-production/index.pack +0 -0
- package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/0.pack +0 -0
- package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/packages/scrabble-solver/.next/cache/webpack/server-production/0.pack +0 -0
- package/packages/scrabble-solver/.next/cache/webpack/server-production/index.pack +0 -0
- package/packages/scrabble-solver/.next/next-server.js.nft.json +1 -1
- package/packages/scrabble-solver/.next/prerender-manifest.json +1 -1
- package/packages/scrabble-solver/.next/routes-manifest.json +1 -1
- package/packages/scrabble-solver/.next/server/chunks/277.js +653 -399
- package/packages/scrabble-solver/.next/server/chunks/865.js +153 -115
- package/packages/scrabble-solver/.next/server/middleware-build-manifest.js +1 -1
- package/packages/scrabble-solver/.next/server/pages/404.html +1 -1
- package/packages/scrabble-solver/.next/server/pages/404.js.nft.json +1 -1
- package/packages/scrabble-solver/.next/server/pages/500.html +1 -1
- package/packages/scrabble-solver/.next/server/pages/_app.js +9 -2
- package/packages/scrabble-solver/.next/server/pages/_app.js.nft.json +1 -1
- package/packages/scrabble-solver/.next/server/pages/api/solve.js +30 -1
- package/packages/scrabble-solver/.next/server/pages/index.html +1 -1
- package/packages/scrabble-solver/.next/server/pages/index.js +35 -6
- package/packages/scrabble-solver/.next/server/pages/index.js.nft.json +1 -1
- package/packages/scrabble-solver/.next/server/pages/index.json +1 -1
- package/packages/scrabble-solver/.next/server/pages-manifest.json +3 -3
- package/packages/scrabble-solver/.next/static/5ttGCAW8jcIKxpR8om9fK/_buildManifest.js +1 -0
- package/packages/scrabble-solver/.next/static/chunks/framework-2c5cac93e8c637b5.js +49 -0
- package/packages/scrabble-solver/.next/static/chunks/pages/{404-8176f4acd0cfeb42.js → 404-ca203fa27afc37d8.js} +1 -1
- package/packages/scrabble-solver/.next/static/chunks/pages/_app-76a8840b6244d5a2.js +28 -0
- package/packages/scrabble-solver/.next/static/chunks/pages/index-6894f40e6cac9243.js +1 -0
- package/packages/scrabble-solver/.next/static/css/af871fef886ef5b7.css +2 -0
- package/packages/scrabble-solver/.next/static/css/c6e0e01f44fc0425.css +1 -0
- package/packages/scrabble-solver/.next/trace +50 -50
- package/packages/scrabble-solver/package-lock.json +2 -2
- package/packages/scrabble-solver/package.json +8 -8
- package/packages/scrabble-solver/src/components/Board/Board.module.scss +2 -59
- package/packages/scrabble-solver/src/components/Board/Board.tsx +22 -10
- package/packages/scrabble-solver/src/components/Board/BoardPure.tsx +13 -8
- package/packages/scrabble-solver/src/components/Board/components/Cell/Cell.module.scss +8 -140
- package/packages/scrabble-solver/src/components/Board/components/Cell/Cell.tsx +18 -37
- package/packages/scrabble-solver/src/components/Board/hooks/index.ts +1 -0
- package/packages/scrabble-solver/src/components/Board/hooks/useBackgroundImage.tsx +174 -0
- package/packages/scrabble-solver/src/components/Board/hooks/useGrid.ts +17 -1
- package/packages/scrabble-solver/src/components/Board/lib/getBonusColor.ts +18 -0
- package/packages/scrabble-solver/src/components/Board/lib/index.ts +1 -0
- package/packages/scrabble-solver/src/components/DictionaryInput/DictionaryInput.tsx +5 -3
- package/packages/scrabble-solver/src/components/Key/Key.module.scss +7 -11
- package/packages/scrabble-solver/src/components/Rack/Rack.tsx +4 -1
- package/packages/scrabble-solver/src/components/Rack/RackTile.tsx +12 -2
- package/packages/scrabble-solver/src/components/Results/Cell.tsx +2 -2
- package/packages/scrabble-solver/src/components/Results/Result.tsx +4 -8
- package/packages/scrabble-solver/src/components/Results/Results.module.scss +5 -3
- package/packages/scrabble-solver/src/components/Solver/Solver.tsx +2 -2
- package/packages/scrabble-solver/src/components/Tile/Tile.module.scss +4 -0
- package/packages/scrabble-solver/src/components/Tooltip/Tooltip.module.scss +2 -0
- package/packages/scrabble-solver/src/hooks/useAppLayout.ts +1 -0
- package/packages/scrabble-solver/src/i18n/constants.ts +25 -8
- package/packages/scrabble-solver/src/i18n/de.json +2 -2
- package/packages/scrabble-solver/src/i18n/en.json +2 -2
- package/packages/scrabble-solver/src/i18n/es.json +2 -2
- package/packages/scrabble-solver/src/i18n/fa.json +2 -2
- package/packages/scrabble-solver/src/i18n/fr.json +2 -2
- package/packages/scrabble-solver/src/i18n/pl.json +2 -2
- package/packages/scrabble-solver/src/lib/dataUrlToBlob.ts +20 -0
- package/packages/scrabble-solver/src/lib/index.ts +2 -0
- package/packages/scrabble-solver/src/lib/isCtrl.ts +7 -0
- package/packages/scrabble-solver/src/modals/KeyMapModal/KeyMapModal.tsx +20 -4
- package/packages/scrabble-solver/src/modals/KeyMapModal/components/Mapping/Mapping.module.scss +10 -4
- package/packages/scrabble-solver/src/pages/_app.tsx +1 -3
- package/packages/scrabble-solver/src/parameters/index.ts +33 -2
- package/packages/scrabble-solver/src/state/index.ts +1 -1
- package/packages/scrabble-solver/src/state/sagas.ts +3 -4
- package/packages/scrabble-solver/src/state/store.ts +34 -0
- package/packages/scrabble-solver/src/styles/variables.scss +4 -0
- package/packages/solver/package-lock.json +2 -2
- package/packages/solver/package.json +5 -5
- package/packages/types/package.json +2 -2
- package/packages/word-definitions/package-lock.json +161 -11
- package/packages/word-definitions/package.json +2 -2
- package/packages/word-lists/package-lock.json +2 -2
- package/packages/word-lists/package.json +2 -2
- package/screencast.gif +0 -0
- package/img/screencast.gif +0 -0
- package/packages/scrabble-solver/.next/static/Jmk00rVXCbdjFgP77tKXQ/_buildManifest.js +0 -1
- package/packages/scrabble-solver/.next/static/chunks/framework-2c79e2a64abdb08b.js +0 -33
- package/packages/scrabble-solver/.next/static/chunks/pages/_app-b4fa92112b8f0385.js +0 -28
- package/packages/scrabble-solver/.next/static/chunks/pages/index-ccd762f8f5028729.js +0 -1
- package/packages/scrabble-solver/.next/static/css/1cd302e7648d209c.css +0 -2
- package/packages/scrabble-solver/.next/static/css/34adfcf12a7d9bb6.css +0 -1
- package/packages/scrabble-solver/src/components/Board/components/Cell/CellPure.tsx +0 -93
- package/packages/scrabble-solver/src/components/Board/components/Cell/lib.ts +0 -59
- package/packages/scrabble-solver/src/state/createAppStore.ts +0 -38
- /package/packages/scrabble-solver/.next/static/{Jmk00rVXCbdjFgP77tKXQ → 5ttGCAW8jcIKxpR8om9fK}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/* eslint-disable max-statements */
|
|
2
|
+
|
|
3
|
+
import { BONUS_CHARACTER, BONUS_WORD } from '@scrabble-solver/constants';
|
|
4
|
+
import { useMemo } from 'react';
|
|
5
|
+
import { renderToString } from 'react-dom/server';
|
|
6
|
+
import { Provider } from 'react-redux';
|
|
7
|
+
|
|
8
|
+
import { useAppLayout, useMediaQueries } from 'hooks';
|
|
9
|
+
import { FlagFill, Star } from 'icons';
|
|
10
|
+
import { dataUrlToBlob, getTileSizes } from 'lib';
|
|
11
|
+
import { BORDER_COLOR_LIGHT, BORDER_RADIUS, BORDER_WIDTH, COLOR_BONUS_START, COLOR_FILTERED } from 'parameters';
|
|
12
|
+
import { selectCellFilter, selectConfig, store, useTypedSelector } from 'state';
|
|
13
|
+
import { Point } from 'types';
|
|
14
|
+
|
|
15
|
+
import { getBonusColor } from '../lib';
|
|
16
|
+
|
|
17
|
+
const BORDER_RADIUS_XS = 3;
|
|
18
|
+
const GRID_LINE_SIZE = 1;
|
|
19
|
+
const HORIZONTAL_LINE = 'h';
|
|
20
|
+
const VERTICAL_LINE = 'v';
|
|
21
|
+
const BONUS = 'b';
|
|
22
|
+
const BONUS_WORD_2 = 'b2';
|
|
23
|
+
const BONUS_WORD_3 = 'b3';
|
|
24
|
+
const CELL_FILTER = 'c';
|
|
25
|
+
|
|
26
|
+
const useBackgroundImage = () => {
|
|
27
|
+
const { boardSize, cellSize } = useAppLayout();
|
|
28
|
+
const { isLessThanXs } = useMediaQueries();
|
|
29
|
+
const borderRadius = isLessThanXs ? BORDER_RADIUS_XS : BORDER_RADIUS;
|
|
30
|
+
const config = useTypedSelector(selectConfig);
|
|
31
|
+
const center = { x: Math.floor(config.boardWidth / 2), y: Math.floor(config.boardHeight / 2) };
|
|
32
|
+
const cellFilter = useTypedSelector(selectCellFilter);
|
|
33
|
+
const viewBoxHeight = boardSize;
|
|
34
|
+
const viewBoxWidth = boardSize;
|
|
35
|
+
const bonusSize = cellSize * 0.8;
|
|
36
|
+
const bonusOffset = cellSize * 0.1;
|
|
37
|
+
const iconSize = cellSize * 0.4;
|
|
38
|
+
const iconOffset = (cellSize - iconSize) / 2;
|
|
39
|
+
const { tileFontSize } = getTileSizes(cellSize);
|
|
40
|
+
const fontSize = tileFontSize * 0.6;
|
|
41
|
+
const fontOffset = cellSize / 2;
|
|
42
|
+
const characterBonuses = config.bonuses.filter((bonus) => bonus.type === BONUS_CHARACTER);
|
|
43
|
+
const word2Bonuses = config.bonuses.filter((bonus) => bonus.type === BONUS_WORD && bonus.multiplier === 2);
|
|
44
|
+
const word3Bonuses = config.bonuses.filter((bonus) => bonus.type === BONUS_WORD && bonus.multiplier === 3);
|
|
45
|
+
|
|
46
|
+
const getX = (point: Point): number => point.x * (cellSize + BORDER_WIDTH);
|
|
47
|
+
|
|
48
|
+
const getY = (point: Point): number => point.y * (cellSize + BORDER_WIDTH);
|
|
49
|
+
|
|
50
|
+
const backgroundSvg = renderToString(
|
|
51
|
+
<Provider store={store}>
|
|
52
|
+
<svg
|
|
53
|
+
height={viewBoxHeight}
|
|
54
|
+
viewBox={`0 0 ${viewBoxWidth} ${viewBoxHeight}`}
|
|
55
|
+
width={viewBoxWidth}
|
|
56
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
57
|
+
>
|
|
58
|
+
<defs>
|
|
59
|
+
<symbol id={HORIZONTAL_LINE}>
|
|
60
|
+
<rect fill={BORDER_COLOR_LIGHT} height={GRID_LINE_SIZE} width={viewBoxWidth} />
|
|
61
|
+
</symbol>
|
|
62
|
+
|
|
63
|
+
<symbol id={VERTICAL_LINE}>
|
|
64
|
+
<rect fill={BORDER_COLOR_LIGHT} height={viewBoxHeight} width={GRID_LINE_SIZE} />
|
|
65
|
+
</symbol>
|
|
66
|
+
|
|
67
|
+
<symbol id={BONUS}>
|
|
68
|
+
<rect height={bonusSize} rx={borderRadius} width={bonusSize} x={bonusOffset} y={bonusOffset} />
|
|
69
|
+
</symbol>
|
|
70
|
+
|
|
71
|
+
<symbol id={BONUS_WORD_2}>
|
|
72
|
+
<rect height={bonusSize} rx={borderRadius} width={bonusSize} x={bonusOffset} y={bonusOffset} />
|
|
73
|
+
|
|
74
|
+
<text
|
|
75
|
+
dominantBaseline="central"
|
|
76
|
+
fill="white"
|
|
77
|
+
fontFamily="system-ui, sans-serif"
|
|
78
|
+
fontSize={fontSize}
|
|
79
|
+
fontWeight="bold"
|
|
80
|
+
textAnchor="middle"
|
|
81
|
+
x={fontOffset}
|
|
82
|
+
y={fontOffset}
|
|
83
|
+
>
|
|
84
|
+
x2
|
|
85
|
+
</text>
|
|
86
|
+
</symbol>
|
|
87
|
+
|
|
88
|
+
<symbol id={BONUS_WORD_3}>
|
|
89
|
+
<rect height={bonusSize} rx={borderRadius} width={bonusSize} x={bonusOffset} y={bonusOffset} />
|
|
90
|
+
|
|
91
|
+
<text
|
|
92
|
+
dominantBaseline="central"
|
|
93
|
+
fill="white"
|
|
94
|
+
fontFamily="system-ui, sans-serif"
|
|
95
|
+
fontSize={fontSize}
|
|
96
|
+
fontWeight="bold"
|
|
97
|
+
textAnchor="middle"
|
|
98
|
+
x={fontOffset}
|
|
99
|
+
y={fontOffset}
|
|
100
|
+
>
|
|
101
|
+
x3
|
|
102
|
+
</text>
|
|
103
|
+
</symbol>
|
|
104
|
+
|
|
105
|
+
<symbol id={CELL_FILTER}>
|
|
106
|
+
<rect
|
|
107
|
+
fill={COLOR_FILTERED}
|
|
108
|
+
height={bonusSize}
|
|
109
|
+
rx={borderRadius}
|
|
110
|
+
width={bonusSize}
|
|
111
|
+
x={bonusOffset}
|
|
112
|
+
y={bonusOffset}
|
|
113
|
+
/>
|
|
114
|
+
|
|
115
|
+
<FlagFill color="white" height={iconSize} width={iconSize} x={iconOffset} y={iconOffset} />
|
|
116
|
+
</symbol>
|
|
117
|
+
</defs>
|
|
118
|
+
|
|
119
|
+
<rect fill="white" height={viewBoxHeight} rx={borderRadius} width={viewBoxWidth} x="0" y="0" />
|
|
120
|
+
|
|
121
|
+
{Array.from({ length: config.boardHeight - 1 }).map((_value, index) => (
|
|
122
|
+
<use key={index} href={`#${HORIZONTAL_LINE}`} y={(index + 1) * (cellSize + BORDER_WIDTH) - BORDER_WIDTH} />
|
|
123
|
+
))}
|
|
124
|
+
|
|
125
|
+
{Array.from({ length: config.boardWidth - 1 }).map((_value, index) => (
|
|
126
|
+
<use key={index} href={`#${VERTICAL_LINE}`} x={(index + 1) * (cellSize + BORDER_WIDTH) - BORDER_WIDTH} />
|
|
127
|
+
))}
|
|
128
|
+
|
|
129
|
+
{characterBonuses.map((bonus, index) => (
|
|
130
|
+
<use fill={getBonusColor(bonus)} key={index} href={`#${BONUS}`} x={getX(bonus)} y={getY(bonus)} />
|
|
131
|
+
))}
|
|
132
|
+
|
|
133
|
+
{word2Bonuses.map((bonus, index) => (
|
|
134
|
+
<use fill={getBonusColor(bonus)} key={index} href={`#${BONUS_WORD_2}`} x={getX(bonus)} y={getY(bonus)} />
|
|
135
|
+
))}
|
|
136
|
+
|
|
137
|
+
{word3Bonuses.map((bonus, index) => (
|
|
138
|
+
<use fill={getBonusColor(bonus)} key={index} href={`#${BONUS_WORD_3}`} x={getX(bonus)} y={getY(bonus)} />
|
|
139
|
+
))}
|
|
140
|
+
|
|
141
|
+
<rect
|
|
142
|
+
fill={COLOR_BONUS_START}
|
|
143
|
+
height={bonusSize}
|
|
144
|
+
rx={borderRadius}
|
|
145
|
+
width={bonusSize}
|
|
146
|
+
x={getX(center) + bonusOffset}
|
|
147
|
+
y={getY(center) + bonusOffset}
|
|
148
|
+
/>
|
|
149
|
+
|
|
150
|
+
<Star
|
|
151
|
+
color="white"
|
|
152
|
+
height={iconSize}
|
|
153
|
+
width={iconSize}
|
|
154
|
+
x={getX(center) + iconOffset}
|
|
155
|
+
y={getY(center) + iconOffset}
|
|
156
|
+
/>
|
|
157
|
+
|
|
158
|
+
{cellFilter.map((cell, index) => (
|
|
159
|
+
<use key={index} href={`#${CELL_FILTER}`} x={getX(cell)} y={getY(cell)} />
|
|
160
|
+
))}
|
|
161
|
+
</svg>
|
|
162
|
+
</Provider>,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const encodedSvg = globalThis.btoa(backgroundSvg);
|
|
166
|
+
const dataUrl = `data:image/svg+xml;base64,${encodedSvg}`;
|
|
167
|
+
const blob = useMemo(() => dataUrlToBlob(dataUrl), [dataUrl]);
|
|
168
|
+
const blobUrl = useMemo(() => URL.createObjectURL(blob), [blob]);
|
|
169
|
+
const url = `url(${blobUrl})`;
|
|
170
|
+
|
|
171
|
+
return url;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export default useBackgroundImage;
|
|
@@ -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 {
|
|
@@ -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
|
};
|
|
@@ -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",
|
|
@@ -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",
|
|
@@ -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",
|
|
@@ -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": "بستن",
|
|
@@ -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",
|
|
@@ -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;
|