@scrabble-solver/scrabble-solver 2.10.7 → 2.10.9

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 (128) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +15 -21
  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/images-manifest.json +1 -1
  13. package/.next/next-server.js.nft.json +1 -1
  14. package/.next/prerender-manifest.json +1 -1
  15. package/.next/required-server-files.json +1 -1
  16. package/.next/routes-manifest.json +1 -1
  17. package/.next/server/chunks/176.js +4603 -395
  18. package/.next/server/chunks/664.js +27 -2414
  19. package/.next/server/chunks/859.js +29 -1
  20. package/.next/server/font-loader-manifest.js +1 -0
  21. package/.next/server/font-loader-manifest.json +6 -0
  22. package/.next/server/middleware-build-manifest.js +1 -1
  23. package/.next/server/pages/404.html +2 -2
  24. package/.next/server/pages/404.js.nft.json +1 -1
  25. package/.next/server/pages/500.html +1 -12
  26. package/.next/server/pages/_app.js +176 -13
  27. package/.next/server/pages/_app.js.nft.json +1 -1
  28. package/.next/server/pages/_document.js +1 -1
  29. package/.next/server/pages/_document.js.nft.json +1 -1
  30. package/.next/server/pages/_error.js +133 -17
  31. package/.next/server/pages/_error.js.nft.json +1 -1
  32. package/.next/server/pages/api/solve.js +16 -0
  33. package/.next/server/pages/index.html +1 -9
  34. package/.next/server/pages/index.js +52 -165
  35. package/.next/server/pages/index.js.nft.json +1 -1
  36. package/.next/server/pages/index.json +1 -1
  37. package/.next/server/pages-manifest.json +1 -1
  38. package/.next/static/chunks/main-74c4d6b2b5c362f3.js +1 -0
  39. package/.next/static/chunks/pages/{404-67383848027ec49b.js → 404-d5ff00df1c687977.js} +1 -1
  40. package/.next/static/chunks/pages/_app-52cb288dc680bdfe.js +28 -0
  41. package/.next/static/chunks/pages/index-5c2544930e46c5ce.js +1 -0
  42. package/.next/static/chunks/webpack-6ef43a8d4a395f49.js +1 -0
  43. package/.next/static/css/ec4e47a6b1866fe5.css +1 -0
  44. package/.next/static/css/f65b7b2a74f57c1c.css +2 -0
  45. package/.next/static/fZRsz4P0gQ8Wgb9jP8eap/_buildManifest.js +1 -0
  46. package/.next/trace +55 -55
  47. package/package.json +12 -10
  48. package/src/components/Badge/Badge.module.scss +1 -1
  49. package/src/components/Board/Board.module.scss +14 -0
  50. package/src/components/Board/Board.tsx +117 -19
  51. package/src/components/Board/BoardPure.tsx +7 -15
  52. package/src/components/Board/components/Actions/Actions.module.scss +64 -0
  53. package/src/components/Board/components/Actions/Actions.tsx +68 -0
  54. package/src/components/Board/components/Actions/index.ts +1 -0
  55. package/src/components/Board/components/Cell/Cell.module.scss +22 -165
  56. package/src/components/Board/components/Cell/Cell.tsx +0 -37
  57. package/src/components/Board/components/Cell/CellPure.tsx +5 -75
  58. package/src/components/Board/components/index.ts +1 -0
  59. package/src/components/Board/hooks/useGrid.ts +16 -16
  60. package/src/components/Button/Button.module.scss +3 -3
  61. package/src/components/Checkbox/Checkbox.tsx +1 -4
  62. package/src/components/Dictionary/Dictionary.tsx +28 -30
  63. package/src/components/DictionaryInput/DictionaryInput.tsx +3 -0
  64. package/src/components/Key/Key.module.scss +1 -1
  65. package/src/components/LogoSplashScreen/LogoSplashScreen.module.scss +1 -1
  66. package/src/components/Modal/Modal.module.scss +4 -2
  67. package/src/components/Rack/Rack.module.scss +5 -0
  68. package/src/components/Radio/Radio.tsx +1 -4
  69. package/src/components/Results/Results.module.scss +2 -2
  70. package/src/components/SeoMessage/SeoMessage.tsx +19 -0
  71. package/src/components/SeoMessage/index.ts +1 -0
  72. package/src/components/Solver/Solver.module.scss +0 -5
  73. package/src/components/Solver/Solver.tsx +1 -1
  74. package/src/components/Solver/components/EmptyState/EmptyState.module.scss +1 -1
  75. package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.module.scss +9 -2
  76. package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.tsx +2 -1
  77. package/src/components/Tile/Tile.module.scss +23 -11
  78. package/src/components/Tile/Tile.tsx +26 -9
  79. package/src/components/Tile/TilePure.tsx +9 -4
  80. package/src/components/Tooltip/Tooltip.module.scss +7 -7
  81. package/src/components/index.ts +1 -0
  82. package/src/hooks/useLocalStorage/useLocalStorageBoard.ts +6 -3
  83. package/src/hooks/useLocalStorage/useLocalStorageConfigId.ts +6 -3
  84. package/src/hooks/useLocalStorage/useLocalStorageLocale.ts +6 -3
  85. package/src/hooks/useLocalStorage/useLocalStorageRack.ts +6 -3
  86. package/src/i18n/de.json +1 -0
  87. package/src/i18n/en.json +1 -0
  88. package/src/i18n/es.json +1 -0
  89. package/src/i18n/fa.json +1 -0
  90. package/src/i18n/fr.json +1 -0
  91. package/src/i18n/pl.json +1 -0
  92. package/src/icons/Flag.svg +2 -2
  93. package/src/icons/FlagFill.svg +4 -0
  94. package/src/icons/Square.svg +4 -0
  95. package/src/icons/SquareFill.svg +4 -0
  96. package/src/icons/index.ts +3 -0
  97. package/src/modals/RemainingTilesModal/components/Character/Character.module.scss +1 -1
  98. package/src/modals/ResultsModal/ResultsModal.module.scss +1 -1
  99. package/src/modals/SettingsModal/components/AutoGroupTilesSetting/AutoGroupTilesSetting.tsx +1 -2
  100. package/src/modals/SettingsModal/components/ConfigSetting/ConfigSetting.module.scss +1 -1
  101. package/src/modals/SettingsModal/components/ConfigSetting/ConfigSetting.tsx +1 -2
  102. package/src/modals/SettingsModal/components/LocaleSetting/LocaleSetting.module.scss +14 -24
  103. package/src/modals/SettingsModal/components/LocaleSetting/LocaleSetting.tsx +1 -2
  104. package/src/pages/_app.tsx +9 -5
  105. package/src/pages/index.module.scss +1 -2
  106. package/src/pages/index.tsx +10 -8
  107. package/src/parameters/index.ts +12 -0
  108. package/src/state/slices/boardSlice.ts +5 -5
  109. package/src/styles/animations.scss +10 -0
  110. package/src/styles/global.scss +2 -2
  111. package/src/styles/mixins.scss +60 -3
  112. package/src/styles/variables.scss +43 -33
  113. package/src/types/index.ts +1 -0
  114. package/.next/server/chunks/210.js +0 -122
  115. package/.next/server/chunks/579.js +0 -3925
  116. package/.next/server/chunks/676.js +0 -32
  117. package/.next/static/6RggBFm8kHrh-k1-CG3um/_buildManifest.js +0 -1
  118. package/.next/static/chunks/490-d29992f1c264d70e.js +0 -5
  119. package/.next/static/chunks/509-6ad4482d4351452c.js +0 -1
  120. package/.next/static/chunks/main-f11614d8aa7ee555.js +0 -1
  121. package/.next/static/chunks/pages/_app-c58cfa832b76cc87.js +0 -24
  122. package/.next/static/chunks/pages/index-146039f501e49c08.js +0 -1
  123. package/.next/static/chunks/webpack-59c5c889f52620d6.js +0 -1
  124. package/.next/static/css/4482c4a0064d3807.css +0 -1
  125. package/.next/static/css/78e42ad01f580f64.css +0 -1
  126. package/.next/static/css/9d1013ec684361b9.css +0 -1
  127. package/src/components/Board/components/Cell/Button.tsx +0 -32
  128. /package/.next/static/{6RggBFm8kHrh-k1-CG3um → fZRsz4P0gQ8Wgb9jP8eap}/_ssgManifest.js +0 -0
@@ -27,6 +27,10 @@ const Dictionary: FunctionComponent<Props> = ({ className }) => {
27
27
  >
28
28
  {typeof error !== 'undefined' && <EmptyState variant="error">{error.message}</EmptyState>}
29
29
 
30
+ {results.length === 0 && (
31
+ <EmptyState variant="info">{translate('dictionary.empty-state.uninitialized')}</EmptyState>
32
+ )}
33
+
30
34
  {results.map(({ definitions, exists, isAllowed, word }) => (
31
35
  <div
32
36
  className={classNames(styles.result, {
@@ -35,40 +39,34 @@ const Dictionary: FunctionComponent<Props> = ({ className }) => {
35
39
  })}
36
40
  key={word}
37
41
  >
38
- {typeof word === 'undefined' && (
39
- <EmptyState variant="info">{translate('dictionary.empty-state.uninitialized')}</EmptyState>
40
- )}
41
-
42
- {typeof word !== 'undefined' && (
43
- <div className={styles.content}>
44
- {word && <h2 className={styles.word}>{word}</h2>}
42
+ <div className={styles.content}>
43
+ {word && <h2 className={styles.word}>{word}</h2>}
45
44
 
46
- {isAllowed === false && <div>{translate('dictionary.empty-state.not-allowed')}</div>}
45
+ {isAllowed === false && <div>{translate('dictionary.empty-state.not-allowed')}</div>}
47
46
 
48
- {isAllowed === true && (
49
- <>
50
- {definitions.length === 0 && (
51
- <>
52
- {exists && <div>{translate('dictionary.empty-state.no-definitions')}</div>}
53
- {!exists && <div>{translate('dictionary.empty-state.no-results')}</div>}
54
- </>
55
- )}
47
+ {isAllowed === true && (
48
+ <>
49
+ {definitions.length === 0 && (
50
+ <>
51
+ {exists && <div>{translate('dictionary.empty-state.no-definitions')}</div>}
52
+ {!exists && <div>{translate('dictionary.empty-state.no-results')}</div>}
53
+ </>
54
+ )}
56
55
 
57
- {definitions.length > 0 && (
58
- <ul className={styles.definitions}>
59
- {definitions.map((result, index) => (
60
- <li key={index} className={styles.definition}>
61
- {result}
62
- </li>
63
- ))}
64
- </ul>
65
- )}
66
- </>
67
- )}
56
+ {definitions.length > 0 && (
57
+ <ul className={styles.definitions}>
58
+ {definitions.map((result, index) => (
59
+ <li key={index} className={styles.definition}>
60
+ {result}
61
+ </li>
62
+ ))}
63
+ </ul>
64
+ )}
65
+ </>
66
+ )}
68
67
 
69
- {!isLoading && isAllowed === null && <div>{translate('dictionary.empty-state.no-results')}</div>}
70
- </div>
71
- )}
68
+ {!isLoading && isAllowed === null && <div>{translate('dictionary.empty-state.no-results')}</div>}
69
+ </div>
72
70
  </div>
73
71
  ))}
74
72
 
@@ -1,3 +1,4 @@
1
+ import { COMMA_ARABIC, COMMA_LATIN } from '@scrabble-solver/constants';
1
2
  import classNames from 'classnames';
2
3
  import { ChangeEvent, FormEvent, FunctionComponent } from 'react';
3
4
  import { useDispatch } from 'react-redux';
@@ -28,8 +29,10 @@ const DictionaryInput: FunctionComponent<Props> = ({ className }) => {
28
29
  <form className={classNames(styles.dictionaryInput, className)} onSubmit={handleSubmit}>
29
30
  <input
30
31
  className={styles.input}
32
+ pattern={`.*[^\\s${COMMA_ARABIC}${COMMA_LATIN}].*`}
31
33
  placeholder={translate('dictionary.input.placeholder')}
32
34
  required
35
+ title={translate('dictionary.input.title')}
33
36
  type="text"
34
37
  value={input}
35
38
  onChange={handleChange}
@@ -8,7 +8,7 @@ $icon-size: 15px;
8
8
  min-width: $key-size;
9
9
  height: $key-size;
10
10
  padding: var(--spacing--s) var(--spacing--m);
11
- background-color: white;
11
+ background-color: var(--color--white);
12
12
  border: var(--border);
13
13
  border-radius: var(--border--radius);
14
14
  box-shadow: var(--box-shadow);
@@ -43,7 +43,7 @@ $hiding-duration: 200ms;
43
43
 
44
44
  .logoGrayscale {
45
45
  filter: grayscale(1);
46
- opacity: 0.3;
46
+ opacity: var(--opacity--disabled);
47
47
  }
48
48
 
49
49
  .logoColor {
@@ -60,12 +60,14 @@
60
60
  }
61
61
 
62
62
  .container {
63
+ --box-shadow--offset: 1px;
64
+ --box-shadow--offset--negative: calc(-1 * var(--box-shadow--offset));
65
+
63
66
  display: flex;
64
67
  flex-direction: column;
65
68
  height: 100%;
66
69
  background-color: var(--color--background);
67
- transition: var(--transition);
68
- transition-duration: var(--transition--duration--long);
70
+ transition: var(--transition--long);
69
71
  transform: translateX(var(--modal--width));
70
72
  opacity: 0;
71
73
 
@@ -7,6 +7,11 @@
7
7
 
8
8
  .tile {
9
9
  @include focus-effect;
10
+ @include lighthouse-input-size-hack;
10
11
 
11
12
  --background-color: var(--color--background);
13
+
14
+ &:focus-within {
15
+ z-index: 2;
16
+ }
12
17
  }
@@ -8,24 +8,21 @@ interface Props {
8
8
  children?: ReactNode;
9
9
  className?: string;
10
10
  disabled?: boolean;
11
- id: string;
12
11
  name: string;
13
12
  value: string;
14
13
  onChange: ChangeEventHandler<HTMLInputElement>;
15
14
  }
16
15
 
17
- const Radio: FunctionComponent<Props> = ({ checked, children, className, disabled, id, name, value, onChange }) => (
16
+ const Radio: FunctionComponent<Props> = ({ checked, children, className, disabled, name, value, onChange }) => (
18
17
  <label
19
18
  className={classNames(styles.radio, className, {
20
19
  [styles.checked]: checked,
21
20
  })}
22
- htmlFor={id}
23
21
  >
24
22
  <input
25
23
  checked={checked}
26
24
  className={styles.input}
27
25
  disabled={disabled}
28
- id={id}
29
26
  name={name}
30
27
  type="radio"
31
28
  value={value}
@@ -45,7 +45,7 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
45
45
  &:focus,
46
46
  &:hover {
47
47
  background-color: var(--color--primary);
48
- color: white;
48
+ color: var(--color--primary--opposite);
49
49
  }
50
50
  }
51
51
 
@@ -73,7 +73,7 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
73
73
  &.highlighted {
74
74
  &:not(:disabled) {
75
75
  background-color: var(--color--primary);
76
- color: white;
76
+ color: var(--color--primary--opposite);
77
77
  }
78
78
  }
79
79
  }
@@ -0,0 +1,19 @@
1
+ import { CSSProperties, FunctionComponent } from 'react';
2
+
3
+ const INVISIBLE_STYLE: CSSProperties = {
4
+ color: 'transparent',
5
+ pointerEvents: 'none',
6
+ position: 'absolute',
7
+ userSelect: 'none',
8
+ transform: 'translateY(-9999px)',
9
+ };
10
+
11
+ const SeoMessage: FunctionComponent = () => (
12
+ <p style={INVISIBLE_STYLE}>
13
+ Scrabble Solver 2 is a free and open-source analysis tool for Scrabble and Literaki. Quickly find top scoring words
14
+ using given letters and board state. Available in English, French, German, Polish & Spanish. Source code is
15
+ available on GitHub - contributions are welcome!
16
+ </p>
17
+ );
18
+
19
+ export default SeoMessage;
@@ -0,0 +1 @@
1
+ export { default } from './SeoMessage';
@@ -96,11 +96,6 @@
96
96
  min-width: 0;
97
97
  }
98
98
 
99
- .rackContainer {
100
- display: flex;
101
- justify-content: center;
102
- }
103
-
104
99
  .rack {
105
100
  border: var(--border);
106
101
  }
@@ -157,7 +157,7 @@ const Solver: FunctionComponent<Props> = ({ className, height, width, onShowResu
157
157
 
158
158
  <div className={styles.bottomContainer} ref={bottomContainerRef}>
159
159
  <div className={styles.bottomContent}>
160
- <form className={styles.rackContainer} onSubmit={handleSubmit}>
160
+ <form onSubmit={handleSubmit}>
161
161
  <Rack className={styles.rack} tileSize={tileSize} />
162
162
  <input className={styles.submitInput} tabIndex={-1} type="submit" />
163
163
  </form>
@@ -4,7 +4,7 @@
4
4
  width: 100%;
5
5
  border: var(--border);
6
6
  border-radius: var(--border--radius);
7
- background-color: white;
7
+ background-color: var(--color--white);
8
8
  text-align: center;
9
9
  }
10
10
 
@@ -2,7 +2,6 @@
2
2
 
3
3
  .resultCandidatePicker {
4
4
  display: flex;
5
- align-items: center;
6
5
  gap: var(--spacing--l);
7
6
 
8
7
  @include media('<xs') {
@@ -14,6 +13,14 @@
14
13
  flex: 0 0 auto;
15
14
  display: flex;
16
15
  border-radius: var(--border--radius);
16
+
17
+ &.bothEnabled {
18
+ box-shadow: var(--box-shadow);
19
+
20
+ .button {
21
+ box-shadow: none;
22
+ }
23
+ }
17
24
  }
18
25
 
19
26
  .button {
@@ -50,7 +57,7 @@
50
57
  width: 100%;
51
58
  border: var(--border);
52
59
  border-radius: var(--border--radius);
53
- background-color: white;
60
+ background-color: var(--color--white);
54
61
  box-shadow: var(--box-shadow);
55
62
  cursor: pointer;
56
63
 
@@ -34,6 +34,7 @@ const ResultCandidatePicker: FunctionComponent<Props> = ({ className, onResultCl
34
34
  const disabled = isOutdated || !resultCandidate;
35
35
  const isPreviousDisabled = index <= 0 || disabled;
36
36
  const isNextDisabled = index >= results.length - 1 || disabled;
37
+ const bothEnabled = !isPreviousDisabled && !isNextDisabled;
37
38
 
38
39
  const handleNextClick = () => {
39
40
  if (!isNextDisabled) {
@@ -51,7 +52,7 @@ const ResultCandidatePicker: FunctionComponent<Props> = ({ className, onResultCl
51
52
 
52
53
  return (
53
54
  <div className={classNames(styles.resultCandidatePicker, className)} {...props}>
54
- <div className={styles.buttons}>
55
+ <div className={classNames(styles.buttons, { [styles.bothEnabled]: bothEnabled })}>
55
56
  <Button
56
57
  aria-label={translate('common.previous')}
57
58
  className={styles.button}
@@ -24,18 +24,20 @@
24
24
  }
25
25
 
26
26
  &.blank {
27
- --background-color: white;
27
+ --background-color: var(--color--white);
28
28
 
29
+ .placeholder,
29
30
  .character {
30
- color: black;
31
+ color: var(--color--foreground);
31
32
  }
32
33
  }
33
34
 
34
35
  &.highlighted {
35
36
  --background-color: var(--color--primary);
36
37
 
37
- color: white;
38
+ color: var(--color--primary--opposite);
38
39
 
40
+ .placeholder,
39
41
  .character {
40
42
  color: inherit;
41
43
  }
@@ -45,12 +47,6 @@
45
47
  }
46
48
  }
47
49
 
48
- &.invalid {
49
- --background-color: var(--color--red--light);
50
-
51
- color: var(--color--error);
52
- }
53
-
54
50
  &:not(.disabled) {
55
51
  .input::selection {
56
52
  --background--color: transparent;
@@ -59,7 +55,8 @@
59
55
  }
60
56
 
61
57
  .input,
62
- .character {
58
+ .character,
59
+ .placeholder {
63
60
  padding: 0;
64
61
  font-weight: bold;
65
62
  text-transform: uppercase;
@@ -77,7 +74,8 @@
77
74
  font-size: 16px; // prevent iOS from automatically zooming in on focus
78
75
  }
79
76
 
80
- .character {
77
+ .character,
78
+ .placeholder {
81
79
  position: absolute;
82
80
  top: 0;
83
81
  right: 0;
@@ -97,6 +95,18 @@
97
95
 
98
96
  .raised & {
99
97
  box-shadow: inset -2px -2px 2px -1px rgba(34, 34, 34, 0.8);
98
+
99
+ @include media('<xs') {
100
+ box-shadow: inset -2px -2px 1px -1px rgba(34, 34, 34, 0.8);
101
+ }
102
+ }
103
+ }
104
+
105
+ .character {
106
+ opacity: 1;
107
+
108
+ .empty & {
109
+ opacity: 0;
100
110
  }
101
111
  }
102
112
 
@@ -130,6 +140,8 @@
130
140
  position: absolute;
131
141
  width: $size;
132
142
  height: $size;
143
+ background-color: var(--color--error--opposite);
144
+ color: var(--color--error);
133
145
 
134
146
  [dir='ltr'] & {
135
147
  top: 0;
@@ -1,16 +1,19 @@
1
1
  import { EMPTY_CELL } from '@scrabble-solver/constants';
2
+ import mergeRefs from 'merge-refs';
2
3
  import {
3
4
  ChangeEventHandler,
4
- createRef,
5
5
  FocusEventHandler,
6
6
  FunctionComponent,
7
7
  KeyboardEventHandler,
8
8
  RefObject,
9
9
  useEffect,
10
10
  useMemo,
11
+ useRef,
11
12
  } from 'react';
12
13
 
14
+ import { useMediaQuery } from 'hooks';
13
15
  import { getTileSizes, noop } from 'lib';
16
+ import { EASE_OUT_CUBIC, TILE_APPEAR_DURATION, TILE_APPEAR_KEYFRAMES } from 'parameters';
14
17
  import { selectLocale, useTypedSelector } from 'state';
15
18
 
16
19
  import TilePure from './TilePure';
@@ -42,7 +45,7 @@ const Tile: FunctionComponent<Props> = ({
42
45
  character = '',
43
46
  disabled,
44
47
  highlighted,
45
- inputRef: ref,
48
+ inputRef,
46
49
  isBlank,
47
50
  isValid,
48
51
  placeholder,
@@ -59,21 +62,35 @@ const Tile: FunctionComponent<Props> = ({
59
62
  const style = useMemo(() => ({ height: tileSize, width: tileSize }), [tileSize]);
60
63
  const characterStyle = useMemo(() => ({ fontSize: tileFontSize }), [tileFontSize]);
61
64
  const pointsStyle = useMemo(() => ({ fontSize: pointsFontSize }), [pointsFontSize]);
62
- const inputRef = useMemo<RefObject<HTMLInputElement>>(() => ref || createRef(), [ref]);
65
+ const ref = useRef<HTMLInputElement>(null);
66
+ const mergedRef = inputRef ? mergeRefs(ref, inputRef) : ref;
63
67
  const isEmpty = !character || character === EMPTY_CELL;
64
- const canShowPoints = (isBlank || !isEmpty) && typeof points !== 'undefined';
68
+ const isLessThanXs = useMediaQuery('<xs');
69
+ const canShowPoints = (isBlank || !isEmpty) && typeof points !== 'undefined' && !isLessThanXs;
65
70
  const pointsFormatted = typeof points === 'number' ? points.toLocaleString(locale) : '';
66
71
 
67
72
  const handleKeyDown: KeyboardEventHandler<HTMLInputElement> = (event) => {
68
- inputRef.current?.select();
73
+ ref.current?.select();
69
74
  onKeyDown(event);
70
75
  };
71
76
 
72
77
  useEffect(() => {
73
- if (autoFocus && inputRef.current) {
74
- inputRef.current.focus();
78
+ if (autoFocus && ref.current) {
79
+ ref.current.focus();
75
80
  }
76
- }, [autoFocus, inputRef]);
81
+ }, [autoFocus, ref]);
82
+
83
+ useEffect(() => {
84
+ if (!ref.current?.parentElement || !character) {
85
+ return;
86
+ }
87
+
88
+ ref.current.parentElement.animate(TILE_APPEAR_KEYFRAMES, {
89
+ duration: TILE_APPEAR_DURATION,
90
+ easing: EASE_OUT_CUBIC,
91
+ fill: 'forwards',
92
+ });
93
+ }, [character]);
77
94
 
78
95
  return (
79
96
  <TilePure
@@ -85,7 +102,7 @@ const Tile: FunctionComponent<Props> = ({
85
102
  className={className}
86
103
  disabled={disabled}
87
104
  highlighted={highlighted}
88
- inputRef={inputRef}
105
+ inputRef={mergedRef}
89
106
  isBlank={isBlank}
90
107
  isValid={isValid}
91
108
  placeholder={placeholder}
@@ -6,7 +6,7 @@ import {
6
6
  FunctionComponent,
7
7
  KeyboardEventHandler,
8
8
  memo,
9
- RefObject,
9
+ Ref,
10
10
  } from 'react';
11
11
 
12
12
  import { ExclamationSquareFill } from 'icons';
@@ -22,7 +22,7 @@ interface Props {
22
22
  className?: string;
23
23
  disabled?: boolean;
24
24
  highlighted?: boolean;
25
- inputRef: RefObject<HTMLInputElement>;
25
+ inputRef: Ref<HTMLInputElement>;
26
26
  isBlank?: boolean;
27
27
  isValid?: boolean;
28
28
  placeholder?: string;
@@ -66,7 +66,6 @@ const TilePure: FunctionComponent<Props> = ({
66
66
  [styles.disabled]: disabled,
67
67
  [styles.empty]: !character,
68
68
  [styles.highlighted]: highlighted,
69
- [styles.invalid]: !isValid,
70
69
  [styles.raised]: raised,
71
70
  [styles.points1]: points === 1,
72
71
  [styles.points2]: points === 2,
@@ -93,8 +92,14 @@ const TilePure: FunctionComponent<Props> = ({
93
92
  onKeyDown={onKeyDown}
94
93
  />
95
94
 
95
+ {placeholder && (
96
+ <div className={styles.placeholder} style={characterStyle} tabIndex={-1}>
97
+ {placeholder}
98
+ </div>
99
+ )}
100
+
96
101
  <div className={styles.character} style={characterStyle} tabIndex={-1}>
97
- {character || placeholder}
102
+ {character}
98
103
  </div>
99
104
 
100
105
  {canShowPoints && (
@@ -10,9 +10,9 @@ $arrow-size: 4px;
10
10
  padding: var(--spacing--s) var(--spacing--m);
11
11
  box-shadow: var(--box-shadow);
12
12
  border-radius: var(--border--radius);
13
- background-color: var(--tooltip--background);
14
- color: var(--tooltip--foreground);
15
- z-index: 200;
13
+ background-color: var(--color--tooltip--background);
14
+ color: var(--color--tooltip--foreground);
15
+ z-index: var(--z-index--tooltip);
16
16
 
17
17
  &.top {
18
18
  .arrow {
@@ -21,7 +21,7 @@ $arrow-size: 4px;
21
21
  &::after {
22
22
  left: 0;
23
23
  bottom: 0;
24
- border-top-color: var(--tooltip--background);
24
+ border-top-color: var(--color--tooltip--background);
25
25
  border-bottom: none;
26
26
  }
27
27
  }
@@ -34,7 +34,7 @@ $arrow-size: 4px;
34
34
  &::after {
35
35
  left: 0;
36
36
  top: 0;
37
- border-right-color: var(--tooltip--background);
37
+ border-right-color: var(--color--tooltip--background);
38
38
  border-left: none;
39
39
  }
40
40
  }
@@ -47,7 +47,7 @@ $arrow-size: 4px;
47
47
  &::after {
48
48
  top: 0;
49
49
  left: 0;
50
- border-bottom-color: var(--tooltip--background);
50
+ border-bottom-color: var(--color--tooltip--background);
51
51
  border-top: none;
52
52
  }
53
53
  }
@@ -60,7 +60,7 @@ $arrow-size: 4px;
60
60
  &::after {
61
61
  right: 0;
62
62
  top: 0;
63
- border-left-color: var(--tooltip--background);
63
+ border-left-color: var(--color--tooltip--background);
64
64
  border-right: none;
65
65
  }
66
66
  }
@@ -19,6 +19,7 @@ export { default as Rack } from './Rack';
19
19
  export { default as Radio } from './Radio';
20
20
  export { default as Results } from './Results';
21
21
  export { default as ResultsInput } from './ResultsInput';
22
+ export { default as SeoMessage } from './SeoMessage';
22
23
  export { default as Sizer } from './Sizer';
23
24
  export { default as Solver } from './Solver';
24
25
  export { default as SplashScreen } from './SplashScreen';
@@ -1,4 +1,4 @@
1
- import { useEffect } from 'react';
1
+ import { useEffect, useState } from 'react';
2
2
  import { useDispatch } from 'react-redux';
3
3
  import { useEffectOnce } from 'react-use';
4
4
 
@@ -7,6 +7,7 @@ import { boardSlice, localStorage, selectBoard, useTypedSelector } from 'state';
7
7
  const useLocalStorageBoard = (): void => {
8
8
  const dispatch = useDispatch();
9
9
  const board = useTypedSelector(selectBoard);
10
+ const [isLoaded, setIsLoaded] = useState(false);
10
11
 
11
12
  useEffectOnce(() => {
12
13
  const persistedBoard = localStorage.getBoard();
@@ -14,13 +15,15 @@ const useLocalStorageBoard = (): void => {
14
15
  if (persistedBoard) {
15
16
  dispatch(boardSlice.actions.init(persistedBoard));
16
17
  }
18
+
19
+ setIsLoaded(true);
17
20
  });
18
21
 
19
22
  useEffect(() => {
20
- if (board) {
23
+ if (board && isLoaded) {
21
24
  localStorage.setBoard(board);
22
25
  }
23
- }, [board]);
26
+ }, [board, isLoaded]);
24
27
  };
25
28
 
26
29
  export default useLocalStorageBoard;
@@ -1,4 +1,4 @@
1
- import { useEffect } from 'react';
1
+ import { useEffect, useState } from 'react';
2
2
  import { useDispatch } from 'react-redux';
3
3
  import { useEffectOnce } from 'react-use';
4
4
 
@@ -7,6 +7,7 @@ import { localStorage, selectConfigId, settingsSlice, useTypedSelector } from 's
7
7
  const useLocalStorageConfigId = (): void => {
8
8
  const dispatch = useDispatch();
9
9
  const configId = useTypedSelector(selectConfigId);
10
+ const [isLoaded, setIsLoaded] = useState(false);
10
11
 
11
12
  useEffectOnce(() => {
12
13
  const persistedConfigId = localStorage.getConfigId();
@@ -14,13 +15,15 @@ const useLocalStorageConfigId = (): void => {
14
15
  if (persistedConfigId) {
15
16
  dispatch(settingsSlice.actions.init({ configId: persistedConfigId }));
16
17
  }
18
+
19
+ setIsLoaded(true);
17
20
  });
18
21
 
19
22
  useEffect(() => {
20
- if (configId) {
23
+ if (configId && isLoaded) {
21
24
  localStorage.setConfigId(configId);
22
25
  }
23
- }, [configId]);
26
+ }, [configId, isLoaded]);
24
27
  };
25
28
 
26
29
  export default useLocalStorageConfigId;
@@ -1,4 +1,4 @@
1
- import { useEffect } from 'react';
1
+ import { useEffect, useState } from 'react';
2
2
  import { useDispatch } from 'react-redux';
3
3
  import { useEffectOnce } from 'react-use';
4
4
 
@@ -8,6 +8,7 @@ import { localStorage, selectLocale, settingsSlice, useTypedSelector } from 'sta
8
8
  const useLocalStorageLocale = (): void => {
9
9
  const dispatch = useDispatch();
10
10
  const locale = useTypedSelector(selectLocale);
11
+ const [isLoaded, setIsLoaded] = useState(false);
11
12
 
12
13
  useEffectOnce(() => {
13
14
  const persistedLocale = localStorage.getLocale();
@@ -17,13 +18,15 @@ const useLocalStorageLocale = (): void => {
17
18
  } else {
18
19
  dispatch(settingsSlice.actions.init({ locale: detectLocale() }));
19
20
  }
21
+
22
+ setIsLoaded(true);
20
23
  });
21
24
 
22
25
  useEffect(() => {
23
- if (locale) {
26
+ if (locale && isLoaded) {
24
27
  localStorage.setLocale(locale);
25
28
  }
26
- }, [locale]);
29
+ }, [locale, isLoaded]);
27
30
  };
28
31
 
29
32
  export default useLocalStorageLocale;