@scrabble-solver/scrabble-solver 2.11.8 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +7 -7
  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/131.js +153 -115
  16. package/.next/server/chunks/277.js +1426 -718
  17. package/.next/server/chunks/44.js +6 -3
  18. package/.next/server/chunks/50.js +20 -78
  19. package/.next/server/chunks/911.js +14 -14
  20. package/.next/server/middleware-build-manifest.js +1 -1
  21. package/.next/server/pages/404.html +1 -1
  22. package/.next/server/pages/404.js.nft.json +1 -1
  23. package/.next/server/pages/500.html +1 -1
  24. package/.next/server/pages/_app.js +16 -0
  25. package/.next/server/pages/_app.js.nft.json +1 -1
  26. package/.next/server/pages/_document.js.nft.json +1 -1
  27. package/.next/server/pages/api/solve.js +43 -11
  28. package/.next/server/pages/index.html +1 -1
  29. package/.next/server/pages/index.js +152 -11
  30. package/.next/server/pages/index.js.nft.json +1 -1
  31. package/.next/server/pages/index.json +1 -1
  32. package/.next/server/pages-manifest.json +2 -2
  33. package/.next/static/chunks/pages/{404-ca203fa27afc37d8.js → 404-b4b5ce15153d4825.js} +1 -1
  34. package/.next/static/chunks/pages/_app-bea4539a6b8042de.js +32 -0
  35. package/.next/static/chunks/pages/index-4e8566409753e1c3.js +1 -0
  36. package/.next/static/css/58053f9594647860.css +2 -0
  37. package/.next/static/css/{c6e0e01f44fc0425.css → 60e8258da7362a1a.css} +1 -1
  38. package/.next/static/fsjQvvJ13WNxBdMioL4sc/_buildManifest.js +1 -0
  39. package/.next/trace +52 -50
  40. package/package.json +16 -13
  41. package/src/components/Board/Board.module.scss +18 -4
  42. package/src/components/Board/Board.tsx +145 -76
  43. package/src/components/Board/BoardPure.tsx +32 -40
  44. package/src/components/Board/components/Actions/Actions.module.scss +6 -17
  45. package/src/components/Board/components/Actions/Actions.tsx +36 -18
  46. package/src/components/Board/components/Cell/Cell.module.scss +12 -13
  47. package/src/components/Board/components/Cell/Cell.tsx +53 -3
  48. package/src/components/Board/components/InputPrompt/InputPrompt.module.scss +48 -0
  49. package/src/components/Board/components/InputPrompt/InputPrompt.tsx +81 -0
  50. package/src/components/Board/components/InputPrompt/index.ts +1 -0
  51. package/src/components/Board/components/ToggleDirectionButton/ToggleDirectionButton.module.scss +21 -0
  52. package/src/components/Board/components/ToggleDirectionButton/ToggleDirectionButton.tsx +34 -0
  53. package/src/components/Board/components/ToggleDirectionButton/index.ts +1 -0
  54. package/src/components/Board/components/index.ts +2 -0
  55. package/src/components/Board/hooks/index.ts +4 -0
  56. package/src/components/Board/hooks/useBoardStyle.ts +27 -0
  57. package/src/components/Board/hooks/useFloatingActions.ts +22 -0
  58. package/src/components/Board/hooks/useFloatingFocus.ts +10 -0
  59. package/src/components/Board/hooks/useFloatingInputPrompt.ts +19 -0
  60. package/src/components/Board/hooks/useGrid.ts +2 -1
  61. package/src/components/NavButtons/NavButtons.tsx +2 -2
  62. package/src/components/Rack/Rack.module.scss +6 -6
  63. package/src/components/Rack/Rack.tsx +98 -23
  64. package/src/components/Rack/components/InputPrompt/InputPrompt.module.scss +22 -0
  65. package/src/components/Rack/components/InputPrompt/InputPrompt.tsx +89 -0
  66. package/src/components/Rack/components/InputPrompt/index.ts +1 -0
  67. package/src/components/Rack/components/RackTile/RackTile.module.scss +11 -0
  68. package/src/components/Rack/{RackTile.tsx → components/RackTile/RackTile.tsx} +47 -7
  69. package/src/components/Rack/components/RackTile/index.ts +1 -0
  70. package/src/components/Rack/components/index.ts +2 -0
  71. package/src/components/Radio/Radio.module.scss +0 -8
  72. package/src/components/Results/Cell.tsx +4 -3
  73. package/src/components/Results/Result.tsx +6 -2
  74. package/src/components/Results/Results.module.scss +6 -0
  75. package/src/components/Solver/Solver.module.scss +0 -20
  76. package/src/components/Solver/Solver.tsx +2 -4
  77. package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.tsx +2 -10
  78. package/src/components/Solver/components/index.ts +0 -1
  79. package/src/components/Tile/Tile.module.scss +1 -0
  80. package/src/components/Tile/Tile.tsx +8 -6
  81. package/src/components/Tile/TilePure.tsx +8 -0
  82. package/src/hooks/useAppLayout.ts +3 -1
  83. package/src/hooks/useLocalStorage.ts +8 -0
  84. package/src/i18n/de.json +6 -1
  85. package/src/i18n/en.json +6 -1
  86. package/src/i18n/es.json +6 -1
  87. package/src/i18n/fa.json +6 -1
  88. package/src/i18n/fr.json +6 -1
  89. package/src/i18n/pl.json +6 -1
  90. package/src/icons/Keyboard.svg +4 -3
  91. package/src/icons/KeyboardFill.svg +4 -0
  92. package/src/icons/index.ts +1 -0
  93. package/src/lib/extractCharacters.test.ts +26 -0
  94. package/src/lib/extractCharacters.ts +11 -9
  95. package/src/lib/extractCharactersByCase.test.ts +31 -0
  96. package/src/lib/extractCharactersByCase.ts +31 -0
  97. package/src/lib/index.ts +3 -1
  98. package/src/lib/isUpperCase.ts +7 -0
  99. package/src/modals/SettingsModal/SettingsModal.tsx +5 -1
  100. package/src/modals/SettingsModal/components/InputModeSetting/InputModeSetting.module.scss +12 -0
  101. package/src/modals/SettingsModal/components/InputModeSetting/InputModeSetting.tsx +55 -0
  102. package/src/modals/SettingsModal/components/InputModeSetting/index.ts +1 -0
  103. package/src/modals/SettingsModal/components/InputModeSetting/lib.ts +13 -0
  104. package/src/modals/SettingsModal/components/InputModeSetting/types.ts +7 -0
  105. package/src/modals/SettingsModal/components/index.ts +1 -0
  106. package/src/state/localStorage.ts +10 -1
  107. package/src/state/selectors.ts +2 -0
  108. package/src/state/slices/settingsInitialState.ts +4 -1
  109. package/src/state/slices/settingsSlice.ts +6 -1
  110. package/src/styles/mixins.scss +1 -0
  111. package/src/styles/variables.scss +1 -0
  112. package/src/types/index.ts +7 -0
  113. package/.next/static/5ttGCAW8jcIKxpR8om9fK/_buildManifest.js +0 -1
  114. package/.next/static/chunks/pages/_app-76a8840b6244d5a2.js +0 -28
  115. package/.next/static/chunks/pages/index-6894f40e6cac9243.js +0 -1
  116. package/.next/static/css/af871fef886ef5b7.css +0 -2
  117. package/src/components/Solver/components/FloatingSolveButton/FloatingSolveButton.module.scss +0 -7
  118. package/src/components/Solver/components/FloatingSolveButton/FloatingSolveButton.tsx +0 -53
  119. package/src/components/Solver/components/FloatingSolveButton/index.ts +0 -1
  120. /package/.next/static/{5ttGCAW8jcIKxpR8om9fK → fsjQvvJ13WNxBdMioL4sc}/_ssgManifest.js +0 -0
@@ -0,0 +1,89 @@
1
+ /* eslint-disable max-lines, max-statements */
2
+
3
+ import classNames from 'classnames';
4
+ import {
5
+ CSSProperties,
6
+ ChangeEventHandler,
7
+ FormEventHandler,
8
+ forwardRef,
9
+ useCallback,
10
+ useEffect,
11
+ useState,
12
+ } from 'react';
13
+ import { useDispatch } from 'react-redux';
14
+
15
+ import { useAppLayout } from 'hooks';
16
+ import { extractCharactersByCase } from 'lib';
17
+ import { rackSlice, selectConfig, useTranslate, useTypedSelector } from 'state';
18
+
19
+ import styles from './InputPrompt.module.scss';
20
+
21
+ interface Props {
22
+ className?: string;
23
+ style?: CSSProperties;
24
+ value: string;
25
+ onBlur: () => void;
26
+ onChange: ChangeEventHandler<HTMLInputElement>;
27
+ onSubmit: FormEventHandler<HTMLFormElement>;
28
+ }
29
+
30
+ const InputPrompt = forwardRef<HTMLFormElement, Props>(
31
+ ({ className, style, value, onBlur, onChange, onSubmit, ...props }, ref) => {
32
+ const dispatch = useDispatch();
33
+ const translate = useTranslate();
34
+ const { rackHeight, rackWidth } = useAppLayout();
35
+ const config = useTypedSelector(selectConfig);
36
+ const [inputRef, setInputRef] = useState<HTMLInputElement | null>(null);
37
+
38
+ const handleSubmit: FormEventHandler<HTMLFormElement> = useCallback(
39
+ (event) => {
40
+ event.preventDefault();
41
+ const charactersByCase = extractCharactersByCase(config, value);
42
+ const characters = Array.from({ length: config.maximumCharactersCount }, (_, index) => {
43
+ return typeof charactersByCase[index] === 'string' ? charactersByCase[index] : null;
44
+ });
45
+ dispatch(rackSlice.actions.changeCharacters({ characters, index: 0 }));
46
+ onSubmit(event);
47
+ },
48
+ [config, value, onSubmit],
49
+ );
50
+
51
+ useEffect(() => {
52
+ if (inputRef) {
53
+ inputRef.focus();
54
+ inputRef.select();
55
+ }
56
+ }, [inputRef]);
57
+
58
+ return (
59
+ <form
60
+ className={classNames(styles.form, className)}
61
+ ref={ref}
62
+ style={{
63
+ width: rackWidth,
64
+ height: rackHeight,
65
+ ...style,
66
+ }}
67
+ onSubmit={handleSubmit}
68
+ {...props}
69
+ >
70
+ <input
71
+ autoCapitalize="none"
72
+ autoComplete="off"
73
+ autoCorrect="off"
74
+ className={styles.input}
75
+ placeholder={translate('rack.touchscreen.placeholder')}
76
+ ref={setInputRef}
77
+ spellCheck={false}
78
+ value={value}
79
+ onBlur={onBlur}
80
+ onChange={onChange}
81
+ />
82
+ </form>
83
+ );
84
+ },
85
+ );
86
+
87
+ export default Object.assign(InputPrompt, {
88
+ styles,
89
+ });
@@ -0,0 +1 @@
1
+ export { default } from './InputPrompt';
@@ -0,0 +1,11 @@
1
+ @import 'styles/mixins';
2
+
3
+ .rackTile {
4
+ @include focus-effect;
5
+
6
+ --background-color: var(--color--background);
7
+
8
+ &:focus-within {
9
+ z-index: 2;
10
+ }
11
+ }
@@ -4,10 +4,13 @@ import classNames from 'classnames';
4
4
  import {
5
5
  ChangeEvent,
6
6
  ChangeEventHandler,
7
+ FocusEventHandler,
7
8
  FunctionComponent,
8
9
  KeyboardEventHandler,
10
+ MouseEventHandler,
9
11
  MutableRefObject,
10
12
  RefObject,
13
+ TouchEventHandler,
11
14
  useCallback,
12
15
  useMemo,
13
16
  } from 'react';
@@ -19,14 +22,15 @@ import {
19
22
  selectCharacterIsValid,
20
23
  selectCharacterPoints,
21
24
  selectConfig,
25
+ selectInputMode,
22
26
  selectLocale,
23
27
  useTranslate,
24
28
  useTypedSelector,
25
29
  } from 'state';
26
30
 
27
- import Tile from '../Tile';
31
+ import Tile from '../../../Tile';
28
32
 
29
- import styles from './Rack.module.scss';
33
+ import styles from './RackTile.module.scss';
30
34
 
31
35
  interface Props {
32
36
  activeIndexRef: MutableRefObject<number | undefined>;
@@ -38,6 +42,7 @@ interface Props {
38
42
  tile: TileModel | null;
39
43
  onChange: ChangeEventHandler<HTMLInputElement>;
40
44
  onKeyDown: KeyboardEventHandler<HTMLInputElement>;
45
+ onFocus: () => void;
41
46
  }
42
47
 
43
48
  const RackTile: FunctionComponent<Props> = ({
@@ -50,17 +55,28 @@ const RackTile: FunctionComponent<Props> = ({
50
55
  tile,
51
56
  onChange,
52
57
  onKeyDown,
58
+ onFocus,
53
59
  }) => {
54
60
  const dispatch = useDispatch();
55
61
  const translate = useTranslate();
56
62
  const locale = useTypedSelector(selectLocale);
57
63
  const config = useTypedSelector(selectConfig);
64
+ const inputMode = useTypedSelector(selectInputMode);
58
65
  const points = useTypedSelector((state) => selectCharacterPoints(state, character));
59
66
  const isValid = useTypedSelector((state) => selectCharacterIsValid(state, character));
60
67
 
61
- const handleFocus = useCallback(() => {
62
- activeIndexRef.current = index;
63
- }, [index]);
68
+ const handleFocus: FocusEventHandler<HTMLInputElement> = useCallback(
69
+ (event) => {
70
+ if (inputMode === 'touchscreen') {
71
+ event.preventDefault();
72
+ event.target.blur();
73
+ onFocus();
74
+ }
75
+
76
+ activeIndexRef.current = index;
77
+ },
78
+ [activeIndexRef, index, inputMode, onFocus],
79
+ );
64
80
 
65
81
  const handleChange = useCallback(
66
82
  (event: ChangeEvent<HTMLInputElement>) => {
@@ -94,13 +110,35 @@ const RackTile: FunctionComponent<Props> = ({
94
110
  });
95
111
  }, [index, onKeyDown]);
96
112
 
113
+ const handleMouseDown: MouseEventHandler<HTMLInputElement> = useCallback(
114
+ (event) => {
115
+ if (inputMode === 'touchscreen') {
116
+ event.preventDefault();
117
+ }
118
+
119
+ onFocus();
120
+ },
121
+ [inputMode, onFocus],
122
+ );
123
+
124
+ const handleTouchStart: TouchEventHandler<HTMLInputElement> = useCallback(
125
+ (event) => {
126
+ if (inputMode === 'touchscreen') {
127
+ event.preventDefault();
128
+ }
129
+
130
+ onFocus();
131
+ },
132
+ [inputMode, onFocus],
133
+ );
134
+
97
135
  return (
98
136
  <Tile
99
137
  aria-label={translate('rack.tile.location', {
100
138
  index: (index + 1).toLocaleString(locale),
101
139
  })}
102
- autoFocus={index === 0}
103
- className={classNames(styles.tile, className)}
140
+ autoFocus={inputMode === 'keyboard' && index === 0}
141
+ className={classNames(styles.rackTile, className)}
104
142
  character={character === null ? undefined : character}
105
143
  highlighted={tile !== null}
106
144
  inputRef={inputRef}
@@ -115,6 +153,8 @@ const RackTile: FunctionComponent<Props> = ({
115
153
  onChange={handleChange}
116
154
  onFocus={handleFocus}
117
155
  onKeyDown={handleKeyDown}
156
+ onMouseDown={handleMouseDown}
157
+ onTouchStart={handleTouchStart}
118
158
  />
119
159
  );
120
160
  };
@@ -0,0 +1 @@
1
+ export { default } from './RackTile';
@@ -0,0 +1,2 @@
1
+ export { default as InputPrompt } from './InputPrompt';
2
+ export { default as RackTile } from './RackTile';
@@ -15,13 +15,6 @@ $radio-box-size: $radio-size + 2 * $radio-inner-border;
15
15
  transition: var(--transition);
16
16
  cursor: pointer;
17
17
 
18
- &:hover,
19
- &.checked {
20
- .icon {
21
- box-shadow: var(--box-shadow);
22
- }
23
- }
24
-
25
18
  &.checked {
26
19
  .icon {
27
20
  &::after {
@@ -48,7 +41,6 @@ $radio-box-size: $radio-size + 2 * $radio-inner-border;
48
41
  height: $radio-box-size;
49
42
  border-radius: 50%;
50
43
  border: $radio-inner-border solid var(--color--primary);
51
- box-shadow: var(--box-shadow--null);
52
44
  pointer-events: none;
53
45
 
54
46
  &::after {
@@ -1,5 +1,5 @@
1
1
  import classNames from 'classnames';
2
- import { FunctionComponent } from 'react';
2
+ import { FunctionComponent, ReactNode } from 'react';
3
3
 
4
4
  import { selectLocale, useTranslate, useTypedSelector } from 'state';
5
5
  import { TranslationKey } from 'types';
@@ -9,13 +9,14 @@ import { useTooltip } from '../Tooltip';
9
9
  import styles from './Results.module.scss';
10
10
 
11
11
  interface Props {
12
+ children?: ReactNode;
12
13
  className?: string;
13
14
  translationKey: TranslationKey;
14
15
  tooltip?: string | number;
15
16
  value: string | number;
16
17
  }
17
18
 
18
- const Cell: FunctionComponent<Props> = ({ className, translationKey, tooltip, value }) => {
19
+ const Cell: FunctionComponent<Props> = ({ children, className, translationKey, tooltip, value }) => {
19
20
  const translate = useTranslate();
20
21
  const locale = useTypedSelector(selectLocale);
21
22
  const formattedValue = value.toLocaleString(locale);
@@ -23,7 +24,7 @@ const Cell: FunctionComponent<Props> = ({ className, translationKey, tooltip, va
23
24
 
24
25
  return (
25
26
  <div className={classNames(styles.cell, className)} {...triggerProps}>
26
- {formattedValue}
27
+ {children || formattedValue}
27
28
  </div>
28
29
  );
29
30
  };
@@ -1,9 +1,10 @@
1
1
  import classNames from 'classnames';
2
2
  import { CSSProperties, FocusEventHandler, MouseEventHandler, ReactElement, useRef } from 'react';
3
+ import Highlighter from 'react-highlight-words';
3
4
 
4
5
  import { LOCALE_FEATURES } from 'i18n';
5
6
  import { noop } from 'lib';
6
- import { selectIsResultMatching, selectLocale, useTypedSelector } from 'state';
7
+ import { selectIsResultMatching, selectLocale, selectResultsQuery, useTypedSelector } from 'state';
7
8
  import { ResultColumn } from 'types';
8
9
 
9
10
  import Cell from './Cell';
@@ -30,6 +31,7 @@ const Result = ({ data, index, style }: Props): ReactElement => {
30
31
  const ref = useRef<HTMLButtonElement>(null);
31
32
  const columns = useColumns();
32
33
  const locale = useTypedSelector(selectLocale);
34
+ const query = useTypedSelector(selectResultsQuery);
33
35
  const { consonants, direction, separator, vowels } = LOCALE_FEATURES[locale];
34
36
  const result = results[index];
35
37
  const isMatching = useTypedSelector((state) => selectIsResultMatching(state, index));
@@ -60,7 +62,9 @@ const Result = ({ data, index, style }: Props): ReactElement => {
60
62
  >
61
63
  <span className={styles.resultContent}>
62
64
  {enabledColumns[ResultColumn.Word] && (
63
- <Cell className={styles.word} translationKey="common.word" value={result.word} />
65
+ <Cell className={styles.word} translationKey="common.word" value={result.word}>
66
+ <Highlighter highlightClassName={styles.highlight} searchWords={[query]} textToHighlight={result.word} />
67
+ </Cell>
64
68
  )}
65
69
 
66
70
  {enabledColumns[ResultColumn.TilesCount] && (
@@ -220,3 +220,9 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
220
220
  border-bottom-left-radius: var(--border--radius);
221
221
  border-bottom-right-radius: var(--border--radius);
222
222
  }
223
+
224
+ .highlight {
225
+ background-color: var(--color--mark);
226
+ color: var(--color--foreground);
227
+ border-radius: var(--border--radius);
228
+ }
@@ -113,23 +113,3 @@
113
113
  .emptyState {
114
114
  margin-top: var(--spacing);
115
115
  }
116
-
117
- .solve {
118
- --spacing: var(--spacing--l);
119
-
120
- position: fixed;
121
- bottom: var(--spacing);
122
- z-index: 1;
123
-
124
- @include media('<xs') {
125
- --spacing: var(--spacing--m);
126
- }
127
-
128
- [dir='ltr'] & {
129
- right: var(--spacing);
130
- }
131
-
132
- [dir='rtl'] & {
133
- left: var(--spacing);
134
- }
135
- }
@@ -22,7 +22,7 @@ import DictionaryInput from '../DictionaryInput';
22
22
  import Rack from '../Rack';
23
23
  import Results from '../Results';
24
24
 
25
- import { FloatingSolveButton, ResultCandidatePicker } from './components';
25
+ import { ResultCandidatePicker } from './components';
26
26
  import styles from './Solver.module.scss';
27
27
 
28
28
  interface Props {
@@ -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 { maxControlsWidth, showCompactControls, showFloatingSolveButton, tileSize } = useAppLayout();
37
+ const { maxControlsWidth, showCompactControls, tileSize } = useAppLayout();
38
38
  const error = useTypedSelector(selectSolveError);
39
39
  const isOutdated = useTypedSelector(selectAreResultsOutdated);
40
40
  const resultCandidate = useTypedSelector(selectResultCandidate);
@@ -134,8 +134,6 @@ const Solver: FunctionComponent<Props> = ({ className, onShowResults }) => {
134
134
  )}
135
135
  </div>
136
136
  </div>
137
-
138
- {showFloatingSolveButton && <FloatingSolveButton className={styles.solve} onClick={handleSubmit} />}
139
137
  </div>
140
138
  );
141
139
  };
@@ -2,7 +2,6 @@ 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';
6
5
  import { ChevronDown, ChevronLeft, ChevronRight } from 'icons';
7
6
  import {
8
7
  resultsSlice,
@@ -38,7 +37,6 @@ const ResultCandidatePicker: FunctionComponent<Props> = ({ className, onResultCl
38
37
  const isPreviousDisabled = !results || index <= 0 || disabled;
39
38
  const isNextDisabled = !results || index >= results.length - 1 || disabled;
40
39
  const bothEnabled = !isPreviousDisabled && !isNextDisabled;
41
- const { showFloatingSolveButton } = useAppLayout();
42
40
 
43
41
  const handleNextClick = () => {
44
42
  if (!isNextDisabled) {
@@ -91,14 +89,8 @@ const ResultCandidatePicker: FunctionComponent<Props> = ({ className, onResultCl
91
89
  {!resultCandidate && <div className={styles.word}> </div>}
92
90
 
93
91
  <div className={styles.iconContainer}>
94
- {showFloatingSolveButton && <ChevronDown className={styles.icon} />}
95
-
96
- {!showFloatingSolveButton && (
97
- <>
98
- {isLoading && <Spinner className={styles.loading} />}
99
- {!isLoading && <ChevronDown className={styles.icon} />}
100
- </>
101
- )}
92
+ {isLoading && <Spinner className={styles.loading} />}
93
+ {!isLoading && <ChevronDown className={styles.icon} />}
102
94
  </div>
103
95
  </button>
104
96
 
@@ -1,3 +1,2 @@
1
- export { default as FloatingSolveButton } from './FloatingSolveButton';
2
1
  export { default as InsertButton } from './InsertButton';
3
2
  export { default as ResultCandidatePicker } from './ResultCandidatePicker';
@@ -87,6 +87,7 @@
87
87
  color: transparent;
88
88
  caret-color: transparent;
89
89
  font-size: 16px; // prevent iOS from automatically zooming in on focus
90
+ outline: none;
90
91
 
91
92
  &::selection {
92
93
  --background--color: transparent;
@@ -5,7 +5,9 @@ import {
5
5
  FocusEventHandler,
6
6
  FunctionComponent,
7
7
  KeyboardEventHandler,
8
+ MouseEventHandler,
8
9
  Ref,
10
+ TouchEventHandler,
9
11
  useCallback,
10
12
  useEffect,
11
13
  useMemo,
@@ -37,6 +39,8 @@ interface Props {
37
39
  onChange?: ChangeEventHandler<HTMLInputElement>;
38
40
  onFocus?: FocusEventHandler<HTMLInputElement>;
39
41
  onKeyDown?: KeyboardEventHandler<HTMLInputElement>;
42
+ onMouseDown?: MouseEventHandler<HTMLInputElement>;
43
+ onTouchStart?: TouchEventHandler<HTMLInputElement>;
40
44
  }
41
45
 
42
46
  const Tile: FunctionComponent<Props> = ({
@@ -57,6 +61,8 @@ const Tile: FunctionComponent<Props> = ({
57
61
  onChange,
58
62
  onFocus = noop,
59
63
  onKeyDown = noop,
64
+ onMouseDown = noop,
65
+ onTouchStart = noop,
60
66
  }) => {
61
67
  const locale = useTypedSelector(selectLocale);
62
68
  const { animateTile, showTilePoints } = useAppLayout();
@@ -77,12 +83,6 @@ const Tile: FunctionComponent<Props> = ({
77
83
  [onKeyDown],
78
84
  );
79
85
 
80
- useEffect(() => {
81
- if (autoFocus && ref.current) {
82
- ref.current.focus();
83
- }
84
- }, [autoFocus, ref]);
85
-
86
86
  useEffect(() => {
87
87
  if (!ref.current?.parentElement || !character || !animateTile) {
88
88
  return;
@@ -117,6 +117,8 @@ const Tile: FunctionComponent<Props> = ({
117
117
  onChange={onChange}
118
118
  onFocus={onFocus}
119
119
  onKeyDown={handleKeyDown}
120
+ onMouseDown={onMouseDown}
121
+ onTouchStart={onTouchStart}
120
122
  />
121
123
  );
122
124
  };
@@ -6,7 +6,9 @@ import {
6
6
  FunctionComponent,
7
7
  KeyboardEventHandler,
8
8
  memo,
9
+ MouseEventHandler,
9
10
  Ref,
11
+ TouchEventHandler,
10
12
  } from 'react';
11
13
 
12
14
  import { ExclamationSquareFill } from 'icons';
@@ -34,6 +36,8 @@ interface Props {
34
36
  onChange?: ChangeEventHandler<HTMLInputElement>;
35
37
  onFocus?: FocusEventHandler<HTMLInputElement>;
36
38
  onKeyDown?: KeyboardEventHandler<HTMLInputElement>;
39
+ onMouseDown?: MouseEventHandler<HTMLInputElement>;
40
+ onTouchStart?: TouchEventHandler<HTMLInputElement>;
37
41
  }
38
42
 
39
43
  const TilePure: FunctionComponent<Props> = ({
@@ -57,6 +61,8 @@ const TilePure: FunctionComponent<Props> = ({
57
61
  onChange,
58
62
  onFocus,
59
63
  onKeyDown,
64
+ onMouseDown,
65
+ onTouchStart,
60
66
  }) => (
61
67
  <div
62
68
  className={classNames(styles.tile, className, {
@@ -90,6 +96,8 @@ const TilePure: FunctionComponent<Props> = ({
90
96
  onChange={onChange}
91
97
  onFocus={onFocus}
92
98
  onKeyDown={onKeyDown}
99
+ onMouseDown={onMouseDown}
100
+ onTouchStart={onTouchStart}
93
101
  />
94
102
 
95
103
  {canShowPoints && (
@@ -56,6 +56,7 @@ const useAppLayout = () => {
56
56
  const resultsHeight = isLessThanL
57
57
  ? viewportHeight - dictionaryHeight - BUTTON_HEIGHT - MODAL_HEADER_HEIGHT - 5 * componentsSpacing
58
58
  : boardSize - componentsSpacing - dictionaryHeight;
59
+ const rackWidth = tileSize * config.maximumCharactersCount;
59
60
 
60
61
  return {
61
62
  actionsWidth: 2 * BUTTON_HEIGHT - BORDER_WIDTH,
@@ -67,10 +68,11 @@ const useAppLayout = () => {
67
68
  logoHeight,
68
69
  logoWidth: logoHeight * LOGO_ASPECT_RATIO,
69
70
  maxControlsWidth,
71
+ rackHeight: tileSize,
72
+ rackWidth,
70
73
  resultsHeight,
71
74
  resultsWidth: isLessThanL ? modalWidth - 2 * componentsSpacing : SOLVER_COLUMN_WIDTH,
72
75
  showCompactControls: !showColumn,
73
- showFloatingSolveButton: isTouchDevice,
74
76
  showKeyMap: !isTouchDevice,
75
77
  showResultsInModal,
76
78
  showShortNav: isLessThanS,
@@ -5,6 +5,7 @@ import {
5
5
  selectAutoGroupTiles,
6
6
  selectBoard,
7
7
  selectConfigId,
8
+ selectInputMode,
8
9
  selectLocale,
9
10
  selectRack,
10
11
  useTypedSelector,
@@ -14,6 +15,7 @@ const useLocalStorage = () => {
14
15
  const autoGroupTiles = useTypedSelector(selectAutoGroupTiles);
15
16
  const board = useTypedSelector(selectBoard);
16
17
  const configId = useTypedSelector(selectConfigId);
18
+ const inputMode = useTypedSelector(selectInputMode);
17
19
  const locale = useTypedSelector(selectLocale);
18
20
  const rack = useTypedSelector(selectRack);
19
21
 
@@ -35,6 +37,12 @@ const useLocalStorage = () => {
35
37
  }
36
38
  }, [configId]);
37
39
 
40
+ useEffect(() => {
41
+ if (inputMode) {
42
+ localStorage.setInputMode(inputMode);
43
+ }
44
+ }, [inputMode]);
45
+
38
46
  useEffect(() => {
39
47
  if (locale) {
40
48
  localStorage.setLocale(locale);
package/src/i18n/de.json CHANGED
@@ -1,4 +1,5 @@
1
1
  {
2
+ "cell.enter-word": "Wort eingeben",
2
3
  "cell.filter-cell": "Zielort",
3
4
  "cell.set-blank": "Als Blanko markieren",
4
5
  "cell.set-not-blank": "Nicht als Blanko markieren",
@@ -22,7 +23,7 @@
22
23
  "dictionary.empty-state.no-results": "Wort kann nicht im Wörterbuch gefunden werden.",
23
24
  "dictionary.empty-state.not-allowed": "Dieses Wort ist nicht erlaubt.",
24
25
  "dictionary.empty-state.uninitialized": "Die Wörterbuchdéfinition wird hier angezeigt.",
25
- "dictionary.input.placeholder": "Durchsuche Wörterbuch...",
26
+ "dictionary.input.placeholder": "Durchsuche Wörterbuch",
26
27
  "dictionary.input.title": "Durch Kommas getrennte Wörter",
27
28
  "empty-state.error": "Fehler",
28
29
  "empty-state.info": "Info",
@@ -43,6 +44,7 @@
43
44
  "menu": "Menü",
44
45
  "rack.placeholder": "Steine…",
45
46
  "rack.tile.location": "Ablage: Stein ({{index}})",
47
+ "rack.touchscreen.placeholder": "Steine…",
46
48
  "remaining-tiles": "Restliche Steine",
47
49
  "results": "Ergebnisse",
48
50
  "results.empty-state.no-results": "Keine Ergebnisse - kein Wort konnte generiert werden.",
@@ -58,6 +60,9 @@
58
60
  "settings.autoGroupTiles.right": "Rechte Seite",
59
61
  "settings.autoGroupTiles.null": "Nicht gruppieren",
60
62
  "settings.game": "Spiel",
63
+ "settings.inputMode": "Eingabemodus",
64
+ "settings.inputMode.keyboard": "Tastatur",
65
+ "settings.inputMode.touchscreen": "Touchscreen",
61
66
  "settings.language": "Sprache",
62
67
  "words": "Gebildete Wörter",
63
68
  "words.invalid": "Falsch",
package/src/i18n/en.json CHANGED
@@ -1,4 +1,5 @@
1
1
  {
2
+ "cell.enter-word": "Enter word",
2
3
  "cell.filter-cell": "Target destination",
3
4
  "cell.set-blank": "Mark it a blank",
4
5
  "cell.set-not-blank": "Mark it not a blank",
@@ -22,7 +23,7 @@
22
23
  "dictionary.empty-state.no-results": "Unable to find word definition in the dictionary.",
23
24
  "dictionary.empty-state.not-allowed": "This word is not allowed.",
24
25
  "dictionary.empty-state.uninitialized": "Word definition will be shown here.",
25
- "dictionary.input.placeholder": "Search dictionary...",
26
+ "dictionary.input.placeholder": "Search dictionary",
26
27
  "dictionary.input.title": "Comma-separated words",
27
28
  "empty-state.error": "Error",
28
29
  "empty-state.info": "Info",
@@ -43,6 +44,7 @@
43
44
  "menu": "Menu",
44
45
  "rack.placeholder": "Letters",
45
46
  "rack.tile.location": "Rack: tile ({{index}})",
47
+ "rack.touchscreen.placeholder": "Letters…",
46
48
  "remaining-tiles": "Remaining tiles",
47
49
  "results": "Results",
48
50
  "results.empty-state.no-results": "No results - unable to generate any words.",
@@ -58,6 +60,9 @@
58
60
  "settings.autoGroupTiles.right": "On the right",
59
61
  "settings.autoGroupTiles.null": "Do not group",
60
62
  "settings.game": "Game",
63
+ "settings.inputMode": "Input mode",
64
+ "settings.inputMode.keyboard": "Keyboard",
65
+ "settings.inputMode.touchscreen": "Touchscreen",
61
66
  "settings.language": "Language",
62
67
  "words": "Created words",
63
68
  "words.invalid": "Invalid",
package/src/i18n/es.json CHANGED
@@ -1,4 +1,5 @@
1
1
  {
2
+ "cell.enter-word": "Ingresar palabra",
2
3
  "cell.filter-cell": "Destino objetivo",
3
4
  "cell.set-blank": "Marcar como en blanco",
4
5
  "cell.set-not-blank": "Marcar como no en blanco",
@@ -22,7 +23,7 @@
22
23
  "dictionary.empty-state.no-results": "No se puede encontrar la definición de palabra en el diccionario.",
23
24
  "dictionary.empty-state.not-allowed": "Esta palabra no es aceptable.",
24
25
  "dictionary.empty-state.uninitialized": "Aquí se mostrará la definición del diccionario.",
25
- "dictionary.input.placeholder": "Busca el diccionario...",
26
+ "dictionary.input.placeholder": "Busca el diccionario",
26
27
  "dictionary.input.title": "Palabras separadas por comas",
27
28
  "empty-state.error": "Error",
28
29
  "empty-state.info": "Info",
@@ -43,6 +44,7 @@
43
44
  "menu": "Menú",
44
45
  "rack.placeholder": "Letras…",
45
46
  "rack.tile.location": "Estante: espacio ({{index}})",
47
+ "rack.touchscreen.placeholder": "Letras… (p.ej. LLabcCHhRR)",
46
48
  "remaining-tiles": "Casillas restantes",
47
49
  "results": "Resultados",
48
50
  "results.empty-state.no-results": "No hay resultados; no se pueden generar palabras",
@@ -58,6 +60,9 @@
58
60
  "settings.autoGroupTiles.right": "A la derecha",
59
61
  "settings.autoGroupTiles.null": "No agrupar",
60
62
  "settings.game": "Juego",
63
+ "settings.inputMode": "Modo de entrada",
64
+ "settings.inputMode.keyboard": "Teclado",
65
+ "settings.inputMode.touchscreen": "Pantalla táctil",
61
66
  "settings.language": "Idioma",
62
67
  "words": "Palabras creadas",
63
68
  "words.invalid": "Incorrecto",