@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.
Files changed (86) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +11 -11
  3. package/.next/cache/.tsbuildinfo +1 -1
  4. package/.next/cache/eslint/.cache_8dgz12 +1 -1
  5. package/.next/cache/next-server.js.nft.json +1 -1
  6. package/.next/cache/webpack/client-production/0.pack +0 -0
  7. package/.next/cache/webpack/client-production/index.pack +0 -0
  8. package/.next/cache/webpack/edge-server-production/0.pack +0 -0
  9. package/.next/cache/webpack/edge-server-production/index.pack +0 -0
  10. package/.next/cache/webpack/server-production/0.pack +0 -0
  11. package/.next/cache/webpack/server-production/index.pack +0 -0
  12. package/.next/next-server.js.nft.json +1 -1
  13. package/.next/prerender-manifest.json +1 -1
  14. package/.next/routes-manifest.json +1 -1
  15. package/.next/server/chunks/277.js +653 -399
  16. package/.next/server/chunks/865.js +153 -115
  17. package/.next/server/middleware-build-manifest.js +1 -1
  18. package/.next/server/pages/404.html +1 -1
  19. package/.next/server/pages/404.js.nft.json +1 -1
  20. package/.next/server/pages/500.html +1 -1
  21. package/.next/server/pages/_app.js +9 -2
  22. package/.next/server/pages/_app.js.nft.json +1 -1
  23. package/.next/server/pages/api/solve.js +30 -1
  24. package/.next/server/pages/index.html +1 -1
  25. package/.next/server/pages/index.js +35 -6
  26. package/.next/server/pages/index.js.nft.json +1 -1
  27. package/.next/server/pages/index.json +1 -1
  28. package/.next/server/pages-manifest.json +3 -3
  29. package/.next/static/5ttGCAW8jcIKxpR8om9fK/_buildManifest.js +1 -0
  30. package/.next/static/chunks/framework-2c5cac93e8c637b5.js +49 -0
  31. package/.next/static/chunks/pages/{404-8176f4acd0cfeb42.js → 404-ca203fa27afc37d8.js} +1 -1
  32. package/.next/static/chunks/pages/_app-76a8840b6244d5a2.js +28 -0
  33. package/.next/static/chunks/pages/index-6894f40e6cac9243.js +1 -0
  34. package/.next/static/css/af871fef886ef5b7.css +2 -0
  35. package/.next/static/css/c6e0e01f44fc0425.css +1 -0
  36. package/.next/trace +50 -50
  37. package/package.json +9 -9
  38. package/src/components/Board/Board.module.scss +2 -59
  39. package/src/components/Board/Board.tsx +22 -10
  40. package/src/components/Board/BoardPure.tsx +13 -8
  41. package/src/components/Board/components/Cell/Cell.module.scss +8 -140
  42. package/src/components/Board/components/Cell/Cell.tsx +18 -37
  43. package/src/components/Board/hooks/index.ts +1 -0
  44. package/src/components/Board/hooks/useBackgroundImage.tsx +174 -0
  45. package/src/components/Board/hooks/useGrid.ts +17 -1
  46. package/src/components/Board/lib/getBonusColor.ts +18 -0
  47. package/src/components/Board/lib/index.ts +1 -0
  48. package/src/components/DictionaryInput/DictionaryInput.tsx +5 -3
  49. package/src/components/Key/Key.module.scss +7 -11
  50. package/src/components/Rack/Rack.tsx +4 -1
  51. package/src/components/Rack/RackTile.tsx +12 -2
  52. package/src/components/Results/Cell.tsx +2 -2
  53. package/src/components/Results/Result.tsx +4 -8
  54. package/src/components/Results/Results.module.scss +5 -3
  55. package/src/components/Solver/Solver.tsx +2 -2
  56. package/src/components/Tile/Tile.module.scss +4 -0
  57. package/src/components/Tooltip/Tooltip.module.scss +2 -0
  58. package/src/hooks/useAppLayout.ts +1 -0
  59. package/src/i18n/constants.ts +25 -8
  60. package/src/i18n/de.json +2 -2
  61. package/src/i18n/en.json +2 -2
  62. package/src/i18n/es.json +2 -2
  63. package/src/i18n/fa.json +2 -2
  64. package/src/i18n/fr.json +2 -2
  65. package/src/i18n/pl.json +2 -2
  66. package/src/lib/dataUrlToBlob.ts +20 -0
  67. package/src/lib/index.ts +2 -0
  68. package/src/lib/isCtrl.ts +7 -0
  69. package/src/modals/KeyMapModal/KeyMapModal.tsx +20 -4
  70. package/src/modals/KeyMapModal/components/Mapping/Mapping.module.scss +10 -4
  71. package/src/pages/_app.tsx +1 -3
  72. package/src/parameters/index.ts +33 -2
  73. package/src/state/index.ts +1 -1
  74. package/src/state/sagas.ts +3 -4
  75. package/src/state/store.ts +34 -0
  76. package/src/styles/variables.scss +4 -0
  77. package/.next/static/Jmk00rVXCbdjFgP77tKXQ/_buildManifest.js +0 -1
  78. package/.next/static/chunks/framework-2c79e2a64abdb08b.js +0 -33
  79. package/.next/static/chunks/pages/_app-b4fa92112b8f0385.js +0 -28
  80. package/.next/static/chunks/pages/index-ccd762f8f5028729.js +0 -1
  81. package/.next/static/css/1cd302e7648d209c.css +0 -2
  82. package/.next/static/css/34adfcf12a7d9bb6.css +0 -1
  83. package/src/components/Board/components/Cell/CellPure.tsx +0 -93
  84. package/src/components/Board/components/Cell/lib.ts +0 -59
  85. package/src/state/createAppStore.ts +0 -38
  86. /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 +1,2 @@
1
+ export { default as getBonusColor } from './getBonusColor';
1
2
  export { default as getPositionInGrid } from './getPositionInGrid';
@@ -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 { dictionarySlice, selectDictionary, useTranslate, useTypedSelector } from 'state';
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${COMMA_ARABIC}${COMMA_LATIN}].*`}
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: $key-size;
9
- height: $key-size;
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(#{$key-size} - 2 * (var(--spacing--s) + var(--border--width)));
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(#{($key-size - $icon-size) / 2} - var(--border--width));
28
- left: calc(#{($key-size - $icon-size) / 2} - var(--border--width));
29
- width: $icon-size;
30
- height: $icon-size;
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.currentTarget.value === event.key) {
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
- <span className={classNames(styles.cell, className)} {...triggerProps}>
25
+ <div className={classNames(styles.cell, className)} {...triggerProps}>
26
26
  {formattedValue}
27
- </span>
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 otherWords = result.words.slice(1).join(' / ').toLocaleUpperCase();
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} (${result.words.join(' / ').toLocaleUpperCase()})`}
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 180px;
185
+ flex: 1 0;
184
186
  text-transform: uppercase;
185
187
  }
186
188
 
187
189
  .stat {
188
- $width: 50px;
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 { cellSize, maxControlsWidth, showCompactControls, showFloatingSolveButton, tileSize } = useAppLayout();
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 cellSize={cellSize} className={styles.board} />
96
+ <Board className={styles.board} />
97
97
  <input className={styles.submitInput} tabIndex={-1} type="submit" />
98
98
  </form>
99
99
 
@@ -17,6 +17,10 @@
17
17
  transition-property: background-color, color, box-shadow;
18
18
  user-select: none;
19
19
 
20
+ @include media('<xs') {
21
+ --border--radius: 3px;
22
+ }
23
+
20
24
  &.points1 {
21
25
  --background-color: var(--color--yellow);
22
26
  }
@@ -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 {
@@ -60,6 +60,7 @@ const useAppLayout = () => {
60
60
  return {
61
61
  actionsWidth: 2 * BUTTON_HEIGHT - BORDER_WIDTH,
62
62
  animateTile: !isLessThanXs,
63
+ boardSize,
63
64
  cellSize,
64
65
  dictionaryHeight,
65
66
  isModalFullWidth: isLessThanS,
@@ -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
- direction: 'ltr' | 'rtl';
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
- direction: 'ltr',
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
- direction: 'ltr',
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
- direction: 'ltr',
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
- direction: 'ltr',
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
- direction: 'rtl',
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
- direction: 'ltr',
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
- direction: 'ltr',
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 - klicken zum Wechseln",
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 - klicken zum Wechseln",
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 - click to toggle",
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 - click to toggle",
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: haga clic para alternar",
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: haga clic para alternar",
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 - cliquer pour changer",
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 - cliquer pour changer",
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 - kliknij aby zmienić",
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 - kliknij aby zmienić",
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';
@@ -0,0 +1,7 @@
1
+ import { KeyboardEvent } from 'react';
2
+
3
+ const isCtrl = <T>(event: KeyboardEvent<T> | globalThis.KeyboardEvent): boolean => {
4
+ return event.ctrlKey || event.metaKey;
5
+ };
6
+
7
+ export default isCtrl;
@@ -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
- margin: 0 var(--spacing--m);
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 {
@@ -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 { createAppStore } from 'state';
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>