@scrabble-solver/scrabble-solver 2.10.7 → 2.10.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 (95) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +7 -13
  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/176.js +4273 -214
  16. package/.next/server/middleware-build-manifest.js +1 -1
  17. package/.next/server/pages/404.html +2 -2
  18. package/.next/server/pages/404.js.nft.json +1 -1
  19. package/.next/server/pages/500.html +2 -2
  20. package/.next/server/pages/_app.js +296 -13
  21. package/.next/server/pages/_app.js.nft.json +1 -1
  22. package/.next/server/pages/_document.js.nft.json +1 -1
  23. package/.next/server/pages/api/solve.js +17 -0
  24. package/.next/server/pages/index.html +1 -9
  25. package/.next/server/pages/index.js +23 -16
  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 +1 -1
  29. package/.next/static/Cs23uxWG6AxS72F2yrjHu/_buildManifest.js +1 -0
  30. package/.next/static/chunks/pages/{404-67383848027ec49b.js → 404-8cab6d62fe4ead73.js} +1 -1
  31. package/.next/static/chunks/pages/_app-dcbbb823dc93a031.js +28 -0
  32. package/.next/static/chunks/pages/index-df1ff01aa82d2d4d.js +1 -0
  33. package/.next/static/css/bf2e969b88c4e3dd.css +2 -0
  34. package/.next/static/css/d1cc6b79b211b7b8.css +1 -0
  35. package/.next/trace +55 -55
  36. package/package.json +10 -9
  37. package/src/components/Badge/Badge.module.scss +1 -1
  38. package/src/components/Board/components/Cell/Cell.module.scss +32 -64
  39. package/src/components/Board/components/Cell/CellPure.tsx +15 -22
  40. package/src/components/Button/Button.module.scss +2 -2
  41. package/src/components/Checkbox/Checkbox.tsx +1 -4
  42. package/src/components/Dictionary/Dictionary.tsx +28 -30
  43. package/src/components/DictionaryInput/DictionaryInput.tsx +3 -0
  44. package/src/components/Key/Key.module.scss +1 -1
  45. package/src/components/LogoSplashScreen/LogoSplashScreen.module.scss +1 -1
  46. package/src/components/Modal/Modal.module.scss +4 -2
  47. package/src/components/Rack/Rack.module.scss +4 -0
  48. package/src/components/Radio/Radio.tsx +1 -4
  49. package/src/components/Results/Results.module.scss +2 -2
  50. package/src/components/SeoMessage/SeoMessage.tsx +19 -0
  51. package/src/components/SeoMessage/index.ts +1 -0
  52. package/src/components/Solver/components/EmptyState/EmptyState.module.scss +1 -1
  53. package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.module.scss +9 -2
  54. package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.tsx +2 -1
  55. package/src/components/Tile/Tile.module.scss +49 -11
  56. package/src/components/Tile/Tile.tsx +23 -8
  57. package/src/components/Tile/TilePure.tsx +27 -20
  58. package/src/components/Tooltip/Tooltip.module.scss +7 -7
  59. package/src/components/index.ts +1 -0
  60. package/src/i18n/de.json +1 -0
  61. package/src/i18n/en.json +1 -0
  62. package/src/i18n/es.json +1 -0
  63. package/src/i18n/fa.json +1 -0
  64. package/src/i18n/fr.json +1 -0
  65. package/src/i18n/pl.json +1 -0
  66. package/src/icons/Flag.svg +2 -2
  67. package/src/icons/FlagFill.svg +4 -0
  68. package/src/icons/Square.svg +4 -0
  69. package/src/icons/SquareFill.svg +4 -0
  70. package/src/icons/index.ts +3 -0
  71. package/src/modals/RemainingTilesModal/components/Character/Character.module.scss +1 -1
  72. package/src/modals/ResultsModal/ResultsModal.module.scss +1 -1
  73. package/src/modals/SettingsModal/components/AutoGroupTilesSetting/AutoGroupTilesSetting.tsx +1 -2
  74. package/src/modals/SettingsModal/components/ConfigSetting/ConfigSetting.tsx +1 -2
  75. package/src/modals/SettingsModal/components/LocaleSetting/LocaleSetting.module.scss +14 -24
  76. package/src/modals/SettingsModal/components/LocaleSetting/LocaleSetting.tsx +1 -2
  77. package/src/pages/_app.tsx +9 -5
  78. package/src/pages/index.module.scss +1 -2
  79. package/src/pages/index.tsx +10 -8
  80. package/src/parameters/index.ts +10 -0
  81. package/src/state/slices/boardSlice.ts +5 -5
  82. package/src/styles/mixins.scss +4 -2
  83. package/src/styles/variables.scss +39 -32
  84. package/src/types/index.ts +1 -0
  85. package/.next/server/chunks/579.js +0 -3925
  86. package/.next/static/6RggBFm8kHrh-k1-CG3um/_buildManifest.js +0 -1
  87. package/.next/static/chunks/490-d29992f1c264d70e.js +0 -5
  88. package/.next/static/chunks/509-6ad4482d4351452c.js +0 -1
  89. package/.next/static/chunks/pages/_app-c58cfa832b76cc87.js +0 -24
  90. package/.next/static/chunks/pages/index-146039f501e49c08.js +0 -1
  91. package/.next/static/css/4482c4a0064d3807.css +0 -1
  92. package/.next/static/css/78e42ad01f580f64.css +0 -1
  93. package/.next/static/css/9d1013ec684361b9.css +0 -1
  94. package/src/components/Board/components/Cell/Button.tsx +0 -32
  95. /package/.next/static/{6RggBFm8kHrh-k1-CG3um → Cs23uxWG6AxS72F2yrjHu}/_ssgManifest.js +0 -0
@@ -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;
@@ -58,8 +54,19 @@
58
54
  }
59
55
  }
60
56
 
57
+ .inputContainer {
58
+ position: absolute;
59
+ top: 0;
60
+ right: 0;
61
+ bottom: 0;
62
+ left: 0;
63
+ display: flex;
64
+ overflow: hidden;
65
+ }
66
+
61
67
  .input,
62
- .character {
68
+ .character,
69
+ .placeholder {
63
70
  padding: 0;
64
71
  font-weight: bold;
65
72
  text-transform: uppercase;
@@ -75,9 +82,26 @@
75
82
  color: transparent;
76
83
  border: none;
77
84
  font-size: 16px; // prevent iOS from automatically zooming in on focus
85
+
86
+ // Hack for this Lighthouse warning:
87
+ // > Interactive elements like buttons and links should be large enough (48x48px), and have
88
+ // > enough space around them, to be easy enough to tap without overlapping onto other elements.
89
+ //
90
+ // .inputContainer exists only for the purpose of this hack
91
+ $min-tile-width: 20px;
92
+ $min-lighthouse-size: 48px;
93
+
94
+ position: absolute;
95
+ top: calc((100% - $min-lighthouse-size) / 2);
96
+ right: calc((100% - $min-lighthouse-size) / 2);
97
+ bottom: calc((100% - $min-lighthouse-size) / 2);
98
+ left: calc((100% - $min-lighthouse-size) / 2);
99
+ width: $min-lighthouse-size;
100
+ height: $min-lighthouse-size;
78
101
  }
79
102
 
80
- .character {
103
+ .character,
104
+ .placeholder {
81
105
  position: absolute;
82
106
  top: 0;
83
107
  right: 0;
@@ -97,6 +121,18 @@
97
121
 
98
122
  .raised & {
99
123
  box-shadow: inset -2px -2px 2px -1px rgba(34, 34, 34, 0.8);
124
+
125
+ @include media('<xs') {
126
+ box-shadow: inset -2px -2px 1px -1px rgba(34, 34, 34, 0.8);
127
+ }
128
+ }
129
+ }
130
+
131
+ .character {
132
+ opacity: 1;
133
+
134
+ .empty & {
135
+ opacity: 0;
100
136
  }
101
137
  }
102
138
 
@@ -130,6 +166,8 @@
130
166
  position: absolute;
131
167
  width: $size;
132
168
  height: $size;
169
+ background-color: var(--color--error--opposite);
170
+ color: var(--color--error);
133
171
 
134
172
  [dir='ltr'] & {
135
173
  top: 0;
@@ -1,16 +1,18 @@
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
 
13
14
  import { getTileSizes, noop } from 'lib';
15
+ import { EASE_OUT_CUBIC, TILE_APPEAR_DURATION, TILE_APPEAR_KEYFRAMES } from 'parameters';
14
16
  import { selectLocale, useTypedSelector } from 'state';
15
17
 
16
18
  import TilePure from './TilePure';
@@ -42,7 +44,7 @@ const Tile: FunctionComponent<Props> = ({
42
44
  character = '',
43
45
  disabled,
44
46
  highlighted,
45
- inputRef: ref,
47
+ inputRef,
46
48
  isBlank,
47
49
  isValid,
48
50
  placeholder,
@@ -59,21 +61,34 @@ const Tile: FunctionComponent<Props> = ({
59
61
  const style = useMemo(() => ({ height: tileSize, width: tileSize }), [tileSize]);
60
62
  const characterStyle = useMemo(() => ({ fontSize: tileFontSize }), [tileFontSize]);
61
63
  const pointsStyle = useMemo(() => ({ fontSize: pointsFontSize }), [pointsFontSize]);
62
- const inputRef = useMemo<RefObject<HTMLInputElement>>(() => ref || createRef(), [ref]);
64
+ const ref = useRef<HTMLInputElement>(null);
65
+ const mergedRef = inputRef ? mergeRefs(ref, inputRef) : ref;
63
66
  const isEmpty = !character || character === EMPTY_CELL;
64
67
  const canShowPoints = (isBlank || !isEmpty) && typeof points !== 'undefined';
65
68
  const pointsFormatted = typeof points === 'number' ? points.toLocaleString(locale) : '';
66
69
 
67
70
  const handleKeyDown: KeyboardEventHandler<HTMLInputElement> = (event) => {
68
- inputRef.current?.select();
71
+ ref.current?.select();
69
72
  onKeyDown(event);
70
73
  };
71
74
 
72
75
  useEffect(() => {
73
- if (autoFocus && inputRef.current) {
74
- inputRef.current.focus();
76
+ if (autoFocus && ref.current) {
77
+ ref.current.focus();
75
78
  }
76
- }, [autoFocus, inputRef]);
79
+ }, [autoFocus, ref]);
80
+
81
+ useEffect(() => {
82
+ if (!ref.current?.parentElement || !character) {
83
+ return;
84
+ }
85
+
86
+ ref.current.parentElement.animate(TILE_APPEAR_KEYFRAMES, {
87
+ duration: TILE_APPEAR_DURATION,
88
+ easing: EASE_OUT_CUBIC,
89
+ fill: 'forwards',
90
+ });
91
+ }, [character]);
77
92
 
78
93
  return (
79
94
  <TilePure
@@ -85,7 +100,7 @@ const Tile: FunctionComponent<Props> = ({
85
100
  className={className}
86
101
  disabled={disabled}
87
102
  highlighted={highlighted}
88
- inputRef={inputRef}
103
+ inputRef={mergedRef}
89
104
  isBlank={isBlank}
90
105
  isValid={isValid}
91
106
  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,
@@ -76,25 +75,33 @@ const TilePure: FunctionComponent<Props> = ({
76
75
  })}
77
76
  style={style}
78
77
  >
79
- <input
80
- aria-label={ariaLabel}
81
- autoCapitalize="none"
82
- autoComplete="off"
83
- autoCorrect="off"
84
- autoFocus={autoFocus}
85
- className={styles.input}
86
- disabled={disabled}
87
- ref={inputRef}
88
- spellCheck={false}
89
- tabIndex={tabIndex}
90
- value={character || ''}
91
- onChange={onChange}
92
- onFocus={onFocus}
93
- onKeyDown={onKeyDown}
94
- />
78
+ <div className={styles.inputContainer}>
79
+ <input
80
+ aria-label={ariaLabel}
81
+ autoCapitalize="none"
82
+ autoComplete="off"
83
+ autoCorrect="off"
84
+ autoFocus={autoFocus}
85
+ className={styles.input}
86
+ disabled={disabled}
87
+ ref={inputRef}
88
+ spellCheck={false}
89
+ tabIndex={tabIndex}
90
+ value={character || ''}
91
+ onChange={onChange}
92
+ onFocus={onFocus}
93
+ onKeyDown={onKeyDown}
94
+ />
95
+ </div>
96
+
97
+ {placeholder && (
98
+ <div className={styles.placeholder} style={characterStyle} tabIndex={-1}>
99
+ {placeholder}
100
+ </div>
101
+ )}
95
102
 
96
103
  <div className={styles.character} style={characterStyle} tabIndex={-1}>
97
- {character || placeholder}
104
+ {character}
98
105
  </div>
99
106
 
100
107
  {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';
package/src/i18n/de.json CHANGED
@@ -22,6 +22,7 @@
22
22
  "dictionary.empty-state.not-allowed": "Dieses Wort ist nicht erlaubt.",
23
23
  "dictionary.empty-state.uninitialized": "Die Wörterbuchdefinition des letzten markierten Wortes wird hier angezeigt.",
24
24
  "dictionary.input.placeholder": "Durchsuche Wörterbuch...",
25
+ "dictionary.input.title": "Durch Kommas getrennte Wörter",
25
26
  "empty-state.error": "Fehler",
26
27
  "empty-state.info": "Info",
27
28
  "empty-state.success": "Juhuu!",
package/src/i18n/en.json CHANGED
@@ -22,6 +22,7 @@
22
22
  "dictionary.empty-state.not-allowed": "This word is not allowed.",
23
23
  "dictionary.empty-state.uninitialized": "Dictionary definition of the most recently highlighted word will be shown here.",
24
24
  "dictionary.input.placeholder": "Search dictionary...",
25
+ "dictionary.input.title": "Comma-separated words",
25
26
  "empty-state.error": "Error",
26
27
  "empty-state.info": "Info",
27
28
  "empty-state.success": "Yeah!",
package/src/i18n/es.json CHANGED
@@ -22,6 +22,7 @@
22
22
  "dictionary.empty-state.not-allowed": "Esta palabra no es aceptable.",
23
23
  "dictionary.empty-state.uninitialized": "Aquí se mostrará la definición del diccionario de la última palabra resaltada.",
24
24
  "dictionary.input.placeholder": "Busca el diccionario...",
25
+ "dictionary.input.title": "Palabras separadas por comas",
25
26
  "empty-state.error": "Error",
26
27
  "empty-state.info": "Info",
27
28
  "empty-state.success": "Sí!",
package/src/i18n/fa.json CHANGED
@@ -22,6 +22,7 @@
22
22
  "dictionary.empty-state.not-allowed": "این کلمه مجاز نیست.",
23
23
  "dictionary.empty-state.uninitialized": "معنی لغت اینجا نمایش داده خواهد شد.",
24
24
  "dictionary.input.placeholder": "جستجو در فرهنگ لغت ...",
25
+ "dictionary.input.title": "کلمات جدا شده با کاما",
25
26
  "empty-state.error": "خطا",
26
27
  "empty-state.info": "اطلاعات",
27
28
  "empty-state.success": "حله!",
package/src/i18n/fr.json CHANGED
@@ -22,6 +22,7 @@
22
22
  "dictionary.empty-state.not-allowed": "Ce mot n'est pas pas acceptable.",
23
23
  "dictionary.empty-state.uninitialized": "La définition dictionaire du dernier mot surligné sera affichée ici.",
24
24
  "dictionary.input.placeholder": "Rechercher dans le dictionnaire...",
25
+ "dictionary.input.title": "Mots séparées par des virgules",
25
26
  "empty-state.error": "Erreur",
26
27
  "empty-state.info": "Info",
27
28
  "empty-state.success": "Ouais!",
package/src/i18n/pl.json CHANGED
@@ -22,6 +22,7 @@
22
22
  "dictionary.empty-state.not-allowed": "To słowo nie jest dopuszczalne w grach.",
23
23
  "dictionary.empty-state.uninitialized": "Tu zostanie wyświetlona słownikowa definicja ostatnio podświetlonego słowa.",
24
24
  "dictionary.input.placeholder": "Szukaj w słowniku...",
25
+ "dictionary.input.title": "Słowa rozdzielone przecinkiem",
25
26
  "empty-state.error": "Błąd",
26
27
  "empty-state.info": "Info",
27
28
  "empty-state.success": "Hurra!",
@@ -1,4 +1,4 @@
1
- <!-- https://icons.getbootstrap.com/icons/flag-fill/ -->
1
+ <!-- https://icons.getbootstrap.com/icons/flag/ -->
2
2
  <svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
3
- <path d="M14.778.085A.5.5 0 0 1 15 .5V8a.5.5 0 0 1-.314.464L14.5 8l.186.464-.003.001-.006.003-.023.009a12.435 12.435 0 0 1-.397.15c-.264.095-.631.223-1.047.35-.816.252-1.879.523-2.71.523-.847 0-1.548-.28-2.158-.525l-.028-.01C7.68 8.71 7.14 8.5 6.5 8.5c-.7 0-1.638.23-2.437.477A19.626 19.626 0 0 0 3 9.342V15.5a.5.5 0 0 1-1 0V.5a.5.5 0 0 1 1 0v.282c.226-.079.496-.17.79-.26C4.606.272 5.67 0 6.5 0c.84 0 1.524.277 2.121.519l.043.018C9.286.788 9.828 1 10.5 1c.7 0 1.638-.23 2.437-.477a19.587 19.587 0 0 0 1.349-.476l.019-.007.004-.002h.001" fill="currentColor" />
3
+ <path d="M14.778.085A.5.5 0 0 1 15 .5V8a.5.5 0 0 1-.314.464L14.5 8l.186.464-.003.001-.006.003-.023.009a12.435 12.435 0 0 1-.397.15c-.264.095-.631.223-1.047.35-.816.252-1.879.523-2.71.523-.847 0-1.548-.28-2.158-.525l-.028-.01C7.68 8.71 7.14 8.5 6.5 8.5c-.7 0-1.638.23-2.437.477A19.626 19.626 0 0 0 3 9.342V15.5a.5.5 0 0 1-1 0V.5a.5.5 0 0 1 1 0v.282c.226-.079.496-.17.79-.26C4.606.272 5.67 0 6.5 0c.84 0 1.524.277 2.121.519l.043.018C9.286.788 9.828 1 10.5 1c.7 0 1.638-.23 2.437-.477a19.587 19.587 0 0 0 1.349-.476l.019-.007.004-.002h.001M14 1.221c-.22.078-.48.167-.766.255-.81.252-1.872.523-2.734.523-.886 0-1.592-.286-2.203-.534l-.008-.003C7.662 1.21 7.139 1 6.5 1c-.669 0-1.606.229-2.415.478A21.294 21.294 0 0 0 3 1.845v6.433c.22-.078.48-.167.766-.255C4.576 7.77 5.638 7.5 6.5 7.5c.847 0 1.548.28 2.158.525l.028.01C9.32 8.29 9.86 8.5 10.5 8.5c.668 0 1.606-.229 2.415-.478A21.317 21.317 0 0 0 14 7.655V1.222z" fill="currentColor" />
4
4
  </svg>
@@ -0,0 +1,4 @@
1
+ <!-- https://icons.getbootstrap.com/icons/flag-fill/ -->
2
+ <svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
3
+ <path d="M14.778.085A.5.5 0 0 1 15 .5V8a.5.5 0 0 1-.314.464L14.5 8l.186.464-.003.001-.006.003-.023.009a12.435 12.435 0 0 1-.397.15c-.264.095-.631.223-1.047.35-.816.252-1.879.523-2.71.523-.847 0-1.548-.28-2.158-.525l-.028-.01C7.68 8.71 7.14 8.5 6.5 8.5c-.7 0-1.638.23-2.437.477A19.626 19.626 0 0 0 3 9.342V15.5a.5.5 0 0 1-1 0V.5a.5.5 0 0 1 1 0v.282c.226-.079.496-.17.79-.26C4.606.272 5.67 0 6.5 0c.84 0 1.524.277 2.121.519l.043.018C9.286.788 9.828 1 10.5 1c.7 0 1.638-.23 2.437-.477a19.587 19.587 0 0 0 1.349-.476l.019-.007.004-.002h.001" fill="currentColor" />
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <!-- https://icons.getbootstrap.com/icons/square/ -->
2
+ <svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
3
+ <path d="M14 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h12zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2z" fill="currentColor" />
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <!-- https://icons.getbootstrap.com/icons/square-fill/ -->
2
+ <svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
3
+ <path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2z" fill="currentColor" />
4
+ </svg>
@@ -19,6 +19,7 @@ export { default as Eraser } from './Eraser.svg';
19
19
  export { default as ExclamationSquareFill } from './ExclamationSquareFill.svg';
20
20
  export { default as ExclamationTriangleFill } from './ExclamationTriangleFill.svg';
21
21
  export { default as Flag } from './Flag.svg';
22
+ export { default as FlagFill } from './FlagFill.svg';
22
23
  export { default as FlagEs } from './FlagEs.svg';
23
24
  export { default as FlagFa } from './FlagFa.svg';
24
25
  export { default as FlagFr } from './FlagFr.svg';
@@ -34,4 +35,6 @@ export { default as Sack } from './Sack.svg';
34
35
  export { default as Search } from './Search.svg';
35
36
  export { default as SortDown } from './SortDown.svg';
36
37
  export { default as SortUp } from './SortUp.svg';
38
+ export { default as Square } from './Square.svg';
39
+ export { default as SquareFill } from './SquareFill.svg';
37
40
  export { default as Star } from './Star.svg';
@@ -5,7 +5,7 @@
5
5
  transition: var(--transition);
6
6
 
7
7
  &.finished {
8
- opacity: 0.2;
8
+ opacity: var(--opacity--disabled);
9
9
  }
10
10
 
11
11
  &.overused {
@@ -3,5 +3,5 @@
3
3
  display: flex;
4
4
  flex-direction: column;
5
5
  height: 100%;
6
- background-color: white;
6
+ background-color: var(--color--white);
7
7
  }
@@ -40,12 +40,11 @@ const AutoGroupTilesSetting: FunctionComponent<Props> = ({ className, disabled }
40
40
 
41
41
  return (
42
42
  <div className={className}>
43
- {options.map((option, index) => (
43
+ {options.map((option) => (
44
44
  <Radio
45
45
  checked={value === parseValue(option.value)}
46
46
  className={styles.option}
47
47
  disabled={disabled}
48
- id={`autoGroupTiles-${index}`}
49
48
  key={option.value}
50
49
  name="autoGroupTiles"
51
50
  value={option.value}
@@ -22,12 +22,11 @@ const ConfigSetting: FunctionComponent<Props> = ({ className, disabled }) => {
22
22
 
23
23
  return (
24
24
  <div className={className}>
25
- {options.map((option, index) => (
25
+ {options.map((option) => (
26
26
  <Radio
27
27
  checked={configId === option.value}
28
28
  className={styles.option}
29
29
  disabled={disabled}
30
- id={`configId-${index}`}
31
30
  key={option.value}
32
31
  name="configId"
33
32
  value={option.value}
@@ -1,17 +1,8 @@
1
1
  @import 'styles/mixins';
2
2
 
3
3
  .option {
4
- margin-bottom: var(--spacing--m);
5
-
6
- &:last-child {
7
- margin-bottom: 0;
8
- }
9
-
10
- &:hover,
11
- &.checked {
12
- .flag {
13
- box-shadow: var(--box-shadow);
14
- }
4
+ & + & {
5
+ margin-top: var(--spacing--m);
15
6
  }
16
7
  }
17
8
 
@@ -19,10 +10,10 @@
19
10
  display: flex;
20
11
  align-items: center;
21
12
  gap: var(--spacing--l);
22
- font-family: 'Open Sans', sans-serif;
13
+ font-family: var(--font--family), sans-serif;
23
14
 
24
15
  &.fa {
25
- font-family: 'Vazirmatn', 'Open Sans', sans-serif;
16
+ font-family: var(--font--family--arabic), var(--font--family), sans-serif;
26
17
  }
27
18
  }
28
19
 
@@ -30,17 +21,16 @@
30
21
  $height: 32px;
31
22
 
32
23
  height: $height;
33
- box-shadow: var(--box-shadow--null);
34
24
  transition: var(--transition);
35
25
 
36
- &.es {
37
- $aspect-ratio: 1.5;
26
+ &.de {
27
+ $aspect-ratio: 1.6;
38
28
 
39
29
  width: $height * $aspect-ratio;
40
30
  }
41
31
 
42
- &.fr {
43
- $aspect-ratio: 1.6;
32
+ &.es {
33
+ $aspect-ratio: 1.5;
44
34
 
45
35
  width: $height * $aspect-ratio;
46
36
  }
@@ -51,6 +41,12 @@
51
41
  width: $height * $aspect-ratio;
52
42
  }
53
43
 
44
+ &.fr {
45
+ $aspect-ratio: 1.6;
46
+
47
+ width: $height * $aspect-ratio;
48
+ }
49
+
54
50
  &.gb {
55
51
  $aspect-ratio: 2;
56
52
 
@@ -68,10 +64,4 @@
68
64
 
69
65
  width: $height * $aspect-ratio;
70
66
  }
71
-
72
- &.de {
73
- $aspect-ratio: 1.6;
74
-
75
- width: $height * $aspect-ratio;
76
- }
77
67
  }
@@ -25,14 +25,13 @@ const LocaleSetting: FunctionComponent<Props> = ({ className, disabled }) => {
25
25
 
26
26
  return (
27
27
  <div className={className}>
28
- {options.map(({ Icon, ...option }, index) => (
28
+ {options.map(({ Icon, ...option }) => (
29
29
  <Radio
30
30
  checked={locale === option.value}
31
31
  className={classNames(styles.option, className, {
32
32
  [styles.checked]: locale === option.value,
33
33
  })}
34
34
  disabled={disabled}
35
- id={`locale-${index}`}
36
35
  key={option.value}
37
36
  name="locale"
38
37
  value={option.value}
@@ -3,6 +3,7 @@ import Head from 'next/head';
3
3
  import { FunctionComponent } from 'react';
4
4
  import { Provider } from 'react-redux';
5
5
 
6
+ import { SeoMessage } from 'components';
6
7
  import { createAppStore } from 'state';
7
8
 
8
9
  import 'styles/global.scss';
@@ -10,10 +11,16 @@ import 'styles/global.scss';
10
11
  const DESCRIPTION =
11
12
  // eslint-disable-next-line max-len
12
13
  'Scrabble Solver 2 - Free and open-source analysis tool for Scrabble and Literaki. Quickly find top scoring words using given letters and board state. Available in English, French, German, Polish & Spanish.';
14
+
13
15
  const KEYWORDS = [
16
+ 'Scrabble Solver',
14
17
  'Scrabble',
15
18
  'Solver',
19
+ 'Board',
16
20
  'Open-source',
21
+ 'Open',
22
+ 'Source',
23
+ 'Word',
17
24
  'Finder',
18
25
  'Cheating',
19
26
  'Literaki',
@@ -22,6 +29,7 @@ const KEYWORDS = [
22
29
  'Français',
23
30
  'Deutsch',
24
31
  'Polski',
32
+ 'فارسی',
25
33
  'Español',
26
34
  'SOWPODS',
27
35
  'TWL06',
@@ -55,11 +63,7 @@ const App: FunctionComponent<AppProps> = ({ Component, pageProps }) => (
55
63
  </Head>
56
64
 
57
65
  <Provider store={store}>
58
- <p style={{ fontSize: 0 }}>
59
- Scrabble Solver 2 is a free and open-source analysis tool for Scrabble and Literaki. Quickly find top scoring
60
- words using given letters and board state. Available in English, French, German, Polish & Spanish. Source code
61
- is available on GitHub - contributions are welcome!
62
- </p>
66
+ <SeoMessage />
63
67
  <Component {...pageProps} />
64
68
  </Provider>
65
69
  </>
@@ -6,8 +6,7 @@
6
6
  height: 100%;
7
7
  opacity: 0;
8
8
  overflow: auto;
9
- transition: var(--transition);
10
- transition-duration: var(--transition--duration--long);
9
+ transition: var(--transition--long);
11
10
 
12
11
  &.initialized {
13
12
  opacity: 1;
@@ -76,6 +76,10 @@ const Index: FunctionComponent<Props> = ({ version }) => {
76
76
  }
77
77
  }, []);
78
78
 
79
+ if (!isClient) {
80
+ return null;
81
+ }
82
+
79
83
  return (
80
84
  <>
81
85
  <SvgFontFix />
@@ -100,14 +104,12 @@ const Index: FunctionComponent<Props> = ({ version }) => {
100
104
  </div>
101
105
  </div>
102
106
 
103
- {isClient && (
104
- <Solver
105
- className={styles.solver}
106
- height={solverHeight}
107
- width={solverWidth}
108
- onShowResults={() => setShowResults(true)}
109
- />
110
- )}
107
+ <Solver
108
+ className={styles.solver}
109
+ height={solverHeight}
110
+ width={solverWidth}
111
+ onShowResults={() => setShowResults(true)}
112
+ />
111
113
  </div>
112
114
 
113
115
  <MenuModal